38747a93fc
- EquipmentModel (каталог: тип+бренд+модель заводятся раз) отделён от Equipment (экземпляр: серийник+локация); миграция сворачивает данные. Массовый ввод серийников, массовая выдача на локации/сотрудников - Enum-choices (тип/статус/приоритет заявок, вид работ, каналы, расходники, договоры) переведены на core.Directory с quick-add из формы - Фронт-страница /it-manager/: вкладки оборудование/заявки/работы/ расходники/провайдеры/обращения; searchable-select локаций - Тесты (10) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1132 lines
99 KiB
HTML
1132 lines
99 KiB
HTML
{% load common_tags %}
|
||
<div class="page-content">
|
||
|
||
<div style="margin-bottom:1.5rem;">
|
||
<h1 style="font-size:1.25rem; font-weight:500; color:var(--color-foreground);">IT-відділ</h1>
|
||
<p style="font-size:0.875rem; color:var(--color-muted-foreground); margin-top:0.25rem;">Інвентаризація обладнання, черга заявок та журнал виконаних робіт</p>
|
||
</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="it-tab-btn it-tab-active" data-tab="equipment" onclick="itTabSwitch('equipment')"
|
||
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);">Обладнання</button>
|
||
<button type="button" class="it-tab-btn" data-tab="tickets" onclick="itTabSwitch('tickets')"
|
||
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);">Заявки</button>
|
||
<button type="button" class="it-tab-btn" data-tab="worklog" onclick="itTabSwitch('worklog')"
|
||
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);">Виконані роботи</button>
|
||
<button type="button" class="it-tab-btn" data-tab="consumables" onclick="itTabSwitch('consumables')"
|
||
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);">Витратні матеріали</button>
|
||
<button type="button" class="it-tab-btn" data-tab="providers" onclick="itTabSwitch('providers')"
|
||
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);">Провайдери</button>
|
||
<button type="button" class="it-tab-btn" data-tab="servicecalls" onclick="itTabSwitch('servicecalls')"
|
||
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);">Обращення</button>
|
||
</div>
|
||
|
||
<!-- ══ ВКЛАДКА: ОБЛАДНАННЯ ══ -->
|
||
<div id="it-panel-equipment" class="it-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;">Всього одиниць</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ equipment_list|length }}</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;">На складі</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ equipment_counts.stock }}</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;">Видано</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ equipment_counts.assigned }}</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;">В ремонті / списано</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ equipment_counts.repair|add:equipment_counts.written_off }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if equipment_form_message %}
|
||
<div style="background:rgba(22,163,74,0.08); border:1px solid rgba(22,163,74,0.3); border-radius:0.75rem; padding:0.75rem 1rem; margin-bottom:1rem; font-size:0.875rem; color:#16a34a;">{{ equipment_form_message }}</div>
|
||
{% endif %}
|
||
{% if equipment_form_error %}
|
||
<div style="background:color-mix(in srgb, var(--color-destructive) 8%, transparent); border:1px solid color-mix(in srgb, var(--color-destructive) 30%, transparent); border-radius:0.75rem; padding:0.75rem 1rem; margin-bottom:1rem; font-size:0.875rem; color:var(--color-destructive);">{{ equipment_form_error }}</div>
|
||
{% endif %}
|
||
|
||
<div style="display:flex; justify-content:flex-end; gap:0.5rem; margin-bottom:1rem;">
|
||
<button type="button" class="btn btn-secondary" style="border:1px solid var(--color-border);" onclick="itPanelOpen('it-equipment-models-panel')">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
||
Каталог моделей ({{ equipment_models|length }})
|
||
</button>
|
||
<button type="button" class="btn btn-primary" onclick="itPanelOpen('it-equipment-panel')">
|
||
<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 id="it-equipment-panel-overlay" onclick="itPanelClose('it-equipment-panel')" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:1000;"></div>
|
||
<div id="it-equipment-panel" style="position:fixed; top:0; right:0; height:100vh; width:28rem; max-width:92vw; background:var(--color-card); box-shadow:-8px 0 24px rgba(0,0,0,0.18); z-index:1001; transform:translateX(100%); transition:transform 250ms ease; overflow-y:auto;">
|
||
<form hx-post="/it-manager/equipment/bulk-create/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]">
|
||
{% csrf_token %}
|
||
<div style="padding:1rem 1.25rem; border-bottom:1px solid var(--color-border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--color-card); z-index:1;">
|
||
<span style="font-size:0.9375rem; font-weight:600; color:var(--color-foreground);">Додати екземпляри</span>
|
||
<button type="button" onclick="itPanelClose('it-equipment-panel')" class="btn btn-ghost btn-icon" aria-label="Закрити">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div style="padding:1.25rem; display:flex; flex-direction:column; gap:1rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Модель з каталогу <span style="color:var(--color-destructive);">*</span></label>
|
||
<div class="searchable-select" data-placeholder="Пошук моделі за назвою чи брендом...">
|
||
<select name="equipment_model_id" required hidden>
|
||
<option value="">—</option>
|
||
{% for m in equipment_models %}
|
||
<option value="{{ m.id }}">{{ m.equipment_type.name }} · {{ m }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.375rem;">Немає потрібної? <a href="#" onclick="itPanelClose('it-equipment-panel'); itPanelOpen('it-equipment-models-panel'); return false;" style="color:var(--color-primary);">Додати модель у каталог</a></div>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Серійні номери <span style="color:var(--color-destructive);">*</span></label>
|
||
<textarea name="serial_numbers" required placeholder="По одному в рядку — вставте список серійників, наприклад HGC09SJD200 HGC09VB5ZKX HGC09VT0P2H"
|
||
style="width:100%; min-height:8rem; resize:vertical; background:var(--color-card); border:1px solid var(--color-input); border-radius:var(--radius); padding:0.4375rem 0.75rem; font-size:0.875rem; color:var(--color-foreground); font-family:var(--font-mono, monospace); outline:none; box-sizing:border-box;"></textarea>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.375rem;">Скільки серійників — стільки одиниць буде створено. Дублікати й вже наявні пропускаються.</div>
|
||
</div>
|
||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.75rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Дата покупки</label>
|
||
<input type="date" name="purchase_date" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Гарантія до</label>
|
||
<input type="date" name="warranty_until" class="input" style="width:100%;">
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Одразу видати на локацію <span style="font-weight:400; color:var(--color-muted-foreground);">(необов'язково)</span></label>
|
||
<div class="searchable-select" data-placeholder="Пошук за адресою чи номером аптеки...">
|
||
<select name="location_id" hidden>
|
||
<option value="">— на склад</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}">{{ location }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex; gap:0.5rem;">
|
||
<button type="submit" class="btn btn-primary" style="flex:1;">
|
||
Створити екземпляри
|
||
<span class="btn-label-loading htmx-indicator">Збереження…</span>
|
||
</button>
|
||
<button type="button" class="btn btn-secondary" style="border:1px solid var(--color-border);" onclick="itPanelClose('it-equipment-panel')">Скасувати</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Виїзна панель: каталог моделей -->
|
||
<div id="it-equipment-models-panel-overlay" onclick="itPanelClose('it-equipment-models-panel')" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:1000;"></div>
|
||
<div id="it-equipment-models-panel" style="position:fixed; top:0; right:0; height:100vh; width:28rem; max-width:92vw; background:var(--color-card); box-shadow:-8px 0 24px rgba(0,0,0,0.18); z-index:1001; transform:translateX(100%); transition:transform 250ms ease; overflow-y:auto;">
|
||
<div style="padding:1rem 1.25rem; border-bottom:1px solid var(--color-border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--color-card); z-index:1;">
|
||
<span style="font-size:0.9375rem; font-weight:600; color:var(--color-foreground);">Каталог моделей</span>
|
||
<button type="button" onclick="itPanelClose('it-equipment-models-panel')" class="btn btn-ghost btn-icon" aria-label="Закрити">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<form hx-post="/it-manager/equipment/models/create/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]"
|
||
style="padding:1.25rem; display:flex; flex-direction:column; gap:1rem; border-bottom:1px solid var(--color-border);">
|
||
{% csrf_token %}
|
||
<div style="font-size:0.8125rem; font-weight:600; color:var(--color-foreground);">Нова модель</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Тип <span style="color:var(--color-destructive);">*</span></label>
|
||
<select name="equipment_type" required class="input" style="width:100%;" onchange="dqaHandleChange(this, 'equipment_type', 'code')">
|
||
{% for value, label in equipment_types %}
|
||
<option value="{{ value }}">{{ label }}</option>
|
||
{% endfor %}
|
||
<option value="__new__">+ Додати новий тип...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Бренд</label>
|
||
<select name="brand_id" class="input" style="width:100%;" onchange="dqaHandleChange(this, 'equipment_brand', 'id')">
|
||
<option value="">—</option>
|
||
{% for brand in equipment_brands %}
|
||
<option value="{{ brand.id }}">{{ brand.name }}</option>
|
||
{% endfor %}
|
||
<option value="__new__">+ Додати новий бренд...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Модель <span style="color:var(--color-destructive);">*</span></label>
|
||
<input type="text" name="name" required placeholder="Наприклад: RB952Ui-5ac2nD" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Гарантія (міс.) <span style="font-weight:400; color:var(--color-muted-foreground);">(необов'язково)</span></label>
|
||
<input type="number" min="0" name="warranty_months" class="input" style="width:100%;">
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%;">
|
||
Додати модель
|
||
<span class="btn-label-loading htmx-indicator">Збереження…</span>
|
||
</button>
|
||
</form>
|
||
<div style="padding:1rem 1.25rem;">
|
||
<div style="font-size:0.8125rem; font-weight:600; color:var(--color-foreground); margin-bottom:0.75rem;">У каталозі ({{ equipment_models|length }})</div>
|
||
<div style="display:flex; flex-direction:column; gap:0.5rem;">
|
||
{% for m in equipment_models %}
|
||
<div style="display:flex; align-items:center; justify-content:space-between; gap:0.5rem; padding:0.5rem 0.75rem; background:var(--color-muted); border-radius:var(--radius);">
|
||
<div style="min-width:0;">
|
||
<div style="font-size:0.8125rem; color:var(--color-foreground); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ m }}</div>
|
||
<div style="font-size:0.6875rem; color:var(--color-muted-foreground);">{{ m.equipment_type.name }}</div>
|
||
</div>
|
||
<span class="badge badge-muted" style="flex-shrink:0;">{{ m.units_total }} од.</span>
|
||
</div>
|
||
{% empty %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">Каталог порожній — додайте першу модель.</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Панель масової видачі (з'являється при виборі екземплярів) -->
|
||
<div id="it-equipment-bulk-assign" style="display:none; background:var(--color-card); border:1px solid var(--color-primary); border-radius:0.75rem; padding:1rem 1.25rem; margin-bottom:1rem;">
|
||
<form hx-post="/it-manager/equipment/bulk-assign/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]"
|
||
style="display:flex; align-items:flex-end; gap:0.75rem; flex-wrap:wrap;">
|
||
{% csrf_token %}
|
||
<div id="it-bulk-assign-inputs"></div>
|
||
<div style="font-size:0.875rem; color:var(--color-foreground); font-weight:500; align-self:center;">Видати <span id="it-bulk-assign-count">0</span> од.:</div>
|
||
<div style="min-width:16rem; flex:1;">
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">На локацію</label>
|
||
<div class="searchable-select" data-placeholder="Пошук за адресою чи номером аптеки...">
|
||
<select name="location_id" hidden>
|
||
<option value="">—</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}">{{ location }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div style="min-width:12rem;">
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Співробітнику</label>
|
||
<select name="assigned_to_id" class="input" style="width:100%;">
|
||
<option value="">—</option>
|
||
{% for emp in it_employees %}
|
||
<option value="{{ emp.id }}">{{ emp.last_name }} {{ emp.first_name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">Видати</button>
|
||
<button type="button" class="btn btn-ghost" onclick="itBulkClearSelection()">Скасувати вибір</button>
|
||
</form>
|
||
</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="width:2.25rem; text-align:center;"><input type="checkbox" onclick="itBulkToggleAll(this)" title="Виділити всі на сторінці" style="cursor:pointer;"></th>
|
||
<th style="width:2rem;"></th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Тип / Бренд / Модель</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">С/н · Інв. №</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Локація</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Видано</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Статус</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for equipment in equipment_page %}
|
||
<tr class="it-row" style="border-bottom:1px solid var(--color-border); cursor:pointer;" onclick="itToggleRow('equipment-{{ equipment.id }}')">
|
||
<td style="padding:0.75rem 0; text-align:center;" onclick="event.stopPropagation();">
|
||
<input type="checkbox" class="it-equip-check" value="{{ equipment.id }}" onchange="itBulkUpdate()" style="cursor:pointer;">
|
||
</td>
|
||
<td style="padding:0.75rem 0; text-align:center; color:var(--color-muted-foreground);">
|
||
<span id="it-toggle-equipment-{{ equipment.id }}" style="display:inline-block; transition:transform 150ms;">▸</span>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">
|
||
<strong>{{ equipment.equipment_type.name }}</strong>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{{ equipment.brand.name|default:"" }} {{ equipment.model }}</div>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground); font-size:0.8125rem;">
|
||
{{ equipment.serial_number }}{% if equipment.inventory_number %} · {{ equipment.inventory_number }}{% endif %}
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground);">{{ equipment.location|default:"—" }}</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">{{ equipment.assigned_to.get_full_name|default:"—" }}</td>
|
||
<td style="padding:0.75rem 1rem;">
|
||
{% if equipment.status.code == 'stock' %}<span class="badge badge-muted">На складі</span>
|
||
{% elif equipment.status.code == 'assigned' %}<span class="badge badge-success">Видано</span>
|
||
{% elif equipment.status.code == 'repair' %}<span class="badge badge-warning">В ремонті</span>
|
||
{% else %}<span class="badge badge-danger">Списано</span>{% endif %}
|
||
</td>
|
||
</tr>
|
||
<tr id="it-row-equipment-{{ equipment.id }}" class="it-detail-row" style="display:none; border-bottom:1px solid var(--color-border); background:var(--color-muted);">
|
||
<td></td>
|
||
<td></td>
|
||
<td colspan="5" style="padding:1rem;">
|
||
<form hx-post="/it-manager/equipment/{{ equipment.id }}/update/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]"
|
||
style="display:flex; align-items:flex-end; gap:0.75rem; flex-wrap:wrap;" onclick="event.stopPropagation();">
|
||
{% csrf_token %}
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Статус</label>
|
||
<select name="status" class="input" style="min-width:10rem;">
|
||
{% for value, label in equipment_statuses %}
|
||
<option value="{{ value }}" {% if value == equipment.status.code %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Видано кому</label>
|
||
<select name="assigned_to_id" class="input" style="min-width:14rem;">
|
||
<option value="">— (на складі)</option>
|
||
{% for emp in it_employees %}
|
||
<option value="{{ emp.id }}" {% if emp.id == equipment.assigned_to_id %}selected{% endif %}>{{ emp.last_name }} {{ emp.first_name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
Зберегти
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
{% if equipment.notes %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground); flex-basis:100%;">Примітки: {{ equipment.notes }}</div>
|
||
{% endif %}
|
||
{% if equipment.warranty_until %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground); flex-basis:100%;">Гарантія: {{ equipment.warranty_status }} ({{ equipment.warranty_until|date:"d.m.Y" }})</div>
|
||
{% endif %}
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="7" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">Обладнання ще не додано</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% include "partials/pagination.html" with page=equipment_page base_url="/it-manager/" page_param="equipment_page" size_param="equipment_per_page" item_label="одиниць обладнання" %}
|
||
</div>
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: ОБЛАДНАННЯ ══ -->
|
||
|
||
<!-- ══ ВКЛАДКА: ЗАЯВКИ ══ -->
|
||
<div id="it-panel-tickets" class="it-panel" style="display:none;">
|
||
|
||
<div style="display:grid; grid-template-columns:repeat(5, 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;">Нові</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ ticket_counts.new }}</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;">В роботі</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ ticket_counts.in_progress }}</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;">Очікування</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ ticket_counts.waiting }}</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;">Вирішені</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ ticket_counts.resolved|add:ticket_counts.closed }}</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;">Прострочені</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:{% if ticket_overdue_count %}var(--color-destructive){% else %}var(--color-foreground){% endif %};">{{ ticket_overdue_count }}</div>
|
||
</div>
|
||
</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="width:2rem;"></th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">№ / Тема</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Пріоритет</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Заявник</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Виконавець</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Дата</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Статус</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for ticket in tickets_page %}
|
||
<tr class="it-row" style="border-bottom:1px solid var(--color-border); cursor:pointer;" onclick="itToggleRow('ticket-{{ ticket.id }}')">
|
||
<td style="padding:0.75rem 0; text-align:center; color:var(--color-muted-foreground);">
|
||
<span id="it-toggle-ticket-{{ ticket.id }}" style="display:inline-block; transition:transform 150ms;">▸</span>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">
|
||
<strong>{{ ticket.number }}</strong>
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{{ ticket.title }}{% if ticket.is_overdue %} <span class="badge badge-danger">прострочено</span>{% endif %}</div>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem;">
|
||
{% if ticket.priority.code == 'critical' %}<span class="badge badge-danger">Критичний</span>
|
||
{% elif ticket.priority.code == 'high' %}<span class="badge badge-warning">Високий</span>
|
||
{% elif ticket.priority.code == 'medium' %}<span class="badge badge-info">Середній</span>
|
||
{% else %}<span class="badge badge-muted">Низький</span>{% endif %}
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground);">{{ ticket.requester.get_full_name|default:ticket.requester.username }}</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground);">{{ ticket.assignee.get_full_name|default:"—" }}</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground); font-size:0.8125rem;">{{ ticket.created_at|date:"d.m.Y" }}</td>
|
||
<td style="padding:0.75rem 1rem;">
|
||
{% if ticket.status.code == 'new' %}<span class="badge badge-info">Нова</span>
|
||
{% elif ticket.status.code == 'in_progress' %}<span class="badge badge-warning">В роботі</span>
|
||
{% elif ticket.status.code == 'waiting' %}<span class="badge badge-muted">Очікування</span>
|
||
{% elif ticket.status.code == 'resolved' %}<span class="badge badge-success">Вирішена</span>
|
||
{% else %}<span class="badge badge-muted">Закрита</span>{% endif %}
|
||
</td>
|
||
</tr>
|
||
<tr id="it-row-ticket-{{ ticket.id }}" class="it-detail-row" style="display:none; border-bottom:1px solid var(--color-border); background:var(--color-muted);">
|
||
<td></td>
|
||
<td colspan="6" style="padding:1rem;">
|
||
<div style="font-size:0.8125rem; color:var(--color-foreground); margin-bottom:0.75rem; white-space:pre-wrap;">{{ ticket.description }}</div>
|
||
|
||
{% if ticket.comments.all %}
|
||
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-bottom:0.75rem;">
|
||
{% for comment in ticket.comments.all %}
|
||
<div style="background:var(--color-card); border:1px solid var(--color-border); border-radius:0.5rem; padding:0.5rem 0.75rem;">
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); display:flex; justify-content:space-between;">
|
||
<span>{{ comment.author.get_full_name|default:comment.author.username }} · {{ comment.created_at|date:"d.m.Y H:i" }}</span>
|
||
{% if comment.is_internal %}<span class="badge badge-muted">внутрішній</span>{% endif %}
|
||
</div>
|
||
<div style="font-size:0.8125rem; color:var(--color-foreground); margin-top:0.25rem; white-space:pre-wrap;">{{ comment.text }}</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<form hx-post="/it-manager/tickets/{{ ticket.id }}/update/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]"
|
||
style="display:flex; align-items:flex-end; gap:0.75rem; flex-wrap:wrap; margin-bottom:0.75rem;" onclick="event.stopPropagation();">
|
||
{% csrf_token %}
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Статус</label>
|
||
<select name="status" class="input" style="min-width:10rem;">
|
||
{% for value, label in ticket_statuses %}
|
||
<option value="{{ value }}" {% if value == ticket.status.code %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Виконавець</label>
|
||
<select name="assignee_id" class="input" style="min-width:12rem;">
|
||
<option value="">— не призначено</option>
|
||
{% for emp in it_staff %}
|
||
<option value="{{ emp.id }}" {% if emp.id == ticket.assignee_id %}selected{% endif %}>{{ emp.last_name }} {{ emp.first_name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
Зберегти
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
</form>
|
||
|
||
<form hx-post="/it-manager/tickets/{{ ticket.id }}/comment/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]"
|
||
style="display:flex; gap:0.5rem; align-items:flex-start;" onclick="event.stopPropagation();">
|
||
{% csrf_token %}
|
||
<input type="text" name="text" placeholder="Коментар..." class="input" style="flex:1;">
|
||
<label style="display:flex; align-items:center; gap:0.25rem; font-size:0.75rem; color:var(--color-muted-foreground); white-space:nowrap; padding-top:0.5rem;">
|
||
<input type="checkbox" name="is_internal"> внутрішній
|
||
</label>
|
||
<button type="submit" class="btn btn-secondary" style="border:1px solid var(--color-border);">
|
||
Додати
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="7" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">Заявок поки немає</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% include "partials/pagination.html" with page=tickets_page base_url="/it-manager/" page_param="tickets_page" size_param="tickets_per_page" item_label="заявок" %}
|
||
</div>
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: ЗАЯВКИ ══ -->
|
||
|
||
<!-- ══ ВКЛАДКА: ВИКОНАНІ РОБОТИ ══ -->
|
||
<div id="it-panel-worklog" class="it-panel" style="display:none;">
|
||
|
||
<div style="display:grid; grid-template-columns:repeat(2, 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;">Записів цього місяця</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ month_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;">Годин цього місяця</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ month_hours }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex; justify-content:flex-end; margin-bottom:1rem;">
|
||
<button type="button" class="btn btn-primary" onclick="itPanelOpen('it-worklog-panel')">
|
||
<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 id="it-worklog-panel-overlay" onclick="itPanelClose('it-worklog-panel')" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:1000;"></div>
|
||
<div id="it-worklog-panel" style="position:fixed; top:0; right:0; height:100vh; width:26rem; max-width:92vw; background:var(--color-card); box-shadow:-8px 0 24px rgba(0,0,0,0.18); z-index:1001; transform:translateX(100%); transition:transform 250ms ease; overflow-y:auto;">
|
||
<form hx-post="/it-manager/worklog/create/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]">
|
||
{% csrf_token %}
|
||
<div style="padding:1rem 1.25rem; border-bottom:1px solid var(--color-border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--color-card); z-index:1;">
|
||
<span style="font-size:0.9375rem; font-weight:600; color:var(--color-foreground);">Додати запис про виконану роботу</span>
|
||
<button type="button" onclick="itPanelClose('it-worklog-panel')" class="btn btn-ghost btn-icon" aria-label="Закрити">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div style="padding:1.25rem; display:flex; flex-direction:column; gap:1rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Дата</label>
|
||
<input type="date" name="date" value="{% now 'Y-m-d' %}" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Вид робіт <span style="color:var(--color-destructive);">*</span></label>
|
||
<select name="work_type" required class="input" style="width:100%;" onchange="dqaHandleChange(this, 'worklog_type', 'code')">
|
||
{% for value, label in worklog_types %}
|
||
<option value="{{ value }}">{{ label }}</option>
|
||
{% endfor %}
|
||
<option value="__new__">+ Додати новий вид робіт...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Локація</label>
|
||
<div class="searchable-select" data-placeholder="Пошук за адресою чи номером аптеки...">
|
||
<select name="location_id" hidden>
|
||
<option value="">—</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}">{{ location }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Витрачено годин</label>
|
||
<input type="number" step="0.5" min="0" name="hours_spent" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Обладнання</label>
|
||
<select name="equipment_id" class="input" style="width:100%;">
|
||
<option value="">—</option>
|
||
{% for equipment in equipment_list %}
|
||
<option value="{{ equipment.id }}">{{ equipment.equipment_model }} [{{ equipment.serial_number }}]</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Заявка</label>
|
||
<select name="ticket_id" class="input" style="width:100%;">
|
||
<option value="">—</option>
|
||
{% for ticket in tickets %}
|
||
<option value="{{ ticket.id }}">{{ ticket.number }} — {{ ticket.title }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Опис робіт <span style="color:var(--color-destructive);">*</span></label>
|
||
<textarea name="description" required style="width:100%; min-height:4rem; resize:vertical; background:var(--color-card); border:1px solid var(--color-input); border-radius:var(--radius); padding:0.4375rem 0.75rem; font-size:0.875rem; color:var(--color-foreground); font-family:var(--font-sans); outline:none; box-sizing:border-box;"></textarea>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Результат</label>
|
||
<textarea name="result" style="width:100%; min-height:3.5rem; resize:vertical; background:var(--color-card); border:1px solid var(--color-input); border-radius:var(--radius); padding:0.4375rem 0.75rem; font-size:0.875rem; color:var(--color-foreground); font-family:var(--font-sans); outline:none; box-sizing:border-box;"></textarea>
|
||
</div>
|
||
<div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%; justify-content:center;">
|
||
Додати запис
|
||
<span class="btn-label-loading htmx-indicator">Збереження…</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</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;">Дата</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Виконавець</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Вид робіт</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Опис</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Локація</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Годин</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for log in worklogs_page %}
|
||
<tr style="border-bottom:1px solid var(--color-border);">
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground); white-space:nowrap;">{{ log.date|date:"d.m.Y" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground);">{{ log.technician.get_full_name|default:log.technician.username }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground);">{{ log.work_type.name }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground); max-width:22rem;">{{ log.description|truncatechars:120 }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground); font-size:0.8125rem;">{{ log.location|default:"—" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground);">{{ log.hours_spent|default:"—" }}</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="6" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">Записів ще немає</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% include "partials/pagination.html" with page=worklogs_page base_url="/it-manager/" page_param="worklogs_page" size_param="worklogs_per_page" item_label="записів" %}
|
||
</div>
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: ВИКОНАНІ РОБОТИ ══ -->
|
||
|
||
<!-- ══ ВКЛАДКА: ВИТРАТНІ МАТЕРІАЛИ ══ -->
|
||
<div id="it-panel-consumables" class="it-panel" style="display:none;">
|
||
|
||
<div style="display:grid; grid-template-columns:repeat(2, 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;">Всього позицій</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ consumables_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;">Закінчується / на заміну</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:{% if consumable_low_count %}var(--color-destructive){% else %}var(--color-foreground){% endif %};">{{ consumable_low_count }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex; justify-content:flex-end; margin-bottom:1rem;">
|
||
<button type="button" class="btn btn-primary" onclick="itPanelOpen('it-consumable-panel')">
|
||
<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 id="it-consumable-panel-overlay" onclick="itPanelClose('it-consumable-panel')" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:1000;"></div>
|
||
<div id="it-consumable-panel" style="position:fixed; top:0; right:0; height:100vh; width:26rem; max-width:92vw; background:var(--color-card); box-shadow:-8px 0 24px rgba(0,0,0,0.18); z-index:1001; transform:translateX(100%); transition:transform 250ms ease; overflow-y:auto;">
|
||
<form hx-post="/it-manager/consumables/create/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]">
|
||
{% csrf_token %}
|
||
<div style="padding:1rem 1.25rem; border-bottom:1px solid var(--color-border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--color-card); z-index:1;">
|
||
<span style="font-size:0.9375rem; font-weight:600; color:var(--color-foreground);">Додати витратний матеріал</span>
|
||
<button type="button" onclick="itPanelClose('it-consumable-panel')" class="btn btn-ghost btn-icon" aria-label="Закрити">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div style="padding:1.25rem; display:flex; flex-direction:column; gap:1rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Тип <span style="color:var(--color-destructive);">*</span></label>
|
||
<select name="consumable_type" required class="input" style="width:100%;" onchange="dqaHandleChange(this, 'consumable_type', 'code')">
|
||
{% for value, label in consumable_types %}
|
||
<option value="{{ value }}">{{ label }}</option>
|
||
{% endfor %}
|
||
<option value="__new__">+ Додати новий тип...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Колір</label>
|
||
<select name="color" class="input" style="width:100%;" onchange="dqaHandleChange(this, 'consumable_color', 'code')">
|
||
{% for value, label in consumable_colors %}
|
||
<option value="{{ value }}">{{ label }}</option>
|
||
{% endfor %}
|
||
<option value="__new__">+ Додати новий колір...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Виробник</label>
|
||
<input type="text" name="brand" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Модель / артикул <span style="color:var(--color-destructive);">*</span></label>
|
||
<input type="text" name="model_number" required class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Принтер</label>
|
||
<select name="equipment_id" class="input" style="width:100%;">
|
||
<option value="">—</option>
|
||
{% for printer in printers %}
|
||
<option value="{{ printer.id }}">{{ printer.equipment_model }} [{{ printer.serial_number }}]</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Локація зберігання</label>
|
||
<div class="searchable-select" data-placeholder="Пошук за адресою чи номером аптеки...">
|
||
<select name="location_id" hidden>
|
||
<option value="">—</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}">{{ location }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Ресурс (сторінок)</label>
|
||
<input type="number" min="0" name="page_yield" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Дата покупки</label>
|
||
<input type="date" name="purchase_date" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Примітки</label>
|
||
<textarea name="notes" style="width:100%; min-height:3rem; resize:vertical; background:var(--color-card); border:1px solid var(--color-input); border-radius:var(--radius); padding:0.4375rem 0.75rem; font-size:0.875rem; color:var(--color-foreground); font-family:var(--font-sans); outline:none; box-sizing:border-box;"></textarea>
|
||
</div>
|
||
<div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%; justify-content:center;">
|
||
Додати
|
||
<span class="btn-label-loading htmx-indicator">Збереження…</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</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="width:2rem;"></th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Тип / Модель</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Колір</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Принтер</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Статус</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for consumable in consumables_page %}
|
||
<tr class="it-row" style="border-bottom:1px solid var(--color-border); cursor:pointer;" onclick="itToggleRow('consumable-{{ consumable.id }}')">
|
||
<td style="padding:0.75rem 0; text-align:center; color:var(--color-muted-foreground);">
|
||
<span id="it-toggle-consumable-{{ consumable.id }}" style="display:inline-block; transition:transform 150ms;">▸</span>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">
|
||
<strong>{{ consumable.consumable_type.name }}</strong>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{{ consumable.brand }} {{ consumable.model_number }}</div>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground);">{{ consumable.color.name }}</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground);">{{ consumable.equipment|default:"—" }}</td>
|
||
<td style="padding:0.75rem 1rem;">
|
||
{% if consumable.is_low %}<span class="badge badge-danger">{{ consumable.status.name }}</span>
|
||
{% elif consumable.status.code == 'installed' %}<span class="badge badge-success">{{ consumable.status.name }}</span>
|
||
{% else %}<span class="badge badge-muted">{{ consumable.status.name }}</span>{% endif %}
|
||
</td>
|
||
</tr>
|
||
<tr id="it-row-consumable-{{ consumable.id }}" class="it-detail-row" style="display:none; border-bottom:1px solid var(--color-border); background:var(--color-muted);">
|
||
<td></td>
|
||
<td colspan="4" style="padding:1rem;">
|
||
<form hx-post="/it-manager/consumables/{{ consumable.id }}/update/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]"
|
||
style="display:flex; align-items:flex-end; gap:0.75rem; flex-wrap:wrap;" onclick="event.stopPropagation();">
|
||
{% csrf_token %}
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Статус</label>
|
||
<select name="status" class="input" style="min-width:12rem;">
|
||
{% for value, label in consumable_statuses %}
|
||
<option value="{{ value }}" {% if value == consumable.status.code %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
Зберегти
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
{% if consumable.page_yield %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">Ресурс: {{ consumable.page_yield }} стор.</div>
|
||
{% endif %}
|
||
{% if consumable.notes %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground); flex-basis:100%;">Примітки: {{ consumable.notes }}</div>
|
||
{% endif %}
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="5" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">Витратних матеріалів ще не додано</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% include "partials/pagination.html" with page=consumables_page base_url="/it-manager/" page_param="consumables_page" size_param="consumables_per_page" item_label="матеріалів" %}
|
||
</div>
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: ВИТРАТНІ МАТЕРІАЛИ ══ -->
|
||
|
||
<!-- ══ ВКЛАДКА: ПРОВАЙДЕРИ ══ -->
|
||
<div id="it-panel-providers" class="it-panel" style="display:none;">
|
||
|
||
<div style="display:grid; grid-template-columns:repeat(3, 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;">Провайдерів</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ providers|length }}</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;">Договорів</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ contracts_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;">Закінчуються за 30 днів</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:{% if contracts_expiring_count %}var(--color-destructive){% else %}var(--color-foreground){% endif %};">{{ contracts_expiring_count }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex; justify-content:flex-end; gap:0.5rem; margin-bottom:1rem;">
|
||
<button type="button" class="btn btn-secondary" onclick="itPanelOpen('it-provider-panel')">
|
||
<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>
|
||
<button type="button" class="btn btn-primary" onclick="itPanelOpen('it-contract-panel')">
|
||
<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 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; margin-bottom:1.25rem;">
|
||
<div style="padding:0.875rem 1.25rem; border-bottom:1px solid var(--color-border);">
|
||
<span style="font-size:0.8125rem; font-weight:500; color:var(--color-foreground);">Провайдери</span>
|
||
</div>
|
||
<div style="display:flex; flex-wrap:wrap; gap:0.625rem; padding:1rem 1.25rem;">
|
||
{% for provider in providers %}
|
||
<div style="display:flex; align-items:center; gap:0.5rem; background:var(--color-muted); border-radius:0.5rem; padding:0.5rem 0.75rem;">
|
||
<div>
|
||
<div style="font-size:0.8125rem; font-weight:500; color:var(--color-foreground);">{{ provider.name }}</div>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground);">{{ provider.phone|default:"—" }}{% if provider.website %} · {{ provider.website }}{% endif %}</div>
|
||
</div>
|
||
<span class="badge badge-info">{{ provider.active_contracts_count }}</span>
|
||
</div>
|
||
{% empty %}
|
||
<div style="color:var(--color-muted-foreground); font-size:0.8125rem;">Провайдерів ще немає</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Виїзна панель: новий провайдер -->
|
||
<div id="it-provider-panel-overlay" onclick="itPanelClose('it-provider-panel')" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:1000;"></div>
|
||
<div id="it-provider-panel" style="position:fixed; top:0; right:0; height:100vh; width:26rem; max-width:92vw; background:var(--color-card); box-shadow:-8px 0 24px rgba(0,0,0,0.18); z-index:1001; transform:translateX(100%); transition:transform 250ms ease; overflow-y:auto;">
|
||
<form hx-post="/it-manager/providers/create/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]">
|
||
{% csrf_token %}
|
||
<div style="padding:1rem 1.25rem; border-bottom:1px solid var(--color-border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--color-card); z-index:1;">
|
||
<span style="font-size:0.9375rem; font-weight:600; color:var(--color-foreground);">Новий провайдер</span>
|
||
<button type="button" onclick="itPanelClose('it-provider-panel')" class="btn btn-ghost btn-icon" aria-label="Закрити">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div style="padding:1.25rem; display:flex; flex-direction:column; gap:1rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Назва <span style="color:var(--color-destructive);">*</span></label>
|
||
<input type="text" name="name" required class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Телефон</label>
|
||
<input type="text" name="phone" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Email</label>
|
||
<input type="email" name="email" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Сайт</label>
|
||
<input type="url" name="website" placeholder="https://" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Телефон техпідтримки</label>
|
||
<input type="text" name="support_phone" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Email техпідтримки</label>
|
||
<input type="email" name="support_email" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Примітки</label>
|
||
<textarea name="notes" style="width:100%; min-height:3rem; resize:vertical; background:var(--color-card); border:1px solid var(--color-input); border-radius:var(--radius); padding:0.4375rem 0.75rem; font-size:0.875rem; color:var(--color-foreground); font-family:var(--font-sans); outline:none; box-sizing:border-box;"></textarea>
|
||
</div>
|
||
<div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%; justify-content:center;">
|
||
Додати провайдера
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Виїзна панель: новий договір -->
|
||
<div id="it-contract-panel-overlay" onclick="itPanelClose('it-contract-panel')" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:1000;"></div>
|
||
<div id="it-contract-panel" style="position:fixed; top:0; right:0; height:100vh; width:26rem; max-width:92vw; background:var(--color-card); box-shadow:-8px 0 24px rgba(0,0,0,0.18); z-index:1001; transform:translateX(100%); transition:transform 250ms ease; overflow-y:auto;">
|
||
<form hx-post="/it-manager/contracts/create/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]">
|
||
{% csrf_token %}
|
||
<div style="padding:1rem 1.25rem; border-bottom:1px solid var(--color-border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--color-card); z-index:1;">
|
||
<span style="font-size:0.9375rem; font-weight:600; color:var(--color-foreground);">Новий договір</span>
|
||
<button type="button" onclick="itPanelClose('it-contract-panel')" class="btn btn-ghost btn-icon" aria-label="Закрити">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div style="padding:1.25rem; display:flex; flex-direction:column; gap:1rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Провайдер <span style="color:var(--color-destructive);">*</span></label>
|
||
<select name="provider_id" required class="input" style="width:100%;">
|
||
<option value="">—</option>
|
||
{% for provider in providers %}
|
||
<option value="{{ provider.id }}">{{ provider.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Локація <span style="color:var(--color-destructive);">*</span></label>
|
||
<div class="searchable-select" data-placeholder="Пошук за адресою чи номером аптеки...">
|
||
<select name="location_id" required hidden>
|
||
<option value="">—</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}">{{ location }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Номер договору <span style="color:var(--color-destructive);">*</span></label>
|
||
<input type="text" name="contract_number" required class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Тип послуги <span style="color:var(--color-destructive);">*</span></label>
|
||
<select name="service_type" required class="input" style="width:100%;" onchange="dqaHandleChange(this, 'contract_service_type', 'code'); itContractServiceToggle(this.value)">
|
||
{% for value, label in contract_service_types %}
|
||
<option value="{{ value }}">{{ label }}</option>
|
||
{% endfor %}
|
||
<option value="__new__">+ Додати новий тип...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Технологія підключення</label>
|
||
<select name="connection_type" class="input" style="width:100%;" onchange="dqaHandleChange(this, 'contract_connection_type', 'code')">
|
||
<option value="">—</option>
|
||
{% for value, label in contract_connection_types %}
|
||
<option value="{{ value }}">{{ label }}</option>
|
||
{% endfor %}
|
||
<option value="__new__">+ Додати нову технологію...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Дата початку <span style="color:var(--color-destructive);">*</span></label>
|
||
<input type="date" name="start_date" required class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Дата закінчення</label>
|
||
<input type="date" name="end_date" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Вартість/міс.</label>
|
||
<input type="number" step="0.01" min="0" name="monthly_cost" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Швидкість ↓ Мбіт/с</label>
|
||
<input type="number" min="0" name="bandwidth_down" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Швидкість ↑ Мбіт/с</label>
|
||
<input type="number" min="0" name="bandwidth_up" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Особовий рахунок</label>
|
||
<input type="text" name="account_number" class="input" style="width:100%;">
|
||
</div>
|
||
|
||
<div id="it-contract-ip-block" style="display:none; background:var(--color-muted); border-radius:0.625rem; padding:0.875rem 1rem;">
|
||
<label style="display:flex; align-items:center; gap:0.5rem; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); cursor:pointer;">
|
||
<input type="checkbox" name="dedicated_ip" onchange="document.getElementById('it-contract-ip-field').style.display = this.checked ? '' : 'none'">
|
||
Виділений IP
|
||
</label>
|
||
<div id="it-contract-ip-field" style="display:none; margin-top:0.625rem;">
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">IP-адреси</label>
|
||
<textarea name="static_ips" placeholder="По одній IP-адресі на рядок" style="width:100%; min-height:3rem; resize:vertical; background:var(--color-card); border:1px solid var(--color-input); border-radius:var(--radius); padding:0.4375rem 0.75rem; font-size:0.875rem; color:var(--color-foreground); font-family:var(--font-sans); outline:none; box-sizing:border-box;"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Примітки</label>
|
||
<textarea name="notes" style="width:100%; min-height:3rem; resize:vertical; background:var(--color-card); border:1px solid var(--color-input); border-radius:var(--radius); padding:0.4375rem 0.75rem; font-size:0.875rem; color:var(--color-foreground); font-family:var(--font-sans); outline:none; box-sizing:border-box;"></textarea>
|
||
</div>
|
||
<div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%; justify-content:center;">
|
||
Додати договір
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</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="width:2rem;"></th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Провайдер / Локація</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Тип послуги</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Швидкість</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Закінчення</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Статус</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for contract in contracts_page %}
|
||
<tr class="it-row" style="border-bottom:1px solid var(--color-border); cursor:pointer;" onclick="itToggleRow('contract-{{ contract.id }}')">
|
||
<td style="padding:0.75rem 0; text-align:center; color:var(--color-muted-foreground);">
|
||
<span id="it-toggle-contract-{{ contract.id }}" style="display:inline-block; transition:transform 150ms;">▸</span>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">
|
||
<strong>{{ contract.provider.name }}</strong> <span style="font-size:0.75rem; color:var(--color-muted-foreground);">№{{ contract.contract_number }}</span>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{{ contract.location }}</div>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground);">{{ contract.service_type.name }}</td>
|
||
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground); font-size:0.8125rem;">{{ contract.bandwidth_display }}</td>
|
||
<td style="padding:0.75rem 1rem; color:{% if contract.is_expiring_soon %}var(--color-destructive){% else %}var(--color-muted-foreground){% endif %};">{{ contract.end_date|date:"d.m.Y"|default:"—" }}</td>
|
||
<td style="padding:0.75rem 1rem;">
|
||
{% if contract.status.code == 'active' %}<span class="badge badge-success">{{ contract.status.name }}</span>
|
||
{% elif contract.status.code == 'suspended' %}<span class="badge badge-warning">{{ contract.status.name }}</span>
|
||
{% else %}<span class="badge badge-danger">{{ contract.status.name }}</span>{% endif %}
|
||
</td>
|
||
</tr>
|
||
<tr id="it-row-contract-{{ contract.id }}" class="it-detail-row" style="display:none; border-bottom:1px solid var(--color-border); background:var(--color-muted);">
|
||
<td></td>
|
||
<td colspan="5" style="padding:1rem;">
|
||
<form hx-post="/it-manager/contracts/{{ contract.id }}/update/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]"
|
||
style="display:flex; align-items:flex-end; gap:0.75rem; flex-wrap:wrap;" onclick="event.stopPropagation();">
|
||
{% csrf_token %}
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Статус</label>
|
||
<select name="status" class="input" style="min-width:12rem;">
|
||
{% for value, label in contract_statuses %}
|
||
<option value="{{ value }}" {% if value == contract.status.code %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
Зберегти
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">Рахунок: {{ contract.account_number|default:"—" }} · {{ contract.monthly_cost|default:"—" }} грн/міс</div>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="6" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">Договорів ще немає</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% include "partials/pagination.html" with page=contracts_page base_url="/it-manager/" page_param="contracts_page" size_param="contracts_per_page" item_label="договорів" %}
|
||
</div>
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: ПРОВАЙДЕРИ ══ -->
|
||
|
||
<!-- ══ ВКЛАДКА: ОБРАЩЕННЯ ══ -->
|
||
<div id="it-panel-servicecalls" class="it-panel" style="display:none;">
|
||
|
||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||
<span style="font-size:0.875rem; color:var(--color-muted-foreground);">{{ service_calls_count }} звернень</span>
|
||
<button type="button" class="btn btn-primary" onclick="itPanelOpen('it-servicecall-panel')">
|
||
<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 id="it-servicecall-panel-overlay" onclick="itPanelClose('it-servicecall-panel')" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:1000;"></div>
|
||
<div id="it-servicecall-panel" style="position:fixed; top:0; right:0; height:100vh; width:26rem; max-width:92vw; background:var(--color-card); box-shadow:-8px 0 24px rgba(0,0,0,0.18); z-index:1001; transform:translateX(100%); transition:transform 250ms ease; overflow-y:auto;">
|
||
<form hx-post="/it-manager/servicecalls/create/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]">
|
||
{% csrf_token %}
|
||
<div style="padding:1rem 1.25rem; border-bottom:1px solid var(--color-border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--color-card); z-index:1;">
|
||
<span style="font-size:0.9375rem; font-weight:600; color:var(--color-foreground);">Нове звернення</span>
|
||
<button type="button" onclick="itPanelClose('it-servicecall-panel')" class="btn btn-ghost btn-icon" aria-label="Закрити">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div style="padding:1.25rem; display:flex; flex-direction:column; gap:1rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Хто звернувся <span style="color:var(--color-destructive);">*</span></label>
|
||
<select name="caller_id" required class="input" style="width:100%;">
|
||
<option value="">—</option>
|
||
{% for emp in it_employees %}
|
||
<option value="{{ emp.id }}">{{ emp.last_name }} {{ emp.first_name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Канал <span style="color:var(--color-destructive);">*</span></label>
|
||
<select name="channel" required class="input" style="width:100%;" onchange="dqaHandleChange(this, 'servicecall_channel', 'code')">
|
||
{% for value, label in servicecall_channels %}
|
||
<option value="{{ value }}">{{ label }}</option>
|
||
{% endfor %}
|
||
<option value="__new__">+ Додати новий канал...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Витрачено хвилин</label>
|
||
<input type="number" min="0" name="minutes_spent" class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Опис звернення <span style="color:var(--color-destructive);">*</span></label>
|
||
<textarea name="description" required style="width:100%; min-height:3.5rem; resize:vertical; background:var(--color-card); border:1px solid var(--color-input); border-radius:var(--radius); padding:0.4375rem 0.75rem; font-size:0.875rem; color:var(--color-foreground); font-family:var(--font-sans); outline:none; box-sizing:border-box;"></textarea>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">Рішення</label>
|
||
<textarea name="resolution" style="width:100%; min-height:3rem; resize:vertical; background:var(--color-card); border:1px solid var(--color-input); border-radius:var(--radius); padding:0.4375rem 0.75rem; font-size:0.875rem; color:var(--color-foreground); font-family:var(--font-sans); outline:none; box-sizing:border-box;"></textarea>
|
||
</div>
|
||
<div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%; justify-content:center;">
|
||
Додати звернення
|
||
<span class="btn-label-loading htmx-indicator">Збереження…</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</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;">Дата</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Хто звернувся</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Канал</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Прийняв</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Опис</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">Тривалість</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for call in service_calls_page %}
|
||
<tr style="border-bottom:1px solid var(--color-border);">
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground); white-space:nowrap;">{{ call.date_time|date:"d.m.Y H:i" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground);">{{ call.caller.get_full_name|default:call.caller.username }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground);">{{ call.channel.name }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground);">{{ call.handler.get_full_name|default:"—" }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-foreground); max-width:20rem;">{{ call.description|truncatechars:100 }}</td>
|
||
<td style="padding:0.625rem 1rem; color:var(--color-muted-foreground);">{{ call.duration_display }}</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="6" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">Звернень ще немає</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% include "partials/pagination.html" with page=service_calls_page base_url="/it-manager/" page_param="servicecalls_page" size_param="servicecalls_per_page" item_label="звернень" %}
|
||
</div>
|
||
</div>
|
||
<!-- ══ /ВКЛАДКА: ОБРАЩЕННЯ ══ -->
|
||
|
||
{% if it_active_tab and it_active_tab != 'equipment' %}
|
||
<script>itTabSwitch('{{ it_active_tab }}');</script>
|
||
{% endif %}
|
||
<script>initSearchableSelects(document);</script>
|
||
|
||
</div>
|