Files
ACORE/templates/pages/pharmacies.html
T
lokses 897ffc99c9 locations: право редактирования аптеки, формы и дислокация
Отдельное право на редактирование карточки аптеки, формы, пресеты колонок
дислокации и правки видимости карточки.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 18:03:32 +03:00

673 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% load i18n %}
<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.count }} аптек · {{ active_count }} активних</p>
</div>
<div style="display:flex; gap:0.5rem;">
<button class="btn btn-secondary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/>
</svg>
Експорт
</button>
<button class="btn btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" x2="12" y1="5" y2="19"/><line x1="5" x2="19" y1="12" y2="12"/>
</svg>
Додати аптеку
</button>
</div>
</div>
<!-- Метрики -->
<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.count }}</div>
<div style="font-size:0.75rem; color:var(--color-success); margin-top:0.25rem;">{{ active_count }} активних</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);">-</div>
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.25rem;">укупно</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);">-</div>
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.25rem;">середня</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);">0</div>
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.25rem;">відкритих</div>
</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="pharma-search" placeholder="{% trans 'Поиск аптеки...' %}" oninput="pharmaFilter()"
class="input" style="border:none; background:transparent; padding:0; box-shadow:none; font-size:0.8125rem;">
</div>
<select class="input" id="filter-enterprise" onchange="handleEnterpriseChange()" 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>
<select class="input" id="filter-pharmacy" onchange="pharmaFilter()" style="width:auto; font-size:0.8125rem; padding:0.375rem 0.625rem; max-width:16rem; display:none;">
<option value="">{% trans "Все аптеки" %}</option>
</select>
<div class="filter-tree" style="position:relative; min-width:14rem;">
<button id="filter-region-button" type="button" class="input" onclick="toggleRegionTree()"
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="filter-region-label">{% trans "Все города/регионы" %}</span>
<span style="font-size:0.75rem; color:var(--color-muted-foreground);">▾</span>
</button>
<div id="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 class="region-tree-item" style="padding:0.55rem 0.65rem; cursor:pointer; border-bottom:1px solid rgba(0,0,0,.06); font-size:0.8rem; font-size:0.8rem;"
onclick="selectRegionNode('', 'Всі міста/регіони')">
Всі міста/регіони
</div>
<div id="region-tree-root"></div>
</div>
<input type="hidden" id="filter-region" value="">
</div>
<select class="input" id="filter-status" onchange="pharmaFilter()" 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="btn-table" onclick="pharmaView('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>
Таблиця
</button>
<button id="btn-map" onclick="pharmaView('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>
Карта
</button>
</div>
</div>
<!-- Таблица -->
<div id="pv-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="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;">{% 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 pharmacy in pharmacies %}
<tr style="border-bottom:1px solid var(--color-border); transition:background 150ms;"
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 }}">
<td style="padding:0.75rem 1rem; color:var(--color-foreground);"><strong>№ {{ pharmacy.pharmacy_number }}</strong></td>
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">{{ pharmacy.organization.name }}</td>
<td style="padding:0.75rem 1rem; color:var(--color-foreground); max-width:200px; overflow:hidden; text-overflow:ellipsis;">
{% if pharmacy.address %}{{ pharmacy.address }}{% else %}-{% endif %}
</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-muted-foreground);">-</span>
{% endif %}
</td>
<td style="padding:0.75rem 1rem; color:var(--color-foreground); text-align:center;">{{ pharmacy.pharmacists_count }}</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 style="display:inline-block; padding:0.25rem 0.75rem; background:var(--color-success); color:white; border-radius:9999px; font-size:0.75rem; font-weight:600;">{% trans "Активна" %}</span>
{% else %}
<span style="display:inline-block; padding:0.25rem 0.75rem; background:var(--color-destructive); color:white; border-radius:9999px; font-size:0.75rem; font-weight:600;">{% trans "Неактивна" %}</span>
{% 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="pharma-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="pharma-page-size" class="input" style="font-size:0.8125rem;padding:0.3rem 0.5rem;height:auto;width:auto;" onchange="pharmaSetPageSize(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="pharma-prev-btn" class="btn btn-secondary" style="padding:0.375rem 0.75rem;font-size:0.8125rem;" onclick="pharmaGoToPage(pharmaPage - 1)">← Попередня</button>
<span id="pharma-page-label" style="font-size:0.8125rem;color:var(--color-muted-foreground);white-space:nowrap;">{% trans "Стр. 1 из 1" %}</span>
<button type="button" id="pharma-next-btn" class="btn btn-secondary" style="padding:0.375rem 0.75rem;font-size:0.8125rem;" onclick="pharmaGoToPage(pharmaPage + 1)">{% trans "Следующая" %} →</button>
</div>
</div>
</div>
</div>
</div>
<!-- Карта -->
<div id="pv-map" style="display:none;">
<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="pharma-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;">
Список аптек
</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>
{{ region_tree|json_script:"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>
let pharmaciesMap = null;
let pharmaciesLayer = null;
const pharmaciesData = [
{% for pharmacy in pharmacies %}
{
id: {{ pharmacy.id }},
num: "{{ pharmacy.pharmacy_number|escapejs|default:'0' }}",
name: "{{ pharmacy.organization.name|escapejs|default:'' }} #{{ pharmacy.pharmacy_number|escapejs|default:'0' }}",
address: "{{ pharmacy.address|escapejs|default:'Адреса невідома' }}",
region: "{{ pharmacy.location_region.name|escapejs|default:'' }}",
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:'-' }}",
active: {{ pharmacy.is_active|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 regionTreeData = JSON.parse(document.getElementById('region-tree-data').textContent || '[]');
function getAllowedRegionIdsByEnterprise() {
const enterpriseFilter = document.getElementById('filter-enterprise').value;
const rows = document.querySelectorAll('tbody tr[data-pharmacy-id]');
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 filterRegionTreeByAllowed(nodes, allowedIds) {
return nodes
.map(node => {
const children = filterRegionTreeByAllowed(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 getFilteredRegionTreeData() {
const enterpriseFilter = document.getElementById('filter-enterprise').value;
if (!enterpriseFilter) {
return regionTreeData;
}
const allowedIds = getAllowedRegionIdsByEnterprise();
return filterRegionTreeByAllowed(regionTreeData, allowedIds);
}
function handleEnterpriseChange() {
const regionInput = document.getElementById('filter-region');
const selectedRegion = regionInput.value;
const allowedIds = getAllowedRegionIdsByEnterprise();
if (selectedRegion && !allowedIds.has(selectedRegion)) {
regionInput.value = '';
document.getElementById('filter-region-label').textContent = 'Всі міста/регіони';
}
// Оновити список аптек
rebuildPharmacySelect();
renderRegionTree();
pharmaFilter();
}
function rebuildPharmacySelect() {
const enterpriseFilter = document.getElementById('filter-enterprise').value;
const pharmSel = document.getElementById('filter-pharmacy');
if (!pharmSel) return;
const rows = document.querySelectorAll('tbody tr[data-pharmacy-id]');
const options = [];
rows.forEach(function(row) {
const enterprise = row.dataset.enterprise || '';
if (!enterpriseFilter || enterprise === enterpriseFilter) {
options.push({ id: row.dataset.pharmacyId, name: row.dataset.name || ('#' + row.dataset.pharmacyId) });
}
});
if (enterpriseFilter && options.length > 0) {
pharmSel.style.display = '';
const currentVal = pharmSel.value;
pharmSel.innerHTML = '<option value="">{% trans "Все аптеки" %}</option>' +
options.map(function(o) {
return '<option value="' + o.id + '"' + (o.id === currentVal ? ' selected' : '') + '>' + o.name + '</option>';
}).join('');
// Скинули підприємство — скидаємо вибір аптеки якщо її немає в новому списку
if (currentVal && !options.find(function(o){ return o.id === currentVal; })) {
pharmSel.value = '';
}
} else {
pharmSel.style.display = 'none';
pharmSel.value = '';
}
}
function toggleRegionTree() {
const panel = document.getElementById('region-tree-panel');
panel.style.display = panel.style.display === 'block' ? 'none' : 'block';
}
function closeRegionTree() {
document.getElementById('region-tree-panel').style.display = 'none';
}
function selectRegionNode(regionId, label) {
document.getElementById('filter-region').value = regionId || '';
document.getElementById('filter-region-label').textContent = label || 'Всі міста/регіони';
closeRegionTree();
pharmaFilter();
}
function createRegionNodeElement(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() {
selectRegionNode(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(createRegionNodeElement(child, level + 1));
});
wrapper.appendChild(childrenContainer);
}
return wrapper;
}
function renderRegionTree() {
const root = document.getElementById('region-tree-root');
root.innerHTML = '';
const dataToRender = getFilteredRegionTreeData();
dataToRender.forEach(node => {
root.appendChild(createRegionNodeElement(node, 0));
});
}
renderRegionTree();
pharmaFilter();
function addMapBaseLayerWithFallback(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 initPharmaciesMap() {
// Центр карты — средняя точка всех аптек
let centerLat = 50.5;
let centerLng = 26.5;
if (pharmaciesData.length > 0) {
centerLat = pharmaciesData.reduce((sum, p) => sum + p.lat, 0) / pharmaciesData.length;
centerLng = pharmaciesData.reduce((sum, p) => sum + p.lng, 0) / pharmaciesData.length;
}
pharmaciesMap = L.map('pharma-map').setView([centerLat, centerLng], 6);
addMapBaseLayerWithFallback(pharmaciesMap);
pharmaciesLayer = L.featureGroup().addTo(pharmaciesMap);
// Добавить маркеры для каждой аптеки
pharmaciesData.forEach(function(pharmacy) {
const markerColor = pharmacy.active ? '#22c55e' : '#ef4444';
const markerHtml = `
<div style="
background: ${markerColor};
color: white;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
font-size: 12px;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
">#${pharmacy.id}</div>
`;
const customIcon = L.divIcon({
html: markerHtml,
className: '',
iconSize: [40, 30],
iconAnchor: [20, 30],
popupAnchor: [0, -30]
});
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/>
<span style="display:inline-block;margin-top:4px;padding:2px 6px;border-radius:3px;background:${markerColor};color:white;font-size:11px;">
${pharmacy.active ? 'Активна' : 'Неактивна'}
</span>
</div>
`)
.addTo(pharmaciesLayer);
// Клик на маркер — выделить в списке справа
marker.on('click', function() {
const sidebarItems = document.querySelectorAll('[data-pharmacy-id]');
sidebarItems.forEach(item => item.style.background = 'transparent');
const activeItem = document.querySelector(`[data-pharmacy-id="${pharmacy.id}"]`);
if (activeItem) activeItem.style.background = 'var(--color-muted)';
});
});
// Зум на все маркеры
if (pharmaciesLayer.getLayers().length > 0) {
pharmaciesMap.fitBounds(pharmaciesLayer.getBounds(), { padding: [50, 50] });
}
// Закрыть попапы при зуме
pharmaciesMap.on('zoomstart', function() {
pharmaciesLayer.eachLayer(function(marker) {
marker.closePopup();
});
});
}
function pharmaView(view) {
const tableView = document.getElementById('pv-table');
const mapView = document.getElementById('pv-map');
const btnTable = document.getElementById('btn-table');
const btnMap = document.getElementById('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 (!pharmaciesMap) {
setTimeout(initPharmaciesMap, 100);
} else {
pharmaciesMap.invalidateSize();
}
}
}
// Пагінація таблиці (клієнтська — усі рядки вже в DOM, карта показує весь відфільтрований набір)
let pharmaPage = 1;
let pharmaPageSize = 25;
function pharmaSetPageSize(size) {
pharmaPageSize = parseInt(size, 10) || 25;
pharmaPage = 1;
pharmaFilter();
}
function pharmaGoToPage(page) {
pharmaPage = Math.max(1, page);
pharmaFilter();
}
function pharmaUpdatePaginationBar(total, start, end, page, totalPages) {
const info = document.getElementById('pharma-page-info');
const label = document.getElementById('pharma-page-label');
const prevBtn = document.getElementById('pharma-prev-btn');
const nextBtn = document.getElementById('pharma-next-btn');
if (info) info.textContent = total ? `Показано ${start + 1}–${end} з ${total}` : 'Немає записів';
if (label) label.textContent = `Стор. ${page} з ${totalPages}`;
if (prevBtn) prevBtn.disabled = page <= 1;
if (nextBtn) nextBtn.disabled = page >= totalPages;
}
function pharmaFilter() {
const searchText = document.getElementById('pharma-search').value.toLowerCase();
const regionFilter = document.getElementById('filter-region').value;
const statusFilter = document.getElementById('filter-status').value;
const enterpriseFilter = document.getElementById('filter-enterprise').value;
const pharmacyFilter = document.getElementById('filter-pharmacy') ? document.getElementById('filter-pharmacy').value : '';
// Фільтрація таблиці
const tableRows = document.querySelectorAll('tbody tr[data-pharmacy-id]');
const visibleIds = new Set();
const matchedRows = [];
tableRows.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 pharmacyId = row.dataset.pharmacyId || '';
const matchSearch = searchText === '' || name.includes(searchText);
const matchRegion = regionFilter === '' || regionIds.includes(regionFilter);
const matchStatus = statusFilter === '' || status === statusFilter;
const matchEnterprise = enterpriseFilter === '' || enterprise === enterpriseFilter;
const matchPharmacy = pharmacyFilter === '' || pharmacyId === pharmacyFilter;
const matches = matchSearch && matchRegion && matchStatus && matchEnterprise && matchPharmacy;
if (matches) {
visibleIds.add(parseInt(row.dataset.pharmacyId));
matchedRows.push(row);
} else {
row.style.display = 'none';
}
});
// Пагінація в межах відфільтрованих рядків таблиці (карта/сайдбар показують весь набір — див. нижче)
const totalMatched = matchedRows.length;
const totalPages = Math.max(1, Math.ceil(totalMatched / pharmaPageSize));
if (pharmaPage > totalPages) pharmaPage = totalPages;
const start = (pharmaPage - 1) * pharmaPageSize;
const end = Math.min(start + pharmaPageSize, totalMatched);
matchedRows.forEach((row, idx) => {
row.style.display = (idx >= start && idx < end) ? '' : 'none';
});
pharmaUpdatePaginationBar(totalMatched, totalMatched ? start : 0, end, pharmaPage, totalPages);
// Фильтрация элементов сайдбара на карте
const sidebarItems = document.querySelectorAll('div[data-pharmacy-id]');
sidebarItems.forEach(item => {
const pharmacyId = parseInt(item.dataset.pharmacyId);
const visible = visibleIds.has(pharmacyId);
item.style.opacity = visible ? '1' : '0.3';
item.style.pointerEvents = visible ? 'auto' : 'none';
});
// Фильтрация маркеров на карте
if (pharmaciesMap && pharmaciesLayer) {
const visibleData = pharmaciesData.filter(p => visibleIds.has(p.id));
pharmaciesLayer.clearLayers();
visibleData.forEach(function(pharmacy) {
const markerColor = pharmacy.active ? '#22c55e' : '#ef4444';
const markerHtml = `
<div style="
background: ${markerColor};
color: white;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
font-size: 12px;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
">#${pharmacy.num}</div>
`;
const customIcon = L.divIcon({
html: markerHtml,
className: '',
iconSize: [40, 30],
iconAnchor: [20, 30],
popupAnchor: [0, -30]
});
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/>
<span style="display:inline-block;margin-top:4px;padding:2px 6px;border-radius:3px;background:${markerColor};color:white;font-size:11px;">
${pharmacy.active ? 'Активна' : 'Неактивна'}
</span>
</div>
`)
.addTo(pharmaciesLayer);
});
}
console.log(`Visible pharmacies: ${visibleIds.size}`);
}
// Инициализировать карту в конце загрузки
document.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'main-content') {
// Карта инициализируется при клике на кнопку "Карта"
}
});
</script>
</div>