be7e5853cb
Добавлены фильтры списка, редактирование инв. номеров/заметок/дат, история выдачи в карточке и тесты. Co-authored-by: Cursor <cursoragent@cursor.com>
1457 lines
133 KiB
HTML
1457 lines
133 KiB
HTML
{% load common_tags %}
|
||
{% load i18n %}
|
||
{% with active_tab=it_active_tab|default:"equipment" %}
|
||
<div class="page-content" data-it-active-tab="{{ active_tab }}">
|
||
|
||
<div style="margin-bottom:1.5rem;">
|
||
<h1 style="font-size:1.25rem; font-weight:500; color:var(--color-foreground);">{% trans "IT-отдел" %}</h1>
|
||
<p style="font-size:0.875rem; color:var(--color-muted-foreground); margin-top:0.25rem;">{% trans "Инвентаризация оборудования, очередь заявок и журнал выполненных работ" %}</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{% if active_tab == 'equipment' %} it-tab-active{% endif %}" 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; {% if active_tab == 'equipment' %}background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %}background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Оборудование" %}</button>
|
||
<button type="button" class="it-tab-btn{% if active_tab == 'tickets' %} it-tab-active{% endif %}" 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; {% if active_tab == 'tickets' %}background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %}background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Заявки" %}</button>
|
||
<button type="button" class="it-tab-btn{% if active_tab == 'worklog' %} it-tab-active{% endif %}" 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; {% if active_tab == 'worklog' %}background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %}background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Выполненные работы" %}</button>
|
||
<button type="button" class="it-tab-btn{% if active_tab == 'consumables' %} it-tab-active{% endif %}" 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; {% if active_tab == 'consumables' %}background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %}background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Расходные материалы" %}</button>
|
||
<button type="button" class="it-tab-btn{% if active_tab == 'providers' %} it-tab-active{% endif %}" 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; {% if active_tab == 'providers' %}background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %}background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Провайдеры" %}</button>
|
||
<button type="button" class="it-tab-btn{% if active_tab == 'servicecalls' %} it-tab-active{% endif %}" 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; {% if active_tab == 'servicecalls' %}background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %}background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Обращение" %}</button>
|
||
{% if it_can_view_monthly_report %}
|
||
<a href="/reports/it/monthly/"
|
||
hx-get="/reports/it/monthly/" hx-target="#main-content" hx-push-url="/reports/it/monthly/" hx-indicator="#loader"
|
||
class="it-tab-btn"
|
||
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); text-decoration:none; display:inline-flex; align-items:center;">
|
||
{% trans "Месячный отчёт" %} →
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- ══ ВКЛАДКА: ОБЛАДНАННЯ ══ -->
|
||
<div id="it-panel-equipment" class="it-panel"{% if active_tab != 'equipment' %} style="display:none;"{% endif %}>
|
||
|
||
<div style="display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:0.75rem; margin-bottom:1.5rem;">
|
||
<a href="/it-manager/?tab=equipment"
|
||
hx-get="/it-manager/?tab=equipment" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
||
class="stat-card" style="padding:1rem 1.25rem; text-decoration:none; cursor:pointer; display:block;{% if not equipment_filters.status %} outline:2px solid var(--color-primary); outline-offset:-2px;{% endif %}">
|
||
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Всего единиц" %}</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ equipment_total_count }}</div>
|
||
</a>
|
||
<a href="/it-manager/?tab=equipment&status=stock"
|
||
hx-get="/it-manager/?tab=equipment&status=stock" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
||
class="stat-card" style="padding:1rem 1.25rem; text-decoration:none; cursor:pointer; display:block;{% if equipment_filters.status == 'stock' %} outline:2px solid var(--color-primary); outline-offset:-2px;{% endif %}">
|
||
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "На складе" %}</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ equipment_counts.stock }}</div>
|
||
</a>
|
||
<a href="/it-manager/?tab=equipment&status=assigned"
|
||
hx-get="/it-manager/?tab=equipment&status=assigned" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
||
class="stat-card" style="padding:1rem 1.25rem; text-decoration:none; cursor:pointer; display:block;{% if equipment_filters.status == 'assigned' %} outline:2px solid var(--color-primary); outline-offset:-2px;{% endif %}">
|
||
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Выдано" %}</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ equipment_counts.assigned }}</div>
|
||
</a>
|
||
<a href="/it-manager/?tab=equipment&status=repair"
|
||
hx-get="/it-manager/?tab=equipment&status=repair" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
||
class="stat-card" style="padding:1rem 1.25rem; text-decoration:none; cursor:pointer; display:block;{% if equipment_filters.status == 'repair' %} outline:2px solid var(--color-primary); outline-offset:-2px;{% endif %}">
|
||
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "В ремонте / списано" %}</div>
|
||
<div style="font-size:1.5rem; font-weight:500; color:var(--color-foreground);">{{ equipment_counts.repair|add:equipment_counts.written_off }}</div>
|
||
</a>
|
||
</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>
|
||
{% trans "Каталог моделей" %} ({{ 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>
|
||
{% trans "Добавить экземпляры" %}
|
||
</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);">{% trans "Добавить экземпляры" %}</span>
|
||
<button type="button" onclick="itPanelClose('it-equipment-panel')" class="btn btn-ghost btn-icon" aria-label="{% trans 'Закрыть' %}">
|
||
<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;">{% trans "Модель из каталога" %} <span style="color:var(--color-destructive);">*</span></label>
|
||
<div class="searchable-select" data-placeholder="{% trans 'Поиск модели по названию или бренду...' %}">
|
||
<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;">{% trans "Нет нужной?" %} <a href="#" onclick="itPanelClose('it-equipment-panel'); itPanelOpen('it-equipment-models-panel'); return false;" style="color:var(--color-primary);">{% trans "Добавить модель в каталог" %}</a></div>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Серийные номера" %} <span style="color:var(--color-destructive);">*</span></label>
|
||
<textarea name="serial_numbers" required placeholder="{% trans 'По одному в строку — вставьте список серийников, например' %} 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;">{% trans "Сколько серийников — столько единиц будет создано. Дубликаты и уже имеющиеся пропускаются." %}</div>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Инвентарные номера" %} <span style="font-weight:400; color:var(--color-muted-foreground);">{% trans "(необязательно, по одному на строку)" %}</span></label>
|
||
<textarea name="inventory_numbers" placeholder="{% trans 'По одному на строку — столько же строк, сколько серийников' %}"
|
||
style="width:100%; min-height: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-mono, monospace); outline:none; box-sizing:border-box;"></textarea>
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.375rem;">{% trans "Если заполняете — число строк должно совпадать с серийными номерами. Пустая строка = без инв. номера." %}</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;">{% trans "Дата покупки" %}</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;">{% trans "Гарантия до" %}</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;">{% trans "Сразу выдать" %} <span style="font-weight:400; color:var(--color-muted-foreground);">{% trans "(необязательно)" %}</span></label>
|
||
{% include "partials/it-equipment-assign-fields.html" with widget_id="bulk-create" selected_target="" selected_location_id=None selected_employee_id=None require_target=False allow_empty=True %}
|
||
</div>
|
||
<div style="display:flex; gap:0.5rem;">
|
||
<button type="submit" class="btn btn-primary" style="flex:1;">
|
||
{% trans "Создать экземпляры" %}
|
||
<span class="btn-label-loading htmx-indicator">{% trans "Сохранение…" %}</span>
|
||
</button>
|
||
<button type="button" class="btn btn-secondary" style="border:1px solid var(--color-border);" onclick="itPanelClose('it-equipment-panel')">{% trans "Отменить" %}</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);">{% trans "Каталог моделей" %}</span>
|
||
<button type="button" onclick="itPanelClose('it-equipment-models-panel')" class="btn btn-ghost btn-icon" aria-label="{% trans 'Закрыть' %}">
|
||
<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);">{% trans "Новая модель" %}</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Тип" %} <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__">{% trans "+ Добавить новый тип..." %}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Бренд" %}</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__">{% trans "+ Добавить новый бренд..." %}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Модель" %} <span style="color:var(--color-destructive);">*</span></label>
|
||
<input type="text" name="name" required placeholder="{% trans 'Например: 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;">{% trans "Гарантия (мес.)" %} <span style="font-weight:400; color:var(--color-muted-foreground);">{% trans "(необязательно)" %}</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%;">
|
||
{% trans "Добавить модель" %}
|
||
<span class="btn-label-loading htmx-indicator">{% trans "Сохранение…" %}</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;">{% trans "В каталоге" %} ({{ 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 }} {% trans "ед." %}</span>
|
||
</div>
|
||
{% empty %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">{% trans "Каталог пуст — добавьте первую модель." %}</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;">{% trans "Выдать" %} <span id="it-bulk-assign-count">0</span> {% trans "ед.:" %}</div>
|
||
{% include "partials/it-equipment-assign-fields.html" with widget_id="bulk-assign" selected_target="" selected_location_id=None selected_employee_id=None require_target=True allow_empty=False %}
|
||
<button type="submit" class="btn btn-primary">{% trans "Выдать" %}</button>
|
||
<button type="button" class="btn btn-ghost" onclick="itBulkClearSelection()">{% trans "Отменить выбор" %}</button>
|
||
</form>
|
||
</div>
|
||
|
||
<form id="it-equipment-filters"
|
||
hx-get="/it-manager/"
|
||
hx-target="#main-content"
|
||
hx-push-url="true"
|
||
hx-indicator="#loader"
|
||
hx-trigger="submit, change from:select, keyup changed delay:400ms from:input[name='q']"
|
||
style="display:flex; align-items:flex-end; gap:0.75rem; flex-wrap:wrap; margin-bottom:1rem;">
|
||
<input type="hidden" name="tab" value="equipment">
|
||
<div style="flex:1; min-width:12rem;">
|
||
<label for="it-equipment-q" style="display:block; font-size:0.75rem; font-weight:500; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Поиск" %}</label>
|
||
<input type="search" id="it-equipment-q" name="q" value="{{ equipment_filters.q }}"
|
||
placeholder="{% trans 'Серийный, инв. №, модель, бренд…' %}"
|
||
class="input" style="width:100%;">
|
||
</div>
|
||
<div>
|
||
<label for="it-equipment-status" style="display:block; font-size:0.75rem; font-weight:500; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Статус" %}</label>
|
||
<select id="it-equipment-status" name="status" class="input" style="min-width:9rem; width:auto;">
|
||
<option value="">{% trans "Все" %}</option>
|
||
{% for value, label in equipment_statuses %}
|
||
<option value="{{ value }}" {% if equipment_filters.status == value %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="it-equipment-type" style="display:block; font-size:0.75rem; font-weight:500; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Тип" %}</label>
|
||
<select id="it-equipment-type" name="equipment_type" class="input" style="min-width:9rem; width:auto;">
|
||
<option value="">{% trans "Все" %}</option>
|
||
{% for value, label in equipment_types %}
|
||
<option value="{{ value }}" {% if equipment_filters.equipment_type == value %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="it-equipment-brand" style="display:block; font-size:0.75rem; font-weight:500; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Бренд" %}</label>
|
||
<select id="it-equipment-brand" name="brand" class="input" style="min-width:9rem; width:auto;">
|
||
<option value="">{% trans "Все" %}</option>
|
||
{% for brand in equipment_brands %}
|
||
<option value="{{ brand.id }}" {% if equipment_filters.brand == brand.id|stringformat:"s" %}selected{% endif %}>{{ brand.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="it-equipment-location" style="display:block; font-size:0.75rem; font-weight:500; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Локация" %}</label>
|
||
<select id="it-equipment-location" name="location_id" class="input" style="min-width:12rem; width:auto;">
|
||
<option value="">{% trans "Все" %}</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}" {% if equipment_filters.location_id == location.id %}selected{% endif %}>{{ location }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="it-equipment-assignee" style="display:block; font-size:0.75rem; font-weight:500; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Сотрудник" %}</label>
|
||
<select id="it-equipment-assignee" name="assigned_to" class="input" style="min-width:12rem; width:auto;">
|
||
<option value="">{% trans "Все" %}</option>
|
||
{% for emp in it_employees %}
|
||
<option value="{{ emp.id }}" {% if equipment_filters.assigned_to == emp.id %}selected{% endif %}>{{ emp.get_full_name|default:emp.username }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
{% if equipment_filters_active %}
|
||
<a href="/it-manager/?tab=equipment"
|
||
hx-get="/it-manager/?tab=equipment" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
||
class="btn btn-ghost" style="margin-bottom:0.05rem;">{% trans "Сбросить" %}</a>
|
||
{% endif %}
|
||
</form>
|
||
|
||
<!-- Список обладнання -->
|
||
<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="{% trans 'Выделить все на странице' %}" 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;">{% trans "Тип / Бренд / Модель" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "С/н · Инв. №" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Аптека / локация" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Сотрудник" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Статус" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for 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">{% trans "На складе" %}</span>
|
||
{% elif equipment.status.code == 'assigned' %}<span class="badge badge-success">{% trans "Выдано" %}</span>
|
||
{% elif equipment.status.code == 'repair' %}<span class="badge badge-warning">{% trans "В ремонте" %}</span>
|
||
{% else %}<span class="badge badge-danger">{% trans "Списано" %}</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;">{% trans "Статус" %}</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>
|
||
{% if equipment.assigned_to_id %}
|
||
{% include "partials/it-equipment-assign-fields.html" with widget_id=equipment.id selected_target="employee" selected_location_id=equipment.location_id selected_employee_id=equipment.assigned_to_id require_target=False allow_empty=True %}
|
||
{% elif equipment.location_id and equipment.location.location_type == 'pharmacy' %}
|
||
{% include "partials/it-equipment-assign-fields.html" with widget_id=equipment.id selected_target="pharmacy" selected_location_id=equipment.location_id selected_employee_id=None require_target=False allow_empty=True %}
|
||
{% elif equipment.location_id %}
|
||
{% include "partials/it-equipment-assign-fields.html" with widget_id=equipment.id selected_target="office" selected_location_id=equipment.location_id selected_employee_id=None require_target=False allow_empty=True %}
|
||
{% else %}
|
||
{% include "partials/it-equipment-assign-fields.html" with widget_id=equipment.id selected_target="" selected_location_id=None selected_employee_id=None require_target=False allow_empty=True %}
|
||
{% endif %}
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Инв. №" %}</label>
|
||
<input type="text" name="inventory_number" value="{{ equipment.inventory_number|default:'' }}" class="input" style="min-width:9rem;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Дата покупки" %}</label>
|
||
<input type="date" name="purchase_date" value="{{ equipment.purchase_date|date:'Y-m-d' }}" class="input" style="min-width:9rem;">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Гарантия до" %}</label>
|
||
<input type="date" name="warranty_until" value="{{ equipment.warranty_until|date:'Y-m-d' }}" class="input" style="min-width:9rem;">
|
||
</div>
|
||
<div style="flex-basis:100%;">
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Примечания" %}</label>
|
||
<textarea name="notes" rows="2" class="input" style="width:100%; min-height:2.5rem; resize:vertical;">{{ equipment.notes }}</textarea>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
{% trans "Сохранить" %}
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
{% if equipment.warranty_until %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground); flex-basis:100%;">{% trans "Гарантия:" %} {{ equipment.warranty_status }} ({{ equipment.warranty_until|date:"d.m.Y" }})</div>
|
||
{% endif %}
|
||
</form>
|
||
<div style="margin-top:1rem; padding-top:0.75rem; border-top:1px solid var(--color-border);" onclick="event.stopPropagation();">
|
||
<div style="font-size:0.8125rem; font-weight:600; color:var(--color-foreground); margin-bottom:0.5rem;">{% trans "История перемещений" %}</div>
|
||
{% with history=equipment.assignments.all %}
|
||
{% if history %}
|
||
<ul style="list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:0.5rem;">
|
||
{% for a in history %}
|
||
<li style="font-size:0.8125rem; color:var(--color-foreground); display:flex; flex-wrap:wrap; gap:0.35rem 0.75rem; padding:0.375rem 0.5rem; background:var(--color-card); border-radius:var(--radius);">
|
||
<span style="color:var(--color-muted-foreground); white-space:nowrap;">{{ a.assigned_at|date:"d.m.Y H:i" }}</span>
|
||
<span>
|
||
{% if a.assigned_to %}{{ a.assigned_to.get_full_name|default:a.assigned_to.username }}{% endif %}
|
||
{% if a.assigned_to and a.location %} · {% endif %}
|
||
{% if a.location %}{{ a.location }}{% endif %}
|
||
{% if not a.assigned_to and not a.location %}—{% endif %}
|
||
</span>
|
||
{% if a.assigned_by %}
|
||
<span style="color:var(--color-muted-foreground);">{% trans "выдал" %}: {{ a.assigned_by.get_full_name|default:a.assigned_by.username }}</span>
|
||
{% endif %}
|
||
{% if a.returned_at %}
|
||
<span class="badge badge-muted">{% trans "возврат" %} {{ a.returned_at|date:"d.m.Y" }}</span>
|
||
{% else %}
|
||
<span class="badge badge-success">{% trans "открыта" %}</span>
|
||
{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">{% trans "Перемещений ещё не было" %}</div>
|
||
{% endif %}
|
||
{% endwith %}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr>
|
||
<td colspan="7" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">
|
||
{% if equipment_filters_active %}
|
||
{% trans "Ничего не найдено — измените фильтры или сбросьте поиск" %}
|
||
{% else %}
|
||
{% trans "Оборудование ещё не добавлено" %}
|
||
{% endif %}
|
||
</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"{% if active_tab != 'tickets' %} style="display:none;"{% endif %}>
|
||
|
||
<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;">{% trans "Новые" %}</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;">{% trans "В работе" %}</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;">{% trans "Ожидание" %}</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;">{% trans "Решённые" %}</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;">{% trans "Просроченные" %}</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;">{% trans "№ / Тема" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Приоритет" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Заявитель" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Исполнитель" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Дата" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Статус" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for 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">{% trans "просрочено" %}</span>{% endif %}</div>
|
||
</td>
|
||
<td style="padding:0.75rem 1rem;">
|
||
{% if ticket.priority.code == 'critical' %}<span class="badge badge-danger">{% trans "Критический" %}</span>
|
||
{% elif ticket.priority.code == 'high' %}<span class="badge badge-warning">{% trans "Высокий" %}</span>
|
||
{% elif ticket.priority.code == 'medium' %}<span class="badge badge-info">{% trans "Средний" %}</span>
|
||
{% else %}<span class="badge badge-muted">{% trans "Низкий" %}</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">{% trans "Новая" %}</span>
|
||
{% elif ticket.status.code == 'in_progress' %}<span class="badge badge-warning">{% trans "В работе" %}</span>
|
||
{% elif ticket.status.code == 'waiting' %}<span class="badge badge-muted">{% trans "Ожидание" %}</span>
|
||
{% elif ticket.status.code == 'resolved' %}<span class="badge badge-success">{% trans "Решена" %}</span>
|
||
{% else %}<span class="badge badge-muted">{% trans "Закрыта" %}</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">{% trans "внутренний" %}</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;">{% trans "Статус" %}</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;">{% trans "Исполнитель" %}</label>
|
||
<select name="assignee_id" class="input" style="min-width:12rem;"
|
||
onclick="event.stopPropagation();" onmousedown="event.stopPropagation();">
|
||
<option value="">{% trans "— не назначено" %}</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>
|
||
{% empty %}
|
||
<option value="" disabled>{% trans "Нет сотрудников IT / staff — добавьте в группу IT" %}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
{% trans "Сохранить" %}
|
||
<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="{% trans 'Комментарий...' %}" 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"> {% trans "внутренний" %}
|
||
</label>
|
||
<button type="submit" class="btn btn-secondary" style="border:1px solid var(--color-border);">
|
||
{% trans "Добавить" %}
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
</form>
|
||
|
||
<div style="margin-top:0.875rem; padding-top:0.875rem; border-top:1px solid var(--color-border);" onclick="event.stopPropagation();">
|
||
<div style="display:flex; align-items:center; justify-content:space-between; gap:0.75rem; margin-bottom:0.5rem; flex-wrap:wrap;">
|
||
<span style="font-size:0.8125rem; font-weight:500; color:var(--color-foreground);">
|
||
{% trans "Выполненные работы" %}
|
||
{% if ticket.worklogs_count %}<span class="badge badge-muted">{{ ticket.worklogs_count }}</span>{% endif %}
|
||
</span>
|
||
<button type="button" class="btn btn-primary" style="font-size:0.75rem; padding:0.3rem 0.65rem;"
|
||
onclick="itWorklogFromTicket({
|
||
id: {{ ticket.id }},
|
||
number: '{{ ticket.number|escapejs }}',
|
||
title: '{{ ticket.title|escapejs }}',
|
||
locationId: '{{ ticket.location_id|default:"" }}',
|
||
equipmentId: '{{ ticket.equipment_id|default:"" }}'
|
||
})">
|
||
{% trans "Добавить работу" %}
|
||
</button>
|
||
</div>
|
||
{% if ticket.work_logs.all %}
|
||
<div style="display:flex; flex-direction:column; gap:0.35rem;">
|
||
{% for log in ticket.work_logs.all %}
|
||
<div style="font-size:0.8125rem; background:var(--color-card); border:1px solid var(--color-border); border-radius:0.5rem; padding:0.5rem 0.75rem;">
|
||
<div style="display:flex; justify-content:space-between; gap:0.5rem; color:var(--color-muted-foreground); font-size:0.75rem;">
|
||
<span>{{ log.date|date:"d.m.Y" }} · {{ log.technician.get_full_name|default:log.technician.username }} · {{ log.work_type.name }}</span>
|
||
<span>{% if log.hours_spent %}{{ log.hours_spent }} {% trans "ч" %}{% endif %}</span>
|
||
</div>
|
||
<div style="color:var(--color-foreground); margin-top:0.2rem;">{{ log.description|truncatechars:160 }}</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">{% trans "К заявке ещё не привязаны работы" %}</div>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="7" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Заявок пока нет" %}</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"{% if active_tab != 'worklog' %} style="display:none;"{% endif %}>
|
||
|
||
<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;">
|
||
{% if worklog_month_id != 'all' %}{% trans "Записей за" %} {{ worklog_stats_label }}{% else %}{% trans "Записей в этом месяце" %}{% endif %}
|
||
</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;">
|
||
{% if worklog_month_id != 'all' %}{% trans "Часов за" %} {{ worklog_stats_label }}{% else %}{% trans "Часов в этом месяце" %}{% endif %}
|
||
</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:space-between; align-items:flex-end; gap:1rem; flex-wrap:wrap; margin-bottom:1rem;">
|
||
<form id="it-worklog-filters"
|
||
hx-get="/it-manager/"
|
||
hx-target="#main-content"
|
||
hx-push-url="true"
|
||
hx-indicator="#loader"
|
||
hx-trigger="change"
|
||
style="display:flex; align-items:flex-end; gap:0.75rem; flex-wrap:wrap; margin:0;">
|
||
<input type="hidden" name="tab" value="worklog">
|
||
<div>
|
||
<label for="it-worklog-filter-month" style="display:block; font-size:0.75rem; font-weight:500; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Месяц" %}</label>
|
||
<select id="it-worklog-filter-month" name="worklog_month" class="input" style="min-width:12rem; width:auto;">
|
||
<option value="all" {% if worklog_month_id == 'all' %}selected{% endif %}>{% trans "Все месяцы" %}</option>
|
||
{% for mid, label in worklog_month_choices %}
|
||
<option value="{{ mid }}" {% if worklog_month_id == mid %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="it-worklog-filter-tech" style="display:block; font-size:0.75rem; font-weight:500; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Сотрудник" %}</label>
|
||
<select id="it-worklog-filter-tech" name="worklog_technician" class="input" style="min-width:14rem; width:auto;">
|
||
<option value="">{% trans "Все сотрудники" %}</option>
|
||
{% for emp in worklog_technicians %}
|
||
<option value="{{ emp.pk }}" {% if worklog_technician_id == emp.pk %}selected{% endif %}>
|
||
{{ emp.get_full_name|default:emp.username }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
{% if it_can_edit_worklogs %}
|
||
<p style="font-size:0.75rem; color:var(--color-muted-foreground); margin:0 0 0.5rem; max-width:18rem;">
|
||
{% trans "Как руководитель вы можете править записи прямо в таблице — изменения сохраняются при смене поля." %}
|
||
</p>
|
||
{% endif %}
|
||
</form>
|
||
<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>
|
||
{% trans "Добавить запись" %}
|
||
</button>
|
||
</div>
|
||
|
||
{% if it_worklog_message %}
|
||
<div style="margin-bottom:1rem; padding:0.75rem 1rem; border-radius:0.5rem; border:1px solid hsl(152 40% 70%); background:color-mix(in srgb, var(--color-success) 12%, var(--color-card)); color:var(--color-foreground); font-size:0.875rem;">
|
||
{{ it_worklog_message }}
|
||
</div>
|
||
{% endif %}
|
||
{% if it_worklog_error %}
|
||
<div style="margin-bottom:1rem; padding:0.75rem 1rem; border-radius:0.5rem; border:1px solid color-mix(in srgb, var(--color-destructive) 40%, var(--color-border)); background:color-mix(in srgb, var(--color-destructive) 10%, var(--color-card)); color:var(--color-foreground); font-size:0.875rem;">
|
||
{{ it_worklog_error }}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Виїзна панель: новий запис про виконану роботу -->
|
||
<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);">{% trans "Добавить запись о выполненной работе" %}</span>
|
||
<button type="button" onclick="itPanelClose('it-worklog-panel')" class="btn btn-ghost btn-icon" aria-label="{% trans 'Закрыть' %}">
|
||
<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 id="it-worklog-ticket-hint" style="display:none; padding:0.625rem 0.75rem; border-radius:0.5rem; background:color-mix(in srgb, var(--color-primary) 8%, var(--color-card)); border:1px solid color-mix(in srgb, var(--color-primary) 25%, var(--color-border)); font-size:0.8125rem; 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;">{% trans "Дата" %}</label>
|
||
<input type="date" name="date" id="it-worklog-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;">{% trans "Вид работ" %} <span style="color:var(--color-destructive);">*</span></label>
|
||
<select name="work_type" id="it-worklog-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__">{% trans "+ Добавить новый вид работ..." %}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Заявка" %}</label>
|
||
<div class="searchable-select" data-placeholder="{% trans 'Поиск заявки по номеру или теме...' %}">
|
||
<select name="ticket_id" id="it-worklog-ticket" hidden>
|
||
<option value="">—</option>
|
||
{% for ticket in worklog_tickets %}
|
||
<option value="{{ ticket.id }}"
|
||
data-location="{{ ticket.location_id|default:'' }}"
|
||
data-equipment="{{ ticket.equipment_id|default:'' }}"
|
||
data-title="{{ ticket.title }}">
|
||
{{ ticket.number }} — {{ ticket.title }}{% if ticket.status.code == 'closed' or ticket.status.code == 'resolved' %} ({{ ticket.status.name }}){% endif %}
|
||
</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;">{% trans "Локация" %}</label>
|
||
<div class="searchable-select" data-placeholder="{% trans 'Поиск: А3, П10, организация, адрес...' %}">
|
||
<select name="location_id" id="it-worklog-location" hidden>
|
||
<option value="">—</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}" data-search="{{ location.search_text }}">{{ 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;">{% trans "Затрачено часов" %}</label>
|
||
<input type="number" step="0.5" min="0" name="hours_spent" id="it-worklog-hours" 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;">{% trans "Оборудование" %}</label>
|
||
<select name="equipment_id" id="it-worklog-equipment" 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;">{% trans "Описание работ" %} <span style="color:var(--color-destructive);">*</span></label>
|
||
<textarea name="description" id="it-worklog-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;">{% trans "Результат" %}</label>
|
||
<textarea name="result" id="it-worklog-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;">
|
||
{% trans "Добавить запись" %}
|
||
<span class="btn-label-loading htmx-indicator">{% trans "Сохранение…" %}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Журнал -->
|
||
{% if it_can_edit_worklogs %}
|
||
{% for log in worklogs_page %}
|
||
<form id="worklog-form-{{ log.pk }}"
|
||
hx-post="/it-manager/worklog/{{ log.pk }}/update/"
|
||
hx-target="#worklog-status-{{ log.pk }}"
|
||
hx-swap="innerHTML"
|
||
style="display:none;">{% csrf_token %}</form>
|
||
{% endfor %}
|
||
{% endif %}
|
||
<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;">{% trans "Дата" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Исполнитель" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Заявка" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Вид работ" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Описание" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Локация" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Часов" %}</th>
|
||
{% if it_can_edit_worklogs %}
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Результат" %}</th>
|
||
<th style="padding:0.75rem 0.5rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;"></th>
|
||
{% endif %}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for log in worklogs_page %}
|
||
{% if it_can_edit_worklogs %}
|
||
<tr style="border-bottom:1px solid var(--color-border); vertical-align:top;">
|
||
<td style="padding:0.5rem 0.75rem; white-space:nowrap;">
|
||
<input form="worklog-form-{{ log.pk }}" type="date" name="date"
|
||
value="{{ log.date|date:'Y-m-d' }}" class="input"
|
||
style="width:9.5rem; font-size:0.8125rem; padding:0.3rem 0.4rem;"
|
||
onchange="this.form.requestSubmit()">
|
||
</td>
|
||
<td style="padding:0.5rem 0.75rem;">
|
||
<select form="worklog-form-{{ log.pk }}" name="technician_id" class="input"
|
||
style="min-width:9rem; max-width:12rem; font-size:0.8125rem; padding:0.3rem 0.4rem;"
|
||
onchange="this.form.requestSubmit()">
|
||
{% for emp in worklog_technicians %}
|
||
<option value="{{ emp.pk }}" {% if log.technician_id == emp.pk %}selected{% endif %}>
|
||
{{ emp.get_full_name|default:emp.username }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</td>
|
||
<td style="padding:0.5rem 0.75rem; color:var(--color-muted-foreground); font-size:0.8125rem; white-space:nowrap;">
|
||
{% if log.ticket %}
|
||
<button type="button" class="btn btn-ghost" style="padding:0; font-size:0.8125rem; color:var(--color-primary);"
|
||
onclick="itTabSwitch('tickets'); itToggleRow('ticket-{{ log.ticket_id }}');"
|
||
title="{{ log.ticket.title }}">{{ log.ticket.number }}</button>
|
||
{% else %}—{% endif %}
|
||
</td>
|
||
<td style="padding:0.5rem 0.75rem;">
|
||
<select form="worklog-form-{{ log.pk }}" name="work_type" class="input"
|
||
style="min-width:8rem; max-width:11rem; font-size:0.8125rem; padding:0.3rem 0.4rem;"
|
||
onchange="this.form.requestSubmit()">
|
||
{% for value, label in worklog_types %}
|
||
<option value="{{ value }}" {% if log.work_type.code == value %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</td>
|
||
<td style="padding:0.5rem 0.75rem; min-width:14rem;">
|
||
<textarea form="worklog-form-{{ log.pk }}" name="description" rows="2" required
|
||
class="input" style="width:100%; min-width:12rem; font-size:0.8125rem; padding:0.3rem 0.4rem; resize:vertical;"
|
||
onchange="this.form.requestSubmit()">{{ log.description }}</textarea>
|
||
</td>
|
||
<td style="padding:0.5rem 0.75rem;">
|
||
<select form="worklog-form-{{ log.pk }}" name="location_id" class="input"
|
||
style="min-width:10rem; max-width:14rem; font-size:0.8125rem; padding:0.3rem 0.4rem;"
|
||
onchange="this.form.requestSubmit()">
|
||
<option value="">—</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}" {% if log.location_id == location.id %}selected{% endif %}>{{ location }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</td>
|
||
<td style="padding:0.5rem 0.75rem;">
|
||
<input form="worklog-form-{{ log.pk }}" type="number" step="0.5" min="0" name="hours_spent"
|
||
value="{{ log.hours_spent|default_if_none:'' }}" class="input"
|
||
style="width:4.5rem; font-size:0.8125rem; padding:0.3rem 0.4rem;"
|
||
onchange="this.form.requestSubmit()">
|
||
</td>
|
||
<td style="padding:0.5rem 0.75rem; min-width:10rem;">
|
||
<textarea form="worklog-form-{{ log.pk }}" name="result" rows="2"
|
||
class="input" style="width:100%; font-size:0.8125rem; padding:0.3rem 0.4rem; resize:vertical;"
|
||
onchange="this.form.requestSubmit()">{{ log.result }}</textarea>
|
||
</td>
|
||
<td style="padding:0.5rem 0.5rem; white-space:nowrap;">
|
||
<span id="worklog-status-{{ log.pk }}" aria-live="polite"></span>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<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); font-size:0.8125rem; white-space:nowrap;">
|
||
{% if log.ticket %}
|
||
<button type="button" class="btn btn-ghost" style="padding:0; font-size:0.8125rem; color:var(--color-primary);"
|
||
onclick="itTabSwitch('tickets'); itToggleRow('ticket-{{ log.ticket_id }}');"
|
||
title="{{ log.ticket.title }}">{{ log.ticket.number }}</button>
|
||
{% else %}—{% endif %}
|
||
</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>
|
||
{% endif %}
|
||
{% empty %}
|
||
<tr><td colspan="{% if it_can_edit_worklogs %}9{% else %}7{% endif %}" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Записей ещё нет" %}</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"{% if active_tab != 'consumables' %} style="display:none;"{% endif %}>
|
||
|
||
<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;">{% trans "Всего позиций" %}</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;">{% trans "Заканчивается / на замену" %}</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>
|
||
{% trans "Добавить материал" %}
|
||
</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);">{% trans "Добавить расходный материал" %}</span>
|
||
<button type="button" onclick="itPanelClose('it-consumable-panel')" class="btn btn-ghost btn-icon" aria-label="{% trans 'Закрыть' %}">
|
||
<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;">{% trans "Тип" %} <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__">{% trans "+ Добавить новый тип..." %}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Цвет" %}</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__">{% trans "+ Добавить новый цвет..." %}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Производитель" %}</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;">{% trans "Модель / артикул" %} <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;">{% trans "Принтер" %}</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;">{% trans "Локация хранения" %}</label>
|
||
<div class="searchable-select" data-placeholder="{% trans 'Поиск по адресу или номеру аптеки...' %}">
|
||
<select name="location_id" hidden>
|
||
<option value="">—</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}" data-search="{{ location.search_text }}">{{ 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;">{% trans "Ресурс (страниц)" %}</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;">{% trans "Дата покупки" %}</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;">{% trans "Примечания" %}</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;">
|
||
{% trans "Добавить" %}
|
||
<span class="btn-label-loading htmx-indicator">{% trans "Сохранение…" %}</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;">{% trans "Тип / Модель" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Цвет" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Принтер" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Статус" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for 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;">{% trans "Статус" %}</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">
|
||
{% trans "Сохранить" %}
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
{% if consumable.page_yield %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">{% trans "Ресурс:" %} {{ consumable.page_yield }} {% trans "стр." %}</div>
|
||
{% endif %}
|
||
{% if consumable.notes %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground); flex-basis:100%;">{% trans "Примечания:" %} {{ consumable.notes }}</div>
|
||
{% endif %}
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="5" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Расходных материалов ещё не добавлено" %}</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"{% if active_tab != 'providers' %} style="display:none;"{% endif %}>
|
||
|
||
<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;">{% trans "Провайдеров" %}</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;">{% trans "Договоров" %}</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;">{% trans "Заканчиваются через 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>
|
||
{% trans "Добавить провайдера" %}
|
||
</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>
|
||
{% trans "Добавить договор" %}
|
||
</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);">{% trans "Провайдеры" %}</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;">{% trans "Провайдеров ещё нет" %}</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);">{% trans "Новый провайдер" %}</span>
|
||
<button type="button" onclick="itPanelClose('it-provider-panel')" class="btn btn-ghost btn-icon" aria-label="{% trans 'Закрыть' %}">
|
||
<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;">{% trans "Название" %} <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;">{% trans "Телефон" %}</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;">{% trans "Сайт" %}</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;">{% trans "Телефон техподдержки" %}</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;">{% trans "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;">{% trans "Примечания" %}</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;">
|
||
{% trans "Добавить провайдера" %}
|
||
<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);">{% trans "Новый договор" %}</span>
|
||
<button type="button" onclick="itPanelClose('it-contract-panel')" class="btn btn-ghost btn-icon" aria-label="{% trans 'Закрыть' %}">
|
||
<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;">{% trans "Провайдер" %} <span style="color:var(--color-destructive);">*</span></label>
|
||
<select name="provider_id" required class="input" style="width:100%;" id="it-contract-provider-select" onchange="itContractFilterParents()">
|
||
<option value="">—</option>
|
||
{% for provider in providers %}
|
||
<option value="{{ provider.id }}">{{ provider.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div id="it-contract-location-wrap">
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">
|
||
{% trans "Локация" %} <span id="it-contract-location-required" style="color:var(--color-destructive);">*</span>
|
||
</label>
|
||
<div class="searchable-select" data-placeholder="{% trans 'Поиск по адресу или номеру аптеки...' %}">
|
||
<select name="location_id" id="it-contract-location-select" required hidden>
|
||
<option value="">—</option>
|
||
{% for location in it_locations %}
|
||
<option value="{{ location.id }}" data-search="{{ location.search_text }}">{{ location }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<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="is_framework" id="it-contract-is-framework" onchange="itContractFrameworkToggle(this.checked)">
|
||
{% trans "Общий (рамочный) договор" %}
|
||
</label>
|
||
<p style="font-size:0.75rem; color:var(--color-muted-foreground); margin:0.35rem 0 0;">{% trans "Например, 708052 — к нему привязываются дочерние 70805208, 70805206… Локация для такого договора не нужна." %}</p>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Номер договора" %} <span style="color:var(--color-destructive);">*</span></label>
|
||
<input type="text" name="contract_number" required class="input" style="width:100%;">
|
||
</div>
|
||
<div id="it-contract-parent-wrap">
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Общий договор" %}</label>
|
||
<select name="parent_contract_id" id="it-contract-parent-select" class="input" style="width:100%;">
|
||
<option value="">— {% trans "нет" %} —</option>
|
||
{% for fc in framework_contracts %}
|
||
<option value="{{ fc.pk }}" data-provider="{{ fc.provider_id }}">№{{ fc.contract_number }} · {{ fc.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;">{% trans "Тип услуги" %} <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__">{% trans "+ Добавить новый тип..." %}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Технология подключения" %}</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__">{% trans "+ Добавить новую технологию..." %}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Дата начала" %} <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;">{% trans "Дата окончания" %}</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;">{% trans "Стоимость/мес." %}</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;">{% trans "Скорость ↓ Мбит/с" %}</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;">{% trans "Скорость ↑ Мбит/с" %}</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;">{% trans "Лицевой счёт" %}</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'">
|
||
{% trans "Выделенный 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;">{% trans "IP-адреса" %}</label>
|
||
<textarea name="static_ips" placeholder="{% trans 'По одному 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;">{% trans "Примечания" %}</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;">
|
||
{% trans "Добавить договор" %}
|
||
<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;">{% trans "Провайдер / Локация" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Тип услуги" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Скорость" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Окончание" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Статус" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for 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>
|
||
{% if contract.is_framework %}<span class="badge badge-info" style="font-size:0.6875rem; margin-left:0.35rem;">{% trans "Общий" %}</span>{% endif %}
|
||
<span style="font-size:0.75rem; color:var(--color-muted-foreground);"> №{{ contract.contract_number }}</span>
|
||
{% if contract.parent_contract %}
|
||
<span style="font-size:0.6875rem; color:var(--color-muted-foreground);"> · {% trans "к" %} №{{ contract.parent_contract.contract_number }}</span>
|
||
{% endif %}
|
||
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{{ contract.location_display }}</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;">{% trans "Статус" %}</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">
|
||
{% trans "Сохранить" %}
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">{% trans "Счёт:" %} {{ contract.account_number|default:"—" }} · {{ contract.monthly_cost|default:"—" }} {% trans "грн/мес" %}</div>
|
||
{% if contract.child_contracts.all %}
|
||
<div style="font-size:0.8125rem; color:var(--color-muted-foreground); margin-top:0.35rem;">
|
||
{% trans "Дочерние:" %}
|
||
{% for child in contract.child_contracts.all %}№{{ child.contract_number }}{% if not forloop.last %}, {% endif %}{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="6" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Договоров ещё нет" %}</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"{% if active_tab != 'servicecalls' %} style="display:none;"{% endif %}>
|
||
|
||
<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 }} {% trans "обращений" %}</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>
|
||
{% trans "Новое обращение" %}
|
||
</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);">{% trans "Новое обращение" %}</span>
|
||
<button type="button" onclick="itPanelClose('it-servicecall-panel')" class="btn btn-ghost btn-icon" aria-label="{% trans 'Закрыть' %}">
|
||
<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;">{% trans "Кто обратился" %} <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;">{% trans "Канал" %} <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__">{% trans "+ Добавить новый канал..." %}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Затрачено минут" %}</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;">{% trans "Описание обращения" %} <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;">{% trans "Решение" %}</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;">
|
||
{% trans "Добавить обращение" %}
|
||
<span class="btn-label-loading htmx-indicator">{% trans "Сохранение…" %}</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;">{% trans "Дата" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Кто обратился" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Канал" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Принял" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Описание" %}</th>
|
||
<th style="padding:0.75rem 1rem;text-align:left;color:var(--color-muted-foreground);font-weight:600;font-size:0.8125rem;">{% trans "Длительность" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for 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);">{% trans "Обращений ещё нет" %}</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>
|
||
<!-- ══ /ВКЛАДКА: ОБРАЩЕННЯ ══ -->
|
||
|
||
|
||
<script>initSearchableSelects(document);</script>
|
||
|
||
</div>
|
||
{% endwith %}
|