92527cc52a
Вкладки аптеки/офис, метрики, фильтры, таблицы, пагинация, карта и её
popup/JS-строки помечены {% trans %} (в т.ч. внутри <script>, с обходом
кавычек) и переведены на uk/en. 51 строка.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
778 lines
50 KiB
HTML
778 lines
50 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 "сотрудников офиса/склада" %} · {% 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 dislo-tab-active" 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; background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);">{% trans "Аптеки" %}</button>
|
||
<button type="button" class="dislo-tab-btn" 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; background:transparent; color:var(--color-muted-foreground);">{% trans "Офис / Склад" %}</button>
|
||
</div>
|
||
|
||
<!-- ══ ВКЛАДКА: АПТЕКИ ══ -->
|
||
<div id="dislo-panel-pharmacy" class="dislo-panel">
|
||
|
||
<!-- Метрики -->
|
||
<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">{% trans "Активные" %}</option>
|
||
<option value="inactive">{% trans "Неактивные" %}</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex; gap:2px; background:var(--color-muted); border-radius:var(--radius); padding:3px; flex-shrink:0;">
|
||
<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 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>
|
||
<th class="dislo-sort-th" onclick="disloSortBy('pharmacy','number')" 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;">{% trans "Аптека" %} <span class="dislo-sort-icon" data-key="pharmacy" style="opacity:0.35;">⇅</span></th>
|
||
<th class="dislo-sort-th" onclick="disloSortBy('org','string')" 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;">{% trans "Организация" %} <span class="dislo-sort-icon" data-key="org" style="opacity:0.35;">⇅</span></th>
|
||
<th class="dislo-sort-th" onclick="disloSortBy('head','string')" 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;">{% trans "Заведующий" %} <span class="dislo-sort-icon" data-key="head" style="opacity:0.35;">⇅</span></th>
|
||
<th class="dislo-sort-th" onclick="disloSortBy('staff','number')" 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;">{% trans "Персонал" %} <span class="dislo-sort-icon" data-key="staff" style="opacity:0.35;">⇅</span></th>
|
||
<th class="dislo-sort-th" onclick="disloSortBy('phone','string')" 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;">{% trans "Телефон" %} <span class="dislo-sort-icon" data-key="phone" style="opacity:0.35;">⇅</span></th>
|
||
<th class="dislo-sort-th" onclick="disloSortBy('hours','string')" 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;">{% trans "Режим работы" %} <span class="dislo-sort-icon" data-key="hours" style="opacity:0.35;">⇅</span></th>
|
||
<th class="dislo-sort-th" onclick="disloSortBy('status','number')" 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;">{% trans "Статус" %} <span class="dislo-sort-icon" data-key="status" style="opacity:0.35;">⇅</span></th>
|
||
</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-org="{{ 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 %}">
|
||
<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>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">
|
||
<a href="/pharmacies/{{ pharmacy.id }}/" hx-get="/pharmacies/{{ pharmacy.id }}/" hx-target="#main-content" hx-push-url="/pharmacies/{{ pharmacy.id }}/" hx-indicator="#loader"
|
||
onclick="event.stopPropagation();" style="font-weight:600; color:var(--color-foreground); text-decoration:none;">№ {{ pharmacy.pharmacy_number }}</a>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.125rem; max-width:220px; overflow:hidden; text-overflow:ellipsis;">{{ pharmacy.address|default:no_addr_short }}</div>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">{{ pharmacy.organization.name|default:"—" }}</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">
|
||
{% if pharmacy.head_pharmacist %}
|
||
{{ pharmacy.head_pharmacist.first_name }} {{ pharmacy.head_pharmacist.last_name|default:'' }}
|
||
{% else %}
|
||
<span style="color:var(--color-warning); font-size:0.8125rem;">{% trans "не назначен" %}</span>
|
||
{% endif %}
|
||
</td>
|
||
<td style="padding:0.75rem 1rem;">
|
||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||
<span style="color:var(--color-foreground); font-weight:500;">{{ pharmacy.staff_count }}</span>
|
||
{% if pharmacy.staff_status == 'danger' %}
|
||
<span class="badge badge-danger">{% trans "Нехватка" %}</span>
|
||
{% elif pharmacy.staff_status == 'warning' %}
|
||
<span class="badge badge-warning">{% trans "Внимание" %}</span>
|
||
{% else %}
|
||
<span class="badge badge-success">{% trans "Норма" %}</span>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">
|
||
{% if pharmacy.phone %}{{ pharmacy.phone }}{% else %}<span style="color:var(--color-muted-foreground);">-</span>{% endif %}
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground); font-size:0.8125rem;">{{ pharmacy.working_hours }}</td>
|
||
<td style="padding:0.75rem 1rem;">
|
||
{% if pharmacy.is_active %}
|
||
<span class="badge badge-success">{% trans "Активна" %}</span>
|
||
{% else %}
|
||
<span class="badge badge-danger">{% trans "Неактивна" %}</span>
|
||
{% endif %}
|
||
</td>
|
||
</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="7" 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="8" 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>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: АПТЕКИ ══ -->
|
||
|
||
<!-- ══ ВКЛАДКА: ОФІС / СКЛАД ══ -->
|
||
<div id="dislo-panel-office" class="dislo-panel" style="display:none;">
|
||
|
||
<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>
|
||
<!-- ══ /ВКЛАДКА: ОФІС / СКЛАД ══ -->
|
||
|
||
<script id="dislo-region-tree-data" type="application/json">{{ region_tree_json|safe }}</script>
|
||
<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 }},
|
||
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 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) {
|
||
const sources = [
|
||
{
|
||
url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
|
||
options: { attribution: '© OpenStreetMap contributors © CARTO', maxZoom: 20, subdomains: 'abcd' }
|
||
},
|
||
{
|
||
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||
options: { attribution: '© OpenStreetMap contributors', maxZoom: 19 }
|
||
}
|
||
];
|
||
let idx = 0;
|
||
let layer = null;
|
||
const mount = () => {
|
||
if (idx >= sources.length) return;
|
||
const src = sources[idx];
|
||
layer = L.tileLayer(src.url, src.options).addTo(map);
|
||
layer.on('tileerror', () => {
|
||
if (idx + 1 >= sources.length) return;
|
||
map.removeLayer(layer);
|
||
idx += 1;
|
||
mount();
|
||
});
|
||
};
|
||
mount();
|
||
}
|
||
|
||
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 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><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 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');
|
||
|
||
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';
|
||
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 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>
|