d54f6fbe42
Co-authored-by: Cursor <cursoragent@cursor.com>
1229 lines
79 KiB
HTML
1229 lines
79 KiB
HTML
{% load common_tags %}
|
||
{% load i18n %}
|
||
{% trans "адрес не указан" as no_addr_short %}{% trans "Должность не указана" as no_post %}{% trans "Адрес неизвестен" as no_addr %}
|
||
<div class="page-content">
|
||
|
||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.5rem;">
|
||
<div>
|
||
<h1 style="font-size:1.25rem; font-weight:500; color:var(--color-foreground);">{% trans "Дислокация" %}</h1>
|
||
<p style="font-size:0.875rem; color:var(--color-muted-foreground); margin-top:0.25rem;">{{ pharmacies|length }} {% trans "аптек" %} · {{ office_employees_count }} {% trans "сотрудников офиса/склада" %} · {{ vendors|length }} {% trans "сервисов/поставщиков" %}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Вкладки -->
|
||
<div style="display:flex; gap:0.25rem; background:var(--color-muted); border-radius:var(--radius); padding:0.25rem; width:fit-content; margin-bottom:1.5rem;">
|
||
<button type="button" class="dislo-tab-btn{% if dislo_active_tab == 'pharmacy' or not dislo_active_tab %} dislo-tab-active{% endif %}" data-tab="pharmacy" onclick="disloTabSwitch('pharmacy')"
|
||
style="padding:0.375rem 1rem; font-size:0.8125rem; font-weight:500; border:none; border-radius:calc(var(--radius) - 2px); cursor:pointer; {% if dislo_active_tab == 'pharmacy' or not dislo_active_tab %}background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %}background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Аптеки" %}</button>
|
||
<button type="button" class="dislo-tab-btn{% if dislo_active_tab == 'office' %} dislo-tab-active{% endif %}" data-tab="office" onclick="disloTabSwitch('office')"
|
||
style="padding:0.375rem 1rem; font-size:0.8125rem; font-weight:500; border:none; border-radius:calc(var(--radius) - 2px); cursor:pointer; {% if dislo_active_tab == 'office' %}background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %}background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Офис / Склад" %}</button>
|
||
<button type="button" class="dislo-tab-btn{% if dislo_active_tab == 'vendors' %} dislo-tab-active{% endif %}" data-tab="vendors" onclick="disloTabSwitch('vendors')"
|
||
style="padding:0.375rem 1rem; font-size:0.8125rem; font-weight:500; border:none; border-radius:calc(var(--radius) - 2px); cursor:pointer; {% if dislo_active_tab == 'vendors' %}background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %}background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Сервисы / поставщики" %}</button>
|
||
</div>
|
||
|
||
<!-- ══ ВКЛАДКА: АПТЕКИ ══ -->
|
||
<div id="dislo-panel-pharmacy" class="dislo-panel"{% if dislo_active_tab and dislo_active_tab != 'pharmacy' %} style="display:none;"{% endif %}>
|
||
|
||
<!-- Метрики -->
|
||
<div style="display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:0.75rem; margin-bottom:1.5rem;">
|
||
<div class="stat-card" style="padding:1rem 1.25rem;">
|
||
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Всего аптек" %}</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ pharmacies|length }}</div>
|
||
<div style="font-size:0.75rem; color:var(--color-success); margin-top:0.25rem;">{{ active_count }} {% trans "активных" %}</div>
|
||
</div>
|
||
<div class="stat-card" style="padding:1rem 1.25rem;">
|
||
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Организаций" %}</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ organizations_count }}</div>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.25rem;">{% trans "юридических лиц" %}</div>
|
||
</div>
|
||
<div class="stat-card" style="padding:1rem 1.25rem;">
|
||
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Без заведующего" %}</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ pharmacies_no_head }}</div>
|
||
{% if pharmacies_no_head %}
|
||
<div style="font-size:0.75rem; color:var(--color-warning); margin-top:0.25rem;">{% trans "требуют назначения" %}</div>
|
||
{% else %}
|
||
<div style="font-size:0.75rem; color:var(--color-success); margin-top:0.25rem;">{% trans "все аптеки с заведующим" %}</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="stat-card" style="padding:1rem 1.25rem;">
|
||
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Без персонала" %}</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ pharmacies_empty }}</div>
|
||
{% if pharmacies_empty %}
|
||
<div style="font-size:0.75rem; color:var(--color-destructive); margin-top:0.25rem;">{% trans "аптек без сотрудников" %}</div>
|
||
{% else %}
|
||
<div style="font-size:0.75rem; color:var(--color-success); margin-top:0.25rem;">{% trans "все укомплектованы" %}</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Фільтри + переключатель вида -->
|
||
{% regroup pharmacies by organization.name as enterprise_list %}
|
||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1rem; gap:1rem; flex-wrap:wrap;">
|
||
<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap;">
|
||
<div style="display:flex; align-items:center; gap:0.5rem; background:var(--color-muted); border:1px solid var(--color-border); border-radius:var(--radius); padding:0.375rem 0.75rem; width:14rem;">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--color-muted-foreground); flex-shrink:0;">
|
||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
||
</svg>
|
||
<input type="text" id="dislo-search" placeholder="{% trans 'Поиск аптеки...' %}" oninput="disloFilter()"
|
||
class="input" style="border:none; background:transparent; padding:0; box-shadow:none; font-size:0.8125rem;">
|
||
</div>
|
||
<select class="input" id="dislo-filter-enterprise" onchange="disloEnterpriseChange()" style="width:auto; font-size:0.8125rem; padding:0.375rem 0.625rem; max-width:15rem;">
|
||
<option value="">{% trans "Все предприятия" %}</option>
|
||
{% for enterprise in enterprise_list %}
|
||
<option value="{{ enterprise.grouper|escape }}">{{ enterprise.grouper }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<div class="filter-tree" style="position:relative; min-width:14rem;">
|
||
<button id="dislo-filter-region-button" type="button" class="input" onclick="disloToggleRegionTree()"
|
||
style="width:100%; text-align:left; font-size:0.78rem; padding:0.3rem 0.55rem; display:flex; justify-content:space-between; align-items:center; gap:0.4rem; border:1px solid var(--color-border); background:var(--color-muted); color:var(--color-foreground);">
|
||
<span id="dislo-filter-region-label">{% trans "Все города/регионы" %}</span>
|
||
<span style="font-size:0.75rem; color:var(--color-muted-foreground);">▾</span>
|
||
</button>
|
||
<div id="dislo-region-tree-panel" style="position:absolute; top:100%; left:0; right:0; z-index:20; max-height:240px; overflow:auto; background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; margin-top:0.25rem; box-shadow:0 12px 24px rgb(0 0 0 / 8%); display:none;">
|
||
<div style="padding:0.55rem 0.65rem; cursor:pointer; border-bottom:1px solid rgba(0,0,0,.06); font-size:0.8rem;"
|
||
onclick="disloSelectRegionNode('', "{% trans 'Все города/регионы' %}")">
|
||
{% trans "Все города/регионы" %}
|
||
</div>
|
||
<div id="dislo-region-tree-root"></div>
|
||
</div>
|
||
<input type="hidden" id="dislo-filter-region" value="">
|
||
</div>
|
||
<select class="input" id="dislo-filter-status" onchange="disloFilter()" style="width:auto; font-size:0.8125rem; padding:0.375rem 0.625rem;">
|
||
<option value="">{% trans "Все статусы" %}</option>
|
||
<option value="active" selected>{% trans "Активные" %}</option>
|
||
<option value="inactive">{% trans "Неактивные" %}</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex; gap:0.5rem; flex-shrink:0; align-items:center;">
|
||
<button type="button" id="dislo-btn-columns" onclick="disloToggleColumnsPanel()"
|
||
class="btn btn-secondary"
|
||
style="display:flex;align-items:center;gap:0.375rem;padding:0.3125rem 0.75rem;font-size:0.8125rem;font-weight:500;border:1px solid var(--color-border);background:var(--color-card);color:var(--color-foreground);box-shadow:0 1px 1px rgb(0 0 0/0.04);">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h10M4 18h14"/><circle cx="18" cy="12" r="2"/></svg>
|
||
{% trans "Колонки" %}
|
||
</button>
|
||
<div style="display:flex; gap:2px; background:var(--color-muted); border-radius:var(--radius); padding:3px; align-items:center;">
|
||
<button id="dislo-btn-table" onclick="disloView('table')"
|
||
style="display:flex;align-items:center;gap:0.375rem;padding:0.3125rem 0.75rem;border-radius:calc(var(--radius) - 2px);font-size:0.8125rem;font-weight:500;border:none;cursor:pointer;transition:background 150ms;background:var(--color-card);color:var(--color-foreground);">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3h18v18H3zM3 9h18M3 15h18M9 3v18"/></svg>
|
||
{% trans "Таблица" %}
|
||
</button>
|
||
<button id="dislo-btn-map" onclick="disloView('map')"
|
||
style="display:flex;align-items:center;gap:0.375rem;padding:0.3125rem 0.75rem;border-radius:calc(var(--radius) - 2px);font-size:0.8125rem;font-weight:500;border:none;cursor:pointer;transition:background 150ms;background:transparent;color:var(--color-muted-foreground);">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"/><path d="M15 5.764v15"/><path d="M9 3.236v15"/></svg>
|
||
{% trans "Карта" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="dislo-columns-panel" style="display:none; margin-bottom:1rem; background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:1rem 1.25rem; box-shadow:0 1px 2px rgb(0 0 0/0.05);">
|
||
<div style="display:flex; align-items:center; justify-content:space-between; gap:0.75rem; flex-wrap:wrap; margin-bottom:0.75rem;">
|
||
<div style="font-size:0.875rem; font-weight:600; color:var(--color-foreground);">{% trans "Колонки таблицы" %}</div>
|
||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||
<label for="dislo-preset-select" style="font-size:0.75rem; color:var(--color-muted-foreground);">{% trans "Пресет" %}</label>
|
||
<select id="dislo-preset-select" class="input" style="font-size:0.8125rem; padding:0.3rem 0.5rem; height:auto;" onchange="disloApplyPreset(this)">
|
||
<option value="">{% trans "— свой набор —" %}</option>
|
||
{% for preset in dislocation_presets %}
|
||
<option value="{{ preset.code }}" data-columns="{{ preset.columns|join:',' }}">{{ preset.name }}{% if preset.is_default %} ★{% endif %}{% if preset.department %} · {{ preset.department.name }}{% endif %}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<form id="dislo-columns-form" hx-post="/dislocation/columns/" hx-target="#main-content" hx-indicator="#loader" hx-disabled-elt="find button[type=submit]">
|
||
{% csrf_token %}
|
||
<div id="dislo-col-order" style="display:flex; flex-direction:column; gap:0.35rem; margin-bottom:0.75rem;">
|
||
{% for col in panel_column_defs %}
|
||
<div class="dislo-col-row" data-key="{{ col.key }}" style="display:flex; align-items:center; gap:0.5rem; font-size:0.8125rem;">
|
||
{% if col.locked %}
|
||
<input type="checkbox" checked disabled>
|
||
<input type="hidden" name="columns" value="{{ col.key }}">
|
||
{% else %}
|
||
<input type="checkbox" name="columns" value="{{ col.key }}" {% if col.key in visible_column_keys %}checked{% endif %}>
|
||
{% endif %}
|
||
<span style="flex:1; color:var(--color-foreground);">{{ col.label }}{% if col.locked %} <span style="color:var(--color-muted-foreground); font-size:0.7rem;">({% trans "обяз." %})</span>{% endif %}</span>
|
||
<button type="button" class="btn btn-ghost" style="padding:0.15rem 0.4rem; font-size:0.7rem;" onclick="disloColMove(this, -1)" title="{% trans 'Выше' %}">▲</button>
|
||
<button type="button" class="btn btn-ghost" style="padding:0.15rem 0.4rem; font-size:0.7rem;" onclick="disloColMove(this, 1)" title="{% trans 'Ниже' %}">▼</button>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
|
||
<button type="submit" class="btn btn-primary" style="font-size:0.8125rem;">{% trans "Сохранить для себя" %}</button>
|
||
<button type="button" class="btn btn-secondary" style="font-size:0.8125rem; border:1px solid var(--color-border);" onclick="disloToggleColumnsPanel()">{% trans "Отмена" %}</button>
|
||
</div>
|
||
</form>
|
||
|
||
{% if can_manage_dislo_presets %}
|
||
<div style="margin-top:1rem; padding-top:1rem; border-top:1px solid var(--color-border);">
|
||
<div style="font-size:0.8125rem; font-weight:600; color:var(--color-foreground); margin-bottom:0.65rem;">{% trans "Пресеты (админ)" %}</div>
|
||
<div style="display:grid; gap:0.55rem; grid-template-columns:repeat(auto-fit, minmax(11rem, 1fr)); margin-bottom:0.65rem;">
|
||
<div>
|
||
<label for="dislo-preset-name" style="display:block; font-size:0.7rem; color:var(--color-muted-foreground); margin-bottom:0.2rem;">{% trans "Название" %}</label>
|
||
<input type="text" id="dislo-preset-name" class="input" style="width:100%; font-size:0.8125rem; padding:0.35rem 0.5rem; height:auto;" placeholder="{% trans 'Например: IT' %}">
|
||
</div>
|
||
<div>
|
||
<label for="dislo-preset-code" style="display:block; font-size:0.7rem; color:var(--color-muted-foreground); margin-bottom:0.2rem;">{% trans "Код" %}</label>
|
||
<input type="text" id="dislo-preset-code" class="input" style="width:100%; font-size:0.8125rem; padding:0.35rem 0.5rem; height:auto;" placeholder="{% trans 'it (необяз.)' %}">
|
||
</div>
|
||
<div>
|
||
<label for="dislo-preset-department" style="display:block; font-size:0.7rem; color:var(--color-muted-foreground); margin-bottom:0.2rem;">{% trans "Отдел" %}</label>
|
||
<select id="dislo-preset-department" class="input" style="width:100%; font-size:0.8125rem; padding:0.35rem 0.5rem; height:auto;">
|
||
<option value="">{% trans "— без отдела —" %}</option>
|
||
{% for dept in dislocation_departments %}
|
||
<option value="{{ dept.id }}">{{ dept.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<label style="display:inline-flex; align-items:center; gap:0.4rem; font-size:0.8125rem; color:var(--color-foreground); margin-bottom:0.65rem;">
|
||
<input type="checkbox" id="dislo-preset-default">
|
||
{% trans "Пресет по умолчанию" %}
|
||
</label>
|
||
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
|
||
<button type="button" class="btn btn-primary" style="font-size:0.8125rem;" onclick="disloPresetAction('create')">{% trans "Создать пресет" %}</button>
|
||
<button type="button" class="btn btn-secondary" style="font-size:0.8125rem; border:1px solid var(--color-border);" onclick="disloPresetAction('update')">{% trans "Обновить выбранный" %}</button>
|
||
<button type="button" class="btn btn-secondary" style="font-size:0.8125rem; border:1px solid color-mix(in srgb, #ef4444 35%, var(--color-border)); color:#b91c1c;" onclick="disloPresetAction('delete')">{% trans "Удалить выбранный" %}</button>
|
||
</div>
|
||
<p style="margin:0.5rem 0 0; font-size:0.7rem; color:var(--color-muted-foreground);">{% trans "Создание и обновление берут отмеченные колонки выше. Обновление/удаление — по выбранному пресету в списке." %}</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{{ dislocation_presets_payload|json_script:"dislo-presets-data" }}
|
||
</div>
|
||
|
||
<!-- Таблица -->
|
||
<div id="dislo-view-table">
|
||
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0/0.05);">
|
||
<div style="overflow-x:auto;">
|
||
<table style="width:100%; border-collapse:collapse; font-size:0.875rem;">
|
||
<thead>
|
||
<tr style="border-bottom:1px solid var(--color-border);">
|
||
<th style="width:2rem;"></th>
|
||
{% for col in visible_columns %}
|
||
<th class="dislo-sort-th" onclick="disloSortBy('{{ col.key }}','{{ col.sort_type }}')" style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;cursor:pointer;user-select:none;white-space:nowrap;">{{ col.label }} <span class="dislo-sort-icon" data-key="{{ col.key }}" style="opacity:0.35;">⇅</span></th>
|
||
{% endfor %}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for pharmacy in pharmacies %}
|
||
<tr class="dislo-row" style="border-bottom:1px solid var(--color-border); cursor:pointer;"
|
||
onclick="disloToggleStaff({{ pharmacy.id }})"
|
||
data-pharmacy-id="{{ pharmacy.id }}"
|
||
{% if pharmacy.location_region %}
|
||
data-region="{{ pharmacy.location_region.name }}"
|
||
{% else %}
|
||
data-region=""
|
||
{% endif %}
|
||
data-region-ids="{% if pharmacy.location_region %}{% for ancestor in pharmacy.location_region.get_ancestors %}{{ ancestor.id }},{% endfor %}{{ pharmacy.location_region.id }}{% endif %}"
|
||
data-status="{% if pharmacy.is_active %}active{% else %}inactive{% endif %}"
|
||
data-enterprise="{{ pharmacy.organization.name|escape }}"
|
||
data-name="{{ pharmacy.organization.name }} #{{ pharmacy.pharmacy_number }}"
|
||
data-sort-pharmacy="{{ pharmacy.pharmacy_number|default:0 }}"
|
||
data-sort-organization="{{ pharmacy.organization.name|default:""|escape }}"
|
||
data-sort-head="{% if pharmacy.head_pharmacist %}{{ pharmacy.head_pharmacist.last_name }} {{ pharmacy.head_pharmacist.first_name }}{% endif %}"
|
||
data-sort-staff="{{ pharmacy.staff_count|default:0 }}"
|
||
data-sort-phone="{{ pharmacy.phone|default:"" }}"
|
||
data-sort-hours="{{ pharmacy.working_hours|default:"" }}"
|
||
data-sort-status="{% if pharmacy.is_active %}1{% else %}0{% endif %}"
|
||
data-sort-region="{{ pharmacy.location_region.name|default:""|escape }}"
|
||
data-sort-category="{{ pharmacy.category|default:"" }}"
|
||
data-sort-curator="{% if pharmacy.curator %}{{ pharmacy.curator.last_name }} {{ pharmacy.curator.first_name }}{% endif %}"
|
||
data-sort-manager="{% if pharmacy.responsible_manager %}{{ pharmacy.responsible_manager.last_name }} {{ pharmacy.responsible_manager.first_name }}{% endif %}"
|
||
data-sort-license="{{ pharmacy.license_number|default:"" }}"
|
||
data-sort-vat="{% if pharmacy.vat_category %}{{ pharmacy.get_vat_category_display }}{% endif %}"
|
||
data-sort-email="{{ pharmacy.carbonio_email|default:"" }}">
|
||
<td style="padding:0.75rem 0; text-align:center; color:var(--color-muted-foreground);">
|
||
<span id="dislo-toggle-{{ pharmacy.id }}" style="display:inline-block; transition:transform 150ms;">▸</span>
|
||
</td>
|
||
{% for col in visible_columns %}
|
||
{% include "partials/dislo_cell.html" with column=col pharmacy=pharmacy no_addr_short=no_addr_short %}
|
||
{% endfor %}
|
||
</tr>
|
||
<tr id="dislo-staff-{{ pharmacy.id }}" class="dislo-staff-row" style="display:none; border-bottom:1px solid var(--color-border); background:var(--color-muted);">
|
||
<td></td>
|
||
<td colspan="{{ visible_columns|length }}" style="padding:0.75rem 1rem 1rem;">
|
||
{% if pharmacy.active_staff %}
|
||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(15rem, 1fr)); gap:0.5rem;">
|
||
{% for employee in pharmacy.active_staff %}
|
||
<div class="li-hover" style="display:flex; align-items:center; gap:0.5rem; padding:0.5rem 0.625rem; background:var(--color-card); border:1px solid var(--color-border); border-radius:0.5rem; cursor:pointer;"
|
||
hx-get="/employees/{{ employee.pk }}/" hx-target="#main-content" hx-push-url="/employees/{{ employee.pk }}/" hx-indicator="#loader"
|
||
onclick="event.stopPropagation();">
|
||
{% if employee.photo %}
|
||
<img src="{{ employee.photo.url }}" alt="" style="width:1.75rem; height:1.75rem; border-radius:50%; object-fit:cover; flex-shrink:0;">
|
||
{% else %}
|
||
<span style="width:1.75rem; height:1.75rem; border-radius:50%; background:color-mix(in srgb, var(--color-primary) 10%, transparent); color:var(--color-primary); display:flex; align-items:center; justify-content:center; font-size:0.6875rem; font-weight:500; flex-shrink:0;">{{ employee|initials }}</span>
|
||
{% endif %}
|
||
<div style="min-width:0;">
|
||
<div style="font-size:0.8125rem; font-weight:500; color:var(--color-card-foreground); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ employee.last_name }} {{ employee.first_name }}</div>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ employee.post.name|default:no_post }}{% if employee.phone %} · {{ employee.phone }}{% endif %}</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div style="padding:0.5rem 0; color:var(--color-muted-foreground); font-size:0.8125rem;">{% trans "В этой аптеке пока нет активных сотрудников" %}</div>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr>
|
||
<td colspan="{{ dislo_colspan }}" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Аптеки не найдены" %}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div style="padding:0.875rem 1.5rem;border-top:1px solid var(--color-border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.75rem;">
|
||
<span id="dislo-page-info" style="font-size:0.8125rem;color:var(--color-muted-foreground);">{% trans "Показано 0–0 из 0" %}</span>
|
||
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
|
||
<div style="display:flex;align-items:center;gap:0.375rem;">
|
||
<span style="font-size:0.8125rem;color:var(--color-muted-foreground);white-space:nowrap;">{% trans "На странице:" %}</span>
|
||
<select id="dislo-page-size" class="input" style="font-size:0.8125rem;padding:0.3rem 0.5rem;height:auto;width:auto;" onchange="disloSetPageSize(this.value)">
|
||
<option value="10">10</option>
|
||
<option value="15">15</option>
|
||
<option value="25" selected>25</option>
|
||
<option value="100">100</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:0.375rem;">
|
||
<button type="button" id="dislo-prev-btn" class="btn btn-secondary" style="padding:0.375rem 0.75rem;font-size:0.8125rem;" onclick="disloGoToPage(disloPage - 1)">← {% trans "Предыдущая" %}</button>
|
||
<span id="dislo-page-label" style="font-size:0.8125rem;color:var(--color-muted-foreground);white-space:nowrap;">{% trans "Стр. 1 из 1" %}</span>
|
||
<button type="button" id="dislo-next-btn" class="btn btn-secondary" style="padding:0.375rem 0.75rem;font-size:0.8125rem;" onclick="disloGoToPage(disloPage + 1)">{% trans "Следующая" %} →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Карта -->
|
||
<div id="dislo-view-map" style="display:none;">
|
||
{% if pharmacies_no_location %}
|
||
<div style="font-size:0.8125rem; color:var(--color-warning); margin-bottom:0.75rem;">
|
||
⚠ {{ pharmacies_no_location }} {% trans "апт. без указанного адреса не показаны на карте (нет координат)" %}
|
||
</div>
|
||
{% endif %}
|
||
<div style="display:grid;grid-template-columns:1fr 300px;gap:1rem;">
|
||
<div style="background:var(--color-card);border-radius:0.75rem;border:1px solid var(--color-border);overflow:hidden;">
|
||
<div id="dislo-map" style="height:520px;width:100%;"></div>
|
||
</div>
|
||
<div style="background:var(--color-card);border-radius:0.75rem;border:1px solid var(--color-border);max-height:520px;overflow-y:auto;">
|
||
<div style="padding:0.875rem 1rem;border-bottom:1px solid var(--color-border);font-size:0.8125rem;font-weight:600;color:var(--color-foreground);position:sticky;top:0;background:var(--color-card);z-index:1;">
|
||
{% trans "Список аптек" %}
|
||
</div>
|
||
<div style="padding:0.75rem 0.75rem;">
|
||
{% for pharmacy in pharmacies %}
|
||
<div style="padding:0.625rem 0.75rem;border-bottom:1px solid var(--color-border);cursor:pointer;transition:background 150ms;opacity:1;"
|
||
onmouseover="this.style.background='var(--color-muted)'"
|
||
onmouseout="this.style.background='transparent';"
|
||
data-pharmacy-id="{{ pharmacy.id }}"
|
||
{% if pharmacy.location_region %}
|
||
data-region="{{ pharmacy.location_region.name }}"
|
||
{% else %}
|
||
data-region=""
|
||
{% endif %}
|
||
data-region-ids="{% if pharmacy.location_region %}{% for ancestor in pharmacy.location_region.get_ancestors %}{{ ancestor.id }},{% endfor %}{{ pharmacy.location_region.id }}{% endif %}"
|
||
data-status="{% if pharmacy.is_active %}active{% else %}inactive{% endif %}"
|
||
data-enterprise="{{ pharmacy.organization.name|escape }}"
|
||
data-name="{{ pharmacy.organization.name }} #{{ pharmacy.pharmacy_number }}">
|
||
<div style="font-size:0.8125rem;font-weight:600;color:var(--color-foreground);">{{ pharmacy.organization.name }} #{{ pharmacy.pharmacy_number }}</div>
|
||
<div style="font-size:0.75rem;color:var(--color-muted-foreground);margin-top:0.25rem;">{{ pharmacy.address }}</div>
|
||
{% if pharmacy.has_generator or pharmacy.has_inverter or pharmacy.has_mikrotik_router or pharmacy.dislo_provider or pharmacy.has_gpon %}
|
||
<div style="font-size:0.7rem;color:var(--color-muted-foreground);margin-top:0.35rem;line-height:1.4;">
|
||
{% if pharmacy.has_generator %}<div>{% trans "Генератор" %}: {% trans "да" %}</div>{% endif %}
|
||
{% if pharmacy.has_inverter %}<div>{% trans "Инвертор" %}: {% trans "да" %}</div>{% endif %}
|
||
{% if pharmacy.has_mikrotik_router %}<div>{% trans "MikroTik" %}: {% trans "да" %}</div>{% endif %}
|
||
{% if pharmacy.dislo_provider %}<div>{% trans "Провайдер" %}: {{ pharmacy.dislo_provider }}</div>{% endif %}
|
||
{% if pharmacy.dislo_contract %}<div>{% trans "Договор" %}: {{ pharmacy.dislo_contract }}</div>{% endif %}
|
||
{% if pharmacy.has_gpon %}<div>{% trans "GPON" %}: {% trans "да" %}</div>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: АПТЕКИ ══ -->
|
||
|
||
<!-- ══ ВКЛАДКА: ОФІС / СКЛАД ══ -->
|
||
<div id="dislo-panel-office" class="dislo-panel"{% if dislo_active_tab != 'office' %} style="display:none;"{% endif %}>
|
||
|
||
<div style="margin-bottom:1rem;">
|
||
<span style="font-size:0.875rem; color:var(--color-muted-foreground);">{{ office_employees_count }} {% trans "сотрудников офиса и склада, сгруппировано по отделам" %}</span>
|
||
</div>
|
||
|
||
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0/0.05); overflow:hidden;">
|
||
<div style="overflow-x:auto;">
|
||
<table style="width:100%; border-collapse:collapse; font-size:0.875rem;">
|
||
<thead>
|
||
<tr style="border-bottom:1px solid var(--color-border);">
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem; width:8rem;">{% trans "Внутр. номер" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Должность" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Ф.И.О." %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Корпоративный телефон" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for dept in office_departments %}
|
||
<tr>
|
||
<td colspan="4" style="padding:0.5rem 1rem; background:color-mix(in srgb, var(--color-success) 18%, var(--color-card)); font-weight:600; color:var(--color-foreground); padding-left:{% widthratio dept.indent_depth 1 1 %}.5rem;">{{ dept.name }}</td>
|
||
</tr>
|
||
{% for employee in dept.staff %}
|
||
<tr class="tr-hover" style="border-bottom:1px solid var(--color-border); cursor:pointer;"
|
||
hx-get="/employees/{{ employee.pk }}/" hx-target="#main-content" hx-push-url="/employees/{{ employee.pk }}/" hx-indicator="#loader">
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground);">{{ employee.internal_number|default:"—" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground);">{{ employee.post.name|default:"—" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-card-foreground); font-weight:500;">{{ employee.last_name }} {{ employee.first_name }} {{ employee.middle_name }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground);">{{ employee.phone|default:"—" }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
{% endfor %}
|
||
|
||
{% if office_no_department %}
|
||
<tr>
|
||
<td colspan="4" style="padding:0.5rem 1rem; background:color-mix(in srgb, var(--color-warning) 18%, var(--color-card)); font-weight:600; color:var(--color-foreground);">{% trans "Без отдела" %}</td>
|
||
</tr>
|
||
{% for employee in office_no_department %}
|
||
<tr class="tr-hover" style="border-bottom:1px solid var(--color-border); cursor:pointer;"
|
||
hx-get="/employees/{{ employee.pk }}/" hx-target="#main-content" hx-push-url="/employees/{{ employee.pk }}/" hx-indicator="#loader">
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground);">{{ employee.internal_number|default:"—" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground);">{{ employee.post.name|default:"—" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-card-foreground); font-weight:500;">{{ employee.last_name }} {{ employee.first_name }} {{ employee.middle_name }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground);">{{ employee.phone|default:"—" }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
{% if not office_departments and not office_no_department %}
|
||
<tr>
|
||
<td colspan="4" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Сотрудников офиса/склада не найдено" %}</td>
|
||
</tr>
|
||
{% endif %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: ОФІС / СКЛАД ══ -->
|
||
|
||
<!-- ══ ВКЛАДКА: СЕРВИСЫ / ПОСТАВЩИКИ ══ -->
|
||
<div id="dislo-panel-vendors" class="dislo-panel"{% if dislo_active_tab != 'vendors' %} style="display:none;"{% endif %}>
|
||
|
||
<div style="display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; margin-bottom:1rem;">
|
||
<div>
|
||
<span style="font-size:0.875rem; color:var(--color-muted-foreground);">
|
||
{{ vendors_active_count }} {% trans "активных" %} · {{ vendors|length }} {% trans "всего" %}
|
||
— {% trans "для работ, отчётов и выдачи оборудования" %}
|
||
</span>
|
||
</div>
|
||
{% if can_manage_vendors %}
|
||
<button type="button" class="btn btn-secondary" onclick="disloVendorPanelOpen()"
|
||
style="display:flex;align-items:center;gap:0.375rem;padding:0.3125rem 0.75rem;font-size:0.8125rem;">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
|
||
{% trans "Добавить" %}
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if vendor_form_message %}
|
||
<div style="margin-bottom:0.75rem; padding:0.625rem 0.875rem; border-radius:var(--radius); background:color-mix(in srgb, var(--color-success) 14%, var(--color-card)); color:var(--color-foreground); font-size:0.8125rem;">{{ vendor_form_message }}</div>
|
||
{% endif %}
|
||
{% if vendor_form_error %}
|
||
<div style="margin-bottom:0.75rem; padding:0.625rem 0.875rem; border-radius:var(--radius); background:color-mix(in srgb, var(--color-destructive) 12%, var(--color-card)); color:var(--color-foreground); font-size:0.8125rem;">{{ vendor_form_error }}</div>
|
||
{% endif %}
|
||
|
||
<div style="display:flex; align-items:center; gap:0.5rem; margin-bottom:0.75rem; flex-wrap:wrap;">
|
||
<div style="display:flex; align-items:center; gap:0.5rem; background:var(--color-muted); border:1px solid var(--color-border); border-radius:var(--radius); padding:0.375rem 0.75rem; width:16rem;">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--color-muted-foreground); flex-shrink:0;">
|
||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
||
</svg>
|
||
<input type="text" id="dislo-vendor-search" placeholder="{% trans 'Поиск сервиса…' %}" oninput="disloVendorFilter()"
|
||
class="input" style="border:none; background:transparent; padding:0; box-shadow:none; font-size:0.8125rem;">
|
||
</div>
|
||
<select class="input" id="dislo-vendor-filter-status" onchange="disloVendorFilter()" style="width:auto; font-size:0.8125rem; padding:0.375rem 0.625rem;">
|
||
<option value="">{% trans "Все статусы" %}</option>
|
||
<option value="active" selected>{% trans "Активные" %}</option>
|
||
<option value="inactive">{% trans "Неактивные" %}</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0/0.05); overflow:hidden;">
|
||
<div style="overflow-x:auto;">
|
||
<table style="width:100%; border-collapse:collapse; font-size:0.875rem;">
|
||
<thead>
|
||
<tr style="border-bottom:1px solid var(--color-border);">
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Название" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Город/район" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Адрес" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Комментарий" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem; width:6rem;">{% trans "Статус" %}</th>
|
||
{% if can_manage_vendors %}
|
||
<th style="padding:0.75rem 1rem;text-align:right;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem; width:5rem;"></th>
|
||
{% endif %}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for vendor in vendors %}
|
||
<tr class="tr-hover dislo-vendor-row" style="border-bottom:1px solid var(--color-border); cursor:pointer;"
|
||
data-name="{{ vendor.name|default:''|escape }}"
|
||
data-status="{% if vendor.is_active %}active{% else %}inactive{% endif %}"
|
||
hx-get="/dislocation/vendors/{{ vendor.pk }}/"
|
||
hx-target="#main-content"
|
||
hx-push-url="/dislocation/vendors/{{ vendor.pk }}/"
|
||
hx-indicator="#loader">
|
||
<td style="padding:0.625rem 1rem; color:var(--color-card-foreground); font-weight:500;">{{ vendor.name|default:"—" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground);">{{ vendor.location_region.name|default:"—" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground);">
|
||
{% if vendor.street or vendor.house_number %}
|
||
{% if vendor.street %}ул. {{ vendor.street }}{% endif %}{% if vendor.house_number %} {{ vendor.house_number }}{% endif %}{% if vendor.building %}, корп. {{ vendor.building }}{% endif %}
|
||
{% else %}
|
||
—
|
||
{% endif %}
|
||
</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground); max-width:16rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="{{ vendor.comments|default:''|escape }}">{{ vendor.comments|default:"—" }}</td>
|
||
<td style="padding:0.625rem 1rem;">
|
||
{% if vendor.is_active %}
|
||
<span class="badge badge-success">{% trans "Активен" %}</span>
|
||
{% else %}
|
||
<span class="badge badge-secondary">{% trans "Неактивен" %}</span>
|
||
{% endif %}
|
||
</td>
|
||
{% if can_manage_vendors %}
|
||
<td style="padding:0.625rem 1rem; text-align:right;" onclick="event.stopPropagation();">
|
||
<button type="button" class="btn btn-ghost" style="font-size:0.75rem; padding:0.25rem 0.5rem;"
|
||
onclick="event.stopPropagation(); disloVendorEdit({{ vendor.pk }})"
|
||
data-name="{{ vendor.name|escapejs }}"
|
||
data-region="{{ vendor.location_region_id|default:'' }}"
|
||
data-street="{{ vendor.street|escapejs }}"
|
||
data-house="{{ vendor.house_number|escapejs }}"
|
||
data-building="{{ vendor.building|escapejs }}"
|
||
data-apartment="{{ vendor.apartment|escapejs }}"
|
||
data-location="{{ vendor.location|escapejs }}"
|
||
data-comments="{{ vendor.comments|escapejs }}"
|
||
data-active="{% if vendor.is_active %}1{% else %}0{% endif %}">{% trans "Изменить" %}</button>
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% empty %}
|
||
<tr>
|
||
<td colspan="{% if can_manage_vendors %}6{% else %}5{% endif %}" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Сервисов и поставщиков пока нет" %}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{% if can_manage_vendors and vendor_form %}
|
||
<div id="dislo-vendor-panel-overlay" onclick="disloVendorPanelClose()" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:1000;"></div>
|
||
<div id="dislo-vendor-panel" style="position:fixed; top:0; right:0; height:100vh; width:26rem; max-width:92vw; background:var(--color-card); box-shadow:-8px 0 24px rgba(0,0,0,0.18); z-index:1001; transform:translateX(100%); transition:transform 250ms ease; overflow-y:auto;">
|
||
<form id="dislo-vendor-form"
|
||
hx-post="{% if vendor_form.instance.pk %}/dislocation/vendors/{{ vendor_form.instance.pk }}/update/{% else %}/dislocation/vendors/create/{% endif %}"
|
||
hx-target="#main-content" hx-indicator="#loader" hx-disabled-elt="find button[type=submit]">
|
||
{% csrf_token %}
|
||
<div style="display:flex; align-items:center; justify-content:space-between; padding:1rem 1.25rem; border-bottom:1px solid var(--color-border);">
|
||
<h2 id="dislo-vendor-panel-title" style="font-size:1rem; font-weight:600; color:var(--color-foreground); margin:0;">{% if vendor_form.instance.pk %}{% trans "Изменить сервис / поставщик" %}{% else %}{% trans "Новый сервис / поставщик" %}{% endif %}</h2>
|
||
<button type="button" onclick="disloVendorPanelClose()" class="btn btn-ghost btn-icon" aria-label="{% trans 'Закрыть' %}">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div style="padding:1.25rem; display:flex; flex-direction:column; gap:0.875rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Название" %} <span style="color:var(--color-destructive);">*</span></label>
|
||
{{ vendor_form.name }}
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Город/район" %} <span style="color:var(--color-destructive);">*</span></label>
|
||
{{ vendor_form.location_region }}
|
||
</div>
|
||
<div style="display:grid; grid-template-columns:1fr 5rem; gap:0.5rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Улица" %}</label>
|
||
{{ vendor_form.street }}
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Дом" %}</label>
|
||
{{ vendor_form.house_number }}
|
||
</div>
|
||
</div>
|
||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.5rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Корпус" %}</label>
|
||
{{ vendor_form.building }}
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Помещение" %}</label>
|
||
{{ vendor_form.apartment }}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Координаты" %} <span style="font-weight:400; color:var(--color-muted-foreground);">lat,lng</span></label>
|
||
{{ vendor_form.location }}
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Комментарий" %}</label>
|
||
{{ vendor_form.comments }}
|
||
</div>
|
||
<label style="display:flex; align-items:center; gap:0.5rem; font-size:0.8125rem; color:var(--color-foreground); cursor:pointer;">
|
||
{{ vendor_form.is_active }}
|
||
{% trans "Активен" %}
|
||
</label>
|
||
<button type="submit" class="btn btn-primary" style="width:100%; margin-top:0.5rem;">{% trans "Сохранить" %}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: СЕРВИСЫ / ПОСТАВЩИКИ ══ -->
|
||
|
||
{{ region_tree|json_script:"dislo-region-tree-data" }}
|
||
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css">
|
||
<script src="https://cdn.jsdelivr.net/npm/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet.markercluster@1.5.3/dist/MarkerCluster.css">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css">
|
||
|
||
<script>
|
||
let disloMap = null;
|
||
let disloLayer = null;
|
||
const disloData = [
|
||
{% for pharmacy in pharmacies %}
|
||
{
|
||
id: {{ pharmacy.id }},
|
||
name: "{{ pharmacy.organization.name|escapejs|default:'' }} #{{ pharmacy.pharmacy_number|escapejs|default:'0' }}",
|
||
address: "{{ pharmacy.address|escapejs|default:no_addr }}",
|
||
head: "{{ pharmacy.head_pharmacist.first_name|escapejs|default:'' }} {{ pharmacy.head_pharmacist.last_name|escapejs|default:'' }}".trim() || "—",
|
||
phone: "{{ pharmacy.phone|escapejs|default:'-' }}",
|
||
hours: "{{ pharmacy.working_hours|escapejs|default:'-' }}",
|
||
staff: {{ pharmacy.staff_count }},
|
||
active: {{ pharmacy.is_active|lower }},
|
||
hasLocation: {{ pharmacy.has_map_location|lower }},
|
||
hasGenerator: {{ pharmacy.has_generator|yesno:"true,false" }},
|
||
hasInverter: {{ pharmacy.has_inverter|yesno:"true,false" }},
|
||
hasMikrotik: {{ pharmacy.has_mikrotik_router|yesno:"true,false" }},
|
||
provider: "{{ pharmacy.dislo_provider|escapejs|default:'' }}",
|
||
contract: "{{ pharmacy.dislo_contract|escapejs|default:'' }}",
|
||
hasGpon: {{ pharmacy.has_gpon|yesno:"true,false" }},
|
||
lat: parseFloat("{{ pharmacy.latitude|default:50.5|floatformat:'6' }}".replace(',', '.')),
|
||
lng: parseFloat("{{ pharmacy.longitude|default:26.5|floatformat:'6' }}".replace(',', '.'))
|
||
}{% if not forloop.last %},{% endif %}
|
||
{% endfor %}
|
||
];
|
||
|
||
const disloRegionTreeData = JSON.parse(document.getElementById('dislo-region-tree-data').textContent || '[]');
|
||
|
||
function disloToggleStaff(pharmacyId) {
|
||
const row = document.getElementById('dislo-staff-' + pharmacyId);
|
||
const toggle = document.getElementById('dislo-toggle-' + pharmacyId);
|
||
if (!row) return;
|
||
const opening = row.style.display === 'none';
|
||
row.style.display = opening ? 'table-row' : 'none';
|
||
if (toggle) toggle.style.transform = opening ? 'rotate(90deg)' : '';
|
||
}
|
||
|
||
function disloTabSwitch(tab) {
|
||
document.querySelectorAll('.dislo-panel').forEach(function (panel) {
|
||
panel.style.display = panel.id === 'dislo-panel-' + tab ? '' : 'none';
|
||
});
|
||
document.querySelectorAll('.dislo-tab-btn').forEach(function (btn) {
|
||
const active = btn.getAttribute('data-tab') === tab;
|
||
btn.classList.toggle('dislo-tab-active', active);
|
||
btn.style.background = active ? 'var(--color-card)' : 'transparent';
|
||
btn.style.color = active ? 'var(--color-card-foreground)' : 'var(--color-muted-foreground)';
|
||
btn.style.boxShadow = active ? '0 1px 2px rgb(0 0 0/0.08)' : '';
|
||
});
|
||
if (tab === 'pharmacy' && disloMap) {
|
||
disloMap.invalidateSize();
|
||
}
|
||
}
|
||
|
||
function disloVendorFilter() {
|
||
const q = (document.getElementById('dislo-vendor-search') || {}).value || '';
|
||
const search = q.toLowerCase();
|
||
const status = (document.getElementById('dislo-vendor-filter-status') || {}).value || '';
|
||
document.querySelectorAll('.dislo-vendor-row').forEach(function (row) {
|
||
const name = (row.dataset.name || '').toLowerCase();
|
||
const st = row.dataset.status || '';
|
||
const matchSearch = !search || name.includes(search);
|
||
const matchStatus = !status || st === status;
|
||
row.style.display = matchSearch && matchStatus ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
function disloVendorPanelOpen() {
|
||
const form = document.getElementById('dislo-vendor-form');
|
||
if (!form) return;
|
||
form.setAttribute('hx-post', '/dislocation/vendors/create/');
|
||
if (window.htmx) htmx.process(form);
|
||
const title = document.getElementById('dislo-vendor-panel-title');
|
||
if (title) title.textContent = '{% trans "Новый сервис / поставщик" %}';
|
||
const nameEl = form.querySelector('[name="name"]');
|
||
if (nameEl) nameEl.value = '';
|
||
const regionEl = form.querySelector('[name="location_region"]');
|
||
if (regionEl) regionEl.value = '';
|
||
['street', 'house_number', 'building', 'apartment', 'location', 'comments'].forEach(function (n) {
|
||
const el = form.querySelector('[name="' + n + '"]');
|
||
if (el) el.value = '';
|
||
});
|
||
const activeEl = form.querySelector('[name="is_active"]');
|
||
if (activeEl) activeEl.checked = true;
|
||
document.getElementById('dislo-vendor-panel-overlay').style.display = 'block';
|
||
document.getElementById('dislo-vendor-panel').style.transform = 'translateX(0)';
|
||
}
|
||
|
||
function disloVendorPanelClose() {
|
||
const overlay = document.getElementById('dislo-vendor-panel-overlay');
|
||
const panel = document.getElementById('dislo-vendor-panel');
|
||
if (overlay) overlay.style.display = 'none';
|
||
if (panel) panel.style.transform = 'translateX(100%)';
|
||
}
|
||
|
||
function disloVendorEdit(pk) {
|
||
const form = document.getElementById('dislo-vendor-form');
|
||
if (!form) return;
|
||
const btn = document.querySelector('button[onclick*="disloVendorEdit(' + pk + ')"]');
|
||
if (!btn) return;
|
||
form.setAttribute('hx-post', '/dislocation/vendors/' + pk + '/update/');
|
||
if (window.htmx) htmx.process(form);
|
||
const title = document.getElementById('dislo-vendor-panel-title');
|
||
if (title) title.textContent = '{% trans "Изменить сервис / поставщик" %}';
|
||
const set = function (name, value) {
|
||
const el = form.querySelector('[name="' + name + '"]');
|
||
if (el) el.value = value || '';
|
||
};
|
||
set('name', btn.dataset.name);
|
||
set('location_region', btn.dataset.region);
|
||
set('street', btn.dataset.street);
|
||
set('house_number', btn.dataset.house);
|
||
set('building', btn.dataset.building);
|
||
set('apartment', btn.dataset.apartment);
|
||
set('location', btn.dataset.location);
|
||
set('comments', btn.dataset.comments);
|
||
const activeEl = form.querySelector('[name="is_active"]');
|
||
if (activeEl) activeEl.checked = btn.dataset.active === '1';
|
||
document.getElementById('dislo-vendor-panel-overlay').style.display = 'block';
|
||
document.getElementById('dislo-vendor-panel').style.transform = 'translateX(0)';
|
||
}
|
||
|
||
{% if vendor_form_error and can_manage_vendors %}
|
||
document.getElementById('dislo-vendor-panel-overlay').style.display = 'block';
|
||
document.getElementById('dislo-vendor-panel').style.transform = 'translateX(0)';
|
||
{% endif %}
|
||
disloVendorFilter();
|
||
|
||
function disloGetAllowedRegionIdsByEnterprise() {
|
||
const enterpriseFilter = document.getElementById('dislo-filter-enterprise').value;
|
||
const rows = document.querySelectorAll('tbody tr.dislo-row');
|
||
const allowed = new Set();
|
||
rows.forEach(row => {
|
||
const enterprise = row.dataset.enterprise || '';
|
||
if (!enterpriseFilter || enterprise === enterpriseFilter) {
|
||
(row.dataset.regionIds || '').split(',').filter(Boolean).forEach(id => allowed.add(String(id)));
|
||
}
|
||
});
|
||
return allowed;
|
||
}
|
||
|
||
function disloFilterRegionTreeByAllowed(nodes, allowedIds) {
|
||
return nodes
|
||
.map(node => {
|
||
const children = disloFilterRegionTreeByAllowed(node.children || [], allowedIds);
|
||
const includeSelf = allowedIds.has(String(node.id)) || children.length > 0;
|
||
if (!includeSelf) return null;
|
||
return { id: node.id, name: node.name, children: children };
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function disloGetFilteredRegionTreeData() {
|
||
const enterpriseFilter = document.getElementById('dislo-filter-enterprise').value;
|
||
if (!enterpriseFilter) return disloRegionTreeData;
|
||
const allowedIds = disloGetAllowedRegionIdsByEnterprise();
|
||
return disloFilterRegionTreeByAllowed(disloRegionTreeData, allowedIds);
|
||
}
|
||
|
||
function disloEnterpriseChange() {
|
||
const regionInput = document.getElementById('dislo-filter-region');
|
||
const selectedRegion = regionInput.value;
|
||
const allowedIds = disloGetAllowedRegionIdsByEnterprise();
|
||
if (selectedRegion && !allowedIds.has(selectedRegion)) {
|
||
regionInput.value = '';
|
||
document.getElementById('dislo-filter-region-label').textContent = '{% trans "Все города/регионы" %}';
|
||
}
|
||
disloRenderRegionTree();
|
||
disloFilter();
|
||
}
|
||
|
||
function disloToggleRegionTree() {
|
||
const panel = document.getElementById('dislo-region-tree-panel');
|
||
panel.style.display = panel.style.display === 'block' ? 'none' : 'block';
|
||
}
|
||
|
||
function disloCloseRegionTree() {
|
||
document.getElementById('dislo-region-tree-panel').style.display = 'none';
|
||
}
|
||
|
||
function disloSelectRegionNode(regionId, label) {
|
||
document.getElementById('dislo-filter-region').value = regionId || '';
|
||
document.getElementById('dislo-filter-region-label').textContent = label || '{% trans "Все города/регионы" %}';
|
||
disloCloseRegionTree();
|
||
disloFilter();
|
||
}
|
||
|
||
function disloCreateRegionNodeElement(node, level) {
|
||
const wrapper = document.createElement('div');
|
||
wrapper.style.paddingLeft = `${level * 0.75}rem`;
|
||
|
||
const titleRow = document.createElement('div');
|
||
titleRow.style.padding = '0.55rem 0.75rem';
|
||
titleRow.style.cursor = 'pointer';
|
||
titleRow.style.fontSize = '0.84rem';
|
||
titleRow.style.color = 'var(--color-foreground)';
|
||
titleRow.style.borderBottom = '1px solid rgba(0,0,0,.06)';
|
||
titleRow.style.display = 'flex';
|
||
titleRow.style.alignItems = 'center';
|
||
titleRow.style.justifyContent = 'space-between';
|
||
|
||
const title = document.createElement('span');
|
||
title.textContent = node.name;
|
||
title.style.flex = '1';
|
||
title.style.whiteSpace = 'nowrap';
|
||
title.style.overflow = 'hidden';
|
||
title.style.textOverflow = 'ellipsis';
|
||
|
||
const arrow = document.createElement('span');
|
||
arrow.textContent = node.children && node.children.length ? '▸' : '';
|
||
arrow.style.opacity = node.children && node.children.length ? '0.7' : '0';
|
||
arrow.style.marginLeft = '0.5rem';
|
||
arrow.style.fontSize = '0.75rem';
|
||
|
||
titleRow.appendChild(title);
|
||
titleRow.appendChild(arrow);
|
||
titleRow.onclick = function() { disloSelectRegionNode(String(node.id), node.name); };
|
||
wrapper.appendChild(titleRow);
|
||
|
||
if (node.children && node.children.length) {
|
||
const childrenContainer = document.createElement('div');
|
||
node.children.forEach(child => {
|
||
childrenContainer.appendChild(disloCreateRegionNodeElement(child, level + 1));
|
||
});
|
||
wrapper.appendChild(childrenContainer);
|
||
}
|
||
return wrapper;
|
||
}
|
||
|
||
function disloRenderRegionTree() {
|
||
const root = document.getElementById('dislo-region-tree-root');
|
||
root.innerHTML = '';
|
||
disloGetFilteredRegionTreeData().forEach(node => {
|
||
root.appendChild(disloCreateRegionNodeElement(node, 0));
|
||
});
|
||
}
|
||
|
||
disloRenderRegionTree();
|
||
|
||
function disloAddMapBaseLayerWithFallback(map) {
|
||
if (typeof acoreAddMapBaseLayer === 'function') {
|
||
acoreAddMapBaseLayer(map);
|
||
return;
|
||
}
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap contributors', maxZoom: 19
|
||
}).addTo(map);
|
||
}
|
||
|
||
function disloCreateMarker(pharmacy) {
|
||
const markerColor = pharmacy.active ? '#22c55e' : '#ef4444';
|
||
// Класична крапля-пін замість квадратика з числом: гострий кінчик вказує на точну
|
||
// координату (iconAnchor у вершині), кількість персоналу — у білому кружку зверху
|
||
const markerHtml = `
|
||
<svg width="30" height="40" viewBox="0 0 30 40" xmlns="http://www.w3.org/2000/svg" style="filter:drop-shadow(0 2px 3px rgba(0,0,0,0.35));">
|
||
<path d="M15 0C6.716 0 0 6.716 0 15c0 10.5 15 25 15 25s15-14.5 15-25C30 6.716 23.284 0 15 0z" fill="${markerColor}"/>
|
||
<circle cx="15" cy="15" r="10" fill="white"/>
|
||
<text x="15" y="19.5" text-anchor="middle" font-size="11" font-weight="700" font-family="system-ui, sans-serif" fill="${markerColor}">${pharmacy.staff}</text>
|
||
</svg>`;
|
||
const customIcon = L.divIcon({ html: markerHtml, className: '', iconSize: [30, 40], iconAnchor: [15, 40], popupAnchor: [0, -36] });
|
||
const equipBits = [];
|
||
if (pharmacy.hasGenerator) equipBits.push('{% trans "Генератор" %}: {% trans "да" %}');
|
||
if (pharmacy.hasInverter) equipBits.push('{% trans "Инвертор" %}: {% trans "да" %}');
|
||
if (pharmacy.hasMikrotik) equipBits.push('{% trans "MikroTik" %}: {% trans "да" %}');
|
||
if (pharmacy.provider) equipBits.push(`{% trans "Провайдер" %}: ${pharmacy.provider}`);
|
||
if (pharmacy.contract) equipBits.push(`{% trans "Договор" %}: ${pharmacy.contract}`);
|
||
if (pharmacy.hasGpon) equipBits.push('{% trans "GPON" %}: {% trans "да" %}');
|
||
const equipLine = equipBits.length
|
||
? `<small style="display:block;margin-top:4px;">${equipBits.join('<br/>')}</small>`
|
||
: '';
|
||
const marker = L.marker([pharmacy.lat, pharmacy.lng], { icon: customIcon })
|
||
.bindPopup(`
|
||
<div style="font-size: 12px;">
|
||
<strong style="font-size: 13px;">${pharmacy.name}</strong><br/>
|
||
<small>${pharmacy.address}</small><br/>
|
||
<small>🧑 ${pharmacy.head || '-'}</small><br/>
|
||
<small>☎️ ${pharmacy.phone}</small><br/>
|
||
<small>🕐 ${pharmacy.hours}</small><br/>
|
||
<small>👥 {% trans "Персонал" %}: ${pharmacy.staff}</small>
|
||
${equipLine}<br/>
|
||
<span style="display:inline-block;margin-top:4px;padding:2px 6px;border-radius:3px;background:${markerColor};color:white;font-size:11px;">
|
||
${pharmacy.active ? '{% trans "Активна" %}' : '{% trans "Неактивна" %}'}
|
||
</span><br/>
|
||
<a href="/pharmacies/${pharmacy.id}/" style="display:inline-block;margin-top:6px;font-size:11px;">{% trans "Подробнее" %} →</a>
|
||
</div>
|
||
`);
|
||
marker.on('click', function() {
|
||
const sidebarItems = document.querySelectorAll('#dislo-view-map [data-pharmacy-id]');
|
||
sidebarItems.forEach(item => item.style.background = 'transparent');
|
||
const activeItem = document.querySelector(`#dislo-view-map [data-pharmacy-id="${pharmacy.id}"]`);
|
||
if (activeItem) activeItem.style.background = 'var(--color-muted)';
|
||
});
|
||
return marker;
|
||
}
|
||
|
||
// Без адреси немає реальних координат (PlainLocationField тихо лишає дефолт) —
|
||
// такі аптеки на карту не виводимо, лише в таблицю/список
|
||
function disloRenderMapMarkers(visibleIds) {
|
||
disloLayer.clearLayers();
|
||
const mappable = disloData.filter(p => p.hasLocation && visibleIds.has(p.id));
|
||
mappable.forEach(p => disloCreateMarker(p).addTo(disloLayer));
|
||
return mappable;
|
||
}
|
||
|
||
function disloInitMap() {
|
||
disloMap = L.map('dislo-map').setView([50.5, 26.5], 6);
|
||
disloAddMapBaseLayerWithFallback(disloMap);
|
||
// Кластеризація — без неї аптеки з однаковими/близькими координатами (типово в
|
||
// центрі міста) накладаються одна на одну суцільною купою міток
|
||
disloLayer = L.markerClusterGroup({ maxClusterRadius: 50 }).addTo(disloMap);
|
||
|
||
// Ініціалізація карти лінива (перший клік на вкладку «Карта») — якщо до цього
|
||
// вже застосували фільтр в таблиці, він має враховуватись одразу, а не показувати все
|
||
disloRenderMapMarkers(disloGetVisiblePharmacyIds());
|
||
|
||
if (disloLayer.getLayers().length > 0) {
|
||
disloMap.fitBounds(disloLayer.getBounds(), { padding: [50, 50] });
|
||
}
|
||
disloMap.on('zoomstart', function() {
|
||
disloLayer.eachLayer(function(marker) { marker.closePopup(); });
|
||
});
|
||
}
|
||
|
||
function disloSetColumnsButtonActive(active) {
|
||
const btn = document.getElementById('dislo-btn-columns');
|
||
if (!btn) return;
|
||
if (active) {
|
||
btn.style.background = 'color-mix(in srgb, var(--color-primary) 12%, var(--color-card))';
|
||
btn.style.borderColor = 'color-mix(in srgb, var(--color-primary) 45%, var(--color-border))';
|
||
btn.style.color = 'var(--color-foreground)';
|
||
} else {
|
||
btn.style.background = 'var(--color-card)';
|
||
btn.style.borderColor = 'var(--color-border)';
|
||
btn.style.color = 'var(--color-foreground)';
|
||
}
|
||
}
|
||
|
||
function disloView(view) {
|
||
const tableView = document.getElementById('dislo-view-table');
|
||
const mapView = document.getElementById('dislo-view-map');
|
||
const btnTable = document.getElementById('dislo-btn-table');
|
||
const btnMap = document.getElementById('dislo-btn-map');
|
||
const columnsPanel = document.getElementById('dislo-columns-panel');
|
||
|
||
if (view === 'table') {
|
||
tableView.style.display = 'block';
|
||
mapView.style.display = 'none';
|
||
btnTable.style.background = 'var(--color-card)';
|
||
btnTable.style.color = 'var(--color-foreground)';
|
||
btnMap.style.background = 'transparent';
|
||
btnMap.style.color = 'var(--color-muted-foreground)';
|
||
} else {
|
||
tableView.style.display = 'none';
|
||
mapView.style.display = 'block';
|
||
if (columnsPanel) columnsPanel.style.display = 'none';
|
||
disloSetColumnsButtonActive(false);
|
||
btnTable.style.background = 'transparent';
|
||
btnTable.style.color = 'var(--color-muted-foreground)';
|
||
btnMap.style.background = 'var(--color-card)';
|
||
btnMap.style.color = 'var(--color-foreground)';
|
||
if (!disloMap) {
|
||
setTimeout(disloInitMap, 100);
|
||
} else {
|
||
disloMap.invalidateSize();
|
||
}
|
||
}
|
||
}
|
||
|
||
function disloToggleColumnsPanel() {
|
||
const panel = document.getElementById('dislo-columns-panel');
|
||
if (!panel) return;
|
||
const opening = panel.style.display === 'none' || !panel.style.display;
|
||
panel.style.display = opening ? 'block' : 'none';
|
||
disloSetColumnsButtonActive(opening);
|
||
if (opening) disloView('table');
|
||
}
|
||
|
||
function disloColMove(btn, dir) {
|
||
const row = btn.closest('.dislo-col-row');
|
||
const container = document.getElementById('dislo-col-order');
|
||
if (!row || !container) return;
|
||
const siblings = Array.from(container.children);
|
||
const idx = siblings.indexOf(row);
|
||
const targetIdx = idx + dir;
|
||
if (targetIdx < 0 || targetIdx >= siblings.length) return;
|
||
const target = siblings[targetIdx];
|
||
if (dir < 0) {
|
||
container.insertBefore(row, target);
|
||
} else {
|
||
container.insertBefore(target, row);
|
||
}
|
||
}
|
||
|
||
function disloSelectedColumns() {
|
||
const form = document.getElementById('dislo-columns-form');
|
||
if (!form) return [];
|
||
return Array.from(form.querySelectorAll('input[name="columns"]'))
|
||
.filter((el) => el.type === 'hidden' || el.checked)
|
||
.map((el) => el.value);
|
||
}
|
||
|
||
function disloPresetAction(action) {
|
||
const selectEl = document.getElementById('dislo-preset-select');
|
||
const presetCode = selectEl ? selectEl.value : '';
|
||
if ((action === 'update' || action === 'delete') && !presetCode) {
|
||
alert('{% trans "Выберите пресет в списке." %}');
|
||
return;
|
||
}
|
||
if (action === 'delete' && !confirm('{% trans "Удалить выбранный пресет?" %}')) {
|
||
return;
|
||
}
|
||
if (action === 'create') {
|
||
const name = (document.getElementById('dislo-preset-name') || {}).value || '';
|
||
if (!name.trim()) {
|
||
alert('{% trans "Укажите название пресета." %}');
|
||
return;
|
||
}
|
||
}
|
||
const csrf = document.querySelector('#dislo-columns-panel input[name=csrfmiddlewaretoken]');
|
||
const values = {
|
||
action: action,
|
||
preset_code: presetCode,
|
||
name: (document.getElementById('dislo-preset-name') || {}).value || '',
|
||
code: (document.getElementById('dislo-preset-code') || {}).value || '',
|
||
department_id: (document.getElementById('dislo-preset-department') || {}).value || '',
|
||
is_default: document.getElementById('dislo-preset-default')?.checked ? '1' : '',
|
||
csrfmiddlewaretoken: csrf ? csrf.value : '',
|
||
columns: disloSelectedColumns(),
|
||
};
|
||
if (window.htmx) {
|
||
htmx.ajax('POST', '/dislocation/column-presets/', {
|
||
target: '#main-content',
|
||
swap: 'innerHTML',
|
||
values: values,
|
||
headers: csrf ? { 'X-CSRFToken': csrf.value } : {},
|
||
});
|
||
return;
|
||
}
|
||
const form = document.createElement('form');
|
||
form.method = 'POST';
|
||
form.action = '/dislocation/column-presets/';
|
||
Object.entries(values).forEach(([key, value]) => {
|
||
if (Array.isArray(value)) {
|
||
value.forEach((item) => {
|
||
const input = document.createElement('input');
|
||
input.type = 'hidden';
|
||
input.name = key;
|
||
input.value = item;
|
||
form.appendChild(input);
|
||
});
|
||
return;
|
||
}
|
||
const input = document.createElement('input');
|
||
input.type = 'hidden';
|
||
input.name = key;
|
||
input.value = value;
|
||
form.appendChild(input);
|
||
});
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
}
|
||
|
||
function disloApplyPreset(selectEl) {
|
||
const code = selectEl.value;
|
||
if (!code) return;
|
||
const csrf = document.querySelector('#dislo-columns-panel input[name=csrfmiddlewaretoken]');
|
||
if (window.htmx) {
|
||
htmx.ajax('POST', '/dislocation/columns/', {
|
||
target: '#main-content',
|
||
swap: 'innerHTML',
|
||
values: { preset_code: code, csrfmiddlewaretoken: csrf ? csrf.value : '' },
|
||
headers: csrf ? { 'X-CSRFToken': csrf.value } : {},
|
||
});
|
||
return;
|
||
}
|
||
const form = document.createElement('form');
|
||
form.method = 'POST';
|
||
form.action = '/dislocation/columns/';
|
||
if (csrf) {
|
||
const csrfInput = document.createElement('input');
|
||
csrfInput.type = 'hidden';
|
||
csrfInput.name = 'csrfmiddlewaretoken';
|
||
csrfInput.value = csrf.value;
|
||
form.appendChild(csrfInput);
|
||
}
|
||
const presetInput = document.createElement('input');
|
||
presetInput.type = 'hidden';
|
||
presetInput.name = 'preset_code';
|
||
presetInput.value = code;
|
||
form.appendChild(presetInput);
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
}
|
||
|
||
function disloGetVisiblePharmacyIds() {
|
||
const searchText = document.getElementById('dislo-search').value.toLowerCase();
|
||
const regionFilter = document.getElementById('dislo-filter-region').value;
|
||
const statusFilter = document.getElementById('dislo-filter-status').value;
|
||
const enterpriseFilter = document.getElementById('dislo-filter-enterprise').value;
|
||
|
||
const visibleIds = new Set();
|
||
document.querySelectorAll('tbody tr.dislo-row').forEach(row => {
|
||
const name = (row.dataset.name || '').toLowerCase();
|
||
const regionIds = (row.dataset.regionIds || '').split(',').filter(Boolean);
|
||
const status = row.dataset.status || '';
|
||
const enterprise = row.dataset.enterprise || '';
|
||
|
||
const matchSearch = searchText === '' || name.includes(searchText);
|
||
const matchRegion = regionFilter === '' || regionIds.includes(regionFilter);
|
||
const matchStatus = statusFilter === '' || status === statusFilter;
|
||
const matchEnterprise = enterpriseFilter === '' || enterprise === enterpriseFilter;
|
||
|
||
if (matchSearch && matchRegion && matchStatus && matchEnterprise) {
|
||
visibleIds.add(parseInt(row.dataset.pharmacyId));
|
||
}
|
||
});
|
||
return visibleIds;
|
||
}
|
||
|
||
var disloSortState = { key: null, dir: 1 };
|
||
|
||
function disloSortBy(key, type) {
|
||
const tbody = document.querySelector('#dislo-view-table tbody');
|
||
if (!tbody) return;
|
||
|
||
if (disloSortState.key === key) {
|
||
disloSortState.dir *= -1;
|
||
} else {
|
||
disloSortState.key = key;
|
||
disloSortState.dir = 1;
|
||
}
|
||
|
||
const rows = Array.from(tbody.querySelectorAll('tr.dislo-row'));
|
||
rows.sort((a, b) => {
|
||
const rawA = a.getAttribute('data-sort-' + key) || '';
|
||
const rawB = b.getAttribute('data-sort-' + key) || '';
|
||
let result;
|
||
if (type === 'number') {
|
||
result = (parseFloat(rawA) || 0) - (parseFloat(rawB) || 0);
|
||
} else {
|
||
result = rawA.localeCompare(rawB, 'uk');
|
||
}
|
||
return result * disloSortState.dir;
|
||
});
|
||
|
||
rows.forEach(row => {
|
||
tbody.appendChild(row);
|
||
const staffRow = document.getElementById('dislo-staff-' + row.dataset.pharmacyId);
|
||
if (staffRow) tbody.appendChild(staffRow);
|
||
});
|
||
|
||
document.querySelectorAll('.dislo-sort-icon').forEach(icon => {
|
||
if (icon.dataset.key === key) {
|
||
icon.textContent = disloSortState.dir === 1 ? '▲' : '▼';
|
||
icon.style.opacity = '1';
|
||
} else {
|
||
icon.textContent = '⇅';
|
||
icon.style.opacity = '0.35';
|
||
}
|
||
});
|
||
|
||
disloFilter();
|
||
}
|
||
|
||
// Пагінація таблиці (клієнтська — карта та бічна панель показують весь відфільтрований набір,
|
||
// пагінація впливає лише на таблицю аптек, з урахуванням поточного сортування)
|
||
let disloPage = 1;
|
||
let disloPageSize = 25;
|
||
|
||
function disloSetPageSize(size) {
|
||
disloPageSize = parseInt(size, 10) || 25;
|
||
disloPage = 1;
|
||
disloFilter();
|
||
}
|
||
|
||
function disloGoToPage(page) {
|
||
disloPage = Math.max(1, page);
|
||
disloFilter();
|
||
}
|
||
|
||
function disloUpdatePaginationBar(total, start, end, page, totalPages) {
|
||
const info = document.getElementById('dislo-page-info');
|
||
const label = document.getElementById('dislo-page-label');
|
||
const prevBtn = document.getElementById('dislo-prev-btn');
|
||
const nextBtn = document.getElementById('dislo-next-btn');
|
||
if (info) info.textContent = total ? `{% trans "Показано" %} ${start + 1}–${end} {% trans "из" %} ${total}` : '{% trans "Нет записей" %}';
|
||
if (label) label.textContent = `Стор. ${page} з ${totalPages}`;
|
||
if (prevBtn) prevBtn.disabled = page <= 1;
|
||
if (nextBtn) nextBtn.disabled = page >= totalPages;
|
||
}
|
||
|
||
function disloFilter() {
|
||
const visibleIds = disloGetVisiblePharmacyIds();
|
||
|
||
const matchedRows = [];
|
||
document.querySelectorAll('tbody tr.dislo-row').forEach(row => {
|
||
const pharmacyId = row.dataset.pharmacyId;
|
||
if (visibleIds.has(parseInt(pharmacyId))) {
|
||
matchedRows.push(row);
|
||
} else {
|
||
row.style.display = 'none';
|
||
const staffRow = document.getElementById('dislo-staff-' + pharmacyId);
|
||
if (staffRow) staffRow.style.display = 'none';
|
||
const toggle = document.getElementById('dislo-toggle-' + pharmacyId);
|
||
if (toggle) toggle.style.transform = '';
|
||
}
|
||
});
|
||
|
||
const totalMatched = matchedRows.length;
|
||
const totalPages = Math.max(1, Math.ceil(totalMatched / disloPageSize));
|
||
if (disloPage > totalPages) disloPage = totalPages;
|
||
const start = (disloPage - 1) * disloPageSize;
|
||
const end = Math.min(start + disloPageSize, totalMatched);
|
||
|
||
matchedRows.forEach((row, idx) => {
|
||
const pharmacyId = row.dataset.pharmacyId;
|
||
const onPage = idx >= start && idx < end;
|
||
row.style.display = onPage ? '' : 'none';
|
||
if (!onPage) {
|
||
const staffRow = document.getElementById('dislo-staff-' + pharmacyId);
|
||
if (staffRow) staffRow.style.display = 'none';
|
||
const toggle = document.getElementById('dislo-toggle-' + pharmacyId);
|
||
if (toggle) toggle.style.transform = '';
|
||
}
|
||
});
|
||
|
||
disloUpdatePaginationBar(totalMatched, totalMatched ? start : 0, end, disloPage, totalPages);
|
||
|
||
const sidebarItems = document.querySelectorAll('#dislo-view-map div[data-pharmacy-id]');
|
||
sidebarItems.forEach(item => {
|
||
const pharmacyId = parseInt(item.dataset.pharmacyId);
|
||
item.style.display = visibleIds.has(pharmacyId) ? '' : 'none';
|
||
});
|
||
|
||
if (disloMap && disloLayer) {
|
||
disloRenderMapMarkers(visibleIds);
|
||
}
|
||
}
|
||
|
||
disloFilter();
|
||
</script>
|
||
|
||
</div>
|