locations: право редактирования аптеки, формы и дислокация
Отдельное право на редактирование карточки аптеки, формы, пресеты колонок дислокации и правки видимости карточки. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -87,26 +87,29 @@
|
||||
</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="active" selected>{% 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; align-items:center;">
|
||||
<div style="display:flex; gap:0.5rem; flex-shrink:0; align-items:center;">
|
||||
<button type="button" id="dislo-btn-columns" onclick="disloToggleColumnsPanel()"
|
||||
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;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="M4 6h16M4 12h10M4 18h14"/></svg>
|
||||
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>
|
||||
<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 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>
|
||||
|
||||
@@ -114,16 +117,16 @@
|
||||
<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 style="font-size:0.75rem; color:var(--color-muted-foreground);">{% trans "Пресет" %}</label>
|
||||
<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 %}</option>
|
||||
<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 hx-post="/dislocation/columns/" hx-target="#main-content" hx-indicator="#loader" hx-disabled-elt="find button[type=submit]">
|
||||
<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 %}
|
||||
@@ -141,10 +144,46 @@
|
||||
{% 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="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>
|
||||
|
||||
@@ -284,6 +323,16 @@
|
||||
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>
|
||||
@@ -377,6 +426,12 @@ const disloData = [
|
||||
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 %}
|
||||
@@ -522,30 +577,13 @@ function disloRenderRegionTree() {
|
||||
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();
|
||||
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) {
|
||||
@@ -559,6 +597,16 @@ function disloCreateMarker(pharmacy) {
|
||||
<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;">
|
||||
@@ -567,7 +615,8 @@ function disloCreateMarker(pharmacy) {
|
||||
<small>🧑 ${pharmacy.head || '-'}</small><br/>
|
||||
<small>☎️ ${pharmacy.phone}</small><br/>
|
||||
<small>🕐 ${pharmacy.hours}</small><br/>
|
||||
<small>👥 {% trans "Персонал" %}: ${pharmacy.staff}</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/>
|
||||
@@ -611,6 +660,20 @@ function disloInitMap() {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -629,6 +692,7 @@ function disloView(view) {
|
||||
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)';
|
||||
@@ -646,6 +710,7 @@ function disloToggleColumnsPanel() {
|
||||
if (!panel) return;
|
||||
const opening = panel.style.display === 'none' || !panel.style.display;
|
||||
panel.style.display = opening ? 'block' : 'none';
|
||||
disloSetColumnsButtonActive(opening);
|
||||
if (opening) disloView('table');
|
||||
}
|
||||
|
||||
@@ -665,6 +730,75 @@ function disloColMove(btn, dir) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user