Add RRO, portal section perms, dislocation presets, and hiring intern flow.
Ship cash-register registry, section-based portal access, configurable dislocation columns, and trial-hire intern accounts with credentials PDF.
This commit is contained in:
@@ -91,7 +91,12 @@
|
||||
<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;">
|
||||
<div style="display:flex; gap:2px; background:var(--color-muted); border-radius:var(--radius); padding:3px; 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>
|
||||
{% 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>
|
||||
@@ -105,6 +110,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dislo-columns-panel" style="display:none; margin-bottom:1rem; background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:1rem 1.25rem; box-shadow:0 1px 2px rgb(0 0 0/0.05);">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:0.75rem; flex-wrap:wrap; margin-bottom:0.75rem;">
|
||||
<div style="font-size:0.875rem; font-weight:600; color:var(--color-foreground);">{% trans "Колонки таблицы" %}</div>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<label 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>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/dislocation/columns/" hx-target="#main-content" hx-indicator="#loader" hx-disabled-elt="find button[type=submit]">
|
||||
{% csrf_token %}
|
||||
<div id="dislo-col-order" style="display:flex; flex-direction:column; gap:0.35rem; margin-bottom:0.75rem;">
|
||||
{% for col in panel_column_defs %}
|
||||
<div class="dislo-col-row" data-key="{{ col.key }}" style="display:flex; align-items:center; gap:0.5rem; font-size:0.8125rem;">
|
||||
{% if col.locked %}
|
||||
<input type="checkbox" checked disabled>
|
||||
<input type="hidden" name="columns" value="{{ col.key }}">
|
||||
{% else %}
|
||||
<input type="checkbox" name="columns" value="{{ col.key }}" {% if col.key in visible_column_keys %}checked{% endif %}>
|
||||
{% endif %}
|
||||
<span style="flex:1; color:var(--color-foreground);">{{ col.label }}{% if col.locked %} <span style="color:var(--color-muted-foreground); font-size:0.7rem;">({% trans "обяз." %})</span>{% endif %}</span>
|
||||
<button type="button" class="btn btn-ghost" style="padding:0.15rem 0.4rem; font-size:0.7rem;" onclick="disloColMove(this, -1)" title="{% trans 'Выше' %}">▲</button>
|
||||
<button type="button" class="btn btn-ghost" style="padding:0.15rem 0.4rem; font-size:0.7rem;" onclick="disloColMove(this, 1)" title="{% trans 'Ниже' %}">▼</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
|
||||
<button type="submit" class="btn btn-primary" style="font-size:0.8125rem;">{% trans "Сохранить" %}</button>
|
||||
<button type="button" class="btn btn-secondary" style="font-size:0.8125rem; border:1px solid var(--color-border);" onclick="disloToggleColumnsPanel()">{% trans "Отмена" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{{ dislocation_presets_payload|json_script:"dislo-presets-data" }}
|
||||
</div>
|
||||
|
||||
<!-- Таблица -->
|
||||
<div id="dislo-view-table">
|
||||
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0/0.05);">
|
||||
@@ -113,13 +156,9 @@
|
||||
<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>
|
||||
{% for col in visible_columns %}
|
||||
<th class="dislo-sort-th" onclick="disloSortBy('{{ col.key }}','{{ col.sort_type }}')" style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;cursor:pointer;user-select:none;white-space:nowrap;">{{ col.label }} <span class="dislo-sort-icon" data-key="{{ col.key }}" style="opacity:0.35;">⇅</span></th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -137,55 +176,29 @@
|
||||
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-organization="{{ pharmacy.organization.name|default:""|escape }}"
|
||||
data-sort-head="{% if pharmacy.head_pharmacist %}{{ pharmacy.head_pharmacist.last_name }} {{ pharmacy.head_pharmacist.first_name }}{% endif %}"
|
||||
data-sort-staff="{{ pharmacy.staff_count|default:0 }}"
|
||||
data-sort-phone="{{ pharmacy.phone|default:"" }}"
|
||||
data-sort-hours="{{ pharmacy.working_hours|default:"" }}"
|
||||
data-sort-status="{% if pharmacy.is_active %}1{% else %}0{% endif %}">
|
||||
data-sort-status="{% if pharmacy.is_active %}1{% else %}0{% endif %}"
|
||||
data-sort-region="{{ pharmacy.location_region.name|default:""|escape }}"
|
||||
data-sort-category="{{ pharmacy.category|default:"" }}"
|
||||
data-sort-curator="{% if pharmacy.curator %}{{ pharmacy.curator.last_name }} {{ pharmacy.curator.first_name }}{% endif %}"
|
||||
data-sort-manager="{% if pharmacy.responsible_manager %}{{ pharmacy.responsible_manager.last_name }} {{ pharmacy.responsible_manager.first_name }}{% endif %}"
|
||||
data-sort-license="{{ pharmacy.license_number|default:"" }}"
|
||||
data-sort-vat="{% if pharmacy.vat_category %}{{ pharmacy.get_vat_category_display }}{% endif %}"
|
||||
data-sort-email="{{ pharmacy.carbonio_email|default:"" }}">
|
||||
<td style="padding:0.75rem 0; text-align:center; color:var(--color-muted-foreground);">
|
||||
<span id="dislo-toggle-{{ pharmacy.id }}" style="display:inline-block; transition:transform 150ms;">▸</span>
|
||||
</td>
|
||||
<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>
|
||||
{% for col in visible_columns %}
|
||||
{% include "partials/dislo_cell.html" with column=col pharmacy=pharmacy no_addr_short=no_addr_short %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr id="dislo-staff-{{ pharmacy.id }}" class="dislo-staff-row" style="display:none; border-bottom:1px solid var(--color-border); background:var(--color-muted);">
|
||||
<td></td>
|
||||
<td colspan="7" style="padding:0.75rem 1rem 1rem;">
|
||||
<td colspan="{{ visible_columns|length }}" style="padding:0.75rem 1rem 1rem;">
|
||||
{% if pharmacy.active_staff %}
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(15rem, 1fr)); gap:0.5rem;">
|
||||
{% for employee in pharmacy.active_staff %}
|
||||
@@ -211,7 +224,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Аптеки не найдены" %}</td>
|
||||
<td colspan="{{ dislo_colspan }}" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Аптеки не найдены" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -603,6 +616,7 @@ function disloView(view) {
|
||||
const mapView = document.getElementById('dislo-view-map');
|
||||
const btnTable = document.getElementById('dislo-btn-table');
|
||||
const btnMap = document.getElementById('dislo-btn-map');
|
||||
const columnsPanel = document.getElementById('dislo-columns-panel');
|
||||
|
||||
if (view === 'table') {
|
||||
tableView.style.display = 'block';
|
||||
@@ -614,6 +628,7 @@ function disloView(view) {
|
||||
} else {
|
||||
tableView.style.display = 'none';
|
||||
mapView.style.display = 'block';
|
||||
if (columnsPanel) columnsPanel.style.display = 'none';
|
||||
btnTable.style.background = 'transparent';
|
||||
btnTable.style.color = 'var(--color-muted-foreground)';
|
||||
btnMap.style.background = 'var(--color-card)';
|
||||
@@ -626,6 +641,62 @@ function disloView(view) {
|
||||
}
|
||||
}
|
||||
|
||||
function disloToggleColumnsPanel() {
|
||||
const panel = document.getElementById('dislo-columns-panel');
|
||||
if (!panel) return;
|
||||
const opening = panel.style.display === 'none' || !panel.style.display;
|
||||
panel.style.display = opening ? 'block' : 'none';
|
||||
if (opening) disloView('table');
|
||||
}
|
||||
|
||||
function disloColMove(btn, dir) {
|
||||
const row = btn.closest('.dislo-col-row');
|
||||
const container = document.getElementById('dislo-col-order');
|
||||
if (!row || !container) return;
|
||||
const siblings = Array.from(container.children);
|
||||
const idx = siblings.indexOf(row);
|
||||
const targetIdx = idx + dir;
|
||||
if (targetIdx < 0 || targetIdx >= siblings.length) return;
|
||||
const target = siblings[targetIdx];
|
||||
if (dir < 0) {
|
||||
container.insertBefore(row, target);
|
||||
} else {
|
||||
container.insertBefore(target, row);
|
||||
}
|
||||
}
|
||||
|
||||
function disloApplyPreset(selectEl) {
|
||||
const code = selectEl.value;
|
||||
if (!code) return;
|
||||
const csrf = document.querySelector('#dislo-columns-panel input[name=csrfmiddlewaretoken]');
|
||||
if (window.htmx) {
|
||||
htmx.ajax('POST', '/dislocation/columns/', {
|
||||
target: '#main-content',
|
||||
swap: 'innerHTML',
|
||||
values: { preset_code: code, csrfmiddlewaretoken: csrf ? csrf.value : '' },
|
||||
headers: csrf ? { 'X-CSRFToken': csrf.value } : {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/dislocation/columns/';
|
||||
if (csrf) {
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = csrf.value;
|
||||
form.appendChild(csrfInput);
|
||||
}
|
||||
const presetInput = document.createElement('input');
|
||||
presetInput.type = 'hidden';
|
||||
presetInput.name = 'preset_code';
|
||||
presetInput.value = code;
|
||||
form.appendChild(presetInput);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function disloGetVisiblePharmacyIds() {
|
||||
const searchText = document.getElementById('dislo-search').value.toLowerCase();
|
||||
const regionFilter = document.getElementById('dislo-filter-region').value;
|
||||
|
||||
Reference in New Issue
Block a user