From d3244ff66221ee5a95ab0d3017ac9bef36d82085 Mon Sep 17 00:00:00 2001 From: TronoSfera <119615520+TronoSfera@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:13:59 +0300 Subject: [PATCH] fix user UI 7 --- Dockerfile | 10 +- app/api/admin/crud_modules/access.py | 5 +- app/api/admin/crud_modules/service.py | 17 + app/api/admin/invoices.py | 29 +- app/api/admin/requests_modules/kanban.py | 101 +- app/api/admin/requests_modules/status_flow.py | 103 +- app/api/public/requests.py | 71 +- app/assets/invoice_signature_stamp.png | Bin 0 -> 151987 bytes app/services/billing_flow.py | 27 +- app/services/chat_secure_service.py | 3 + app/services/invoice_chat.py | 181 + app/services/invoice_numbering.py | 46 + app/services/invoice_pdf.py | 531 +- app/web/admin.css | 136 + app/web/admin.html | 4 +- app/web/admin.js | 10015 ++++++++++++++++ app/web/admin.jsx | 387 +- .../features/dashboard/DashboardSection.jsx | 107 +- .../features/requests/RequestWorkspace.jsx | 305 +- app/web/admin/hooks/useRequestWorkspace.js | 160 +- app/web/admin/shared/state.js | 1 + app/web/admin/shared/utils.js | 5 +- app/web/client.html | 4 +- app/web/client.js | 959 +- app/web/client.jsx | 46 +- app/web/landing.html | 2 +- app/web/landing.js | 80 +- requirements.txt | 1 + tests/admin/test_assignment_users.py | 60 + tests/admin/test_status_flow_kanban.py | 138 +- tests/test_billing_flow.py | 84 + tests/test_dashboard_finance.py | 13 +- tests/test_invoices.py | 67 +- tests/test_migrations.py | 2 +- tests/test_notifications.py | 21 +- tests/test_public_cabinet.py | 42 +- tests/test_public_requests.py | 4 +- tests/test_sms_provider_health.py | 1 + 38 files changed, 13185 insertions(+), 583 deletions(-) create mode 100644 app/assets/invoice_signature_stamp.png create mode 100644 app/services/invoice_chat.py create mode 100644 app/services/invoice_numbering.py create mode 100644 app/web/admin.js diff --git a/Dockerfile b/Dockerfile index 5842a81..5d92ed3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,14 @@ FROM python:3.12-slim WORKDIR /app -RUN apt-get update && apt-get install -y build-essential curl openssl ca-certificates && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + openssl \ + ca-certificates \ + fontconfig \ + fonts-dejavu-core \ + fonts-liberation \ + && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . diff --git a/app/api/admin/crud_modules/access.py b/app/api/admin/crud_modules/access.py index 1003d25..a6d6683 100644 --- a/app/api/admin/crud_modules/access.py +++ b/app/api/admin/crud_modules/access.py @@ -56,7 +56,10 @@ TABLE_ROLE_ACTIONS: dict[str, dict[str, set[str]]] = { "audit_log": {"ADMIN": {"query", "read"}}, "security_audit_log": {"ADMIN": {"query", "read"}}, "otp_sessions": {"ADMIN": {"query", "read"}}, - "admin_users": {"ADMIN": set(CRUD_ACTIONS)}, + "admin_users": { + "ADMIN": set(CRUD_ACTIONS), + "LAWYER": {"read", "update"}, + }, "admin_user_topics": {"ADMIN": set(CRUD_ACTIONS)}, "landing_featured_staff": {"ADMIN": set(CRUD_ACTIONS)}, "topic_status_transitions": {"ADMIN": set(CRUD_ACTIONS)}, diff --git a/app/api/admin/crud_modules/service.py b/app/api/admin/crud_modules/service.py index a7b73da..09e4f26 100644 --- a/app/api/admin/crud_modules/service.py +++ b/app/api/admin/crud_modules/service.py @@ -82,6 +82,15 @@ from .payloads import ( ) +def _ensure_lawyer_owns_admin_user_row_or_403(admin: dict, row_id: str) -> None: + if not _is_lawyer(admin): + return + actor_id = _lawyer_actor_id_or_401(admin).strip().lower() + target_id = str(row_id or "").strip().lower() + if not actor_id or not target_id or actor_id != target_id: + raise HTTPException(status_code=403, detail="Недостаточно прав") + + def _apply_create_side_effects(db: Session, *, table_name: str, row: Any, admin: dict) -> None: if table_name == "messages" and isinstance(row, Message): req = db.get(Request, row.request_id) @@ -254,6 +263,8 @@ def query_table_service(table_name: str, uq: UniversalQuery, db: Session, admin: def get_row_service(table_name: str, row_id: str, db: Session, admin: dict) -> dict[str, Any]: normalized, model = _resolve_table_model(table_name) _require_table_action(admin, normalized, "read") + if normalized == "admin_users": + _ensure_lawyer_owns_admin_user_row_or_403(admin, row_id) row = _load_row_or_404(db, model, row_id) if normalized == "requests": req = row if isinstance(row, Request) else None @@ -408,6 +419,12 @@ def update_row_service(table_name: str, row_id: str, payload: dict[str, Any], db normalized, model = _resolve_table_model(table_name) _require_table_action(admin, normalized, "update") responsible = _resolve_responsible(admin) + if normalized == "admin_users" and _is_lawyer(admin): + _ensure_lawyer_owns_admin_user_row_or_403(admin, row_id) + allowed_fields = {"name", "email", "phone", "password", "avatar_url"} + forbidden_fields = sorted(set(payload.keys()) - allowed_fields) + if forbidden_fields: + raise HTTPException(status_code=403, detail="Недостаточно прав") if normalized == "requests" and _is_lawyer(admin) and isinstance(payload, dict): if "assigned_lawyer_id" in payload: raise HTTPException(status_code=403, detail='Назначение доступно только через действие "Взять в работу"') diff --git a/app/api/admin/invoices.py b/app/api/admin/invoices.py index 3344124..3a36e51 100644 --- a/app/api/admin/invoices.py +++ b/app/api/admin/invoices.py @@ -3,7 +3,7 @@ from __future__ import annotations import json from datetime import datetime, timezone from decimal import Decimal -from uuid import UUID, uuid4 +from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Request as FastapiRequest from fastapi.responses import StreamingResponse @@ -16,7 +16,9 @@ from app.models.admin_user import AdminUser from app.models.invoice import Invoice from app.models.request import Request from app.schemas.universal import UniversalQuery +from app.services.invoice_chat import create_invoice_chat_message_with_attachment from app.services.invoice_crypto import decrypt_requisites, encrypt_requisites +from app.services.invoice_numbering import generate_invoice_number from app.services.invoice_pdf import build_invoice_pdf_bytes from app.services.security_audit import extract_client_ip, record_pii_access_event from app.services.universal_query import apply_universal_query @@ -90,15 +92,6 @@ def _now_utc() -> datetime: return datetime.now(timezone.utc) -def _invoice_number(db: Session) -> str: - prefix = _now_utc().strftime("%Y%m%d") - candidate = f"INV-{prefix}-{uuid4().hex[:8].upper()}" - exists = db.query(Invoice.id).filter(Invoice.invoice_number == candidate).first() - if exists is None: - return candidate - return f"INV-{prefix}-{uuid4().hex[:12].upper()}" - - def _parse_requisites(raw) -> dict: if raw is None: return {} @@ -290,6 +283,8 @@ def create_invoice( role = str(admin.get("role") or "").upper() actor_id = _actor_uuid_or_401(admin) actor_email = str(admin.get("email") or "").strip() or "Администратор системы" + actor_user = db.get(AdminUser, actor_id) + actor_name = str(actor_user.name if actor_user else "").strip() or str(actor_user.email if actor_user else "").strip() or actor_email req = _request_from_payload_or_404(db, payload) _ensure_lawyer_owns_request_or_403(role, actor_id, req) @@ -302,10 +297,11 @@ def create_invoice( if not payer_display_name: raise HTTPException(status_code=400, detail='Поле "payer_display_name" обязательно') + issued_at = _now_utc() invoice = Invoice( request_id=req.id, client_id=req.client_id, - invoice_number=str(payload.get("invoice_number") or "").strip() or _invoice_number(db), + invoice_number=str(payload.get("invoice_number") or "").strip() or generate_invoice_number(db, issued_at), status=status, amount=_amount_or_400(payload.get("amount")), currency=_normalize_currency(payload.get("currency")), @@ -313,7 +309,7 @@ def create_invoice( payer_details_encrypted=encrypt_requisites(_parse_requisites(payload.get("payer_details"))), issued_by_admin_user_id=actor_id, issued_by_role=role, - issued_at=_now_utc(), + issued_at=issued_at, paid_at=None, responsible=actor_email, ) @@ -327,6 +323,15 @@ def create_invoice( db.add(invoice) db.add(req) + create_invoice_chat_message_with_attachment( + db, + request=req, + invoice=invoice, + actor_role=role, + actor_name=actor_name, + actor_admin_user_id=str(actor_id), + responsible=actor_email, + ) _commit_or_400(db, "Счет с таким номером уже существует") db.refresh(invoice) diff --git a/app/api/admin/requests_modules/kanban.py b/app/api/admin/requests_modules/kanban.py index 5e5a31c..e767708 100644 --- a/app/api/admin/requests_modules/kanban.py +++ b/app/api/admin/requests_modules/kanban.py @@ -10,6 +10,7 @@ from sqlalchemy import or_ from sqlalchemy.orm import Session from app.models.admin_user import AdminUser +from app.models.notification import Notification from app.models.request import Request from app.models.status import Status from app.models.status_group import StatusGroup @@ -20,8 +21,18 @@ from app.services.universal_query import apply_universal_query from .common import parse_datetime_safe -ALLOWED_KANBAN_FILTER_FIELDS = {"assigned_lawyer_id", "client_name", "status_code", "created_at", "topic_code", "overdue"} +ALLOWED_KANBAN_FILTER_FIELDS = { + "assigned_lawyer_id", + "client_name", + "status_code", + "created_at", + "topic_code", + "overdue", + "has_unread_updates", + "deadline_alert", +} ALLOWED_KANBAN_SORT_MODES = {"created_newest", "lawyer", "deadline"} +BOOLEAN_KANBAN_FILTER_FIELDS = {"overdue", "has_unread_updates", "deadline_alert"} FALLBACK_KANBAN_GROUPS = [ ("fallback_new", "Новые", 10), ("fallback_in_progress", "В работе", 20), @@ -86,7 +97,7 @@ def extract_case_deadline(extra_fields: object) -> datetime | None: return None -def coerce_kanban_bool(value: object) -> bool: +def coerce_kanban_bool(value: object, field_name: str) -> bool: if isinstance(value, bool): return value text = str(value or "").strip().lower() @@ -94,10 +105,10 @@ def coerce_kanban_bool(value: object) -> bool: return True if text in {"0", "false", "no", "n", "off"}: return False - raise HTTPException(status_code=400, detail='Поле "overdue" должно быть boolean') + raise HTTPException(status_code=400, detail=f'Поле "{field_name}" должно быть boolean') -def parse_kanban_filters_or_400(raw_filters: str | None) -> tuple[list[FilterClause], list[tuple[str, bool]]]: +def parse_kanban_filters_or_400(raw_filters: str | None) -> tuple[list[FilterClause], list[tuple[str, str, bool]]]: if not raw_filters: return [], [] try: @@ -108,7 +119,7 @@ def parse_kanban_filters_or_400(raw_filters: str | None) -> tuple[list[FilterCla raise HTTPException(status_code=400, detail="Фильтры канбана должны быть массивом") universal_filters: list[FilterClause] = [] - overdue_filters: list[tuple[str, bool]] = [] + boolean_filters: list[tuple[str, str, bool]] = [] for index, item in enumerate(parsed): if not isinstance(item, dict): raise HTTPException(status_code=400, detail=f"Фильтр #{index + 1} должен быть объектом") @@ -119,30 +130,40 @@ def parse_kanban_filters_or_400(raw_filters: str | None) -> tuple[list[FilterCla raise HTTPException(status_code=400, detail=f'Недоступное поле фильтра: "{field}"') if op not in {"=", "!=", ">", "<", ">=", "<=", "~"}: raise HTTPException(status_code=400, detail=f'Недопустимый оператор фильтра: "{op}"') - if field == "overdue": + if field in BOOLEAN_KANBAN_FILTER_FIELDS: if op not in {"=", "!="}: - raise HTTPException(status_code=400, detail='Для поля "overdue" доступны только операторы "=" и "!="') - overdue_filters.append((op, coerce_kanban_bool(value))) + raise HTTPException(status_code=400, detail=f'Для поля "{field}" доступны только операторы "=" и "!="') + boolean_filters.append((field, op, coerce_kanban_bool(value, field))) continue universal_filters.append(FilterClause(field=field, op=op, value=value)) - return universal_filters, overdue_filters + return universal_filters, boolean_filters -def apply_overdue_filters(items: list[dict[str, object]], overdue_filters: list[tuple[str, bool]]) -> list[dict[str, object]]: - if not overdue_filters: +def apply_boolean_kanban_filters( + items: list[dict[str, object]], + boolean_filters: list[tuple[str, str, bool]], +) -> list[dict[str, object]]: + if not boolean_filters: return items now = datetime.now(timezone.utc) out: list[dict[str, object]] = [] for item in items: - raw_deadline = item.get("sla_deadline_at") or item.get("case_deadline_at") - deadline_at = parse_datetime_safe(raw_deadline) - is_overdue = bool(deadline_at and deadline_at <= now) ok = True - for op, expected in overdue_filters: + for field, op, expected in boolean_filters: + if field == "overdue": + raw_deadline = item.get("sla_deadline_at") or item.get("case_deadline_at") + deadline_at = parse_datetime_safe(raw_deadline) + actual = bool(deadline_at and deadline_at <= now) + elif field == "has_unread_updates": + actual = bool(item.get("has_unread_updates")) + elif field == "deadline_alert": + actual = bool(item.get("deadline_alert")) + else: + actual = False if op == "=": - ok = ok and (is_overdue == expected) + ok = ok and (actual == expected) elif op == "!=": - ok = ok and (is_overdue != expected) + ok = ok and (actual != expected) if not ok: break if ok: @@ -204,7 +225,7 @@ def get_requests_kanban_service( ) normalized_sort_mode = sort_mode if sort_mode in ALLOWED_KANBAN_SORT_MODES else "created_newest" - query_filters, overdue_filters = parse_kanban_filters_or_400(filters) + query_filters, boolean_filters = parse_kanban_filters_or_400(filters) if query_filters: base_query = apply_universal_query( base_query, @@ -220,7 +241,33 @@ def get_requests_kanban_service( request_id_to_row = {str(row.id): row for row in request_rows} request_ids = [row.id for row in request_rows] + unread_notification_request_ids: set[str] = set() + actor_uuid = None + if actor: + try: + actor_uuid = UUID(actor) + except ValueError: + actor_uuid = None + if actor_uuid is not None and request_ids: + unread_notification_rows = ( + db.query(Notification.request_id) + .filter( + Notification.recipient_type == "ADMIN_USER", + Notification.recipient_admin_user_id == actor_uuid, + Notification.is_read.is_(False), + Notification.request_id.is_not(None), + Notification.request_id.in_(request_ids), + ) + .all() + ) + unread_notification_request_ids = { + str(notification_request_id) + for (notification_request_id,) in unread_notification_rows + if notification_request_id is not None + } status_codes = {str(row.status_code or "").strip() for row in request_rows if str(row.status_code or "").strip()} + now_utc = datetime.now(timezone.utc) + next_day_start = datetime(now_utc.year, now_utc.month, now_utc.day, tzinfo=timezone.utc) + timedelta(days=1) status_meta_map: dict[str, dict[str, object]] = {} if status_codes: @@ -448,9 +495,21 @@ def get_requests_kanban_service( sla_deadline = entered_at + timedelta(hours=int(transition_rule.sla_hours)) assigned_id = str(row.assigned_lawyer_id or "").strip() or None + request_id = str(row.id) + status_is_terminal = bool(status_meta.get("is_terminal")) + has_actor_unread_notification = request_id in unread_notification_request_ids + has_unread_updates = bool(row.lawyer_has_unread_updates) + if role != "LAWYER": + has_unread_updates = bool(row.lawyer_has_unread_updates or row.client_has_unread_updates) + if has_actor_unread_notification: + has_unread_updates = True + important_date_at = parse_datetime_safe(row.important_date_at) + deadline_alert = bool(important_date_at and important_date_at < next_day_start and not status_is_terminal) + if role == "LAWYER": + deadline_alert = deadline_alert and bool(assigned_id) and assigned_id == actor items.append( { - "id": str(row.id), + "id": request_id, "track_number": row.track_number, "client_name": row.client_name, "client_phone": row.client_phone, @@ -470,13 +529,15 @@ def get_requests_kanban_service( "lawyer_unread_event_type": row.lawyer_unread_event_type, "client_has_unread_updates": bool(row.client_has_unread_updates), "client_unread_event_type": row.client_unread_event_type, + "has_unread_updates": has_unread_updates, + "deadline_alert": deadline_alert, "case_deadline_at": case_deadline.isoformat() if case_deadline else None, "sla_deadline_at": sla_deadline.isoformat() if sla_deadline is not None else None, "available_transitions": available_transitions, } ) - items = apply_overdue_filters(items, overdue_filters) + items = apply_boolean_kanban_filters(items, boolean_filters) items = sort_kanban_items(items, normalized_sort_mode) total = len(items) if total > limit: diff --git a/app/api/admin/requests_modules/status_flow.py b/app/api/admin/requests_modules/status_flow.py index 6184a43..fe6c456 100644 --- a/app/api/admin/requests_modules/status_flow.py +++ b/app/api/admin/requests_modules/status_flow.py @@ -294,55 +294,92 @@ def get_request_status_route_service( transition_sla_by_edge[(from_status, to_status)] = sla_hours incoming_sla_by_status.setdefault(to_status, sla_hours) - sequence_from_history: list[str] = [] + route_steps: list[dict[str, Any]] = [] if history_rows: first_from = str(history_rows[0].from_status or "").strip() if first_from: - sequence_from_history.append(first_from) + route_steps.append( + { + "code": first_from, + "edge_from": None, + "changed_at": None, + "source": "history", + } + ) for row in history_rows: to_code = str(row.to_status or "").strip() - if to_code: - sequence_from_history.append(to_code) + if not to_code: + continue + from_code = str(row.from_status or "").strip() or None + route_steps.append( + { + "code": to_code, + "edge_from": from_code, + "changed_at": row.created_at.isoformat() if row.created_at else None, + "source": "history", + } + ) elif current_status: - sequence_from_history.append(current_status) + route_steps.append( + { + "code": current_status, + "edge_from": None, + "changed_at": (req.updated_at or req.created_at).isoformat() if (req.updated_at or req.created_at) else None, + "source": "current", + } + ) - ordered_codes: list[str] = [] - seen_codes: set[str] = set() + if current_status and not any(str(step.get("code") or "").strip() == current_status for step in route_steps): + route_steps.append( + { + "code": current_status, + "edge_from": None, + "changed_at": (req.updated_at or req.created_at).isoformat() if (req.updated_at or req.created_at) else None, + "source": "current", + } + ) - def add_code(code: str) -> None: - normalized = str(code or "").strip() - if not normalized or normalized in seen_codes: - return - seen_codes.add(normalized) - ordered_codes.append(normalized) - - for code in sequence_from_history: - add_code(code) - - add_code(current_status) for to_status in outgoing_by_status.get(current_status, []): - add_code(to_status) + normalized = str(to_status or "").strip() + if not normalized: + continue + route_steps.append( + { + "code": normalized, + "edge_from": current_status or None, + "changed_at": None, + "source": "outgoing", + } + ) - changed_at_by_status: dict[str, str] = {} - for row in history_rows: - to_code = str(row.to_status or "").strip() - if to_code and row.created_at: - changed_at_by_status[to_code] = row.created_at.isoformat() - - visited_codes = {code for code in sequence_from_history if code} - current_index = ordered_codes.index(current_status) if current_status in ordered_codes else -1 + current_index = -1 + if current_status: + for idx in range(len(route_steps) - 1, -1, -1): + code = str(route_steps[idx].get("code") or "").strip() + source = str(route_steps[idx].get("source") or "").strip() + if code != current_status: + continue + if source == "outgoing": + continue + current_index = idx + break + if current_index < 0 and route_steps: + current_index = len(route_steps) - 1 def status_name(code: str) -> str: meta = statuses_map.get(code) or {} return str(meta.get("name") or code) nodes: list[dict[str, str | int | None]] = [] - for index, code in enumerate(ordered_codes): + for index, step in enumerate(route_steps): + code = str(step.get("code") or "").strip() + if not code: + continue meta = statuses_map.get(code) or {} state = "pending" - if code == current_status: + if index == current_index: state = "current" - elif code in visited_codes or (current_index >= 0 and index < current_index): + elif current_index >= 0 and index < current_index: state = "completed" note_parts: list[str] = [] @@ -358,10 +395,10 @@ def get_request_status_route_service( "name": status_name(code), "kind": kind, "state": state, - "changed_at": changed_at_by_status.get(code), + "changed_at": str(step.get("changed_at") or "").strip() or None, "sla_hours": ( - transition_sla_by_edge.get((ordered_codes[index - 1], code)) - if index > 0 + transition_sla_by_edge.get((str(step.get("edge_from") or "").strip(), code)) + if str(step.get("edge_from") or "").strip() else None ) or incoming_sla_by_status.get(code), diff --git a/app/api/public/requests.py b/app/api/public/requests.py index b3cb0eb..4cb0db7 100644 --- a/app/api/public/requests.py +++ b/app/api/public/requests.py @@ -28,7 +28,6 @@ from app.models.status_history import StatusHistory from app.models.topic import Topic from app.services.invoice_crypto import decrypt_requisites from app.services.invoice_pdf import build_invoice_pdf_bytes -from app.services.chat_secure_service import create_client_message, list_messages_for_request from app.services.origin_guard import enforce_public_origin_or_403 from app.services.notifications import ( get_client_notification, @@ -44,8 +43,6 @@ from app.services.security_audit import extract_client_ip, record_pii_access_eve from app.api.admin.requests_modules.status_flow import get_request_status_route_service from app.schemas.public import ( PublicAttachmentRead, - PublicMessageCreate, - PublicMessageRead, PublicRequestCreate, PublicRequestCreated, PublicServiceRequestCreate, @@ -427,6 +424,14 @@ def get_request_by_track( session: dict = Depends(get_public_session), ): req = _request_for_track_or_404(db, session, track_number) + status_name = str(req.status_code or "") + if str(req.status_code or "").strip(): + try: + status_row = db.query(Status).filter(Status.code == req.status_code).first() + except SQLAlchemyError: + status_row = None + if status_row is not None: + status_name = str(status_row.name or req.status_code or "") topic_name = None if str(req.topic_code or "").strip(): try: @@ -478,15 +483,13 @@ def get_request_by_track( "topic_code": req.topic_code, "topic_name": topic_name, "status_code": req.status_code, + "status_name": status_name, "important_date_at": _to_iso(req.important_date_at), "description": req.description, "extra_fields": req.extra_fields, "assigned_lawyer_id": req.assigned_lawyer_id, "assigned_lawyer_name": lawyer_name or req.assigned_lawyer_id, "assigned_lawyer_phone": lawyer_phone, - "request_cost": float(req.request_cost) if req.request_cost is not None else None, - "effective_rate": float(req.effective_rate) if req.effective_rate is not None else None, - "paid_at": _to_iso(req.paid_at), "client_has_unread_updates": req.client_has_unread_updates, "client_unread_event_type": req.client_unread_event_type, "lawyer_has_unread_updates": req.lawyer_has_unread_updates, @@ -589,62 +592,6 @@ def get_status_route_by_track( return payload -@router.get("/{track_number}/messages", response_model=list[PublicMessageRead]) -def list_messages_by_track( - track_number: str, - request: FastapiRequest, - db: Session = Depends(get_db), - session: dict = Depends(get_public_session), -): - req = _request_for_track_or_404(db, session, track_number) - rows = list_messages_for_request(db, req.id) - payload = [ - PublicMessageRead( - id=row.id, - request_id=row.request_id, - author_type=row.author_type, - author_name=row.author_name, - body=row.body, - created_at=_to_iso(row.created_at), - updated_at=_to_iso(row.updated_at), - ) - for row in rows - ] - _record_public_read_audit( - db, - session=session, - http_request=request, - action="READ_CHAT_MESSAGES", - scope="CHAT", - request_id=req.id, - details={"rows": len(rows)}, - ) - return payload - - -@router.post("/{track_number}/messages", response_model=PublicMessageRead, status_code=201) -def create_message_by_track( - track_number: str, - payload: PublicMessageCreate, - request: FastapiRequest, - db: Session = Depends(get_db), - session: dict = Depends(get_public_session), -): - enforce_public_origin_or_403(request, endpoint="/api/public/requests/{track_number}/messages") - req = _request_for_track_or_404(db, session, track_number) - row = create_client_message(db, request=req, body=payload.body) - - return PublicMessageRead( - id=row.id, - request_id=row.request_id, - author_type=row.author_type, - author_name=row.author_name, - body=row.body, - created_at=_to_iso(row.created_at), - updated_at=_to_iso(row.updated_at), - ) - - @router.get("/{track_number}/attachments", response_model=list[PublicAttachmentRead]) def list_attachments_by_track( track_number: str, diff --git a/app/assets/invoice_signature_stamp.png b/app/assets/invoice_signature_stamp.png new file mode 100644 index 0000000000000000000000000000000000000000..b76dbd34a271355da6d8a537292b07de2895f098 GIT binary patch literal 151987 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"$KTPDk0gMz^n=f?+kh9bg019!!gk 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%0AYEecV+D^=HQXCK=y?q3pwzSneXr%_GR=#;C|xER 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 z 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 literal 0 HcmV?d00001 diff --git a/app/services/billing_flow.py b/app/services/billing_flow.py index 7bfeaf1..5285b4c 100644 --- a/app/services/billing_flow.py +++ b/app/services/billing_flow.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from decimal import Decimal from string import Formatter from typing import Any -from uuid import UUID, uuid4 +from uuid import UUID from fastapi import HTTPException from sqlalchemy import inspect @@ -14,7 +14,9 @@ from sqlalchemy.orm import Session from app.models.invoice import Invoice from app.models.request import Request from app.models.status import Status +from app.services.invoice_chat import create_invoice_chat_message_with_attachment from app.services.invoice_crypto import encrypt_requisites +from app.services.invoice_numbering import generate_invoice_number STATUS_KIND_DEFAULT = "DEFAULT" STATUS_KIND_INVOICE = "INVOICE" @@ -109,15 +111,6 @@ def _status_template(db: Session, status_code: str) -> str | None: return value or None -def _invoice_number(db: Session) -> str: - prefix = _now_utc().strftime("%Y%m%d") - candidate = f"INV-{prefix}-{uuid4().hex[:8].upper()}" - exists = db.query(Invoice.id).filter(Invoice.invoice_number == candidate).first() - if exists is None: - return candidate - return f"INV-{prefix}-{uuid4().hex[:12].upper()}" - - def _safe_render_template(template: str, values: dict[str, Any]) -> str: source = str(template or "").strip() or DEFAULT_INVOICE_TEMPLATE allowed = { @@ -188,9 +181,10 @@ def _create_waiting_invoice( actor = _actor_uuid_or_none(admin) role = str((admin or {}).get("role") or "").strip().upper() or None + issued_at = _now_utc() invoice = Invoice( request_id=req.id, - invoice_number=_invoice_number(db), + invoice_number=generate_invoice_number(db, issued_at), status=INVOICE_STATUS_WAITING, amount=amount, currency="RUB", @@ -204,7 +198,7 @@ def _create_waiting_invoice( ), issued_by_admin_user_id=actor, issued_by_role=role, - issued_at=_now_utc(), + issued_at=issued_at, paid_at=None, responsible=responsible, ) @@ -213,6 +207,15 @@ def _create_waiting_invoice( req.invoice_amount = amount req.responsible = responsible db.add(req) + create_invoice_chat_message_with_attachment( + db, + request=req, + invoice=invoice, + actor_role=role or "ADMIN", + actor_name=str((admin or {}).get("name") or (admin or {}).get("email") or responsible), + actor_admin_user_id=(admin or {}).get("sub"), + responsible=responsible, + ) return invoice.invoice_number diff --git a/app/services/chat_secure_service.py b/app/services/chat_secure_service.py index cf4728b..7b01254 100644 --- a/app/services/chat_secure_service.py +++ b/app/services/chat_secure_service.py @@ -43,6 +43,9 @@ def serialize_message(row: Message) -> dict[str, Any]: "author_type": row.author_type, "author_name": row.author_name, "body": row.body, + "message_kind": "TEXT", + "request_data_items": [], + "request_data_all_filled": False, "created_at": row.created_at.isoformat() if row.created_at else None, "updated_at": row.updated_at.isoformat() if row.updated_at else None, } diff --git a/app/services/invoice_chat.py b/app/services/invoice_chat.py new file mode 100644 index 0000000..535f05a --- /dev/null +++ b/app/services/invoice_chat.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import uuid +from datetime import datetime, timezone +from typing import Any + +from fastapi import HTTPException +from sqlalchemy.orm import Session + +from app.models.admin_user import AdminUser +from app.models.attachment import Attachment +from app.models.invoice import Invoice +from app.models.message import Message +from app.models.request import Request +from app.services.attachment_scan import SCAN_STATUS_CLEAN +from app.services.invoice_crypto import decrypt_requisites +from app.services.invoice_pdf import build_invoice_pdf_bytes +from app.services.notifications import EVENT_MESSAGE as NOTIFICATION_EVENT_MESSAGE, notify_request_event +from app.services.request_read_markers import EVENT_MESSAGE, mark_unread_for_client +from app.services.s3_storage import build_object_key, get_s3_storage + +INVOICE_CHAT_MESSAGE_BODY = "Счет на оплату" +CHAT_PARTICIPANT_ADMIN_IDS_KEY = "chat_participant_admin_ids" +INVOICE_STATUS_LABELS = { + "WAITING_PAYMENT": "Ожидает оплату", + "PAID": "Оплачен", + "CANCELED": "Отменен", +} + + +def _now_utc() -> datetime: + return datetime.now(timezone.utc) + + +def _normalize_admin_uuid(value: Any) -> str | None: + raw = str(value or "").strip() + if not raw: + return None + try: + return str(uuid.UUID(raw)) + except (TypeError, ValueError): + return None + + +def _register_chat_participant(request: Request, admin_user_id: Any) -> None: + normalized = _normalize_admin_uuid(admin_user_id) + if not normalized: + return + current = request.extra_fields if isinstance(request.extra_fields, dict) else {} + extra = dict(current or {}) + raw_ids = extra.get(CHAT_PARTICIPANT_ADMIN_IDS_KEY) + known_ids: set[str] = set() + if isinstance(raw_ids, list): + for value in raw_ids: + item = _normalize_admin_uuid(value) + if item: + known_ids.add(item) + elif isinstance(raw_ids, str): + item = _normalize_admin_uuid(raw_ids) + if item: + known_ids.add(item) + known_ids.add(normalized) + extra[CHAT_PARTICIPANT_ADMIN_IDS_KEY] = sorted(known_ids) + request.extra_fields = extra + + +def _write_invoice_pdf_to_storage_or_500(*, key: str, content: bytes) -> None: + storage = get_s3_storage() + if hasattr(storage, "client") and hasattr(storage.client, "put_object") and hasattr(storage, "bucket"): + storage.client.put_object( + Bucket=storage.bucket, + Key=key, + Body=content, + ContentType="application/pdf", + ) + return + objects = getattr(storage, "objects", None) + if isinstance(objects, dict): + objects[key] = { + "size": int(len(content)), + "mime": "application/pdf", + "content": bytes(content), + } + return + raise HTTPException(status_code=500, detail="Хранилище не поддерживает запись PDF счета") + + +def _status_label(status: str | None) -> str: + normalized = str(status or "").strip().upper() + if not normalized: + return "-" + return INVOICE_STATUS_LABELS.get(normalized, normalized) + + +def _issuer_name(db: Session, *, actor_admin_user_id: Any, actor_name: str) -> str: + normalized = _normalize_admin_uuid(actor_admin_user_id) + if not normalized: + return str(actor_name or "").strip() or "Администратор системы" + row = db.get(AdminUser, uuid.UUID(normalized)) + if row is None: + return str(actor_name or "").strip() or "Администратор системы" + return str(row.name or row.email or actor_name or "Администратор системы").strip() or "Администратор системы" + + +def create_invoice_chat_message_with_attachment( + db: Session, + *, + request: Request, + invoice: Invoice, + actor_role: str, + actor_name: str, + actor_admin_user_id: Any, + responsible: str, +) -> tuple[Message, Attachment]: + normalized_role = str(actor_role or "").strip().upper() or "ADMIN" + author_type = "LAWYER" if normalized_role in {"LAWYER", "CURATOR"} else "SYSTEM" + author_name = str(actor_name or "").strip() or ("Юрист" if author_type == "LAWYER" else "Администратор системы") + safe_responsible = str(responsible or "").strip() or "Администратор системы" + + message = Message( + request_id=request.id, + author_type=author_type, + author_name=author_name, + body=INVOICE_CHAT_MESSAGE_BODY, + responsible=safe_responsible, + ) + db.add(message) + db.flush() + + requisites = decrypt_requisites(invoice.payer_details_encrypted) + pdf_bytes = build_invoice_pdf_bytes( + invoice_number=invoice.invoice_number, + amount=float(invoice.amount or 0), + currency=str(invoice.currency or "RUB"), + status=_status_label(invoice.status), + issued_at=invoice.issued_at, + paid_at=invoice.paid_at, + payer_display_name=str(invoice.payer_display_name or "").strip() or "Клиент", + request_track_number=str(request.track_number or "").strip() or str(request.id), + issued_by_name=_issuer_name(db, actor_admin_user_id=actor_admin_user_id, actor_name=author_name), + requisites=requisites, + ) + if not pdf_bytes: + raise HTTPException(status_code=500, detail="Не удалось сформировать PDF счета") + + file_name = f"Счет {invoice.invoice_number}.pdf" + object_key = build_object_key(f"requests/{request.id}", file_name) + _write_invoice_pdf_to_storage_or_500(key=object_key, content=pdf_bytes) + + attachment = Attachment( + request_id=request.id, + message_id=message.id, + file_name=file_name, + mime_type="application/pdf", + size_bytes=int(len(pdf_bytes)), + s3_key=object_key, + immutable=False, + scan_status=SCAN_STATUS_CLEAN, + scan_signature=None, + scan_error=None, + scanned_at=_now_utc(), + detected_mime="application/pdf", + responsible=safe_responsible, + ) + db.add(attachment) + + _register_chat_participant(request, actor_admin_user_id) + mark_unread_for_client(request, EVENT_MESSAGE) + request.total_attachments_bytes = int(request.total_attachments_bytes or 0) + int(len(pdf_bytes)) + request.responsible = safe_responsible + db.add(request) + notify_request_event( + db, + request=request, + event_type=NOTIFICATION_EVENT_MESSAGE, + actor_role=normalized_role, + actor_admin_user_id=actor_admin_user_id, + body=None, + responsible=safe_responsible, + ) + return message, attachment diff --git a/app/services/invoice_numbering.py b/app/services/invoice_numbering.py new file mode 100644 index 0000000..75bcca4 --- /dev/null +++ b/app/services/invoice_numbering.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import re +from datetime import datetime, timezone + +from sqlalchemy.orm import Session + +from app.models.invoice import Invoice + + +def _now_utc() -> datetime: + return datetime.now(timezone.utc) + + +def generate_invoice_number(db: Session, issued_at: datetime | None = None) -> str: + dt = issued_at or _now_utc() + prefix = dt.strftime("%Y%m%d") + pattern = re.compile(rf"^{re.escape(prefix)}(?:-(\d+))?$") + + rows = db.query(Invoice.invoice_number).filter(Invoice.invoice_number.like(f"{prefix}%")).all() + max_order = 0 + has_base = False + for (raw_number,) in rows: + number = str(raw_number or "").strip() + match = pattern.match(number) + if not match: + continue + suffix = match.group(1) + if not suffix: + has_base = True + max_order = max(max_order, 1) + continue + try: + order = int(suffix) + except ValueError: + continue + if order <= 1: + order = 1 + max_order = max(max_order, order) + + if not has_base and max_order == 0: + return prefix + + next_order = max(max_order, 1) + 1 + return f"{prefix}-{next_order}" + diff --git a/app/services/invoice_pdf.py b/app/services/invoice_pdf.py index f01fe03..f4c885d 100644 --- a/app/services/invoice_pdf.py +++ b/app/services/invoice_pdf.py @@ -1,8 +1,87 @@ from __future__ import annotations -from datetime import datetime -from typing import Any +import io +import os import unicodedata +from datetime import datetime +from decimal import Decimal, ROUND_HALF_UP +from typing import Any + +REPORTLAB_AVAILABLE = True +try: + from reportlab.lib import colors + from reportlab.lib.pagesizes import A4 + from reportlab.lib.units import mm + from reportlab.lib.utils import ImageReader, simpleSplit + from reportlab.pdfbase import pdfmetrics + from reportlab.pdfbase.ttfonts import TTFont + from reportlab.pdfgen import canvas + from reportlab.platypus import Table, TableStyle +except Exception: + REPORTLAB_AVAILABLE = False + + +_DEFAULT_ISSUER = 'ООО "Аудиторы корпоративной безопасности"' +_DEFAULT_ISSUER_ADDRESS = "г. Ярославль, ул. Богдановича, 6А" +_DEFAULT_ISSUER_PHONE = "+7 (977) 268-94-06" +_DEFAULT_ISSUER_INN = "7604226740" +_DEFAULT_ISSUER_KPP = "760401001" +_DEFAULT_ISSUER_OGRN = "1127604008806" +_DEFAULT_BANK_NAME = 'АО "АЛЬФА-БАНК"' +_DEFAULT_BANK_BIK = "044525593" +_DEFAULT_BANK_ACCOUNT = "40702810501860000582" +_DEFAULT_BANK_CORR_ACCOUNT = "30101810200000000593" +_DEFAULT_SIGNATURE_STAMP_IMAGE = "invoice_signature_stamp.png" +_DEFAULT_DIRECTOR_NAME = "Андрианова С.С." + +_RU_MONTHS = [ + "января", + "февраля", + "марта", + "апреля", + "мая", + "июня", + "июля", + "августа", + "сентября", + "октября", + "ноября", + "декабря", +] + +_FONT_CANDIDATES: list[tuple[str, str | None]] = [ + ("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"), + ("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf"), + ("/usr/share/fonts/truetype/freefont/FreeSans.ttf", "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf"), + ("/System/Library/Fonts/Supplemental/Arial.ttf", "/System/Library/Fonts/Supplemental/Arial Bold.ttf"), + ("/System/Library/Fonts/Supplemental/Arial Unicode.ttf", None), + ("/Library/Fonts/Arial.ttf", "/Library/Fonts/Arial Bold.ttf"), + ("/Library/Fonts/Arial Unicode.ttf", None), +] +_FONT_CACHE: tuple[str, str] | None = None + +_UNITS_MALE = ("", "один", "два", "три", "четыре", "пять", "шесть", "семь", "восемь", "девять") +_UNITS_FEMALE = ("", "одна", "две", "три", "четыре", "пять", "шесть", "семь", "восемь", "девять") +_TEENS = ( + "десять", + "одиннадцать", + "двенадцать", + "тринадцать", + "четырнадцать", + "пятнадцать", + "шестнадцать", + "семнадцать", + "восемнадцать", + "девятнадцать", +) +_TENS = ("", "", "двадцать", "тридцать", "сорок", "пятьдесят", "шестьдесят", "семьдесят", "восемьдесят", "девяносто") +_HUNDREDS = ("", "сто", "двести", "триста", "четыреста", "пятьсот", "шестьсот", "семьсот", "восемьсот", "девятьсот") +_SCALES = [ + ("", "", "", False), + ("тысяча", "тысячи", "тысяч", True), + ("миллион", "миллиона", "миллионов", False), + ("миллиард", "миллиарда", "миллиардов", False), +] def _ascii_text(value: Any) -> str: @@ -30,6 +109,413 @@ def _build_content_stream(lines: list[str]) -> bytes: return "\n".join(parts).encode("latin-1", errors="ignore") +def _build_legacy_invoice_pdf_bytes(lines: list[str]) -> bytes: + stream = _build_content_stream(lines) + objects = [ + b"1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n", + b"2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n", + b"3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> endobj\n", + b"4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj\n", + f"5 0 obj << /Length {len(stream)} >> stream\n".encode("latin-1") + stream + b"\nendstream endobj\n", + ] + body = b"%PDF-1.4\n" + offsets = [0] + for obj in objects: + offsets.append(len(body)) + body += obj + xref_offset = len(body) + body += f"xref\n0 {len(objects)+1}\n".encode("latin-1") + body += b"0000000000 65535 f \n" + for offset in offsets[1:]: + body += f"{offset:010d} 00000 n \n".encode("latin-1") + body += f"trailer << /Size {len(objects)+1} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n".encode("latin-1") + return body + + +def _first_non_empty(source: dict[str, Any], *keys: str, default: str = "") -> str: + for key in keys: + value = source.get(key) + if value is None: + continue + text = str(value).strip() + if text: + return text + return default + + +def _format_amount(value: float) -> str: + amount = Decimal(str(value or 0)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + return f"{amount:.2f}" + + +def _format_amount_ru(value: float) -> str: + amount = Decimal(str(value or 0)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + integer_part = int(amount) + fraction = int((amount - Decimal(integer_part)) * 100) + grouped = f"{integer_part:,}".replace(",", " ") + if fraction == 0: + return grouped + return f"{grouped},{fraction:02d}" + + +def _plural_ru(value: int, forms: tuple[str, str, str]) -> str: + n = abs(int(value)) % 100 + if 11 <= n <= 19: + return forms[2] + n = n % 10 + if n == 1: + return forms[0] + if 2 <= n <= 4: + return forms[1] + return forms[2] + + +def _triplet_to_words(value: int, *, female: bool) -> list[str]: + n = int(value) % 1000 + if n == 0: + return [] + words: list[str] = [] + words.append(_HUNDREDS[n // 100]) + n = n % 100 + if 10 <= n <= 19: + words.append(_TEENS[n - 10]) + else: + words.append(_TENS[n // 10]) + unit_map = _UNITS_FEMALE if female else _UNITS_MALE + words.append(unit_map[n % 10]) + return [word for word in words if word] + + +def _integer_to_words_ru(value: int) -> str: + number = int(value) + if number == 0: + return "ноль" + parts: list[str] = [] + scale_index = 0 + while number > 0: + triplet = number % 1000 + if triplet: + one, two, five, female = _SCALES[min(scale_index, len(_SCALES) - 1)] + segment = _triplet_to_words(triplet, female=female) + if scale_index > 0: + segment.append(_plural_ru(triplet, (one, two, five))) + parts.append(" ".join(segment)) + number //= 1000 + scale_index += 1 + return " ".join(reversed(parts)).strip() + + +def _amount_words_ru(amount: float) -> str: + dec = Decimal(str(amount or 0)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + rub = int(dec) + kop = int((dec - Decimal(rub)) * 100) + words = _integer_to_words_ru(rub) + rub_label = _plural_ru(rub, ("рубль", "рубля", "рублей")) + kop_label = _plural_ru(kop, ("копейка", "копейки", "копеек")) + return f"{words} {rub_label} {kop:02d} {kop_label}".strip() + + +def _capitalize_first(text: str) -> str: + value = str(text or "").strip() + if not value: + return "" + return value[0].upper() + value[1:] + + +def _format_invoice_date(value: datetime | None) -> str: + dt = value or datetime.now() + month = _RU_MONTHS[max(0, min(11, dt.month - 1))] + return f"{dt.day:02d} {month} {dt.year} г." + + +def _resolve_reportlab_fonts() -> tuple[str, str]: + global _FONT_CACHE + if _FONT_CACHE is not None: + return _FONT_CACHE + + regular_name = "Helvetica" + bold_name = "Helvetica-Bold" + for regular_path, bold_path in _FONT_CANDIDATES: + if not os.path.exists(regular_path): + continue + try: + regular_name = "InvoiceSans" + pdfmetrics.registerFont(TTFont(regular_name, regular_path)) + if bold_path and os.path.exists(bold_path): + bold_name = "InvoiceSansBold" + pdfmetrics.registerFont(TTFont(bold_name, bold_path)) + else: + bold_name = regular_name + _FONT_CACHE = (regular_name, bold_name) + return _FONT_CACHE + except Exception: + regular_name = "Helvetica" + bold_name = "Helvetica-Bold" + + _FONT_CACHE = (regular_name, bold_name) + return _FONT_CACHE + + +def _resolve_signature_stamp_image_path(req: dict[str, Any]) -> str: + provided = _first_non_empty( + req, + "signature_stamp_image_path", + "signature_stamp_path", + "signature_image_path", + default="", + ) + local_default = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", _DEFAULT_SIGNATURE_STAMP_IMAGE) + candidates = [provided, local_default, f"/app/app/assets/{_DEFAULT_SIGNATURE_STAMP_IMAGE}"] + for path in candidates: + candidate = str(path or "").strip() + if candidate and os.path.exists(candidate): + return candidate + return "" + + +def _display_invoice_number(raw_number: str, issued_at: datetime | None) -> str: + value = str(raw_number or "").strip() + if not value: + return (issued_at or datetime.now()).strftime("%Y%m%d") + upper = value.upper() + if upper.startswith("INV-"): + tail = value[4:] + if len(tail) >= 8 and tail[:8].isdigit(): + date_part = tail[:8] + remainder = tail[8:] + if not remainder: + return date_part + if remainder.startswith("-"): + suffix = remainder[1:] + if suffix.isdigit(): + return f"{date_part}-{suffix}" + return date_part + return date_part + return value + + +def _draw_wrapped_line(pdf: Any, *, text: str, x: float, y: float, width: float, font: str, size: int, leading: float) -> float: + lines = simpleSplit(str(text or ""), font, size, width) or [""] + pdf.setFont(font, size) + cursor = y + for line in lines: + pdf.drawString(x, cursor, line) + cursor -= leading + return cursor + + +def _build_reportlab_invoice_pdf_bytes( + *, + invoice_number: str, + amount: float, + currency: str, + status: str, + issued_at: datetime | None, + paid_at: datetime | None, + payer_display_name: str, + request_track_number: str, + issued_by_name: str | None, + requisites: dict[str, Any] | None, +) -> bytes: + regular_font, bold_font = _resolve_reportlab_fonts() + req = dict(requisites or {}) + + issuer_name = _first_non_empty(req, "issuer_name", "beneficiary_name", "recipient_name", default=_DEFAULT_ISSUER) + issuer_address = _first_non_empty(req, "issuer_address", "address", default=_DEFAULT_ISSUER_ADDRESS) + issuer_phone = _first_non_empty(req, "issuer_phone", "phone", default=_DEFAULT_ISSUER_PHONE) + issuer_inn = _first_non_empty(req, "issuer_inn", "inn", default=_DEFAULT_ISSUER_INN) + issuer_kpp = _first_non_empty(req, "issuer_kpp", "kpp", default=_DEFAULT_ISSUER_KPP) + issuer_ogrn = _first_non_empty(req, "issuer_ogrn", "ogrn", default=_DEFAULT_ISSUER_OGRN) + bank_name = _first_non_empty(req, "bank_name", "bank", default=_DEFAULT_BANK_NAME) + bank_bik = _first_non_empty(req, "bank_bik", "bik", default=_DEFAULT_BANK_BIK) + bank_account = _first_non_empty(req, "bank_account", "account", default=_DEFAULT_BANK_ACCOUNT) + bank_corr_account = _first_non_empty(req, "bank_corr_account", "corr_account", default=_DEFAULT_BANK_CORR_ACCOUNT) + service_description = _first_non_empty(req, "service_description", "service", "template_rendered", default="Юридические услуги") + vat_note = _first_non_empty(req, "vat_note", default="без НДС") + director_name = _DEFAULT_DIRECTOR_NAME + signature_stamp_image_path = _resolve_signature_stamp_image_path(req) + + amount_text = _format_amount_ru(amount) + amount_words = _capitalize_first(_amount_words_ru(amount)) + issue_date = issued_at or datetime.now() + invoice_number_display = _display_invoice_number(invoice_number, issue_date) + issue_date_compact = issue_date.strftime("%d.%m.%Y") + + buffer = io.BytesIO() + pdf = canvas.Canvas(buffer, pagesize=A4) + page_width, page_height = A4 + left = 15 * mm + content_width = page_width - 30 * mm + cursor_y = page_height - 13 * mm + + # Header block close to the supplied invoice sample. + pdf.setFillColorRGB(0.17, 0.35, 0.40) + pdf.setFont(bold_font, 18) + pdf.drawCentredString(page_width / 2, cursor_y, "АУДИТОРЫ КОРПОРАТИВНОЙ БЕЗОПАСНОСТИ") + cursor_y -= 6.5 * mm + pdf.setFillColorRGB(0, 0, 0) + pdf.setFont(bold_font, 7) + pdf.drawCentredString(page_width / 2, cursor_y, "О Б Щ Е С Т В О С О Г Р А Н И Ч Е Н Н О Й О Т В Е Т С Т В Е Н Н О С Т Ь Ю") + cursor_y -= 4.6 * mm + pdf.setFont(regular_font, 8) + pdf.drawCentredString(page_width / 2, cursor_y, "Россия, 150014, Ярославль, ул. Богдановича, 6А") + cursor_y -= 2.2 * mm + pdf.line(left, cursor_y, page_width - left, cursor_y) + cursor_y -= 6.2 * mm + + pdf.setFont(bold_font, 10) + pdf.drawString(left + 1 * mm, cursor_y, "Образец заполнения платежного поручения") + cursor_y -= 2.2 * mm + + bank_table = Table( + [ + [f"ИНН {issuer_inn}", f"КПП {issuer_kpp}", "", "Сч. №", bank_account], + [f"Получатель\n{issuer_name}", "", "", "", ""], + [f"Банк получателя\n{bank_name}", "", "", "БИК", bank_bik], + ["", "", "", "Сч. №", bank_corr_account], + ], + colWidths=[37 * mm, 34 * mm, 39 * mm, 25 * mm, 50 * mm], + ) + bank_table.setStyle( + TableStyle( + [ + ("FONT", (0, 0), (-1, -1), regular_font, 9), + ("FONT", (0, 0), (2, 0), bold_font, 8), + ("GRID", (0, 0), (-1, -1), 0.7, colors.black), + ("SPAN", (1, 0), (2, 0)), + ("SPAN", (0, 1), (2, 1)), + ("SPAN", (0, 2), (2, 3)), + ("SPAN", (3, 0), (3, 1)), + ("SPAN", (4, 0), (4, 1)), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("ALIGN", (3, 0), (3, -1), "CENTER"), + ("ALIGN", (4, 0), (4, -1), "LEFT"), + ("LEFTPADDING", (0, 0), (-1, -1), 4), + ("RIGHTPADDING", (0, 0), (-1, -1), 4), + ("TOPPADDING", (0, 0), (-1, -1), 4), + ("BOTTOMPADDING", (0, 0), (-1, -1), 4), + ] + ) + ) + _, bank_table_height = bank_table.wrap(content_width, cursor_y) + bank_table.drawOn(pdf, left, cursor_y - bank_table_height) + cursor_y -= bank_table_height + 5.5 * mm + + pdf.setFont(bold_font, 13) + pdf.drawCentredString(page_width / 2, cursor_y, f"СЧЕТ № {invoice_number_display} от {issue_date_compact} года") + cursor_y -= 6.2 * mm + + details_table = Table( + [ + ["Исполнитель", issuer_name], + ["Адрес", issuer_address], + ["Телефон", issuer_phone], + ["Расчетный счет", bank_account], + ["Банк", bank_name], + ["БИК", bank_bik], + ["Корр. счет", bank_corr_account], + ["ИНН", issuer_inn], + ["КПП", issuer_kpp], + ["ОГРН", issuer_ogrn], + ], + colWidths=[30 * mm, content_width - 30 * mm], + ) + details_table.setStyle( + TableStyle( + [ + ("FONT", (0, 0), (-1, -1), regular_font, 9), + ("GRID", (0, 0), (-1, -1), 0.7, colors.black), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("LEFTPADDING", (0, 0), (-1, -1), 4), + ("RIGHTPADDING", (0, 0), (-1, -1), 4), + ("TOPPADDING", (0, 0), (-1, -1), 3), + ("BOTTOMPADDING", (0, 0), (-1, -1), 3), + ] + ) + ) + _, details_table_height = details_table.wrap(content_width, cursor_y) + details_table.drawOn(pdf, left, cursor_y - details_table_height) + cursor_y -= details_table_height + 5 * mm + + pdf.line(left, cursor_y, page_width - left, cursor_y) + cursor_y -= 2.4 * mm + + item_name_width = 95 * mm - 8 + wrapped_service = "\n".join(simpleSplit(service_description, regular_font, 9, item_name_width) or [service_description]) + + item_table = Table( + [ + ["№\nПП", "Наименование", "Кол-во", "Цена\n(за единицу)", "ВСЕГО"], + ["1", wrapped_service, "1", amount_text, amount_text], + ["ВСЕГО", "", "", "", amount_text], + ], + colWidths=[13 * mm, 95 * mm, 18 * mm, 27 * mm, 28 * mm], + ) + item_table.setStyle( + TableStyle( + [ + ("FONT", (0, 0), (-1, -1), regular_font, 9), + ("FONT", (0, 0), (-1, 0), bold_font, 9), + ("FONT", (0, 2), (4, 2), bold_font, 9), + ("GRID", (0, 0), (-1, -1), 0.7, colors.black), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("ALIGN", (0, 0), (0, -1), "CENTER"), + ("ALIGN", (2, 0), (4, -1), "CENTER"), + ("ALIGN", (3, 1), (4, -1), "RIGHT"), + ("SPAN", (0, 2), (3, 2)), + ("ALIGN", (0, 2), (3, 2), "LEFT"), + ("LEFTPADDING", (0, 0), (-1, -1), 4), + ("RIGHTPADDING", (0, 0), (-1, -1), 4), + ("TOPPADDING", (0, 0), (-1, -1), 4), + ("BOTTOMPADDING", (0, 0), (-1, -1), 4), + ] + ) + ) + _, item_table_height = item_table.wrap(content_width, cursor_y) + item_table.drawOn(pdf, left, cursor_y - item_table_height) + cursor_y -= item_table_height + 5.5 * mm + + pdf.setFont(regular_font, 9) + prefix = "Сумма прописью: " + pdf.drawString(left, cursor_y, prefix) + prefix_width = pdfmetrics.stringWidth(prefix, regular_font, 9) + pdf.setFont(bold_font, 10) + pdf.drawString(left + prefix_width, cursor_y, f"{amount_words} ({vat_note}).") + cursor_y -= 10 * mm + + block_width = min(155 * mm, content_width) + block_left = left + (content_width - block_width) / 2 + block_center_x = block_left + block_width / 2 + block_top = cursor_y + signature_name = director_name or _DEFAULT_DIRECTOR_NAME + + pdf.setFont(regular_font, 11) + pdf.drawString(block_left + 2 * mm, block_top, "С уважением,") + pdf.drawString(block_left + 2 * mm, block_top - 13 * mm, "Генеральный директор") + pdf.drawString(block_left + 2 * mm, block_top - 19 * mm, "ООО «АКБ»") + pdf.drawString(block_left + block_width - 35 * mm, block_top - 19 * mm, signature_name) + + if signature_stamp_image_path: + try: + stamp_image = ImageReader(signature_stamp_image_path) + img_w, img_h = stamp_image.getSize() + target_h = 40 * mm + target_w = target_h * (float(img_w) / max(float(img_h), 1.0)) + x = block_center_x - target_w / 2 + y = max(12 * mm, block_top - 43 * mm) + pdf.drawImage(stamp_image, x, y, width=target_w, height=target_h, mask="auto") + pdf.setFont(regular_font, 11) + pdf.drawString(x + target_w + 3 * mm, y + 6 * mm, "МП") + except Exception: + pdf.drawString(block_center_x + 28 * mm, block_top - 19 * mm, "МП") + else: + pdf.drawString(block_center_x + 28 * mm, block_top - 19 * mm, "МП") + + pdf.showPage() + pdf.save() + return buffer.getvalue() + + def build_invoice_pdf_bytes( *, invoice_number: str, @@ -43,6 +529,24 @@ def build_invoice_pdf_bytes( issued_by_name: str | None, requisites: dict[str, Any] | None, ) -> bytes: + if REPORTLAB_AVAILABLE: + try: + return _build_reportlab_invoice_pdf_bytes( + invoice_number=invoice_number, + amount=amount, + currency=currency, + status=status, + issued_at=issued_at, + paid_at=paid_at, + payer_display_name=payer_display_name, + request_track_number=request_track_number, + issued_by_name=issued_by_name, + requisites=requisites, + ) + except Exception: + # Safety fallback for environments without fonts/reportlab internals. + pass + lines = [ f"Invoice: {invoice_number}", f"Request: {request_track_number}", @@ -60,25 +564,4 @@ def build_invoice_pdf_bytes( lines.append(f"{key}: {req.get(key)}") else: lines.append("-") - - stream = _build_content_stream(lines) - objects = [ - b"1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n", - b"2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n", - b"3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> endobj\n", - b"4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj\n", - f"5 0 obj << /Length {len(stream)} >> stream\n".encode("latin-1") + stream + b"\nendstream endobj\n", - ] - - body = b"%PDF-1.4\n" - offsets = [0] - for obj in objects: - offsets.append(len(body)) - body += obj - xref_offset = len(body) - body += f"xref\n0 {len(objects)+1}\n".encode("latin-1") - body += b"0000000000 65535 f \n" - for offset in offsets[1:]: - body += f"{offset:010d} 00000 n \n".encode("latin-1") - body += f"trailer << /Size {len(objects)+1} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n".encode("latin-1") - return body + return _build_legacy_invoice_pdf_bytes(lines) diff --git a/app/web/admin.css b/app/web/admin.css index d1027d9..e05decf 100644 --- a/app/web/admin.css +++ b/app/web/admin.css @@ -355,6 +355,7 @@ margin-top: 0.3rem; font-size: 1.2rem; color: #f6dab0; + text-align: right; } .lawyer-dashboard-grid { @@ -1694,6 +1695,110 @@ margin-top: 0.2rem; } + .request-finance-actions { + margin-top: 0.65rem; + padding-top: 0.2rem; + } + + .request-finance-actions-inline { + margin-top: 0.1rem; + padding-top: 0; + } + + .request-finance-issue-form { + margin-top: 0.2rem; + border: 1px solid var(--line); + border-radius: 10px; + padding: 0.55rem; + background: rgba(255, 255, 255, 0.02); + gap: 0.55rem; + } + + .request-finance-issue-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.55rem; + } + + .request-finance-invoices { + margin-top: 0.75rem; + border-top: 1px solid var(--line); + padding-top: 0.6rem; + max-height: min(42vh, 340px); + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 0.45rem; + min-height: 0; + } + + .request-finance-invoices-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.45rem; + margin-bottom: 0.4rem; + } + + .request-finance-invoices-head h4 { + margin: 0; + font-size: 0.92rem; + } + + .request-finance-invoice-list { + display: grid; + gap: 0.45rem; + max-height: none; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding-right: 0.1rem; + } + + .request-finance-invoice-row { + border: 1px solid var(--line); + border-radius: 10px; + padding: 0.5rem 0.55rem; + background: rgba(255, 255, 255, 0.02); + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + } + + .request-finance-invoice-meta { + display: grid; + gap: 0.3rem; + min-width: 0; + flex: 1 1 auto; + } + + .request-finance-invoice-number { + color: #d8e5f7; + font-size: 0.83rem; + } + + .request-finance-invoice-details { + display: flex; + flex-wrap: wrap; + gap: 0.45rem 0.65rem; + color: #b8c8db; + font-size: 0.79rem; + } + + .request-finance-empty { + margin: 0.1rem 0 0; + } + + .request-finance-invoice-download-btn { + width: 32px; + height: 32px; + border-radius: 8px; + font-size: 0.95rem; + line-height: 1; + padding: 0; + flex: 0 0 auto; + } + .request-data-modal { width: min(860px, 100%); } @@ -2558,6 +2663,14 @@ margin-bottom: 0.2rem; } + .chat-service-head { + font-size: 0.84rem; + font-weight: 800; + color: #ffe0a6; + margin-bottom: 0.2rem; + line-height: 1.3; + } + .chat-request-data-bubble.all-filled .chat-request-data-head { color: #d3f4dc; margin-bottom: 0.08rem; @@ -2688,6 +2801,22 @@ line-height: 1.2; } + .request-chat-list li.chat-empty-state { + align-self: center; + width: fit-content; + max-width: min(70%, 360px); + margin: 0.1rem 0 0; + padding: 0.36rem 0.72rem; + border-radius: 999px; + border: 1px solid rgba(131, 151, 178, 0.36); + background: rgba(46, 61, 84, 0.44); + color: #bfd0e6; + font-size: 0.78rem; + line-height: 1.2; + text-align: center; + white-space: nowrap; + } + .request-chat-composer-actions { display: flex; align-items: center; @@ -3026,6 +3155,10 @@ .request-main-column { order: 2; } + .request-finance-invoice-row { + flex-direction: column; + align-items: flex-start; + } } @media (max-width: 620px) { @@ -3082,4 +3215,7 @@ flex-direction: column; align-items: flex-start; } + .request-finance-issue-grid { + grid-template-columns: 1fr; + } } diff --git a/app/web/admin.html b/app/web/admin.html index 4581c53..7993b5a 100644 --- a/app/web/admin.html +++ b/app/web/admin.html @@ -5,12 +5,12 @@ Административная панель • Правовой трекер - +
- + diff --git a/app/web/admin.js b/app/web/admin.js new file mode 100644 index 0000000..10d72a0 --- /dev/null +++ b/app/web/admin.js @@ -0,0 +1,10015 @@ +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod + )); + + // node_modules/qrcode/lib/can-promise.js + var require_can_promise = __commonJS({ + "node_modules/qrcode/lib/can-promise.js"(exports, module) { + module.exports = function() { + return typeof Promise === "function" && Promise.prototype && Promise.prototype.then; + }; + } + }); + + // node_modules/qrcode/lib/core/utils.js + var require_utils = __commonJS({ + "node_modules/qrcode/lib/core/utils.js"(exports) { + var toSJISFunction; + var CODEWORDS_COUNT = [ + 0, + // Not used + 26, + 44, + 70, + 100, + 134, + 172, + 196, + 242, + 292, + 346, + 404, + 466, + 532, + 581, + 655, + 733, + 815, + 901, + 991, + 1085, + 1156, + 1258, + 1364, + 1474, + 1588, + 1706, + 1828, + 1921, + 2051, + 2185, + 2323, + 2465, + 2611, + 2761, + 2876, + 3034, + 3196, + 3362, + 3532, + 3706 + ]; + exports.getSymbolSize = function getSymbolSize(version) { + if (!version) throw new Error('"version" cannot be null or undefined'); + if (version < 1 || version > 40) throw new Error('"version" should be in range from 1 to 40'); + return version * 4 + 17; + }; + exports.getSymbolTotalCodewords = function getSymbolTotalCodewords(version) { + return CODEWORDS_COUNT[version]; + }; + exports.getBCHDigit = function(data) { + let digit = 0; + while (data !== 0) { + digit++; + data >>>= 1; + } + return digit; + }; + exports.setToSJISFunction = function setToSJISFunction(f) { + if (typeof f !== "function") { + throw new Error('"toSJISFunc" is not a valid function.'); + } + toSJISFunction = f; + }; + exports.isKanjiModeEnabled = function() { + return typeof toSJISFunction !== "undefined"; + }; + exports.toSJIS = function toSJIS(kanji) { + return toSJISFunction(kanji); + }; + } + }); + + // node_modules/qrcode/lib/core/error-correction-level.js + var require_error_correction_level = __commonJS({ + "node_modules/qrcode/lib/core/error-correction-level.js"(exports) { + exports.L = { bit: 1 }; + exports.M = { bit: 0 }; + exports.Q = { bit: 3 }; + exports.H = { bit: 2 }; + function fromString(string) { + if (typeof string !== "string") { + throw new Error("Param is not a string"); + } + const lcStr = string.toLowerCase(); + switch (lcStr) { + case "l": + case "low": + return exports.L; + case "m": + case "medium": + return exports.M; + case "q": + case "quartile": + return exports.Q; + case "h": + case "high": + return exports.H; + default: + throw new Error("Unknown EC Level: " + string); + } + } + exports.isValid = function isValid(level) { + return level && typeof level.bit !== "undefined" && level.bit >= 0 && level.bit < 4; + }; + exports.from = function from(value, defaultValue) { + if (exports.isValid(value)) { + return value; + } + try { + return fromString(value); + } catch (e) { + return defaultValue; + } + }; + } + }); + + // node_modules/qrcode/lib/core/bit-buffer.js + var require_bit_buffer = __commonJS({ + "node_modules/qrcode/lib/core/bit-buffer.js"(exports, module) { + function BitBuffer() { + this.buffer = []; + this.length = 0; + } + BitBuffer.prototype = { + get: function(index) { + const bufIndex = Math.floor(index / 8); + return (this.buffer[bufIndex] >>> 7 - index % 8 & 1) === 1; + }, + put: function(num, length) { + for (let i = 0; i < length; i++) { + this.putBit((num >>> length - i - 1 & 1) === 1); + } + }, + getLengthInBits: function() { + return this.length; + }, + putBit: function(bit) { + const bufIndex = Math.floor(this.length / 8); + if (this.buffer.length <= bufIndex) { + this.buffer.push(0); + } + if (bit) { + this.buffer[bufIndex] |= 128 >>> this.length % 8; + } + this.length++; + } + }; + module.exports = BitBuffer; + } + }); + + // node_modules/qrcode/lib/core/bit-matrix.js + var require_bit_matrix = __commonJS({ + "node_modules/qrcode/lib/core/bit-matrix.js"(exports, module) { + function BitMatrix(size) { + if (!size || size < 1) { + throw new Error("BitMatrix size must be defined and greater than 0"); + } + this.size = size; + this.data = new Uint8Array(size * size); + this.reservedBit = new Uint8Array(size * size); + } + BitMatrix.prototype.set = function(row, col, value, reserved) { + const index = row * this.size + col; + this.data[index] = value; + if (reserved) this.reservedBit[index] = true; + }; + BitMatrix.prototype.get = function(row, col) { + return this.data[row * this.size + col]; + }; + BitMatrix.prototype.xor = function(row, col, value) { + this.data[row * this.size + col] ^= value; + }; + BitMatrix.prototype.isReserved = function(row, col) { + return this.reservedBit[row * this.size + col]; + }; + module.exports = BitMatrix; + } + }); + + // node_modules/qrcode/lib/core/alignment-pattern.js + var require_alignment_pattern = __commonJS({ + "node_modules/qrcode/lib/core/alignment-pattern.js"(exports) { + var getSymbolSize = require_utils().getSymbolSize; + exports.getRowColCoords = function getRowColCoords(version) { + if (version === 1) return []; + const posCount = Math.floor(version / 7) + 2; + const size = getSymbolSize(version); + const intervals = size === 145 ? 26 : Math.ceil((size - 13) / (2 * posCount - 2)) * 2; + const positions = [size - 7]; + for (let i = 1; i < posCount - 1; i++) { + positions[i] = positions[i - 1] - intervals; + } + positions.push(6); + return positions.reverse(); + }; + exports.getPositions = function getPositions(version) { + const coords = []; + const pos = exports.getRowColCoords(version); + const posLength = pos.length; + for (let i = 0; i < posLength; i++) { + for (let j = 0; j < posLength; j++) { + if (i === 0 && j === 0 || // top-left + i === 0 && j === posLength - 1 || // bottom-left + i === posLength - 1 && j === 0) { + continue; + } + coords.push([pos[i], pos[j]]); + } + } + return coords; + }; + } + }); + + // node_modules/qrcode/lib/core/finder-pattern.js + var require_finder_pattern = __commonJS({ + "node_modules/qrcode/lib/core/finder-pattern.js"(exports) { + var getSymbolSize = require_utils().getSymbolSize; + var FINDER_PATTERN_SIZE = 7; + exports.getPositions = function getPositions(version) { + const size = getSymbolSize(version); + return [ + // top-left + [0, 0], + // top-right + [size - FINDER_PATTERN_SIZE, 0], + // bottom-left + [0, size - FINDER_PATTERN_SIZE] + ]; + }; + } + }); + + // node_modules/qrcode/lib/core/mask-pattern.js + var require_mask_pattern = __commonJS({ + "node_modules/qrcode/lib/core/mask-pattern.js"(exports) { + exports.Patterns = { + PATTERN000: 0, + PATTERN001: 1, + PATTERN010: 2, + PATTERN011: 3, + PATTERN100: 4, + PATTERN101: 5, + PATTERN110: 6, + PATTERN111: 7 + }; + var PenaltyScores = { + N1: 3, + N2: 3, + N3: 40, + N4: 10 + }; + exports.isValid = function isValid(mask) { + return mask != null && mask !== "" && !isNaN(mask) && mask >= 0 && mask <= 7; + }; + exports.from = function from(value) { + return exports.isValid(value) ? parseInt(value, 10) : void 0; + }; + exports.getPenaltyN1 = function getPenaltyN1(data) { + const size = data.size; + let points = 0; + let sameCountCol = 0; + let sameCountRow = 0; + let lastCol = null; + let lastRow = null; + for (let row = 0; row < size; row++) { + sameCountCol = sameCountRow = 0; + lastCol = lastRow = null; + for (let col = 0; col < size; col++) { + let module2 = data.get(row, col); + if (module2 === lastCol) { + sameCountCol++; + } else { + if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5); + lastCol = module2; + sameCountCol = 1; + } + module2 = data.get(col, row); + if (module2 === lastRow) { + sameCountRow++; + } else { + if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5); + lastRow = module2; + sameCountRow = 1; + } + } + if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5); + if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5); + } + return points; + }; + exports.getPenaltyN2 = function getPenaltyN2(data) { + const size = data.size; + let points = 0; + for (let row = 0; row < size - 1; row++) { + for (let col = 0; col < size - 1; col++) { + const last = data.get(row, col) + data.get(row, col + 1) + data.get(row + 1, col) + data.get(row + 1, col + 1); + if (last === 4 || last === 0) points++; + } + } + return points * PenaltyScores.N2; + }; + exports.getPenaltyN3 = function getPenaltyN3(data) { + const size = data.size; + let points = 0; + let bitsCol = 0; + let bitsRow = 0; + for (let row = 0; row < size; row++) { + bitsCol = bitsRow = 0; + for (let col = 0; col < size; col++) { + bitsCol = bitsCol << 1 & 2047 | data.get(row, col); + if (col >= 10 && (bitsCol === 1488 || bitsCol === 93)) points++; + bitsRow = bitsRow << 1 & 2047 | data.get(col, row); + if (col >= 10 && (bitsRow === 1488 || bitsRow === 93)) points++; + } + } + return points * PenaltyScores.N3; + }; + exports.getPenaltyN4 = function getPenaltyN4(data) { + let darkCount = 0; + const modulesCount = data.data.length; + for (let i = 0; i < modulesCount; i++) darkCount += data.data[i]; + const k = Math.abs(Math.ceil(darkCount * 100 / modulesCount / 5) - 10); + return k * PenaltyScores.N4; + }; + function getMaskAt(maskPattern, i, j) { + switch (maskPattern) { + case exports.Patterns.PATTERN000: + return (i + j) % 2 === 0; + case exports.Patterns.PATTERN001: + return i % 2 === 0; + case exports.Patterns.PATTERN010: + return j % 3 === 0; + case exports.Patterns.PATTERN011: + return (i + j) % 3 === 0; + case exports.Patterns.PATTERN100: + return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0; + case exports.Patterns.PATTERN101: + return i * j % 2 + i * j % 3 === 0; + case exports.Patterns.PATTERN110: + return (i * j % 2 + i * j % 3) % 2 === 0; + case exports.Patterns.PATTERN111: + return (i * j % 3 + (i + j) % 2) % 2 === 0; + default: + throw new Error("bad maskPattern:" + maskPattern); + } + } + exports.applyMask = function applyMask(pattern, data) { + const size = data.size; + for (let col = 0; col < size; col++) { + for (let row = 0; row < size; row++) { + if (data.isReserved(row, col)) continue; + data.xor(row, col, getMaskAt(pattern, row, col)); + } + } + }; + exports.getBestMask = function getBestMask(data, setupFormatFunc) { + const numPatterns = Object.keys(exports.Patterns).length; + let bestPattern = 0; + let lowerPenalty = Infinity; + for (let p = 0; p < numPatterns; p++) { + setupFormatFunc(p); + exports.applyMask(p, data); + const penalty = exports.getPenaltyN1(data) + exports.getPenaltyN2(data) + exports.getPenaltyN3(data) + exports.getPenaltyN4(data); + exports.applyMask(p, data); + if (penalty < lowerPenalty) { + lowerPenalty = penalty; + bestPattern = p; + } + } + return bestPattern; + }; + } + }); + + // node_modules/qrcode/lib/core/error-correction-code.js + var require_error_correction_code = __commonJS({ + "node_modules/qrcode/lib/core/error-correction-code.js"(exports) { + var ECLevel = require_error_correction_level(); + var EC_BLOCKS_TABLE = [ + // L M Q H + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 1, + 2, + 2, + 4, + 1, + 2, + 4, + 4, + 2, + 4, + 4, + 4, + 2, + 4, + 6, + 5, + 2, + 4, + 6, + 6, + 2, + 5, + 8, + 8, + 4, + 5, + 8, + 8, + 4, + 5, + 8, + 11, + 4, + 8, + 10, + 11, + 4, + 9, + 12, + 16, + 4, + 9, + 16, + 16, + 6, + 10, + 12, + 18, + 6, + 10, + 17, + 16, + 6, + 11, + 16, + 19, + 6, + 13, + 18, + 21, + 7, + 14, + 21, + 25, + 8, + 16, + 20, + 25, + 8, + 17, + 23, + 25, + 9, + 17, + 23, + 34, + 9, + 18, + 25, + 30, + 10, + 20, + 27, + 32, + 12, + 21, + 29, + 35, + 12, + 23, + 34, + 37, + 12, + 25, + 34, + 40, + 13, + 26, + 35, + 42, + 14, + 28, + 38, + 45, + 15, + 29, + 40, + 48, + 16, + 31, + 43, + 51, + 17, + 33, + 45, + 54, + 18, + 35, + 48, + 57, + 19, + 37, + 51, + 60, + 19, + 38, + 53, + 63, + 20, + 40, + 56, + 66, + 21, + 43, + 59, + 70, + 22, + 45, + 62, + 74, + 24, + 47, + 65, + 77, + 25, + 49, + 68, + 81 + ]; + var EC_CODEWORDS_TABLE = [ + // L M Q H + 7, + 10, + 13, + 17, + 10, + 16, + 22, + 28, + 15, + 26, + 36, + 44, + 20, + 36, + 52, + 64, + 26, + 48, + 72, + 88, + 36, + 64, + 96, + 112, + 40, + 72, + 108, + 130, + 48, + 88, + 132, + 156, + 60, + 110, + 160, + 192, + 72, + 130, + 192, + 224, + 80, + 150, + 224, + 264, + 96, + 176, + 260, + 308, + 104, + 198, + 288, + 352, + 120, + 216, + 320, + 384, + 132, + 240, + 360, + 432, + 144, + 280, + 408, + 480, + 168, + 308, + 448, + 532, + 180, + 338, + 504, + 588, + 196, + 364, + 546, + 650, + 224, + 416, + 600, + 700, + 224, + 442, + 644, + 750, + 252, + 476, + 690, + 816, + 270, + 504, + 750, + 900, + 300, + 560, + 810, + 960, + 312, + 588, + 870, + 1050, + 336, + 644, + 952, + 1110, + 360, + 700, + 1020, + 1200, + 390, + 728, + 1050, + 1260, + 420, + 784, + 1140, + 1350, + 450, + 812, + 1200, + 1440, + 480, + 868, + 1290, + 1530, + 510, + 924, + 1350, + 1620, + 540, + 980, + 1440, + 1710, + 570, + 1036, + 1530, + 1800, + 570, + 1064, + 1590, + 1890, + 600, + 1120, + 1680, + 1980, + 630, + 1204, + 1770, + 2100, + 660, + 1260, + 1860, + 2220, + 720, + 1316, + 1950, + 2310, + 750, + 1372, + 2040, + 2430 + ]; + exports.getBlocksCount = function getBlocksCount(version, errorCorrectionLevel) { + switch (errorCorrectionLevel) { + case ECLevel.L: + return EC_BLOCKS_TABLE[(version - 1) * 4 + 0]; + case ECLevel.M: + return EC_BLOCKS_TABLE[(version - 1) * 4 + 1]; + case ECLevel.Q: + return EC_BLOCKS_TABLE[(version - 1) * 4 + 2]; + case ECLevel.H: + return EC_BLOCKS_TABLE[(version - 1) * 4 + 3]; + default: + return void 0; + } + }; + exports.getTotalCodewordsCount = function getTotalCodewordsCount(version, errorCorrectionLevel) { + switch (errorCorrectionLevel) { + case ECLevel.L: + return EC_CODEWORDS_TABLE[(version - 1) * 4 + 0]; + case ECLevel.M: + return EC_CODEWORDS_TABLE[(version - 1) * 4 + 1]; + case ECLevel.Q: + return EC_CODEWORDS_TABLE[(version - 1) * 4 + 2]; + case ECLevel.H: + return EC_CODEWORDS_TABLE[(version - 1) * 4 + 3]; + default: + return void 0; + } + }; + } + }); + + // node_modules/qrcode/lib/core/galois-field.js + var require_galois_field = __commonJS({ + "node_modules/qrcode/lib/core/galois-field.js"(exports) { + var EXP_TABLE = new Uint8Array(512); + var LOG_TABLE = new Uint8Array(256); + (function initTables() { + let x = 1; + for (let i = 0; i < 255; i++) { + EXP_TABLE[i] = x; + LOG_TABLE[x] = i; + x <<= 1; + if (x & 256) { + x ^= 285; + } + } + for (let i = 255; i < 512; i++) { + EXP_TABLE[i] = EXP_TABLE[i - 255]; + } + })(); + exports.log = function log(n) { + if (n < 1) throw new Error("log(" + n + ")"); + return LOG_TABLE[n]; + }; + exports.exp = function exp(n) { + return EXP_TABLE[n]; + }; + exports.mul = function mul(x, y) { + if (x === 0 || y === 0) return 0; + return EXP_TABLE[LOG_TABLE[x] + LOG_TABLE[y]]; + }; + } + }); + + // node_modules/qrcode/lib/core/polynomial.js + var require_polynomial = __commonJS({ + "node_modules/qrcode/lib/core/polynomial.js"(exports) { + var GF = require_galois_field(); + exports.mul = function mul(p1, p2) { + const coeff = new Uint8Array(p1.length + p2.length - 1); + for (let i = 0; i < p1.length; i++) { + for (let j = 0; j < p2.length; j++) { + coeff[i + j] ^= GF.mul(p1[i], p2[j]); + } + } + return coeff; + }; + exports.mod = function mod(divident, divisor) { + let result = new Uint8Array(divident); + while (result.length - divisor.length >= 0) { + const coeff = result[0]; + for (let i = 0; i < divisor.length; i++) { + result[i] ^= GF.mul(divisor[i], coeff); + } + let offset = 0; + while (offset < result.length && result[offset] === 0) offset++; + result = result.slice(offset); + } + return result; + }; + exports.generateECPolynomial = function generateECPolynomial(degree) { + let poly = new Uint8Array([1]); + for (let i = 0; i < degree; i++) { + poly = exports.mul(poly, new Uint8Array([1, GF.exp(i)])); + } + return poly; + }; + } + }); + + // node_modules/qrcode/lib/core/reed-solomon-encoder.js + var require_reed_solomon_encoder = __commonJS({ + "node_modules/qrcode/lib/core/reed-solomon-encoder.js"(exports, module) { + var Polynomial = require_polynomial(); + function ReedSolomonEncoder(degree) { + this.genPoly = void 0; + this.degree = degree; + if (this.degree) this.initialize(this.degree); + } + ReedSolomonEncoder.prototype.initialize = function initialize(degree) { + this.degree = degree; + this.genPoly = Polynomial.generateECPolynomial(this.degree); + }; + ReedSolomonEncoder.prototype.encode = function encode(data) { + if (!this.genPoly) { + throw new Error("Encoder not initialized"); + } + const paddedData = new Uint8Array(data.length + this.degree); + paddedData.set(data); + const remainder = Polynomial.mod(paddedData, this.genPoly); + const start = this.degree - remainder.length; + if (start > 0) { + const buff = new Uint8Array(this.degree); + buff.set(remainder, start); + return buff; + } + return remainder; + }; + module.exports = ReedSolomonEncoder; + } + }); + + // node_modules/qrcode/lib/core/version-check.js + var require_version_check = __commonJS({ + "node_modules/qrcode/lib/core/version-check.js"(exports) { + exports.isValid = function isValid(version) { + return !isNaN(version) && version >= 1 && version <= 40; + }; + } + }); + + // node_modules/qrcode/lib/core/regex.js + var require_regex = __commonJS({ + "node_modules/qrcode/lib/core/regex.js"(exports) { + var numeric = "[0-9]+"; + var alphanumeric = "[A-Z $%*+\\-./:]+"; + var kanji = "(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+"; + kanji = kanji.replace(/u/g, "\\u"); + var byte = "(?:(?![A-Z0-9 $%*+\\-./:]|" + kanji + ")(?:.|[\r\n]))+"; + exports.KANJI = new RegExp(kanji, "g"); + exports.BYTE_KANJI = new RegExp("[^A-Z0-9 $%*+\\-./:]+", "g"); + exports.BYTE = new RegExp(byte, "g"); + exports.NUMERIC = new RegExp(numeric, "g"); + exports.ALPHANUMERIC = new RegExp(alphanumeric, "g"); + var TEST_KANJI = new RegExp("^" + kanji + "$"); + var TEST_NUMERIC = new RegExp("^" + numeric + "$"); + var TEST_ALPHANUMERIC = new RegExp("^[A-Z0-9 $%*+\\-./:]+$"); + exports.testKanji = function testKanji(str) { + return TEST_KANJI.test(str); + }; + exports.testNumeric = function testNumeric(str) { + return TEST_NUMERIC.test(str); + }; + exports.testAlphanumeric = function testAlphanumeric(str) { + return TEST_ALPHANUMERIC.test(str); + }; + } + }); + + // node_modules/qrcode/lib/core/mode.js + var require_mode = __commonJS({ + "node_modules/qrcode/lib/core/mode.js"(exports) { + var VersionCheck = require_version_check(); + var Regex = require_regex(); + exports.NUMERIC = { + id: "Numeric", + bit: 1 << 0, + ccBits: [10, 12, 14] + }; + exports.ALPHANUMERIC = { + id: "Alphanumeric", + bit: 1 << 1, + ccBits: [9, 11, 13] + }; + exports.BYTE = { + id: "Byte", + bit: 1 << 2, + ccBits: [8, 16, 16] + }; + exports.KANJI = { + id: "Kanji", + bit: 1 << 3, + ccBits: [8, 10, 12] + }; + exports.MIXED = { + bit: -1 + }; + exports.getCharCountIndicator = function getCharCountIndicator(mode, version) { + if (!mode.ccBits) throw new Error("Invalid mode: " + mode); + if (!VersionCheck.isValid(version)) { + throw new Error("Invalid version: " + version); + } + if (version >= 1 && version < 10) return mode.ccBits[0]; + else if (version < 27) return mode.ccBits[1]; + return mode.ccBits[2]; + }; + exports.getBestModeForData = function getBestModeForData(dataStr) { + if (Regex.testNumeric(dataStr)) return exports.NUMERIC; + else if (Regex.testAlphanumeric(dataStr)) return exports.ALPHANUMERIC; + else if (Regex.testKanji(dataStr)) return exports.KANJI; + else return exports.BYTE; + }; + exports.toString = function toString(mode) { + if (mode && mode.id) return mode.id; + throw new Error("Invalid mode"); + }; + exports.isValid = function isValid(mode) { + return mode && mode.bit && mode.ccBits; + }; + function fromString(string) { + if (typeof string !== "string") { + throw new Error("Param is not a string"); + } + const lcStr = string.toLowerCase(); + switch (lcStr) { + case "numeric": + return exports.NUMERIC; + case "alphanumeric": + return exports.ALPHANUMERIC; + case "kanji": + return exports.KANJI; + case "byte": + return exports.BYTE; + default: + throw new Error("Unknown mode: " + string); + } + } + exports.from = function from(value, defaultValue) { + if (exports.isValid(value)) { + return value; + } + try { + return fromString(value); + } catch (e) { + return defaultValue; + } + }; + } + }); + + // node_modules/qrcode/lib/core/version.js + var require_version = __commonJS({ + "node_modules/qrcode/lib/core/version.js"(exports) { + var Utils = require_utils(); + var ECCode = require_error_correction_code(); + var ECLevel = require_error_correction_level(); + var Mode = require_mode(); + var VersionCheck = require_version_check(); + var G18 = 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0; + var G18_BCH = Utils.getBCHDigit(G18); + function getBestVersionForDataLength(mode, length, errorCorrectionLevel) { + for (let currentVersion = 1; currentVersion <= 40; currentVersion++) { + if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, mode)) { + return currentVersion; + } + } + return void 0; + } + function getReservedBitsCount(mode, version) { + return Mode.getCharCountIndicator(mode, version) + 4; + } + function getTotalBitsFromDataArray(segments, version) { + let totalBits = 0; + segments.forEach(function(data) { + const reservedBits = getReservedBitsCount(data.mode, version); + totalBits += reservedBits + data.getBitsLength(); + }); + return totalBits; + } + function getBestVersionForMixedData(segments, errorCorrectionLevel) { + for (let currentVersion = 1; currentVersion <= 40; currentVersion++) { + const length = getTotalBitsFromDataArray(segments, currentVersion); + if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, Mode.MIXED)) { + return currentVersion; + } + } + return void 0; + } + exports.from = function from(value, defaultValue) { + if (VersionCheck.isValid(value)) { + return parseInt(value, 10); + } + return defaultValue; + }; + exports.getCapacity = function getCapacity(version, errorCorrectionLevel, mode) { + if (!VersionCheck.isValid(version)) { + throw new Error("Invalid QR Code version"); + } + if (typeof mode === "undefined") mode = Mode.BYTE; + const totalCodewords = Utils.getSymbolTotalCodewords(version); + const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel); + const dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8; + if (mode === Mode.MIXED) return dataTotalCodewordsBits; + const usableBits = dataTotalCodewordsBits - getReservedBitsCount(mode, version); + switch (mode) { + case Mode.NUMERIC: + return Math.floor(usableBits / 10 * 3); + case Mode.ALPHANUMERIC: + return Math.floor(usableBits / 11 * 2); + case Mode.KANJI: + return Math.floor(usableBits / 13); + case Mode.BYTE: + default: + return Math.floor(usableBits / 8); + } + }; + exports.getBestVersionForData = function getBestVersionForData(data, errorCorrectionLevel) { + let seg; + const ecl = ECLevel.from(errorCorrectionLevel, ECLevel.M); + if (Array.isArray(data)) { + if (data.length > 1) { + return getBestVersionForMixedData(data, ecl); + } + if (data.length === 0) { + return 1; + } + seg = data[0]; + } else { + seg = data; + } + return getBestVersionForDataLength(seg.mode, seg.getLength(), ecl); + }; + exports.getEncodedBits = function getEncodedBits(version) { + if (!VersionCheck.isValid(version) || version < 7) { + throw new Error("Invalid QR Code version"); + } + let d = version << 12; + while (Utils.getBCHDigit(d) - G18_BCH >= 0) { + d ^= G18 << Utils.getBCHDigit(d) - G18_BCH; + } + return version << 12 | d; + }; + } + }); + + // node_modules/qrcode/lib/core/format-info.js + var require_format_info = __commonJS({ + "node_modules/qrcode/lib/core/format-info.js"(exports) { + var Utils = require_utils(); + var G15 = 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0; + var G15_MASK = 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1; + var G15_BCH = Utils.getBCHDigit(G15); + exports.getEncodedBits = function getEncodedBits(errorCorrectionLevel, mask) { + const data = errorCorrectionLevel.bit << 3 | mask; + let d = data << 10; + while (Utils.getBCHDigit(d) - G15_BCH >= 0) { + d ^= G15 << Utils.getBCHDigit(d) - G15_BCH; + } + return (data << 10 | d) ^ G15_MASK; + }; + } + }); + + // node_modules/qrcode/lib/core/numeric-data.js + var require_numeric_data = __commonJS({ + "node_modules/qrcode/lib/core/numeric-data.js"(exports, module) { + var Mode = require_mode(); + function NumericData(data) { + this.mode = Mode.NUMERIC; + this.data = data.toString(); + } + NumericData.getBitsLength = function getBitsLength(length) { + return 10 * Math.floor(length / 3) + (length % 3 ? length % 3 * 3 + 1 : 0); + }; + NumericData.prototype.getLength = function getLength() { + return this.data.length; + }; + NumericData.prototype.getBitsLength = function getBitsLength() { + return NumericData.getBitsLength(this.data.length); + }; + NumericData.prototype.write = function write(bitBuffer) { + let i, group, value; + for (i = 0; i + 3 <= this.data.length; i += 3) { + group = this.data.substr(i, 3); + value = parseInt(group, 10); + bitBuffer.put(value, 10); + } + const remainingNum = this.data.length - i; + if (remainingNum > 0) { + group = this.data.substr(i); + value = parseInt(group, 10); + bitBuffer.put(value, remainingNum * 3 + 1); + } + }; + module.exports = NumericData; + } + }); + + // node_modules/qrcode/lib/core/alphanumeric-data.js + var require_alphanumeric_data = __commonJS({ + "node_modules/qrcode/lib/core/alphanumeric-data.js"(exports, module) { + var Mode = require_mode(); + var ALPHA_NUM_CHARS = [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + " ", + "$", + "%", + "*", + "+", + "-", + ".", + "/", + ":" + ]; + function AlphanumericData(data) { + this.mode = Mode.ALPHANUMERIC; + this.data = data; + } + AlphanumericData.getBitsLength = function getBitsLength(length) { + return 11 * Math.floor(length / 2) + 6 * (length % 2); + }; + AlphanumericData.prototype.getLength = function getLength() { + return this.data.length; + }; + AlphanumericData.prototype.getBitsLength = function getBitsLength() { + return AlphanumericData.getBitsLength(this.data.length); + }; + AlphanumericData.prototype.write = function write(bitBuffer) { + let i; + for (i = 0; i + 2 <= this.data.length; i += 2) { + let value = ALPHA_NUM_CHARS.indexOf(this.data[i]) * 45; + value += ALPHA_NUM_CHARS.indexOf(this.data[i + 1]); + bitBuffer.put(value, 11); + } + if (this.data.length % 2) { + bitBuffer.put(ALPHA_NUM_CHARS.indexOf(this.data[i]), 6); + } + }; + module.exports = AlphanumericData; + } + }); + + // node_modules/qrcode/lib/core/byte-data.js + var require_byte_data = __commonJS({ + "node_modules/qrcode/lib/core/byte-data.js"(exports, module) { + var Mode = require_mode(); + function ByteData(data) { + this.mode = Mode.BYTE; + if (typeof data === "string") { + this.data = new TextEncoder().encode(data); + } else { + this.data = new Uint8Array(data); + } + } + ByteData.getBitsLength = function getBitsLength(length) { + return length * 8; + }; + ByteData.prototype.getLength = function getLength() { + return this.data.length; + }; + ByteData.prototype.getBitsLength = function getBitsLength() { + return ByteData.getBitsLength(this.data.length); + }; + ByteData.prototype.write = function(bitBuffer) { + for (let i = 0, l = this.data.length; i < l; i++) { + bitBuffer.put(this.data[i], 8); + } + }; + module.exports = ByteData; + } + }); + + // node_modules/qrcode/lib/core/kanji-data.js + var require_kanji_data = __commonJS({ + "node_modules/qrcode/lib/core/kanji-data.js"(exports, module) { + var Mode = require_mode(); + var Utils = require_utils(); + function KanjiData(data) { + this.mode = Mode.KANJI; + this.data = data; + } + KanjiData.getBitsLength = function getBitsLength(length) { + return length * 13; + }; + KanjiData.prototype.getLength = function getLength() { + return this.data.length; + }; + KanjiData.prototype.getBitsLength = function getBitsLength() { + return KanjiData.getBitsLength(this.data.length); + }; + KanjiData.prototype.write = function(bitBuffer) { + let i; + for (i = 0; i < this.data.length; i++) { + let value = Utils.toSJIS(this.data[i]); + if (value >= 33088 && value <= 40956) { + value -= 33088; + } else if (value >= 57408 && value <= 60351) { + value -= 49472; + } else { + throw new Error( + "Invalid SJIS character: " + this.data[i] + "\nMake sure your charset is UTF-8" + ); + } + value = (value >>> 8 & 255) * 192 + (value & 255); + bitBuffer.put(value, 13); + } + }; + module.exports = KanjiData; + } + }); + + // node_modules/dijkstrajs/dijkstra.js + var require_dijkstra = __commonJS({ + "node_modules/dijkstrajs/dijkstra.js"(exports, module) { + "use strict"; + var dijkstra = { + single_source_shortest_paths: function(graph, s, d) { + var predecessors = {}; + var costs = {}; + costs[s] = 0; + var open = dijkstra.PriorityQueue.make(); + open.push(s, 0); + var closest, u, v, cost_of_s_to_u, adjacent_nodes, cost_of_e, cost_of_s_to_u_plus_cost_of_e, cost_of_s_to_v, first_visit; + while (!open.empty()) { + closest = open.pop(); + u = closest.value; + cost_of_s_to_u = closest.cost; + adjacent_nodes = graph[u] || {}; + for (v in adjacent_nodes) { + if (adjacent_nodes.hasOwnProperty(v)) { + cost_of_e = adjacent_nodes[v]; + cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e; + cost_of_s_to_v = costs[v]; + first_visit = typeof costs[v] === "undefined"; + if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) { + costs[v] = cost_of_s_to_u_plus_cost_of_e; + open.push(v, cost_of_s_to_u_plus_cost_of_e); + predecessors[v] = u; + } + } + } + } + if (typeof d !== "undefined" && typeof costs[d] === "undefined") { + var msg = ["Could not find a path from ", s, " to ", d, "."].join(""); + throw new Error(msg); + } + return predecessors; + }, + extract_shortest_path_from_predecessor_list: function(predecessors, d) { + var nodes = []; + var u = d; + var predecessor; + while (u) { + nodes.push(u); + predecessor = predecessors[u]; + u = predecessors[u]; + } + nodes.reverse(); + return nodes; + }, + find_path: function(graph, s, d) { + var predecessors = dijkstra.single_source_shortest_paths(graph, s, d); + return dijkstra.extract_shortest_path_from_predecessor_list( + predecessors, + d + ); + }, + /** + * A very naive priority queue implementation. + */ + PriorityQueue: { + make: function(opts) { + var T = dijkstra.PriorityQueue, t = {}, key; + opts = opts || {}; + for (key in T) { + if (T.hasOwnProperty(key)) { + t[key] = T[key]; + } + } + t.queue = []; + t.sorter = opts.sorter || T.default_sorter; + return t; + }, + default_sorter: function(a, b) { + return a.cost - b.cost; + }, + /** + * Add a new item to the queue and ensure the highest priority element + * is at the front of the queue. + */ + push: function(value, cost) { + var item = { value, cost }; + this.queue.push(item); + this.queue.sort(this.sorter); + }, + /** + * Return the highest priority element in the queue. + */ + pop: function() { + return this.queue.shift(); + }, + empty: function() { + return this.queue.length === 0; + } + } + }; + if (typeof module !== "undefined") { + module.exports = dijkstra; + } + } + }); + + // node_modules/qrcode/lib/core/segments.js + var require_segments = __commonJS({ + "node_modules/qrcode/lib/core/segments.js"(exports) { + var Mode = require_mode(); + var NumericData = require_numeric_data(); + var AlphanumericData = require_alphanumeric_data(); + var ByteData = require_byte_data(); + var KanjiData = require_kanji_data(); + var Regex = require_regex(); + var Utils = require_utils(); + var dijkstra = require_dijkstra(); + function getStringByteLength(str) { + return unescape(encodeURIComponent(str)).length; + } + function getSegments(regex, mode, str) { + const segments = []; + let result; + while ((result = regex.exec(str)) !== null) { + segments.push({ + data: result[0], + index: result.index, + mode, + length: result[0].length + }); + } + return segments; + } + function getSegmentsFromString(dataStr) { + const numSegs = getSegments(Regex.NUMERIC, Mode.NUMERIC, dataStr); + const alphaNumSegs = getSegments(Regex.ALPHANUMERIC, Mode.ALPHANUMERIC, dataStr); + let byteSegs; + let kanjiSegs; + if (Utils.isKanjiModeEnabled()) { + byteSegs = getSegments(Regex.BYTE, Mode.BYTE, dataStr); + kanjiSegs = getSegments(Regex.KANJI, Mode.KANJI, dataStr); + } else { + byteSegs = getSegments(Regex.BYTE_KANJI, Mode.BYTE, dataStr); + kanjiSegs = []; + } + const segs = numSegs.concat(alphaNumSegs, byteSegs, kanjiSegs); + return segs.sort(function(s1, s2) { + return s1.index - s2.index; + }).map(function(obj) { + return { + data: obj.data, + mode: obj.mode, + length: obj.length + }; + }); + } + function getSegmentBitsLength(length, mode) { + switch (mode) { + case Mode.NUMERIC: + return NumericData.getBitsLength(length); + case Mode.ALPHANUMERIC: + return AlphanumericData.getBitsLength(length); + case Mode.KANJI: + return KanjiData.getBitsLength(length); + case Mode.BYTE: + return ByteData.getBitsLength(length); + } + } + function mergeSegments(segs) { + return segs.reduce(function(acc, curr) { + const prevSeg = acc.length - 1 >= 0 ? acc[acc.length - 1] : null; + if (prevSeg && prevSeg.mode === curr.mode) { + acc[acc.length - 1].data += curr.data; + return acc; + } + acc.push(curr); + return acc; + }, []); + } + function buildNodes(segs) { + const nodes = []; + for (let i = 0; i < segs.length; i++) { + const seg = segs[i]; + switch (seg.mode) { + case Mode.NUMERIC: + nodes.push([ + seg, + { data: seg.data, mode: Mode.ALPHANUMERIC, length: seg.length }, + { data: seg.data, mode: Mode.BYTE, length: seg.length } + ]); + break; + case Mode.ALPHANUMERIC: + nodes.push([ + seg, + { data: seg.data, mode: Mode.BYTE, length: seg.length } + ]); + break; + case Mode.KANJI: + nodes.push([ + seg, + { data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) } + ]); + break; + case Mode.BYTE: + nodes.push([ + { data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) } + ]); + } + } + return nodes; + } + function buildGraph(nodes, version) { + const table = {}; + const graph = { start: {} }; + let prevNodeIds = ["start"]; + for (let i = 0; i < nodes.length; i++) { + const nodeGroup = nodes[i]; + const currentNodeIds = []; + for (let j = 0; j < nodeGroup.length; j++) { + const node = nodeGroup[j]; + const key = "" + i + j; + currentNodeIds.push(key); + table[key] = { node, lastCount: 0 }; + graph[key] = {}; + for (let n = 0; n < prevNodeIds.length; n++) { + const prevNodeId = prevNodeIds[n]; + if (table[prevNodeId] && table[prevNodeId].node.mode === node.mode) { + graph[prevNodeId][key] = getSegmentBitsLength(table[prevNodeId].lastCount + node.length, node.mode) - getSegmentBitsLength(table[prevNodeId].lastCount, node.mode); + table[prevNodeId].lastCount += node.length; + } else { + if (table[prevNodeId]) table[prevNodeId].lastCount = node.length; + graph[prevNodeId][key] = getSegmentBitsLength(node.length, node.mode) + 4 + Mode.getCharCountIndicator(node.mode, version); + } + } + } + prevNodeIds = currentNodeIds; + } + for (let n = 0; n < prevNodeIds.length; n++) { + graph[prevNodeIds[n]].end = 0; + } + return { map: graph, table }; + } + function buildSingleSegment(data, modesHint) { + let mode; + const bestMode = Mode.getBestModeForData(data); + mode = Mode.from(modesHint, bestMode); + if (mode !== Mode.BYTE && mode.bit < bestMode.bit) { + throw new Error('"' + data + '" cannot be encoded with mode ' + Mode.toString(mode) + ".\n Suggested mode is: " + Mode.toString(bestMode)); + } + if (mode === Mode.KANJI && !Utils.isKanjiModeEnabled()) { + mode = Mode.BYTE; + } + switch (mode) { + case Mode.NUMERIC: + return new NumericData(data); + case Mode.ALPHANUMERIC: + return new AlphanumericData(data); + case Mode.KANJI: + return new KanjiData(data); + case Mode.BYTE: + return new ByteData(data); + } + } + exports.fromArray = function fromArray(array) { + return array.reduce(function(acc, seg) { + if (typeof seg === "string") { + acc.push(buildSingleSegment(seg, null)); + } else if (seg.data) { + acc.push(buildSingleSegment(seg.data, seg.mode)); + } + return acc; + }, []); + }; + exports.fromString = function fromString(data, version) { + const segs = getSegmentsFromString(data, Utils.isKanjiModeEnabled()); + const nodes = buildNodes(segs); + const graph = buildGraph(nodes, version); + const path = dijkstra.find_path(graph.map, "start", "end"); + const optimizedSegs = []; + for (let i = 1; i < path.length - 1; i++) { + optimizedSegs.push(graph.table[path[i]].node); + } + return exports.fromArray(mergeSegments(optimizedSegs)); + }; + exports.rawSplit = function rawSplit(data) { + return exports.fromArray( + getSegmentsFromString(data, Utils.isKanjiModeEnabled()) + ); + }; + } + }); + + // node_modules/qrcode/lib/core/qrcode.js + var require_qrcode = __commonJS({ + "node_modules/qrcode/lib/core/qrcode.js"(exports) { + var Utils = require_utils(); + var ECLevel = require_error_correction_level(); + var BitBuffer = require_bit_buffer(); + var BitMatrix = require_bit_matrix(); + var AlignmentPattern = require_alignment_pattern(); + var FinderPattern = require_finder_pattern(); + var MaskPattern = require_mask_pattern(); + var ECCode = require_error_correction_code(); + var ReedSolomonEncoder = require_reed_solomon_encoder(); + var Version = require_version(); + var FormatInfo = require_format_info(); + var Mode = require_mode(); + var Segments = require_segments(); + function setupFinderPattern(matrix, version) { + const size = matrix.size; + const pos = FinderPattern.getPositions(version); + for (let i = 0; i < pos.length; i++) { + const row = pos[i][0]; + const col = pos[i][1]; + for (let r = -1; r <= 7; r++) { + if (row + r <= -1 || size <= row + r) continue; + for (let c = -1; c <= 7; c++) { + if (col + c <= -1 || size <= col + c) continue; + if (r >= 0 && r <= 6 && (c === 0 || c === 6) || c >= 0 && c <= 6 && (r === 0 || r === 6) || r >= 2 && r <= 4 && c >= 2 && c <= 4) { + matrix.set(row + r, col + c, true, true); + } else { + matrix.set(row + r, col + c, false, true); + } + } + } + } + } + function setupTimingPattern(matrix) { + const size = matrix.size; + for (let r = 8; r < size - 8; r++) { + const value = r % 2 === 0; + matrix.set(r, 6, value, true); + matrix.set(6, r, value, true); + } + } + function setupAlignmentPattern(matrix, version) { + const pos = AlignmentPattern.getPositions(version); + for (let i = 0; i < pos.length; i++) { + const row = pos[i][0]; + const col = pos[i][1]; + for (let r = -2; r <= 2; r++) { + for (let c = -2; c <= 2; c++) { + if (r === -2 || r === 2 || c === -2 || c === 2 || r === 0 && c === 0) { + matrix.set(row + r, col + c, true, true); + } else { + matrix.set(row + r, col + c, false, true); + } + } + } + } + } + function setupVersionInfo(matrix, version) { + const size = matrix.size; + const bits = Version.getEncodedBits(version); + let row, col, mod; + for (let i = 0; i < 18; i++) { + row = Math.floor(i / 3); + col = i % 3 + size - 8 - 3; + mod = (bits >> i & 1) === 1; + matrix.set(row, col, mod, true); + matrix.set(col, row, mod, true); + } + } + function setupFormatInfo(matrix, errorCorrectionLevel, maskPattern) { + const size = matrix.size; + const bits = FormatInfo.getEncodedBits(errorCorrectionLevel, maskPattern); + let i, mod; + for (i = 0; i < 15; i++) { + mod = (bits >> i & 1) === 1; + if (i < 6) { + matrix.set(i, 8, mod, true); + } else if (i < 8) { + matrix.set(i + 1, 8, mod, true); + } else { + matrix.set(size - 15 + i, 8, mod, true); + } + if (i < 8) { + matrix.set(8, size - i - 1, mod, true); + } else if (i < 9) { + matrix.set(8, 15 - i - 1 + 1, mod, true); + } else { + matrix.set(8, 15 - i - 1, mod, true); + } + } + matrix.set(size - 8, 8, 1, true); + } + function setupData(matrix, data) { + const size = matrix.size; + let inc = -1; + let row = size - 1; + let bitIndex = 7; + let byteIndex = 0; + for (let col = size - 1; col > 0; col -= 2) { + if (col === 6) col--; + while (true) { + for (let c = 0; c < 2; c++) { + if (!matrix.isReserved(row, col - c)) { + let dark = false; + if (byteIndex < data.length) { + dark = (data[byteIndex] >>> bitIndex & 1) === 1; + } + matrix.set(row, col - c, dark); + bitIndex--; + if (bitIndex === -1) { + byteIndex++; + bitIndex = 7; + } + } + } + row += inc; + if (row < 0 || size <= row) { + row -= inc; + inc = -inc; + break; + } + } + } + } + function createData(version, errorCorrectionLevel, segments) { + const buffer = new BitBuffer(); + segments.forEach(function(data) { + buffer.put(data.mode.bit, 4); + buffer.put(data.getLength(), Mode.getCharCountIndicator(data.mode, version)); + data.write(buffer); + }); + const totalCodewords = Utils.getSymbolTotalCodewords(version); + const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel); + const dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8; + if (buffer.getLengthInBits() + 4 <= dataTotalCodewordsBits) { + buffer.put(0, 4); + } + while (buffer.getLengthInBits() % 8 !== 0) { + buffer.putBit(0); + } + const remainingByte = (dataTotalCodewordsBits - buffer.getLengthInBits()) / 8; + for (let i = 0; i < remainingByte; i++) { + buffer.put(i % 2 ? 17 : 236, 8); + } + return createCodewords(buffer, version, errorCorrectionLevel); + } + function createCodewords(bitBuffer, version, errorCorrectionLevel) { + const totalCodewords = Utils.getSymbolTotalCodewords(version); + const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel); + const dataTotalCodewords = totalCodewords - ecTotalCodewords; + const ecTotalBlocks = ECCode.getBlocksCount(version, errorCorrectionLevel); + const blocksInGroup2 = totalCodewords % ecTotalBlocks; + const blocksInGroup1 = ecTotalBlocks - blocksInGroup2; + const totalCodewordsInGroup1 = Math.floor(totalCodewords / ecTotalBlocks); + const dataCodewordsInGroup1 = Math.floor(dataTotalCodewords / ecTotalBlocks); + const dataCodewordsInGroup2 = dataCodewordsInGroup1 + 1; + const ecCount = totalCodewordsInGroup1 - dataCodewordsInGroup1; + const rs = new ReedSolomonEncoder(ecCount); + let offset = 0; + const dcData = new Array(ecTotalBlocks); + const ecData = new Array(ecTotalBlocks); + let maxDataSize = 0; + const buffer = new Uint8Array(bitBuffer.buffer); + for (let b = 0; b < ecTotalBlocks; b++) { + const dataSize = b < blocksInGroup1 ? dataCodewordsInGroup1 : dataCodewordsInGroup2; + dcData[b] = buffer.slice(offset, offset + dataSize); + ecData[b] = rs.encode(dcData[b]); + offset += dataSize; + maxDataSize = Math.max(maxDataSize, dataSize); + } + const data = new Uint8Array(totalCodewords); + let index = 0; + let i, r; + for (i = 0; i < maxDataSize; i++) { + for (r = 0; r < ecTotalBlocks; r++) { + if (i < dcData[r].length) { + data[index++] = dcData[r][i]; + } + } + } + for (i = 0; i < ecCount; i++) { + for (r = 0; r < ecTotalBlocks; r++) { + data[index++] = ecData[r][i]; + } + } + return data; + } + function createSymbol(data, version, errorCorrectionLevel, maskPattern) { + let segments; + if (Array.isArray(data)) { + segments = Segments.fromArray(data); + } else if (typeof data === "string") { + let estimatedVersion = version; + if (!estimatedVersion) { + const rawSegments = Segments.rawSplit(data); + estimatedVersion = Version.getBestVersionForData(rawSegments, errorCorrectionLevel); + } + segments = Segments.fromString(data, estimatedVersion || 40); + } else { + throw new Error("Invalid data"); + } + const bestVersion = Version.getBestVersionForData(segments, errorCorrectionLevel); + if (!bestVersion) { + throw new Error("The amount of data is too big to be stored in a QR Code"); + } + if (!version) { + version = bestVersion; + } else if (version < bestVersion) { + throw new Error( + "\nThe chosen QR Code version cannot contain this amount of data.\nMinimum version required to store current data is: " + bestVersion + ".\n" + ); + } + const dataBits = createData(version, errorCorrectionLevel, segments); + const moduleCount = Utils.getSymbolSize(version); + const modules = new BitMatrix(moduleCount); + setupFinderPattern(modules, version); + setupTimingPattern(modules); + setupAlignmentPattern(modules, version); + setupFormatInfo(modules, errorCorrectionLevel, 0); + if (version >= 7) { + setupVersionInfo(modules, version); + } + setupData(modules, dataBits); + if (isNaN(maskPattern)) { + maskPattern = MaskPattern.getBestMask( + modules, + setupFormatInfo.bind(null, modules, errorCorrectionLevel) + ); + } + MaskPattern.applyMask(maskPattern, modules); + setupFormatInfo(modules, errorCorrectionLevel, maskPattern); + return { + modules, + version, + errorCorrectionLevel, + maskPattern, + segments + }; + } + exports.create = function create(data, options) { + if (typeof data === "undefined" || data === "") { + throw new Error("No input text"); + } + let errorCorrectionLevel = ECLevel.M; + let version; + let mask; + if (typeof options !== "undefined") { + errorCorrectionLevel = ECLevel.from(options.errorCorrectionLevel, ECLevel.M); + version = Version.from(options.version); + mask = MaskPattern.from(options.maskPattern); + if (options.toSJISFunc) { + Utils.setToSJISFunction(options.toSJISFunc); + } + } + return createSymbol(data, version, errorCorrectionLevel, mask); + }; + } + }); + + // node_modules/qrcode/lib/renderer/utils.js + var require_utils2 = __commonJS({ + "node_modules/qrcode/lib/renderer/utils.js"(exports) { + function hex2rgba(hex) { + if (typeof hex === "number") { + hex = hex.toString(); + } + if (typeof hex !== "string") { + throw new Error("Color should be defined as hex string"); + } + let hexCode = hex.slice().replace("#", "").split(""); + if (hexCode.length < 3 || hexCode.length === 5 || hexCode.length > 8) { + throw new Error("Invalid hex color: " + hex); + } + if (hexCode.length === 3 || hexCode.length === 4) { + hexCode = Array.prototype.concat.apply([], hexCode.map(function(c) { + return [c, c]; + })); + } + if (hexCode.length === 6) hexCode.push("F", "F"); + const hexValue = parseInt(hexCode.join(""), 16); + return { + r: hexValue >> 24 & 255, + g: hexValue >> 16 & 255, + b: hexValue >> 8 & 255, + a: hexValue & 255, + hex: "#" + hexCode.slice(0, 6).join("") + }; + } + exports.getOptions = function getOptions(options) { + if (!options) options = {}; + if (!options.color) options.color = {}; + const margin = typeof options.margin === "undefined" || options.margin === null || options.margin < 0 ? 4 : options.margin; + const width = options.width && options.width >= 21 ? options.width : void 0; + const scale = options.scale || 4; + return { + width, + scale: width ? 4 : scale, + margin, + color: { + dark: hex2rgba(options.color.dark || "#000000ff"), + light: hex2rgba(options.color.light || "#ffffffff") + }, + type: options.type, + rendererOpts: options.rendererOpts || {} + }; + }; + exports.getScale = function getScale(qrSize, opts) { + return opts.width && opts.width >= qrSize + opts.margin * 2 ? opts.width / (qrSize + opts.margin * 2) : opts.scale; + }; + exports.getImageWidth = function getImageWidth(qrSize, opts) { + const scale = exports.getScale(qrSize, opts); + return Math.floor((qrSize + opts.margin * 2) * scale); + }; + exports.qrToImageData = function qrToImageData(imgData, qr, opts) { + const size = qr.modules.size; + const data = qr.modules.data; + const scale = exports.getScale(size, opts); + const symbolSize = Math.floor((size + opts.margin * 2) * scale); + const scaledMargin = opts.margin * scale; + const palette = [opts.color.light, opts.color.dark]; + for (let i = 0; i < symbolSize; i++) { + for (let j = 0; j < symbolSize; j++) { + let posDst = (i * symbolSize + j) * 4; + let pxColor = opts.color.light; + if (i >= scaledMargin && j >= scaledMargin && i < symbolSize - scaledMargin && j < symbolSize - scaledMargin) { + const iSrc = Math.floor((i - scaledMargin) / scale); + const jSrc = Math.floor((j - scaledMargin) / scale); + pxColor = palette[data[iSrc * size + jSrc] ? 1 : 0]; + } + imgData[posDst++] = pxColor.r; + imgData[posDst++] = pxColor.g; + imgData[posDst++] = pxColor.b; + imgData[posDst] = pxColor.a; + } + } + }; + } + }); + + // node_modules/qrcode/lib/renderer/canvas.js + var require_canvas = __commonJS({ + "node_modules/qrcode/lib/renderer/canvas.js"(exports) { + var Utils = require_utils2(); + function clearCanvas(ctx, canvas, size) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (!canvas.style) canvas.style = {}; + canvas.height = size; + canvas.width = size; + canvas.style.height = size + "px"; + canvas.style.width = size + "px"; + } + function getCanvasElement() { + try { + return document.createElement("canvas"); + } catch (e) { + throw new Error("You need to specify a canvas element"); + } + } + exports.render = function render(qrData, canvas, options) { + let opts = options; + let canvasEl = canvas; + if (typeof opts === "undefined" && (!canvas || !canvas.getContext)) { + opts = canvas; + canvas = void 0; + } + if (!canvas) { + canvasEl = getCanvasElement(); + } + opts = Utils.getOptions(opts); + const size = Utils.getImageWidth(qrData.modules.size, opts); + const ctx = canvasEl.getContext("2d"); + const image = ctx.createImageData(size, size); + Utils.qrToImageData(image.data, qrData, opts); + clearCanvas(ctx, canvasEl, size); + ctx.putImageData(image, 0, 0); + return canvasEl; + }; + exports.renderToDataURL = function renderToDataURL(qrData, canvas, options) { + let opts = options; + if (typeof opts === "undefined" && (!canvas || !canvas.getContext)) { + opts = canvas; + canvas = void 0; + } + if (!opts) opts = {}; + const canvasEl = exports.render(qrData, canvas, opts); + const type = opts.type || "image/png"; + const rendererOpts = opts.rendererOpts || {}; + return canvasEl.toDataURL(type, rendererOpts.quality); + }; + } + }); + + // node_modules/qrcode/lib/renderer/svg-tag.js + var require_svg_tag = __commonJS({ + "node_modules/qrcode/lib/renderer/svg-tag.js"(exports) { + var Utils = require_utils2(); + function getColorAttrib(color, attrib) { + const alpha = color.a / 255; + const str = attrib + '="' + color.hex + '"'; + return alpha < 1 ? str + " " + attrib + '-opacity="' + alpha.toFixed(2).slice(1) + '"' : str; + } + function svgCmd(cmd, x, y) { + let str = cmd + x; + if (typeof y !== "undefined") str += " " + y; + return str; + } + function qrToPath(data, size, margin) { + let path = ""; + let moveBy = 0; + let newRow = false; + let lineLength = 0; + for (let i = 0; i < data.length; i++) { + const col = Math.floor(i % size); + const row = Math.floor(i / size); + if (!col && !newRow) newRow = true; + if (data[i]) { + lineLength++; + if (!(i > 0 && col > 0 && data[i - 1])) { + path += newRow ? svgCmd("M", col + margin, 0.5 + row + margin) : svgCmd("m", moveBy, 0); + moveBy = 0; + newRow = false; + } + if (!(col + 1 < size && data[i + 1])) { + path += svgCmd("h", lineLength); + lineLength = 0; + } + } else { + moveBy++; + } + } + return path; + } + exports.render = function render(qrData, options, cb) { + const opts = Utils.getOptions(options); + const size = qrData.modules.size; + const data = qrData.modules.data; + const qrcodesize = size + opts.margin * 2; + const bg = !opts.color.light.a ? "" : "'; + const path = "'; + const viewBox = 'viewBox="0 0 ' + qrcodesize + " " + qrcodesize + '"'; + const width = !opts.width ? "" : 'width="' + opts.width + '" height="' + opts.width + '" '; + const svgTag = '' + bg + path + "\n"; + if (typeof cb === "function") { + cb(null, svgTag); + } + return svgTag; + }; + } + }); + + // node_modules/qrcode/lib/browser.js + var require_browser = __commonJS({ + "node_modules/qrcode/lib/browser.js"(exports) { + var canPromise = require_can_promise(); + var QRCode2 = require_qrcode(); + var CanvasRenderer = require_canvas(); + var SvgRenderer = require_svg_tag(); + function renderCanvas(renderFunc, canvas, text, opts, cb) { + const args = [].slice.call(arguments, 1); + const argsNum = args.length; + const isLastArgCb = typeof args[argsNum - 1] === "function"; + if (!isLastArgCb && !canPromise()) { + throw new Error("Callback required as last argument"); + } + if (isLastArgCb) { + if (argsNum < 2) { + throw new Error("Too few arguments provided"); + } + if (argsNum === 2) { + cb = text; + text = canvas; + canvas = opts = void 0; + } else if (argsNum === 3) { + if (canvas.getContext && typeof cb === "undefined") { + cb = opts; + opts = void 0; + } else { + cb = opts; + opts = text; + text = canvas; + canvas = void 0; + } + } + } else { + if (argsNum < 1) { + throw new Error("Too few arguments provided"); + } + if (argsNum === 1) { + text = canvas; + canvas = opts = void 0; + } else if (argsNum === 2 && !canvas.getContext) { + opts = text; + text = canvas; + canvas = void 0; + } + return new Promise(function(resolve, reject) { + try { + const data = QRCode2.create(text, opts); + resolve(renderFunc(data, canvas, opts)); + } catch (e) { + reject(e); + } + }); + } + try { + const data = QRCode2.create(text, opts); + cb(null, renderFunc(data, canvas, opts)); + } catch (e) { + cb(e); + } + } + exports.create = QRCode2.create; + exports.toCanvas = renderCanvas.bind(null, CanvasRenderer.render); + exports.toDataURL = renderCanvas.bind(null, CanvasRenderer.renderToDataURL); + exports.toString = renderCanvas.bind(null, function(data, _, opts) { + return SvgRenderer.render(data, opts); + }); + } + }); + + // app/web/admin/shared/constants.js + var LS_TOKEN = "admin_access_token"; + var PAGE_SIZE = 50; + var DEFAULT_FORM_FIELD_TYPES = ["string", "text", "number", "boolean", "date"]; + var ALL_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "~"]; + var OPERATOR_LABELS = { + "=": "=", + "!=": "!=", + ">": ">", + "<": "<", + ">=": ">=", + "<=": "<=", + "~": "~" + }; + var ROLE_LABELS = { + ADMIN: "\u0410\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440", + LAWYER: "\u042E\u0440\u0438\u0441\u0442", + CURATOR: "\u041A\u0443\u0440\u0430\u0442\u043E\u0440" + }; + var STATUS_LABELS = { + NEW: "\u041D\u043E\u0432\u0430\u044F", + IN_PROGRESS: "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435", + WAITING_CLIENT: "\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u043A\u043B\u0438\u0435\u043D\u0442\u0430", + WAITING_COURT: "\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0441\u0443\u0434\u0430", + RESOLVED: "\u0420\u0435\u0448\u0435\u043D\u0430", + CLOSED: "\u0417\u0430\u043A\u0440\u044B\u0442\u0430", + REJECTED: "\u041E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u0430" + }; + var INVOICE_STATUS_LABELS = { + WAITING_PAYMENT: "\u041E\u0436\u0438\u0434\u0430\u0435\u0442 \u043E\u043F\u043B\u0430\u0442\u0443", + PAID: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D", + CANCELED: "\u041E\u0442\u043C\u0435\u043D\u0435\u043D" + }; + var STATUS_KIND_LABELS = { + DEFAULT: "\u041E\u0431\u044B\u0447\u043D\u044B\u0439", + INVOICE: "\u0412\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u0447\u0435\u0442\u0430", + PAID: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D\u043E" + }; + var REQUEST_UPDATE_EVENT_LABELS = { + MESSAGE: "\u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435", + ATTACHMENT: "\u0444\u0430\u0439\u043B", + REQUEST_DATA: "\u0434\u0430\u043D\u043D\u044B\u0435", + ASSIGNMENT: "\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435", + REASSIGNMENT: "\u043F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435", + STATUS: "\u0441\u0442\u0430\u0442\u0443\u0441" + }; + var SERVICE_REQUEST_TYPE_LABELS = { + CURATOR_CONTACT: "\u0417\u0430\u043F\u0440\u043E\u0441 \u043A \u043A\u0443\u0440\u0430\u0442\u043E\u0440\u0443", + LAWYER_CHANGE_REQUEST: "\u0421\u043C\u0435\u043D\u0430 \u044E\u0440\u0438\u0441\u0442\u0430" + }; + var SERVICE_REQUEST_STATUS_LABELS = { + NEW: "\u041D\u043E\u0432\u044B\u0439", + IN_PROGRESS: "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435", + RESOLVED: "\u0420\u0435\u0448\u0435\u043D", + REJECTED: "\u041E\u0442\u043A\u043B\u043E\u043D\u0435\u043D" + }; + var KANBAN_GROUPS = [ + { key: "NEW", label: "\u041D\u043E\u0432\u044B\u0435" }, + { key: "IN_PROGRESS", label: "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435" }, + { key: "WAITING", label: "\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435" }, + { key: "DONE", label: "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u044B" } + ]; + var TABLE_SERVER_CONFIG = { + requests: { + table: "requests", + // Requests use a specialized endpoint because it supports virtual/server-side filters + // (e.g. deadline alerts and unread notifications) that are not plain table columns. + endpoint: "/api/admin/requests/query", + sort: [{ field: "created_at", dir: "desc" }] + }, + serviceRequests: { + table: "request_service_requests", + endpoint: "/api/admin/crud/request_service_requests/query", + sort: [{ field: "created_at", dir: "desc" }] + }, + invoices: { + table: "invoices", + endpoint: "/api/admin/invoices/query", + sort: [{ field: "issued_at", dir: "desc" }] + }, + quotes: { + table: "quotes", + endpoint: "/api/admin/crud/quotes/query", + sort: [{ field: "sort_order", dir: "asc" }] + }, + topics: { + table: "topics", + endpoint: "/api/admin/crud/topics/query", + sort: [{ field: "sort_order", dir: "asc" }] + }, + statuses: { + table: "statuses", + endpoint: "/api/admin/crud/statuses/query", + sort: [{ field: "sort_order", dir: "asc" }] + }, + formFields: { + table: "form_fields", + endpoint: "/api/admin/crud/form_fields/query", + sort: [{ field: "sort_order", dir: "asc" }] + }, + topicRequiredFields: { + table: "topic_required_fields", + endpoint: "/api/admin/crud/topic_required_fields/query", + sort: [{ field: "sort_order", dir: "asc" }] + }, + topicDataTemplates: { + table: "topic_data_templates", + endpoint: "/api/admin/crud/topic_data_templates/query", + sort: [{ field: "sort_order", dir: "asc" }] + }, + statusTransitions: { + table: "topic_status_transitions", + endpoint: "/api/admin/crud/topic_status_transitions/query", + sort: [{ field: "sort_order", dir: "asc" }] + }, + users: { + table: "admin_users", + endpoint: "/api/admin/crud/admin_users/query", + sort: [{ field: "created_at", dir: "desc" }] + }, + userTopics: { + table: "admin_user_topics", + endpoint: "/api/admin/crud/admin_user_topics/query", + sort: [{ field: "created_at", dir: "desc" }] + } + }; + var TABLE_MUTATION_CONFIG = Object.fromEntries( + Object.entries(TABLE_SERVER_CONFIG).map(([tableKey, config]) => [ + tableKey, + { + create: "/api/admin/crud/" + config.table, + update: (id) => "/api/admin/crud/" + config.table + "/" + id, + delete: (id) => "/api/admin/crud/" + config.table + "/" + id + } + ]) + ); + TABLE_MUTATION_CONFIG.invoices = { + create: "/api/admin/invoices", + update: (id) => "/api/admin/invoices/" + id, + delete: (id) => "/api/admin/invoices/" + id + }; + var TABLE_KEY_ALIASES = { + request_service_requests: "serviceRequests", + form_fields: "formFields", + status_groups: "statusGroups", + topic_required_fields: "topicRequiredFields", + topic_data_templates: "topicDataTemplates", + topic_status_transitions: "statusTransitions", + admin_users: "users", + admin_user_topics: "userTopics" + }; + var TABLE_UNALIASES = Object.fromEntries(Object.entries(TABLE_KEY_ALIASES).map(([table, alias]) => [alias, table])); + var KNOWN_CONFIG_TABLE_KEYS = /* @__PURE__ */ new Set([ + "quotes", + "topics", + "statuses", + "formFields", + "topicRequiredFields", + "topicDataTemplates", + "statusTransitions", + "users", + "userTopics" + ]); + + // app/web/admin/shared/state.js + function createTableState() { + return { + filters: [], + sort: null, + offset: 0, + total: 0, + showAll: false, + rows: [] + }; + } + function createRequestModalState() { + return { + loading: false, + requestId: null, + trackNumber: "", + requestData: null, + financeSummary: null, + invoices: [], + statusRouteNodes: [], + statusHistory: [], + availableStatuses: [], + currentImportantDateAt: "", + pendingStatusChangePreset: null, + messages: [], + attachments: [], + messageDraft: "", + selectedFiles: [], + fileUploading: false + }; + } + + // app/web/admin/shared/icons.jsx + function RefreshIcon() { + return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M21 12a9 9 0 1 1-2.64-6.36" }), /* @__PURE__ */ React.createElement("polyline", { points: "21 3 21 9 15 9" })); + } + function FilterIcon() { + return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M3 5h18l-7 8v5l-4 2v-7z" })); + } + function AddIcon() { + return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M12 5v14" }), /* @__PURE__ */ React.createElement("path", { d: "M5 12h14" })); + } + function PrevIcon() { + return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M15 18l-6-6 6-6" })); + } + function NextIcon() { + return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M9 18l6-6-6-6" })); + } + function DownloadIcon() { + return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M12 4v11" }), /* @__PURE__ */ React.createElement("path", { d: "M8 11l4 4 4-4" }), /* @__PURE__ */ React.createElement("path", { d: "M5 20h14" })); + } + + // app/web/admin/shared/utils.js + function resolveAdminRoute(search) { + const params = new URLSearchParams(String(search || "")); + const section = String(params.get("section") || "").trim(); + const view = String(params.get("view") || "").trim(); + const requestId = String(params.get("requestId") || "").trim(); + return { section, view, requestId }; + } + function humanizeKey(value) { + const text = String(value || "").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim(); + if (!text) return "-"; + return text.charAt(0).toUpperCase() + text.slice(1); + } + function metaKindToFilterType(kind) { + if (kind === "boolean") return "boolean"; + if (kind === "number") return "number"; + if (kind === "date" || kind === "datetime") return "date"; + return "text"; + } + function metaKindToRecordType(kind) { + if (kind === "boolean") return "boolean"; + if (kind === "number") return "number"; + if (kind === "json") return "json"; + return "text"; + } + function decodeJwtPayload(token) { + try { + const payload = token.split(".")[1] || ""; + const base64 = payload.replace(/-/g, "+").replace(/_/g, "/"); + const json = decodeURIComponent( + atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("") + ); + return JSON.parse(json); + } catch (_) { + return null; + } + } + function sortByName(items) { + return [...items].sort((a, b) => String(a.name || a.code || "").localeCompare(String(b.name || b.code || ""), "ru")); + } + function roleLabel(role) { + return ROLE_LABELS[role] || role || "-"; + } + function statusLabel(code) { + return STATUS_LABELS[code] || code || "-"; + } + function invoiceStatusLabel(code) { + return INVOICE_STATUS_LABELS[code] || code || "-"; + } + function statusKindLabel(code) { + return STATUS_KIND_LABELS[code] || code || "-"; + } + function fallbackStatusGroup(statusCode) { + const code = String(statusCode || "").toUpperCase(); + if (!code) return "NEW"; + if (code.startsWith("NEW")) return "NEW"; + if (code.includes("WAIT") || code.includes("PEND") || code.includes("HOLD")) return "WAITING"; + if (code.includes("CLOSE") || code.includes("RESOLV") || code.includes("REJECT") || code.includes("DONE") || code.includes("PAID")) return "DONE"; + return "IN_PROGRESS"; + } + function boolLabel(value) { + return value ? "\u0414\u0430" : "\u041D\u0435\u0442"; + } + function boolFilterLabel(value) { + return value ? "True" : "False"; + } + function fmtDate(value) { + if (!value) return "-"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return String(value); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = String(date.getFullYear()).slice(-2); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + return `${day}.${month}.${year} ${hours}:${minutes}`; + } + function fmtDateOnly(value) { + if (!value) return "-"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return String(value); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = String(date.getFullYear()).slice(-2); + return `${day}.${month}.${year}`; + } + function fmtTimeOnly(value) { + if (!value) return "-"; + const date = new Date(value); + return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }); + } + function fmtKanbanDate(value) { + if (!value) return "-"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return String(value); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = String(date.getFullYear()).slice(-2); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + return `${day}.${month}.${year} ${hours}:${minutes}`; + } + function fmtShortDateTime(value) { + if (!value) return "-"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return String(value); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = String(date.getFullYear()).slice(-2); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + return `${day}.${month}.${year} ${hours}:${minutes}`; + } + function resolveDeadlineTone(value) { + if (!value) return "ok"; + const time = new Date(value).getTime(); + if (!Number.isFinite(time)) return "ok"; + const delta = time - Date.now(); + const fourDaysMs = 4 * 24 * 60 * 60 * 1e3; + const oneDayMs = 24 * 60 * 60 * 1e3; + if (delta > fourDaysMs) return "ok"; + if (delta > oneDayMs) return "warn"; + return "danger"; + } + function fmtAmount(value) { + if (value == null || value === "") return "-"; + const number = Number(value); + if (Number.isNaN(number)) return String(value); + return number.toLocaleString("ru-RU"); + } + function fmtBytes(value) { + const size = Number(value || 0); + if (!Number.isFinite(size) || size <= 0) return "0 \u0411"; + const units = ["\u0411", "\u041A\u0411", "\u041C\u0411", "\u0413\u0411"]; + let normalized = size; + let index = 0; + while (normalized >= 1024 && index < units.length - 1) { + normalized /= 1024; + index += 1; + } + return normalized.toLocaleString("ru-RU", { maximumFractionDigits: index === 0 ? 0 : 1 }) + " " + units[index]; + } + function normalizeStringList(value) { + if (!Array.isArray(value)) return []; + const out = []; + const seen = /* @__PURE__ */ new Set(); + value.forEach((item) => { + const text = String(item || "").trim(); + if (!text) return; + const key = text.toLowerCase(); + if (seen.has(key)) return; + seen.add(key); + out.push(text); + }); + return out; + } + function listPreview(value, emptyLabel) { + const items = normalizeStringList(value); + return items.length ? items.join(", ") : emptyLabel; + } + function normalizeReferenceMeta(raw) { + if (!raw || typeof raw !== "object") return null; + const table = String(raw.table || "").trim(); + const valueField = String(raw.value_field || "id").trim() || "id"; + const labelField = String(raw.label_field || valueField).trim() || valueField; + if (!table) return null; + return { table, value_field: valueField, label_field: labelField }; + } + function userInitials(name, email) { + const source = String(name || "").trim(); + if (source) { + const parts = source.split(/\s+/).filter(Boolean); + if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase(); + return source.slice(0, 2).toUpperCase(); + } + const mail = String(email || "").trim(); + return (mail.slice(0, 2) || "U").toUpperCase(); + } + function avatarColor(seed) { + const palette = ["#6f8fa9", "#568f7d", "#a07a5c", "#7d6ea9", "#8f6f8f", "#7f8c5a"]; + const text = String(seed || ""); + let hash = 0; + for (let i = 0; i < text.length; i += 1) hash = hash * 31 + text.charCodeAt(i) >>> 0; + return palette[hash % palette.length]; + } + function resolveAvatarSrc(avatarUrl, accessToken) { + const raw = String(avatarUrl || "").trim(); + if (!raw) return ""; + if (raw.startsWith("s3://")) { + const key = raw.slice("s3://".length); + if (!key || !accessToken) return ""; + return "/api/admin/uploads/object/" + encodeURIComponent(key) + "?token=" + encodeURIComponent(accessToken); + } + return raw; + } + function resolveAdminObjectSrc(s3Key, accessToken) { + const key = String(s3Key || "").trim(); + if (!key || !accessToken) return ""; + return "/api/admin/uploads/object/" + encodeURIComponent(key) + "?token=" + encodeURIComponent(accessToken); + } + function detectAttachmentPreviewKind(fileName, mimeType) { + const name = String(fileName || "").toLowerCase(); + const mime = String(mimeType || "").toLowerCase(); + if (/\.(txt|md|csv|json|log|xml|ya?ml|ini|cfg)$/i.test(name)) return "text"; + if (mime.startsWith("text/") || mime === "application/json" || mime === "application/xml" || mime === "text/xml") { + return "text"; + } + if (mime.startsWith("image/") || /\.(png|jpe?g|gif|webp|bmp|svg)$/.test(name)) return "image"; + if (mime.startsWith("video/") || /\.(mp4|webm|ogg|mov|m4v)$/.test(name)) return "video"; + if (mime === "application/pdf" || /\.pdf$/.test(name)) return "pdf"; + return "none"; + } + function buildUniversalQuery(filters, sort, limit, offset) { + return { + filters: filters || [], + sort: sort || [], + page: { limit: limit != null ? limit : 50, offset: offset != null ? offset : 0 } + }; + } + function canAccessSection(role, section) { + const roleCode = String(role || "").toUpperCase(); + const allowed = /* @__PURE__ */ new Set([ + "dashboard", + "kanban", + "requests", + "serviceRequests", + "requestWorkspace", + "invoices", + "meta", + "quotes", + "config", + "availableTables" + ]); + if (!allowed.has(section)) return false; + if (section === "requests") return roleCode === "ADMIN"; + if (section === "serviceRequests") return roleCode === "ADMIN" || roleCode === "CURATOR"; + if (section === "quotes" || section === "config" || section === "availableTables") return roleCode === "ADMIN"; + return true; + } + function translateApiError(message) { + const direct = { + "Missing auth token": "\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438", + "Missing bearer token": "\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438", + "Invalid token": "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u0442\u043E\u043A\u0435\u043D", + Forbidden: "\u041D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E \u043F\u0440\u0430\u0432", + "Invalid credentials": "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043B\u043E\u0433\u0438\u043D \u0438\u043B\u0438 \u043F\u0430\u0440\u043E\u043B\u044C", + "Request not found": "\u0417\u0430\u044F\u0432\u043A\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430", + "Quote not found": "\u0426\u0438\u0442\u0430\u0442\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430", + not_found: "\u0417\u0430\u043F\u0438\u0441\u044C \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430" + }; + if (direct[message]) return direct[message]; + if (String(message).startsWith("HTTP ")) return "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (" + message + ")"; + return message; + } + function getOperatorsForType(type) { + if (type === "number" || type === "date" || type === "datetime") return ["=", "!=", ">", "<", ">=", "<="]; + if (type === "boolean" || type === "reference" || type === "enum") return ["=", "!="]; + return [...ALL_OPERATORS]; + } + function localizeMeta(data) { + const fieldTypeMap = { + string: "\u0441\u0442\u0440\u043E\u043A\u0430", + text: "\u0442\u0435\u043A\u0441\u0442", + boolean: "\u0431\u0443\u043B\u0435\u0432\u043E", + number: "\u0447\u0438\u0441\u043B\u043E", + date: "\u0434\u0430\u0442\u0430" + }; + return { + \u0421\u0443\u0449\u043D\u043E\u0441\u0442\u044C: data.entity, + \u041F\u043E\u043B\u044F: (data.fields || []).map((field) => ({ + "\u041A\u043E\u0434 \u043F\u043E\u043B\u044F": field.field_name, + \u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435: field.label, + \u0422\u0438\u043F: fieldTypeMap[field.type] || field.type, + \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435: boolLabel(field.required), + "\u0422\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u0435\u043D\u0438\u0435": boolLabel(field.read_only), + "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0443\u0435\u043C\u044B\u0435 \u0440\u043E\u043B\u0438": (field.editable_roles || []).map(roleLabel) + })) + }; + } + + // app/web/admin/features/kanban/KanbanBoard.jsx + function KanbanBoard({ + loading, + columns, + rows, + role, + actorId, + filters, + onRefresh, + onOpenFilter, + onRemoveFilter, + onEditFilter, + getFilterChipLabel, + onOpenSort, + sortActive, + onOpenRequest, + onClaimRequest, + onMoveRequest, + status, + FilterToolbarComponent, + StatusLineComponent + }) { + const { useMemo, useState } = React; + const [draggingId, setDraggingId] = useState(""); + const [dragOverGroup, setDragOverGroup] = useState(""); + const safeColumns = Array.isArray(columns) && columns.length ? columns : KANBAN_GROUPS; + const grouped = useMemo(() => { + const map = {}; + safeColumns.forEach((column) => { + map[String(column.key)] = []; + }); + (rows || []).forEach((row) => { + const group = String((row == null ? void 0 : row.status_group) || fallbackStatusGroup(row == null ? void 0 : row.status_code)); + if (!map[group]) map[group] = []; + map[group].push(row); + }); + return map; + }, [rows, safeColumns]); + const rowMap = useMemo(() => { + const map = /* @__PURE__ */ new Map(); + (rows || []).forEach((row) => { + if (!(row == null ? void 0 : row.id)) return; + map.set(String(row.id), row); + }); + return map; + }, [rows]); + const onDropToGroup = (event, groupKey) => { + event.preventDefault(); + const requestId = String(event.dataTransfer.getData("text/plain") || draggingId || ""); + setDragOverGroup(""); + setDraggingId(""); + if (!requestId) return; + const row = rowMap.get(requestId); + if (!row) return; + onMoveRequest(row, String(groupKey || "")); + }; + const FilterToolbar = FilterToolbarComponent; + const StatusLine = StatusLineComponent; + return /* @__PURE__ */ React.createElement("div", { className: "kanban-wrap" }, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u041A\u0430\u043D\u0431\u0430\u043D \u0437\u0430\u044F\u0432\u043E\u043A"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0413\u0440\u0443\u043F\u043F\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E \u0433\u0440\u0443\u043F\u043F\u0430\u043C \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432 \u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u0430\u044F \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u0446\u0438\u044F \u043A\u0430\u0440\u0442\u043E\u0447\u0435\u043A.")), /* @__PURE__ */ React.createElement("div", { className: "section-head-actions" }, /* @__PURE__ */ React.createElement("button", { className: "btn secondary" + (sortActive ? " active-success" : ""), type: "button", onClick: onOpenSort }, "\u0421\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onRefresh, disabled: loading, title: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C", "aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(RefreshIcon, null)), /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onOpenFilter, title: "\u0424\u0438\u043B\u044C\u0442\u0440", "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" }, /* @__PURE__ */ React.createElement(FilterIcon, null)))), FilterToolbar ? /* @__PURE__ */ React.createElement( + FilterToolbar, + { + filters: filters || [], + onOpen: onOpenFilter, + onRemove: onRemoveFilter, + onEdit: onEditFilter, + hideAction: true, + getChipLabel: getFilterChipLabel + } + ) : null, /* @__PURE__ */ React.createElement("div", { className: "kanban-board", id: "kanban-board" }, safeColumns.map((column) => { + var _a; + const key = String(column.key || ""); + const cards = grouped[key] || []; + const isOver = dragOverGroup === key; + return /* @__PURE__ */ React.createElement( + "div", + { + key, + className: "kanban-column" + (isOver ? " drag-over" : ""), + onDragOver: (event) => { + event.preventDefault(); + setDragOverGroup(key); + }, + onDragLeave: (event) => { + if (event.currentTarget.contains(event.relatedTarget)) return; + setDragOverGroup((prev) => prev === key ? "" : prev); + }, + onDrop: (event) => onDropToGroup(event, key) + }, + /* @__PURE__ */ React.createElement("div", { className: "kanban-column-head" }, /* @__PURE__ */ React.createElement("b", null, column.label || key), /* @__PURE__ */ React.createElement("span", null, Number((_a = column.total) != null ? _a : cards.length))), + /* @__PURE__ */ React.createElement("div", { className: "kanban-column-body" }, cards.length ? cards.map((row) => { + const requestId = String(row.id || ""); + const isUnassigned = !String(row.assigned_lawyer_id || "").trim(); + const canClaim = role === "LAWYER" && isUnassigned; + const canMove = role === "ADMIN" || !isUnassigned && String(row.assigned_lawyer_id || "").trim() === String(actorId || "").trim(); + const transitionOptions = Array.isArray(row.available_transitions) ? row.available_transitions : []; + const deadline = row.sla_deadline_at || row.case_deadline_at || ""; + const deadlineTone = resolveDeadlineTone(deadline); + const unreadTypes = /* @__PURE__ */ new Set(); + if (role === "LAWYER") { + if (row.lawyer_has_unread_updates && row.lawyer_unread_event_type) unreadTypes.add(String(row.lawyer_unread_event_type).toUpperCase()); + } else { + if (row.client_has_unread_updates && row.client_unread_event_type) unreadTypes.add(String(row.client_unread_event_type).toUpperCase()); + if (row.lawyer_has_unread_updates && row.lawyer_unread_event_type) unreadTypes.add(String(row.lawyer_unread_event_type).toUpperCase()); + } + const hasUnreadMessage = unreadTypes.has("MESSAGE"); + const hasUnreadAttachment = unreadTypes.has("ATTACHMENT"); + return /* @__PURE__ */ React.createElement( + "article", + { + key: requestId, + className: "kanban-card" + (canMove ? " draggable" : ""), + draggable: canMove, + role: "button", + tabIndex: 0, + onClick: (event) => onOpenRequest(requestId, event), + onKeyDown: (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onOpenRequest(requestId, event); + } + }, + onDragStart: (event) => { + if (!canMove) { + event.preventDefault(); + return; + } + setDraggingId(requestId); + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", requestId); + }, + onDragEnd: () => { + setDraggingId(""); + setDragOverGroup(""); + } + }, + /* @__PURE__ */ React.createElement("div", { className: "kanban-card-head" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "request-track-link", + onClick: (event) => { + event.stopPropagation(); + onOpenRequest(requestId, event); + }, + title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443" + }, + /* @__PURE__ */ React.createElement("code", null, row.track_number || "-") + ), /* @__PURE__ */ React.createElement("span", { className: "kanban-status-badge group-" + String(row.status_group || "").toLowerCase() }, row.status_name || statusLabel(row.status_code))), + /* @__PURE__ */ React.createElement("p", { className: "kanban-card-desc" }, String(row.description || "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E")), + /* @__PURE__ */ React.createElement("div", { className: "kanban-card-meta" }, /* @__PURE__ */ React.createElement("span", null, row.client_name || "-"), /* @__PURE__ */ React.createElement("span", null, fmtKanbanDate(row.created_at))), + /* @__PURE__ */ React.createElement("div", { className: "kanban-card-meta" }, /* @__PURE__ */ React.createElement("span", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("span", null, row.assigned_lawyer_name || (isUnassigned ? "\u041D\u0435 \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043E" : row.assigned_lawyer_id || "-"))), + /* @__PURE__ */ React.createElement("div", { className: "kanban-card-meta" }, /* @__PURE__ */ React.createElement("div", { className: "kanban-update-icons" }, /* @__PURE__ */ React.createElement("span", { className: "kanban-update-icon" + (hasUnreadMessage ? " is-unread" : ""), title: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F" }, "\u{1F4AC}"), /* @__PURE__ */ React.createElement("span", { className: "kanban-update-icon" + (hasUnreadAttachment ? " is-unread" : ""), title: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B" }, "\u{1F4CE}")), /* @__PURE__ */ React.createElement("span", { className: "kanban-deadline-chip tone-" + deadlineTone }, deadline ? fmtKanbanDate(deadline) : "\u2014")), + /* @__PURE__ */ React.createElement( + "div", + { + className: "kanban-card-actions", + onClick: (event) => event.stopPropagation(), + onMouseDown: (event) => event.stopPropagation() + }, + canClaim ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary btn-sm", type: "button", onClick: () => onClaimRequest(requestId) }, "\u0412\u0437\u044F\u0442\u044C \u0432 \u0440\u0430\u0431\u043E\u0442\u0443") : null, + canMove && transitionOptions.length ? /* @__PURE__ */ React.createElement( + "select", + { + className: "kanban-transition-select", + defaultValue: "", + onClick: (event) => event.stopPropagation(), + onChange: (event) => { + const targetStatus = String(event.target.value || ""); + if (!targetStatus) return; + onMoveRequest(row, "", targetStatus); + event.target.value = ""; + } + }, + /* @__PURE__ */ React.createElement("option", { value: "" }, "\u041F\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438\u2026"), + transitionOptions.map((transition) => /* @__PURE__ */ React.createElement("option", { key: String(transition.to_status), value: String(transition.to_status) }, String(transition.to_status_name || transition.to_status))) + ) : null + ) + ); + }) : /* @__PURE__ */ React.createElement("p", { className: "muted kanban-empty" }, "\u041F\u0443\u0441\u0442\u043E")) + ); + })), StatusLine ? /* @__PURE__ */ React.createElement(StatusLine, { status }) : null); + } + + // app/web/admin/features/config/ConfigSection.jsx + function fmtBalance(value) { + const number = Number(value); + if (!Number.isFinite(number)) return "-"; + return number.toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " \u20BD"; + } + function smsBalanceSummary(health) { + if (!health || typeof health !== "object") return "\u0411\u0430\u043B\u0430\u043D\u0441 SMS Aero: \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430..."; + const provider = String(health.provider || "").toLowerCase(); + if (provider !== "smsaero") { + return "SMS \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440: " + String(health.provider || "-") + " (\u0431\u0430\u043B\u0430\u043D\u0441 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D)"; + } + if (health.balance_available) { + return "\u0411\u0430\u043B\u0430\u043D\u0441 SMS Aero: " + fmtBalance(health.balance_amount); + } + const issues = Array.isArray(health.issues) ? health.issues.filter(Boolean) : []; + return "\u0411\u0430\u043B\u0430\u043D\u0441 SMS Aero \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" + (issues.length ? " \u2022 " + String(issues[0]) : ""); + } + function ConfigSection(props) { + var _a; + const { + token, + tables, + dictionaries, + configActiveKey, + activeConfigTableState, + activeConfigMeta, + genericConfigHeaders, + canCreateInConfig, + canUpdateInConfig, + canDeleteInConfig, + statusDesignerTopicCode, + statusDesignerCards, + getTableLabel, + getFieldDef, + getFilterValuePreview, + resolveReferenceLabel, + resolveTableConfig, + getStatus, + loadCurrentConfigTable, + onRefreshSmsProviderHealth, + smsProviderHealth, + openCreateRecordModal, + openFilterModal, + removeFilterChip, + openFilterEditModal, + toggleTableSort, + openEditRecordModal, + deleteRecord, + loadStatusDesignerTopic, + openCreateStatusTransitionForTopic, + loadPrevPage, + loadNextPage, + loadAllRows, + FilterToolbarComponent, + DataTableComponent, + StatusLineComponent, + IconButtonComponent, + UserAvatarComponent + } = props; + const FilterToolbar = FilterToolbarComponent; + const DataTable = DataTableComponent; + const StatusLine = StatusLineComponent; + const IconButton = IconButtonComponent; + const UserAvatar = UserAvatarComponent; + const statusRouteLabel = (code) => resolveReferenceLabel({ table: "statuses", value_field: "code", label_field: "name" }, code); + const canRefresh = Boolean(configActiveKey); + const canCreateRecord = Boolean(canCreateInConfig && configActiveKey); + const canLoadAllRows = Boolean( + configActiveKey && activeConfigTableState.total > 0 && !activeConfigTableState.showAll && activeConfigTableState.rows.length < activeConfigTableState.total + ); + const canLoadPrev = Boolean(configActiveKey && !activeConfigTableState.showAll && activeConfigTableState.offset > 0); + const canLoadNext = Boolean( + configActiveKey && !activeConfigTableState.showAll && activeConfigTableState.offset + PAGE_SIZE < activeConfigTableState.total + ); + return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0421\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "breadcrumbs" }, configActiveKey ? getTableLabel(configActiveKey) : "\u0421\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D"), configActiveKey === "otp_sessions" ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, smsBalanceSummary(smsProviderHealth), (smsProviderHealth == null ? void 0 : smsProviderHealth.loaded_at) ? " \u2022 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043E " + fmtDate(smsProviderHealth.loaded_at) : "") : null), /* @__PURE__ */ React.createElement("div", { className: "config-head-actions" }, configActiveKey === "otp_sessions" ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onRefreshSmsProviderHealth }, "\u0411\u0430\u043B\u0430\u043D\u0441") : null)), /* @__PURE__ */ React.createElement("div", { className: "config-layout" }, /* @__PURE__ */ React.createElement("div", { className: "config-panel config-panel-flat" }, /* @__PURE__ */ React.createElement("div", { className: "config-content" }, /* @__PURE__ */ React.createElement("div", { className: "config-floating-actions" }, /* @__PURE__ */ React.createElement( + "button", + { + className: "btn secondary table-control-btn", + type: "button", + onClick: () => openCreateRecordModal(configActiveKey), + disabled: !canCreateRecord, + title: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C", + "aria-label": "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C" + }, + /* @__PURE__ */ React.createElement(AddIcon, null) + ), /* @__PURE__ */ React.createElement( + "button", + { + className: "btn secondary table-control-btn", + type: "button", + onClick: () => openFilterModal(configActiveKey), + disabled: !configActiveKey, + title: "\u0424\u0438\u043B\u044C\u0442\u0440", + "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" + }, + /* @__PURE__ */ React.createElement(FilterIcon, null) + )), /* @__PURE__ */ React.createElement( + FilterToolbar, + { + filters: activeConfigTableState.filters, + onOpen: () => openFilterModal(configActiveKey), + onRemove: (index) => removeFilterChip(configActiveKey, index), + onEdit: (index) => openFilterEditModal(configActiveKey, index), + hideAction: true, + getChipLabel: (clause) => { + const fieldDef = getFieldDef(configActiveKey, clause.field); + return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview(configActiveKey, clause); + } + } + ), configActiveKey === "topics" ? /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "code", label: "\u041A\u043E\u0434", sortable: true, field: "code" }, + { key: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", sortable: true, field: "name" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", sortable: true, field: "enabled" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tables.topics.rows, + emptyColspan: 5, + onSort: (field) => toggleTableSort("topics", field), + sortClause: tables.topics.sort && tables.topics.sort[0] || TABLE_SERVER_CONFIG.topics.sort[0], + renderRow: (row) => { + var _a2; + return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.code || "-")), /* @__PURE__ */ React.createElement("td", null, row.name || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String((_a2 = row.sort_order) != null ? _a2 : 0)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0442\u0435\u043C\u0443", onClick: () => openEditRecordModal("topics", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u0435\u043C\u0443", onClick: () => deleteRecord("topics", row.id), tone: "danger" })))); + } + } + ) : null, configActiveKey === "quotes" ? /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "author", label: "\u0410\u0432\u0442\u043E\u0440", sortable: true, field: "author" }, + { key: "text", label: "\u0422\u0435\u043A\u0441\u0442", sortable: true, field: "text" }, + { key: "source", label: "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A", sortable: true, field: "source" }, + { key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", sortable: true, field: "is_active" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" }, + { key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u0430", sortable: true, field: "created_at" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tables.quotes.rows, + emptyColspan: 7, + onSort: (field) => toggleTableSort("quotes", field), + sortClause: tables.quotes.sort && tables.quotes.sort[0] || TABLE_SERVER_CONFIG.quotes.sort[0], + renderRow: (row) => { + var _a2; + return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.author || "-"), /* @__PURE__ */ React.createElement("td", null, row.text || "-"), /* @__PURE__ */ React.createElement("td", null, row.source || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.is_active)), /* @__PURE__ */ React.createElement("td", null, String((_a2 = row.sort_order) != null ? _a2 : 0)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0446\u0438\u0442\u0430\u0442\u0443", onClick: () => openEditRecordModal("quotes", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0446\u0438\u0442\u0430\u0442\u0443", onClick: () => deleteRecord("quotes", row.id), tone: "danger" })))); + } + } + ) : null, configActiveKey === "statuses" ? /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "code", label: "\u041A\u043E\u0434", sortable: true, field: "code" }, + { key: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", sortable: true, field: "name" }, + { key: "status_group_id", label: "\u0413\u0440\u0443\u043F\u043F\u0430", sortable: true, field: "status_group_id" }, + { key: "kind", label: "\u0422\u0438\u043F", sortable: true, field: "kind" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", sortable: true, field: "enabled" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" }, + { key: "is_terminal", label: "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439", sortable: true, field: "is_terminal" }, + { key: "invoice_template", label: "\u0428\u0430\u0431\u043B\u043E\u043D \u0441\u0447\u0435\u0442\u0430" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tables.statuses.rows, + emptyColspan: 9, + onSort: (field) => toggleTableSort("statuses", field), + sortClause: tables.statuses.sort && tables.statuses.sort[0] || TABLE_SERVER_CONFIG.statuses.sort[0], + renderRow: (row) => { + var _a2; + return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.code || "-")), /* @__PURE__ */ React.createElement("td", null, row.name || "-"), /* @__PURE__ */ React.createElement("td", null, resolveReferenceLabel({ table: "status_groups", value_field: "id", label_field: "name" }, row.status_group_id)), /* @__PURE__ */ React.createElement("td", null, statusKindLabel(row.kind)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String((_a2 = row.sort_order) != null ? _a2 : 0)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.is_terminal)), /* @__PURE__ */ React.createElement("td", null, row.invoice_template || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441", onClick: () => openEditRecordModal("statuses", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441", onClick: () => deleteRecord("statuses", row.id), tone: "danger" })))); + } + } + ) : null, configActiveKey === "formFields" ? /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "key", label: "\u041A\u043B\u044E\u0447", sortable: true, field: "key" }, + { key: "label", label: "\u041C\u0435\u0442\u043A\u0430", sortable: true, field: "label" }, + { key: "type", label: "\u0422\u0438\u043F", sortable: true, field: "type" }, + { key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", sortable: true, field: "required" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", sortable: true, field: "enabled" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tables.formFields.rows, + emptyColspan: 7, + onSort: (field) => toggleTableSort("formFields", field), + sortClause: tables.formFields.sort && tables.formFields.sort[0] || TABLE_SERVER_CONFIG.formFields.sort[0], + renderRow: (row) => { + var _a2; + return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.key || "-")), /* @__PURE__ */ React.createElement("td", null, row.label || "-"), /* @__PURE__ */ React.createElement("td", null, row.type || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.required)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String((_a2 = row.sort_order) != null ? _a2 : 0)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", onClick: () => openEditRecordModal("formFields", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", onClick: () => deleteRecord("formFields", row.id), tone: "danger" })))); + } + } + ) : null, configActiveKey === "topicRequiredFields" ? /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "topic_code", label: "\u0422\u0435\u043C\u0430", sortable: true, field: "topic_code" }, + { key: "field_key", label: "\u041F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", sortable: true, field: "field_key" }, + { key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", sortable: true, field: "required" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", sortable: true, field: "enabled" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" }, + { key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u043E", sortable: true, field: "created_at" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tables.topicRequiredFields.rows, + emptyColspan: 7, + onSort: (field) => toggleTableSort("topicRequiredFields", field), + sortClause: tables.topicRequiredFields.sort && tables.topicRequiredFields.sort[0] || TABLE_SERVER_CONFIG.topicRequiredFields.sort[0], + renderRow: (row) => { + var _a2; + return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.field_key || "-")), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.required)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String((_a2 = row.sort_order) != null ? _a2 : 0)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement( + IconButton, + { + icon: "\u270E", + tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435", + onClick: () => openEditRecordModal("topicRequiredFields", row) + } + ), /* @__PURE__ */ React.createElement( + IconButton, + { + icon: "\u{1F5D1}", + tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435", + onClick: () => deleteRecord("topicRequiredFields", row.id), + tone: "danger" + } + )))); + } + } + ) : null, configActiveKey === "topicDataTemplates" ? /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "topic_code", label: "\u0422\u0435\u043C\u0430", sortable: true, field: "topic_code" }, + { key: "key", label: "\u041A\u043B\u044E\u0447", sortable: true, field: "key" }, + { key: "label", label: "\u041C\u0435\u0442\u043A\u0430", sortable: true, field: "label" }, + { key: "description", label: "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435", sortable: true, field: "description" }, + { key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", sortable: true, field: "required" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", sortable: true, field: "enabled" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" }, + { key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u043E", sortable: true, field: "created_at" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tables.topicDataTemplates.rows, + emptyColspan: 9, + onSort: (field) => toggleTableSort("topicDataTemplates", field), + sortClause: tables.topicDataTemplates.sort && tables.topicDataTemplates.sort[0] || TABLE_SERVER_CONFIG.topicDataTemplates.sort[0], + renderRow: (row) => { + var _a2; + return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.key || "-")), /* @__PURE__ */ React.createElement("td", null, row.label || "-"), /* @__PURE__ */ React.createElement("td", null, row.description || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.required)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String((_a2 = row.sort_order) != null ? _a2 : 0)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D", onClick: () => openEditRecordModal("topicDataTemplates", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D", onClick: () => deleteRecord("topicDataTemplates", row.id), tone: "danger" })))); + } + } + ) : null, configActiveKey === "statusTransitions" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "status-designer" }, /* @__PURE__ */ React.createElement("div", { className: "status-designer-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h4", null, "\u041A\u043E\u043D\u0441\u0442\u0440\u0443\u043A\u0442\u043E\u0440 \u043C\u0430\u0440\u0448\u0440\u0443\u0442\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0412\u0435\u0442\u0432\u043B\u0435\u043D\u0438\u044F, \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u044B, SLA \u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043A \u0434\u0430\u043D\u043D\u044B\u043C/\u0444\u0430\u0439\u043B\u0430\u043C \u043D\u0430 \u043A\u0430\u0436\u0434\u043E\u043C \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0435.")), /* @__PURE__ */ React.createElement("div", { className: "status-designer-controls" }, /* @__PURE__ */ React.createElement( + "select", + { + id: "status-designer-topic", + value: statusDesignerTopicCode, + onChange: (event) => loadStatusDesignerTopic(event.target.value) + }, + /* @__PURE__ */ React.createElement("option", { value: "" }, "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0435\u043C\u0443"), + (dictionaries.topics || []).map((topic) => /* @__PURE__ */ React.createElement("option", { key: topic.code, value: topic.code }, (topic.name || topic.code) + " (" + topic.code + ")")) + ), /* @__PURE__ */ React.createElement("button", { className: "btn secondary btn-sm", type: "button", onClick: () => loadStatusDesignerTopic(statusDesignerTopicCode) }, "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0442\u0435\u043C\u0443"), /* @__PURE__ */ React.createElement("button", { className: "btn btn-sm", type: "button", onClick: openCreateStatusTransitionForTopic }, "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0435\u0440\u0435\u0445\u043E\u0434"))), statusDesignerCards.length ? /* @__PURE__ */ React.createElement("div", { className: "status-designer-grid", id: "status-designer-cards" }, statusDesignerCards.map((card) => /* @__PURE__ */ React.createElement("div", { className: "status-node-card", key: card.code }, /* @__PURE__ */ React.createElement("div", { className: "status-node-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("b", null, card.name), /* @__PURE__ */ React.createElement("code", null, card.code)), card.isTerminal ? /* @__PURE__ */ React.createElement("span", { className: "status-node-terminal" }, "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439") : null), card.outgoing.length ? /* @__PURE__ */ React.createElement("ul", { className: "simple-list status-node-links" }, card.outgoing.map((link) => /* @__PURE__ */ React.createElement("li", { key: String(link.id) }, /* @__PURE__ */ React.createElement( + "button", + { + className: "status-link-chip", + type: "button", + onClick: () => openEditRecordModal("statusTransitions", link) + }, + /* @__PURE__ */ React.createElement("span", null, statusRouteLabel(link.to_status)), + /* @__PURE__ */ React.createElement("small", null, "SLA: " + (link.sla_hours == null ? "-" : String(link.sla_hours) + " \u0447") + " \u2022 \u0414\u0430\u043D\u043D\u044B\u0435: " + listPreview(link.required_data_keys, "-") + " \u2022 \u0424\u0430\u0439\u043B\u044B: " + listPreview(link.required_mime_types, "-")) + )))) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u041D\u0435\u0442 \u0438\u0441\u0445\u043E\u0434\u044F\u0449\u0438\u0445 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u043E\u0432")))) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0414\u043B\u044F \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0439 \u0442\u0435\u043C\u044B \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u044B \u043F\u043E\u043A\u0430 \u043D\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D\u044B.")), /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "topic_code", label: "\u0422\u0435\u043C\u0430", sortable: true, field: "topic_code" }, + { key: "from_status", label: "\u0418\u0437 \u0441\u0442\u0430\u0442\u0443\u0441\u0430", sortable: true, field: "from_status" }, + { key: "to_status", label: "\u0412 \u0441\u0442\u0430\u0442\u0443\u0441", sortable: true, field: "to_status" }, + { key: "sla_hours", label: "SLA (\u0447\u0430\u0441\u044B)", sortable: true, field: "sla_hours" }, + { key: "required_data_keys", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435" }, + { key: "required_mime_types", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", sortable: true, field: "enabled" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tables.statusTransitions.rows, + emptyColspan: 9, + onSort: (field) => toggleTableSort("statusTransitions", field), + sortClause: tables.statusTransitions.sort && tables.statusTransitions.sort[0] || TABLE_SERVER_CONFIG.statusTransitions.sort[0], + renderRow: (row) => { + var _a2; + return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, statusRouteLabel(row.from_status)), /* @__PURE__ */ React.createElement("td", null, statusRouteLabel(row.to_status)), /* @__PURE__ */ React.createElement("td", null, row.sla_hours == null ? "-" : String(row.sla_hours)), /* @__PURE__ */ React.createElement("td", null, listPreview(row.required_data_keys, "-")), /* @__PURE__ */ React.createElement("td", null, listPreview(row.required_mime_types, "-")), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String((_a2 = row.sort_order) != null ? _a2 : 0)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement( + IconButton, + { + icon: "\u270E", + tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0435\u0440\u0435\u0445\u043E\u0434", + onClick: () => openEditRecordModal("statusTransitions", row) + } + ), /* @__PURE__ */ React.createElement( + IconButton, + { + icon: "\u{1F5D1}", + tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0435\u0440\u0435\u0445\u043E\u0434", + onClick: () => deleteRecord("statusTransitions", row.id), + tone: "danger" + } + )))); + } + } + )) : null, configActiveKey === "users" ? /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "name", label: "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C", sortable: true, field: "name" }, + { key: "email", label: "Email", sortable: true, field: "email" }, + { key: "role", label: "\u0420\u043E\u043B\u044C", sortable: true, field: "role" }, + { key: "primary_topic_code", label: "\u041F\u0440\u043E\u0444\u0438\u043B\u044C (\u0442\u0435\u043C\u0430)", sortable: true, field: "primary_topic_code" }, + { key: "default_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430", sortable: true, field: "default_rate" }, + { key: "salary_percent", label: "\u041F\u0440\u043E\u0446\u0435\u043D\u0442", sortable: true, field: "salary_percent" }, + { key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", sortable: true, field: "is_active" }, + { key: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439", sortable: true, field: "responsible" }, + { key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D", sortable: true, field: "created_at" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tables.users.rows, + emptyColspan: 10, + onSort: (field) => toggleTableSort("users", field), + sortClause: tables.users.sort && tables.users.sort[0] || TABLE_SERVER_CONFIG.users.sort[0], + renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "user-identity" }, /* @__PURE__ */ React.createElement(UserAvatar, { name: row.name, email: row.email, avatarUrl: row.avatar_url, accessToken: token, size: 32 }), /* @__PURE__ */ React.createElement("div", { className: "user-identity-text" }, /* @__PURE__ */ React.createElement("b", null, row.name || "-")))), /* @__PURE__ */ React.createElement("td", null, row.email || "-"), /* @__PURE__ */ React.createElement("td", null, roleLabel(row.role)), /* @__PURE__ */ React.createElement("td", null, resolveReferenceLabel({ table: "topics", value_field: "code", label_field: "name" }, row.primary_topic_code)), /* @__PURE__ */ React.createElement("td", null, row.default_rate == null ? "-" : String(row.default_rate)), /* @__PURE__ */ React.createElement("td", null, row.salary_percent == null ? "-" : String(row.salary_percent)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.is_active)), /* @__PURE__ */ React.createElement("td", null, row.responsible || "-"), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F", onClick: () => openEditRecordModal("users", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F", onClick: () => deleteRecord("users", row.id), tone: "danger" })))) + } + ) : null, configActiveKey === "userTopics" ? /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "admin_user_id", label: "\u042E\u0440\u0438\u0441\u0442", sortable: true, field: "admin_user_id" }, + { key: "topic_code", label: "\u0414\u043E\u043F. \u0442\u0435\u043C\u0430", sortable: true, field: "topic_code" }, + { key: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439", sortable: true, field: "responsible" }, + { key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u043E", sortable: true, field: "created_at" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tables.userTopics.rows, + emptyColspan: 5, + onSort: (field) => toggleTableSort("userTopics", field), + sortClause: tables.userTopics.sort && tables.userTopics.sort[0] || TABLE_SERVER_CONFIG.userTopics.sort[0], + renderRow: (row) => { + const lawyer = (dictionaries.users || []).find((item) => String(item.id) === String(row.admin_user_id)); + const lawyerLabel = lawyer ? lawyer.name || lawyer.email || row.admin_user_id : row.admin_user_id || "-"; + return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, lawyerLabel), /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, row.responsible || "-"), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0432\u044F\u0437\u044C", onClick: () => openEditRecordModal("userTopics", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0432\u044F\u0437\u044C", onClick: () => deleteRecord("userTopics", row.id), tone: "danger" })))); + } + } + ) : null, configActiveKey && !KNOWN_CONFIG_TABLE_KEYS.has(configActiveKey) ? /* @__PURE__ */ React.createElement( + DataTable, + { + headers: genericConfigHeaders, + rows: activeConfigTableState.rows, + emptyColspan: Math.max(1, genericConfigHeaders.length), + onSort: (field) => toggleTableSort(configActiveKey, field), + sortClause: activeConfigTableState.sort && activeConfigTableState.sort[0] || (((_a = resolveTableConfig(configActiveKey)) == null ? void 0 : _a.sort) || [])[0], + renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id || JSON.stringify(row) }, ((activeConfigMeta == null ? void 0 : activeConfigMeta.columns) || []).map((column) => { + const key = String(column.name || ""); + const value = row[key]; + if (column.kind === "boolean") return /* @__PURE__ */ React.createElement("td", { key }, boolLabel(Boolean(value))); + if (column.kind === "date" || column.kind === "datetime") return /* @__PURE__ */ React.createElement("td", { key }, fmtDate(value)); + if (column.kind === "json") return /* @__PURE__ */ React.createElement("td", { key }, value == null ? "-" : JSON.stringify(value)); + const reference = normalizeReferenceMeta(column.reference); + if (reference) return /* @__PURE__ */ React.createElement("td", { key }, resolveReferenceLabel(reference, value)); + return /* @__PURE__ */ React.createElement("td", { key }, value == null || value === "" ? "-" : String(value)); + }), canUpdateInConfig || canDeleteInConfig ? /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, canUpdateInConfig ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0437\u0430\u043F\u0438\u0441\u044C", onClick: () => openEditRecordModal(configActiveKey, row) }) : null, canDeleteInConfig ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043F\u0438\u0441\u044C", onClick: () => deleteRecord(configActiveKey, row.id), tone: "danger" }) : null)) : null) + } + ) : null, /* @__PURE__ */ React.createElement("div", { className: "pager table-footer-bar config-controls-bar" }, /* @__PURE__ */ React.createElement("div", { className: "config-controls-summary" }, activeConfigTableState.showAll ? "\u0412\u0441\u0435\u0433\u043E: " + activeConfigTableState.total + " \u2022 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0432\u0441\u0435 \u0437\u0430\u043F\u0438\u0441\u0438" : "\u0412\u0441\u0435\u0433\u043E: " + activeConfigTableState.total + " \u2022 \u0441\u043C\u0435\u0449\u0435\u043D\u0438\u0435: " + activeConfigTableState.offset), /* @__PURE__ */ React.createElement("div", { className: "config-controls-actions" }, /* @__PURE__ */ React.createElement( + "button", + { + className: "btn secondary table-control-btn table-control-loadall", + type: "button", + onClick: () => loadAllRows(configActiveKey), + disabled: !canLoadAllRows, + title: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0432\u0441\u0435 " + activeConfigTableState.total, + "aria-label": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0432\u0441\u0435 " + activeConfigTableState.total + }, + /* @__PURE__ */ React.createElement(DownloadIcon, null), + /* @__PURE__ */ React.createElement("span", null, activeConfigTableState.total) + ), /* @__PURE__ */ React.createElement( + "button", + { + className: "btn secondary table-control-btn", + type: "button", + onClick: () => loadCurrentConfigTable(true), + disabled: !canRefresh, + title: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C", + "aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C" + }, + /* @__PURE__ */ React.createElement(RefreshIcon, null) + ), /* @__PURE__ */ React.createElement( + "button", + { + className: "btn secondary table-control-btn", + type: "button", + onClick: () => loadPrevPage(configActiveKey), + disabled: !canLoadPrev, + title: "\u041D\u0430\u0437\u0430\u0434", + "aria-label": "\u041D\u0430\u0437\u0430\u0434" + }, + /* @__PURE__ */ React.createElement(PrevIcon, null) + ), /* @__PURE__ */ React.createElement( + "button", + { + className: "btn secondary table-control-btn", + type: "button", + onClick: () => loadNextPage(configActiveKey), + disabled: !canLoadNext, + title: "\u0412\u043F\u0435\u0440\u0435\u0434", + "aria-label": "\u0412\u043F\u0435\u0440\u0435\u0434" + }, + /* @__PURE__ */ React.createElement(NextIcon, null) + ))), /* @__PURE__ */ React.createElement(StatusLine, { status: getStatus(configActiveKey) }))))); + } + + // app/web/admin/features/dashboard/DashboardSection.jsx + function DashboardSection({ + dashboardData, + token, + status, + apiCall, + onOpenRequest, + DataTableComponent, + StatusLineComponent, + UserAvatarComponent + }) { + var _a, _b, _c, _d, _e, _f; + const { useMemo, useState } = React; + const DataTable = DataTableComponent; + const StatusLine = StatusLineComponent; + const UserAvatar = UserAvatarComponent; + const [lawyerModal, setLawyerModal] = useState({ + open: false, + loading: false, + error: "", + lawyer: null, + rows: [], + totals: { amount: 0, salary: 0 } + }); + const statusCards = useMemo(() => { + return Object.entries((dashboardData == null ? void 0 : dashboardData.byStatus) || {}).map(([label, value]) => ({ label, value })).sort((a, b) => String(a.label).localeCompare(String(b.label), "ru")); + }, [dashboardData == null ? void 0 : dashboardData.byStatus]); + const fmtThousandsCompact = (value) => { + const amount = Number(value || 0); + if (!Number.isFinite(amount)) return "0"; + return new Intl.NumberFormat("ru-RU", { + minimumFractionDigits: 0, + maximumFractionDigits: 1 + }).format(amount / 1e3); + }; + const openLawyerModal = async (lawyerRow) => { + if (!(lawyerRow == null ? void 0 : lawyerRow.lawyer_id) || typeof apiCall !== "function") return; + setLawyerModal({ + open: true, + loading: true, + error: "", + lawyer: lawyerRow, + rows: [], + totals: { amount: 0, salary: 0 } + }); + try { + const data = await apiCall("/api/admin/metrics/lawyers/" + encodeURIComponent(String(lawyerRow.lawyer_id)) + "/active-requests"); + setLawyerModal((prev) => { + var _a2, _b2; + return { + ...prev, + loading: false, + error: "", + rows: Array.isArray(data == null ? void 0 : data.rows) ? data.rows : [], + totals: { + amount: Number(((_a2 = data == null ? void 0 : data.totals) == null ? void 0 : _a2.amount) || 0), + salary: Number(((_b2 = data == null ? void 0 : data.totals) == null ? void 0 : _b2.salary) || 0) + } + }; + }); + } catch (error) { + setLawyerModal((prev) => ({ ...prev, loading: false, error: error.message || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438" })); + } + }; + const closeLawyerModal = () => { + setLawyerModal({ open: false, loading: false, error: "", lawyer: null, rows: [], totals: { amount: 0, salary: 0 } }); + }; + const isLawyerScope = (dashboardData == null ? void 0 : dashboardData.scope) === "LAWYER"; + const lawyerCards = Array.isArray(dashboardData == null ? void 0 : dashboardData.lawyerLoads) ? dashboardData.lawyerLoads : []; + const currentLawyer = lawyerCards[0] || null; + const lawyerMetrics = currentLawyer ? [ + { label: "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435", value: String((_a = currentLawyer.active_load) != null ? _a : 0) }, + { label: "\u041D\u043E\u0432\u044B\u0435", value: String((_b = currentLawyer.monthly_assigned_count) != null ? _b : 0) }, + { label: "\u0417\u0430\u043A\u0440\u044B\u0442\u043E", value: String((_c = currentLawyer.monthly_completed_count) != null ? _c : 0) }, + { label: "\u0417\u041F, \u0442\u044B\u0441.", value: fmtThousandsCompact(currentLawyer.monthly_salary) } + ] : []; + return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u041E\u0431\u0437\u043E\u0440 \u043C\u0435\u0442\u0440\u0438\u043A"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, isLawyerScope ? "\u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043E\u043A \u0438 \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u044C\u043D\u0430\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430." : "\u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043E\u043A, \u0444\u0438\u043D\u0430\u043D\u0441\u044B \u043C\u0435\u0441\u044F\u0446\u0430 \u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u044E\u0440\u0438\u0441\u0442\u043E\u0432."))), /* @__PURE__ */ React.createElement("div", { className: "cards" }, ((dashboardData == null ? void 0 : dashboardData.cards) || []).map((card) => /* @__PURE__ */ React.createElement("div", { className: "card", key: card.label }, /* @__PURE__ */ React.createElement("p", null, card.label), /* @__PURE__ */ React.createElement("b", null, card.value)))), statusCards.length ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: "0.8rem" } }, /* @__PURE__ */ React.createElement("div", { className: "section-head", style: { marginBottom: "0.5rem" } }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", { style: { margin: 0 } }, "\u0421\u0442\u0430\u0442\u0443\u0441\u044B \u0437\u0430\u044F\u0432\u043E\u043A"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.2rem" } }, "\u0422\u0435\u043A\u0443\u0449\u0430\u044F \u0440\u0430\u0441\u043A\u043B\u0430\u0434\u043A\u0430 \u043F\u043E \u0432\u0441\u0435\u043C \u0441\u0442\u0430\u0442\u0443\u0441\u0430\u043C."))), /* @__PURE__ */ React.createElement("div", { className: "cards" }, statusCards.map((card) => { + var _a2; + return /* @__PURE__ */ React.createElement("div", { className: "card", key: "status-" + card.label }, /* @__PURE__ */ React.createElement("p", null, card.label), /* @__PURE__ */ React.createElement("b", null, String((_a2 = card.value) != null ? _a2 : 0))); + }))) : null, isLawyerScope ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: "0.9rem" } }, /* @__PURE__ */ React.createElement("h3", { style: { margin: "0 0 0.55rem" } }, "\u041C\u043E\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430"), /* @__PURE__ */ React.createElement("div", { className: "cards" }, lawyerMetrics.length ? lawyerMetrics.map((metric) => /* @__PURE__ */ React.createElement("div", { className: "card", key: "lawyer-metric-" + metric.label }, /* @__PURE__ */ React.createElement("p", null, metric.label), /* @__PURE__ */ React.createElement("b", null, metric.value))) : /* @__PURE__ */ React.createElement("div", { className: "card" }, /* @__PURE__ */ React.createElement("p", null, "\u041C\u043E\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430"), /* @__PURE__ */ React.createElement("b", null, "\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445")))) : /* @__PURE__ */ React.createElement("div", { style: { marginTop: "0.9rem" } }, /* @__PURE__ */ React.createElement("h3", { style: { margin: "0 0 0.55rem" } }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u044E\u0440\u0438\u0441\u0442\u043E\u0432"), /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-grid" }, lawyerCards.length ? lawyerCards.map((row) => { + var _a2, _b2, _c2; + return /* @__PURE__ */ React.createElement( + "button", + { + key: row.lawyer_id, + type: "button", + className: "lawyer-dashboard-card", + onClick: () => openLawyerModal(row), + title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0434\u0435\u0442\u0430\u043B\u0438 \u044E\u0440\u0438\u0441\u0442\u0430" + }, + /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-left" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-avatar" }, /* @__PURE__ */ React.createElement(UserAvatar, { name: row.name, email: row.email, avatarUrl: row.avatar_url, accessToken: token, size: 72 })), /* @__PURE__ */ React.createElement("b", { className: "lawyer-dashboard-name" }, row.name || row.email || "-"), /* @__PURE__ */ React.createElement("span", { className: "lawyer-dashboard-topic" }, row.primary_topic_code || "\u0422\u0435\u043C\u0430 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u0430")), + /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-right" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435"), /* @__PURE__ */ React.createElement("b", null, String((_a2 = row.active_load) != null ? _a2 : 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u041D\u043E\u0432\u044B\u0435"), /* @__PURE__ */ React.createElement("b", null, String((_b2 = row.monthly_assigned_count) != null ? _b2 : 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0417\u0430\u043A\u0440\u044B\u0442\u043E"), /* @__PURE__ */ React.createElement("b", null, String((_c2 = row.monthly_completed_count) != null ? _c2 : 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0421\u0443\u043C\u043C\u0430, \u0442\u044B\u0441."), /* @__PURE__ */ React.createElement("b", null, fmtThousandsCompact(row.monthly_paid_gross))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0417\u041F, \u0442\u044B\u0441."), /* @__PURE__ */ React.createElement("b", null, fmtThousandsCompact(row.monthly_salary)))) + ); + }) : /* @__PURE__ */ React.createElement("div", { className: "card" }, /* @__PURE__ */ React.createElement("p", null, "\u042E\u0440\u0438\u0441\u0442\u044B"), /* @__PURE__ */ React.createElement("b", null, "\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445")))), /* @__PURE__ */ React.createElement(StatusLine, { status }), /* @__PURE__ */ React.createElement("div", { className: "overlay" + (lawyerModal.open ? " open" : ""), onClick: closeLawyerModal }, /* @__PURE__ */ React.createElement("div", { className: "modal lawyer-dashboard-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, lawyerModal.lawyer ? "\u042E\u0440\u0438\u0441\u0442: " + (lawyerModal.lawyer.name || lawyerModal.lawyer.email || "-") : "\u042E\u0440\u0438\u0441\u0442"), lawyerModal.lawyer ? /* @__PURE__ */ React.createElement("p", { className: "muted", style: { margin: "0.2rem 0 0" } }, (lawyerModal.lawyer.primary_topic_code || "\u0422\u0435\u043C\u0430 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u0430") + " \u2022 " + (lawyerModal.lawyer.email || "")) : null), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeLawyerModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), lawyerModal.lawyer ? /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-summary" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-avatar" }, /* @__PURE__ */ React.createElement( + UserAvatar, + { + name: lawyerModal.lawyer.name, + email: lawyerModal.lawyer.email, + avatarUrl: lawyerModal.lawyer.avatar_url, + accessToken: token, + size: 84 + } + )), /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-metrics" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435"), /* @__PURE__ */ React.createElement("b", null, String((_d = lawyerModal.lawyer.active_load) != null ? _d : 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u041D\u043E\u0432\u044B\u0435"), /* @__PURE__ */ React.createElement("b", null, String((_e = lawyerModal.lawyer.monthly_assigned_count) != null ? _e : 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043D\u044B\u0435"), /* @__PURE__ */ React.createElement("b", null, String((_f = lawyerModal.lawyer.monthly_completed_count) != null ? _f : 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0421\u0443\u043C\u043C\u0430"), /* @__PURE__ */ React.createElement("b", null, fmtAmount(lawyerModal.lawyer.monthly_paid_gross))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0417\u0430\u0440\u043F\u043B\u0430\u0442\u0430"), /* @__PURE__ */ React.createElement("b", null, fmtAmount(lawyerModal.lawyer.monthly_salary))))) : null, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-scroll" }, lawyerModal.loading ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0445 \u0437\u0430\u044F\u0432\u043E\u043A...") : null, lawyerModal.error ? /* @__PURE__ */ React.createElement("p", { className: "status error" }, lawyerModal.error) : null, !lawyerModal.loading ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-table-area" }, /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "track_number", label: "\u041D\u043E\u043C\u0435\u0440" }, + { key: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441" }, + { key: "client_name", label: "\u041A\u043B\u0438\u0435\u043D\u0442" }, + { key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u0430" }, + { key: "invoice_amount", label: "\u0421\u0443\u043C\u043C\u0430 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435" }, + { key: "month_paid_amount", label: "\u041E\u043F\u043B\u0430\u0442\u044B" }, + { key: "month_salary_amount", label: "\u0417\u0430\u0440\u043F\u043B\u0430\u0442\u0430" } + ], + rows: lawyerModal.rows || [], + emptyColspan: 7, + renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "request-track-link", + onClick: (event) => { + if (typeof onOpenRequest === "function") onOpenRequest(row.id, event); + closeLawyerModal(); + }, + title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443" + }, + /* @__PURE__ */ React.createElement("code", null, row.track_number || "-") + )), /* @__PURE__ */ React.createElement("td", null, statusLabel(row.status_code)), /* @__PURE__ */ React.createElement("td", null, row.client_name || "-"), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, fmtAmount(row.invoice_amount)), /* @__PURE__ */ React.createElement("td", null, fmtAmount(row.month_paid_amount)), /* @__PURE__ */ React.createElement("td", null, fmtAmount(row.month_salary_amount))) + } + ))) : null), !lawyerModal.loading ? /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-footer" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-total-chip" }, "\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0445: ", /* @__PURE__ */ React.createElement("b", null, String((lawyerModal.rows || []).length))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-total-chip" }, "\u041E\u043F\u043B\u0430\u0442\u044B: ", /* @__PURE__ */ React.createElement("b", null, fmtAmount(lawyerModal.totals.amount))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-total-chip" }, "\u0417\u0430\u0440\u043F\u043B\u0430\u0442\u0430: ", /* @__PURE__ */ React.createElement("b", null, fmtAmount(lawyerModal.totals.salary)))) : null))); + } + + // app/web/admin/features/invoices/InvoicesSection.jsx + function InvoicesSection({ + role, + tables, + status, + getFieldDef, + getFilterValuePreview, + onRefresh, + onCreate, + onOpenFilter, + onRemoveFilter, + onEditFilter, + onSort, + onPrev, + onNext, + onLoadAll, + onOpenRequest, + onDownloadPdf, + onEditRecord, + onDeleteRecord, + FilterToolbarComponent, + DataTableComponent, + TablePagerComponent, + StatusLineComponent, + IconButtonComponent + }) { + const tableState = (tables == null ? void 0 : tables.invoices) || { rows: [], filters: [], sort: [] }; + const FilterToolbar = FilterToolbarComponent; + const DataTable = DataTableComponent; + const TablePager = TablePagerComponent; + const StatusLine = StatusLineComponent; + const IconButton = IconButtonComponent; + return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0421\u0447\u0435\u0442\u0430"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0412\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0435 \u0441\u0447\u0435\u0442\u0430 \u043A\u043B\u0438\u0435\u043D\u0442\u0430\u043C, \u0441\u0442\u0430\u0442\u0443\u0441\u044B \u043E\u043F\u043B\u0430\u0442\u044B \u0438 \u0432\u044B\u0433\u0440\u0443\u0437\u043A\u0430 PDF."))), /* @__PURE__ */ React.createElement( + FilterToolbar, + { + filters: tableState.filters, + onOpen: onOpenFilter, + onRemove: onRemoveFilter, + onEdit: onEditFilter, + hideAction: true, + getChipLabel: (clause) => { + const fieldDef = getFieldDef("invoices", clause.field); + return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("invoices", clause); + } + } + ), /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "invoice_number", label: "\u041D\u043E\u043C\u0435\u0440", sortable: true, field: "invoice_number" }, + { key: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", sortable: true, field: "status" }, + { key: "amount", label: "\u0421\u0443\u043C\u043C\u0430", sortable: true, field: "amount" }, + { key: "payer_display_name", label: "\u041F\u043B\u0430\u0442\u0435\u043B\u044C\u0449\u0438\u043A", sortable: true, field: "payer_display_name" }, + { key: "request_track_number", label: "\u0417\u0430\u044F\u0432\u043A\u0430" }, + { key: "issued_by_name", label: "\u0412\u044B\u0441\u0442\u0430\u0432\u0438\u043B", sortable: true, field: "issued_by_admin_user_id" }, + { key: "issued_at", label: "\u0421\u0444\u043E\u0440\u043C\u0438\u0440\u043E\u0432\u0430\u043D", sortable: true, field: "issued_at" }, + { key: "paid_at", label: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D", sortable: true, field: "paid_at" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tableState.rows, + emptyColspan: 9, + onSort, + sortClause: tableState.sort && tableState.sort[0] || TABLE_SERVER_CONFIG.invoices.sort[0], + renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.invoice_number || "-")), /* @__PURE__ */ React.createElement("td", null, row.status_label || invoiceStatusLabel(row.status)), /* @__PURE__ */ React.createElement("td", null, row.amount == null ? "-" : String(row.amount) + " " + String(row.currency || "RUB")), /* @__PURE__ */ React.createElement("td", null, row.payer_display_name || "-"), /* @__PURE__ */ React.createElement("td", null, row.request_id ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "request-track-link", + onClick: (event) => onOpenRequest(row, event), + title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443" + }, + /* @__PURE__ */ React.createElement("code", null, row.request_track_number || row.request_id || "-") + ) : /* @__PURE__ */ React.createElement("code", null, row.request_track_number || row.request_id || "-")), /* @__PURE__ */ React.createElement("td", null, row.issued_by_name || "-"), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.issued_at)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.paid_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u2B07", tooltip: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C PDF", onClick: () => onDownloadPdf(row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0447\u0435\u0442", onClick: () => onEditRecord(row) }), role === "ADMIN" ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0447\u0435\u0442", onClick: () => onDeleteRecord(row.id), tone: "danger" }) : null))) + } + ), /* @__PURE__ */ React.createElement( + TablePager, + { + tableState, + onPrev, + onNext, + onLoadAll, + onRefresh, + onCreate, + onOpenFilter + } + ), /* @__PURE__ */ React.createElement(StatusLine, { status })); + } + + // app/web/admin/features/requests/RequestsSection.jsx + function renderRequestUpdatesCell(row, role) { + const hasServiceRequestUnread = Boolean(row == null ? void 0 : row.has_service_requests_unread); + const serviceRequestCount = Number((row == null ? void 0 : row.service_requests_unread_count) || 0); + const viewerUnreadTotal = Number((row == null ? void 0 : row.viewer_unread_total) || 0); + const viewerUnreadByEvent = (row == null ? void 0 : row.viewer_unread_by_event) && typeof row.viewer_unread_by_event === "object" ? row.viewer_unread_by_event : {}; + const viewerUnreadLabel = viewerUnreadTotal > 0 ? Object.entries(viewerUnreadByEvent).map(([eventType, count]) => { + const code = String(eventType || "").toUpperCase(); + const label = REQUEST_UPDATE_EVENT_LABELS[code] || code.toLowerCase(); + return label + ": " + String(count || 0); + }).join(", ") : ""; + if (role === "LAWYER") { + const has = Boolean(row.lawyer_has_unread_updates); + const eventType = String(row.lawyer_unread_event_type || "").toUpperCase(); + if (!has && !hasServiceRequestUnread && !viewerUnreadTotal) return /* @__PURE__ */ React.createElement("span", { className: "request-update-empty" }, "\u043D\u0435\u0442"); + return /* @__PURE__ */ React.createElement("span", { className: "request-updates-stack" }, viewerUnreadTotal > 0 ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041C\u043E\u0438 \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435: " + (viewerUnreadLabel || String(viewerUnreadTotal)) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u041C\u043D\u0435: " + String(viewerUnreadTotal)) : null, has ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u0415\u0441\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u043E\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435: " + (REQUEST_UPDATE_EVENT_LABELS[eventType] || eventType.toLowerCase()) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), REQUEST_UPDATE_EVENT_LABELS[eventType] || "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435") : null, hasServiceRequestUnread ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430: " + String(serviceRequestCount) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u0417\u0430\u043F\u0440\u043E\u0441\u044B: " + String(serviceRequestCount || 1)) : null); + } + const clientHas = Boolean(row.client_has_unread_updates); + const clientType = String(row.client_unread_event_type || "").toUpperCase(); + const lawyerHas = Boolean(row.lawyer_has_unread_updates); + const lawyerType = String(row.lawyer_unread_event_type || "").toUpperCase(); + if (!clientHas && !lawyerHas && !hasServiceRequestUnread && !viewerUnreadTotal) return /* @__PURE__ */ React.createElement("span", { className: "request-update-empty" }, "\u043D\u0435\u0442"); + return /* @__PURE__ */ React.createElement("span", { className: "request-updates-stack" }, viewerUnreadTotal > 0 ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041C\u043E\u0438 \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435: " + (viewerUnreadLabel || String(viewerUnreadTotal)) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u041C\u043D\u0435: " + String(viewerUnreadTotal)) : null, clientHas ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041A\u043B\u0438\u0435\u043D\u0442\u0443: " + (REQUEST_UPDATE_EVENT_LABELS[clientType] || clientType.toLowerCase()) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u041A\u043B\u0438\u0435\u043D\u0442: " + (REQUEST_UPDATE_EVENT_LABELS[clientType] || "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435")) : null, lawyerHas ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u042E\u0440\u0438\u0441\u0442\u0443: " + (REQUEST_UPDATE_EVENT_LABELS[lawyerType] || lawyerType.toLowerCase()) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u042E\u0440\u0438\u0441\u0442: " + (REQUEST_UPDATE_EVENT_LABELS[lawyerType] || "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435")) : null, hasServiceRequestUnread ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430: " + String(serviceRequestCount) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u0417\u0430\u043F\u0440\u043E\u0441\u044B: " + String(serviceRequestCount || 1)) : null); + } + function RequestsSection({ + role, + tables, + status, + getStatus, + getFieldDef, + getFilterValuePreview, + resolveReferenceLabel, + onRefresh, + onCreate, + onOpenFilter, + onRemoveFilter, + onEditFilter, + onSort, + onPrev, + onNext, + onLoadAll, + onClaimRequest, + onOpenReassign, + onOpenRequest, + onEditRecord, + onDeleteRecord, + FilterToolbarComponent, + DataTableComponent, + TablePagerComponent, + StatusLineComponent, + IconButtonComponent + }) { + const tableState = (tables == null ? void 0 : tables.requests) || { rows: [], filters: [], sort: [] }; + const FilterToolbar = FilterToolbarComponent; + const DataTable = DataTableComponent; + const TablePager = TablePagerComponent; + const StatusLine = StatusLineComponent; + const IconButton = IconButtonComponent; + return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0417\u0430\u044F\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0421\u0435\u0440\u0432\u0435\u0440\u043D\u0430\u044F \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u0446\u0438\u044F \u0438 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043A\u043B\u0438\u0435\u043D\u0442\u0441\u043A\u0438\u0445 \u0437\u0430\u044F\u0432\u043E\u043A."))), /* @__PURE__ */ React.createElement( + FilterToolbar, + { + filters: tableState.filters, + onOpen: onOpenFilter, + onRemove: onRemoveFilter, + onEdit: onEditFilter, + hideAction: true, + getChipLabel: (clause) => { + const fieldDef = getFieldDef("requests", clause.field); + return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("requests", clause); + } + } + ), /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "track_number", label: "\u041D\u043E\u043C\u0435\u0440", sortable: true, field: "track_number" }, + { key: "client_name", label: "\u041A\u043B\u0438\u0435\u043D\u0442", sortable: true, field: "client_name" }, + { key: "client_phone", label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D", sortable: true, field: "client_phone" }, + { key: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441", sortable: true, field: "status_code" }, + { key: "topic_code", label: "\u0422\u0435\u043C\u0430", sortable: true, field: "topic_code" }, + { key: "assigned_lawyer_id", label: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D", sortable: true, field: "assigned_lawyer_id" }, + { key: "invoice_amount", label: "\u0421\u0447\u0435\u0442", sortable: true, field: "invoice_amount" }, + { key: "paid_at", label: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D\u043E", sortable: true, field: "paid_at" }, + { key: "updates", label: "\u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F" }, + { key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u0430", sortable: true, field: "created_at" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tableState.rows, + emptyColspan: 11, + onSort, + sortClause: tableState.sort && tableState.sort[0] || TABLE_SERVER_CONFIG.requests.sort[0], + renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "request-track-link", + onClick: (event) => onOpenRequest(row.id, event), + title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443" + }, + /* @__PURE__ */ React.createElement("code", null, row.track_number || "-") + )), /* @__PURE__ */ React.createElement("td", null, row.client_name || "-"), /* @__PURE__ */ React.createElement("td", null, row.client_phone || "-"), /* @__PURE__ */ React.createElement("td", null, statusLabel(row.status_code)), /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, resolveReferenceLabel({ table: "admin_users", value_field: "id", label_field: "name" }, row.assigned_lawyer_id)), /* @__PURE__ */ React.createElement("td", null, row.invoice_amount == null ? "-" : String(row.invoice_amount)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.paid_at)), /* @__PURE__ */ React.createElement("td", null, renderRequestUpdatesCell(row, role)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, role === "LAWYER" && !row.assigned_lawyer_id ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F4E5}", tooltip: "\u0412\u0437\u044F\u0442\u044C \u0432 \u0440\u0430\u0431\u043E\u0442\u0443", onClick: () => onClaimRequest(row.id) }) : null, role === "ADMIN" && row.assigned_lawyer_id ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u21C4", tooltip: "\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0438\u0442\u044C", onClick: () => onOpenReassign(row) }) : null, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443", onClick: () => onEditRecord(row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443", onClick: () => onDeleteRecord(row.id), tone: "danger" })))) + } + ), /* @__PURE__ */ React.createElement( + TablePager, + { + tableState, + onPrev, + onNext, + onLoadAll, + onRefresh, + onCreate, + onOpenFilter + } + ), /* @__PURE__ */ React.createElement(StatusLine, { status: status || (typeof getStatus === "function" ? getStatus("requests") : null) })); + } + + // app/web/admin/features/quotes/QuotesSection.jsx + function QuotesSection({ + tables, + status, + getFieldDef, + getFilterValuePreview, + onRefresh, + onCreate, + onOpenFilter, + onRemoveFilter, + onEditFilter, + onSort, + onPrev, + onNext, + onLoadAll, + onEditRecord, + onDeleteRecord, + FilterToolbarComponent, + DataTableComponent, + TablePagerComponent, + StatusLineComponent, + IconButtonComponent + }) { + const tableState = (tables == null ? void 0 : tables.quotes) || { rows: [], filters: [], sort: [] }; + const FilterToolbar = FilterToolbarComponent; + const DataTable = DataTableComponent; + const TablePager = TablePagerComponent; + const StatusLine = StatusLineComponent; + const IconButton = IconButtonComponent; + return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0426\u0438\u0442\u0430\u0442\u044B"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0423\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u043F\u0443\u0431\u043B\u0438\u0447\u043D\u043E\u0439 \u043B\u0435\u043D\u0442\u043E\u0439 \u0446\u0438\u0442\u0430\u0442 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u044B\u043C\u0438 \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u043C\u0438."))), /* @__PURE__ */ React.createElement( + FilterToolbar, + { + filters: tableState.filters, + onOpen: onOpenFilter, + onRemove: onRemoveFilter, + onEdit: onEditFilter, + hideAction: true, + getChipLabel: (clause) => { + const fieldDef = getFieldDef("quotes", clause.field); + return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("quotes", clause); + } + } + ), /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "author", label: "\u0410\u0432\u0442\u043E\u0440", sortable: true, field: "author" }, + { key: "text", label: "\u0422\u0435\u043A\u0441\u0442", sortable: true, field: "text" }, + { key: "source", label: "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A", sortable: true, field: "source" }, + { key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", sortable: true, field: "is_active" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" }, + { key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u0430", sortable: true, field: "created_at" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tableState.rows, + emptyColspan: 7, + onSort, + sortClause: tableState.sort && tableState.sort[0] || TABLE_SERVER_CONFIG.quotes.sort[0], + renderRow: (row) => { + var _a; + return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.author || "-"), /* @__PURE__ */ React.createElement("td", null, row.text || "-"), /* @__PURE__ */ React.createElement("td", null, row.source || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.is_active)), /* @__PURE__ */ React.createElement("td", null, String((_a = row.sort_order) != null ? _a : 0)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0446\u0438\u0442\u0430\u0442\u0443", onClick: () => onEditRecord(row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0446\u0438\u0442\u0430\u0442\u0443", onClick: () => onDeleteRecord(row.id), tone: "danger" })))); + } + } + ), /* @__PURE__ */ React.createElement( + TablePager, + { + tableState, + onPrev, + onNext, + onLoadAll, + onRefresh, + onCreate, + onOpenFilter + } + ), /* @__PURE__ */ React.createElement(StatusLine, { status })); + } + + // app/web/admin/features/service-requests/ServiceRequestsSection.jsx + function serviceRequestTypeLabel(value) { + const code = String(value || "").toUpperCase(); + return SERVICE_REQUEST_TYPE_LABELS[code] || code || "-"; + } + function serviceRequestStatusLabel(value) { + const code = String(value || "").toUpperCase(); + return SERVICE_REQUEST_STATUS_LABELS[code] || code || "-"; + } + function unreadLabel(row, role) { + if (String(role || "").toUpperCase() === "LAWYER") { + return (row == null ? void 0 : row.lawyer_unread) ? "\u0414\u0430" : "\u041D\u0435\u0442"; + } + return (row == null ? void 0 : row.admin_unread) ? "\u0414\u0430" : "\u041D\u0435\u0442"; + } + function ServiceRequestsSection({ + role, + tables, + status, + getStatus, + getFieldDef, + getFilterValuePreview, + onRefresh, + onOpenFilter, + onRemoveFilter, + onEditFilter, + onSort, + onPrev, + onNext, + onLoadAll, + onOpenRequest, + onMarkRead, + onEditRecord, + onDeleteRecord, + FilterToolbarComponent, + DataTableComponent, + TablePagerComponent, + StatusLineComponent, + IconButtonComponent + }) { + const tableState = (tables == null ? void 0 : tables.serviceRequests) || { rows: [], filters: [], sort: [] }; + const FilterToolbar = FilterToolbarComponent; + const DataTable = DataTableComponent; + const TablePager = TablePagerComponent; + const StatusLine = StatusLineComponent; + const IconButton = IconButtonComponent; + const roleCode = String(role || "").toUpperCase(); + return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0417\u0430\u043F\u0440\u043E\u0441\u044B"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430 \u043A \u043A\u0443\u0440\u0430\u0442\u043E\u0440\u0443 \u0438 \u043E\u0431\u0440\u0430\u0449\u0435\u043D\u0438\u044F \u043D\u0430 \u0441\u043C\u0435\u043D\u0443 \u044E\u0440\u0438\u0441\u0442\u0430."))), /* @__PURE__ */ React.createElement( + FilterToolbar, + { + filters: tableState.filters, + onOpen: onOpenFilter, + onRemove: onRemoveFilter, + onEdit: onEditFilter, + hideAction: true, + getChipLabel: (clause) => { + const fieldDef = getFieldDef("serviceRequests", clause.field); + return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("serviceRequests", clause); + } + } + ), /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "type", label: "\u0422\u0438\u043F", sortable: true, field: "type" }, + { key: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", sortable: true, field: "status" }, + { key: "body", label: "\u041E\u0431\u0440\u0430\u0449\u0435\u043D\u0438\u0435", sortable: false }, + { key: "request_id", label: "\u0417\u0430\u044F\u0432\u043A\u0430", sortable: true, field: "request_id" }, + { key: "unread", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E", sortable: true, field: roleCode === "LAWYER" ? "lawyer_unread" : "admin_unread" }, + { key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D", sortable: true, field: "created_at" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tableState.rows, + emptyColspan: 7, + onSort, + sortClause: tableState.sort && tableState.sort[0] || TABLE_SERVER_CONFIG.serviceRequests.sort[0], + renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, serviceRequestTypeLabel(row.type)), /* @__PURE__ */ React.createElement("td", null, serviceRequestStatusLabel(row.status)), /* @__PURE__ */ React.createElement("td", null, row.body || "-"), /* @__PURE__ */ React.createElement("td", null, row.request_id ? /* @__PURE__ */ React.createElement("button", { type: "button", className: "request-track-link", onClick: (event) => onOpenRequest(row.request_id, event), title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443" }, /* @__PURE__ */ React.createElement("code", null, row.request_id)) : "-"), /* @__PURE__ */ React.createElement("td", null, unreadLabel(row, roleCode)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u2713", tooltip: "\u041E\u0442\u043C\u0435\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u043C", onClick: () => onMarkRead(row.id) }), roleCode === "ADMIN" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441", onClick: () => onEditRecord(row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441", onClick: () => onDeleteRecord(row.id), tone: "danger" })) : null))) + } + ), /* @__PURE__ */ React.createElement( + TablePager, + { + tableState, + onPrev, + onNext, + onLoadAll, + onRefresh, + onOpenFilter + } + ), /* @__PURE__ */ React.createElement(StatusLine, { status: status || (typeof getStatus === "function" ? getStatus("serviceRequests") : null) })); + } + + // app/web/admin/features/requests/RequestWorkspace.jsx + function RequestWorkspace({ + viewerRole, + viewerUserId, + loading, + trackNumber, + requestData, + financeSummary, + invoices, + statusRouteNodes, + statusHistory, + availableStatuses, + currentImportantDateAt, + pendingStatusChangePreset, + messages, + attachments, + messageDraft, + selectedFiles, + fileUploading, + status, + onMessageChange, + onSendMessage, + onFilesSelect, + onRemoveSelectedFile, + onClearSelectedFiles, + onLoadRequestDataTemplates, + onLoadRequestDataBatch, + onLoadRequestDataTemplateDetails, + onSaveRequestDataTemplate, + onSaveRequestDataBatch, + onIssueInvoice, + onDownloadInvoicePdf, + onSaveRequestDataValues, + onUploadRequestAttachment, + onChangeStatus, + onConsumePendingStatusChangePreset, + onLiveProbe, + onTypingSignal, + domIds, + AttachmentPreviewModalComponent, + StatusLineComponent + }) { + var _a, _b, _c; + const { useEffect, useMemo, useRef, useState } = React; + const [preview, setPreview] = useState({ open: false, url: "", fileName: "", mimeType: "" }); + const [chatTab, setChatTab] = useState("chat"); + const [dropActive, setDropActive] = useState(false); + const [financeOpen, setFinanceOpen] = useState(false); + const [financeIssueForm, setFinanceIssueForm] = useState({ + open: false, + saving: false, + amount: "", + serviceDescription: "", + payerDisplayName: "", + error: "" + }); + const [requestDataListOpen, setRequestDataListOpen] = useState(false); + const [descriptionOpen, setDescriptionOpen] = useState(false); + const [requestTemplateSuggestOpen, setRequestTemplateSuggestOpen] = useState(false); + const [catalogFieldSuggestOpen, setCatalogFieldSuggestOpen] = useState(false); + const [statusChangeModal, setStatusChangeModal] = useState({ + open: false, + saving: false, + statusCode: "", + allowedStatusCodes: null, + importantDateAt: "", + comment: "", + files: [], + error: "" + }); + const [draggedRequestRowId, setDraggedRequestRowId] = useState(""); + const [dragOverRequestRowId, setDragOverRequestRowId] = useState(""); + const [dataRequestModal, setDataRequestModal] = useState({ + open: false, + loading: false, + saving: false, + savingTemplate: false, + messageId: "", + documentName: "", + availableDocuments: [], + templateList: [], + requestTemplateQuery: "", + templateName: "", + selectedRequestTemplateId: "", + templates: [], + catalogFieldQuery: "", + selectedCatalogTemplateId: "", + rows: [], + customLabel: "", + customType: "string", + templateStatus: "", + error: "" + }); + const [clientDataModal, setClientDataModal] = useState({ + open: false, + loading: false, + saving: false, + messageId: "", + items: [], + status: "", + error: "" + }); + const [composerFocused, setComposerFocused] = useState(false); + const [typingPeers, setTypingPeers] = useState([]); + const [liveMode, setLiveMode] = useState("online"); + const fileInputRef = useRef(null); + const statusChangeFileInputRef = useRef(null); + const chatListRef = useRef(null); + const liveCursorRef = useRef(""); + const liveTimerRef = useRef(null); + const liveInFlightRef = useRef(false); + const liveFailCountRef = useRef(0); + const typingHeartbeatRef = useRef(null); + const typingActiveRef = useRef(false); + const lastAutoScrollCursorRef = useRef(""); + const idMap = useMemo( + () => ({ + messagesList: "request-modal-messages", + filesList: "request-modal-files", + messageBody: "request-modal-message-body", + sendButton: "request-modal-message-send", + fileInput: "request-modal-file-input", + fileUploadButton: "", + dataRequestOverlay: "data-request-overlay", + dataRequestItems: "data-request-items", + dataRequestStatus: "data-request-status", + dataRequestSave: "data-request-save", + ...domIds || {} + }), + [domIds] + ); + const requestDataTypeOptions = useMemo( + () => [ + { value: "string", label: "\u0421\u0442\u0440\u043E\u043A\u0430" }, + { value: "date", label: "\u0414\u0430\u0442\u0430" }, + { value: "number", label: "\u0427\u0438\u0441\u043B\u043E" }, + { value: "file", label: "\u0424\u0430\u0439\u043B" }, + { value: "text", label: "\u0422\u0435\u043A\u0441\u0442" } + ], + [] + ); + const openPreview = (item) => { + if (!(item == null ? void 0 : item.download_url)) return; + setPreview({ + open: true, + url: String(item.download_url), + fileName: String(item.file_name || ""), + mimeType: String(item.mime_type || "") + }); + }; + const closePreview = () => setPreview({ open: false, url: "", fileName: "", mimeType: "" }); + const pendingFiles = Array.isArray(selectedFiles) ? selectedFiles : []; + const hasPendingFiles = pendingFiles.length > 0; + const canSubmit = Boolean(String(messageDraft || "").trim() || hasPendingFiles); + const onInputFiles = (event) => { + const files = Array.from(event.target && event.target.files || []); + if (files.length && typeof onFilesSelect === "function") onFilesSelect(files); + event.target.value = ""; + }; + const onDropFiles = (event) => { + event.preventDefault(); + setDropActive(false); + const files = Array.from(event.dataTransfer && event.dataTransfer.files || []); + if (files.length && typeof onFilesSelect === "function") onFilesSelect(files); + }; + const row = requestData && typeof requestData === "object" ? requestData : null; + const finance = financeSummary && typeof financeSummary === "object" ? financeSummary : null; + const viewerRoleCode = String(viewerRole || "").toUpperCase(); + const canRequestData = viewerRoleCode === "LAWYER" || viewerRoleCode === "ADMIN"; + const canFillRequestData = viewerRoleCode === "CLIENT"; + const canSeeRate = viewerRoleCode !== "CLIENT"; + const canSeeCreatedUpdatedInCard = viewerRoleCode !== "CLIENT"; + const showTopicStatusInCard = viewerRoleCode !== "CLIENT"; + const showContactsInCard = viewerRoleCode !== "CLIENT"; + const safeMessages = Array.isArray(messages) ? messages : []; + const safeAttachments = Array.isArray(attachments) ? attachments : []; + const safeInvoices = Array.isArray(invoices) ? invoices : []; + const safeStatusHistory = Array.isArray(statusHistory) ? statusHistory : []; + const safeAvailableStatuses = Array.isArray(availableStatuses) ? availableStatuses : []; + const totalFilesBytes = safeAttachments.reduce((acc, item) => acc + Number((item == null ? void 0 : item.size_bytes) || 0), 0); + const clientLabel = (row == null ? void 0 : row.client_name) || "-"; + const clientPhone = String((row == null ? void 0 : row.client_phone) || "").trim(); + const lawyerLabel = (row == null ? void 0 : row.assigned_lawyer_name) || (row == null ? void 0 : row.assigned_lawyer_id) || "\u041D\u0435 \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D"; + const lawyerPhone = String((row == null ? void 0 : row.assigned_lawyer_phone) || "").trim(); + const clientHasPhone = Boolean(clientPhone); + const lawyerHasPhone = Boolean(lawyerPhone); + const messagePlaceholder = canFillRequestData ? "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u044E\u0440\u0438\u0441\u0442\u0430" : "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u043A\u043B\u0438\u0435\u043D\u0442\u0430"; + const selectedRequestTemplateCandidate = useMemo( + () => (dataRequestModal.templateList || []).find((item) => { + const query = String(dataRequestModal.requestTemplateQuery || "").trim().toLowerCase(); + if (!query) return false; + return query === String((item == null ? void 0 : item.name) || "").trim().toLowerCase() || query === String((item == null ? void 0 : item.id) || "").trim().toLowerCase(); + }) || null, + [dataRequestModal.requestTemplateQuery, dataRequestModal.templateList] + ); + const selectedCatalogFieldCandidate = useMemo( + () => (dataRequestModal.templates || []).find((item) => { + const query = String(dataRequestModal.catalogFieldQuery || "").trim().toLowerCase(); + if (!query) return false; + return query === String((item == null ? void 0 : item.label) || "").trim().toLowerCase() || query === String((item == null ? void 0 : item.key) || "").trim().toLowerCase() || query === String((item == null ? void 0 : item.id) || "").trim().toLowerCase(); + }) || null, + [dataRequestModal.catalogFieldQuery, dataRequestModal.templates] + ); + const filteredRequestTemplates = useMemo(() => { + const query = String(dataRequestModal.requestTemplateQuery || "").trim().toLowerCase(); + const rows = Array.isArray(dataRequestModal.templateList) ? dataRequestModal.templateList : []; + if (!query) return rows.slice(0, 8); + return rows.filter((item) => String((item == null ? void 0 : item.name) || "").toLowerCase().includes(query)).slice(0, 8); + }, [dataRequestModal.requestTemplateQuery, dataRequestModal.templateList]); + const filteredCatalogFields = useMemo(() => { + const query = String(dataRequestModal.catalogFieldQuery || "").trim().toLowerCase(); + const rows = Array.isArray(dataRequestModal.templates) ? dataRequestModal.templates : []; + if (!query) return rows.slice(0, 10); + return rows.filter((item) => { + const label = String((item == null ? void 0 : item.label) || "").toLowerCase(); + const key = String((item == null ? void 0 : item.key) || "").toLowerCase(); + return label.includes(query) || key.includes(query); + }).slice(0, 10); + }, [dataRequestModal.catalogFieldQuery, dataRequestModal.templates]); + const requestTemplateActionMode = selectedRequestTemplateCandidate ? "save" : String(dataRequestModal.requestTemplateQuery || "").trim() ? "create" : ""; + const catalogFieldActionMode = selectedCatalogFieldCandidate ? "add" : String(dataRequestModal.catalogFieldQuery || "").trim() ? "create" : ""; + const requestTemplateBadge = useMemo(() => { + const query = String(dataRequestModal.requestTemplateQuery || "").trim(); + if (!query) return null; + const matched = selectedRequestTemplateCandidate; + if (!matched) return { kind: "create", label: "\u041D\u043E\u0432\u044B\u0439 \u0448\u0430\u0431\u043B\u043E\u043D" }; + const roleCode = String(viewerRole || "").toUpperCase(); + const actorId = String(viewerUserId || "").trim(); + const ownerId = String(matched.created_by_admin_id || "").trim(); + if (roleCode === "LAWYER" && ownerId && actorId && ownerId !== actorId) { + return { kind: "readonly", label: "\u0427\u0443\u0436\u043E\u0439 \u0448\u0430\u0431\u043B\u043E\u043D" }; + } + return { kind: "existing", label: "\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0439 \u0448\u0430\u0431\u043B\u043E\u043D" }; + }, [dataRequestModal.requestTemplateQuery, selectedRequestTemplateCandidate, viewerRole, viewerUserId]); + const canSaveSelectedRequestTemplate = useMemo(() => { + if (!String(dataRequestModal.requestTemplateQuery || "").trim()) return false; + if (!requestTemplateBadge) return true; + return requestTemplateBadge.kind !== "readonly"; + }, [dataRequestModal.requestTemplateQuery, requestTemplateBadge]); + const attachmentById = useMemo(() => { + const map = /* @__PURE__ */ new Map(); + safeAttachments.forEach((item) => { + const id = String((item == null ? void 0 : item.id) || "").trim(); + if (id) map.set(id, item); + }); + return map; + }, [safeAttachments]); + const statusOptions = useMemo( + () => safeAvailableStatuses.filter((item) => item && item.code).map((item) => ({ + code: String(item.code), + name: String(item.name || "").trim() || humanizeKey(item.code), + groupName: item.status_group_name ? String(item.status_group_name) : "", + isTerminal: Boolean(item.is_terminal) + })), + [safeAvailableStatuses] + ); + const statusByCode = useMemo(() => new Map(statusOptions.map((item) => [item.code, item])), [statusOptions]); + const toDateTimeLocalValue = (value) => { + if (!value) return ""; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ""; + const pad = (n) => String(n).padStart(2, "0"); + return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + "T" + pad(date.getHours()) + ":" + pad(date.getMinutes()); + }; + const defaultImportantDateLocal = useMemo(() => { + const source = String(currentImportantDateAt || (row == null ? void 0 : row.important_date_at) || "").trim(); + if (source) { + const local = toDateTimeLocalValue(source); + if (local) return local; + } + const next = new Date(Date.now() + 3 * 24 * 60 * 60 * 1e3); + return toDateTimeLocalValue(next.toISOString()); + }, [currentImportantDateAt, row == null ? void 0 : row.important_date_at]); + const formatDuration = (seconds) => { + const total = Number(seconds); + if (!Number.isFinite(total) || total < 0) return "\u2014"; + const days = Math.floor(total / 86400); + const hours = Math.floor(total % 86400 / 3600); + const minutes = Math.floor(total % 3600 / 60); + if (days > 0) return days + " \u0434 " + hours + " \u0447"; + if (hours > 0) return hours + " \u0447 " + minutes + " \u043C\u0438\u043D"; + return Math.max(0, minutes) + " \u043C\u0438\u043D"; + }; + const formatMoneyInput = (value) => { + const amount = Number(value); + if (!Number.isFinite(amount) || amount <= 0) return ""; + return String(Math.round((amount + Number.EPSILON) * 100) / 100); + }; + const openFinanceIssueForm = () => { + var _a2, _b2, _c2, _d, _e; + const defaultAmount = (_e = (_d = (_c2 = (_b2 = (_a2 = finance == null ? void 0 : finance.request_cost) != null ? _a2 : row == null ? void 0 : row.request_cost) != null ? _b2 : row == null ? void 0 : row.invoice_amount) != null ? _c2 : finance == null ? void 0 : finance.effective_rate) != null ? _d : row == null ? void 0 : row.effective_rate) != null ? _e : ""; + setFinanceIssueForm({ + open: true, + saving: false, + amount: formatMoneyInput(defaultAmount), + serviceDescription: String((row == null ? void 0 : row.topic_name) || (row == null ? void 0 : row.topic_code) || "\u042E\u0440\u0438\u0434\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0443\u0441\u043B\u0443\u0433\u0438"), + payerDisplayName: String((row == null ? void 0 : row.client_name) || "").trim() || "\u041A\u043B\u0438\u0435\u043D\u0442", + error: "" + }); + }; + const closeFinanceIssueForm = () => { + setFinanceIssueForm((prev) => ({ ...prev, open: false, saving: false, error: "" })); + }; + const closeFinanceModal = () => { + setFinanceOpen(false); + closeFinanceIssueForm(); + }; + const submitFinanceIssueForm = async (event) => { + if (event && typeof event.preventDefault === "function") event.preventDefault(); + if (!(row == null ? void 0 : row.id) || typeof onIssueInvoice !== "function") return; + const normalizedAmount = Number(String(financeIssueForm.amount || "").replace(",", ".")); + if (!Number.isFinite(normalizedAmount) || normalizedAmount <= 0) { + setFinanceIssueForm((prev) => ({ ...prev, error: "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u0443\u044E \u0441\u0443\u043C\u043C\u0443 \u0441\u0447\u0435\u0442\u0430" })); + return; + } + setFinanceIssueForm((prev) => ({ ...prev, saving: true, error: "" })); + try { + await onIssueInvoice({ + requestId: String(row.id), + amount: normalizedAmount, + serviceDescription: String(financeIssueForm.serviceDescription || ""), + payerDisplayName: String(financeIssueForm.payerDisplayName || "") + }); + setFinanceIssueForm((prev) => ({ ...prev, open: false, saving: false, error: "" })); + } catch (error) { + setFinanceIssueForm((prev) => ({ ...prev, saving: false, error: (error == null ? void 0 : error.message) || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0447\u0435\u0442" })); + } + }; + const openStatusChangeModal = (preset) => { + const suggested = Array.isArray(preset == null ? void 0 : preset.suggestedStatuses) ? preset.suggestedStatuses.filter(Boolean) : []; + const currentCode = String((row == null ? void 0 : row.status_code) || "").trim(); + const firstSuggested = suggested.find((code) => code && code !== currentCode) || ""; + setStatusChangeModal({ + open: true, + saving: false, + statusCode: firstSuggested, + allowedStatusCodes: suggested.length ? suggested : null, + importantDateAt: defaultImportantDateLocal, + comment: "", + files: [], + error: "" + }); + }; + const closeStatusChangeModal = () => { + setStatusChangeModal((prev) => ({ ...prev, open: false, saving: false, error: "", files: [] })); + }; + useEffect(() => { + if (!pendingStatusChangePreset) return; + openStatusChangeModal(pendingStatusChangePreset); + if (typeof onConsumePendingStatusChangePreset === "function") onConsumePendingStatusChangePreset(); + }, [pendingStatusChangePreset]); + const requestDataListItems = useMemo(() => { + const byKey = /* @__PURE__ */ new Map(); + const messagesChrono = [...safeMessages].sort((a, b) => { + const at = new Date((a == null ? void 0 : a.created_at) || 0).getTime(); + const bt = new Date((b == null ? void 0 : b.created_at) || 0).getTime(); + if (at !== bt) return at - bt; + return String((a == null ? void 0 : a.id) || "").localeCompare(String((b == null ? void 0 : b.id) || ""), "ru"); + }); + messagesChrono.forEach((msg) => { + if (String((msg == null ? void 0 : msg.message_kind) || "") !== "REQUEST_DATA") return; + const items = Array.isArray(msg == null ? void 0 : msg.request_data_items) ? msg.request_data_items : []; + items.forEach((item, idx) => { + const key = String((item == null ? void 0 : item.key) || (item == null ? void 0 : item.id) || "item-" + idx); + if (!key) return; + byKey.set(key, { + id: String((item == null ? void 0 : item.id) || ""), + key, + label: String((item == null ? void 0 : item.label) || (item == null ? void 0 : item.label_short) || key), + field_type: String((item == null ? void 0 : item.field_type) || "string").toLowerCase(), + value_text: (item == null ? void 0 : item.value_text) == null ? "" : String(item.value_text), + is_filled: Boolean(item == null ? void 0 : item.is_filled), + source_message_id: String((msg == null ? void 0 : msg.id) || ""), + source_message_created_at: (msg == null ? void 0 : msg.created_at) || null, + value_file: (item == null ? void 0 : item.value_file) || null + }); + }); + }); + return Array.from(byKey.values()).sort((a, b) => { + const aFilled = a.is_filled ? 1 : 0; + const bFilled = b.is_filled ? 1 : 0; + if (aFilled !== bFilled) return aFilled - bFilled; + return String(a.label || a.key).localeCompare(String(b.label || b.key), "ru"); + }); + }, [safeMessages]); + const attachmentsByMessageId = useMemo(() => { + const map = /* @__PURE__ */ new Map(); + safeAttachments.forEach((item) => { + const messageId = String((item == null ? void 0 : item.message_id) || "").trim(); + if (!messageId) return; + if (!map.has(messageId)) map.set(messageId, []); + map.get(messageId).push(item); + }); + return map; + }, [safeAttachments]); + const localActivityCursor = useMemo(() => { + let latestTs = 0; + const pickLatest = (value) => { + if (!value) return; + const ts = new Date(value).getTime(); + if (Number.isFinite(ts) && ts > latestTs) latestTs = ts; + }; + safeMessages.forEach((item) => { + pickLatest(item == null ? void 0 : item.updated_at); + pickLatest(item == null ? void 0 : item.created_at); + }); + safeAttachments.forEach((item) => { + pickLatest(item == null ? void 0 : item.updated_at); + pickLatest(item == null ? void 0 : item.created_at); + }); + return latestTs > 0 ? new Date(latestTs).toISOString() : ""; + }, [safeAttachments, safeMessages]); + const typingHintText = useMemo(() => { + const rows = Array.isArray(typingPeers) ? typingPeers : []; + if (!rows.length) return ""; + const labels = rows.map((item) => String((item == null ? void 0 : item.actor_label) || (item == null ? void 0 : item.label) || "").trim()).filter(Boolean); + if (!labels.length) return "\u0421\u043E\u0431\u0435\u0441\u0435\u0434\u043D\u0438\u043A \u043F\u0435\u0447\u0430\u0442\u0430\u0435\u0442..."; + const unique = []; + labels.forEach((label) => { + if (!unique.includes(label)) unique.push(label); + }); + if (unique.length === 1) return unique[0] + " \u043F\u0435\u0447\u0430\u0442\u0430\u0435\u0442..."; + if (unique.length === 2) return unique[0] + " \u0438 " + unique[1] + " \u043F\u0435\u0447\u0430\u0442\u0430\u044E\u0442..."; + return unique[0] + ", " + unique[1] + " \u0438 \u0435\u0449\u0435 " + String(unique.length - 2) + " \u043F\u0435\u0447\u0430\u0442\u0430\u044E\u0442..."; + }, [typingPeers]); + const openAttachmentFromMessage = (item) => { + if (!(item == null ? void 0 : item.download_url)) return; + const kind = detectAttachmentPreviewKind(item.file_name, item.mime_type); + if (kind === "none") { + window.open(String(item.download_url), "_blank", "noopener,noreferrer"); + return; + } + openPreview(item); + }; + const downloadAttachment = (item) => { + const url = String((item == null ? void 0 : item.download_url) || "").trim(); + if (!url) return; + const link = document.createElement("a"); + link.href = url; + link.target = "_blank"; + link.rel = "noreferrer"; + const fileName = String((item == null ? void 0 : item.file_name) || "").trim(); + if (fileName) link.download = fileName; + document.body.appendChild(link); + link.click(); + link.remove(); + }; + useEffect(() => { + liveCursorRef.current = localActivityCursor || ""; + }, [localActivityCursor, row == null ? void 0 : row.id]); + useEffect(() => { + if (!row || typeof onLiveProbe !== "function") { + setTypingPeers([]); + setLiveMode("online"); + if (liveTimerRef.current) { + clearTimeout(liveTimerRef.current); + liveTimerRef.current = null; + } + liveInFlightRef.current = false; + liveFailCountRef.current = 0; + return void 0; + } + let cancelled = false; + const scheduleNext = (ms) => { + if (cancelled) return; + if (liveTimerRef.current) clearTimeout(liveTimerRef.current); + liveTimerRef.current = setTimeout(runProbe, ms); + }; + const runProbe = async () => { + if (cancelled || liveInFlightRef.current) return; + liveInFlightRef.current = true; + try { + const payload = await onLiveProbe({ cursor: liveCursorRef.current }); + const cursor = String((payload == null ? void 0 : payload.cursor) || "").trim(); + if (cursor) liveCursorRef.current = cursor; + setTypingPeers(Array.isArray(payload == null ? void 0 : payload.typing) ? payload.typing : []); + liveFailCountRef.current = 0; + setLiveMode("online"); + } catch (_) { + liveFailCountRef.current += 1; + setLiveMode(liveFailCountRef.current >= 3 ? "degraded" : "online"); + } finally { + liveInFlightRef.current = false; + const hidden = typeof document !== "undefined" && document.visibilityState === "hidden"; + const baseInterval = hidden ? 8e3 : 2500; + const failStep = Math.min(5, Math.max(0, liveFailCountRef.current)); + const backoffInterval = failStep > 0 ? Math.min(3e4, baseInterval * Math.pow(2, failStep - 1)) : baseInterval; + scheduleNext(backoffInterval); + } + }; + runProbe(); + return () => { + cancelled = true; + if (liveTimerRef.current) { + clearTimeout(liveTimerRef.current); + liveTimerRef.current = null; + } + liveInFlightRef.current = false; + liveFailCountRef.current = 0; + setTypingPeers([]); + setLiveMode("online"); + }; + }, [onLiveProbe, row, trackNumber]); + const typingEnabled = Boolean( + row && typeof onTypingSignal === "function" && !loading && !fileUploading && composerFocused && String(messageDraft || "").trim() + ); + useEffect(() => { + if (typeof onTypingSignal !== "function" || !row) { + if (typingHeartbeatRef.current) { + clearInterval(typingHeartbeatRef.current); + typingHeartbeatRef.current = null; + } + typingActiveRef.current = false; + return; + } + if (typingEnabled) { + if (!typingActiveRef.current) { + typingActiveRef.current = true; + void onTypingSignal({ typing: true }).catch(() => null); + } + if (!typingHeartbeatRef.current) { + typingHeartbeatRef.current = setInterval(() => { + void onTypingSignal({ typing: true }).catch(() => null); + }, 2500); + } + return; + } + if (typingHeartbeatRef.current) { + clearInterval(typingHeartbeatRef.current); + typingHeartbeatRef.current = null; + } + if (typingActiveRef.current) { + typingActiveRef.current = false; + void onTypingSignal({ typing: false }).catch(() => null); + } + }, [onTypingSignal, row, typingEnabled]); + useEffect( + () => () => { + if (typingHeartbeatRef.current) { + clearInterval(typingHeartbeatRef.current); + typingHeartbeatRef.current = null; + } + if (typingActiveRef.current && typeof onTypingSignal === "function") { + typingActiveRef.current = false; + void onTypingSignal({ typing: false }).catch(() => null); + } + }, + [onTypingSignal] + ); + const newDataRequestRow = (source) => { + const item = source || {}; + const label = String(item.label || "").trim(); + const key = String(item.key || "").trim(); + const fieldTypeRaw = String(item.field_type || item.value_type || "string").trim().toLowerCase(); + const fieldType = ["string", "text", "date", "number", "file"].includes(fieldTypeRaw) ? fieldTypeRaw : "string"; + return { + localId: "row-" + Math.random().toString(36).slice(2), + id: item.id ? String(item.id) : "", + topic_template_id: item.topic_template_id ? String(item.topic_template_id) : item.id ? String(item.id) : "", + key, + label: label || "\u041F\u043E\u043B\u0435", + field_type: fieldType, + document_name: String(item.document_name || "").trim(), + value_text: item.value_text == null ? "" : String(item.value_text), + value_file: item.value_file || null, + is_filled: Boolean(item.is_filled) + }; + }; + const getRequestDataRowIdentity = (item) => { + const rowItem = item || {}; + const key = String(rowItem.key || "").trim().toLowerCase(); + if (key) return "key:" + key; + const tplId = String(rowItem.topic_template_id || rowItem.id || "").trim(); + if (tplId) return "tpl:" + tplId; + return "label:" + String(rowItem.label || "").trim().toLowerCase(); + }; + const mergeRequestDataRows = (baseRows, incomingRows) => { + const rows = Array.isArray(baseRows) ? [...baseRows] : []; + const nextItems = Array.isArray(incomingRows) ? incomingRows : []; + const seen = new Set(rows.map((rowItem) => getRequestDataRowIdentity(rowItem))); + nextItems.forEach((rowItem) => { + const identity = getRequestDataRowIdentity(rowItem); + if (!identity || seen.has(identity)) return; + seen.add(identity); + rows.push(rowItem); + }); + return rows; + }; + const openCreateDataRequestModal = async () => { + if (!canRequestData || typeof onLoadRequestDataTemplates !== "function") return; + setDataRequestModal((prev) => ({ + ...prev, + open: true, + loading: true, + saving: false, + savingTemplate: false, + messageId: "", + rows: [], + error: "", + templateStatus: "", + requestTemplateQuery: "", + catalogFieldQuery: "", + selectedCatalogTemplateId: "", + selectedRequestTemplateId: "", + templateName: "", + documentName: "", + customLabel: "", + customType: "string" + })); + try { + const data = await onLoadRequestDataTemplates(); + setDataRequestModal((prev) => ({ + ...prev, + open: true, + loading: false, + templates: Array.isArray(data == null ? void 0 : data.rows) ? data.rows : [], + templateList: Array.isArray(data == null ? void 0 : data.templates) ? data.templates : [], + availableDocuments: Array.isArray(data == null ? void 0 : data.documents) ? data.documents : [], + documentName: "", + requestTemplateQuery: "", + catalogFieldQuery: "" + })); + } catch (error) { + setDataRequestModal((prev) => ({ ...prev, loading: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D\u044B" })); + } + }; + const openEditDataRequestModal = async (messageId) => { + if (!canRequestData || !messageId) return; + setDataRequestModal((prev) => ({ + ...prev, + open: true, + loading: true, + saving: false, + savingTemplate: false, + messageId: String(messageId), + rows: [], + error: "", + templateStatus: "", + requestTemplateQuery: "", + catalogFieldQuery: "", + selectedCatalogTemplateId: "", + selectedRequestTemplateId: "", + templateName: "" + })); + try { + const [batch, templates] = await Promise.all([ + typeof onLoadRequestDataBatch === "function" ? onLoadRequestDataBatch(messageId) : Promise.resolve({ items: [] }), + typeof onLoadRequestDataTemplates === "function" ? onLoadRequestDataTemplates() : Promise.resolve({ rows: [], documents: [], templates: [] }) + ]); + setDataRequestModal((prev) => ({ + ...prev, + open: true, + loading: false, + messageId: String(messageId), + rows: Array.isArray(batch == null ? void 0 : batch.items) ? batch.items.map(newDataRequestRow) : [], + documentName: String((batch == null ? void 0 : batch.document_name) || ""), + templates: Array.isArray(templates == null ? void 0 : templates.rows) ? templates.rows : [], + templateList: Array.isArray(templates == null ? void 0 : templates.templates) ? templates.templates : [], + availableDocuments: Array.isArray(templates == null ? void 0 : templates.documents) ? templates.documents : [], + requestTemplateQuery: "", + catalogFieldQuery: "" + })); + } catch (error) { + setDataRequestModal((prev) => ({ ...prev, loading: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441" })); + } + }; + const closeDataRequestModal = () => { + setDataRequestModal((prev) => ({ ...prev, open: false, error: "", saving: false, savingTemplate: false, templateStatus: "" })); + }; + const findRequestTemplateByQuery = (queryValue) => { + const query = String(queryValue || "").trim().toLowerCase(); + if (!query) return null; + return (dataRequestModal.templateList || []).find((item) => { + const id = String((item == null ? void 0 : item.id) || "").toLowerCase(); + const name = String((item == null ? void 0 : item.name) || "").toLowerCase(); + return query === id || query === name; + }) || null; + }; + const findCatalogFieldByQuery = (queryValue) => { + const query = String(queryValue || "").trim().toLowerCase(); + if (!query) return null; + return (dataRequestModal.templates || []).find((item) => { + const id = String((item == null ? void 0 : item.id) || "").toLowerCase(); + const key = String((item == null ? void 0 : item.key) || "").toLowerCase(); + const label = String((item == null ? void 0 : item.label) || "").toLowerCase(); + return query === id || query === key || query === label; + }) || null; + }; + const applyRequestTemplateById = async (rawTemplateId, templateNameHint) => { + if (typeof onLoadRequestDataTemplateDetails !== "function") return; + const templateId = String(rawTemplateId || "").trim(); + if (!templateId) return; + setDataRequestModal((prev) => ({ ...prev, loading: true, error: "" })); + try { + const data = await onLoadRequestDataTemplateDetails(templateId); + const incomingRows = (Array.isArray(data == null ? void 0 : data.items) ? data.items : []).map( + (item) => newDataRequestRow({ + ...item, + topic_template_id: item.topic_data_template_id || item.topic_template_id || "", + field_type: item.value_type || item.field_type + }) + ); + setDataRequestModal((prev) => { + var _a2, _b2; + return { + ...prev, + loading: false, + rows: mergeRequestDataRows(prev.rows, incomingRows), + selectedRequestTemplateId: String(((_a2 = data == null ? void 0 : data.template) == null ? void 0 : _a2.id) || prev.selectedRequestTemplateId || ""), + requestTemplateQuery: String(((_b2 = data == null ? void 0 : data.template) == null ? void 0 : _b2.name) || templateNameHint || prev.requestTemplateQuery || ""), + templateStatus: "" + }; + }); + } catch (error) { + setDataRequestModal((prev) => ({ ...prev, loading: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D" })); + } + }; + const applySelectedRequestTemplate = async () => { + const selectedByQuery = findRequestTemplateByQuery(dataRequestModal.requestTemplateQuery); + const templateId = String((selectedByQuery == null ? void 0 : selectedByQuery.id) || dataRequestModal.selectedRequestTemplateId || "").trim(); + return applyRequestTemplateById(templateId, (selectedByQuery == null ? void 0 : selectedByQuery.name) || ""); + }; + const refreshDataRequestCatalog = async () => { + if (typeof onLoadRequestDataTemplates !== "function") return null; + const data = await onLoadRequestDataTemplates(); + setDataRequestModal((prev) => ({ + ...prev, + templates: Array.isArray(data == null ? void 0 : data.rows) ? data.rows : [], + templateList: Array.isArray(data == null ? void 0 : data.templates) ? data.templates : [], + availableDocuments: Array.isArray(data == null ? void 0 : data.documents) ? data.documents : [], + selectedRequestTemplateId: prev.selectedRequestTemplateId && (Array.isArray(data == null ? void 0 : data.templates) ? data.templates : []).some((item) => String(item == null ? void 0 : item.id) === String(prev.selectedRequestTemplateId)) ? prev.selectedRequestTemplateId : "" + })); + return data; + }; + const saveCurrentDataRequestTemplate = async () => { + if (typeof onSaveRequestDataTemplate !== "function") return; + const selectedFromQuery = findRequestTemplateByQuery(dataRequestModal.requestTemplateQuery); + const templateName = String(dataRequestModal.requestTemplateQuery || "").trim(); + const rows = (dataRequestModal.rows || []).filter((row2) => String(row2.label || "").trim()); + if (!templateName) { + setDataRequestModal((prev) => ({ ...prev, error: "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0448\u0430\u0431\u043B\u043E\u043D\u0430" })); + return; + } + if (!rows.length) { + setDataRequestModal((prev) => ({ ...prev, error: "\u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u043D\u043E \u043F\u043E\u043B\u0435 \u0434\u043B\u044F \u0448\u0430\u0431\u043B\u043E\u043D\u0430" })); + return; + } + setDataRequestModal((prev) => ({ ...prev, savingTemplate: true, error: "", templateStatus: "" })); + try { + const result = await onSaveRequestDataTemplate({ + template_id: String((selectedFromQuery == null ? void 0 : selectedFromQuery.id) || dataRequestModal.selectedRequestTemplateId || "").trim() || void 0, + name: templateName, + items: rows.map((row2) => ({ + topic_data_template_id: row2.topic_template_id || void 0, + key: row2.key || void 0, + label: row2.label, + value_type: row2.field_type || "string" + })) + }); + const savedRows = (Array.isArray(result == null ? void 0 : result.items) ? result.items : []).map( + (item) => newDataRequestRow({ + ...item, + topic_template_id: item.topic_data_template_id || item.topic_template_id || "", + field_type: item.value_type || item.field_type + }) + ); + setDataRequestModal((prev) => { + var _a2, _b2; + return { + ...prev, + savingTemplate: false, + rows: savedRows.length ? savedRows : prev.rows, + selectedRequestTemplateId: String(((_a2 = result == null ? void 0 : result.template) == null ? void 0 : _a2.id) || prev.selectedRequestTemplateId || ""), + requestTemplateQuery: String(((_b2 = result == null ? void 0 : result.template) == null ? void 0 : _b2.name) || templateName), + templateStatus: "\u0428\u0430\u0431\u043B\u043E\u043D \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D" + }; + }); + await refreshDataRequestCatalog(); + } catch (error) { + setDataRequestModal((prev) => ({ ...prev, savingTemplate: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D" })); + } + }; + const addSelectedTemplateRow = () => { + const selectedByQuery = findCatalogFieldByQuery(dataRequestModal.catalogFieldQuery); + const templateId = String((selectedByQuery == null ? void 0 : selectedByQuery.id) || dataRequestModal.selectedCatalogTemplateId || "").trim(); + const template = (dataRequestModal.templates || []).find((item) => String(item.id) === templateId); + if (!template) { + const manualLabel = String(dataRequestModal.catalogFieldQuery || "").trim(); + if (!manualLabel) return; + setDataRequestModal((prev) => ({ + ...prev, + catalogFieldQuery: "", + templateStatus: "", + rows: [...prev.rows || [], newDataRequestRow({ label: manualLabel, field_type: "string" })] + })); + return; + } + setDataRequestModal((prev) => { + const exists = (prev.rows || []).some((row2) => String(row2.key || "") === String(template.key || "")); + if (exists) return { ...prev, selectedCatalogTemplateId: "", catalogFieldQuery: "" }; + return { + ...prev, + selectedCatalogTemplateId: "", + catalogFieldQuery: "", + templateStatus: "", + rows: [...prev.rows || [], newDataRequestRow({ ...template, topic_template_id: template.id, field_type: template.value_type })] + }; + }); + }; + const updateDataRequestRow = (localId, patch) => { + setDataRequestModal((prev) => ({ + ...prev, + templateStatus: "", + rows: (prev.rows || []).map((row2) => row2.localId === localId ? { ...row2, ...patch || {} } : row2) + })); + }; + const removeDataRequestRow = (localId) => { + setDataRequestModal((prev) => ({ + ...prev, + templateStatus: "", + rows: (prev.rows || []).filter((row2) => row2.localId !== localId) + })); + }; + const moveDataRequestRow = (localId, delta) => { + const shift = Number(delta) || 0; + if (!shift) return; + setDataRequestModal((prev) => { + const rows = Array.isArray(prev.rows) ? [...prev.rows] : []; + const index = rows.findIndex((row2) => row2.localId === localId); + if (index < 0) return prev; + const nextIndex = index + shift; + if (nextIndex < 0 || nextIndex >= rows.length) return prev; + const [item] = rows.splice(index, 1); + rows.splice(nextIndex, 0, item); + return { ...prev, templateStatus: "", rows }; + }); + }; + const moveDataRequestRowToIndex = (localId, targetIndexRaw) => { + const targetIndex = Number(targetIndexRaw); + if (!Number.isInteger(targetIndex)) return; + setDataRequestModal((prev) => { + const rows = Array.isArray(prev.rows) ? [...prev.rows] : []; + const fromIndex = rows.findIndex((rowItem) => rowItem.localId === localId); + if (fromIndex < 0) return prev; + const boundedIndex = Math.max(0, Math.min(rows.length - 1, targetIndex)); + if (fromIndex === boundedIndex) return prev; + const [item] = rows.splice(fromIndex, 1); + rows.splice(boundedIndex, 0, item); + return { ...prev, templateStatus: "", rows }; + }); + }; + const submitDataRequestModal = async () => { + if (typeof onSaveRequestDataBatch !== "function") return; + const rows = (dataRequestModal.rows || []).filter((row2) => String(row2.label || "").trim()); + if (!rows.length) { + setDataRequestModal((prev) => ({ ...prev, error: "\u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u043D\u043E \u043F\u043E\u043B\u0435" })); + return; + } + setDataRequestModal((prev) => ({ ...prev, saving: true, error: "" })); + try { + await onSaveRequestDataBatch({ + message_id: dataRequestModal.messageId || void 0, + items: rows.map((row2) => ({ + id: row2.id || void 0, + topic_template_id: row2.topic_template_id || void 0, + key: row2.key || void 0, + label: row2.label, + field_type: row2.field_type || "string", + document_name: row2.document_name || void 0 + })) + }); + closeDataRequestModal(); + } catch (error) { + setDataRequestModal((prev) => ({ ...prev, saving: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441" })); + } + }; + const closeClientDataModal = () => { + setClientDataModal({ + open: false, + loading: false, + saving: false, + messageId: "", + items: [], + status: "", + error: "" + }); + }; + const openClientDataRequestModal = async (messageId) => { + if (!canFillRequestData || typeof onLoadRequestDataBatch !== "function" || !messageId) return; + setClientDataModal({ + open: true, + loading: true, + saving: false, + messageId: String(messageId), + items: [], + status: "", + error: "" + }); + try { + const data = await onLoadRequestDataBatch(String(messageId)); + const items = Array.isArray(data == null ? void 0 : data.items) ? data.items.slice().sort((a, b) => Number((a == null ? void 0 : a.sort_order) || 0) - Number((b == null ? void 0 : b.sort_order) || 0)).map((item, index) => ({ + localId: "client-data-" + String((item == null ? void 0 : item.id) || (item == null ? void 0 : item.key) || index), + id: String((item == null ? void 0 : item.id) || ""), + key: String((item == null ? void 0 : item.key) || ""), + label: String((item == null ? void 0 : item.label) || (item == null ? void 0 : item.key) || "\u041F\u043E\u043B\u0435"), + field_type: String((item == null ? void 0 : item.field_type) || "string").toLowerCase(), + value_text: (item == null ? void 0 : item.value_text) == null ? "" : String(item.value_text), + value_file: (item == null ? void 0 : item.value_file) || null, + pendingFile: null + })) : []; + setClientDataModal((prev) => ({ ...prev, loading: false, items })); + } catch (error) { + setClientDataModal((prev) => ({ ...prev, loading: false, error: (error == null ? void 0 : error.message) || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441 \u0434\u0430\u043D\u043D\u044B\u0445" })); + } + }; + const updateClientDataItem = (localId, patch) => { + setClientDataModal((prev) => ({ + ...prev, + status: "", + error: "", + items: (prev.items || []).map((item) => item.localId === localId ? { ...item, ...patch || {} } : item) + })); + }; + const submitClientDataModal = async (event) => { + if (event && typeof event.preventDefault === "function") event.preventDefault(); + if (!canFillRequestData || typeof onSaveRequestDataValues !== "function") return; + const currentMessageId = String(clientDataModal.messageId || "").trim(); + if (!currentMessageId) return; + setClientDataModal((prev) => ({ ...prev, saving: true, status: "", error: "" })); + try { + const payloadItems = []; + for (const item of clientDataModal.items || []) { + const fieldType = String((item == null ? void 0 : item.field_type) || "string").toLowerCase(); + if (fieldType === "file") { + let attachmentId = String((item == null ? void 0 : item.value_text) || "").trim(); + if (item == null ? void 0 : item.pendingFile) { + if (typeof onUploadRequestAttachment !== "function") { + throw new Error("\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0444\u0430\u0439\u043B\u0430 \u0434\u043B\u044F \u043F\u043E\u043B\u044F \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430"); + } + const uploadResult = await onUploadRequestAttachment(item.pendingFile, { + source: "data_request", + message_id: currentMessageId, + key: String((item == null ? void 0 : item.key) || "") + }); + attachmentId = String( + uploadResult && (uploadResult.attachment_id || uploadResult.id || uploadResult.value || uploadResult) || "" + ).trim(); + if (!attachmentId) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u043E\u043B\u044F \u0437\u0430\u043F\u0440\u043E\u0441\u0430"); + } + payloadItems.push({ + id: String((item == null ? void 0 : item.id) || ""), + key: String((item == null ? void 0 : item.key) || ""), + attachment_id: attachmentId || "", + value_text: attachmentId || "" + }); + continue; + } + payloadItems.push({ + id: String((item == null ? void 0 : item.id) || ""), + key: String((item == null ? void 0 : item.key) || ""), + value_text: String((item == null ? void 0 : item.value_text) || "") + }); + } + await onSaveRequestDataValues({ + message_id: currentMessageId, + items: payloadItems + }); + closeClientDataModal(); + } catch (error) { + setClientDataModal((prev) => ({ + ...prev, + saving: false, + error: (error == null ? void 0 : error.message) || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435" + })); + } + }; + const handleRequestRowDragStart = (event, rowItem, rowLocked) => { + if (rowLocked || dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate) { + event.preventDefault(); + return; + } + setDraggedRequestRowId(String(rowItem.localId || "")); + setDragOverRequestRowId(String(rowItem.localId || "")); + try { + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", String(rowItem.localId || "")); + } catch (_error) { + } + }; + const handleRequestRowDragEnd = () => { + setDraggedRequestRowId(""); + setDragOverRequestRowId(""); + }; + const appendStatusChangeFiles = (files) => { + const list = Array.isArray(files) ? files.filter(Boolean) : []; + if (!list.length) return; + setStatusChangeModal((prev) => { + const existing = Array.isArray(prev.files) ? prev.files : []; + 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, files: next }; + }); + }; + const removeStatusChangeFile = (index) => { + setStatusChangeModal((prev) => { + const files = Array.isArray(prev.files) ? [...prev.files] : []; + files.splice(index, 1); + return { ...prev, files }; + }); + }; + const submitStatusChange = async (event) => { + if (event && typeof event.preventDefault === "function") event.preventDefault(); + if (!(row == null ? void 0 : row.id) || typeof onChangeStatus !== "function") return; + const nextStatus = String(statusChangeModal.statusCode || "").trim(); + if (!nextStatus) { + setStatusChangeModal((prev) => ({ ...prev, error: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043D\u043E\u0432\u044B\u0439 \u0441\u0442\u0430\u0442\u0443\u0441" })); + return; + } + if (nextStatus === String((row == null ? void 0 : row.status_code) || "").trim()) { + setStatusChangeModal((prev) => ({ ...prev, error: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441, \u043E\u0442\u043B\u0438\u0447\u043D\u044B\u0439 \u043E\u0442 \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E" })); + return; + } + setStatusChangeModal((prev) => ({ ...prev, saving: true, error: "" })); + try { + const localValue = String(statusChangeModal.importantDateAt || "").trim(); + const importantDateIso = localValue ? new Date(localValue).toISOString() : ""; + await onChangeStatus({ + requestId: String(row.id), + statusCode: nextStatus, + importantDateAt: importantDateIso || null, + comment: statusChangeModal.comment || "", + files: statusChangeModal.files || [] + }); + closeStatusChangeModal(); + } catch (error) { + setStatusChangeModal((prev) => ({ ...prev, saving: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441" })); + } + }; + const chatTimelineItems = []; + let previousDate = ""; + const timelineSource = []; + safeMessages.forEach((item) => { + timelineSource.push({ + type: "message", + key: "msg-" + String((item == null ? void 0 : item.id) || Math.random()), + created_at: (item == null ? void 0 : item.created_at) || null, + payload: item + }); + }); + safeAttachments.filter((item) => !String((item == null ? void 0 : item.message_id) || "").trim()).forEach((item) => { + timelineSource.push({ + type: "file", + key: "file-" + String((item == null ? void 0 : item.id) || Math.random()), + created_at: (item == null ? void 0 : item.created_at) || null, + payload: item + }); + }); + timelineSource.sort((a, b) => { + const aTime = new Date(a.created_at || 0).getTime(); + const bTime = new Date(b.created_at || 0).getTime(); + if (!Number.isFinite(aTime) && !Number.isFinite(bTime)) return 0; + if (!Number.isFinite(aTime)) return 1; + if (!Number.isFinite(bTime)) return -1; + if (aTime !== bTime) return aTime - bTime; + return String(a.key).localeCompare(String(b.key), "ru"); + }); + timelineSource.forEach((entry, index) => { + const dateLabel = fmtDateOnly(entry.created_at); + const normalizedDate = dateLabel && dateLabel !== "-" ? dateLabel : "\u0411\u0435\u0437 \u0434\u0430\u0442\u044B"; + if (normalizedDate !== previousDate) { + chatTimelineItems.push({ type: "date", key: "date-" + normalizedDate + "-" + index, label: normalizedDate }); + previousDate = normalizedDate; + } + chatTimelineItems.push(entry); + }); + useEffect(() => { + if (chatTab !== "chat") return; + const listNode = chatListRef.current; + if (!listNode) return; + const cursor = String(localActivityCursor || ""); + if (!cursor || cursor === lastAutoScrollCursorRef.current) return; + lastAutoScrollCursorRef.current = cursor; + const raf = window.requestAnimationFrame(() => { + if (!chatListRef.current) return; + chatListRef.current.scrollTop = chatListRef.current.scrollHeight; + }); + return () => window.cancelAnimationFrame(raf); + }, [chatTab, localActivityCursor]); + const baseRouteNodes = Array.isArray(statusRouteNodes) && statusRouteNodes.length ? statusRouteNodes : (row == null ? void 0 : row.status_code) ? [{ code: row.status_code, name: String((row == null ? void 0 : row.status_name) || statusLabel(row.status_code) || row.status_code), state: "current", note: "\u0422\u0435\u043A\u0443\u0449\u0438\u0439 \u044D\u0442\u0430\u043F \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0438 \u0437\u0430\u044F\u0432\u043A\u0438" }] : []; + const upcomingImportantDate = useMemo(() => { + const source = String(currentImportantDateAt || (row == null ? void 0 : row.important_date_at) || "").trim(); + if (!source) return ""; + const timestamp = new Date(source).getTime(); + if (!Number.isFinite(timestamp) || timestamp <= Date.now()) return ""; + return new Date(timestamp).toISOString(); + }, [currentImportantDateAt, row == null ? void 0 : row.important_date_at]); + const routeNodes = useMemo(() => { + if (viewerRoleCode !== "CLIENT" && viewerRoleCode !== "LAWYER" || !upcomingImportantDate) return baseRouteNodes; + if (!Array.isArray(baseRouteNodes) || !baseRouteNodes.length) { + return [ + { + code: "__IMPORTANT_DATE__", + name: "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430", + state: "pending", + changed_at: upcomingImportantDate, + note: "\u041A\u043E\u043D\u0442\u0440\u043E\u043B\u044C\u043D\u044B\u0439 \u0441\u0440\u043E\u043A" + } + ]; + } + const hasVirtualNode = baseRouteNodes.some((node) => String((node == null ? void 0 : node.code) || "").trim() === "__IMPORTANT_DATE__"); + if (hasVirtualNode) return baseRouteNodes; + const currentIndex = baseRouteNodes.findIndex((node) => String((node == null ? void 0 : node.state) || "").trim().toLowerCase() === "current"); + const virtualNode = { + code: "__IMPORTANT_DATE__", + name: "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430", + state: "pending", + changed_at: upcomingImportantDate, + note: "\u041A\u043E\u043D\u0442\u0440\u043E\u043B\u044C\u043D\u044B\u0439 \u0441\u0440\u043E\u043A" + }; + if (currentIndex < 0) return [...baseRouteNodes, virtualNode]; + const next = [...baseRouteNodes]; + next.splice(currentIndex + 1, 0, virtualNode); + return next; + }, [baseRouteNodes, upcomingImportantDate, viewerRoleCode]); + const routeNodesForDisplay = useMemo(() => { + if (!Array.isArray(routeNodes) || !routeNodes.length) return []; + const important = []; + const current = []; + const completed = []; + const pending = []; + routeNodes.forEach((node) => { + const code = String((node == null ? void 0 : node.code) || "").trim(); + const state = String((node == null ? void 0 : node.state) || "pending").trim().toLowerCase(); + if (code === "__IMPORTANT_DATE__") { + important.push(node); + return; + } + if (state === "current") { + current.push(node); + return; + } + if (state === "completed") { + completed.push(node); + return; + } + pending.push(node); + }); + return [...important, ...current, ...completed.reverse(), ...pending]; + }, [routeNodes]); + const AttachmentPreviewModal = AttachmentPreviewModalComponent; + const StatusLine = StatusLineComponent; + const renderRequestDataMessageItems = (payload) => { + var _a2; + const items = Array.isArray(payload == null ? void 0 : payload.request_data_items) ? payload.request_data_items : []; + const allFilled = Boolean(payload == null ? void 0 : payload.request_data_all_filled); + if (!items.length) return /* @__PURE__ */ React.createElement("p", { className: "chat-message-text" }, "\u0417\u0430\u043F\u0440\u043E\u0441"); + if (allFilled) { + const fileOnly = items.length === 1 && String(((_a2 = items[0]) == null ? void 0 : _a2.field_type) || "").toLowerCase() === "file"; + return /* @__PURE__ */ React.createElement("p", { className: "chat-message-text chat-request-data-collapsed" }, fileOnly ? "\u0424\u0430\u0439\u043B" : "\u0417\u0430\u043F\u043E\u043B\u043D\u0435\u043D"); + } + const visibleItems = items.slice(0, 7); + const hiddenCount = Math.max(0, items.length - visibleItems.length); + return /* @__PURE__ */ React.createElement("div", { className: "chat-request-data-list" }, visibleItems.map((item, idx) => /* @__PURE__ */ React.createElement("div", { className: "chat-request-data-item" + ((item == null ? void 0 : item.is_filled) ? " filled" : ""), key: String((item == null ? void 0 : item.id) || idx) }, /* @__PURE__ */ React.createElement("span", { className: "chat-request-data-index" }, (item == null ? void 0 : item.is_filled) ? /* @__PURE__ */ React.createElement("span", { className: "chat-request-data-check" }, "\u2713") : null, String((item == null ? void 0 : item.index) || idx + 1) + "."), /* @__PURE__ */ React.createElement("span", { className: "chat-request-data-label" }, String((item == null ? void 0 : item.label_short) || (item == null ? void 0 : item.label) || "\u041F\u043E\u043B\u0435")))), hiddenCount > 0 ? /* @__PURE__ */ React.createElement("div", { className: "chat-request-data-more" }, "... \u0435\u0449\u0435 ", hiddenCount) : null); + }; + const resolveServiceMessageContent = (payload) => { + const messageKind = String((payload == null ? void 0 : payload.message_kind) || ""); + if (messageKind === "REQUEST_DATA") return null; + const bodyRaw = String((payload == null ? void 0 : payload.body) || "").replace(/\r/g, "").trim(); + if (!bodyRaw) return null; + const lines = bodyRaw.split("\n"); + const firstLine = String(lines[0] || "").trim(); + const restLines = lines.slice(1); + const normalizeDetail = (value) => String(value || "").trim(); + const withTail = (firstDetail) => [normalizeDetail(firstDetail), ...restLines.map((line) => normalizeDetail(line)).filter(Boolean)].filter(Boolean).join("\n"); + if (firstLine === "\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443" || firstLine.startsWith("\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443:")) { + return { + title: "\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443", + text: withTail(firstLine.startsWith("\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443:") ? firstLine.slice("\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443:".length) : "") + }; + } + if (firstLine.startsWith("\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441:") || firstLine.startsWith("\u0421\u043C\u0435\u043D\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430:")) { + const source = firstLine.startsWith("\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441:") ? firstLine : firstLine.slice("\u0421\u043C\u0435\u043D\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430:".length); + const detail = firstLine.startsWith("\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441:") ? source.slice("\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441:".length) : source; + return { + title: "\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441", + text: withTail(detail) + }; + } + if (firstLine.startsWith("\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u044E\u0440\u0438\u0441\u0442:") || firstLine.startsWith("\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043E:")) { + const detail = firstLine.startsWith("\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u044E\u0440\u0438\u0441\u0442:") ? firstLine.slice("\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u044E\u0440\u0438\u0441\u0442:".length) : firstLine.slice("\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043E:".length); + return { + title: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u044E\u0440\u0438\u0441\u0442", + text: withTail(detail) + }; + } + return null; + }; + const resolveStatusDisplayName = (code, explicitName) => { + var _a2; + const explicit = String(explicitName || "").trim(); + if (explicit) return explicit; + const normalizedCode = String(code || "").trim(); + if (!normalizedCode) return "-"; + const optionName = String(((_a2 = statusByCode.get(normalizedCode)) == null ? void 0 : _a2.name) || "").trim(); + if (optionName) return optionName; + const legacyName = String(statusLabel(normalizedCode) || "").trim(); + if (legacyName && legacyName !== normalizedCode) return legacyName; + return humanizeKey(normalizedCode); + }; + const formatRequestDataValue = (item) => { + const type = String((item == null ? void 0 : item.field_type) || "string").toLowerCase(); + if (type === "date") { + const text2 = String((item == null ? void 0 : item.value_text) || "").trim(); + return text2 ? fmtDateOnly(text2) : "\u041D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E"; + } + if (type === "file") { + const attachmentId = String((item == null ? void 0 : item.value_text) || "").trim(); + const linkedAttachment = attachmentId ? attachmentById.get(attachmentId) : null; + const fileMeta = (item == null ? void 0 : item.value_file) || (linkedAttachment ? { + attachment_id: linkedAttachment.id, + file_name: linkedAttachment.file_name, + mime_type: linkedAttachment.mime_type, + size_bytes: linkedAttachment.size_bytes, + download_url: linkedAttachment.download_url + } : null); + return fileMeta || null; + } + const text = String((item == null ? void 0 : item.value_text) || "").trim(); + return text || "\u041D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E"; + }; + const currentStatusName = resolveStatusDisplayName(row == null ? void 0 : row.status_code, (row == null ? void 0 : row.status_name) || ""); + const dataRequestProgress = useMemo(() => { + const rows = Array.isArray(dataRequestModal.rows) ? dataRequestModal.rows : []; + const total = rows.length; + const filled = rows.filter((rowItem) => Boolean((rowItem == null ? void 0 : rowItem.is_filled) || String((rowItem == null ? void 0 : rowItem.value_text) || "").trim())).length; + return { total, filled }; + }, [dataRequestModal.rows]); + return /* @__PURE__ */ React.createElement("div", { className: "block" }, /* @__PURE__ */ React.createElement("div", { className: "request-workspace-layout" }, /* @__PURE__ */ React.createElement("div", { className: "request-main-column" }, /* @__PURE__ */ React.createElement("div", { className: "block" }, /* @__PURE__ */ React.createElement("div", { className: "request-card-head" }, /* @__PURE__ */ React.createElement("h3", null, "\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0430"), /* @__PURE__ */ React.createElement("div", { className: "request-card-head-actions" }, canRequestData ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn request-card-status-btn", + "data-tooltip": "\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441", + "aria-label": "\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441", + onClick: () => openStatusChangeModal(), + disabled: loading || !row + }, + "\u21C4" + ) : null, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn request-card-data-btn", + "data-tooltip": "\u0414\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u044F\u0432\u043A\u0438", + "aria-label": "\u0414\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u044F\u0432\u043A\u0438", + onClick: () => setRequestDataListOpen(true), + disabled: loading || !row + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M4 5h16v2H4V5Zm0 6h16v2H4v-2Zm0 6h10v2H4v-2Z", fill: "currentColor" })) + ), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn request-card-finance-btn", + "data-tooltip": "\u0424\u0438\u043D\u0430\u043D\u0441\u044B \u0437\u0430\u044F\u0432\u043A\u0438", + "aria-label": "\u0424\u0438\u043D\u0430\u043D\u0441\u044B \u0437\u0430\u044F\u0432\u043A\u0438", + onClick: () => setFinanceOpen(true), + disabled: loading || !row + }, + "$" + ))), /* @__PURE__ */ React.createElement("div", { className: "request-card-head-spacer", "aria-hidden": "true" }), loading ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...") : row ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "request-card-grid request-card-grid-compact" }, showTopicStatusInCard ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0422\u0435\u043C\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, String(row.topic_name || row.topic_code || "-"))), /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u0442\u0430\u0442\u0443\u0441"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, currentStatusName))) : null, /* @__PURE__ */ React.createElement("div", { className: "request-field request-field-span-2 request-field-description" }, /* @__PURE__ */ React.createElement("div", { className: "request-field-head" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B"), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn request-field-expand-btn", + "data-tooltip": "\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435", + "aria-label": "\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435", + onClick: () => setDescriptionOpen(true) + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "path", + { + d: "M4 9V4h5v2H6v3H4zm10-5h6v6h-2V6h-4V4zM4 15h2v3h3v2H4v-5zm14 3v-3h2v5h-5v-2h3z", + fill: "currentColor" + } + )) + )), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, row.description ? String(row.description) : "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E")), showContactsInCard ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041A\u043B\u0438\u0435\u043D\u0442"), /* @__PURE__ */ React.createElement( + "span", + { + className: "request-field-value" + (clientHasPhone ? " has-tooltip request-contact-value" : ""), + "data-tooltip": clientHasPhone ? clientPhone : void 0 + }, + clientLabel + )), /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u042E\u0440\u0438\u0441\u0442"), /* @__PURE__ */ React.createElement( + "span", + { + className: "request-field-value" + (lawyerHasPhone ? " has-tooltip request-contact-value" : ""), + "data-tooltip": lawyerHasPhone ? lawyerPhone : void 0 + }, + lawyerLabel + ))) : null, canSeeCreatedUpdatedInCard ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u043E\u0437\u0434\u0430\u043D\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime(row.created_at))), /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime(row.updated_at)))) : null), /* @__PURE__ */ React.createElement("div", { className: "request-status-route" }, /* @__PURE__ */ React.createElement("h4", null, "\u041C\u0430\u0440\u0448\u0440\u0443\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432"), routeNodesForDisplay.length ? /* @__PURE__ */ React.createElement("ol", { className: "request-route-list", id: "request-status-route" }, routeNodesForDisplay.map((node, index) => { + const state = String((node == null ? void 0 : node.state) || "pending"); + const code = String((node == null ? void 0 : node.code) || "").trim(); + const rawName = String((node == null ? void 0 : node.name) || "").trim(); + const name = resolveStatusDisplayName(code, rawName && rawName !== code ? rawName : ""); + const note = String((node == null ? void 0 : node.note) || "").trim(); + const isImportantDateNode = code === "__IMPORTANT_DATE__"; + const changedAtSource = String((node == null ? void 0 : node.changed_at) || "").trim() || (isImportantDateNode ? String(currentImportantDateAt || (row == null ? void 0 : row.important_date_at) || "").trim() : ""); + const changedAt = changedAtSource ? fmtDate(changedAtSource) : ""; + const className = "route-item " + (state === "current" ? "current" : state === "completed" ? "completed" : "pending") + (isImportantDateNode ? " important-date" : ""); + return /* @__PURE__ */ React.createElement("li", { className, key: ((node == null ? void 0 : node.code) || "node") + "-" + index }, /* @__PURE__ */ React.createElement("span", { className: "route-dot" }), /* @__PURE__ */ React.createElement("div", { className: "route-body" }, /* @__PURE__ */ React.createElement("b", null, name), isImportantDateNode ? /* @__PURE__ */ React.createElement("p", null, "\u041A\u043E\u043D\u0442\u0440\u043E\u043B\u044C\u043D\u044B\u0439 \u0441\u0440\u043E\u043A: " + (changedAt || "-")) : /* @__PURE__ */ React.createElement(React.Fragment, null, note ? /* @__PURE__ */ React.createElement("p", null, note) : null, /* @__PURE__ */ React.createElement("div", { className: "muted route-time" }, "\u0414\u0430\u0442\u0430 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F: ", changedAt || "-")))); + })) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u041C\u0430\u0440\u0448\u0440\u0443\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432 \u0434\u043B\u044F \u0442\u0435\u043C\u044B \u043D\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D"))) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435"))), /* @__PURE__ */ React.createElement("div", { className: "block request-chat-block" }, /* @__PURE__ */ React.createElement("div", { className: "request-chat-head" }, /* @__PURE__ */ React.createElement("h3", null, "\u041A\u043E\u043C\u043C\u0443\u043D\u0438\u043A\u0430\u0446\u0438\u044F"), /* @__PURE__ */ React.createElement("div", { className: "request-chat-tabs", role: "tablist", "aria-label": "\u041A\u043E\u043C\u043C\u0443\u043D\u0438\u043A\u0430\u0446\u0438\u044F" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + role: "tab", + "aria-selected": chatTab === "chat", + className: "tab-btn" + (chatTab === "chat" ? " active" : ""), + onClick: () => setChatTab("chat") + }, + "\u0427\u0430\u0442" + ), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + role: "tab", + "aria-selected": chatTab === "files", + className: "tab-btn" + (chatTab === "files" ? " active" : ""), + onClick: () => setChatTab("files") + }, + "\u0424\u0430\u0439\u043B\u044B" + (safeAttachments.length ? " (" + safeAttachments.length + ")" : "") + ))), /* @__PURE__ */ React.createElement("div", { className: "request-chat-live-row", "aria-live": "polite" }, /* @__PURE__ */ React.createElement("span", { className: "chat-live-dot" + (liveMode === "degraded" ? " degraded" : "") }), /* @__PURE__ */ React.createElement("span", { className: "request-chat-live-text" }, typingHintText || (liveMode === "degraded" ? "\u0421\u0432\u044F\u0437\u044C \u043D\u0435\u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u0430, \u0432\u043A\u043B\u044E\u0447\u0435\u043D backoff" : "\u041E\u043D\u043B\u0430\u0439\u043D"))), /* @__PURE__ */ React.createElement( + "input", + { + id: idMap.fileInput, + ref: fileInputRef, + type: "file", + multiple: true, + onChange: onInputFiles, + disabled: loading || fileUploading, + style: { position: "absolute", width: "1px", height: "1px", opacity: 0, pointerEvents: "none" } + } + ), chatTab === "chat" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("ul", { className: "simple-list request-modal-list request-chat-list", id: idMap.messagesList, ref: chatListRef }, chatTimelineItems.length ? chatTimelineItems.map( + (entry) => { + var _a2, _b2, _c2, _d, _e; + return entry.type === "date" ? /* @__PURE__ */ React.createElement("li", { key: entry.key, className: "chat-date-divider" }, /* @__PURE__ */ React.createElement("span", null, entry.label)) : entry.type === "file" ? /* @__PURE__ */ React.createElement( + "li", + { + key: entry.key, + className: "chat-message " + (String(((_a2 = entry.payload) == null ? void 0 : _a2.responsible) || "").toUpperCase().includes("\u041A\u041B\u0418\u0415\u041D\u0422") ? "incoming" : "outgoing") + }, + /* @__PURE__ */ React.createElement("div", { className: "chat-message-author" }, String(((_b2 = entry.payload) == null ? void 0 : _b2.responsible) || "\u0421\u0438\u0441\u0442\u0435\u043C\u0430")), + /* @__PURE__ */ React.createElement("div", { className: "chat-message-bubble" }, /* @__PURE__ */ React.createElement("div", { className: "chat-message-files" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "chat-message-file-chip", + onClick: () => openAttachmentFromMessage(entry.payload), + title: String(((_c2 = entry.payload) == null ? void 0 : _c2.file_name) || "\u0424\u0430\u0439\u043B") + }, + /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), + /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(((_d = entry.payload) == null ? void 0 : _d.file_name) || "\u0424\u0430\u0439\u043B")) + )), /* @__PURE__ */ React.createElement("div", { className: "chat-message-time" }, fmtTimeOnly((_e = entry.payload) == null ? void 0 : _e.created_at))) + ) : (() => { + var _a3, _b3, _c3, _d2, _e2, _f, _g, _h, _i; + const messageKind = String(((_a3 = entry.payload) == null ? void 0 : _a3.message_kind) || ""); + const isRequestDataMessage = messageKind === "REQUEST_DATA"; + const serviceMessageContent = resolveServiceMessageContent(entry.payload); + const requestDataInteractive = isRequestDataMessage && (canRequestData || canFillRequestData); + const bubbleClass = "chat-message-bubble" + (isRequestDataMessage ? " chat-request-data-bubble" : "") + (((_b3 = entry.payload) == null ? void 0 : _b3.request_data_all_filled) ? " all-filled" : "") + (isRequestDataMessage && canFillRequestData ? " request-data-message-btn" : ""); + const itemClass = "chat-message " + (String(((_c3 = entry.payload) == null ? void 0 : _c3.author_type) || "").toUpperCase() === "CLIENT" ? "incoming" : "outgoing") + (isRequestDataMessage && canFillRequestData ? " request-data-item" + (((_d2 = entry.payload) == null ? void 0 : _d2.request_data_all_filled) ? " done" : "") : ""); + return /* @__PURE__ */ React.createElement("li", { key: entry.key, className: itemClass }, /* @__PURE__ */ React.createElement("div", { className: "chat-message-author" }, String(((_e2 = entry.payload) == null ? void 0 : _e2.author_name) || ((_f = entry.payload) == null ? void 0 : _f.author_type) || "\u0421\u0438\u0441\u0442\u0435\u043C\u0430")), /* @__PURE__ */ React.createElement( + "div", + { + className: bubbleClass, + onClick: requestDataInteractive ? () => { + var _a4, _b4; + return canRequestData ? openEditDataRequestModal(String(((_a4 = entry.payload) == null ? void 0 : _a4.id) || "")) : openClientDataRequestModal(String(((_b4 = entry.payload) == null ? void 0 : _b4.id) || "")); + } : void 0, + role: requestDataInteractive ? "button" : void 0, + tabIndex: requestDataInteractive ? 0 : void 0, + onKeyDown: requestDataInteractive ? (event) => { + var _a4, _b4; + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + if (canRequestData) openEditDataRequestModal(String(((_a4 = entry.payload) == null ? void 0 : _a4.id) || "")); + else openClientDataRequestModal(String(((_b4 = entry.payload) == null ? void 0 : _b4.id) || "")); + } + } : void 0 + }, + String(((_g = entry.payload) == null ? void 0 : _g.message_kind) || "") === "REQUEST_DATA" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "chat-request-data-head" }, "\u0417\u0430\u043F\u0440\u043E\u0441"), renderRequestDataMessageItems(entry.payload)) : /* @__PURE__ */ React.createElement(React.Fragment, null, (serviceMessageContent == null ? void 0 : serviceMessageContent.title) ? /* @__PURE__ */ React.createElement("div", { className: "chat-service-head" }, serviceMessageContent.title) : null, serviceMessageContent ? serviceMessageContent.text ? /* @__PURE__ */ React.createElement("p", { className: "chat-message-text" }, serviceMessageContent.text) : null : /* @__PURE__ */ React.createElement("p", { className: "chat-message-text" }, String(((_h = entry.payload) == null ? void 0 : _h.body) || ""))), + (() => { + var _a4, _b4; + if (String(((_a4 = entry.payload) == null ? void 0 : _a4.message_kind) || "") === "REQUEST_DATA") return null; + const messageId = String(((_b4 = entry.payload) == null ? void 0 : _b4.id) || "").trim(); + if (!messageId) return null; + const messageFiles = attachmentsByMessageId.get(messageId) || []; + if (!messageFiles.length) return null; + return /* @__PURE__ */ React.createElement("div", { className: "chat-message-files" }, messageFiles.map((file) => /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + key: String(file.id), + className: "chat-message-file-chip", + onClick: () => openAttachmentFromMessage(file), + title: String(file.file_name || "\u0424\u0430\u0439\u043B") + }, + /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), + /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(file.file_name || "\u0424\u0430\u0439\u043B")) + ))); + })(), + /* @__PURE__ */ React.createElement("div", { className: "chat-message-time" }, fmtTimeOnly((_i = entry.payload) == null ? void 0 : _i.created_at)) + )); + })(); + } + ) : /* @__PURE__ */ React.createElement("li", { className: "muted chat-empty-state" }, "\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439 \u043D\u0435\u0442")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit: onSendMessage }, /* @__PURE__ */ React.createElement( + "div", + { + className: "field request-chat-composer-dropzone" + (dropActive ? " drag-active" : ""), + onDragOver: (event) => { + event.preventDefault(); + setDropActive(true); + }, + onDragLeave: (event) => { + if (event.currentTarget.contains(event.relatedTarget)) return; + setDropActive(false); + }, + onDrop: onDropFiles + }, + /* @__PURE__ */ React.createElement("label", { htmlFor: idMap.messageBody }, "\u041D\u043E\u0432\u043E\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435"), + /* @__PURE__ */ React.createElement( + "textarea", + { + id: idMap.messageBody, + placeholder: messagePlaceholder, + value: messageDraft, + onChange: onMessageChange, + onFocus: () => setComposerFocused(true), + onBlur: () => setComposerFocused(false), + disabled: loading || fileUploading + } + ), + /* @__PURE__ */ React.createElement("div", { className: "request-drop-hint muted" }, "\u041F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B \u0441\u044E\u0434\u0430 \u0438\u043B\u0438 \u043F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u0435 \u0441\u043A\u0440\u0435\u043F\u043A\u043E\u0439") + ), hasPendingFiles ? /* @__PURE__ */ React.createElement("div", { className: "request-pending-files" }, pendingFiles.map((file, index) => /* @__PURE__ */ React.createElement("div", { className: "pending-file-chip", key: (file.name || "file") + "-" + String(file.lastModified || index) }, /* @__PURE__ */ React.createElement("span", { className: "pending-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), /* @__PURE__ */ React.createElement("span", { className: "pending-file-name" }, file.name), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "pending-file-remove", + "aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0444\u0430\u0439\u043B " + file.name, + onClick: () => onRemoveSelectedFile(index) + }, + "\xD7" + ))), /* @__PURE__ */ React.createElement("button", { type: "button", className: "btn secondary btn-sm", onClick: onClearSelectedFiles }, "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F")) : null, /* @__PURE__ */ React.createElement("div", { className: "request-chat-composer-actions" }, canRequestData ? /* @__PURE__ */ React.createElement( + "button", + { + className: "btn secondary btn-sm", + type: "button", + onClick: openCreateDataRequestModal, + disabled: loading || fileUploading + }, + "\u0417\u0430\u043F\u0440\u043E\u0441\u0438\u0442\u044C" + ) : null, /* @__PURE__ */ React.createElement( + "button", + { + className: "icon-btn file-action-btn composer-attach-btn", + type: "button", + "data-tooltip": "\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0444\u0430\u0439\u043B", + "aria-label": "\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0444\u0430\u0439\u043B", + onClick: () => { + var _a2; + return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click(); + }, + disabled: loading || fileUploading + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "path", + { + d: "M8.6 13.8 15 7.4a3 3 0 0 1 4.2 4.2l-8.1 8.1a5 5 0 1 1-7.1-7.1l8.6-8.6a1 1 0 0 1 1.4 1.4l-8.6 8.6a3 3 0 1 0 4.2 4.2l8.1-8.1a1 1 0 0 0-1.4-1.4l-6.4 6.4a1 1 0 0 1-1.4-1.4z", + fill: "currentColor" + } + )) + ), /* @__PURE__ */ React.createElement( + "button", + { + className: "btn", + id: idMap.sendButton, + type: "submit", + disabled: loading || fileUploading || !canSubmit + }, + "\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C" + )))) : /* @__PURE__ */ React.createElement("div", { className: "request-files-tab" }, /* @__PURE__ */ React.createElement("ul", { className: "simple-list request-modal-list", id: idMap.filesList }, safeAttachments.length ? safeAttachments.map((item) => /* @__PURE__ */ React.createElement("li", { key: String(item.id) }, /* @__PURE__ */ React.createElement("div", null, item.file_name || "\u0424\u0430\u0439\u043B"), /* @__PURE__ */ React.createElement("div", { className: "muted request-modal-item-meta" }, String(item.mime_type || "application/octet-stream") + " \u2022 " + fmtBytes(item.size_bytes) + " \u2022 " + fmtDate(item.created_at)), /* @__PURE__ */ React.createElement("div", { className: "request-file-actions" }, item.download_url && detectAttachmentPreviewKind(item.file_name, item.mime_type) !== "none" ? /* @__PURE__ */ React.createElement( + "button", + { + className: "icon-btn file-action-btn", + type: "button", + "data-tooltip": "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440", + onClick: () => openPreview(item), + "aria-label": "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440" + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "path", + { + d: "M12 5C6.8 5 3 9.2 2 12c1 2.8 4.8 7 10 7s9-4.2 10-7c-1-2.8-4.8-7-10-7zm0 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2.2A1.8 1.8 0 1 0 12 10a1.8 1.8 0 0 0 0 3.8z", + fill: "currentColor" + } + )) + ) : null, item.download_url ? /* @__PURE__ */ React.createElement( + "a", + { + className: "icon-btn file-action-btn request-file-link-icon", + "data-tooltip": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C", + "aria-label": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C: " + String(item.file_name || "\u0444\u0430\u0439\u043B"), + href: item.download_url, + target: "_blank", + rel: "noreferrer" + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "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" + } + )) + ) : null))) : /* @__PURE__ */ React.createElement("li", { className: "muted" }, "\u0424\u0430\u0439\u043B\u043E\u0432 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442")), /* @__PURE__ */ React.createElement("div", { className: "request-files-tab-actions" }, /* @__PURE__ */ React.createElement("span", { className: "muted" }, "\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439: " + String(safeMessages.length) + " \u2022 \u041E\u0431\u0449\u0438\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0444\u0430\u0439\u043B\u043E\u0432: " + fmtBytes(totalFilesBytes)))))), StatusLine ? /* @__PURE__ */ React.createElement(StatusLine, { status }) : null, AttachmentPreviewModal ? /* @__PURE__ */ React.createElement( + AttachmentPreviewModal, + { + open: preview.open, + title: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u0444\u0430\u0439\u043B\u0430", + url: preview.url, + fileName: preview.fileName, + mimeType: preview.mimeType, + onClose: closePreview + } + ) : null, /* @__PURE__ */ React.createElement( + "div", + { + className: "overlay" + (clientDataModal.open ? " open" : ""), + onClick: closeClientDataModal, + "aria-hidden": clientDataModal.open ? "false" : "true", + id: idMap.dataRequestOverlay + }, + /* @__PURE__ */ React.createElement("div", { className: "modal request-data-summary-modal data-request-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0417\u0430\u043F\u0440\u043E\u0441 \u0434\u0430\u043D\u043D\u044B\u0445"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, (row == null ? void 0 : row.track_number) ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u043F\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0443 \u044E\u0440\u0438\u0441\u0442\u0430")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeClientDataModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit: submitClientDataModal }, /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-list", id: idMap.dataRequestItems }, clientDataModal.loading ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...") : (clientDataModal.items || []).length ? (clientDataModal.items || []).map((item, index) => { + const fieldType = String((item == null ? void 0 : item.field_type) || "string").toLowerCase(); + const fileMeta = item == null ? void 0 : item.value_file; + return /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-row", key: String(item.localId || index) }, /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-label" }, String(index + 1) + ". " + String((item == null ? void 0 : item.label) || (item == null ? void 0 : item.key) || "\u041F\u043E\u043B\u0435")), /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-value" }, fieldType === "text" ? /* @__PURE__ */ React.createElement( + "textarea", + { + value: String((item == null ? void 0 : item.value_text) || ""), + onChange: (event) => updateClientDataItem(item.localId, { value_text: event.target.value }), + rows: 3, + disabled: clientDataModal.saving || clientDataModal.loading + } + ) : fieldType === "date" ? /* @__PURE__ */ React.createElement( + "input", + { + type: "date", + value: String((item == null ? void 0 : item.value_text) || "").slice(0, 10), + onChange: (event) => updateClientDataItem(item.localId, { value_text: event.target.value }), + disabled: clientDataModal.saving || clientDataModal.loading + } + ) : fieldType === "number" ? /* @__PURE__ */ React.createElement( + "input", + { + type: "number", + step: "any", + value: String((item == null ? void 0 : item.value_text) || ""), + onChange: (event) => updateClientDataItem(item.localId, { value_text: event.target.value }), + disabled: clientDataModal.saving || clientDataModal.loading + } + ) : fieldType === "file" ? /* @__PURE__ */ React.createElement("div", { className: "stack" }, fileMeta && fileMeta.download_url ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "chat-message-file-chip", + onClick: () => openAttachmentFromMessage(fileMeta) + }, + /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), + /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(fileMeta.file_name || "\u0424\u0430\u0439\u043B")) + ) : null, /* @__PURE__ */ React.createElement( + "input", + { + type: "file", + onChange: (event) => updateClientDataItem(item.localId, { + pendingFile: event.target.files && event.target.files[0] ? event.target.files[0] : null + }), + disabled: clientDataModal.saving || clientDataModal.loading + } + ), (item == null ? void 0 : item.pendingFile) ? /* @__PURE__ */ React.createElement("span", { className: "muted" }, String(item.pendingFile.name || "")) : null) : /* @__PURE__ */ React.createElement( + "input", + { + type: "text", + value: String((item == null ? void 0 : item.value_text) || ""), + onChange: (event) => updateClientDataItem(item.localId, { value_text: event.target.value }), + disabled: clientDataModal.saving || clientDataModal.loading + } + ))); + }) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u041D\u0435\u0442 \u043F\u043E\u043B\u0435\u0439 \u0434\u043B\u044F \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F.")), clientDataModal.error ? /* @__PURE__ */ React.createElement("div", { className: "status error" }, clientDataModal.error) : null, /* @__PURE__ */ React.createElement("div", { className: "request-data-status" + (clientDataModal.status ? " ok" : ""), id: idMap.dataRequestStatus }, clientDataModal.status || ""), /* @__PURE__ */ React.createElement("div", { className: "modal-actions modal-actions-right" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "submit", + className: "btn btn-sm request-data-submit-btn", + id: idMap.dataRequestSave, + disabled: clientDataModal.loading || clientDataModal.saving + }, + clientDataModal.saving ? "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435..." : "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C" + )))) + ), /* @__PURE__ */ React.createElement( + "div", + { + className: "overlay" + (statusChangeModal.open ? " open" : ""), + onClick: closeStatusChangeModal, + "aria-hidden": statusChangeModal.open ? "false" : "true" + }, + /* @__PURE__ */ React.createElement("div", { className: "modal request-status-change-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0421\u043C\u0435\u043D\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, (row == null ? void 0 : row.track_number) ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441 \u0438 \u0432\u0430\u0436\u043D\u0443\u044E \u0434\u0430\u0442\u0443")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeStatusChangeModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement( + "input", + { + ref: statusChangeFileInputRef, + type: "file", + multiple: true, + onChange: (event) => { + appendStatusChangeFiles(Array.from(event.target && event.target.files || [])); + event.target.value = ""; + }, + style: { position: "absolute", width: "1px", height: "1px", opacity: 0, pointerEvents: "none" } + } + ), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit: submitStatusChange }, /* @__PURE__ */ React.createElement("div", { className: "request-status-change-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "status-change-next-status" }, "\u041D\u043E\u0432\u044B\u0439 \u0441\u0442\u0430\u0442\u0443\u0441"), /* @__PURE__ */ React.createElement( + "select", + { + id: "status-change-next-status", + value: statusChangeModal.statusCode, + onChange: (event) => setStatusChangeModal((prev) => ({ ...prev, statusCode: event.target.value, error: "" })), + disabled: statusChangeModal.saving || loading + }, + /* @__PURE__ */ React.createElement("option", { value: "" }, "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441"), + statusOptions.filter((item) => item.code !== String((row == null ? void 0 : row.status_code) || "").trim()).filter( + (item) => Array.isArray(statusChangeModal.allowedStatusCodes) && statusChangeModal.allowedStatusCodes.length ? statusChangeModal.allowedStatusCodes.includes(item.code) : true + ).map((item) => /* @__PURE__ */ React.createElement("option", { key: item.code, value: item.code }, item.name + (item.groupName ? " \u2022 " + item.groupName : ""))) + )), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "status-change-important-date" }, "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430 (\u0434\u0435\u0434\u043B\u0430\u0439\u043D)"), /* @__PURE__ */ React.createElement( + "input", + { + id: "status-change-important-date", + type: "datetime-local", + value: statusChangeModal.importantDateAt, + onChange: (event) => setStatusChangeModal((prev) => ({ ...prev, importantDateAt: event.target.value, error: "" })), + disabled: statusChangeModal.saving || loading + } + ))), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "status-change-comment" }, "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043A \u0441\u043C\u0435\u043D\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u0430"), /* @__PURE__ */ React.createElement( + "textarea", + { + id: "status-change-comment", + placeholder: "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0438 \u0447\u0430\u0442 (\u0435\u0441\u043B\u0438 \u0443\u043A\u0430\u0437\u0430\u043D)", + value: statusChangeModal.comment, + onChange: (event) => setStatusChangeModal((prev) => ({ ...prev, comment: event.target.value })), + disabled: statusChangeModal.saving || loading + } + )), /* @__PURE__ */ React.createElement("div", { className: "request-status-change-files" }, /* @__PURE__ */ React.createElement("div", { className: "request-status-change-files-head" }, /* @__PURE__ */ React.createElement("b", null, "\u0412\u043B\u043E\u0436\u0435\u043D\u0438\u044F"), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn file-action-btn", + "data-tooltip": "\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0444\u0430\u0439\u043B\u044B", + onClick: () => { + var _a2; + return (_a2 = statusChangeFileInputRef.current) == null ? void 0 : _a2.click(); + }, + disabled: statusChangeModal.saving || loading + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "path", + { + d: "M8.6 13.8 15 7.4a3 3 0 0 1 4.2 4.2l-8.1 8.1a5 5 0 1 1-7.1-7.1l8.6-8.6a1 1 0 0 1 1.4 1.4l-8.6 8.6a3 3 0 1 0 4.2 4.2l8.1-8.1a1 1 0 0 0-1.4-1.4l-6.4 6.4a1 1 0 0 1-1.4-1.4z", + fill: "currentColor" + } + )) + )), Array.isArray(statusChangeModal.files) && statusChangeModal.files.length ? /* @__PURE__ */ React.createElement("div", { className: "request-pending-files" }, statusChangeModal.files.map((file, index) => /* @__PURE__ */ React.createElement("div", { className: "pending-file-chip", key: (file.name || "file") + "-" + String(file.lastModified || index) }, /* @__PURE__ */ React.createElement("span", { className: "pending-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), /* @__PURE__ */ React.createElement("span", { className: "pending-file-name" }, file.name), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "pending-file-remove", + "aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0444\u0430\u0439\u043B " + file.name, + onClick: () => removeStatusChangeFile(index) + }, + "\xD7" + )))) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0424\u0430\u0439\u043B\u044B \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u044B")), /* @__PURE__ */ React.createElement("div", { className: "request-status-history-block" }, /* @__PURE__ */ React.createElement("div", { className: "request-status-history-head" }, /* @__PURE__ */ React.createElement("b", null, "\u0418\u0441\u0442\u043E\u0440\u0438\u044F \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432"), /* @__PURE__ */ React.createElement("span", { className: "muted" }, safeStatusHistory.length ? String(safeStatusHistory.length) + " \u0437\u0430\u043F\u0438\u0441\u0435\u0439" : "\u041D\u0435\u0442 \u0437\u0430\u043F\u0438\u0441\u0435\u0439")), /* @__PURE__ */ React.createElement("ol", { className: "request-route-list request-status-history-list" }, safeStatusHistory.length ? safeStatusHistory.map((item, index) => { + const statusCode = String((item == null ? void 0 : item.to_status) || ""); + const statusMeta = statusByCode.get(statusCode); + const itemClass = "route-item request-status-history-route-item " + (index === 0 ? "current" : "completed"); + return /* @__PURE__ */ React.createElement("li", { key: String((item == null ? void 0 : item.id) || index), className: itemClass }, /* @__PURE__ */ React.createElement("span", { className: "route-dot" }), /* @__PURE__ */ React.createElement("div", { className: "route-body" }, /* @__PURE__ */ React.createElement("div", { className: "request-status-history-row" }, /* @__PURE__ */ React.createElement("b", null, resolveStatusDisplayName(statusCode, (item == null ? void 0 : item.to_status_name) || (statusMeta == null ? void 0 : statusMeta.name) || "")), (statusMeta == null ? void 0 : statusMeta.isTerminal) ? /* @__PURE__ */ React.createElement("span", { className: "request-status-history-chip" }, "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439") : null), /* @__PURE__ */ React.createElement("div", { className: "muted route-time" }, fmtShortDateTime(item == null ? void 0 : item.changed_at)), /* @__PURE__ */ React.createElement("div", { className: "request-status-history-meta" }, /* @__PURE__ */ React.createElement("span", null, "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430: " + fmtShortDateTime(item == null ? void 0 : item.important_date_at)), /* @__PURE__ */ React.createElement("span", null, "\u0414\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C: " + formatDuration(item == null ? void 0 : item.duration_seconds))), (item == null ? void 0 : item.from_status) ? /* @__PURE__ */ React.createElement("div", { className: "request-status-history-meta" }, /* @__PURE__ */ React.createElement("span", null, "\u0418\u0437: " + resolveStatusDisplayName(item.from_status, ""))) : null, String((item == null ? void 0 : item.comment) || "").trim() ? /* @__PURE__ */ React.createElement("div", { className: "request-status-history-comment" }, String(item.comment)) : null)); + }) : /* @__PURE__ */ React.createElement("li", { className: "muted" }, "\u0418\u0441\u0442\u043E\u0440\u0438\u044F \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432 \u043F\u043E\u043A\u0430 \u043F\u0443\u0441\u0442\u0430\u044F"))), statusChangeModal.error ? /* @__PURE__ */ React.createElement("div", { className: "status error" }, statusChangeModal.error) : null, /* @__PURE__ */ React.createElement("div", { className: "modal-actions modal-actions-right" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "submit", + className: "btn btn-sm request-data-submit-btn", + disabled: statusChangeModal.saving || loading + }, + statusChangeModal.saving ? "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435..." : "\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C" + )))) + ), /* @__PURE__ */ React.createElement( + "div", + { + className: "overlay" + (financeOpen ? " open" : ""), + onClick: closeFinanceModal, + "aria-hidden": financeOpen ? "false" : "true" + }, + /* @__PURE__ */ React.createElement("div", { className: "modal request-finance-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0424\u0438\u043D\u0430\u043D\u0441\u044B \u0437\u0430\u044F\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, (row == null ? void 0 : row.track_number) ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0414\u0430\u043D\u043D\u044B\u0435 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeFinanceModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "request-card-grid request-finance-grid" }, /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtAmount((_a = finance == null ? void 0 : finance.request_cost) != null ? _a : row == null ? void 0 : row.request_cost))), /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041E\u043F\u043B\u0430\u0447\u0435\u043D\u043E"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtAmount(finance == null ? void 0 : finance.paid_total))), /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0414\u0430\u0442\u0430 \u043E\u043F\u043B\u0430\u0442\u044B"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime((_b = finance == null ? void 0 : finance.last_paid_at) != null ? _b : row == null ? void 0 : row.paid_at))), canSeeRate ? /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u0442\u0430\u0432\u043A\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtAmount((_c = finance == null ? void 0 : finance.effective_rate) != null ? _c : row == null ? void 0 : row.effective_rate))) : null), typeof onIssueInvoice === "function" ? /* @__PURE__ */ React.createElement("div", { className: "request-finance-actions" }, !financeIssueForm.open ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "btn btn-sm", + onClick: openFinanceIssueForm, + disabled: loading || !row + }, + "\u0412\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0447\u0435\u0442" + ) : /* @__PURE__ */ React.createElement("form", { className: "stack request-finance-issue-form", onSubmit: submitFinanceIssueForm }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-issue-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-finance-invoice-amount" }, "\u0421\u0443\u043C\u043C\u0430"), /* @__PURE__ */ React.createElement( + "input", + { + id: "request-finance-invoice-amount", + type: "number", + min: "0.01", + step: "0.01", + value: financeIssueForm.amount, + onChange: (event) => setFinanceIssueForm((prev) => ({ ...prev, amount: event.target.value, error: "" })), + disabled: financeIssueForm.saving || loading, + placeholder: "0.00" + } + )), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-finance-invoice-payer" }, "\u041F\u043B\u0430\u0442\u0435\u043B\u044C\u0449\u0438\u043A"), /* @__PURE__ */ React.createElement( + "input", + { + id: "request-finance-invoice-payer", + type: "text", + value: financeIssueForm.payerDisplayName, + onChange: (event) => setFinanceIssueForm((prev) => ({ ...prev, payerDisplayName: event.target.value, error: "" })), + disabled: financeIssueForm.saving || loading, + placeholder: "\u0424\u0418\u041E / \u043A\u043E\u043C\u043F\u0430\u043D\u0438\u044F" + } + ))), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-finance-invoice-service" }, "\u0423\u0441\u043B\u0443\u0433\u0430"), /* @__PURE__ */ React.createElement( + "input", + { + id: "request-finance-invoice-service", + type: "text", + value: financeIssueForm.serviceDescription, + onChange: (event) => setFinanceIssueForm((prev) => ({ ...prev, serviceDescription: event.target.value, error: "" })), + disabled: financeIssueForm.saving || loading, + placeholder: "\u042E\u0440\u0438\u0434\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0443\u0441\u043B\u0443\u0433\u0438" + } + )), financeIssueForm.error ? /* @__PURE__ */ React.createElement("div", { className: "status error" }, financeIssueForm.error) : null, /* @__PURE__ */ React.createElement("div", { className: "modal-actions modal-actions-right request-finance-actions-inline" }, /* @__PURE__ */ React.createElement("button", { type: "button", className: "btn secondary btn-sm", onClick: closeFinanceIssueForm, disabled: financeIssueForm.saving }, "\u041E\u0442\u043C\u0435\u043D\u0430"), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "btn btn-sm", disabled: financeIssueForm.saving || loading }, financeIssueForm.saving ? "\u0412\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u0435..." : "\u0412\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C")))) : null, /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoices" }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoices-head" }, /* @__PURE__ */ React.createElement("h4", null, "\u0421\u0447\u0435\u0442\u0430"), /* @__PURE__ */ React.createElement("span", { className: "muted" }, safeInvoices.length ? String(safeInvoices.length) + " \u0448\u0442." : "\u041D\u0435\u0442 \u0432\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0441\u0447\u0435\u0442\u043E\u0432")), safeInvoices.length ? /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-list" }, safeInvoices.map((item) => /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-row", key: String((item == null ? void 0 : item.id) || (item == null ? void 0 : item.invoice_number) || (item == null ? void 0 : item.issued_at) || "-") }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-meta" }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-number" }, /* @__PURE__ */ React.createElement("code", null, String((item == null ? void 0 : item.invoice_number) || "-"))), /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-details" }, /* @__PURE__ */ React.createElement("span", null, invoiceStatusLabel(item == null ? void 0 : item.status)), /* @__PURE__ */ React.createElement("span", null, fmtAmount(item == null ? void 0 : item.amount) + " " + String((item == null ? void 0 : item.currency) || "RUB")), /* @__PURE__ */ React.createElement("span", null, "\u0421\u043E\u0437\u0434\u0430\u043D: " + fmtDate(item == null ? void 0 : item.issued_at)), /* @__PURE__ */ React.createElement("span", null, "\u041E\u043F\u043B\u0430\u0447\u0435\u043D: " + fmtDate(item == null ? void 0 : item.paid_at)))), typeof onDownloadInvoicePdf === "function" ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn request-finance-invoice-download-btn", + onClick: () => onDownloadInvoicePdf(item), + disabled: loading, + "aria-label": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C \u0441\u0447\u0435\u0442 PDF", + "data-tooltip": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C PDF" + }, + "\u2B07" + ) : null))) : /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-empty" }, "\u0421\u0447\u0435\u0442\u0430 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435 \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u044B\u0441\u0442\u0430\u0432\u043B\u044F\u043B\u0438\u0441\u044C"))) + ), /* @__PURE__ */ React.createElement( + "div", + { + className: "overlay" + (descriptionOpen ? " open" : ""), + onClick: () => setDescriptionOpen(false), + "aria-hidden": descriptionOpen ? "false" : "true" + }, + /* @__PURE__ */ React.createElement("div", { className: "modal request-description-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, (row == null ? void 0 : row.track_number) ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0417\u0430\u044F\u0432\u043A\u0430"), /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-headline" }, /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, String((row == null ? void 0 : row.topic_name) || (row == null ? void 0 : row.topic_code) || "\u0422\u0435\u043C\u0430 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u0430")), /* @__PURE__ */ React.createElement("span", { className: "request-description-status-chip" }, currentStatusName))), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: () => setDescriptionOpen(false), "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-body" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-main" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-title" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B")), /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-text" }, (row == null ? void 0 : row.description) ? String(row.description) : "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E")), /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-meta-wrap" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-meta" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-meta-item" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041A\u043B\u0438\u0435\u043D\u0442"), /* @__PURE__ */ React.createElement( + "span", + { + className: "request-field-value" + (clientHasPhone ? " has-tooltip request-contact-value" : ""), + "data-tooltip": clientHasPhone ? clientPhone : void 0 + }, + clientLabel + )), /* @__PURE__ */ React.createElement("div", { className: "request-description-meta-item align-right" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u042E\u0440\u0438\u0441\u0442"), /* @__PURE__ */ React.createElement( + "span", + { + className: "request-field-value" + (lawyerHasPhone ? " has-tooltip request-contact-value" : ""), + "data-tooltip": lawyerHasPhone ? lawyerPhone : void 0 + }, + lawyerLabel + )), /* @__PURE__ */ React.createElement("div", { className: "request-description-meta-item" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u043E\u0437\u0434\u0430\u043D\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime(row == null ? void 0 : row.created_at))), /* @__PURE__ */ React.createElement("div", { className: "request-description-meta-item align-right" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime(row == null ? void 0 : row.updated_at))))))) + ), /* @__PURE__ */ React.createElement( + "div", + { + className: "overlay" + (dataRequestModal.open ? " open" : ""), + onClick: closeDataRequestModal, + "aria-hidden": dataRequestModal.open ? "false" : "true" + }, + /* @__PURE__ */ React.createElement("div", { className: "modal request-data-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, dataRequestModal.messageId ? "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0434\u0430\u043D\u043D\u044B\u0445" : "\u0417\u0430\u043F\u0440\u043E\u0441 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, (row == null ? void 0 : row.track_number) ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u043E\u043B\u044F \u0434\u043B\u044F \u0437\u0430\u043F\u0440\u043E\u0441\u0430")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeDataRequestModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "stack" }, /* @__PURE__ */ React.createElement("div", { className: "request-data-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-data-request-template-select" }, "\u0428\u0430\u0431\u043B\u043E\u043D \u0437\u0430\u043F\u0440\u043E\u0441\u0430 (\u043F\u043E\u0438\u0441\u043A)"), /* @__PURE__ */ React.createElement("div", { className: "request-data-combobox" }, /* @__PURE__ */ React.createElement( + "input", + { + id: "request-data-request-template-select", + value: dataRequestModal.requestTemplateQuery, + onChange: (event) => setDataRequestModal((prev) => ({ + ...prev, + requestTemplateQuery: event.target.value, + selectedRequestTemplateId: "", + templateStatus: "", + error: "" + })), + onFocus: () => setRequestTemplateSuggestOpen(true), + onBlur: () => window.setTimeout(() => setRequestTemplateSuggestOpen(false), 120), + disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate, + placeholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0448\u0430\u0431\u043B\u043E\u043D\u0430" + } + ), requestTemplateBadge ? /* @__PURE__ */ React.createElement("span", { className: "request-data-template-badge " + requestTemplateBadge.kind }, requestTemplateBadge.label) : null, requestTemplateSuggestOpen && filteredRequestTemplates.length ? /* @__PURE__ */ React.createElement("div", { className: "request-data-suggest-list", role: "listbox", "aria-label": "\u0428\u0430\u0431\u043B\u043E\u043D\u044B \u0437\u0430\u043F\u0440\u043E\u0441\u0430" }, filteredRequestTemplates.map((tpl) => /* @__PURE__ */ React.createElement( + "button", + { + key: String(tpl.id), + type: "button", + className: "request-data-suggest-item", + onMouseDown: (event) => { + event.preventDefault(); + setDataRequestModal((prev) => ({ + ...prev, + requestTemplateQuery: String(tpl.name || ""), + selectedRequestTemplateId: String(tpl.id || ""), + error: "", + templateStatus: "" + })); + setRequestTemplateSuggestOpen(false); + void applyRequestTemplateById(String(tpl.id || ""), String(tpl.name || "")); + } + }, + /* @__PURE__ */ React.createElement("span", null, String(tpl.name || "\u0428\u0430\u0431\u043B\u043E\u043D")) + ))) : null)), /* @__PURE__ */ React.createElement("div", { className: "request-data-modal-actions-inline" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn", + "data-tooltip": !canSaveSelectedRequestTemplate ? "\u0427\u0443\u0436\u043E\u0439 \u0448\u0430\u0431\u043B\u043E\u043D \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0434\u043B\u044F \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F" : requestTemplateActionMode === "save" ? "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D" : requestTemplateActionMode === "create" ? "\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D" : "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0448\u0430\u0431\u043B\u043E\u043D\u0430", + onClick: saveCurrentDataRequestTemplate, + disabled: !canSaveSelectedRequestTemplate || dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate + }, + dataRequestModal.savingTemplate ? "\u2026" : requestTemplateActionMode === "create" ? "\u271A" : "\u{1F4BE}" + ))), dataRequestModal.templateStatus ? /* @__PURE__ */ React.createElement("div", { className: "status ok" }, dataRequestModal.templateStatus) : null, canRequestData && dataRequestModal.messageId ? /* @__PURE__ */ React.createElement("div", { className: "request-data-progress-line" }, /* @__PURE__ */ React.createElement("span", { className: "request-data-progress-chip" }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C: " + String(dataRequestProgress.filled) + " / " + String(dataRequestProgress.total))) : null, /* @__PURE__ */ React.createElement("div", { className: "request-data-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-data-template-select" }, "\u041F\u043E\u043B\u0435 \u0434\u0430\u043D\u043D\u044B\u0445 (\u043F\u043E\u0438\u0441\u043A \u043F\u043E \u0441\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0443)"), /* @__PURE__ */ React.createElement("div", { className: "request-data-combobox" }, /* @__PURE__ */ React.createElement( + "input", + { + id: "request-data-template-select", + value: dataRequestModal.catalogFieldQuery, + onChange: (event) => setDataRequestModal((prev) => ({ + ...prev, + catalogFieldQuery: event.target.value, + selectedCatalogTemplateId: "", + templateStatus: "", + error: "" + })), + onFocus: () => setCatalogFieldSuggestOpen(true), + onBlur: () => window.setTimeout(() => setCatalogFieldSuggestOpen(false), 120), + disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate, + placeholder: "\u041D\u0430\u0447\u043D\u0438\u0442\u0435 \u0432\u0432\u043E\u0434\u0438\u0442\u044C \u043D\u0430\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0435 \u043F\u043E\u043B\u044F", + autoComplete: "off" + } + ), catalogFieldSuggestOpen && filteredCatalogFields.length ? /* @__PURE__ */ React.createElement("div", { className: "request-data-suggest-list", role: "listbox", "aria-label": "\u041F\u043E\u043B\u044F \u0434\u0430\u043D\u043D\u044B\u0445" }, filteredCatalogFields.map((tpl) => /* @__PURE__ */ React.createElement( + "button", + { + key: String(tpl.id), + type: "button", + className: "request-data-suggest-item", + onMouseDown: (event) => { + event.preventDefault(); + setDataRequestModal((prev) => ({ + ...prev, + catalogFieldQuery: String(tpl.label || tpl.key || ""), + selectedCatalogTemplateId: String(tpl.id || ""), + error: "", + templateStatus: "" + })); + setCatalogFieldSuggestOpen(false); + } + }, + /* @__PURE__ */ React.createElement("span", null, String(tpl.label || tpl.key)), + /* @__PURE__ */ React.createElement("small", null, String(tpl.value_type || "string")) + ))) : null)), /* @__PURE__ */ React.createElement("div", { className: "request-data-modal-actions-inline" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn", + "data-tooltip": catalogFieldActionMode === "add" ? "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u043E\u043B\u0435 \u0438\u0437 \u0441\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0430" : "\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u043E\u0432\u043E\u0435 \u043F\u043E\u043B\u0435", + onClick: addSelectedTemplateRow, + disabled: !String(dataRequestModal.catalogFieldQuery || "").trim() && !selectedCatalogFieldCandidate || dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate + }, + catalogFieldActionMode === "add" ? "+" : "\u271A" + ))), /* @__PURE__ */ React.createElement("div", { className: "request-data-rows" }, (dataRequestModal.rows || []).length ? (dataRequestModal.rows || []).map((rowItem, idx) => /* @__PURE__ */ React.createElement( + "div", + { + className: "request-data-row" + (String(draggedRequestRowId) === String(rowItem.localId) ? " dragging" : "") + (String(dragOverRequestRowId) === String(rowItem.localId) && String(draggedRequestRowId) !== String(rowItem.localId) ? " drag-over" : "") + (viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled) ? " row-locked" : ""), + key: rowItem.localId, + onDragOver: (event) => { + if (!draggedRequestRowId) return; + event.preventDefault(); + if (viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled)) return; + setDragOverRequestRowId(String(rowItem.localId || "")); + }, + onDrop: (event) => { + if (!draggedRequestRowId) return; + event.preventDefault(); + if (viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled)) return; + moveDataRequestRowToIndex(draggedRequestRowId, idx); + handleRequestRowDragEnd(); + } + }, + /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn request-data-row-index-handle", + "data-tooltip": viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled) ? "\u0417\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u043B\u0435: \u043F\u0435\u0440\u0435\u043C\u0435\u0449\u0435\u043D\u0438\u0435 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E" : "\u041F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0434\u043B\u044F \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u043F\u043E\u0440\u044F\u0434\u043A\u0430", + draggable: !(viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled)), + onDragStart: (event) => handleRequestRowDragStart(event, rowItem, viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled)), + onDragEnd: handleRequestRowDragEnd, + disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate || viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled), + "aria-label": "\u041F\u043E\u0440\u044F\u0434\u043E\u043A \u043F\u043E\u043B\u044F " + String(idx + 1) + }, + /* @__PURE__ */ React.createElement("span", null, idx + 1) + ), + /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", null, "\u041D\u0430\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0435"), /* @__PURE__ */ React.createElement( + "input", + { + value: rowItem.label, + onChange: (event) => updateDataRequestRow(rowItem.localId, { label: event.target.value }), + disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate || viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled) + } + )), + /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", null, "\u0422\u0438\u043F"), /* @__PURE__ */ React.createElement( + "select", + { + value: rowItem.field_type || "string", + onChange: (event) => updateDataRequestRow(rowItem.localId, { field_type: event.target.value }), + disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate || viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled) + }, + requestDataTypeOptions.map((option) => /* @__PURE__ */ React.createElement("option", { key: option.value, value: option.value }, option.label)) + )), + /* @__PURE__ */ React.createElement("div", { className: "request-data-row-controls" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn danger request-data-row-action-btn", + "data-tooltip": viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled) ? "\u042E\u0440\u0438\u0441\u0442 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u043B\u0435" : "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u0435", + onClick: () => removeDataRequestRow(rowItem.localId), + disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate || viewerRoleCode === "LAWYER" && (rowItem == null ? void 0 : rowItem.is_filled) + }, + "\xD7" + )), + canRequestData && ((rowItem == null ? void 0 : rowItem.is_filled) || String((rowItem == null ? void 0 : rowItem.value_text) || "").trim()) ? /* @__PURE__ */ React.createElement("div", { className: "request-data-row-client-value" }, /* @__PURE__ */ React.createElement("span", { className: "request-data-row-client-label" }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C:"), String((rowItem == null ? void 0 : rowItem.field_type) || "").toLowerCase() === "file" ? (rowItem == null ? void 0 : rowItem.value_file) && rowItem.value_file.download_url ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "chat-message-file-chip", + onClick: () => openAttachmentFromMessage(rowItem.value_file) + }, + /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), + /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(rowItem.value_file.file_name || "\u0424\u0430\u0439\u043B")) + ) : /* @__PURE__ */ React.createElement("span", { className: "muted" }, "\u0424\u0430\u0439\u043B \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D") : /* @__PURE__ */ React.createElement("span", { className: "request-data-row-client-text" }, String((rowItem == null ? void 0 : rowItem.field_type) || "").toLowerCase() === "date" ? fmtDateOnly(rowItem == null ? void 0 : rowItem.value_text) : String((rowItem == null ? void 0 : rowItem.value_text) || "").trim().slice(0, 140))) : null + )) : /* @__PURE__ */ React.createElement("div", { className: "muted" }, "\u041F\u043E\u043B\u044F \u0434\u043B\u044F \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0435\u0449\u0435 \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u044B"))), dataRequestModal.error ? /* @__PURE__ */ React.createElement("div", { className: "status error" }, dataRequestModal.error) : null, /* @__PURE__ */ React.createElement("div", { className: "modal-actions modal-actions-right" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "btn btn-sm request-data-submit-btn", + onClick: submitDataRequestModal, + disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate + }, + dataRequestModal.saving ? "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435..." : "\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C" + ))) + ), /* @__PURE__ */ React.createElement( + "div", + { + className: "overlay" + (requestDataListOpen ? " open" : ""), + onClick: () => setRequestDataListOpen(false), + "aria-hidden": requestDataListOpen ? "false" : "true" + }, + /* @__PURE__ */ React.createElement("div", { className: "modal request-data-summary-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0414\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u044F\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, (row == null ? void 0 : row.track_number) ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: () => setRequestDataListOpen(false), "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-list" }, requestDataListItems.length ? requestDataListItems.map((item) => { + const value = formatRequestDataValue(item); + const isFile = String((item == null ? void 0 : item.field_type) || "").toLowerCase() === "file"; + return /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-row", key: String(item.id || item.key) }, /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-label" }, String(item.label || humanizeKey(item.key))), /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-value" }, isFile ? value && typeof value === "object" ? /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-file" }, /* @__PURE__ */ React.createElement("button", { type: "button", className: "chat-message-file-chip", onClick: () => downloadAttachment(value) }, /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(value.file_name || "\u0424\u0430\u0439\u043B")))) : /* @__PURE__ */ React.createElement("span", { className: "muted" }, "\u041D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E") : String(value || "\u041D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E"))); + }) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442"))) + )); + } + + // app/web/admin/features/tables/AvailableTablesSection.jsx + function AvailableTablesSection({ + tables, + status, + onRefresh, + onToggleActive, + DataTableComponent, + StatusLineComponent, + IconButtonComponent + }) { + const tableState = (tables == null ? void 0 : tables.availableTables) || { rows: [] }; + const DataTable = DataTableComponent; + const StatusLine = StatusLineComponent; + const IconButton = IconButtonComponent; + return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u044C \u0442\u0430\u0431\u043B\u0438\u0446"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0421\u043A\u0440\u044B\u0442\u0430\u044F \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u0430\u044F \u0432\u043A\u043B\u0430\u0434\u043A\u0430. \u0414\u043E\u0441\u0442\u0443\u043F \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u043F\u043E \u043F\u0440\u044F\u043C\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435.")), /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onRefresh, title: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C", "aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(RefreshIcon, null))), /* @__PURE__ */ React.createElement( + DataTable, + { + headers: [ + { key: "label", label: "\u0422\u0430\u0431\u043B\u0438\u0446\u0430" }, + { key: "table", label: "\u041A\u043E\u0434" }, + { key: "section", label: "\u0420\u0430\u0437\u0434\u0435\u043B" }, + { key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430" }, + { key: "updated_at", label: "\u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430" }, + { key: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439" }, + { key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" } + ], + rows: tableState.rows, + emptyColspan: 7, + renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: String(row.table || row.label) }, /* @__PURE__ */ React.createElement("td", null, row.label || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.table || "-")), /* @__PURE__ */ React.createElement("td", null, row.section || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(Boolean(row.is_active))), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.updated_at)), /* @__PURE__ */ React.createElement("td", null, row.responsible || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement( + IconButton, + { + icon: row.is_active ? "\u23F8" : "\u25B6", + tooltip: row.is_active ? "\u0414\u0435\u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0442\u0430\u0431\u043B\u0438\u0446\u0443" : "\u0410\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0442\u0430\u0431\u043B\u0438\u0446\u0443", + onClick: () => onToggleActive(row.table, !Boolean(row.is_active)) + } + )))) + } + ), /* @__PURE__ */ React.createElement(StatusLine, { status })); + } + + // app/web/admin/hooks/useAdminApi.js + function useAdminApi(token) { + const { useCallback } = React; + return useCallback( + async (path, options, tokenOverride) => { + const opts = options || {}; + const authToken = tokenOverride !== void 0 ? tokenOverride : token; + const headers = { "Content-Type": "application/json", ...opts.headers || {} }; + if (opts.auth !== false) { + if (!authToken) throw new Error("\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438"); + headers.Authorization = "Bearer " + authToken; + } + const response = await fetch(path, { + method: opts.method || "GET", + headers, + body: opts.body ? JSON.stringify(opts.body) : void 0 + }); + const text = await response.text(); + let payload; + try { + payload = text ? JSON.parse(text) : {}; + } catch (_) { + payload = { raw: text }; + } + if (!response.ok) { + const message = payload && (payload.detail || payload.error || payload.raw) || "HTTP " + response.status; + throw new Error(translateApiError(String(message))); + } + return payload; + }, + [token] + ); + } + + // app/web/admin/hooks/useAdminCatalogLoaders.js + function useAdminCatalogLoaders({ api, setStatus, setTableState, setReferenceRowsMap, buildUniversalQuery: buildUniversalQuery2 }) { + const { useCallback } = React; + const loadAvailableTables = useCallback( + async (tokenOverride) => { + setStatus("availableTables", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", ""); + try { + const data = await api("/api/admin/crud/meta/available-tables", {}, tokenOverride); + const rows = Array.isArray(data.rows) ? data.rows : []; + setTableState("availableTables", { + filters: [], + sort: null, + offset: 0, + total: rows.length, + showAll: true, + rows + }); + setStatus("availableTables", "\u0421\u043F\u0438\u0441\u043E\u043A \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok"); + return true; + } catch (error) { + setStatus("availableTables", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + return false; + } + }, + [api, setStatus, setTableState] + ); + const loadReferenceRows = useCallback( + async (catalogRows, tokenOverride) => { + const rows = Array.isArray(catalogRows) ? catalogRows : []; + const byTable = {}; + rows.forEach((item) => { + const table = String((item == null ? void 0 : item.table) || ""); + if (!table) return; + byTable[table] = item; + }); + const references = /* @__PURE__ */ new Set(); + rows.forEach((item) => { + ((item == null ? void 0 : item.columns) || []).forEach((column) => { + const meta = normalizeReferenceMeta(column == null ? void 0 : column.reference); + if (meta == null ? void 0 : meta.table) references.add(meta.table); + }); + }); + if (!references.size) { + setReferenceRowsMap({}); + return; + } + const nextMap = {}; + await Promise.all( + Array.from(references.values()).map(async (table) => { + const meta = byTable[table]; + const endpoint = String((meta == null ? void 0 : meta.query_endpoint) || "/api/admin/crud/" + table + "/query"); + const sort = Array.isArray(meta == null ? void 0 : meta.default_sort) && meta.default_sort.length ? meta.default_sort : [{ field: "created_at", dir: "desc" }]; + try { + const data = await api( + endpoint, + { + method: "POST", + body: buildUniversalQuery2([], sort, 500, 0) + }, + tokenOverride + ); + nextMap[table] = Array.isArray(data == null ? void 0 : data.rows) ? data.rows : []; + } catch (_) { + nextMap[table] = []; + } + }) + ); + setReferenceRowsMap(nextMap); + }, + [api, buildUniversalQuery2, setReferenceRowsMap] + ); + return { + loadAvailableTables, + loadReferenceRows + }; + } + + // app/web/admin/hooks/useKanban.js + function useKanban({ api, setStatus, setTableState, tablesRef }) { + const { useCallback, useState } = React; + const [kanbanData, setKanbanData] = useState({ + rows: [], + columns: KANBAN_GROUPS, + total: 0, + truncated: false + }); + const [kanbanLoading, setKanbanLoading] = useState(false); + const [kanbanSortModal, setKanbanSortModal] = useState({ + open: false, + value: "created_newest" + }); + const [kanbanSortApplied, setKanbanSortApplied] = useState(false); + const loadKanban = useCallback( + async (tokenOverride, options) => { + const opts = options || {}; + const currentKanbanState = tablesRef.current.kanban || createTableState(); + const activeFilters = Array.isArray(opts.filtersOverride) ? [...opts.filtersOverride] : [...currentKanbanState.filters || []]; + const currentSortMode = Array.isArray(currentKanbanState.sort) && currentKanbanState.sort[0] ? String(currentKanbanState.sort[0].field || "") : ""; + const activeSortMode = String(opts.sortModeOverride || currentSortMode || kanbanSortModal.value || "created_newest").trim() || "created_newest"; + const params = new URLSearchParams({ limit: "400", sort_mode: activeSortMode }); + if (activeFilters.length) params.set("filters", JSON.stringify(activeFilters)); + setKanbanLoading(true); + setStatus("kanban", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", ""); + try { + const data = await api("/api/admin/requests/kanban?" + params.toString(), {}, tokenOverride); + const rows = Array.isArray(data.rows) ? data.rows : []; + const columns = Array.isArray(data.columns) && data.columns.length ? data.columns : KANBAN_GROUPS; + setKanbanData({ + rows, + columns, + total: Number(data.total || rows.length), + truncated: Boolean(data.truncated) + }); + setTableState("kanban", { + ...currentKanbanState, + filters: activeFilters, + sort: [{ field: activeSortMode, dir: "asc" }], + rows, + total: Number(data.total || rows.length), + offset: 0, + showAll: false + }); + const tail = Boolean(data.truncated) ? " \u041F\u043E\u043A\u0430\u0437\u0430\u043D\u0430 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u043D\u0430\u044F \u0432\u044B\u0431\u043E\u0440\u043A\u0430." : ""; + setStatus("kanban", "\u041A\u0430\u043D\u0431\u0430\u043D \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D." + tail, "ok"); + } catch (error) { + setStatus("kanban", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + } finally { + setKanbanLoading(false); + } + }, + [api, kanbanSortModal.value, setStatus, setTableState, tablesRef] + ); + const openKanbanSortModal = useCallback(() => { + const tableState = tablesRef.current.kanban || createTableState(); + const currentMode = Array.isArray(tableState.sort) && tableState.sort[0] ? String(tableState.sort[0].field || "") : ""; + setKanbanSortModal({ + open: true, + value: currentMode || "created_newest" + }); + setStatus("kanbanSort", "", ""); + }, [setStatus, tablesRef]); + const closeKanbanSortModal = useCallback(() => { + setKanbanSortModal((prev) => ({ ...prev, open: false })); + setStatus("kanbanSort", "", ""); + }, [setStatus]); + const updateKanbanSortMode = useCallback((event) => { + setKanbanSortModal((prev) => ({ ...prev, value: String(event.target.value || "created_newest") })); + }, []); + const submitKanbanSortModal = useCallback( + async (event) => { + event.preventDefault(); + const nextMode = String(kanbanSortModal.value || "created_newest"); + const tableState = tablesRef.current.kanban || createTableState(); + setTableState("kanban", { + ...tableState, + sort: [{ field: nextMode, dir: "asc" }], + offset: 0, + showAll: false + }); + setKanbanSortApplied(true); + closeKanbanSortModal(); + await loadKanban(void 0, { sortModeOverride: nextMode }); + }, + [closeKanbanSortModal, kanbanSortModal.value, loadKanban, setTableState, tablesRef] + ); + const resetKanbanState = useCallback(() => { + setKanbanSortModal({ open: false, value: "created_newest" }); + setKanbanSortApplied(false); + setKanbanData({ rows: [], columns: KANBAN_GROUPS, total: 0, truncated: false }); + setKanbanLoading(false); + }, []); + return { + kanbanData, + kanbanLoading, + kanbanSortModal, + kanbanSortApplied, + loadKanban, + openKanbanSortModal, + closeKanbanSortModal, + updateKanbanSortMode, + submitKanbanSortModal, + resetKanbanState + }; + } + + // app/web/admin/hooks/useRequestWorkspace.js + var DEFAULT_INVOICE_REQUISITES = Object.freeze({ + issuer_name: '\u041E\u041E\u041E "\u0410\u0443\u0434\u0438\u0442\u043E\u0440\u044B \u043A\u043E\u0440\u043F\u043E\u0440\u0430\u0442\u0438\u0432\u043D\u043E\u0439 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0441\u0442\u0438"', + issuer_inn: "7604226740", + issuer_kpp: "760401001", + issuer_address: "\u0433. \u042F\u0440\u043E\u0441\u043B\u0430\u0432\u043B\u044C, \u0443\u043B. \u0411\u043E\u0433\u0434\u0430\u043D\u043E\u0432\u0438\u0447\u0430, 6\u0410", + bank_name: '\u0410\u041E "\u0410\u041B\u042C\u0424\u0410-\u0411\u0410\u041D\u041A"', + bank_bik: "044525593", + bank_account: "40702810501860000582", + bank_corr_account: "30101810200000000593" + }); + async function buildStorageUploadError(response, fallbackMessage) { + const base = String(fallbackMessage || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435"); + const status = Number((response == null ? void 0 : response.status) || 0); + const statusText = String((response == null ? void 0 : 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; + } + function useRequestWorkspace(options) { + const { useCallback, useRef, useState } = React; + const opts = options || {}; + const api = opts.api; + const setStatus = opts.setStatus; + const setActiveSection = opts.setActiveSection; + const token = opts.token || ""; + const users = Array.isArray(opts.users) ? opts.users : []; + const buildUniversalQuery2 = opts.buildUniversalQuery; + const resolveAdminObjectSrc2 = opts.resolveAdminObjectSrc; + const [requestModal, setRequestModal] = useState(createRequestModalState()); + const requestOpenGuardRef = useRef({ requestId: "", ts: 0 }); + const resetRequestWorkspaceState = useCallback(() => { + setRequestModal(createRequestModalState()); + requestOpenGuardRef.current = { requestId: "", ts: 0 }; + }, []); + const updateRequestModalMessageDraft = useCallback((event) => { + const value = event.target.value; + setRequestModal((prev) => ({ ...prev, messageDraft: value })); + }, []); + const appendRequestModalFiles = 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 removeRequestModalFile = useCallback((index) => { + setRequestModal((prev) => { + const existing = Array.isArray(prev.selectedFiles) ? [...prev.selectedFiles] : []; + existing.splice(index, 1); + return { ...prev, selectedFiles: existing }; + }); + }, []); + const clearRequestModalFiles = useCallback(() => { + setRequestModal((prev) => ({ ...prev, selectedFiles: [] })); + }, []); + const loadRequestModalData = useCallback( + async (requestId, loadOptions) => { + if (!api || !requestId) return; + const localOpts = loadOptions || {}; + const showLoading = localOpts.showLoading !== false; + if (showLoading) { + setRequestModal((prev) => ({ + ...prev, + loading: true, + requestId, + requestData: null, + financeSummary: null, + invoices: [], + statusRouteNodes: [] + })); + } + const requestFilter = [{ field: "request_id", op: "=", value: String(requestId) }]; + try { + const [row, messagesData, attachmentsData, statusRouteData, invoicesData] = await Promise.all([ + api("/api/admin/crud/requests/" + requestId), + api("/api/admin/chat/requests/" + requestId + "/messages"), + api("/api/admin/crud/attachments/query", { + method: "POST", + body: buildUniversalQuery2(requestFilter, [{ field: "created_at", dir: "asc" }], 500, 0) + }), + api("/api/admin/requests/" + requestId + "/status-route").catch(() => ({ nodes: [] })), + api("/api/admin/invoices/query", { + method: "POST", + body: buildUniversalQuery2(requestFilter, [{ field: "issued_at", dir: "desc" }], 500, 0) + }).catch(() => ({ rows: [] })) + ]); + const usersById = new Map(users.filter((user) => user && user.id).map((user) => [String(user.id), user])); + const rowData = row && typeof row === "object" ? { ...row } : row; + if (rowData && typeof rowData === "object") { + const assignedLawyerId = String(rowData.assigned_lawyer_id || "").trim(); + if (assignedLawyerId) { + const lawyer = usersById.get(assignedLawyerId); + if (lawyer) { + rowData.assigned_lawyer_name = rowData.assigned_lawyer_name || lawyer.name || lawyer.email || assignedLawyerId; + rowData.assigned_lawyer_phone = rowData.assigned_lawyer_phone || lawyer.phone || null; + } + } + } + const attachments = (attachmentsData.rows || []).map((item) => ({ + ...item, + download_url: resolveAdminObjectSrc2(item.s3_key, token) + })); + const usersByEmail = new Map( + users.filter((user) => user && user.email).map((user) => [String(user.email).toLowerCase(), String(user.name || user.email)]) + ); + const normalizedMessages = (messagesData.rows || []).map((item) => { + if (!item || typeof item !== "object") return item; + const authorType = String(item.author_type || "").toUpperCase(); + const authorName = String(item.author_name || "").trim(); + if ((authorType === "LAWYER" || authorType === "SYSTEM") && authorName.includes("@")) { + const mapped = usersByEmail.get(authorName.toLowerCase()); + if (mapped) return { ...item, author_name: mapped }; + } + return item; + }); + const invoices = Array.isArray(invoicesData == null ? void 0 : invoicesData.rows) ? invoicesData.rows : []; + const paidInvoices = invoices.filter( + (item) => String((item == null ? void 0 : item.status) || "").toUpperCase() === "PAID" + ); + const paidTotal = paidInvoices.reduce((acc, item) => { + const amount = Number((item == null ? void 0 : item.amount) || 0); + return Number.isFinite(amount) ? acc + amount : acc; + }, 0); + const latestPaidAt = paidInvoices.reduce((latest, item) => { + const raw = item == null ? void 0 : item.paid_at; + const ts = raw ? new Date(raw).getTime() : Number.NaN; + if (!Number.isFinite(ts)) return latest; + if (!latest) return String(raw); + const latestTs = new Date(latest).getTime(); + return ts > latestTs ? String(raw) : latest; + }, ""); + setRequestModal((prev) => { + var _a, _b; + return { + ...prev, + loading: false, + requestId: (rowData == null ? void 0 : rowData.id) || requestId, + trackNumber: String((rowData == null ? void 0 : rowData.track_number) || ""), + requestData: rowData, + financeSummary: { + request_cost: (_a = rowData == null ? void 0 : rowData.request_cost) != null ? _a : null, + effective_rate: (_b = rowData == null ? void 0 : rowData.effective_rate) != null ? _b : null, + paid_total: Math.round((paidTotal + Number.EPSILON) * 100) / 100, + last_paid_at: latestPaidAt || (rowData == null ? void 0 : rowData.paid_at) || null + }, + invoices, + statusRouteNodes: Array.isArray(statusRouteData == null ? void 0 : statusRouteData.nodes) ? statusRouteData.nodes : [], + statusHistory: Array.isArray(statusRouteData == null ? void 0 : statusRouteData.history) ? statusRouteData.history : [], + availableStatuses: Array.isArray(statusRouteData == null ? void 0 : statusRouteData.available_statuses) ? statusRouteData.available_statuses : [], + currentImportantDateAt: String((statusRouteData == null ? void 0 : statusRouteData.current_important_date_at) || (rowData == null ? void 0 : rowData.important_date_at) || ""), + messages: normalizedMessages, + attachments, + selectedFiles: [], + fileUploading: false + }; + }); + if (showLoading && typeof setStatus === "function") setStatus("requestModal", "", ""); + } catch (error) { + setRequestModal((prev) => ({ + ...prev, + loading: false, + requestId, + requestData: null, + financeSummary: null, + invoices: [], + statusRouteNodes: [], + statusHistory: [], + availableStatuses: [], + currentImportantDateAt: "", + messages: [], + attachments: [], + selectedFiles: [], + fileUploading: false + })); + if (typeof setStatus === "function") setStatus("requestModal", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + } + }, + [api, buildUniversalQuery2, resolveAdminObjectSrc2, setStatus, token, users] + ); + const refreshRequestModal = useCallback(async () => { + if (!requestModal.requestId) return; + await loadRequestModalData(requestModal.requestId, { showLoading: true }); + }, [loadRequestModalData, requestModal.requestId]); + const openRequestDetails = useCallback( + async (requestId, event, options2) => { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + if (!requestId) return; + const normalizedRequestId = String(requestId); + const now = Date.now(); + const prev = requestOpenGuardRef.current; + if (prev.requestId === normalizedRequestId && now - prev.ts < 900) return; + requestOpenGuardRef.current = { requestId: normalizedRequestId, ts: now }; + if (window.location.pathname !== "/admin.html" || window.location.search) { + window.history.replaceState(null, "", "/admin.html"); + } + if (typeof setStatus === "function") setStatus("requestModal", "", ""); + if (typeof setActiveSection === "function") setActiveSection("requestWorkspace"); + await loadRequestModalData(normalizedRequestId, { showLoading: true }); + const preset = options2 && typeof options2 === "object" ? options2.statusChangePreset : null; + if (preset) { + setRequestModal((prev2) => ({ ...prev2, pendingStatusChangePreset: preset })); + } + }, + [loadRequestModalData, setActiveSection, setStatus] + ); + const submitRequestModalMessage = useCallback( + async (event) => { + if (event && typeof event.preventDefault === "function") event.preventDefault(); + if (!api) return; + const requestId = requestModal.requestId; + const body = String(requestModal.messageDraft || "").trim(); + const files = Array.isArray(requestModal.selectedFiles) ? requestModal.selectedFiles : []; + if (!requestId || !body && !files.length) return; + try { + setRequestModal((prev) => ({ ...prev, fileUploading: true })); + if (typeof setStatus === "function") { + setStatus("requestModal", files.length ? "\u041E\u0442\u043F\u0440\u0430\u0432\u043A\u0430 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0438 \u0444\u0430\u0439\u043B\u043E\u0432..." : "\u041E\u0442\u043F\u0440\u0430\u0432\u043A\u0430 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F...", ""); + } + let messageId = null; + if (body) { + const message = await api("/api/admin/chat/requests/" + requestId + "/messages", { + method: "POST", + body: { body } + }); + messageId = String((message == null ? void 0 : message.id) || "").trim() || null; + } + for (const file of files) { + const mimeType = String(file.type || "application/octet-stream"); + const init = await api("/api/admin/uploads/init", { + method: "POST", + body: { + file_name: file.name, + mime_type: mimeType, + size_bytes: file.size, + scope: "REQUEST_ATTACHMENT", + request_id: requestId + } + }); + const putResp = await fetch(init.presigned_url, { + method: "PUT", + headers: { "Content-Type": mimeType }, + body: file + }); + if (!putResp.ok) throw new Error(await buildStorageUploadError(putResp, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435")); + await api("/api/admin/uploads/complete", { + method: "POST", + body: { + key: init.key, + file_name: file.name, + mime_type: mimeType, + size_bytes: file.size, + scope: "REQUEST_ATTACHMENT", + request_id: requestId, + message_id: messageId + } + }); + } + setRequestModal((prev) => ({ ...prev, messageDraft: "", selectedFiles: [], fileUploading: false })); + const successMessage = body && files.length ? "\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0438 \u0444\u0430\u0439\u043B\u044B \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u044B" : files.length ? "\u0424\u0430\u0439\u043B\u044B \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u044B" : "\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E"; + if (typeof setStatus === "function") setStatus("requestModal", successMessage, "ok"); + await loadRequestModalData(requestId, { showLoading: false }); + } catch (error) { + setRequestModal((prev) => ({ ...prev, fileUploading: false })); + if (typeof setStatus === "function") setStatus("requestModal", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438: " + error.message, "error"); + } + }, + [api, loadRequestModalData, requestModal.messageDraft, requestModal.requestId, requestModal.selectedFiles, setStatus] + ); + const loadRequestDataTemplates = useCallback( + async (documentName) => { + const requestId = requestModal.requestId; + if (!api || !requestId) return { rows: [], documents: [] }; + const query = documentName ? "?document=" + encodeURIComponent(String(documentName)) : ""; + return api("/api/admin/chat/requests/" + requestId + "/data-request-templates" + query); + }, + [api, requestModal.requestId] + ); + const loadRequestDataBatch = useCallback( + async (messageId) => { + const requestId = requestModal.requestId; + if (!api || !requestId || !messageId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430"); + return api("/api/admin/chat/requests/" + requestId + "/data-requests/" + encodeURIComponent(String(messageId))); + }, + [api, requestModal.requestId] + ); + const loadRequestDataTemplateDetails = useCallback( + async (templateId) => { + const requestId = requestModal.requestId; + if (!api || !requestId || !templateId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D \u0448\u0430\u0431\u043B\u043E\u043D"); + return api( + "/api/admin/chat/requests/" + requestId + "/data-request-templates/" + encodeURIComponent(String(templateId)) + ); + }, + [api, requestModal.requestId] + ); + const saveRequestDataTemplate = useCallback( + async (payload) => { + const requestId = requestModal.requestId; + if (!api || !requestId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430"); + return api("/api/admin/chat/requests/" + requestId + "/data-request-templates", { + method: "POST", + body: payload || {} + }); + }, + [api, requestModal.requestId] + ); + const saveRequestDataBatch = useCallback( + async (payload) => { + const requestId = requestModal.requestId; + if (!api || !requestId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430"); + const result = await api("/api/admin/chat/requests/" + requestId + "/data-requests", { + method: "POST", + body: payload || {} + }); + await loadRequestModalData(requestId, { showLoading: false }); + return result; + }, + [api, loadRequestModalData, requestModal.requestId] + ); + const clearPendingStatusChangePreset = useCallback(() => { + setRequestModal((prev) => ({ ...prev, pendingStatusChangePreset: null })); + }, []); + const probeRequestLive = useCallback( + async ({ cursor } = {}) => { + const requestId = requestModal.requestId; + if (!api || !requestId) return { has_updates: false, typing: [], cursor: null }; + const query = cursor ? "?cursor=" + encodeURIComponent(String(cursor)) : ""; + const payload = await api("/api/admin/chat/requests/" + requestId + "/live" + query); + if (payload && payload.has_updates) { + await loadRequestModalData(requestId, { showLoading: false }); + } + return payload || { has_updates: false, typing: [], cursor: null }; + }, + [api, loadRequestModalData, requestModal.requestId] + ); + const setRequestTyping = useCallback( + async ({ typing } = {}) => { + const requestId = requestModal.requestId; + if (!api || !requestId) return { status: "skipped", typing: false }; + return api("/api/admin/chat/requests/" + requestId + "/typing", { + method: "POST", + body: { typing: Boolean(typing) } + }); + }, + [api, requestModal.requestId] + ); + const submitRequestStatusChange = useCallback( + async ({ requestId, statusCode, importantDateAt, comment, files } = {}) => { + var _a; + if (!api) throw new Error("API \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D"); + const targetRequestId = String(requestId || requestModal.requestId || "").trim(); + if (!targetRequestId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430"); + const nextStatus = String(statusCode || "").trim(); + if (!nextStatus) throw new Error("\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441"); + const body = { + status_code: nextStatus, + important_date_at: importantDateAt || null, + comment: String(comment || "").trim() || null + }; + if (typeof setStatus === "function") setStatus("requestModal", "\u0421\u043C\u0435\u043D\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430...", ""); + const result = await api("/api/admin/requests/" + targetRequestId + "/status-change", { + method: "POST", + body + }); + const attachedFiles = Array.isArray(files) ? files.filter(Boolean) : []; + const commentText = String(comment || "").trim(); + const availableStatuses = Array.isArray(requestModal.availableStatuses) ? requestModal.availableStatuses : []; + const statusName = (_a = availableStatuses.find((item) => String((item == null ? void 0 : item.code) || "").trim() === String((result == null ? void 0 : result.to_status) || nextStatus).trim())) == null ? void 0 : _a.name; + const nextStatusLabel = String(statusName || (result == null ? void 0 : result.to_status) || nextStatus).trim() || nextStatus; + const importantDateRaw = String((result == null ? void 0 : result.important_date_at) || importantDateAt || "").trim(); + const importantDateLabel = importantDateRaw ? fmtShortDateTime(importantDateRaw) : ""; + const serviceLines = [`\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441: "${nextStatusLabel}"`]; + if (importantDateRaw) { + serviceLines.push("\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430: " + (importantDateLabel && importantDateLabel !== "-" ? importantDateLabel : importantDateRaw)); + } + if (commentText) serviceLines.push(commentText); + let messageId = null; + const serviceMessageBody = serviceLines.filter(Boolean).join("\n").trim(); + if (serviceMessageBody) { + const message = await api("/api/admin/chat/requests/" + targetRequestId + "/messages", { + method: "POST", + body: { body: serviceMessageBody } + }); + messageId = String((message == null ? void 0 : message.id) || "").trim() || null; + } + for (const file of attachedFiles) { + const mimeType = String(file.type || "application/octet-stream"); + const init = await api("/api/admin/uploads/init", { + method: "POST", + body: { + file_name: file.name, + mime_type: mimeType, + size_bytes: file.size, + scope: "REQUEST_ATTACHMENT", + request_id: targetRequestId + } + }); + const putResp = await fetch(init.presigned_url, { + method: "PUT", + headers: { "Content-Type": mimeType }, + body: file + }); + if (!putResp.ok) throw new Error(await buildStorageUploadError(putResp, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435")); + await api("/api/admin/uploads/complete", { + method: "POST", + body: { + key: init.key, + file_name: file.name, + mime_type: mimeType, + size_bytes: file.size, + scope: "REQUEST_ATTACHMENT", + request_id: targetRequestId, + message_id: messageId + } + }); + } + if (typeof setStatus === "function") setStatus("requestModal", "\u0421\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u044F\u0432\u043A\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok"); + await loadRequestModalData(targetRequestId, { showLoading: false }); + return result; + }, + [api, loadRequestModalData, requestModal.availableStatuses, requestModal.requestId, setStatus] + ); + const issueRequestInvoice = useCallback( + async ({ requestId, amount, serviceDescription, payerDisplayName } = {}) => { + if (!api) throw new Error("API \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D"); + const targetRequestId = String(requestId || requestModal.requestId || "").trim(); + if (!targetRequestId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430"); + const parsedAmount = Number(amount); + if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) { + throw new Error("\u0421\u0443\u043C\u043C\u0430 \u0441\u0447\u0435\u0442\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0443\u043B\u044F"); + } + const roundedAmount = Math.round((parsedAmount + Number.EPSILON) * 100) / 100; + const rowData = requestModal.requestData && typeof requestModal.requestData === "object" ? requestModal.requestData : null; + const payerName = String(payerDisplayName || (rowData == null ? void 0 : rowData.client_name) || "").trim() || "\u041A\u043B\u0438\u0435\u043D\u0442"; + const serviceLabel = String(serviceDescription || "").trim() || "\u042E\u0440\u0438\u0434\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0443\u0441\u043B\u0443\u0433\u0438"; + const trackNumber = String((rowData == null ? void 0 : rowData.track_number) || requestModal.trackNumber || "").trim(); + const topicLabel = String((rowData == null ? void 0 : rowData.topic_name) || (rowData == null ? void 0 : rowData.topic_code) || "").trim(); + if (typeof setStatus === "function") setStatus("requestModal", "\u0412\u044B\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u043C \u0441\u0447\u0435\u0442...", ""); + const created = await api("/api/admin/invoices", { + method: "POST", + body: { + request_id: targetRequestId, + status: "WAITING_PAYMENT", + amount: roundedAmount, + currency: "RUB", + payer_display_name: payerName, + payer_details: { + ...DEFAULT_INVOICE_REQUISITES, + request_track_number: trackNumber, + service_description: serviceLabel, + topic_name: topicLabel + } + } + }); + await loadRequestModalData(targetRequestId, { showLoading: false }); + if (typeof setStatus === "function") { + const invoiceNumber = String((created == null ? void 0 : created.invoice_number) || "").trim(); + setStatus("requestModal", invoiceNumber ? "\u0421\u0447\u0435\u0442 \u0432\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D: " + invoiceNumber : "\u0421\u0447\u0435\u0442 \u0432\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D", "ok"); + } + return created; + }, + [api, loadRequestModalData, requestModal.requestData, requestModal.requestId, requestModal.trackNumber, setStatus] + ); + return { + requestModal, + setRequestModal, + requestOpenGuardRef, + resetRequestWorkspaceState, + updateRequestModalMessageDraft, + appendRequestModalFiles, + removeRequestModalFile, + clearRequestModalFiles, + loadRequestModalData, + refreshRequestModal, + openRequestDetails, + clearPendingStatusChangePreset, + submitRequestStatusChange, + submitRequestModalMessage, + probeRequestLive, + setRequestTyping, + loadRequestDataTemplates, + loadRequestDataBatch, + loadRequestDataTemplateDetails, + saveRequestDataTemplate, + saveRequestDataBatch, + issueRequestInvoice + }; + } + + // app/web/admin/hooks/useTableActions.js + function useTableActions({ api, setStatus, resolveTableConfig, tablesRef, setTableState, setDictionaries, buildUniversalQuery: buildUniversalQuery2 }) { + const { useCallback } = React; + const loadTable = useCallback( + async (tableKey, options, tokenOverride) => { + const opts = options || {}; + const config = resolveTableConfig(tableKey); + if (!config) return false; + const current = tablesRef.current[tableKey] || createTableState(); + const next = { + ...current, + filters: Array.isArray(opts.filtersOverride) ? [...opts.filtersOverride] : [...current.filters || []], + sort: Array.isArray(opts.sortOverride) ? [...opts.sortOverride] : Array.isArray(current.sort) ? [...current.sort] : null, + rows: [...current.rows || []] + }; + if (opts.resetOffset) { + next.offset = 0; + next.showAll = false; + } + if (opts.loadAll) { + next.offset = 0; + next.showAll = true; + } + const statusKey = tableKey; + setStatus(statusKey, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", ""); + try { + const activeSort = next.sort && next.sort.length ? next.sort : config.sort; + let limit = next.showAll ? Math.max(next.total || PAGE_SIZE, PAGE_SIZE) : PAGE_SIZE; + const offset = next.showAll ? 0 : next.offset; + let data = await api( + config.endpoint, + { + method: "POST", + body: buildUniversalQuery2(next.filters, activeSort, limit, offset) + }, + tokenOverride + ); + next.total = Number(data.total || 0); + next.rows = data.rows || []; + if (next.showAll && next.total > next.rows.length) { + limit = next.total; + data = await api( + config.endpoint, + { + method: "POST", + body: buildUniversalQuery2(next.filters, activeSort, limit, 0) + }, + tokenOverride + ); + next.total = Number(data.total || next.total); + next.rows = data.rows || []; + } + if (!next.showAll && next.total > 0 && next.offset >= next.total) { + next.offset = Math.floor((next.total - 1) / PAGE_SIZE) * PAGE_SIZE; + setTableState(tableKey, next); + return loadTable(tableKey, {}, tokenOverride); + } + setTableState(tableKey, next); + if (tableKey === "requests") { + setDictionaries((prev) => { + const map = new Map((prev.topics || []).map((topic) => [topic.code, topic])); + (next.rows || []).forEach((row) => { + if (!row.topic_code || map.has(row.topic_code)) return; + map.set(row.topic_code, { code: row.topic_code, name: row.topic_code }); + }); + return { ...prev, topics: sortByName(Array.from(map.values())) }; + }); + } + if (tableKey === "topics") { + setDictionaries((prev) => ({ + ...prev, + topics: sortByName((next.rows || []).map((row) => ({ code: row.code, name: row.name || row.code }))) + })); + } + if (tableKey === "statuses") { + setDictionaries((prev) => { + const map = new Map(Object.entries(STATUS_LABELS).map(([code, name]) => [code, { code, name }])); + (next.rows || []).forEach((row) => { + if (!row.code) return; + map.set(row.code, { code: row.code, name: row.name || statusLabel(row.code) }); + }); + return { ...prev, statuses: sortByName(Array.from(map.values())) }; + }); + } + if (tableKey === "formFields" || tableKey === "form_fields") { + setDictionaries((prev) => { + const set = new Set(DEFAULT_FORM_FIELD_TYPES); + (next.rows || []).forEach((row) => { + if (row == null ? void 0 : row.type) set.add(row.type); + }); + const fieldKeys = (next.rows || []).filter((row) => row && row.key).map((row) => ({ key: row.key, label: row.label || row.key })).sort((a, b) => String(a.label || a.key).localeCompare(String(b.label || b.key), "ru")); + return { + ...prev, + formFieldTypes: Array.from(set.values()).sort((a, b) => String(a).localeCompare(String(b), "ru")), + formFieldKeys: fieldKeys + }; + }); + } + if (tableKey === "users" || tableKey === "admin_users") { + setDictionaries((prev) => { + const map = new Map((prev.users || []).map((user) => [user.id, user])); + (next.rows || []).forEach((row) => { + map.set(row.id, { + id: row.id, + name: row.name || "", + email: row.email || "", + role: row.role || "", + is_active: Boolean(row.is_active) + }); + }); + return { ...prev, users: Array.from(map.values()) }; + }); + } + setStatus(statusKey, "\u0421\u043F\u0438\u0441\u043E\u043A \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok"); + return true; + } catch (error) { + setStatus(statusKey, "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + return false; + } + }, + [api, buildUniversalQuery2, resolveTableConfig, setDictionaries, setStatus, setTableState, tablesRef] + ); + const loadPrevPage = useCallback( + (tableKey) => { + const tableState = tablesRef.current[tableKey] || createTableState(); + const next = { ...tableState, offset: Math.max(0, tableState.offset - PAGE_SIZE), showAll: false }; + setTableState(tableKey, next); + loadTable(tableKey, {}); + }, + [loadTable, setTableState, tablesRef] + ); + const loadNextPage = useCallback( + (tableKey) => { + const tableState = tablesRef.current[tableKey] || createTableState(); + if (tableState.offset + PAGE_SIZE >= tableState.total) return; + const next = { ...tableState, offset: tableState.offset + PAGE_SIZE, showAll: false }; + setTableState(tableKey, next); + loadTable(tableKey, {}); + }, + [loadTable, setTableState, tablesRef] + ); + const loadAllRows = useCallback( + (tableKey) => { + const tableState = tablesRef.current[tableKey] || createTableState(); + if (!tableState.total) return; + const next = { ...tableState, offset: 0, showAll: true }; + setTableState(tableKey, next); + loadTable(tableKey, { loadAll: true }); + }, + [loadTable, setTableState, tablesRef] + ); + const toggleTableSort = useCallback( + (tableKey, field) => { + const tableState = tablesRef.current[tableKey] || createTableState(); + const currentSort = Array.isArray(tableState.sort) ? tableState.sort[0] : null; + const dir = currentSort && currentSort.field === field ? currentSort.dir === "asc" ? "desc" : "asc" : "asc"; + const sortOverride = [{ field, dir }]; + const next = { ...tableState, sort: sortOverride, offset: 0, showAll: false }; + setTableState(tableKey, next); + loadTable(tableKey, { resetOffset: true, sortOverride }); + }, + [loadTable, setTableState, tablesRef] + ); + return { + loadTable, + loadPrevPage, + loadNextPage, + loadAllRows, + toggleTableSort + }; + } + + // app/web/admin/hooks/useTableFilterActions.js + function useTableFilterActions({ + filterModal, + closeFilterModal, + getFieldDef, + loadKanban, + loadTable, + setStatus, + setTableState, + tablesRef + }) { + const { useCallback } = React; + const applyFilterModal = useCallback( + async (event) => { + if (event && typeof event.preventDefault === "function") event.preventDefault(); + if (!filterModal.tableKey) return; + const fieldDef = getFieldDef(filterModal.tableKey, filterModal.field); + if (!fieldDef) { + setStatus("filter", "\u041F\u043E\u043B\u0435 \u0444\u0438\u043B\u044C\u0442\u0440\u0430 \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u043E", "error"); + return; + } + let value; + if (fieldDef.type === "boolean") { + value = filterModal.rawValue === "true"; + } else if (fieldDef.type === "number") { + if (String(filterModal.rawValue || "").trim() === "") { + setStatus("filter", "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043B\u043E", "error"); + return; + } + value = Number(filterModal.rawValue); + if (Number.isNaN(value)) { + setStatus("filter", "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0447\u0438\u0441\u043B\u043E", "error"); + return; + } + } else { + value = String(filterModal.rawValue || "").trim(); + if (!value) { + setStatus("filter", "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0444\u0438\u043B\u044C\u0442\u0440\u0430", "error"); + return; + } + } + const tableState = tablesRef.current[filterModal.tableKey] || createTableState(); + const nextFilters = [...tableState.filters || []]; + const nextClause = { field: fieldDef.field, op: filterModal.op, value }; + if (Number.isInteger(filterModal.editIndex) && filterModal.editIndex >= 0 && filterModal.editIndex < nextFilters.length) { + nextFilters[filterModal.editIndex] = nextClause; + } else { + const existingIndex = nextFilters.findIndex((item) => item.field === nextClause.field && item.op === nextClause.op); + if (existingIndex >= 0) nextFilters[existingIndex] = nextClause; + else nextFilters.push(nextClause); + } + setTableState(filterModal.tableKey, { + ...tableState, + filters: nextFilters, + offset: 0, + showAll: false + }); + closeFilterModal(); + if (filterModal.tableKey === "kanban") { + await loadKanban(void 0, { filtersOverride: nextFilters }); + } else { + await loadTable(filterModal.tableKey, { resetOffset: true, filtersOverride: nextFilters }); + } + }, + [closeFilterModal, filterModal, getFieldDef, loadKanban, loadTable, setStatus, setTableState, tablesRef] + ); + const clearFiltersFromModal = useCallback(async () => { + if (!filterModal.tableKey) return; + const tableState = tablesRef.current[filterModal.tableKey] || createTableState(); + setTableState(filterModal.tableKey, { + ...tableState, + filters: [], + offset: 0, + showAll: false + }); + closeFilterModal(); + if (filterModal.tableKey === "kanban") { + await loadKanban(void 0, { filtersOverride: [] }); + } else { + await loadTable(filterModal.tableKey, { resetOffset: true, filtersOverride: [] }); + } + }, [closeFilterModal, filterModal.tableKey, loadKanban, loadTable, setTableState, tablesRef]); + const removeFilterChip = useCallback( + async (tableKey, index) => { + const tableState = tablesRef.current[tableKey] || createTableState(); + const nextFilters = [...tableState.filters || []]; + nextFilters.splice(index, 1); + setTableState(tableKey, { + ...tableState, + filters: nextFilters, + offset: 0, + showAll: false + }); + if (tableKey === "kanban") { + await loadKanban(void 0, { filtersOverride: nextFilters }); + } else { + await loadTable(tableKey, { resetOffset: true, filtersOverride: nextFilters }); + } + }, + [loadKanban, loadTable, setTableState, tablesRef] + ); + return { + applyFilterModal, + clearFiltersFromModal, + removeFilterChip + }; + } + + // app/web/admin/hooks/useTablesState.js + function createInitialTablesState() { + return { + kanban: createTableState(), + requests: createTableState(), + serviceRequests: createTableState(), + invoices: createTableState(), + quotes: createTableState(), + topics: createTableState(), + statuses: createTableState(), + formFields: createTableState(), + topicRequiredFields: createTableState(), + topicDataTemplates: createTableState(), + statusTransitions: createTableState(), + users: createTableState(), + userTopics: createTableState(), + availableTables: createTableState() + }; + } + function useTablesState() { + const { useCallback, useEffect, useRef, useState } = React; + const [tables, setTables] = useState(createInitialTablesState); + const [tableCatalog, setTableCatalog] = useState([]); + const [referenceRowsMap, setReferenceRowsMap] = useState({}); + const tablesRef = useRef(tables); + useEffect(() => { + tablesRef.current = tables; + }, [tables]); + const setTableState = useCallback((tableKey, next) => { + setTables((prev) => ({ ...prev, [tableKey]: next })); + }, []); + const resetTablesState = useCallback(() => { + setTables(createInitialTablesState()); + setTableCatalog([]); + setReferenceRowsMap({}); + }, []); + return { + tables, + setTables, + tablesRef, + setTableState, + resetTablesState, + tableCatalog, + setTableCatalog, + referenceRowsMap, + setReferenceRowsMap + }; + } + + // app/web/admin.jsx + var import_qrcode = __toESM(require_browser()); + (function() { + const { useCallback, useEffect, useMemo, useRef, useState } = React; + const LEGACY_HIDDEN_DICTIONARY_TABLES = /* @__PURE__ */ new Set(["formFields", "topicRequiredFields", "statusTransitions"]); + const NEW_REQUEST_CLIENT_OPTION = "__new_client__"; + function StatusLine({ status }) { + return /* @__PURE__ */ React.createElement("p", { className: "status" + ((status == null ? void 0 : status.kind) ? " " + status.kind : "") }, (status == null ? void 0 : status.message) || ""); + } + function Section({ active, children, id }) { + return /* @__PURE__ */ React.createElement("section", { className: "section" + (active ? " active" : ""), id }, children); + } + function DataTable({ headers, rows, emptyColspan, renderRow, onSort, sortClause }) { + return /* @__PURE__ */ React.createElement("div", { className: "table-wrap table-scroll-region" }, /* @__PURE__ */ React.createElement("table", null, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, headers.map((header) => { + const h = typeof header === "string" ? { key: header, label: header } : header; + const sortable = Boolean(h.sortable && h.field && onSort); + const active = Boolean(sortable && sortClause && sortClause.field === h.field); + const direction = active ? sortClause.dir : ""; + return /* @__PURE__ */ React.createElement( + "th", + { + key: h.key || h.label, + className: sortable ? "sortable-th" : "", + onClick: sortable ? () => onSort(h.field) : void 0, + title: sortable ? "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0434\u043B\u044F \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438" : void 0 + }, + /* @__PURE__ */ React.createElement("span", { className: sortable ? "sortable-head" : "" }, h.label, sortable ? /* @__PURE__ */ React.createElement("span", { className: "sort-indicator" + (active ? " active" : "") }, direction === "desc" ? "\u2193" : "\u2191") : null) + ); + }))), /* @__PURE__ */ React.createElement("tbody", null, rows.length ? rows.map((row, index) => renderRow(row, index)) : /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: emptyColspan }, "\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445"))))); + } + function TablePager({ tableState, onPrev, onNext, onLoadAll, onRefresh, onCreate, onOpenFilter }) { + return /* @__PURE__ */ React.createElement("div", { className: "pager table-footer-bar" }, /* @__PURE__ */ React.createElement("div", null, tableState.showAll ? "\u0412\u0441\u0435\u0433\u043E: " + tableState.total + " \u2022 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0432\u0441\u0435 \u0437\u0430\u043F\u0438\u0441\u0438" : "\u0412\u0441\u0435\u0433\u043E: " + tableState.total + " \u2022 \u0441\u043C\u0435\u0449\u0435\u043D\u0438\u0435: " + tableState.offset), /* @__PURE__ */ React.createElement("div", { className: "table-footer-actions" }, /* @__PURE__ */ React.createElement( + "button", + { + className: "btn secondary table-control-btn table-control-loadall", + type: "button", + onClick: onLoadAll, + disabled: tableState.total === 0 || tableState.showAll || tableState.rows.length >= tableState.total, + title: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0432\u0441\u0435 " + tableState.total, + "aria-label": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0432\u0441\u0435 " + tableState.total + }, + /* @__PURE__ */ React.createElement(DownloadIcon, null), + /* @__PURE__ */ React.createElement("span", null, tableState.total) + ), onRefresh ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onRefresh, title: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C", "aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(RefreshIcon, null)) : null, onCreate ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onCreate, title: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C", "aria-label": "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(AddIcon, null)) : null, onOpenFilter ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onOpenFilter, title: "\u0424\u0438\u043B\u044C\u0442\u0440", "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" }, /* @__PURE__ */ React.createElement(FilterIcon, null)) : null, /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onPrev, disabled: tableState.showAll || tableState.offset <= 0, title: "\u041D\u0430\u0437\u0430\u0434", "aria-label": "\u041D\u0430\u0437\u0430\u0434" }, /* @__PURE__ */ React.createElement(PrevIcon, null)), /* @__PURE__ */ React.createElement( + "button", + { + className: "btn secondary table-control-btn", + type: "button", + onClick: onNext, + disabled: tableState.showAll || tableState.offset + PAGE_SIZE >= tableState.total, + title: "\u0412\u043F\u0435\u0440\u0435\u0434", + "aria-label": "\u0412\u043F\u0435\u0440\u0435\u0434" + }, + /* @__PURE__ */ React.createElement(NextIcon, null) + ))); + } + function FilterToolbar({ filters, onOpen, onRemove, onEdit, getChipLabel, hideAction = false }) { + return /* @__PURE__ */ React.createElement("div", { className: "filter-toolbar" }, /* @__PURE__ */ React.createElement("div", { className: "filter-chips" }, filters.length ? filters.map((filter, index) => /* @__PURE__ */ React.createElement( + "div", + { + className: "filter-chip", + key: filter.field + filter.op + index, + onClick: () => onEdit(index), + role: "button", + tabIndex: 0, + onKeyDown: (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onEdit(index); + } + }, + title: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0444\u0438\u043B\u044C\u0442\u0440" + }, + /* @__PURE__ */ React.createElement("span", null, getChipLabel(filter)), + /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + "aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0444\u0438\u043B\u044C\u0442\u0440", + onClick: (event) => { + event.stopPropagation(); + onRemove(index); + } + }, + "\xD7" + ) + )) : /* @__PURE__ */ React.createElement("span", { className: "chip-placeholder" }, "\u0424\u0438\u043B\u044C\u0442\u0440\u044B \u043D\u0435 \u0437\u0430\u0434\u0430\u043D\u044B")), !hideAction ? /* @__PURE__ */ React.createElement("div", { className: "filter-action" }, /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onOpen, title: "\u0424\u0438\u043B\u044C\u0442\u0440", "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" }, /* @__PURE__ */ React.createElement(FilterIcon, null))) : null); + } + function Overlay({ open, onClose, children, id }) { + return /* @__PURE__ */ React.createElement("div", { className: "overlay" + (open ? " open" : ""), id, onClick: onClose }, children); + } + function IconButton({ icon, tooltip, onClick, tone }) { + const handleClick = (event) => { + event.preventDefault(); + event.stopPropagation(); + if (event.nativeEvent && typeof event.nativeEvent.stopImmediatePropagation === "function") { + event.nativeEvent.stopImmediatePropagation(); + } + if (typeof onClick === "function") onClick(event); + }; + const handleAuxClick = (event) => { + event.preventDefault(); + event.stopPropagation(); + if (event.nativeEvent && typeof event.nativeEvent.stopImmediatePropagation === "function") { + event.nativeEvent.stopImmediatePropagation(); + } + }; + return /* @__PURE__ */ React.createElement( + "button", + { + className: "icon-btn" + (tone ? " " + tone : ""), + type: "button", + "data-tooltip": tooltip, + onClick: handleClick, + onAuxClick: handleAuxClick, + "aria-label": tooltip + }, + icon + ); + } + function UserAvatar({ name, email, avatarUrl, accessToken, size = 32 }) { + const [broken, setBroken] = useState(false); + useEffect(() => setBroken(false), [avatarUrl]); + const initials = userInitials(name, email); + const bg = avatarColor(name || email || initials); + const src = resolveAvatarSrc(avatarUrl, accessToken); + const canShowImage = Boolean(src && !broken); + return /* @__PURE__ */ React.createElement("span", { className: "avatar", style: { width: size + "px", height: size + "px", backgroundColor: bg } }, canShowImage ? /* @__PURE__ */ React.createElement("img", { src, alt: name || email || "avatar", onError: () => setBroken(true) }) : /* @__PURE__ */ React.createElement("span", null, initials)); + } + function LoginScreen({ onSubmit, status }) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [totpCode, setTotpCode] = useState(""); + const submit = (event) => { + event.preventDefault(); + onSubmit(email, password, totpCode); + }; + return /* @__PURE__ */ React.createElement("div", { className: "login-screen" }, /* @__PURE__ */ React.createElement("div", { className: "login-card" }, /* @__PURE__ */ React.createElement("h2", null, "\u0412\u0445\u043E\u0434 \u0432 \u0430\u0434\u043C\u0438\u043D-\u043F\u0430\u043D\u0435\u043B\u044C"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0435\u0442\u043D\u0443\u044E \u0437\u0430\u043F\u0438\u0441\u044C \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u0438\u043B\u0438 \u044E\u0440\u0438\u0441\u0442\u0430."), /* @__PURE__ */ React.createElement("form", { className: "stack", style: { marginTop: "0.7rem" }, onSubmit: submit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "login-email" }, "\u042D\u043B. \u043F\u043E\u0447\u0442\u0430"), /* @__PURE__ */ React.createElement( + "input", + { + id: "login-email", + type: "email", + required: true, + placeholder: "admin@example.com", + value: email, + onChange: (event) => setEmail(event.target.value) + } + )), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "login-password" }, "\u041F\u0430\u0440\u043E\u043B\u044C"), /* @__PURE__ */ React.createElement( + "input", + { + id: "login-password", + type: "password", + required: true, + placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", + value: password, + onChange: (event) => setPassword(event.target.value) + } + )), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "login-totp" }, "TOTP / \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0439 \u043A\u043E\u0434"), /* @__PURE__ */ React.createElement( + "input", + { + id: "login-totp", + type: "text", + placeholder: "123456 \u0438\u043B\u0438 backup-code", + value: totpCode, + onChange: (event) => setTotpCode(event.target.value) + } + )), /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit" }, "\u0412\u043E\u0439\u0442\u0438"), /* @__PURE__ */ React.createElement(StatusLine, { status })))); + } + function FilterModal({ + open, + tableLabel, + fields, + draft, + status, + onClose, + onFieldChange, + onOpChange, + onValueChange, + onSubmit, + onClear, + getOperators, + getFieldOptions + }) { + if (!open) return null; + const selectedField = fields.find((field) => field.field === draft.field) || fields[0] || null; + const operators = getOperators((selectedField == null ? void 0 : selectedField.type) || "text"); + const options = selectedField ? getFieldOptions(selectedField) : []; + return /* @__PURE__ */ React.createElement(Overlay, { open, id: "filter-overlay", onClose: (event) => event.target.id === "filter-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal", style: { width: "min(560px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0424\u0438\u043B\u044C\u0442\u0440 \u0442\u0430\u0431\u043B\u0438\u0446\u044B"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, tableLabel ? (draft.editIndex !== null ? "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0444\u0438\u043B\u044C\u0442\u0440\u0430 \u2022 " : "\u041D\u043E\u0432\u044B\u0439 \u0444\u0438\u043B\u044C\u0442\u0440 \u2022 ") + "\u0422\u0430\u0431\u043B\u0438\u0446\u0430: " + tableLabel : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u043E\u043B\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0438 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435.")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "filter-field" }, "\u041F\u043E\u043B\u0435"), /* @__PURE__ */ React.createElement("select", { id: "filter-field", value: draft.field, onChange: onFieldChange }, fields.map((field) => /* @__PURE__ */ React.createElement("option", { value: field.field, key: field.field }, field.label)))), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "filter-op" }, "\u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440"), /* @__PURE__ */ React.createElement("select", { id: "filter-op", value: draft.op, onChange: onOpChange }, operators.map((op) => /* @__PURE__ */ React.createElement("option", { value: op, key: op }, OPERATOR_LABELS[op])))), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "filter-value" }, selectedField ? "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435: " + selectedField.label : "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435"), !selectedField || selectedField.type === "text" ? /* @__PURE__ */ React.createElement("input", { id: "filter-value", type: "text", value: draft.rawValue, onChange: onValueChange, placeholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435" }) : selectedField.type === "number" ? /* @__PURE__ */ React.createElement("input", { id: "filter-value", type: "number", step: "any", value: draft.rawValue, onChange: onValueChange, placeholder: "\u0427\u0438\u0441\u043B\u043E" }) : selectedField.type === "date" ? /* @__PURE__ */ React.createElement("input", { id: "filter-value", type: "date", value: draft.rawValue, onChange: onValueChange }) : selectedField.type === "boolean" ? /* @__PURE__ */ React.createElement("select", { id: "filter-value", value: draft.rawValue, onChange: onValueChange }, /* @__PURE__ */ React.createElement("option", { value: "true" }, "True"), /* @__PURE__ */ React.createElement("option", { value: "false" }, "False")) : selectedField.type === "reference" || selectedField.type === "enum" ? /* @__PURE__ */ React.createElement("select", { id: "filter-value", value: draft.rawValue, onChange: onValueChange, disabled: !options.length }, !options.length ? /* @__PURE__ */ React.createElement("option", { value: "" }, "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439") : options.map((option) => /* @__PURE__ */ React.createElement("option", { value: String(option.value), key: String(option.value) }, option.label))) : /* @__PURE__ */ React.createElement("input", { id: "filter-value", type: "text", value: draft.rawValue, onChange: onValueChange, placeholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435" })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit" }, draft.editIndex !== null ? "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C" : "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClear }, "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0441\u0435"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose }, "\u041E\u0442\u043C\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement(StatusLine, { status })))); + } + function ReassignModal({ open, status, options, value, onChange, onClose, onSubmit, trackNumber }) { + if (!open) return null; + return /* @__PURE__ */ React.createElement(Overlay, { open, id: "reassign-overlay", onClose: (event) => event.target.id === "reassign-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal", style: { width: "min(520px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, trackNumber ? "\u0417\u0430\u044F\u0432\u043A\u0430: " + trackNumber : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043D\u043E\u0432\u043E\u0433\u043E \u044E\u0440\u0438\u0441\u0442\u0430")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "reassign-lawyer" }, "\u041D\u043E\u0432\u044B\u0439 \u044E\u0440\u0438\u0441\u0442"), /* @__PURE__ */ React.createElement("select", { id: "reassign-lawyer", value, onChange, disabled: !options.length }, !options.length ? /* @__PURE__ */ React.createElement("option", { value: "" }, "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u044E\u0440\u0438\u0441\u0442\u043E\u0432") : options.map((option) => /* @__PURE__ */ React.createElement("option", { value: String(option.value), key: String(option.value) }, option.label)))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit", disabled: !value }, "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose }, "\u041E\u0442\u043C\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement(StatusLine, { status })))); + } + function KanbanSortModal({ open, value, status, onChange, onClose, onSubmit }) { + if (!open) return null; + return /* @__PURE__ */ React.createElement(Overlay, { open, id: "kanban-sort-overlay", onClose: (event) => event.target.id === "kanban-sort-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal", style: { width: "min(520px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0421\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u043A\u0430\u043D\u0431\u0430\u043D\u0430"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043F\u043E\u0441\u043E\u0431 \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438 \u043A\u0430\u0440\u0442\u043E\u0447\u0435\u043A.")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "kanban-sort-mode" }, "\u0422\u0438\u043F \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("select", { id: "kanban-sort-mode", value, onChange }, /* @__PURE__ */ React.createElement("option", { value: "created_newest" }, "\u0414\u0430\u0442\u0430 \u0437\u0430\u044F\u0432\u043A\u0438 (\u043D\u043E\u0432\u044B\u0435 \u0441\u0432\u0435\u0440\u0445\u0443)"), /* @__PURE__ */ React.createElement("option", { value: "lawyer" }, "\u042E\u0440\u0438\u0441\u0442"), /* @__PURE__ */ React.createElement("option", { value: "deadline" }, "\u0414\u0435\u0434\u043B\u0430\u0439\u043D"))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit" }, "\u041E\u043A"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose }, "\u041E\u0442\u043C\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement(StatusLine, { status })))); + } + function TotpSetupModal({ + open, + status, + secret, + uri, + qrDataUrl, + code, + loading, + onCodeChange, + onClose, + onSubmit, + onCopySecret, + onCopyUri + }) { + if (!open) return null; + return /* @__PURE__ */ React.createElement(Overlay, { open, id: "totp-setup-overlay", onClose: (event) => event.target.id === "totp-setup-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal", style: { width: "min(700px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 2FA"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, "\u0421\u043A\u0430\u043D\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043A\u043E\u0434 \u0432 Google Authenticator \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 6-\u0437\u043D\u0430\u0447\u043D\u044B\u043C \u043A\u043E\u0434\u043E\u043C.")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "totp-setup-grid" }, /* @__PURE__ */ React.createElement("div", { className: "totp-qr-box" }, qrDataUrl ? /* @__PURE__ */ React.createElement("img", { className: "totp-qr-img", src: qrDataUrl, alt: "QR-\u043A\u043E\u0434 \u0434\u043B\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 2FA" }) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "QR-\u043A\u043E\u0434 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u043A\u043B\u044E\u0447 \u0432\u0440\u0443\u0447\u043D\u0443\u044E.")), /* @__PURE__ */ React.createElement("div", { className: "stack" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "totp-secret" }, "\u0421\u0435\u043A\u0440\u0435\u0442\u043D\u044B\u0439 \u043A\u043B\u044E\u0447"), /* @__PURE__ */ React.createElement("input", { id: "totp-secret", type: "text", value: secret, readOnly: true })), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "totp-uri" }, "URI (otpauth)"), /* @__PURE__ */ React.createElement("textarea", { id: "totp-uri", rows: 3, value: uri, readOnly: true })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.5rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onCopySecret }, "\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u043B\u044E\u0447"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onCopyUri }, "\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C URI")))), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "totp-verify-code" }, "\u041A\u043E\u0434 \u0438\u0437 Google Authenticator"), /* @__PURE__ */ React.createElement( + "input", + { + id: "totp-verify-code", + type: "text", + inputMode: "numeric", + autoComplete: "one-time-code", + placeholder: "123456", + value: code, + onChange: onCodeChange + } + )), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit", disabled: loading }, loading ? "\u0412\u043A\u043B\u044E\u0447\u0430\u0435\u043C..." : "\u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C 2FA"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose, disabled: loading }, "\u041E\u0442\u043C\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement(StatusLine, { status })))); + } + function AccountModal({ + open, + status, + profileLoading, + saveLoading, + form, + currentEmail, + currentRoleLabel, + totpStatus, + onFieldChange, + onClose, + onSubmit, + onSetupTotp, + onRegenerateBackupCodes, + onDisableTotp, + onLogout + }) { + if (!open) return null; + return /* @__PURE__ */ React.createElement(Overlay, { open, id: "account-overlay", onClose: (event) => event.target.id === "account-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal account-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u041B\u0438\u0447\u043D\u044B\u0439 \u043A\u0430\u0431\u0438\u043D\u0435\u0442"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, "\u041F\u0440\u043E\u0444\u0438\u043B\u044C \u0438 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0441\u0442\u044C \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430.")), /* @__PURE__ */ React.createElement("div", { className: "modal-head-actions" }, /* @__PURE__ */ React.createElement("button", { className: "icon-btn", type: "button", "data-tooltip": "\u0412\u044B\u0439\u0442\u0438 \u0438\u0437 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430", "aria-label": "\u0412\u044B\u0439\u0442\u0438 \u0438\u0437 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430", onClick: onLogout }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "path", + { + d: "M15.4 5.4a1 1 0 0 1 1.4 0l5.2 5.2a1 1 0 0 1 0 1.4l-5.2 5.2a1 1 0 1 1-1.4-1.4l3.5-3.4H9a1 1 0 1 1 0-2h9.9l-3.5-3.4a1 1 0 0 1 0-1.4zM3 4a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H5v14h6a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1V4z", + fill: "currentColor" + } + ))), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7"))), profileLoading ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u0440\u043E\u0444\u0438\u043B\u044F...") : /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "account-security-box" }, "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C: ", /* @__PURE__ */ React.createElement("b", null, currentEmail || "-"), /* @__PURE__ */ React.createElement("br", null), "\u0420\u043E\u043B\u044C: ", /* @__PURE__ */ React.createElement("b", null, currentRoleLabel || "-"), /* @__PURE__ */ React.createElement("br", null), "2FA: ", /* @__PURE__ */ React.createElement("b", null, totpStatus.enabled ? "\u0412\u043A\u043B\u044E\u0447\u0435\u043D\u0430" : "\u0412\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement("div", { className: "account-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-name" }, "\u0418\u043C\u044F"), /* @__PURE__ */ React.createElement("input", { id: "account-name", name: "name", type: "text", value: form.name, onChange: onFieldChange })), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-email" }, "\u041F\u043E\u0447\u0442\u0430"), /* @__PURE__ */ React.createElement("input", { id: "account-email", name: "email", type: "email", value: form.email, onChange: onFieldChange }))), /* @__PURE__ */ React.createElement("div", { className: "account-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-phone" }, "\u0422\u0435\u043B\u0435\u0444\u043E\u043D"), /* @__PURE__ */ React.createElement("input", { id: "account-phone", name: "phone", type: "text", value: form.phone, onChange: onFieldChange })), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-password" }, "\u041D\u043E\u0432\u044B\u0439 \u043F\u0430\u0440\u043E\u043B\u044C"), /* @__PURE__ */ React.createElement( + "input", + { + id: "account-password", + name: "password", + type: "password", + autoComplete: "new-password", + value: form.password, + onChange: onFieldChange, + placeholder: "\u041E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0441\u0442\u044B\u043C, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043C\u0435\u043D\u044F\u0435\u0442\u0435" + } + ))), /* @__PURE__ */ React.createElement("div", { className: "account-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-password-confirm" }, "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u0430\u0440\u043E\u043B\u044F"), /* @__PURE__ */ React.createElement( + "input", + { + id: "account-password-confirm", + name: "passwordConfirm", + type: "password", + autoComplete: "new-password", + value: form.passwordConfirm, + onChange: onFieldChange + } + )), /* @__PURE__ */ React.createElement("div", { className: "field" })), /* @__PURE__ */ React.createElement("div", { className: "account-security-box" }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: "0.5rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("b", null, "2FA"), ": ", totpStatus.enabled ? "\u0412\u043A\u043B\u044E\u0447\u0435\u043D\u0430" : "\u0412\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u0430"), /* @__PURE__ */ React.createElement("div", { className: "muted" }, "\u0420\u0435\u0436\u0438\u043C: ", String(totpStatus.mode || "-"))), /* @__PURE__ */ React.createElement("div", { style: { marginTop: "0.6rem", display: "flex", gap: "0.45rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onSetupTotp }, "\u041D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C 2FA"), totpStatus.enabled ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onRegenerateBackupCodes }, "Backup-\u043A\u043E\u0434\u044B"), /* @__PURE__ */ React.createElement("button", { className: "btn danger", type: "button", onClick: onDisableTotp }, "\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C 2FA")) : null)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit", disabled: saveLoading }, saveLoading ? "\u0421\u043E\u0445\u0440\u0430\u043D\u044F\u0435\u043C..." : "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose, disabled: saveLoading }, "\u0417\u0430\u043A\u0440\u044B\u0442\u044C")), /* @__PURE__ */ React.createElement(StatusLine, { status })))); + } + 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, ""); + const normalized = text.length > 2e5 ? text.slice(0, 2e5) + "\n\n[\u0422\u0435\u043A\u0441\u0442 \u043E\u0431\u0440\u0435\u0437\u0430\u043D \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430]" : text; + return normalized; + }; + useEffect(() => { + if (!open || !url) { + setResolvedUrl(""); + setResolvedText(""); + setResolvedKind(""); + setHint(""); + setLoading(false); + setError(""); + return; + } + const kind2 = detectAttachmentPreviewKind(fileName, mimeType); + setResolvedKind(kind2); + setResolvedText(""); + setHint(""); + if (kind2 === "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("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430"); + const buffer = await response.arrayBuffer(); + if (cancelled) return; + if (kind2 === "pdf") { + const header = new Uint8Array(buffer.slice(0, 5)); + const isPdf = header.length >= 5 && header[0] === 37 && header[1] === 80 && header[2] === 68 && header[3] === 70 && header[4] === 45; + if (isPdf) { + setResolvedUrl(String(url)); + setResolvedKind("pdf"); + setLoading(false); + return; + } + const textPreview = decodeTextPreview(buffer); + if (textPreview != null) { + setResolvedUrl(""); + setResolvedText(textPreview); + setResolvedKind("text"); + setHint("\u0424\u0430\u0439\u043B \u043F\u043E\u043C\u0435\u0447\u0435\u043D \u043A\u0430\u043A PDF, \u043D\u043E \u043D\u0435 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0432\u0430\u043B\u0438\u0434\u043D\u044B\u043C PDF. \u041F\u043E\u043A\u0430\u0437\u0430\u043D \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440."); + setLoading(false); + return; + } + throw new Error("\u0424\u0430\u0439\u043B \u043F\u043E\u043C\u0435\u0447\u0435\u043D \u043A\u0430\u043A PDF, \u043D\u043E \u043D\u0435 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0432\u0430\u043B\u0438\u0434\u043D\u044B\u043C PDF-\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u043C."); + } + if (kind2 === "text") { + const textPreview = decodeTextPreview(buffer); + if (textPreview == null) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u0442\u044C \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439 \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430."); + 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(kind2); + setLoading(false); + } catch (err) { + if (cancelled) return; + setError(err instanceof Error ? err.message : "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440"); + 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 /* @__PURE__ */ React.createElement(Overlay, { open, id: "request-file-preview-overlay", onClose: (event) => event.target.id === "request-file-preview-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal request-preview-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("h3", null, title || fileName || "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u0444\u0430\u0439\u043B\u0430"), /* @__PURE__ */ React.createElement("div", { className: "request-preview-head-actions" }, /* @__PURE__ */ React.createElement( + "a", + { + className: "icon-btn file-action-btn request-preview-download-icon", + href: url, + target: "_blank", + rel: "noreferrer", + "aria-label": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C \u0444\u0430\u0439\u043B", + "data-tooltip": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C" + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "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" + } + )) + ), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7"))), /* @__PURE__ */ React.createElement("div", { className: "request-preview-body" }, loading ? /* @__PURE__ */ React.createElement("p", { className: "request-preview-note" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430...") : null, !loading && !error && hint ? /* @__PURE__ */ React.createElement("p", { className: "request-preview-note" }, hint) : null, error ? /* @__PURE__ */ React.createElement("p", { className: "request-preview-note" }, error) : null, !loading && !error && kind === "image" && resolvedUrl ? /* @__PURE__ */ React.createElement("img", { className: "request-preview-image", src: resolvedUrl, alt: fileName || "attachment" }) : null, !loading && !error && kind === "video" && resolvedUrl ? /* @__PURE__ */ React.createElement("video", { className: "request-preview-video", src: resolvedUrl, controls: true, preload: "metadata" }) : null, !loading && !error && kind === "pdf" && resolvedUrl ? /* @__PURE__ */ React.createElement("iframe", { className: "request-preview-frame", src: resolvedUrl, title: fileName || "preview" }) : null, !loading && !error && kind === "text" ? /* @__PURE__ */ React.createElement("pre", { className: "request-preview-text" }, resolvedText || "\u0424\u0430\u0439\u043B \u043F\u0443\u0441\u0442.") : null, kind === "none" ? /* @__PURE__ */ React.createElement("p", { className: "request-preview-note" }, "\u0414\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0442\u0438\u043F\u0430 \u0444\u0430\u0439\u043B\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u0442\u043A\u0440\u044B\u0442\u0438\u0435 \u0438\u043B\u0438 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435.") : null))); + } + function RecordModal({ open, title, fields, form, status, onClose, onChange, onSubmit, onUploadField }) { + if (!open) return null; + const visibleFields = (fields || []).filter((field) => { + if (typeof field.visibleWhen !== "function") return true; + try { + return Boolean(field.visibleWhen(form || {})); + } catch (_) { + return true; + } + }); + const renderField = (field) => { + var _a; + const value = (_a = form[field.key]) != null ? _a : ""; + const options = typeof field.options === "function" ? field.options() : []; + const id = "record-field-" + field.key; + const disabled = Boolean(field.readOnly) || (typeof field.readOnlyWhen === "function" ? Boolean(field.readOnlyWhen(form || {})) : false); + if (field.type === "textarea" || field.type === "json") { + return /* @__PURE__ */ React.createElement( + "textarea", + { + id, + value, + onChange: (event) => onChange(field.key, event.target.value), + placeholder: field.placeholder || "", + required: Boolean(field.required), + disabled + } + ); + } + if (field.type === "boolean") { + return /* @__PURE__ */ React.createElement("select", { id, value, onChange: (event) => onChange(field.key, event.target.value), disabled }, /* @__PURE__ */ React.createElement("option", { value: "true" }, "\u0414\u0430"), /* @__PURE__ */ React.createElement("option", { value: "false" }, "\u041D\u0435\u0442")); + } + if (field.type === "reference" || field.type === "enum") { + const extraOptions = Array.isArray(field.extraOptions) ? field.extraOptions : []; + const hasCurrentValue = String(value || "").trim() !== "" && [...extraOptions, ...options].some((option) => String((option == null ? void 0 : option.value) || "") === String(value)); + return /* @__PURE__ */ React.createElement("select", { id, value, onChange: (event) => onChange(field.key, event.target.value), disabled }, field.optional ? /* @__PURE__ */ React.createElement("option", { value: "" }, "-") : null, !hasCurrentValue && String(value || "").trim() !== "" ? /* @__PURE__ */ React.createElement("option", { value: String(value) }, String(value)) : null, extraOptions.map((option) => /* @__PURE__ */ React.createElement("option", { value: String(option.value), key: String(option.value) }, option.label)), options.map((option) => /* @__PURE__ */ React.createElement("option", { value: String(option.value), key: String(option.value) }, option.label))); + } + if (field.uploadScope) { + return /* @__PURE__ */ React.createElement("div", { className: "field-inline" }, /* @__PURE__ */ React.createElement( + "input", + { + id, + type: "text", + value, + onChange: (event) => onChange(field.key, event.target.value), + placeholder: field.placeholder || "", + required: Boolean(field.required), + disabled + } + ), /* @__PURE__ */ React.createElement("label", { className: "btn secondary btn-sm", style: { whiteSpace: "nowrap", opacity: disabled ? 0.6 : 1, pointerEvents: disabled ? "none" : "auto" } }, "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C", /* @__PURE__ */ React.createElement( + "input", + { + type: "file", + accept: field.accept || "*/*", + style: { display: "none" }, + onChange: (event) => { + const file = event.target.files && event.target.files[0]; + if (file && onUploadField) onUploadField(field, file); + event.target.value = ""; + }, + disabled + } + ))); + } + return /* @__PURE__ */ React.createElement( + "input", + { + id, + type: field.type === "number" ? "number" : field.type === "password" ? "password" : "text", + step: field.type === "number" ? "any" : void 0, + value, + onChange: (event) => onChange(field.key, event.target.value), + placeholder: field.placeholder || "", + required: Boolean(field.required), + disabled + } + ); + }; + return /* @__PURE__ */ React.createElement(Overlay, { open, id: "record-overlay", onClose: (event) => event.target.id === "record-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal", style: { width: "min(760px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, title), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, "\u0421\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0437\u0430\u043F\u0438\u0441\u0438.")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "filters", style: { gridTemplateColumns: "repeat(2, minmax(0,1fr))" } }, visibleFields.map((field) => /* @__PURE__ */ React.createElement("div", { className: "field", key: field.key, style: field.fullRow ? { gridColumn: "1 / -1" } : void 0 }, /* @__PURE__ */ React.createElement("label", { htmlFor: "record-field-" + field.key }, field.label), renderField(field)))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit" }, "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose }, "\u041E\u0442\u043C\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement(StatusLine, { status })))); + } + 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 /* @__PURE__ */ React.createElement( + "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 + ); + } + function App() { + var _a; + const routeInfo = useMemo(() => resolveAdminRoute(window.location.search), []); + const isRequestWorkspaceRoute = routeInfo.view === "request" && Boolean(routeInfo.requestId); + const initialSection = isRequestWorkspaceRoute ? "requestWorkspace" : routeInfo.section || "dashboard"; + const [token, setToken] = useState(""); + const [role, setRole] = useState(""); + const [email, setEmail] = useState(""); + const [userId, setUserId] = useState(""); + const [activeSection, setActiveSection] = useState(initialSection); + const [dashboardData, setDashboardData] = useState({ + scope: "", + cards: [], + byStatus: {}, + lawyerLoads: [], + myUnreadByEvent: {}, + myUnreadTotal: 0, + myUnreadNotificationsTotal: 0, + unreadForClients: 0, + unreadForLawyers: 0, + serviceRequestUnreadTotal: 0, + deadlineAlertTotal: 0, + monthRevenue: 0, + monthExpenses: 0 + }); + const { + tables, + tablesRef, + setTableState, + resetTablesState, + tableCatalog, + setTableCatalog, + referenceRowsMap, + setReferenceRowsMap + } = useTablesState(); + const [dictionaries, setDictionaries] = useState({ + topics: [], + statuses: Object.entries(STATUS_LABELS).map(([code, name]) => ({ code, name })), + formFieldTypes: [...DEFAULT_FORM_FIELD_TYPES], + formFieldKeys: [], + users: [] + }); + const [statusMap, setStatusMap] = useState({}); + const [smsProviderHealth, setSmsProviderHealth] = useState(null); + const [totpStatus, setTotpStatus] = useState({ + mode: "password_totp_optional", + enabled: false, + required: false, + has_backup_codes: false + }); + const [totpSetupModal, setTotpSetupModal] = useState({ + open: false, + secret: "", + uri: "", + qrDataUrl: "", + code: "", + loading: false + }); + const [accountModal, setAccountModal] = useState({ + open: false, + loading: false, + saving: false, + initial: { + name: "", + email: "", + phone: "" + }, + form: { + name: "", + email: "", + phone: "", + password: "", + passwordConfirm: "" + } + }); + const [recordModal, setRecordModal] = useState({ + open: false, + tableKey: null, + mode: "create", + rowId: null, + form: {} + }); + const [configActiveKey, setConfigActiveKey] = useState(""); + const [referencesExpanded, setReferencesExpanded] = useState(true); + const [statusDesignerTopicCode, setStatusDesignerTopicCode] = useState(""); + const [metaEntity, setMetaEntity] = useState("quotes"); + const [metaJson, setMetaJson] = useState(""); + const [filterModal, setFilterModal] = useState({ + open: false, + tableKey: null, + field: "", + op: "=", + rawValue: "", + editIndex: null + }); + const [reassignModal, setReassignModal] = useState({ + open: false, + requestId: null, + trackNumber: "", + lawyerId: "" + }); + const initialRouteHandledRef = useRef(false); + const statusDesignerLoadedTopicRef = useRef(""); + const setStatus = useCallback((key, message, kind) => { + setStatusMap((prev) => ({ ...prev, [key]: { message: message || "", kind: kind || "" } })); + }, []); + const getStatus = useCallback((key) => statusMap[key] || { message: "", kind: "" }, [statusMap]); + const api = useAdminApi(token); + const { + requestModal, + setRequestModal, + resetRequestWorkspaceState, + updateRequestModalMessageDraft, + appendRequestModalFiles, + removeRequestModalFile, + clearRequestModalFiles, + loadRequestModalData, + refreshRequestModal, + openRequestDetails, + clearPendingStatusChangePreset, + submitRequestStatusChange, + submitRequestModalMessage, + probeRequestLive, + setRequestTyping, + loadRequestDataTemplates, + loadRequestDataBatch, + loadRequestDataTemplateDetails, + saveRequestDataTemplate, + saveRequestDataBatch, + issueRequestInvoice + } = useRequestWorkspace({ + api, + setStatus, + setActiveSection, + token, + users: dictionaries.users, + buildUniversalQuery, + resolveAdminObjectSrc + }); + const getStatusOptions = useCallback(() => { + return (dictionaries.statuses || []).filter((item) => item && item.code).map((item) => ({ value: item.code, label: String(item.name || "").trim() || humanizeKey(item.code) })); + }, [dictionaries.statuses]); + const getInvoiceStatusOptions = useCallback(() => { + return Object.entries(INVOICE_STATUS_LABELS).map(([code, name]) => ({ value: code, label: name })); + }, []); + const getStatusKindOptions = useCallback(() => { + return Object.entries(STATUS_KIND_LABELS).map(([code, name]) => ({ value: code, label: name })); + }, []); + const getTopicOptions = useCallback(() => { + return (dictionaries.topics || []).filter((item) => item && item.code).map((item) => ({ value: item.code, label: String(item.name || "").trim() || humanizeKey(item.code) })); + }, [dictionaries.topics]); + const getLawyerOptions = useCallback(() => { + return (dictionaries.users || []).filter((item) => item && item.id && String(item.role || "").toUpperCase() === "LAWYER").map((item) => ({ + value: item.id, + label: (item.name || item.email || item.id) + (item.email ? " (" + item.email + ")" : "") + })); + }, [dictionaries.users]); + const getFormFieldTypeOptions = useCallback(() => { + return (dictionaries.formFieldTypes || []).filter(Boolean).map((item) => ({ value: item, label: item })); + }, [dictionaries.formFieldTypes]); + const getRequestDataValueTypeOptions = useCallback(() => { + return [ + { value: "string", label: "\u0421\u0442\u0440\u043E\u043A\u0430" }, + { value: "date", label: "\u0414\u0430\u0442\u0430" }, + { value: "number", label: "\u0427\u0438\u0441\u043B\u043E" }, + { value: "file", label: "\u0424\u0430\u0439\u043B" }, + { value: "text", label: "\u0422\u0435\u043A\u0441\u0442" } + ]; + }, []); + const getFormFieldKeyOptions = useCallback(() => { + return (dictionaries.formFieldKeys || []).filter((item) => item && item.key).map((item) => ({ value: item.key, label: String(item.label || "").trim() || humanizeKey(item.key) })); + }, [dictionaries.formFieldKeys]); + const getRoleOptions = useCallback(() => { + return Object.entries(ROLE_LABELS).map(([code, label]) => ({ value: code, label })); + }, []); + const tableCatalogMap = useMemo(() => { + const map = {}; + (tableCatalog || []).forEach((item) => { + if (!item || !item.key) return; + map[item.key] = item; + }); + return map; + }, [tableCatalog]); + const getReferenceOptions = useCallback( + (rawReference) => { + const reference = normalizeReferenceMeta(rawReference); + if (!reference) return []; + const rows = referenceRowsMap[reference.table] || []; + const map = /* @__PURE__ */ new Map(); + rows.forEach((row) => { + if (!row || typeof row !== "object") return; + const rawValue = row[reference.value_field]; + if (rawValue == null || rawValue === "") return; + const value = String(rawValue); + const labelRaw = row[reference.label_field]; + const label = String(labelRaw == null || labelRaw === "" ? rawValue : labelRaw); + if (!map.has(value)) map.set(value, label); + }); + return Array.from(map.entries()).map(([value, label]) => ({ value, label })).sort((a, b) => String(a.label).localeCompare(String(b.label), "ru")); + }, + [referenceRowsMap] + ); + const resolveReferenceLabel = useCallback( + (rawReference, rawValue) => { + if (rawValue == null || rawValue === "") return "-"; + const value = String(rawValue); + const options = getReferenceOptions(rawReference); + const found = options.find((item) => String(item.value) === value); + return found ? found.label : value; + }, + [getReferenceOptions] + ); + const getStatusGroupOptions = useCallback(() => { + return getReferenceOptions({ table: "status_groups", value_field: "id", label_field: "name" }); + }, [getReferenceOptions]); + const getClientOptions = useCallback(() => { + return getReferenceOptions({ table: "clients", value_field: "id", label_field: "full_name" }); + }, [getReferenceOptions]); + const getInvoiceRequestRows = useCallback(() => { + var _a2; + const fromReferences = Array.isArray(referenceRowsMap.requests) ? referenceRowsMap.requests : []; + const fromTable = Array.isArray((_a2 = tables.requests) == null ? void 0 : _a2.rows) ? tables.requests.rows : []; + const byTrack = /* @__PURE__ */ new Map(); + [...fromReferences, ...fromTable].forEach((row) => { + const track = String((row == null ? void 0 : row.track_number) || "").trim().toUpperCase(); + if (!track) return; + if (!byTrack.has(track)) byTrack.set(track, row); + }); + return Array.from(byTrack.values()); + }, [referenceRowsMap.requests, (_a = tables.requests) == null ? void 0 : _a.rows]); + const getInvoiceRequestTrackOptions = useCallback(() => { + const rows = getInvoiceRequestRows(); + return rows.map((row) => { + const track = String((row == null ? void 0 : row.track_number) || "").trim().toUpperCase(); + if (!track) return null; + const clientName = String((row == null ? void 0 : row.client_name) || "").trim(); + const clientPhone = String((row == null ? void 0 : row.client_phone) || "").trim(); + const parts = [track]; + if (clientName) parts.push(clientName); + if (clientPhone) parts.push(clientPhone); + return { value: track, label: parts.join(" \u2022 ") }; + }).filter(Boolean).sort((a, b) => String(a.label).localeCompare(String(b.label), "ru")); + }, [getInvoiceRequestRows]); + const getInvoicePayerOptions = useCallback(() => { + const map = /* @__PURE__ */ new Map(); + const addPayer = (nameRaw, phoneRaw) => { + const name = String(nameRaw || "").trim(); + if (!name) return; + const phone = String(phoneRaw || "").trim(); + if (map.has(name)) return; + map.set(name, phone ? `${name} (${phone})` : name); + }; + const clientRows = Array.isArray(referenceRowsMap.clients) ? referenceRowsMap.clients : []; + clientRows.forEach((row) => addPayer((row == null ? void 0 : row.full_name) || (row == null ? void 0 : row.client_name), (row == null ? void 0 : row.phone) || (row == null ? void 0 : row.client_phone))); + getInvoiceRequestRows().forEach((row) => addPayer(row == null ? void 0 : row.client_name, row == null ? void 0 : row.client_phone)); + return Array.from(map.entries()).map(([value, label]) => ({ value, label })).sort((a, b) => String(a.label).localeCompare(String(b.label), "ru")); + }, [getInvoiceRequestRows, referenceRowsMap.clients]); + const dictionaryTableItems = useMemo(() => { + return (tableCatalog || []).filter( + (item) => item && item.section === "dictionary" && Array.isArray(item.actions) && item.actions.includes("query") && !LEGACY_HIDDEN_DICTIONARY_TABLES.has(String(item.key || "")) + ).sort((a, b) => String(a.label || a.key).localeCompare(String(b.label || b.key), "ru")); + }, [tableCatalog]); + const resolveTableConfig = useCallback( + (tableKey) => { + if (TABLE_SERVER_CONFIG[tableKey]) return TABLE_SERVER_CONFIG[tableKey]; + const meta = tableCatalogMap[tableKey]; + if (!meta || !meta.table) return null; + const tableName = String(meta.table || tableKey); + return { + table: tableName, + endpoint: String(meta.query_endpoint || "/api/admin/crud/" + tableName + "/query"), + sort: Array.isArray(meta.default_sort) && meta.default_sort.length ? meta.default_sort : [{ field: "created_at", dir: "desc" }] + }; + }, + [tableCatalogMap] + ); + const resolveMutationConfig = useCallback( + (tableKey) => { + if (TABLE_MUTATION_CONFIG[tableKey]) return TABLE_MUTATION_CONFIG[tableKey]; + const meta = tableCatalogMap[tableKey]; + if (!meta || !meta.table) return null; + const tableName = String(meta.table || tableKey); + return { + create: String(meta.create_endpoint || "/api/admin/crud/" + tableName), + update: (id) => String(meta.update_endpoint_template || "/api/admin/crud/" + tableName + "/{id}").replace("{id}", String(id)), + delete: (id) => String(meta.delete_endpoint_template || "/api/admin/crud/" + tableName + "/{id}").replace("{id}", String(id)) + }; + }, + [tableCatalogMap] + ); + const getFilterFields = useCallback( + (tableKey) => { + if (tableKey === "kanban") { + return [ + { field: "assigned_lawyer_id", label: "\u042E\u0440\u0438\u0441\u0442", type: "reference", options: getLawyerOptions }, + { field: "client_name", label: "\u041A\u043B\u0438\u0435\u043D\u0442", type: "text" }, + { field: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "reference", options: getStatusOptions }, + { field: "created_at", label: "\u0414\u0430\u0442\u0430", type: "date" }, + { field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions }, + { field: "has_unread_updates", label: "\u0415\u0441\u0442\u044C \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F", type: "boolean" }, + { field: "deadline_alert", label: "\u0413\u043E\u0440\u044F\u0449\u0438\u0435 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u044B", type: "boolean" }, + { field: "overdue", label: "\u041F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D", type: "boolean" } + ]; + } + if (tableKey === "requests") { + return [ + { field: "track_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0437\u0430\u044F\u0432\u043A\u0438", type: "text" }, + { field: "client_name", label: "\u041A\u043B\u0438\u0435\u043D\u0442", type: "text" }, + { field: "client_phone", label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D", type: "text" }, + { field: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "reference", options: getStatusOptions }, + { field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions }, + { field: "important_date_at", label: "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430", type: "date" }, + { field: "has_unread_updates", label: "\u0415\u0441\u0442\u044C \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F", type: "boolean" }, + { field: "deadline_alert", label: "\u0413\u043E\u0440\u044F\u0449\u0438\u0435 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u044B", type: "boolean" }, + { field: "client_has_unread_updates", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C", type: "boolean" }, + { field: "lawyer_has_unread_updates", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u044E\u0440\u0438\u0441\u0442\u043E\u043C", type: "boolean" }, + { field: "invoice_amount", label: "\u0421\u0443\u043C\u043C\u0430 \u0441\u0447\u0435\u0442\u0430", type: "number" }, + { field: "effective_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430", type: "number" }, + { field: "paid_at", label: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D\u043E", type: "date" }, + { field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" } + ]; + } + if (tableKey === "serviceRequests") { + return [ + { field: "type", label: "\u0422\u0438\u043F", type: "text" }, + { field: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "text" }, + { field: "request_id", label: "ID \u0437\u0430\u044F\u0432\u043A\u0438", type: "text" }, + { field: "client_id", label: "ID \u043A\u043B\u0438\u0435\u043D\u0442\u0430", type: "text" }, + { field: "assigned_lawyer_id", label: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0439 \u044E\u0440\u0438\u0441\u0442", type: "reference", options: getLawyerOptions }, + { field: "admin_unread", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u043E\u043C", type: "boolean" }, + { field: "lawyer_unread", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u044E\u0440\u0438\u0441\u0442\u043E\u043C", type: "boolean" }, + { field: "resolved_at", label: "\u0414\u0430\u0442\u0430 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0438", type: "date" }, + { field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" } + ]; + } + if (tableKey === "invoices") { + return [ + { field: "invoice_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0441\u0447\u0435\u0442\u0430", type: "text" }, + { field: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "enum", options: getInvoiceStatusOptions }, + { field: "amount", label: "\u0421\u0443\u043C\u043C\u0430", type: "number" }, + { field: "currency", label: "\u0412\u0430\u043B\u044E\u0442\u0430", type: "text" }, + { field: "payer_display_name", label: "\u041F\u043B\u0430\u0442\u0435\u043B\u044C\u0449\u0438\u043A", type: "text" }, + { field: "request_id", label: "ID \u0437\u0430\u044F\u0432\u043A\u0438", type: "text" }, + { field: "issued_by_admin_user_id", label: "ID \u0441\u043E\u0442\u0440\u0443\u0434\u043D\u0438\u043A\u0430", type: "text" }, + { field: "issued_at", label: "\u0414\u0430\u0442\u0430 \u0444\u043E\u0440\u043C\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F", type: "date" }, + { field: "paid_at", label: "\u0414\u0430\u0442\u0430 \u043E\u043F\u043B\u0430\u0442\u044B", type: "date" }, + { field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" } + ]; + } + if (tableKey === "quotes") { + return [ + { field: "author", label: "\u0410\u0432\u0442\u043E\u0440", type: "text" }, + { field: "text", label: "\u0422\u0435\u043A\u0441\u0442", type: "text" }, + { field: "source", label: "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A", type: "text" }, + { field: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", type: "boolean" }, + { field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" }, + { field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" } + ]; + } + if (tableKey === "topics") { + return [ + { field: "code", label: "\u041A\u043E\u0434", type: "text" }, + { field: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", type: "text" }, + { field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", type: "boolean" }, + { field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" } + ]; + } + if (tableKey === "statuses") { + return [ + { field: "code", label: "\u041A\u043E\u0434", type: "text" }, + { field: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", type: "text" }, + { field: "status_group_id", label: "\u0413\u0440\u0443\u043F\u043F\u0430", type: "reference", options: getStatusGroupOptions }, + { field: "kind", label: "\u0422\u0438\u043F", type: "enum", options: getStatusKindOptions }, + { field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean" }, + { field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" }, + { field: "is_terminal", label: "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439", type: "boolean" } + ]; + } + if (tableKey === "formFields") { + return [ + { field: "key", label: "\u041A\u043B\u044E\u0447", type: "text" }, + { field: "label", label: "\u041C\u0435\u0442\u043A\u0430", type: "text" }, + { field: "type", label: "\u0422\u0438\u043F", type: "enum", options: getFormFieldTypeOptions }, + { field: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean" }, + { field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean" }, + { field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" } + ]; + } + if (tableKey === "topicRequiredFields") { + return [ + { field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions }, + { field: "field_key", label: "\u041F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", type: "reference", options: getFormFieldKeyOptions }, + { field: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean" }, + { field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean" }, + { field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" } + ]; + } + if (tableKey === "topicDataTemplates") { + return [ + { field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions }, + { field: "key", label: "\u041A\u043B\u044E\u0447", type: "text" }, + { field: "label", label: "\u041C\u0435\u0442\u043A\u0430", type: "text" }, + { field: "value_type", label: "\u0422\u0438\u043F \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F", type: "enum", options: getRequestDataValueTypeOptions }, + { field: "document_name", label: "\u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442", type: "text" }, + { field: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean" }, + { field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean" }, + { field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" }, + { field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" } + ]; + } + if (tableKey === "statusTransitions") { + return [ + { field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions }, + { field: "from_status", label: "\u0418\u0437 \u0441\u0442\u0430\u0442\u0443\u0441\u0430", type: "reference", options: getStatusOptions }, + { field: "to_status", label: "\u0412 \u0441\u0442\u0430\u0442\u0443\u0441", type: "reference", options: getStatusOptions }, + { field: "sla_hours", label: "SLA (\u0447\u0430\u0441\u044B)", type: "number" }, + { field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean" }, + { field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" } + ]; + } + if (tableKey === "users") { + return [ + { field: "name", label: "\u0418\u043C\u044F", type: "text" }, + { field: "email", label: "Email", type: "text" }, + { field: "phone", label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D", type: "text" }, + { field: "role", label: "\u0420\u043E\u043B\u044C", type: "enum", options: getRoleOptions }, + { field: "primary_topic_code", label: "\u041F\u0440\u043E\u0444\u0438\u043B\u044C (\u0442\u0435\u043C\u0430)", type: "reference", options: getTopicOptions }, + { field: "default_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E", type: "number" }, + { field: "salary_percent", label: "\u041F\u0440\u043E\u0446\u0435\u043D\u0442 \u0437\u0430\u0440\u043F\u043B\u0430\u0442\u044B", type: "number" }, + { field: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean" }, + { field: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439", type: "text" }, + { field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" } + ]; + } + if (tableKey === "userTopics") { + return [ + { field: "admin_user_id", label: "\u042E\u0440\u0438\u0441\u0442", type: "reference", options: getLawyerOptions }, + { field: "topic_code", label: "\u0414\u043E\u043F. \u0442\u0435\u043C\u0430", type: "reference", options: getTopicOptions }, + { field: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439", type: "text" }, + { field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" } + ]; + } + const meta = tableCatalogMap[tableKey]; + if (!meta || !Array.isArray(meta.columns)) return []; + return (meta.columns || []).filter((column) => column && column.name && column.filterable !== false).map((column) => { + const name = String(column.name); + const label = String(column.label || humanizeKey(name)); + if (name === "topic_code") return { field: name, label, type: "reference", options: getTopicOptions }; + if (name === "status_code" || name === "from_status" || name === "to_status") { + return { field: name, label, type: "reference", options: getStatusOptions }; + } + if (name === "field_key") return { field: name, label, type: "reference", options: getFormFieldKeyOptions }; + const reference = normalizeReferenceMeta(column.reference); + if (reference) { + return { field: name, label, type: "reference", options: () => getReferenceOptions(reference) }; + } + return { field: name, label, type: metaKindToFilterType(column.kind) }; + }); + }, + [ + getReferenceOptions, + tableCatalogMap, + getFormFieldKeyOptions, + getFormFieldTypeOptions, + getInvoiceStatusOptions, + getLawyerOptions, + getRoleOptions, + role, + getStatusGroupOptions, + getStatusKindOptions, + getStatusOptions, + getTopicOptions + ] + ); + const getTableLabel = useCallback((tableKey) => { + if (tableKey === "kanban") return "\u041A\u0430\u043D\u0431\u0430\u043D"; + if (tableKey === "requests") return "\u0417\u0430\u044F\u0432\u043A\u0438"; + if (tableKey === "serviceRequests") return "\u0417\u0430\u043F\u0440\u043E\u0441\u044B"; + if (tableKey === "invoices") return "\u0421\u0447\u0435\u0442\u0430"; + if (tableKey === "quotes") return "\u0426\u0438\u0442\u0430\u0442\u044B"; + if (tableKey === "topics") return "\u0422\u0435\u043C\u044B"; + if (tableKey === "statuses") return "\u0421\u0442\u0430\u0442\u0443\u0441\u044B"; + if (tableKey === "statusGroups") return "\u0413\u0440\u0443\u043F\u043F\u044B \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432"; + if (tableKey === "formFields") return "\u041F\u043E\u043B\u044F \u0444\u043E\u0440\u043C\u044B"; + if (tableKey === "topicRequiredFields") return "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u043E\u043B\u044F \u043F\u043E \u0442\u0435\u043C\u0430\u043C"; + if (tableKey === "topicDataTemplates") return "\u0428\u0430\u0431\u043B\u043E\u043D\u044B \u0434\u043E\u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u043F\u043E \u0442\u0435\u043C\u0430\u043C"; + if (tableKey === "statusTransitions") return "\u041F\u0435\u0440\u0435\u0445\u043E\u0434\u044B \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432"; + if (tableKey === "users") return "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438"; + if (tableKey === "userTopics") return "\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0442\u0435\u043C\u044B \u044E\u0440\u0438\u0441\u0442\u043E\u0432"; + const meta = tableCatalogMap[tableKey]; + if (meta && meta.label) return String(meta.label); + const raw = TABLE_UNALIASES[tableKey] || tableKey; + return humanizeKey(raw); + }, [tableCatalogMap]); + const statusDesignerRows = useMemo(() => { + const activeTopic = String(statusDesignerTopicCode || "").trim(); + const rows = tables.statusTransitions.rows || []; + if (!activeTopic) return rows; + return rows.filter((row) => String(row.topic_code || "") === activeTopic); + }, [statusDesignerTopicCode, tables.statusTransitions.rows]); + const statusDesignerCards = useMemo(() => { + const rows = statusDesignerRows || []; + if (!rows.length) return []; + const orderMap = /* @__PURE__ */ new Map(); + (tables.statuses.rows || []).forEach((row, index) => { + const code = String((row == null ? void 0 : row.code) || "").trim(); + if (!code) return; + const sortOrder = Number(row == null ? void 0 : row.sort_order); + orderMap.set(code, Number.isFinite(sortOrder) ? sortOrder : index); + }); + const statusMetaMap = /* @__PURE__ */ new Map(); + (dictionaries.statuses || []).forEach((row, index) => { + var _a2; + const code = String((row == null ? void 0 : row.code) || "").trim(); + if (!code) return; + statusMetaMap.set(code, { + name: String((row == null ? void 0 : row.name) || code), + isTerminal: false, + order: (_a2 = orderMap.get(code)) != null ? _a2 : index + }); + }); + (tables.statuses.rows || []).forEach((row, index) => { + var _a2; + const code = String((row == null ? void 0 : row.code) || "").trim(); + if (!code) return; + statusMetaMap.set(code, { + name: String((row == null ? void 0 : row.name) || code), + isTerminal: Boolean(row == null ? void 0 : row.is_terminal), + order: (_a2 = orderMap.get(code)) != null ? _a2 : index + }); + }); + const codeSet = /* @__PURE__ */ new Set(); + rows.forEach((row) => { + const fromCode = String((row == null ? void 0 : row.from_status) || "").trim(); + const toCode = String((row == null ? void 0 : row.to_status) || "").trim(); + if (fromCode) codeSet.add(fromCode); + if (toCode) codeSet.add(toCode); + }); + const codes = Array.from(codeSet.values()).sort((a, b) => { + var _a2, _b; + const aOrder = (_a2 = statusMetaMap.get(a)) == null ? void 0 : _a2.order; + const bOrder = (_b = statusMetaMap.get(b)) == null ? void 0 : _b.order; + if (aOrder != null && bOrder != null && aOrder !== bOrder) return aOrder - bOrder; + if (aOrder != null && bOrder == null) return -1; + if (aOrder == null && bOrder != null) return 1; + return String(a).localeCompare(String(b), "ru"); + }); + return codes.map((code) => { + const outgoing = rows.filter((row) => String((row == null ? void 0 : row.from_status) || "").trim() === code).sort((a, b) => { + const aOrder = Number((a == null ? void 0 : a.sort_order) || 0); + const bOrder = Number((b == null ? void 0 : b.sort_order) || 0); + if (aOrder !== bOrder) return aOrder - bOrder; + return String((a == null ? void 0 : a.to_status) || "").localeCompare(String((b == null ? void 0 : b.to_status) || ""), "ru"); + }); + const meta = statusMetaMap.get(code) || { name: statusLabel(code), isTerminal: false }; + return { + code, + name: String(meta.name || statusLabel(code)), + isTerminal: Boolean(meta.isTerminal), + outgoing + }; + }); + }, [dictionaries.statuses, statusDesignerRows, tables.statuses.rows]); + const getRecordFields = useCallback( + (tableKey) => { + if (tableKey === "requests") { + const isNewClientMode = (form) => { + const value = String((form == null ? void 0 : form.client_id) || "").trim(); + return !value || value === NEW_REQUEST_CLIENT_OPTION; + }; + const fields = [ + { key: "track_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0437\u0430\u044F\u0432\u043A\u0438", type: "text", optional: true, placeholder: "\u041E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0441\u0442\u044B\u043C \u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438" }, + ...role !== "LAWYER" ? [ + { + key: "client_id", + label: "\u041A\u043B\u0438\u0435\u043D\u0442", + type: "reference", + defaultValue: NEW_REQUEST_CLIENT_OPTION, + options: getClientOptions, + extraOptions: [{ value: NEW_REQUEST_CLIENT_OPTION, label: "\u041D\u043E\u0432\u044B\u0439 \u043A\u043B\u0438\u0435\u043D\u0442" }], + fullRow: true + } + ] : [], + { + key: "client_name", + label: role !== "LAWYER" ? "\u0424\u0418\u041E \u043D\u043E\u0432\u043E\u0433\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u0430" : "\u041A\u043B\u0438\u0435\u043D\u0442", + type: "text", + required: true, + visibleWhen: role === "LAWYER" ? void 0 : isNewClientMode + }, + { + key: "client_phone", + label: role !== "LAWYER" ? "\u0422\u0435\u043B\u0435\u0444\u043E\u043D \u043D\u043E\u0432\u043E\u0433\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u0430" : "\u0422\u0435\u043B\u0435\u0444\u043E\u043D", + type: "text", + required: true, + visibleWhen: role === "LAWYER" ? void 0 : isNewClientMode + }, + { key: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", optional: true, options: getTopicOptions }, + { key: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "reference", required: true, options: getStatusOptions }, + { key: "description", label: "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435", type: "textarea", optional: true }, + { key: "request_cost", label: "\u0421\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0438", type: "number", optional: true } + ]; + if (role !== "LAWYER") { + fields.push({ key: "assigned_lawyer_id", label: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0439 \u044E\u0440\u0438\u0441\u0442", type: "reference", optional: true, options: getLawyerOptions }); + fields.push({ key: "effective_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430 (\u0444\u0438\u043A\u0441.)", type: "number", optional: true }); + } + return fields; + } + if (tableKey === "invoices") { + return [ + { key: "request_track_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0437\u0430\u044F\u0432\u043A\u0438", type: "reference", required: true, createOnly: true, options: getInvoiceRequestTrackOptions }, + { key: "invoice_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0441\u0447\u0435\u0442\u0430", type: "text", optional: true, placeholder: "\u041E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0441\u0442\u044B\u043C \u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438" }, + { key: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "enum", required: true, options: getInvoiceStatusOptions, defaultValue: "WAITING_PAYMENT" }, + { key: "amount", label: "\u0421\u0443\u043C\u043C\u0430", type: "number", required: true }, + { key: "currency", label: "\u0412\u0430\u043B\u044E\u0442\u0430", type: "text", optional: true, defaultValue: "RUB" }, + { key: "payer_display_name", label: "\u041F\u043B\u0430\u0442\u0435\u043B\u044C\u0449\u0438\u043A (\u0424\u0418\u041E / \u043A\u043E\u043C\u043F\u0430\u043D\u0438\u044F)", type: "reference", required: true, options: getInvoicePayerOptions }, + { key: "payer_details", label: "\u0420\u0435\u043A\u0432\u0438\u0437\u0438\u0442\u044B (JSON, \u0448\u0438\u0444\u0440\u0443\u0435\u0442\u0441\u044F)", type: "json", optional: true, omitIfEmpty: true, placeholder: '{"inn":"..."}' } + ]; + } + if (tableKey === "quotes") { + return [ + { key: "author", label: "\u0410\u0432\u0442\u043E\u0440", type: "text", required: true }, + { key: "text", label: "\u0422\u0435\u043A\u0441\u0442", type: "textarea", required: true }, + { key: "source", label: "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A", type: "text", optional: true }, + { key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", type: "boolean", defaultValue: "true" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" } + ]; + } + if (tableKey === "topics") { + return [ + { key: "code", label: "\u041A\u043E\u0434", type: "text", required: true, autoCreate: true }, + { key: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", type: "text", required: true }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", type: "boolean", defaultValue: "true" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" } + ]; + } + if (tableKey === "statuses") { + return [ + { key: "code", label: "\u041A\u043E\u0434", type: "text", required: true }, + { key: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", type: "text", required: true }, + { key: "status_group_id", label: "\u0413\u0440\u0443\u043F\u043F\u0430", type: "reference", optional: true, options: getStatusGroupOptions }, + { key: "kind", label: "\u0422\u0438\u043F", type: "enum", required: true, options: getStatusKindOptions, defaultValue: "DEFAULT" }, + { key: "invoice_template", label: "\u0428\u0430\u0431\u043B\u043E\u043D \u0441\u0447\u0435\u0442\u0430", type: "textarea", optional: true, placeholder: "\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u043F\u043E\u043B\u044F: {track_number}, {client_name}, {topic_code}, {amount}" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean", defaultValue: "true" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" }, + { key: "is_terminal", label: "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439", type: "boolean", defaultValue: "false" } + ]; + } + if (tableKey === "formFields") { + return [ + { key: "key", label: "\u041A\u043B\u044E\u0447", type: "text", required: true }, + { key: "label", label: "\u041C\u0435\u0442\u043A\u0430", type: "text", required: true }, + { key: "type", label: "\u0422\u0438\u043F", type: "enum", required: true, options: getFormFieldTypeOptions }, + { key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean", defaultValue: "false" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean", defaultValue: "true" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" }, + { key: "options", label: "\u041E\u043F\u0446\u0438\u0438 (JSON)", type: "json", optional: true } + ]; + } + if (tableKey === "topicRequiredFields") { + return [ + { key: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", required: true, options: getTopicOptions }, + { key: "field_key", label: "\u041F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", type: "reference", required: true, options: getFormFieldKeyOptions }, + { key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean", defaultValue: "true" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean", defaultValue: "true" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" } + ]; + } + if (tableKey === "topicDataTemplates") { + return [ + { key: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", required: true, options: getTopicOptions }, + { key: "key", label: "\u041A\u043B\u044E\u0447", type: "text", required: true }, + { key: "label", label: "\u041C\u0435\u0442\u043A\u0430", type: "text", required: true }, + { key: "value_type", label: "\u0422\u0438\u043F \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F", type: "enum", required: true, options: getRequestDataValueTypeOptions, defaultValue: "string" }, + { key: "document_name", label: "\u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442", type: "text", optional: true, placeholder: "\u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: \u0414\u043E\u0433\u043E\u0432\u043E\u0440 / \u041F\u0430\u0441\u043F\u043E\u0440\u0442" }, + { key: "description", label: "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435", type: "textarea", optional: true }, + { key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean", defaultValue: "true" }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean", defaultValue: "true" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" } + ]; + } + if (tableKey === "statusTransitions") { + return [ + { key: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", required: true, options: getTopicOptions }, + { key: "from_status", label: "\u0418\u0437 \u0441\u0442\u0430\u0442\u0443\u0441\u0430", type: "reference", required: true, options: getStatusOptions }, + { key: "to_status", label: "\u0412 \u0441\u0442\u0430\u0442\u0443\u0441", type: "reference", required: true, options: getStatusOptions }, + { key: "sla_hours", label: "SLA (\u0447\u0430\u0441\u044B)", type: "number", optional: true }, + { + key: "required_data_keys", + label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u043A\u043B\u044E\u0447\u0438 \u0434\u0430\u043D\u043D\u044B\u0445 (JSON-\u043C\u0430\u0441\u0441\u0438\u0432)", + type: "json", + optional: true, + defaultValue: "[]", + placeholder: '["passport_scan", "client_address"]' + }, + { + key: "required_mime_types", + label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 MIME-\u0442\u0438\u043F\u044B \u0444\u0430\u0439\u043B\u043E\u0432 (JSON-\u043C\u0430\u0441\u0441\u0438\u0432)", + type: "json", + optional: true, + defaultValue: "[]", + placeholder: '["application/pdf", "image/*"]' + }, + { key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean", defaultValue: "true" }, + { key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" } + ]; + } + if (tableKey === "users") { + return [ + { key: "name", label: "\u0418\u043C\u044F", type: "text", required: true }, + { key: "email", label: "Email", type: "text", required: true }, + { key: "phone", label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D", type: "text", optional: true, placeholder: "+7..." }, + { key: "role", label: "\u0420\u043E\u043B\u044C", type: "enum", required: true, options: getRoleOptions, defaultValue: "LAWYER" }, + { + key: "avatar_url", + label: "URL \u0430\u0432\u0430\u0442\u0430\u0440\u0430", + type: "text", + optional: true, + placeholder: "https://... \u0438\u043B\u0438 s3://...", + uploadScope: "USER_AVATAR", + accept: "image/*" + }, + { key: "primary_topic_code", label: "\u041F\u0440\u043E\u0444\u0438\u043B\u044C (\u0442\u0435\u043C\u0430)", type: "reference", optional: true, options: getTopicOptions }, + { key: "default_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E", type: "number", optional: true }, + { key: "salary_percent", label: "\u041F\u0440\u043E\u0446\u0435\u043D\u0442 \u0437\u0430\u0440\u043F\u043B\u0430\u0442\u044B", type: "number", optional: true }, + { key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean", defaultValue: "true" }, + { key: "password", label: "\u041F\u0430\u0440\u043E\u043B\u044C", type: "password", requiredOnCreate: true, optional: true, omitIfEmpty: true, placeholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u044C" } + ]; + } + if (tableKey === "userTopics") { + return [ + { key: "admin_user_id", label: "\u042E\u0440\u0438\u0441\u0442", type: "reference", required: true, options: getLawyerOptions }, + { key: "topic_code", label: "\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u0430\u044F \u0442\u0435\u043C\u0430", type: "reference", required: true, options: getTopicOptions } + ]; + } + const meta = tableCatalogMap[tableKey]; + if (!meta || !Array.isArray(meta.columns)) return []; + return (meta.columns || []).filter((column) => column && column.name && column.editable).map((column) => { + const key = String(column.name || ""); + const requiredOnCreate = Boolean(column.required_on_create); + const reference = normalizeReferenceMeta(column.reference); + return { + key, + label: String(column.label || humanizeKey(key)), + type: reference ? "reference" : metaKindToRecordType(column.kind), + options: reference ? () => getReferenceOptions(reference) : void 0, + requiredOnCreate, + optional: !requiredOnCreate + }; + }); + }, + [ + getReferenceOptions, + tableCatalogMap, + getFormFieldKeyOptions, + getFormFieldTypeOptions, + getInvoiceStatusOptions, + getInvoicePayerOptions, + getInvoiceRequestTrackOptions, + getClientOptions, + getLawyerOptions, + getRoleOptions, + getStatusGroupOptions, + getStatusKindOptions, + getStatusOptions, + getTopicOptions + ] + ); + const getFieldDef = useCallback( + (tableKey, fieldName) => { + return getFilterFields(tableKey).find((field) => field.field === fieldName) || null; + }, + [getFilterFields] + ); + const getFieldOptions = useCallback((fieldDef) => { + if (!fieldDef) return []; + if (typeof fieldDef.options === "function") return fieldDef.options() || []; + return []; + }, []); + const getFilterValuePreview = useCallback( + (tableKey, clause) => { + var _a2, _b, _c; + const fieldDef = getFieldDef(tableKey, clause.field); + if (!fieldDef) return String((_a2 = clause.value) != null ? _a2 : ""); + if (fieldDef.type === "boolean") return boolFilterLabel(Boolean(clause.value)); + if (fieldDef.type === "reference" || fieldDef.type === "enum") { + const options = getFieldOptions(fieldDef); + const found = options.find((option) => String(option.value) === String(clause.value)); + return found ? found.label : String((_b = clause.value) != null ? _b : ""); + } + return String((_c = clause.value) != null ? _c : ""); + }, + [getFieldDef, getFieldOptions] + ); + const { + kanbanData, + kanbanLoading, + kanbanSortModal, + kanbanSortApplied, + loadKanban, + openKanbanSortModal, + closeKanbanSortModal, + updateKanbanSortMode, + submitKanbanSortModal, + resetKanbanState + } = useKanban({ + api, + setStatus, + setTableState, + tablesRef + }); + const { loadTable, loadPrevPage, loadNextPage, loadAllRows, toggleTableSort } = useTableActions({ + api, + setStatus, + resolveTableConfig, + tablesRef, + setTableState, + setDictionaries, + buildUniversalQuery + }); + const { loadAvailableTables, loadReferenceRows } = useAdminCatalogLoaders({ + api, + setStatus, + setTableState, + setReferenceRowsMap, + buildUniversalQuery + }); + const loadCurrentConfigTable = useCallback( + async (resetOffset, tokenOverride, keyOverride) => { + const currentKey = keyOverride || configActiveKey; + if (!currentKey) { + return false; + } + return loadTable(currentKey, { resetOffset: Boolean(resetOffset) }, tokenOverride); + }, + [configActiveKey, loadTable] + ); + const loadStatusDesignerTopic = useCallback( + async (topicCode) => { + const code = String(topicCode || "").trim(); + setStatusDesignerTopicCode(code); + statusDesignerLoadedTopicRef.current = code; + if (!code) { + await loadTable("statusTransitions", { resetOffset: true, filtersOverride: [] }); + return; + } + await loadTable("statusTransitions", { + resetOffset: true, + filtersOverride: [{ field: "topic_code", op: "=", value: code }] + }); + }, + [loadTable] + ); + useEffect(() => { + var _a2; + if (configActiveKey !== "statusTransitions") { + statusDesignerLoadedTopicRef.current = ""; + return; + } + const topics = dictionaries.topics || []; + if (!topics.length) { + setStatusDesignerTopicCode(""); + return; + } + const hasSelected = topics.some((item) => String((item == null ? void 0 : item.code) || "") === String(statusDesignerTopicCode || "")); + const nextTopic = String(hasSelected ? statusDesignerTopicCode : ((_a2 = topics[0]) == null ? void 0 : _a2.code) || "").trim(); + if (!nextTopic) return; + if (nextTopic !== statusDesignerTopicCode) { + setStatusDesignerTopicCode(nextTopic); + return; + } + if (statusDesignerLoadedTopicRef.current === nextTopic) return; + statusDesignerLoadedTopicRef.current = nextTopic; + loadTable("statusTransitions", { + resetOffset: true, + filtersOverride: [{ field: "topic_code", op: "=", value: nextTopic }] + }); + }, [configActiveKey, dictionaries.topics, loadTable, statusDesignerTopicCode]); + const loadDashboard = useCallback( + async (tokenOverride) => { + var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p; + setStatus("dashboard", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", ""); + try { + const data = await api("/api/admin/metrics/overview", {}, tokenOverride); + const scope = String(data.scope || role || ""); + const cards = scope === "LAWYER" ? [ + { label: "\u041C\u043E\u0438 \u0437\u0430\u044F\u0432\u043A\u0438", value: (_a2 = data.assigned_total) != null ? _a2 : 0 }, + { label: "\u041C\u043E\u0438 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0435", value: (_b = data.active_assigned_total) != null ? _b : 0 }, + { label: "\u041D\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0435", value: (_c = data.unassigned_total) != null ? _c : 0 }, + { label: "\u041C\u043E\u0438 \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435", value: (_e = (_d = data.my_unread_notifications_total) != null ? _d : data.my_unread_updates) != null ? _e : 0 }, + { label: "\u041F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u043E SLA", value: (_f = data.sla_overdue) != null ? _f : 0 } + ] : [ + { label: "\u041D\u043E\u0432\u044B\u0435", value: (_g = data.new) != null ? _g : 0 }, + { label: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0435", value: (_h = data.assigned_total) != null ? _h : 0 }, + { label: "\u041D\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0435", value: (_i = data.unassigned_total) != null ? _i : 0 }, + { label: "\u041F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u043E SLA", value: (_j = data.sla_overdue) != null ? _j : 0 }, + { label: "\u041C\u043E\u0438 \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435", value: (_l = (_k = data.my_unread_notifications_total) != null ? _k : data.my_unread_updates) != null ? _l : 0 }, + { label: "\u0412\u044B\u0440\u0443\u0447\u043A\u0430 (\u043C\u0435\u0441.)", value: Number((_m = data.month_revenue) != null ? _m : 0).toFixed(2) }, + { label: "\u0420\u0430\u0441\u0445\u043E\u0434\u044B (\u043C\u0435\u0441.)", value: Number((_n = data.month_expenses) != null ? _n : 0).toFixed(2) }, + { label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u044E\u0440\u0438\u0441\u0442\u0430\u043C\u0438", value: (_o = data.unread_for_lawyers) != null ? _o : 0 }, + { label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u0430\u043C\u0438", value: (_p = data.unread_for_clients) != null ? _p : 0 } + ]; + const localized = {}; + Object.entries(data.by_status || {}).forEach(([code, count]) => { + localized[statusLabel(code)] = count; + }); + setDashboardData({ + scope, + cards, + byStatus: localized, + lawyerLoads: data.lawyer_loads || [], + myUnreadByEvent: data.my_unread_by_event || {}, + myUnreadTotal: Number(data.my_unread_updates || 0), + myUnreadNotificationsTotal: Number(data.my_unread_notifications_total || data.my_unread_updates || 0), + unreadForClients: Number(data.unread_for_clients_notifications_total || data.unread_for_clients || 0), + unreadForLawyers: Number(data.unread_for_lawyers_notifications_total || data.unread_for_lawyers || 0), + serviceRequestUnreadTotal: Number(data.service_request_unread_total || 0), + deadlineAlertTotal: Number(data.deadline_alert_total || 0), + monthRevenue: Number(data.month_revenue || 0), + monthExpenses: Number(data.month_expenses || 0) + }); + setStatus("dashboard", "\u0414\u0430\u043D\u043D\u044B\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u044B", "ok"); + } catch (error) { + setStatus("dashboard", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + } + }, + [api, role, setStatus] + ); + const loadMeta = useCallback( + async (tokenOverride) => { + const entity = (metaEntity || "quotes").trim() || "quotes"; + setStatus("meta", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", ""); + try { + const data = await api("/api/admin/meta/" + encodeURIComponent(entity), {}, tokenOverride); + setMetaJson(JSON.stringify(localizeMeta(data), null, 2)); + setStatus("meta", "\u041C\u0435\u0442\u0430\u0434\u0430\u043D\u043D\u044B\u0435 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u044B", "ok"); + } catch (error) { + setStatus("meta", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + } + }, + [api, metaEntity, setStatus] + ); + const loadSmsProviderHealth = useCallback( + async (tokenOverride, options) => { + const opts = options || {}; + const silent = Boolean(opts.silent); + const currentRole = String(role || "").toUpperCase(); + const authToken = tokenOverride !== void 0 ? tokenOverride : token; + if (!authToken || currentRole !== "ADMIN") { + setSmsProviderHealth(null); + return null; + } + if (!silent) setStatus("smsProviderHealth", "\u041E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u043C \u0431\u0430\u043B\u0430\u043D\u0441 SMS Aero...", ""); + try { + const payload = await api("/api/admin/system/sms-provider-health", {}, tokenOverride); + const enriched = { ...payload || {}, loaded_at: (/* @__PURE__ */ new Date()).toISOString() }; + setSmsProviderHealth(enriched); + if (!silent) setStatus("smsProviderHealth", "\u0411\u0430\u043B\u0430\u043D\u0441 SMS Aero \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok"); + return enriched; + } catch (error) { + const fallback = { + provider: "smsaero", + status: "error", + mode: "real", + can_send: false, + balance_available: false, + balance_amount: null, + balance_currency: "RUB", + issues: [error.message], + loaded_at: (/* @__PURE__ */ new Date()).toISOString() + }; + setSmsProviderHealth(fallback); + if (!silent) setStatus("smsProviderHealth", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + return null; + } + }, + [api, role, setStatus, token] + ); + const refreshSection = useCallback( + async (section, tokenOverride) => { + if (!(tokenOverride !== void 0 ? tokenOverride : token)) return; + if (section === "dashboard") return loadDashboard(tokenOverride); + if (section === "kanban") return loadKanban(tokenOverride); + if (section === "requests" && canAccessSection(role, "requests")) return loadTable("requests", {}, tokenOverride); + if (section === "serviceRequests" && canAccessSection(role, "serviceRequests")) return loadTable("serviceRequests", {}, tokenOverride); + if (section === "invoices" && canAccessSection(role, "invoices")) return loadTable("invoices", {}, tokenOverride); + if (section === "quotes" && canAccessSection(role, "quotes")) return loadTable("quotes", {}, tokenOverride); + if (section === "config" && canAccessSection(role, "config")) return loadCurrentConfigTable(false, tokenOverride); + if (section === "availableTables" && canAccessSection(role, "availableTables")) return loadAvailableTables(tokenOverride); + if (section === "meta") return loadMeta(tokenOverride); + }, + [loadAvailableTables, loadCurrentConfigTable, loadDashboard, loadKanban, loadMeta, loadTable, role, token] + ); + const bootstrapReferenceData = useCallback( + async (tokenOverride, roleOverride) => { + setDictionaries((prev) => ({ + ...prev, + statuses: Object.entries(STATUS_LABELS).map(([code, name]) => ({ code, name })) + })); + if (roleOverride !== "ADMIN") return; + try { + const body = buildUniversalQuery([], [{ field: "sort_order", dir: "asc" }], 500, 0); + const usersBody = buildUniversalQuery([], [{ field: "created_at", dir: "desc" }], 500, 0); + const [catalogData, topicsData, statusesData, fieldsData, usersData] = await Promise.all([ + api("/api/admin/crud/meta/tables", {}, tokenOverride), + api("/api/admin/crud/topics/query", { method: "POST", body }, tokenOverride), + api("/api/admin/crud/statuses/query", { method: "POST", body }, tokenOverride), + api("/api/admin/crud/form_fields/query", { method: "POST", body }, tokenOverride), + api("/api/admin/crud/admin_users/query", { method: "POST", body: usersBody }, tokenOverride) + ]); + const catalogRows = (catalogData.tables || []).filter((row) => row && row.table).map((row) => { + const tableName = String(row.table || ""); + const key = TABLE_KEY_ALIASES[tableName] || String(row.key || tableName); + return { ...row, key, table: tableName }; + }); + setTableCatalog(catalogRows); + await loadReferenceRows(catalogRows, tokenOverride); + const statusesMap = new Map(Object.entries(STATUS_LABELS).map(([code, name]) => [code, { code, name }])); + (statusesData.rows || []).forEach((row) => { + if (!row.code) return; + statusesMap.set(row.code, { code: row.code, name: row.name || statusLabel(row.code) }); + }); + const typeSet = new Set(DEFAULT_FORM_FIELD_TYPES); + (fieldsData.rows || []).forEach((row) => { + if (row == null ? void 0 : row.type) typeSet.add(row.type); + }); + const fieldKeys = (fieldsData.rows || []).filter((row) => row && row.key).map((row) => ({ key: row.key, label: row.label || row.key })).sort((a, b) => String(a.label || a.key).localeCompare(String(b.label || b.key), "ru")); + setDictionaries((prev) => ({ + ...prev, + topics: sortByName((topicsData.rows || []).map((row) => ({ code: row.code, name: row.name || row.code }))), + statuses: sortByName(Array.from(statusesMap.values())), + formFieldTypes: Array.from(typeSet.values()).sort((a, b) => String(a).localeCompare(String(b), "ru")), + formFieldKeys: fieldKeys, + users: (usersData.rows || []).map((row) => ({ + id: row.id, + name: row.name || "", + email: row.email || "", + phone: row.phone || "", + role: row.role || "", + is_active: Boolean(row.is_active) + })) + })); + } catch (_) { + } + }, + [api, loadReferenceRows] + ); + const updateAvailableTableState = useCallback( + async (tableName, isActive) => { + const name = String(tableName || "").trim(); + if (!name) return; + try { + setStatus("availableTables", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435...", ""); + await api("/api/admin/crud/meta/available-tables/" + encodeURIComponent(name), { + method: "PATCH", + body: { is_active: Boolean(isActive) } + }); + await Promise.all([loadAvailableTables(), bootstrapReferenceData(token, role)]); + setStatus("availableTables", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E", "ok"); + } catch (error) { + setStatus("availableTables", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + } + }, + [api, bootstrapReferenceData, loadAvailableTables, role, setStatus, token] + ); + const openCreateRecordModal = useCallback( + (tableKey) => { + const fields = getRecordFields(tableKey); + const initial = {}; + fields.forEach((field) => { + if (field.defaultValue !== void 0) initial[field.key] = String(field.defaultValue); + else if (field.type === "boolean") initial[field.key] = "false"; + else if (field.type === "json") initial[field.key] = field.optional ? "" : "{}"; + else if ((field.type === "reference" || field.type === "enum") && !field.optional) { + const options = typeof field.options === "function" ? field.options() : []; + initial[field.key] = options.length ? String(options[0].value) : ""; + } else initial[field.key] = ""; + }); + if (tableKey === "requests" && !initial.status_code) initial.status_code = "NEW"; + setRecordModal({ open: true, tableKey, mode: "create", rowId: null, form: initial }); + setStatus("recordForm", "", ""); + }, + [getRecordFields, setStatus] + ); + const openCreateStatusTransitionForTopic = useCallback(() => { + const topicCode = String(statusDesignerTopicCode || "").trim(); + if (!topicCode) { + setStatus("statusTransitions", "\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0435\u043C\u0443 \u0434\u043B\u044F \u043A\u043E\u043D\u0441\u0442\u0440\u0443\u043A\u0442\u043E\u0440\u0430", "error"); + return; + } + setRecordModal({ + open: true, + tableKey: "statusTransitions", + mode: "create", + rowId: null, + form: { + topic_code: topicCode, + from_status: "", + to_status: "", + sla_hours: "", + required_data_keys: "[]", + required_mime_types: "[]", + enabled: "true", + sort_order: String(Math.max(1, (statusDesignerRows || []).length + 1)) + } + }); + setStatus("recordForm", "", ""); + }, [setStatus, statusDesignerRows, statusDesignerTopicCode]); + const openEditRecordModal = useCallback( + (tableKey, row) => { + const fields = getRecordFields(tableKey); + const nextForm = {}; + fields.forEach((field) => { + const value = row[field.key]; + if (field.type === "boolean") nextForm[field.key] = value ? "true" : "false"; + else if (field.type === "json") nextForm[field.key] = value == null ? "" : JSON.stringify(value, null, 2); + else nextForm[field.key] = value == null ? "" : String(value); + }); + if (tableKey === "requests" && role !== "LAWYER" && !String(nextForm.client_id || "").trim()) { + nextForm.client_id = NEW_REQUEST_CLIENT_OPTION; + } + setRecordModal({ open: true, tableKey, mode: "edit", rowId: row.id, form: nextForm }); + setStatus("recordForm", "", ""); + }, + [getRecordFields, setStatus] + ); + const closeRecordModal = useCallback(() => { + setRecordModal({ open: false, tableKey: null, mode: "create", rowId: null, form: {} }); + setStatus("recordForm", "", ""); + }, [setStatus]); + const updateRecordField = useCallback( + (field, value) => { + setRecordModal((prev) => { + const nextForm = { ...prev.form || {}, [field]: value }; + if (prev.tableKey === "requests") { + if (field === "client_id") { + const selectedId = String(value || "").trim(); + if (!selectedId || selectedId === NEW_REQUEST_CLIENT_OPTION) { + nextForm.client_id = NEW_REQUEST_CLIENT_OPTION; + nextForm.client_name = ""; + nextForm.client_phone = ""; + } else if (selectedId) { + const rows = Array.isArray(referenceRowsMap.clients) ? referenceRowsMap.clients : []; + const found = rows.find((row) => String((row == null ? void 0 : row.id) || "") === selectedId); + if (found) { + nextForm.client_name = String(found.full_name || nextForm.client_name || ""); + nextForm.client_phone = String(found.phone || nextForm.client_phone || ""); + } + } + } + if ((field === "client_name" || field === "client_phone") && String(nextForm.client_id || "").trim() && String(nextForm.client_id || "").trim() !== NEW_REQUEST_CLIENT_OPTION) { + const selectedId = String(nextForm.client_id || "").trim(); + const rows = Array.isArray(referenceRowsMap.clients) ? referenceRowsMap.clients : []; + const found = rows.find((row) => String((row == null ? void 0 : row.id) || "") === selectedId); + if (found) { + const selectedName = String(found.full_name || ""); + const selectedPhone = String(found.phone || ""); + const currentName = String(field === "client_name" ? value : nextForm.client_name || ""); + const currentPhone = String(field === "client_phone" ? value : nextForm.client_phone || ""); + if (currentName !== selectedName || currentPhone !== selectedPhone) { + nextForm.client_id = ""; + } + } + } + } + if (prev.tableKey === "invoices" && field === "request_track_number") { + const selectedTrack = String(value || "").trim().toUpperCase(); + if (selectedTrack) { + const rows = getInvoiceRequestRows(); + const found = rows.find((row) => String((row == null ? void 0 : row.track_number) || "").trim().toUpperCase() === selectedTrack); + if (found) { + nextForm.request_track_number = String(found.track_number || selectedTrack).trim().toUpperCase(); + const autoPayer = String(found.client_name || "").trim(); + if (autoPayer) nextForm.payer_display_name = autoPayer; + } + } + } + return { ...prev, form: nextForm }; + }); + }, + [getInvoiceRequestRows, referenceRowsMap.clients] + ); + const uploadRecordFieldFile = useCallback( + async (field, file) => { + if (!recordModal.tableKey || !field || !file) return; + if (field.uploadScope !== "USER_AVATAR") return; + if (recordModal.tableKey !== "users") return; + if (recordModal.mode !== "edit" || !recordModal.rowId) { + setStatus("recordForm", "\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F, \u0437\u0430\u0442\u0435\u043C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u0430\u0432\u0430\u0442\u0430\u0440", "error"); + return; + } + try { + setStatus("recordForm", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0444\u0430\u0439\u043B\u0430...", ""); + const mimeType = String(file.type || "application/octet-stream"); + const initPayload = { + file_name: file.name, + mime_type: mimeType, + size_bytes: file.size, + scope: "USER_AVATAR", + user_id: recordModal.rowId + }; + const init = await api("/api/admin/uploads/init", { method: "POST", body: initPayload }); + const putResp = await fetch(init.presigned_url, { + method: "PUT", + headers: { "Content-Type": mimeType }, + body: file + }); + if (!putResp.ok) { + throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435"); + } + const done = await api("/api/admin/uploads/complete", { + method: "POST", + body: { + key: init.key, + file_name: file.name, + mime_type: mimeType, + size_bytes: file.size, + scope: "USER_AVATAR", + user_id: recordModal.rowId + } + }); + updateRecordField("avatar_url", String(done.avatar_url || "")); + setStatus("recordForm", "\u0410\u0432\u0430\u0442\u0430\u0440 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D", "ok"); + } catch (error) { + setStatus("recordForm", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438: " + error.message, "error"); + } + }, + [api, recordModal, setStatus, updateRecordField] + ); + const buildRecordPayload = useCallback( + (tableKey, form, mode) => { + const fields = getRecordFields(tableKey); + const payload = {}; + const isLawyerRequestEdit = tableKey === "requests" && role === "LAWYER"; + const lawyerRequestRestricted = /* @__PURE__ */ new Set(["assigned_lawyer_id", "effective_rate", "invoice_amount", "paid_at", "paid_by_admin_id"]); + fields.forEach((field) => { + if (isLawyerRequestEdit && lawyerRequestRestricted.has(field.key)) return; + const raw = form[field.key]; + if (field.type === "boolean") { + payload[field.key] = raw === "true"; + return; + } + if (field.type === "number") { + if (raw === "" || raw == null) { + if (!field.optional) payload[field.key] = 0; + return; + } + const number = Number(raw); + if (Number.isNaN(number)) throw new Error('\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u0432 \u043F\u043E\u043B\u0435 "' + field.label + '"'); + payload[field.key] = number; + return; + } + if (field.type === "json") { + const text = String(raw || "").trim(); + if (!text) { + if (field.omitIfEmpty) return; + if (field.optional) payload[field.key] = null; + else payload[field.key] = {}; + return; + } + try { + payload[field.key] = JSON.parse(text); + } catch (_) { + throw new Error('\u041F\u043E\u043B\u0435 "' + field.label + '" \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0432\u0430\u043B\u0438\u0434\u043D\u044B\u043C JSON'); + } + return; + } + const value = String(raw || "").trim(); + if (tableKey === "requests" && field.key === "client_id" && value === NEW_REQUEST_CLIENT_OPTION) { + payload[field.key] = null; + return; + } + if (!value) { + if (mode === "create" && field.autoCreate) return; + if (mode === "create" && field.requiredOnCreate) throw new Error('\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u0435 "' + field.label + '"'); + if (field.required) throw new Error('\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u0435 "' + field.label + '"'); + if (field.omitIfEmpty) return; + if (tableKey === "requests" && field.key === "track_number") return; + if (field.optional) payload[field.key] = null; + return; + } + payload[field.key] = value; + }); + if (tableKey === "requests" && mode === "create" && !payload.extra_fields) payload.extra_fields = {}; + if (tableKey === "invoices" && mode === "edit") delete payload.request_track_number; + return payload; + }, + [getRecordFields, role] + ); + const submitRecordModal = useCallback( + async (event) => { + event.preventDefault(); + const tableKey = recordModal.tableKey; + if (!tableKey) return; + const endpoints = resolveMutationConfig(tableKey); + if (!endpoints) return; + try { + setStatus("recordForm", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435...", ""); + const payload = buildRecordPayload(tableKey, recordModal.form || {}, recordModal.mode); + if (recordModal.mode === "edit" && recordModal.rowId) { + await api(endpoints.update(recordModal.rowId), { method: "PATCH", body: payload }); + } else { + await api(endpoints.create, { method: "POST", body: payload }); + } + setStatus("recordForm", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E", "ok"); + await loadTable(tableKey, { resetOffset: true }); + await loadReferenceRows(tableCatalog, void 0); + setTimeout(() => closeRecordModal(), 250); + } catch (error) { + setStatus("recordForm", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + } + }, + [api, buildRecordPayload, closeRecordModal, loadReferenceRows, loadTable, recordModal, resolveMutationConfig, setStatus, tableCatalog] + ); + const deleteRecord = useCallback( + async (tableKey, id) => { + const endpoints = resolveMutationConfig(tableKey); + if (!endpoints) return; + if (!confirm("\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043F\u0438\u0441\u044C?")) return; + try { + await api(endpoints.delete(id), { method: "DELETE" }); + setStatus(tableKey, "\u0417\u0430\u043F\u0438\u0441\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u0430", "ok"); + await loadTable(tableKey, { resetOffset: true }); + await loadReferenceRows(tableCatalog, void 0); + } catch (error) { + setStatus(tableKey, "\u041E\u0448\u0438\u0431\u043A\u0430 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u044F: " + error.message, "error"); + } + }, + [api, loadReferenceRows, loadTable, resolveMutationConfig, setStatus, tableCatalog] + ); + const claimRequest = useCallback( + async (requestId) => { + if (!requestId) return; + try { + setStatus("requests", "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043A\u0438...", ""); + setStatus("kanban", "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043A\u0438...", ""); + await api("/api/admin/requests/" + requestId + "/claim", { method: "POST" }); + setStatus("requests", "\u0417\u0430\u044F\u0432\u043A\u0430 \u0432\u0437\u044F\u0442\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0443", "ok"); + setStatus("kanban", "\u0417\u0430\u044F\u0432\u043A\u0430 \u0432\u0437\u044F\u0442\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0443", "ok"); + const refreshRequests = canAccessSection(role, "requests") ? loadTable("requests", { resetOffset: true }) : Promise.resolve(); + await Promise.all([refreshRequests, loadKanban()]); + } catch (error) { + setStatus("requests", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F: " + error.message, "error"); + setStatus("kanban", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F: " + error.message, "error"); + } + }, + [api, loadKanban, loadTable, role, setStatus] + ); + const openInvoiceRequest = useCallback( + (row, event) => { + if (!row || !row.request_id) return; + openRequestDetails(row.request_id, event); + }, + [openRequestDetails] + ); + const moveRequestFromKanban = useCallback( + async (row, targetGroup, explicitStatus) => { + var _a2; + const requestId = String((row == null ? void 0 : row.id) || "").trim(); + if (!requestId) return; + const currentGroup = String((row == null ? void 0 : row.status_group) || fallbackStatusGroup(row == null ? void 0 : row.status_code)); + const groupKey = String(targetGroup || "").trim(); + const targetStatusFromSelect = String(explicitStatus || "").trim(); + const assignedLawyerId = String((row == null ? void 0 : row.assigned_lawyer_id) || "").trim(); + if (role === "LAWYER" && !assignedLawyerId) { + setStatus("kanban", "\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u043E\u0437\u044C\u043C\u0438\u0442\u0435 \u0437\u0430\u044F\u0432\u043A\u0443 \u0432 \u0440\u0430\u0431\u043E\u0442\u0443", "error"); + return; + } + if (role === "LAWYER" && assignedLawyerId && String(assignedLawyerId) !== String(userId || "")) { + setStatus("kanban", "\u042E\u0440\u0438\u0441\u0442 \u043C\u043E\u0436\u0435\u0442 \u043C\u0435\u043D\u044F\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441 \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u0432\u043E\u0438\u0445 \u0437\u0430\u044F\u0432\u043E\u043A", "error"); + return; + } + let targetStatus = targetStatusFromSelect; + const transitions = Array.isArray(row == null ? void 0 : row.available_transitions) ? row.available_transitions : []; + if (!targetStatus) { + if (!groupKey || groupKey === currentGroup) return; + const candidates = transitions.filter((item) => String((item == null ? void 0 : item.target_group) || "") === groupKey); + if (!candidates.length) { + setStatus("kanban", "\u0414\u043B\u044F \u044D\u0442\u043E\u0439 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 \u043D\u0435\u0442 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430 \u0432 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u0443\u044E \u043A\u043E\u043B\u043E\u043D\u043A\u0443", "error"); + return; + } + if (candidates.length > 1) { + await openRequestDetails(requestId, void 0, { + statusChangePreset: { + source: "kanban", + targetGroup: groupKey, + suggestedStatuses: candidates.map((item) => String((item == null ? void 0 : item.to_status) || "")).filter(Boolean) + } + }); + setStatus("kanban", "\u041E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u043C\u043E\u0434\u0430\u043B\u044C\u043D\u043E\u0435 \u043E\u043A\u043D\u043E \u0441\u043C\u0435\u043D\u044B \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u0438 \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0439 \u0441\u0442\u0430\u0442\u0443\u0441", "ok"); + return; + } + targetStatus = String(((_a2 = candidates[0]) == null ? void 0 : _a2.to_status) || "").trim(); + } + if (!targetStatus || targetStatus === String((row == null ? void 0 : row.status_code) || "")) return; + try { + setStatus("kanban", "\u041F\u0435\u0440\u0435\u0432\u043E\u0434\u0438\u043C \u0437\u0430\u044F\u0432\u043A\u0443...", ""); + await submitRequestStatusChange({ requestId, statusCode: targetStatus }); + setStatus("kanban", "\u0421\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u044F\u0432\u043A\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok"); + const refreshRequests = canAccessSection(role, "requests") ? loadTable("requests", { resetOffset: true }) : Promise.resolve(); + await Promise.all([loadKanban(), refreshRequests]); + } catch (error) { + setStatus("kanban", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430: " + error.message, "error"); + } + }, + [loadKanban, loadTable, openRequestDetails, role, setStatus, submitRequestStatusChange, userId] + ); + const downloadInvoicePdf = useCallback( + async (row, statusKey = "invoices") => { + if (!row || !row.id || !token) return; + try { + setStatus(statusKey, "\u0424\u043E\u0440\u043C\u0438\u0440\u0443\u0435\u043C PDF...", ""); + const response = await fetch("/api/admin/invoices/" + row.id + "/pdf", { + headers: { Authorization: "Bearer " + token } + }); + 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(translateApiError(String(message))); + } + const blob = await response.blob(); + const fileName = (row.invoice_number || "invoice") + ".pdf"; + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + setStatus(statusKey, "PDF \u0441\u043A\u0430\u0447\u0430\u043D", "ok"); + } catch (error) { + setStatus(statusKey, "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F: " + error.message, "error"); + } + }, + [setStatus, token] + ); + const downloadRequestInvoicePdf = useCallback( + async (row) => { + await downloadInvoicePdf(row, "requestModal"); + }, + [downloadInvoicePdf] + ); + const resetAdminRoute = useCallback(() => { + const nextUrl = "/admin.html"; + if (window.location.pathname !== nextUrl || window.location.search) { + window.history.replaceState(null, "", nextUrl); + } + }, []); + const goBackFromRequestWorkspace = useCallback(() => { + const targetSection = canAccessSection(role, "requests") ? "requests" : "kanban"; + resetAdminRoute(); + setActiveSection(targetSection); + refreshSection(targetSection); + }, [refreshSection, resetAdminRoute, role]); + const openReassignModal = useCallback( + (row) => { + const options = getLawyerOptions(); + if (!options.length) { + setStatus("reassignForm", "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u044E\u0440\u0438\u0441\u0442\u043E\u0432 \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F", "error"); + return; + } + const current = String((row == null ? void 0 : row.assigned_lawyer_id) || ""); + const hasCurrent = options.some((option) => String(option.value) === current); + const fallback = options[0] ? String(options[0].value) : ""; + setReassignModal({ + open: true, + requestId: (row == null ? void 0 : row.id) || null, + trackNumber: (row == null ? void 0 : row.track_number) || "", + lawyerId: hasCurrent ? current : fallback + }); + setStatus("reassignForm", "", ""); + }, + [getLawyerOptions, setStatus] + ); + const closeReassignModal = useCallback(() => { + setReassignModal({ open: false, requestId: null, trackNumber: "", lawyerId: "" }); + setStatus("reassignForm", "", ""); + }, [setStatus]); + const updateReassignLawyer = useCallback((event) => { + setReassignModal((prev) => ({ ...prev, lawyerId: event.target.value })); + }, []); + const submitReassignModal = useCallback( + async (event) => { + event.preventDefault(); + if (!reassignModal.requestId) return; + const lawyerId = String(reassignModal.lawyerId || "").trim(); + if (!lawyerId) { + setStatus("reassignForm", "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u044E\u0440\u0438\u0441\u0442\u0430", "error"); + return; + } + try { + setStatus("reassignForm", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435...", ""); + await api("/api/admin/requests/" + reassignModal.requestId + "/reassign", { + method: "POST", + body: { lawyer_id: lawyerId } + }); + setStatus("requests", "\u0417\u0430\u044F\u0432\u043A\u0430 \u043F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0430", "ok"); + closeReassignModal(); + await loadTable("requests", { resetOffset: true }); + } catch (error) { + setStatus("reassignForm", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + } + }, + [api, closeReassignModal, loadTable, reassignModal.lawyerId, reassignModal.requestId, setStatus] + ); + const defaultFilterValue = useCallback( + (fieldDef) => { + if (!fieldDef) return ""; + if (fieldDef.type === "boolean") return "true"; + if (fieldDef.type === "reference" || fieldDef.type === "enum") { + const options = getFieldOptions(fieldDef); + return options.length ? String(options[0].value) : ""; + } + return ""; + }, + [getFieldOptions] + ); + const openFilterModal = useCallback( + (tableKey) => { + const fields = getFilterFields(tableKey); + if (!fields.length) { + setStatus("filter", "\u0414\u043B\u044F \u0442\u0430\u0431\u043B\u0438\u0446\u044B \u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043F\u043E\u043B\u0435\u0439 \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u0446\u0438\u0438", "error"); + return; + } + const firstField = fields[0]; + const firstOp = getOperatorsForType(firstField.type)[0] || "="; + setFilterModal({ + open: true, + tableKey, + field: firstField.field, + op: firstOp, + rawValue: defaultFilterValue(firstField), + editIndex: null + }); + setStatus("filter", "", ""); + }, + [defaultFilterValue, getFilterFields, setStatus] + ); + const openFilterEditModal = useCallback( + (tableKey, index) => { + var _a2; + const tableState = tablesRef.current[tableKey] || createTableState(); + const target = (tableState.filters || [])[index]; + if (!target) return; + const fieldDef = getFieldDef(tableKey, target.field); + if (!fieldDef) return; + const allowedOps = getOperatorsForType(fieldDef.type); + const safeOp = allowedOps.includes(target.op) ? target.op : allowedOps[0] || "="; + const rawValue = fieldDef.type === "boolean" ? target.value ? "true" : "false" : String((_a2 = target.value) != null ? _a2 : ""); + setFilterModal({ + open: true, + tableKey, + field: fieldDef.field, + op: safeOp, + rawValue, + editIndex: index + }); + setStatus("filter", "", ""); + }, + [getFieldDef, setStatus] + ); + const closeFilterModal = useCallback(() => { + setFilterModal((prev) => ({ ...prev, open: false, editIndex: null })); + setStatus("filter", "", ""); + }, [setStatus]); + const updateFilterField = useCallback( + (event) => { + const fieldName = event.target.value; + const fields = getFilterFields(filterModal.tableKey); + const fieldDef = fields.find((field) => field.field === fieldName) || null; + if (!fieldDef) return; + const defaultOp = getOperatorsForType(fieldDef.type)[0] || "="; + setFilterModal((prev) => ({ + ...prev, + field: fieldName, + op: defaultOp, + rawValue: defaultFilterValue(fieldDef) + })); + }, + [defaultFilterValue, filterModal.tableKey, getFilterFields] + ); + const updateFilterOp = useCallback((event) => { + const op = event.target.value; + setFilterModal((prev) => ({ ...prev, op })); + }, []); + const updateFilterValue = useCallback((event) => { + setFilterModal((prev) => ({ ...prev, rawValue: event.target.value })); + }, []); + const { applyFilterModal, clearFiltersFromModal, removeFilterChip } = useTableFilterActions({ + filterModal, + closeFilterModal, + getFieldDef, + loadKanban, + loadTable, + setStatus, + setTableState, + tablesRef + }); + const selectConfigNode = useCallback( + (tableKey) => { + resetAdminRoute(); + setConfigActiveKey(tableKey); + setActiveSection("config"); + loadCurrentConfigTable(false, void 0, tableKey); + }, + [loadCurrentConfigTable, resetAdminRoute] + ); + const refreshAll = useCallback(() => { + refreshSection(activeSection); + }, [activeSection, refreshSection]); + const activateSection = useCallback( + (section) => { + const nextSection = canAccessSection(role, section) ? section : "dashboard"; + resetAdminRoute(); + setActiveSection(nextSection); + refreshSection(nextSection); + }, + [refreshSection, resetAdminRoute, role] + ); + const applyRequestsQuickFilterPreset = useCallback( + async (filters, statusMessage) => { + if (!canAccessSection(role, "requests")) return; + const nextFilters = Array.isArray(filters) ? filters.filter((item) => item && item.field) : []; + resetAdminRoute(); + setActiveSection("requests"); + const currentState = tablesRef.current.requests || createTableState(); + setTableState("requests", { + ...currentState, + filters: nextFilters, + offset: 0, + showAll: false + }); + if (statusMessage) setStatus("requests", statusMessage, ""); + await loadTable("requests", { resetOffset: true, filtersOverride: nextFilters }); + }, + [loadTable, resetAdminRoute, role, setStatus, setTableState, tablesRef] + ); + const applyKanbanQuickFilterPreset = useCallback( + async (filters, statusMessage) => { + const nextFilters = Array.isArray(filters) ? filters.filter((item) => item && item.field) : []; + resetAdminRoute(); + setActiveSection("kanban"); + const currentState = tablesRef.current.kanban || createTableState(); + setTableState("kanban", { + ...currentState, + filters: nextFilters, + offset: 0, + showAll: false + }); + if (statusMessage) setStatus("kanban", statusMessage, ""); + await loadKanban(void 0, { filtersOverride: nextFilters }); + }, + [loadKanban, resetAdminRoute, setStatus, setTableState, tablesRef] + ); + const openRequestsWithUnreadAlerts = useCallback(async () => { + await applyRequestsQuickFilterPreset([{ field: "has_unread_updates", op: "=", value: true }], "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u043D\u043E\u0432\u044B\u043C\u0438 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F\u043C\u0438"); + }, [applyRequestsQuickFilterPreset]); + const openRequestsWithDeadlineAlerts = useCallback(async () => { + await applyRequestsQuickFilterPreset([{ field: "deadline_alert", op: "=", value: true }], "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u0433\u043E\u0440\u044F\u0449\u0438\u043C\u0438 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u0430\u043C\u0438"); + }, [applyRequestsQuickFilterPreset]); + const openKanbanWithUnreadAlerts = useCallback(async () => { + await applyKanbanQuickFilterPreset([{ field: "has_unread_updates", op: "=", value: true }], "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u043D\u043E\u0432\u044B\u043C\u0438 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F\u043C\u0438"); + }, [applyKanbanQuickFilterPreset]); + const openKanbanWithDeadlineAlerts = useCallback(async () => { + await applyKanbanQuickFilterPreset([{ field: "deadline_alert", op: "=", value: true }], "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u0433\u043E\u0440\u044F\u0449\u0438\u043C\u0438 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u0430\u043C\u0438"); + }, [applyKanbanQuickFilterPreset]); + const applyServiceRequestsQuickFilterPreset = useCallback( + async (filters, statusMessage) => { + const nextFilters = Array.isArray(filters) ? filters.filter((item) => item && item.field) : []; + resetAdminRoute(); + setActiveSection("serviceRequests"); + const currentState = tablesRef.current.serviceRequests || createTableState(); + setTableState("serviceRequests", { + ...currentState, + filters: nextFilters, + offset: 0, + showAll: false + }); + if (statusMessage) setStatus("serviceRequests", statusMessage, ""); + await loadTable("serviceRequests", { resetOffset: true, filtersOverride: nextFilters }); + }, + [loadTable, resetAdminRoute, setStatus, setTableState, tablesRef] + ); + const openServiceRequestsWithUnreadAlerts = useCallback(async () => { + if (String(role || "").toUpperCase() === "LAWYER") { + await applyServiceRequestsQuickFilterPreset( + [{ field: "lawyer_unread", op: "=", value: true }], + "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430" + ); + return; + } + await applyServiceRequestsQuickFilterPreset( + [{ field: "admin_unread", op: "=", value: true }], + "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430" + ); + }, [applyServiceRequestsQuickFilterPreset, role]); + const markServiceRequestRead = useCallback( + async (serviceRequestId) => { + const rowId = String(serviceRequestId || "").trim(); + if (!rowId) return; + try { + setStatus("serviceRequests", "\u041E\u0442\u043C\u0435\u0447\u0430\u0435\u043C \u043A\u0430\u043A \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0439...", ""); + await api("/api/admin/requests/service-requests/" + encodeURIComponent(rowId) + "/read", { method: "POST" }); + await Promise.all([loadTable("serviceRequests", { resetOffset: true }), loadDashboard()]); + if (canAccessSection(role, "requests")) await loadTable("requests", { resetOffset: true }); + setStatus("serviceRequests", "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u0447\u0435\u043D \u043A\u0430\u043A \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0439", "ok"); + } catch (error) { + setStatus("serviceRequests", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error"); + } + }, + [api, loadDashboard, loadTable, role, setStatus] + ); + const loadTotpStatus = useCallback( + async (tokenOverride) => { + const activeToken = tokenOverride !== void 0 ? tokenOverride : token; + if (!activeToken) return; + try { + const data = await api("/api/admin/auth/totp/status", { method: "GET" }, activeToken); + if (data && typeof data === "object") { + setTotpStatus({ + mode: String(data.mode || "password_totp_optional"), + enabled: Boolean(data.enabled), + required: Boolean(data.required), + has_backup_codes: Boolean(data.has_backup_codes) + }); + } + } catch (_) { + } + }, + [api, token] + ); + const openAccountModal = useCallback(async () => { + if (!token || !userId) { + setStatus("account", "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u043F\u0440\u043E\u0444\u0438\u043B\u044C: \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F", "error"); + return; + } + setAccountModal((prev) => ({ + ...prev, + open: true, + loading: true, + saving: false + })); + setStatus("account", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u0440\u043E\u0444\u0438\u043B\u044F...", ""); + try { + const row = await api("/api/admin/crud/admin_users/" + encodeURIComponent(String(userId))); + const nextInitial = { + name: String((row == null ? void 0 : row.name) || ""), + email: String((row == null ? void 0 : row.email) || email || ""), + phone: String((row == null ? void 0 : row.phone) || "") + }; + setAccountModal({ + open: true, + loading: false, + saving: false, + initial: nextInitial, + form: { + ...nextInitial, + password: "", + passwordConfirm: "" + } + }); + setStatus("account", "", ""); + } catch (error) { + setAccountModal((prev) => ({ ...prev, loading: false })); + setStatus("account", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u043F\u0440\u043E\u0444\u0438\u043B\u044F: " + error.message, "error"); + } + }, [api, email, setStatus, token, userId]); + const closeAccountModal = useCallback(() => { + setAccountModal((prev) => ({ + ...prev, + open: false, + loading: false, + saving: false, + form: { + name: prev.initial.name, + email: prev.initial.email, + phone: prev.initial.phone, + password: "", + passwordConfirm: "" + } + })); + setStatus("account", "", ""); + }, [setStatus]); + const updateAccountField = useCallback((event) => { + var _a2; + const fieldName = String(((_a2 = event == null ? void 0 : event.target) == null ? void 0 : _a2.name) || ""); + if (!fieldName) return; + setAccountModal((prev) => ({ + ...prev, + form: { + ...prev.form, + [fieldName]: event.target.value + } + })); + }, []); + const submitAccountModal = useCallback( + async (event) => { + event.preventDefault(); + if (!token || !userId) return; + const form = accountModal.form || {}; + const initial = accountModal.initial || {}; + const nextName = String(form.name || "").trim(); + const nextEmail = String(form.email || "").trim().toLowerCase(); + const nextPhone = String(form.phone || "").trim(); + const nextPassword = String(form.password || ""); + const nextPasswordConfirm = String(form.passwordConfirm || ""); + if (!nextName) { + setStatus("account", "\u0418\u043C\u044F \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C", "error"); + return; + } + if (!nextEmail) { + setStatus("account", "\u041F\u043E\u0447\u0442\u0430 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u043E\u0439", "error"); + return; + } + if (nextPassword && nextPassword.length < 8) { + setStatus("account", "\u041F\u0430\u0440\u043E\u043B\u044C \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435 \u043C\u0435\u043D\u0435\u0435 8 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432", "error"); + return; + } + if (nextPassword !== nextPasswordConfirm) { + setStatus("account", "\u041F\u0430\u0440\u043E\u043B\u0438 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0442", "error"); + return; + } + const payload = {}; + if (nextName !== String(initial.name || "").trim()) payload.name = nextName; + if (nextEmail !== String(initial.email || "").trim().toLowerCase()) payload.email = nextEmail; + if (nextPhone !== String(initial.phone || "").trim()) payload.phone = nextPhone || null; + if (nextPassword) payload.password = nextPassword; + if (!Object.keys(payload).length) { + setStatus("account", "\u041D\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u0434\u043B\u044F \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F", ""); + return; + } + try { + setAccountModal((prev) => ({ ...prev, saving: true })); + setStatus("account", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435...", ""); + const row = await api("/api/admin/crud/admin_users/" + encodeURIComponent(String(userId)), { + method: "PATCH", + body: payload + }); + const nextInitial = { + name: String((row == null ? void 0 : row.name) || nextName), + email: String((row == null ? void 0 : row.email) || nextEmail), + phone: String((row == null ? void 0 : row.phone) || nextPhone) + }; + setAccountModal((prev) => ({ + ...prev, + saving: false, + initial: nextInitial, + form: { + ...nextInitial, + password: "", + passwordConfirm: "" + } + })); + if (nextInitial.email) setEmail(nextInitial.email); + setStatus("account", "\u041F\u0440\u043E\u0444\u0438\u043B\u044C \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok"); + } catch (error) { + setAccountModal((prev) => ({ ...prev, saving: false })); + setStatus("account", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F: " + error.message, "error"); + } + }, + [accountModal.form, accountModal.initial, api, setStatus, token, userId] + ); + const closeTotpSetupModal = useCallback(() => { + setTotpSetupModal({ + open: false, + secret: "", + uri: "", + qrDataUrl: "", + code: "", + loading: false + }); + setStatus("totpSetup", "", ""); + }, [setStatus]); + const updateTotpSetupCode = useCallback((event) => { + setTotpSetupModal((prev) => ({ ...prev, code: event.target.value })); + }, []); + const copyTotpSecret = useCallback(async () => { + const value = String(totpSetupModal.secret || "").trim(); + if (!value) return; + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(value); + setStatus("totpSetup", "\u041A\u043B\u044E\u0447 \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D \u0432 \u0431\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430", "ok"); + } else { + setStatus("totpSetup", "\u0411\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0432 \u044D\u0442\u043E\u043C \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435", "error"); + } + } catch (_) { + setStatus("totpSetup", "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u043B\u044E\u0447", "error"); + } + }, [setStatus, totpSetupModal.secret]); + const copyTotpUri = useCallback(async () => { + const value = String(totpSetupModal.uri || "").trim(); + if (!value) return; + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(value); + setStatus("totpSetup", "URI \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D \u0432 \u0431\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430", "ok"); + } else { + setStatus("totpSetup", "\u0411\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0432 \u044D\u0442\u043E\u043C \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435", "error"); + } + } catch (_) { + setStatus("totpSetup", "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C URI", "error"); + } + }, [setStatus, totpSetupModal.uri]); + const setupTotp = useCallback(async () => { + try { + const setup = await api("/api/admin/auth/totp/setup", { method: "POST", body: {} }); + const secret = String((setup == null ? void 0 : setup.secret) || "").trim(); + const uri = String((setup == null ? void 0 : setup.otpauth_uri) || "").trim(); + if (!secret || !uri) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0441\u0435\u043A\u0440\u0435\u0442 TOTP"); + let qrDataUrl = ""; + try { + qrDataUrl = await import_qrcode.default.toDataURL(uri, { + margin: 1, + width: 240, + errorCorrectionLevel: "M" + }); + } catch (_) { + qrDataUrl = ""; + } + setTotpSetupModal({ + open: true, + secret, + uri, + qrDataUrl, + code: "", + loading: false + }); + setStatus("totpSetup", "", ""); + } catch (error) { + setStatus("login", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 2FA: " + error.message, "error"); + } + }, [api, setStatus]); + const submitTotpSetup = useCallback( + async (event) => { + event.preventDefault(); + const secret = String(totpSetupModal.secret || "").trim(); + const rawCode = String(totpSetupModal.code || "").trim(); + const digitsOnly = rawCode.replace(/\D+/g, ""); + if (!secret) { + setStatus("totpSetup", "\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D TOTP secret. \u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0443.", "error"); + return; + } + if (digitsOnly.length !== 6) { + setStatus("totpSetup", "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 6-\u0437\u043D\u0430\u0447\u043D\u044B\u0439 \u043A\u043E\u0434", "error"); + return; + } + try { + setTotpSetupModal((prev) => ({ ...prev, loading: true })); + const enabled = await api("/api/admin/auth/totp/enable", { method: "POST", body: { secret, code: digitsOnly } }); + closeTotpSetupModal(); + setStatus("login", "2FA \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0430", "ok"); + const backupCodes = Array.isArray(enabled == null ? void 0 : enabled.backup_codes) ? enabled.backup_codes : []; + window.alert( + "2FA \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0430.\n\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0435 \u043A\u043E\u0434\u044B (\u043E\u0434\u043D\u043E\u043A\u0440\u0430\u0442\u043D\u043E):\n\n" + (backupCodes.length ? backupCodes.join("\n") : "-") + ); + await loadTotpStatus(); + } catch (error) { + setTotpSetupModal((prev) => ({ ...prev, loading: false })); + setStatus("totpSetup", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F 2FA: " + error.message, "error"); + } + }, + [api, closeTotpSetupModal, loadTotpStatus, setStatus, totpSetupModal.code, totpSetupModal.secret] + ); + const regenerateTotpBackupCodes = useCallback(async () => { + try { + const code = String(window.prompt("\u0412\u0432\u0435\u0434\u0438\u0442\u0435 TOTP \u043A\u043E\u0434 (\u0438\u043B\u0438 \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0439 \u043A\u043E\u0434) \u0434\u043B\u044F \u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438", "") || "").trim(); + if (!code) return; + const payload = /^\d{6}$/.test(code) ? { code } : { backup_code: code }; + const data = await api("/api/admin/auth/totp/backup/regenerate", { method: "POST", body: payload }); + const backupCodes = Array.isArray(data == null ? void 0 : data.backup_codes) ? data.backup_codes : []; + window.alert("\u041D\u043E\u0432\u044B\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0435 \u043A\u043E\u0434\u044B:\n\n" + (backupCodes.length ? backupCodes.join("\n") : "-")); + await loadTotpStatus(); + } catch (error) { + setStatus("login", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 backup-\u043A\u043E\u0434\u043E\u0432: " + error.message, "error"); + } + }, [api, loadTotpStatus, setStatus]); + const disableTotp = useCallback(async () => { + try { + const code = String(window.prompt("\u0412\u0432\u0435\u0434\u0438\u0442\u0435 TOTP \u043A\u043E\u0434 (\u0438\u043B\u0438 \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0439 \u043A\u043E\u0434) \u0434\u043B\u044F \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F 2FA", "") || "").trim(); + if (!code) return; + const payload = /^\d{6}$/.test(code) ? { code } : { backup_code: code }; + await api("/api/admin/auth/totp/disable", { method: "POST", body: payload }); + setStatus("login", "2FA \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0430", "ok"); + await loadTotpStatus(); + } catch (error) { + setStatus("login", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F 2FA: " + error.message, "error"); + } + }, [api, loadTotpStatus, setStatus]); + const logout = useCallback(() => { + localStorage.removeItem(LS_TOKEN); + setToken(""); + setRole(""); + setEmail(""); + setUserId(""); + setRecordModal({ open: false, tableKey: null, mode: "create", rowId: null, form: {} }); + resetRequestWorkspaceState(); + setFilterModal({ open: false, tableKey: null, field: "", op: "=", rawValue: "", editIndex: null }); + resetKanbanState(); + setReassignModal({ open: false, requestId: null, trackNumber: "", lawyerId: "" }); + setDashboardData({ + scope: "", + cards: [], + byStatus: {}, + lawyerLoads: [], + myUnreadByEvent: {}, + myUnreadTotal: 0, + myUnreadNotificationsTotal: 0, + unreadForClients: 0, + unreadForLawyers: 0, + serviceRequestUnreadTotal: 0, + deadlineAlertTotal: 0, + monthRevenue: 0, + monthExpenses: 0 + }); + setMetaJson(""); + setConfigActiveKey(""); + setReferencesExpanded(true); + resetTablesState(); + setDictionaries({ + topics: [], + statuses: Object.entries(STATUS_LABELS).map(([code, name]) => ({ code, name })), + formFieldTypes: [...DEFAULT_FORM_FIELD_TYPES], + formFieldKeys: [], + users: [] + }); + setStatusMap({}); + setSmsProviderHealth(null); + setTotpStatus({ + mode: "password_totp_optional", + enabled: false, + required: false, + has_backup_codes: false + }); + setTotpSetupModal({ + open: false, + secret: "", + uri: "", + qrDataUrl: "", + code: "", + loading: false + }); + setAccountModal({ + open: false, + loading: false, + saving: false, + initial: { name: "", email: "", phone: "" }, + form: { name: "", email: "", phone: "", password: "", passwordConfirm: "" } + }); + setActiveSection("dashboard"); + }, [resetKanbanState, resetRequestWorkspaceState, resetTablesState]); + const login = useCallback( + async (emailInput, passwordInput, totpCodeInput) => { + try { + setStatus("login", "\u0412\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u043C \u0432\u0445\u043E\u0434...", ""); + const rawTotp = String(totpCodeInput || "").trim(); + const digitsOnly = rawTotp.replace(/\D+/g, ""); + const loginBody = { + email: String(emailInput || "").trim(), + password: passwordInput || "", + ...rawTotp ? digitsOnly.length === 6 ? { totp_code: digitsOnly } : { backup_code: rawTotp } : {} + }; + const data = await api( + "/api/admin/auth/login", + { + method: "POST", + auth: false, + body: loginBody + }, + "" + ); + const nextToken = data.access_token; + const payload = decodeJwtPayload(nextToken || ""); + if (!payload || !payload.role || !payload.email) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435 \u0442\u043E\u043A\u0435\u043D\u0430"); + localStorage.setItem(LS_TOKEN, nextToken); + setToken(nextToken); + setRole(payload.role); + setEmail(payload.email); + setUserId(String(payload.sub || "")); + await bootstrapReferenceData(nextToken, payload.role); + setActiveSection("dashboard"); + await loadDashboard(nextToken); + await loadTotpStatus(nextToken); + setStatus("login", "\u0423\u0441\u043F\u0435\u0448\u043D\u044B\u0439 \u0432\u0445\u043E\u0434", "ok"); + } catch (error) { + setStatus("login", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u0445\u043E\u0434\u0430: " + error.message, "error"); + } + }, + [api, bootstrapReferenceData, loadDashboard, loadTotpStatus, setStatus] + ); + useEffect(() => { + const saved = localStorage.getItem(LS_TOKEN) || ""; + if (!saved) return; + const payload = decodeJwtPayload(saved); + if (!payload || !payload.role || !payload.email) { + localStorage.removeItem(LS_TOKEN); + return; + } + setToken(saved); + setRole(payload.role); + setEmail(payload.email); + setUserId(String(payload.sub || "")); + }, []); + useEffect(() => { + if (!token || !role) return; + let cancelled = false; + (async () => { + await bootstrapReferenceData(token, role); + if (!cancelled) await loadDashboard(token); + if (!cancelled) await loadTotpStatus(token); + })(); + return () => { + cancelled = true; + }; + }, [bootstrapReferenceData, loadDashboard, loadTotpStatus, role, token]); + useEffect(() => { + if (!token || !role) return; + if (initialRouteHandledRef.current) return; + initialRouteHandledRef.current = true; + if (isRequestWorkspaceRoute && routeInfo.requestId) { + setActiveSection("requestWorkspace"); + loadRequestModalData(routeInfo.requestId, { showLoading: true }); + resetAdminRoute(); + return; + } + if (routeInfo.section) { + if (canAccessSection(role, routeInfo.section)) { + setActiveSection(routeInfo.section); + refreshSection(routeInfo.section, token); + resetAdminRoute(); + } else { + setActiveSection("dashboard"); + refreshSection("dashboard", token); + resetAdminRoute(); + } + } + }, [isRequestWorkspaceRoute, loadRequestModalData, refreshSection, resetAdminRoute, role, routeInfo.requestId, routeInfo.section, token]); + useEffect(() => { + if (!token) { + setSmsProviderHealth(null); + return; + } + if (String(role || "").toUpperCase() !== "ADMIN") { + setSmsProviderHealth(null); + return; + } + if (activeSection !== "config" || configActiveKey !== "otp_sessions") return; + loadSmsProviderHealth(void 0, { silent: true }); + }, [activeSection, configActiveKey, loadSmsProviderHealth, role, token]); + useEffect(() => { + if (!dictionaryTableItems.length) { + if (configActiveKey) setConfigActiveKey(""); + return; + } + const hasCurrent = dictionaryTableItems.some((item) => item.key === configActiveKey); + if (!hasCurrent) setConfigActiveKey(dictionaryTableItems[0].key); + }, [configActiveKey, dictionaryTableItems]); + const anyOverlayOpen = recordModal.open || filterModal.open || reassignModal.open || kanbanSortModal.open || totpSetupModal.open || accountModal.open; + useEffect(() => { + document.body.classList.toggle("modal-open", anyOverlayOpen); + return () => document.body.classList.remove("modal-open"); + }, [anyOverlayOpen]); + useEffect(() => { + const onEsc = (event) => { + if (event.key !== "Escape") return; + setRecordModal((prev) => ({ ...prev, open: false })); + setFilterModal((prev) => ({ ...prev, open: false })); + closeKanbanSortModal(); + setReassignModal((prev) => ({ ...prev, open: false })); + closeTotpSetupModal(); + closeAccountModal(); + }; + document.addEventListener("keydown", onEsc); + return () => document.removeEventListener("keydown", onEsc); + }, [closeAccountModal, closeKanbanSortModal, closeTotpSetupModal]); + const menuItems = useMemo(() => { + const baseItems = [ + { key: "dashboard", label: "\u041E\u0431\u0437\u043E\u0440" }, + { key: "kanban", label: "\u041A\u0430\u043D\u0431\u0430\u043D" }, + { key: "requests", label: "\u0417\u0430\u044F\u0432\u043A\u0438" }, + { key: "serviceRequests", label: "\u0417\u0430\u043F\u0440\u043E\u0441\u044B" }, + { key: "invoices", label: "\u0421\u0447\u0435\u0442\u0430" } + ]; + return baseItems.filter((item) => canAccessSection(role, item.key)); + }, [role]); + const topbarUnreadCount = useMemo(() => { + const roleCode = String(role || "").toUpperCase(); + if (roleCode === "LAWYER" || roleCode === "ADMIN" || roleCode === "CURATOR") { + return Number(dashboardData.myUnreadNotificationsTotal || dashboardData.myUnreadTotal || 0); + } + return Number(dashboardData.unreadForClients || 0) + Number(dashboardData.unreadForLawyers || 0); + }, [dashboardData.myUnreadNotificationsTotal, dashboardData.myUnreadTotal, dashboardData.unreadForClients, dashboardData.unreadForLawyers, role]); + const topbarDeadlineAlertCount = useMemo(() => Number(dashboardData.deadlineAlertTotal || 0), [dashboardData.deadlineAlertTotal]); + const topbarServiceRequestUnreadCount = useMemo( + () => Number(dashboardData.serviceRequestUnreadTotal || 0), + [dashboardData.serviceRequestUnreadTotal] + ); + const topbarRoleCode = String(role || "").toUpperCase(); + const canUseRequestsAlerts = topbarRoleCode === "ADMIN"; + const canUseKanbanAlerts = topbarRoleCode === "LAWYER"; + const showRequestAlertIcons = canUseRequestsAlerts || canUseKanbanAlerts; + const showServiceRequestIcon = canAccessSection(role, "serviceRequests"); + const activeFilterFields = useMemo(() => { + if (!filterModal.tableKey) return []; + return getFilterFields(filterModal.tableKey); + }, [filterModal.tableKey, getFilterFields]); + const filterTableLabel = useMemo(() => getTableLabel(filterModal.tableKey), [filterModal.tableKey, getTableLabel]); + const recordModalFields = useMemo(() => { + const all = getRecordFields(recordModal.tableKey); + if (recordModal.mode !== "create") return all.filter((field) => !field.createOnly); + return all.filter((field) => !field.autoCreate); + }, [getRecordFields, recordModal.mode, recordModal.tableKey]); + const activeConfigTableState = useMemo(() => { + return tables[configActiveKey] || createTableState(); + }, [configActiveKey, tables]); + const activeConfigMeta = useMemo(() => tableCatalogMap[configActiveKey] || null, [configActiveKey, tableCatalogMap]); + const activeConfigActions = useMemo(() => { + return Array.isArray(activeConfigMeta == null ? void 0 : activeConfigMeta.actions) ? activeConfigMeta.actions : []; + }, [activeConfigMeta]); + const canCreateInConfig = activeConfigActions.includes("create"); + const canUpdateInConfig = activeConfigActions.includes("update"); + const canDeleteInConfig = activeConfigActions.includes("delete"); + const genericConfigHeaders = useMemo(() => { + if (!activeConfigMeta || !Array.isArray(activeConfigMeta.columns)) return []; + const headers = (activeConfigMeta.columns || []).filter((column) => column && column.name).map((column) => { + const name = String(column.name); + return { + key: name, + label: String(column.label || humanizeKey(name)), + sortable: Boolean(column.sortable !== false), + field: name + }; + }); + if (canUpdateInConfig || canDeleteInConfig) headers.push({ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }); + return headers; + }, [activeConfigMeta, canDeleteInConfig, canUpdateInConfig]); + return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "layout" }, /* @__PURE__ */ React.createElement("aside", { className: "sidebar" }, /* @__PURE__ */ React.createElement("div", { className: "logo" }, /* @__PURE__ */ React.createElement("a", { href: "/" }, /* @__PURE__ */ React.createElement("img", { className: "brand-mark", src: "/brand-mark.svg", alt: "", width: "24", height: "24" }), /* @__PURE__ */ React.createElement("span", null, "\u041F\u0440\u0430\u0432\u043E\u0432\u043E\u0439 \u0442\u0440\u0435\u043A\u0435\u0440"))), /* @__PURE__ */ React.createElement("nav", { className: "menu" }, menuItems.map((item) => /* @__PURE__ */ React.createElement( + "button", + { + key: item.key, + className: activeSection === item.key ? "active" : "", + "data-section": item.key, + type: "button", + onClick: () => activateSection(item.key) + }, + item.label + )), role === "ADMIN" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement( + "button", + { + className: activeSection === "config" ? "active" : "", + type: "button", + onClick: () => { + setReferencesExpanded((prev) => !prev); + activateSection("config"); + } + }, + "\u0421\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0438 " + (referencesExpanded ? "\u25BE" : "\u25B8") + ), referencesExpanded ? /* @__PURE__ */ React.createElement("div", { className: "menu-tree" }, dictionaryTableItems.map((item) => /* @__PURE__ */ React.createElement( + "button", + { + key: item.key, + type: "button", + className: activeSection === "config" && configActiveKey === item.key ? "active" : "", + onClick: () => selectConfigNode(item.key) + }, + getTableLabel(item.key) + ))) : null) : null), role !== "LAWYER" ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: "0.75rem", display: "flex", gap: "0.5rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: refreshAll }, "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C")) : null), /* @__PURE__ */ React.createElement("main", { className: "main" }, /* @__PURE__ */ React.createElement("div", { className: "topbar" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h1", null, "\u041F\u0430\u043D\u0435\u043B\u044C \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "UniversalQuery, RBAC \u0438 \u0430\u0443\u0434\u0438\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043F\u043E \u043A\u043B\u044E\u0447\u0435\u0432\u044B\u043C \u0441\u0443\u0449\u043D\u043E\u0441\u0442\u044F\u043C \u0441\u0438\u0441\u0442\u0435\u043C\u044B.")), /* @__PURE__ */ React.createElement("div", { className: "topbar-actions", "aria-label": "\u0411\u044B\u0441\u0442\u0440\u044B\u0435 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u044F \u0438 \u043F\u0440\u043E\u0444\u0438\u043B\u044C" }, showServiceRequestIcon ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn topbar-alert-btn" + (topbarServiceRequestUnreadCount > 0 ? " has-alert alert-danger" : ""), + "data-tooltip": topbarServiceRequestUnreadCount > 0 ? "\u041D\u043E\u0432\u044B\u0435 \u043A\u043B\u0438\u0435\u043D\u0442\u0441\u043A\u0438\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B: " + String(topbarServiceRequestUnreadCount) : "\u041D\u043E\u0432\u044B\u0445 \u043A\u043B\u0438\u0435\u043D\u0442\u0441\u043A\u0438\u0445 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432 \u043D\u0435\u0442", + "aria-label": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430", + onClick: openServiceRequestsWithUnreadAlerts + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "17", height: "17", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "path", + { + d: "M4.5 4.5h15a1.5 1.5 0 0 1 1.5 1.5v9.8a1.5 1.5 0 0 1-1.5 1.5H9.1l-3.7 3.1c-.98.82-2.4.13-2.4-1.14V6a1.5 1.5 0 0 1 1.5-1.5zm1.7 4.2a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2zm5.8 0a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2zm5.8 0a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2z", + fill: "currentColor" + } + )), + /* @__PURE__ */ React.createElement("span", { className: "topbar-alert-dot", "aria-hidden": "true" }) + ) : null, showRequestAlertIcons ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn topbar-alert-btn" + (topbarDeadlineAlertCount > 0 ? " has-alert alert-danger" : ""), + "data-tooltip": topbarDeadlineAlertCount > 0 ? "\u0413\u043E\u0440\u044F\u0449\u0438\u0435 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u044B: " + String(topbarDeadlineAlertCount) : "\u0413\u043E\u0440\u044F\u0449\u0438\u0445 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u043E\u0432 \u043D\u0435\u0442", + "aria-label": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u0433\u043E\u0440\u044F\u0449\u0438\u043C\u0438 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u0430\u043C\u0438", + onClick: canUseRequestsAlerts ? openRequestsWithDeadlineAlerts : openKanbanWithDeadlineAlerts + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "17", height: "17", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "path", + { + d: "M12 3a1.6 1.6 0 0 1 1.42.86l7.14 13.7A1.6 1.6 0 0 1 19.14 20H4.86a1.6 1.6 0 0 1-1.42-2.44l7.14-13.7A1.6 1.6 0 0 1 12 3zm0 4.2a1 1 0 0 0-1 1v5.2a1 1 0 1 0 2 0V8.2a1 1 0 0 0-1-1zm0 9.4a1.15 1.15 0 1 0 0 2.3 1.15 1.15 0 0 0 0-2.3z", + fill: "currentColor" + } + )), + /* @__PURE__ */ React.createElement("span", { className: "topbar-alert-dot", "aria-hidden": "true" }) + ), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn topbar-alert-btn" + (topbarUnreadCount > 0 ? " has-alert alert-success" : ""), + "data-tooltip": topbarUnreadCount > 0 ? "\u041D\u043E\u0432\u044B\u0435 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0430\u043C: " + String(topbarUnreadCount) : "\u041D\u043E\u0432\u044B\u0445 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u0439 \u043D\u0435\u0442", + "aria-label": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u043D\u043E\u0432\u044B\u043C\u0438 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F\u043C\u0438", + onClick: canUseRequestsAlerts ? openRequestsWithUnreadAlerts : openKanbanWithUnreadAlerts + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "17", height: "17", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "path", + { + d: "M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 4 17.5v-11zm2 .5v.32l6 4.44 6-4.44V7a.5.5 0 0 0-.5-.5h-11A.5.5 0 0 0 6 7zm12 2.8-5.4 4a1 1 0 0 1-1.2 0L6 9.8v7.7c0 .28.22.5.5.5h11a.5.5 0 0 0 .5-.5V9.8z", + fill: "currentColor" + } + )), + /* @__PURE__ */ React.createElement("span", { className: "topbar-alert-dot", "aria-hidden": "true" }) + )) : null, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "icon-btn topbar-alert-btn", + "data-tooltip": "\u041B\u0438\u0447\u043D\u044B\u0439 \u043A\u0430\u0431\u0438\u043D\u0435\u0442", + "aria-label": "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u043B\u0438\u0447\u043D\u044B\u0439 \u043A\u0430\u0431\u0438\u043D\u0435\u0442", + onClick: openAccountModal + }, + /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "17", height: "17", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement( + "path", + { + d: "M12 12.2a4.1 4.1 0 1 0-4.1-4.1 4.1 4.1 0 0 0 4.1 4.1zm0 2c-3.8 0-7 2.2-7.8 5.3-.1.4.2.8.6.8h14.4c.4 0 .7-.4.6-.8-.8-3.1-4-5.3-7.8-5.3z", + fill: "currentColor" + } + )) + ))), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "dashboard", id: "section-dashboard" }, /* @__PURE__ */ React.createElement( + DashboardSection, + { + dashboardData, + token, + status: getStatus("dashboard"), + apiCall: api, + onOpenRequest: openRequestDetails, + DataTableComponent: DataTable, + StatusLineComponent: StatusLine, + UserAvatarComponent: UserAvatar + } + )), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "kanban", id: "section-kanban" }, /* @__PURE__ */ React.createElement( + KanbanBoard, + { + loading: kanbanLoading, + columns: kanbanData.columns, + rows: kanbanData.rows, + role, + actorId: userId, + onRefresh: () => loadKanban(), + filters: tables.kanban.filters, + onOpenFilter: () => openFilterModal("kanban"), + onRemoveFilter: (index) => removeFilterChip("kanban", index), + onEditFilter: (index) => openFilterEditModal("kanban", index), + getFilterChipLabel: (clause) => { + const fieldDef = getFieldDef("kanban", clause.field); + return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("kanban", clause); + }, + onOpenSort: openKanbanSortModal, + sortActive: kanbanSortApplied, + onOpenRequest: openRequestDetails, + onClaimRequest: claimRequest, + onMoveRequest: moveRequestFromKanban, + status: getStatus("kanban"), + FilterToolbarComponent: FilterToolbar, + StatusLineComponent: StatusLine + } + )), canAccessSection(role, "requests") ? /* @__PURE__ */ React.createElement(Section, { active: activeSection === "requests", id: "section-requests" }, /* @__PURE__ */ React.createElement( + RequestsSection, + { + role, + tables, + status: getStatus("requests"), + getFieldDef, + getFilterValuePreview, + resolveReferenceLabel, + onRefresh: () => loadTable("requests", { resetOffset: true }), + onCreate: () => openCreateRecordModal("requests"), + onOpenFilter: () => openFilterModal("requests"), + onRemoveFilter: (index) => removeFilterChip("requests", index), + onEditFilter: (index) => openFilterEditModal("requests", index), + onSort: (field) => toggleTableSort("requests", field), + onPrev: () => loadPrevPage("requests"), + onNext: () => loadNextPage("requests"), + onLoadAll: () => loadAllRows("requests"), + onClaimRequest: claimRequest, + onOpenReassign: openReassignModal, + onOpenRequest: openRequestDetails, + onEditRecord: (row) => openEditRecordModal("requests", row), + onDeleteRecord: (id) => deleteRecord("requests", id), + FilterToolbarComponent: FilterToolbar, + DataTableComponent: DataTable, + TablePagerComponent: TablePager, + StatusLineComponent: StatusLine, + IconButtonComponent: IconButton + } + )) : null, /* @__PURE__ */ React.createElement(Section, { active: activeSection === "serviceRequests", id: "section-service-requests" }, /* @__PURE__ */ React.createElement( + ServiceRequestsSection, + { + role, + tables, + status: getStatus("serviceRequests"), + getFieldDef, + getFilterValuePreview, + onRefresh: () => loadTable("serviceRequests", { resetOffset: true }), + onOpenFilter: () => openFilterModal("serviceRequests"), + onRemoveFilter: (index) => removeFilterChip("serviceRequests", index), + onEditFilter: (index) => openFilterEditModal("serviceRequests", index), + onSort: (field) => toggleTableSort("serviceRequests", field), + onPrev: () => loadPrevPage("serviceRequests"), + onNext: () => loadNextPage("serviceRequests"), + onLoadAll: () => loadAllRows("serviceRequests"), + onOpenRequest: openRequestDetails, + onMarkRead: markServiceRequestRead, + onEditRecord: (row) => openEditRecordModal("serviceRequests", row), + onDeleteRecord: (id) => deleteRecord("serviceRequests", id), + FilterToolbarComponent: FilterToolbar, + DataTableComponent: DataTable, + TablePagerComponent: TablePager, + StatusLineComponent: StatusLine, + IconButtonComponent: IconButton + } + )), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "requestWorkspace", id: "section-request-workspace" }, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, requestModal.trackNumber ? "\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 \u0437\u0430\u044F\u0432\u043A\u0438 " + requestModal.trackNumber : "\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 \u0437\u0430\u044F\u0432\u043A\u0438")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.45rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "icon-btn workspace-head-icon", type: "button", "data-tooltip": "\u041D\u0430\u0437\u0430\u0434", "aria-label": "\u041D\u0430\u0437\u0430\u0434", onClick: goBackFromRequestWorkspace }, /* @__PURE__ */ React.createElement("span", { className: "workspace-head-icon-glyph" }, "\u21A9")), /* @__PURE__ */ React.createElement( + "button", + { + className: "icon-btn workspace-head-icon", + type: "button", + "data-tooltip": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C", + "aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C", + onClick: refreshRequestModal, + disabled: requestModal.loading || requestModal.fileUploading + }, + /* @__PURE__ */ React.createElement("span", { className: "workspace-head-icon-glyph" }, "\u21BB") + ))), /* @__PURE__ */ React.createElement( + RequestWorkspace, + { + viewerRole: role, + viewerUserId: userId, + loading: requestModal.loading, + trackNumber: requestModal.trackNumber, + requestData: requestModal.requestData, + financeSummary: requestModal.financeSummary, + invoices: requestModal.invoices || [], + statusRouteNodes: requestModal.statusRouteNodes, + statusHistory: requestModal.statusHistory || [], + availableStatuses: requestModal.availableStatuses || [], + currentImportantDateAt: requestModal.currentImportantDateAt || "", + pendingStatusChangePreset: requestModal.pendingStatusChangePreset, + messages: requestModal.messages || [], + attachments: requestModal.attachments || [], + messageDraft: requestModal.messageDraft || "", + selectedFiles: requestModal.selectedFiles || [], + fileUploading: Boolean(requestModal.fileUploading), + status: getStatus("requestModal"), + onMessageChange: updateRequestModalMessageDraft, + onSendMessage: submitRequestModalMessage, + onFilesSelect: appendRequestModalFiles, + onRemoveSelectedFile: removeRequestModalFile, + onClearSelectedFiles: clearRequestModalFiles, + onLoadRequestDataTemplates: loadRequestDataTemplates, + onLoadRequestDataBatch: loadRequestDataBatch, + onLoadRequestDataTemplateDetails: loadRequestDataTemplateDetails, + onSaveRequestDataTemplate: saveRequestDataTemplate, + onSaveRequestDataBatch: saveRequestDataBatch, + onIssueInvoice: issueRequestInvoice, + onDownloadInvoicePdf: downloadRequestInvoicePdf, + onChangeStatus: submitRequestStatusChange, + onConsumePendingStatusChangePreset: clearPendingStatusChangePreset, + onLiveProbe: probeRequestLive, + onTypingSignal: setRequestTyping, + AttachmentPreviewModalComponent: AttachmentPreviewModal, + StatusLineComponent: StatusLine + } + )), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "invoices", id: "section-invoices" }, /* @__PURE__ */ React.createElement( + InvoicesSection, + { + role, + tables, + status: getStatus("invoices"), + getFieldDef, + getFilterValuePreview, + onRefresh: () => loadTable("invoices", { resetOffset: true }), + onCreate: () => openCreateRecordModal("invoices"), + onOpenFilter: () => openFilterModal("invoices"), + onRemoveFilter: (index) => removeFilterChip("invoices", index), + onEditFilter: (index) => openFilterEditModal("invoices", index), + onSort: (field) => toggleTableSort("invoices", field), + onPrev: () => loadPrevPage("invoices"), + onNext: () => loadNextPage("invoices"), + onLoadAll: () => loadAllRows("invoices"), + onOpenRequest: openInvoiceRequest, + onDownloadPdf: downloadInvoicePdf, + onEditRecord: (row) => openEditRecordModal("invoices", row), + onDeleteRecord: (id) => deleteRecord("invoices", id), + FilterToolbarComponent: FilterToolbar, + DataTableComponent: DataTable, + TablePagerComponent: TablePager, + StatusLineComponent: StatusLine, + IconButtonComponent: IconButton + } + )), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "quotes", id: "section-quotes" }, /* @__PURE__ */ React.createElement( + QuotesSection, + { + tables, + status: getStatus("quotes"), + getFieldDef, + getFilterValuePreview, + onRefresh: () => loadTable("quotes", { resetOffset: true }), + onCreate: () => openCreateRecordModal("quotes"), + onOpenFilter: () => openFilterModal("quotes"), + onRemoveFilter: (index) => removeFilterChip("quotes", index), + onEditFilter: (index) => openFilterEditModal("quotes", index), + onSort: (field) => toggleTableSort("quotes", field), + onPrev: () => loadPrevPage("quotes"), + onNext: () => loadNextPage("quotes"), + onLoadAll: () => loadAllRows("quotes"), + onEditRecord: (row) => openEditRecordModal("quotes", row), + onDeleteRecord: (id) => deleteRecord("quotes", id), + FilterToolbarComponent: FilterToolbar, + DataTableComponent: DataTable, + TablePagerComponent: TablePager, + StatusLineComponent: StatusLine, + IconButtonComponent: IconButton + } + )), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "config", id: "section-config" }, /* @__PURE__ */ React.createElement( + ConfigSection, + { + token, + tables, + dictionaries, + configActiveKey, + activeConfigTableState, + activeConfigMeta, + genericConfigHeaders, + canCreateInConfig, + canUpdateInConfig, + canDeleteInConfig, + statusDesignerTopicCode, + statusDesignerCards, + getTableLabel, + getFieldDef, + getFilterValuePreview, + resolveReferenceLabel, + resolveTableConfig, + getStatus, + loadCurrentConfigTable, + onRefreshSmsProviderHealth: () => loadSmsProviderHealth(void 0, { silent: false }), + smsProviderHealth, + openCreateRecordModal, + openFilterModal, + removeFilterChip, + openFilterEditModal, + toggleTableSort, + openEditRecordModal, + deleteRecord, + loadStatusDesignerTopic, + openCreateStatusTransitionForTopic, + loadPrevPage, + loadNextPage, + loadAllRows, + FilterToolbarComponent: FilterToolbar, + DataTableComponent: DataTable, + TablePagerComponent: TablePager, + StatusLineComponent: StatusLine, + IconButtonComponent: IconButton, + UserAvatarComponent: UserAvatar + } + )), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "availableTables", id: "section-available-tables" }, /* @__PURE__ */ React.createElement( + AvailableTablesSection, + { + tables, + status: getStatus("availableTables"), + onRefresh: () => loadAvailableTables(), + onToggleActive: updateAvailableTableState, + DataTableComponent: DataTable, + StatusLineComponent: StatusLine, + IconButtonComponent: IconButton + } + )))), /* @__PURE__ */ React.createElement( + RecordModal, + { + open: recordModal.open, + title: (recordModal.mode === "edit" ? "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u2022 " : "\u0421\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u2022 ") + getTableLabel(recordModal.tableKey), + fields: recordModalFields, + form: recordModal.form || {}, + status: getStatus("recordForm"), + onClose: closeRecordModal, + onChange: updateRecordField, + onUploadField: uploadRecordFieldFile, + onSubmit: submitRecordModal + } + ), /* @__PURE__ */ React.createElement( + FilterModal, + { + open: filterModal.open, + tableLabel: filterTableLabel, + fields: activeFilterFields, + draft: filterModal, + status: getStatus("filter"), + onClose: closeFilterModal, + onFieldChange: updateFilterField, + onOpChange: updateFilterOp, + onValueChange: updateFilterValue, + onSubmit: applyFilterModal, + onClear: clearFiltersFromModal, + getOperators: getOperatorsForType, + getFieldOptions + } + ), /* @__PURE__ */ React.createElement( + KanbanSortModal, + { + open: kanbanSortModal.open, + value: kanbanSortModal.value, + status: getStatus("kanbanSort"), + onChange: updateKanbanSortMode, + onClose: closeKanbanSortModal, + onSubmit: submitKanbanSortModal + } + ), /* @__PURE__ */ React.createElement( + ReassignModal, + { + open: reassignModal.open, + status: getStatus("reassignForm"), + options: getLawyerOptions(), + value: reassignModal.lawyerId, + onChange: updateReassignLawyer, + onClose: closeReassignModal, + onSubmit: submitReassignModal, + trackNumber: reassignModal.trackNumber + } + ), /* @__PURE__ */ React.createElement( + TotpSetupModal, + { + open: totpSetupModal.open, + status: getStatus("totpSetup"), + secret: totpSetupModal.secret, + uri: totpSetupModal.uri, + qrDataUrl: totpSetupModal.qrDataUrl, + code: totpSetupModal.code, + loading: totpSetupModal.loading, + onCodeChange: updateTotpSetupCode, + onClose: closeTotpSetupModal, + onSubmit: submitTotpSetup, + onCopySecret: copyTotpSecret, + onCopyUri: copyTotpUri + } + ), /* @__PURE__ */ React.createElement( + AccountModal, + { + open: accountModal.open, + status: getStatus("account"), + profileLoading: accountModal.loading, + saveLoading: accountModal.saving, + form: accountModal.form, + currentEmail: email, + currentRoleLabel: roleLabel(role), + totpStatus, + onFieldChange: updateAccountField, + onClose: closeAccountModal, + onSubmit: submitAccountModal, + onSetupTotp: setupTotp, + onRegenerateBackupCodes: regenerateTotpBackupCodes, + onDisableTotp: disableTotp, + onLogout: logout + } + ), !token || !role ? /* @__PURE__ */ React.createElement(LoginScreen, { onSubmit: login, status: getStatus("login") }) : null, /* @__PURE__ */ React.createElement(GlobalTooltipLayer, null)); + } + const root = ReactDOM.createRoot(document.getElementById("admin-root")); + root.render(/* @__PURE__ */ React.createElement(App, null)); + })(); +})(); diff --git a/app/web/admin.jsx b/app/web/admin.jsx index 6b9125d..5a7ac23 100644 --- a/app/web/admin.jsx +++ b/app/web/admin.jsx @@ -953,9 +953,13 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__"; } if (field.type === "reference" || field.type === "enum") { const extraOptions = Array.isArray(field.extraOptions) ? field.extraOptions : []; + const hasCurrentValue = + String(value || "").trim() !== "" && + [...extraOptions, ...options].some((option) => String(option?.value || "") === String(value)); return ( @@ -1965,7 +2126,7 @@ export function RequestWorkspace({
- {String(item?.to_status_name || statusMeta?.name || statusLabel(statusCode) || statusCode || "-")} + {resolveStatusDisplayName(statusCode, item?.to_status_name || statusMeta?.name || "")} {statusMeta?.isTerminal ? Терминальный : null}
{fmtShortDateTime(item?.changed_at)}
@@ -1975,7 +2136,7 @@ export function RequestWorkspace({
{item?.from_status ? (
- {"Из: " + statusLabel(item.from_status)} + {"Из: " + resolveStatusDisplayName(item.from_status, "")}
) : null} {String(item?.comment || "").trim() ? ( @@ -2005,7 +2166,7 @@ export function RequestWorkspace({
setFinanceOpen(false)} + onClick={closeFinanceModal} aria-hidden={financeOpen ? "false" : "true"} >
event.stopPropagation()}> @@ -2016,7 +2177,7 @@ export function RequestWorkspace({ {row?.track_number ? "Заявка " + String(row.track_number) : "Данные по заявке"}

-
@@ -2040,6 +2201,112 @@ export function RequestWorkspace({ ) : null} + {typeof onIssueInvoice === "function" ? ( +
+ {!financeIssueForm.open ? ( + + ) : ( +
+
+
+ + setFinanceIssueForm((prev) => ({ ...prev, amount: event.target.value, error: "" }))} + disabled={financeIssueForm.saving || loading} + placeholder="0.00" + /> +
+
+ + + setFinanceIssueForm((prev) => ({ ...prev, payerDisplayName: event.target.value, error: "" })) + } + disabled={financeIssueForm.saving || loading} + placeholder="ФИО / компания" + /> +
+
+
+ + + setFinanceIssueForm((prev) => ({ ...prev, serviceDescription: event.target.value, error: "" })) + } + disabled={financeIssueForm.saving || loading} + placeholder="Юридические услуги" + /> +
+ {financeIssueForm.error ?
{financeIssueForm.error}
: null} +
+ + +
+
+ )} +
+ ) : null} +
+
+

Счета

+ {safeInvoices.length ? String(safeInvoices.length) + " шт." : "Нет выставленных счетов"} +
+ {safeInvoices.length ? ( +
+ {safeInvoices.map((item) => ( +
+
+
+ {String(item?.invoice_number || "-")} +
+
+ {invoiceStatusLabel(item?.status)} + {fmtAmount(item?.amount) + " " + String(item?.currency || "RUB")} + {"Создан: " + fmtDate(item?.issued_at)} + {"Оплачен: " + fmtDate(item?.paid_at)} +
+
+ {typeof onDownloadInvoicePdf === "function" ? ( + + ) : null} +
+ ))} +
+ ) : ( +

Счета по заявке пока не выставлялись

+ )} +
{String(row?.topic_name || row?.topic_code || "Тема не указана")}

- {statusLabel(row?.status_code)} + {currentStatusName}