Files
ACORE/static/assets/js/pharmacies.js
T

384 lines
19 KiB
JavaScript
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.
var _pharmacyPicker = { map: null, marker: null, input: null };
function pharmacyPickerDestroy() {
if (_pharmacyPicker.map) {
_pharmacyPicker.map.remove();
}
_pharmacyPicker = { map: null, marker: null, input: null };
}
function pharmacyModalClose() {
pharmacyPickerDestroy();
var root = document.getElementById('pharmacy-modal-root');
if (root) root.innerHTML = '';
}
function pharmacyModalTab(tab) {
var modal = document.getElementById('pharmacy-edit-modal');
if (!modal) return;
modal.querySelectorAll('.ph-edit-panel').forEach(function (panel) {
var active = panel.getAttribute('data-tab') === tab;
if (active) {
panel.style.display = panel.getAttribute('data-display') || '';
} else {
panel.style.display = 'none';
}
});
modal.querySelectorAll('.ph-edit-tab-btn').forEach(function (btn) {
var active = btn.getAttribute('data-tab') === tab;
btn.classList.toggle('ph-edit-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 === 'address') pharmacyPickerEnsure();
}
function pharmacyModalReady(opts) {
opts = opts || {};
var modal = document.getElementById('pharmacy-edit-modal');
if (!modal) return;
pharmacyModalTab(opts.tab || 'main');
}
function pharmacyPickerParse(value) {
if (!value) return null;
var parts = String(value).split(',');
if (parts.length < 2) return null;
var lat = parseFloat(parts[0].trim());
var lng = parseFloat(parts[1].trim());
if (!isFinite(lat) || !isFinite(lng)) return null;
return [lat, lng];
}
function pharmacyPickerFormat(latlng) {
return (Math.round(latlng.lat * 1e6) / 1e6) + ',' + (Math.round(latlng.lng * 1e6) / 1e6);
}
function pharmacyPickerWrite(latlng) {
if (_pharmacyPicker.input) _pharmacyPicker.input.value = pharmacyPickerFormat(latlng);
}
function pharmacyPickerSetMarker(latlng, pan) {
if (!_pharmacyPicker.map || !window.L) return;
if (_pharmacyPicker.marker) {
_pharmacyPicker.marker.setLatLng(latlng);
} else {
_pharmacyPicker.marker = L.marker(latlng, { draggable: !_pharmacyPicker.readonly }).addTo(_pharmacyPicker.map);
if (!_pharmacyPicker.readonly) {
_pharmacyPicker.marker.on('dragend', function (ev) {
pharmacyPickerWrite(ev.target.getLatLng());
});
}
}
if (pan) _pharmacyPicker.map.panTo(latlng);
pharmacyPickerWrite(latlng);
}
function pharmacyPickerClear() {
if (_pharmacyPicker.readonly) return;
if (_pharmacyPicker.marker && _pharmacyPicker.map) {
_pharmacyPicker.map.removeLayer(_pharmacyPicker.marker);
}
_pharmacyPicker.marker = null;
if (_pharmacyPicker.input) _pharmacyPicker.input.value = '';
}
function pharmacyPickerLoadLeaflet(cb) {
if (window.L) { cb(); return; }
if (document.getElementById('leaflet-js')) {
document.getElementById('leaflet-js').addEventListener('load', cb);
return;
}
var lc = document.createElement('link');
lc.rel = 'stylesheet';
lc.href = 'https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(lc);
var ls = document.createElement('script');
ls.id = 'leaflet-js';
ls.src = 'https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js';
ls.onload = cb;
document.head.appendChild(ls);
}
function pharmacyPickerEnsure() {
var el = document.getElementById('pharmacy-picker-map');
if (!el) return;
pharmacyPickerLoadLeaflet(function () {
var mapEl = document.getElementById('pharmacy-picker-map');
if (!mapEl || !window.L) return;
if (_pharmacyPicker.map) {
setTimeout(function () { _pharmacyPicker.map.invalidateSize(); }, 50);
return;
}
var input = document.getElementById('id_location');
_pharmacyPicker.input = input;
_pharmacyPicker.readonly = mapEl.getAttribute('data-readonly') === '1';
var hasCoords = mapEl.getAttribute('data-has-coords') !== '0';
var parsed = null;
if (hasCoords) {
parsed = pharmacyPickerParse(input && input.value);
if (!parsed && mapEl.getAttribute('data-lat')) {
parsed = [parseFloat(mapEl.getAttribute('data-lat')), parseFloat(mapEl.getAttribute('data-lng'))];
}
}
var center = parsed || [46.4825, 30.7233];
var zoom = parsed ? 16 : 12;
var map = L.map(mapEl, {
scrollWheelZoom: !_pharmacyPicker.readonly,
dragging: true,
zoomControl: true,
}).setView(center, zoom);
if (typeof acoreAddMapBaseLayer === 'function') {
acoreAddMapBaseLayer(map);
} else {
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors', maxZoom: 19
}).addTo(map);
}
_pharmacyPicker.map = map;
if (parsed) pharmacyPickerSetMarker({ lat: parsed[0], lng: parsed[1] }, false);
if (!_pharmacyPicker.readonly) {
map.on('click', function (ev) {
pharmacyPickerSetMarker(ev.latlng, false);
});
if (input) {
input.addEventListener('change', function () {
var next = pharmacyPickerParse(input.value);
if (next) pharmacyPickerSetMarker({ lat: next[0], lng: next[1] }, true);
});
}
}
setTimeout(function () { map.invalidateSize(); }, 80);
});
}
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
var modal = document.getElementById('pharmacy-edit-modal');
if (modal) pharmacyModalClose();
});
document.addEventListener('htmx:afterSwap', function (evt) {
var target = evt.detail && evt.detail.target;
if (target && target.id === 'pharmacy-modal-root') {
var modal = document.getElementById('pharmacy-edit-modal');
if (!modal) return;
var active = modal.querySelector('.ph-edit-tab-btn.ph-edit-tab-active');
pharmacyModalReady({ tab: active ? active.getAttribute('data-tab') : 'main' });
}
});
var PHARMACIES_DATA = [
{id:'А01',name:'Аптека №1 — Центральна', addr:'вул. Дерибасівська, 12', city:'Одеса', lat:46.4825,lng:30.7233,head:'1/1',pharm:'3/3',cashier:'2/3',schedule:'Пн–Нд 8–22',status:'ok',enterprise:'Одесская аптечная сеть'},
{id:'А07',name:'Аптека №7 — Черемушки', addr:'вул. Маршала Жукова, 54', city:'Одеса', lat:46.4191,lng:30.7058,head:'1/1',pharm:'1/2',cashier:'2/2',schedule:'Пн–Сб 9–20',status:'warn',enterprise:'Одесская аптечная сеть'},
{id:'А12',name:'Аптека №12 — Таїрова', addr:'вул. Академіка Корольова, 112',city:'Одеса', lat:46.4311,lng:30.7601,head:'1/1',pharm:'2/2',cashier:'1/1',schedule:'Пн–Нд 9–21',status:'ok',enterprise:'Одесская аптечная сеть'},
{id:'А23',name:'Аптека №23 — Котовський', addr:'пр. Добровольського, 133', city:'Одеса', lat:46.3952,lng:30.6487,head:'0/1',pharm:'1/2',cashier:'1/1',schedule:'Пн–Нд 8–20',status:'crit',enterprise:'Одесская аптечная сеть'},
{id:'А31',name:'Аптека №31 — Пересип', addr:'вул. Балківська, 74', city:'Одеса', lat:46.5089,lng:30.7412,head:'1/1',pharm:'2/2',cashier:'2/2',schedule:'Пн–Пт 9–18',status:'ok',enterprise:'Одесская аптечная сеть'},
{id:'А44',name:'Аптека №44 — Київ Центр', addr:'вул. Хрещатик, 15', city:'Київ', lat:50.4501,lng:30.5234,head:'1/1',pharm:'3/3',cashier:'2/3',schedule:'Пн–Нд 8–22',status:'ok',enterprise:'Киевская аптечная сеть'},
{id:'А51',name:'Аптека №51 — Лівобережна',addr:'пр. Броварський, 23', city:'Київ', lat:50.4721,lng:30.6115,head:'1/1',pharm:'1/2',cashier:'1/1',schedule:'Пн–Сб 8–21',status:'warn',enterprise:'Киевская аптечная сеть'},
{id:'А67',name:'Аптека №67 — Харків', addr:'пр. Науки, 44', city:'Харків',lat:49.9935,lng:36.2304,head:'0/1',pharm:'2/3',cashier:'1/2',schedule:'Пн–Нд 9–21',status:'crit',enterprise:'Харьковская аптечная сеть'},
];
var PHARMA_SC = {
ok: {label:'Норма', cls:'badge-success',color:'#22c55e',border:'#16a34a'},
warn:{label:'Увага', cls:'badge-warning',color:'#f59e0b',border:'#d97706'},
crit:{label:'Нестача',cls:'badge-danger', color:'#ef4444',border:'#dc2626'},
};
var pharmaState = {
cur: 'table',
lmap: null,
markers: [],
filtered: PHARMACIES_DATA.slice()
};
function pharmaInit() {
if (!document.getElementById('pharma-tbody')) return;
pharmaState.filtered = PHARMACIES_DATA.slice();
pharmaRenderTable(pharmaState.filtered);
// Предзагружаем Leaflet в фоне
pharmaLoadLeaflet(function(){});
renderEnterpriseFilter();
}
function pharmaLoadLeaflet(cb) {
if (window.L) { cb(); return; }
if (document.getElementById('leaflet-js')) {
document.getElementById('leaflet-js').addEventListener('load', cb);
return;
}
var lc = document.createElement('link');
lc.rel = 'stylesheet';
lc.href = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css';
document.head.appendChild(lc);
var ls = document.createElement('script');
ls.id = 'leaflet-js';
ls.src = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js';
ls.onload = cb;
document.head.appendChild(ls);
}
function pharmaScell(v) {
var p = v.split('/'), c = +p[0], m = +p[1];
var col = c < m ? (c === 0 ? 'var(--color-destructive)' : 'var(--color-warning)') : 'var(--color-foreground)';
var fw = c < m ? '500' : '400';
return '<td style="padding:1rem;vertical-align:middle;color:' + col + ';font-weight:' + fw + ';">' + v + '</td>';
}
function pharmaRenderTable(data) {
var countEl = document.getElementById('pharma-count');
var tbody = document.getElementById('pharma-tbody');
if (!tbody) return;
if (countEl) countEl.textContent = 'Показано ' + data.length + ' з ' + PHARMACIES_DATA.length + ' аптек';
tbody.innerHTML = data.map(function(p) {
var c = PHARMA_SC[p.status];
return '<tr class="tr-hover" style="border-bottom:1px solid var(--color-border);cursor:pointer;transition:background 150ms;">' +
'<td style="padding:1rem;vertical-align:middle;">' +
'<div style="display:flex;align-items:center;gap:0.625rem;">' +
'<div style="width:2rem;height:2rem;border-radius:0.375rem;background:' + c.color + '22;color:' + c.border + ';display:flex;align-items:center;justify-content:center;font-size:0.625rem;font-weight:700;flex-shrink:0;">' + p.id + '</div>' +
'<div><div style="font-weight:500;color:var(--color-card-foreground);">' + p.name + '</div>' +
'<div style="font-size:0.75rem;color:var(--color-muted-foreground);">' + p.addr + '</div></div>' +
'</div>' +
'</td>' +
'<td style="padding:1rem;vertical-align:middle;color:var(--color-muted-foreground);">' + p.city + '</td>' +
pharmaScell(p.head) + pharmaScell(p.pharm) + pharmaScell(p.cashier) +
'<td style="padding:1rem;vertical-align:middle;color:var(--color-muted-foreground);font-size:0.8125rem;">' + p.schedule + '</td>' +
'<td style="padding:1rem;vertical-align:middle;"><span class="badge ' + c.cls + '">' + c.label + '</span></td>' +
'</tr>';
}).join('');
}
function pharmaMakeIcon(status) {
var c = PHARMA_SC[status];
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="36" viewBox="0 0 28 36">' +
'<path d="M14 0C6.268 0 0 6.268 0 14c0 9.333 14 22 14 22S28 23.333 28 14C28 6.268 21.732 0 14 0z" fill="' + c.color + '"/>' +
'<circle cx="14" cy="14" r="7" fill="white"/>' +
'<text x="14" y="18" text-anchor="middle" font-size="8" font-weight="700" fill="' + c.border + '" font-family="sans-serif">Rx</text></svg>';
return L.divIcon({html:svg, iconSize:[28,36], iconAnchor:[14,36], popupAnchor:[0,-36], className:''});
}
function pharmaRenderMarkers(data) {
pharmaState.markers.forEach(function(m){ pharmaState.lmap.removeLayer(m); });
pharmaState.markers = [];
data.forEach(function(p, i) {
var c = PHARMA_SC[p.status];
var popup = '<div style="font-family:sans-serif;min-width:190px;">' +
'<div style="font-weight:600;font-size:13px;margin-bottom:3px;">' + p.name + '</div>' +
'<div style="font-size:11px;color:#666;margin-bottom:8px;">' + p.addr + '</div>' +
'<table style="width:100%;font-size:11px;border-collapse:collapse;">' +
'<tr><td style="color:#888;padding:2px 0;">Завідуючий</td><td style="font-weight:500;text-align:right;">' + p.head + '</td></tr>' +
'<tr><td style="color:#888;padding:2px 0;">Фармацевт</td><td style="font-weight:500;text-align:right;">' + p.pharm + '</td></tr>' +
'<tr><td style="color:#888;padding:2px 0;">Касир</td><td style="font-weight:500;text-align:right;">' + p.cashier + '</td></tr>' +
'<tr><td style="color:#888;padding:2px 0;">Режим</td><td style="font-weight:500;text-align:right;">' + p.schedule + '</td></tr>' +
'</table><div style="margin-top:8px;padding:3px 8px;border-radius:20px;background:' + c.color + '22;color:' + c.border + ';font-size:11px;font-weight:600;display:inline-block;">' + c.label + '</div></div>';
var mk = L.marker([p.lat, p.lng], {icon: pharmaMakeIcon(p.status)}).addTo(pharmaState.lmap).bindPopup(popup);
pharmaState.markers.push(mk);
});
}
function pharmaRenderSidebar(data) {
var el = document.getElementById('pharma-sidebar');
if (!el) return;
el.innerHTML = data.map(function(p, i) {
var c = PHARMA_SC[p.status];
return '<div onclick="pharmaFocus(' + i + ')" class="li-hover" style="padding:0.75rem 1rem;border-bottom:1px solid var(--color-border);cursor:pointer;transition:background 150ms;">' +
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.2rem;">' +
'<div style="display:flex;align-items:center;gap:0.5rem;">' +
'<div style="width:1.75rem;height:1.75rem;border-radius:0.375rem;background:' + c.color + '22;color:' + c.border + ';display:flex;align-items:center;justify-content:center;font-size:0.5625rem;font-weight:700;flex-shrink:0;">' + p.id + '</div>' +
'<span style="font-size:0.8125rem;font-weight:500;color:var(--color-foreground);">' + p.name.replace(/Аптека №\d+ — /, '') + '</span>' +
'</div>' +
'<span class="badge ' + c.cls + '" style="font-size:0.625rem;flex-shrink:0;">' + c.label + '</span>' +
'</div>' +
'<div style="font-size:0.75rem;color:var(--color-muted-foreground);padding-left:2.25rem;">' + p.addr + '</div>' +
'</div>';
}).join('');
}
function pharmaFocus(i) {
if (pharmaState.lmap && pharmaState.markers[i]) {
pharmaState.lmap.setView(pharmaState.markers[i].getLatLng(), 15);
pharmaState.markers[i].openPopup();
}
}
function pharmaAddBaseLayer(map) {
if (typeof acoreAddMapBaseLayer === 'function') {
acoreAddMapBaseLayer(map);
return;
}
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
}).addTo(map);
}
function pharmaInitMap() {
requestAnimationFrame(function() {
requestAnimationFrame(function() {
var mapEl = document.getElementById('pharma-map');
if (!mapEl) return;
if (!pharmaState.lmap) {
pharmaState.lmap = L.map('pharma-map', {center:[48.3794,31.1656], zoom:6});
pharmaAddBaseLayer(pharmaState.lmap);
}
pharmaState.lmap.invalidateSize();
pharmaRenderMarkers(pharmaState.filtered);
pharmaRenderSidebar(pharmaState.filtered);
});
});
}
function pharmaView(v) {
pharmaState.cur = v;
var isTable = v === 'table';
var pvTable = document.getElementById('pv-table');
var pvMap = document.getElementById('pv-map');
var btnT = document.getElementById('btn-table');
var btnM = document.getElementById('btn-map');
if (!pvTable) return;
pvTable.style.display = isTable ? '' : 'none';
pvMap.style.display = isTable ? 'none' : '';
btnT.style.background = isTable ? 'var(--color-card)' : 'transparent';
btnT.style.color = isTable ? 'var(--color-foreground)' : 'var(--color-muted-foreground)';
btnM.style.background = isTable ? 'transparent' : 'var(--color-card)';
btnM.style.color = isTable ? 'var(--color-muted-foreground)' : 'var(--color-foreground)';
if (!isTable) {
pharmaLoadLeaflet(pharmaInitMap);
}
}
function pharmaFilter() {
var s = (document.getElementById('pharma-search') || {}).value || '';
var city = (document.getElementById('filter-city') || {}).value || '';
var st = (document.getElementById('filter-status') || {}).value || '';
var enterprise = (document.getElementById('filter-enterprise') || {}).value || '';
s = s.toLowerCase();
pharmaState.filtered = PHARMACIES_DATA.filter(function(p) {
return (!s || p.name.toLowerCase().indexOf(s) > -1 || p.addr.toLowerCase().indexOf(s) > -1) &&
(!city || p.city === city) &&
(!st || p.status === st) &&
(!enterprise || p.enterprise === enterprise);
});
pharmaRenderTable(pharmaState.filtered);
if (pharmaState.cur === 'map' && pharmaState.lmap) {
pharmaRenderMarkers(pharmaState.filtered);
pharmaRenderSidebar(pharmaState.filtered);
}
}
function renderEnterpriseFilter() {
var select = document.getElementById('filter-enterprise');
if (!select) return;
var enterprises = PHARMACIES_DATA
.map(function(p) { return p.enterprise; })
.filter(Boolean)
.filter(function(value, index, array) { return array.indexOf(value) === index; });
select.innerHTML = '<option value="">Все предприятия</option>' +
enterprises.map(function(e) {
return '<option value="' + e + '">' + e + '</option>';
}).join('');
}