it_manager: справочник типовых работ в журнале WorkLog.

Категория/работа/required_details вместо свободного work_type; seed Directory, отчёт и тесты.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 13:38:07 +03:00
parent be7e5853cb
commit 31bd4c8fa7
11 changed files with 822 additions and 112 deletions
+79 -17
View File
@@ -574,10 +574,13 @@
{% 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>
{{ log.date|date:"d.m.Y" }} · {{ log.technician.get_full_name|default:log.technician.username }}
{% if log.catalog_work %} · {{ log.catalog_work.parent.name|default:"" }}{% elif log.work_type %} · {{ log.work_type.name }}{% endif %}
</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 style="color:var(--color-foreground); margin-top:0.2rem;">{{ log.display_title|truncatechars:160 }}</div>
</div>
{% endfor %}
</div>
@@ -645,6 +648,15 @@
{% endfor %}
</select>
</div>
<div>
<label for="it-worklog-filter-cat" 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-cat" name="worklog_category" class="input" style="min-width:14rem; width:auto;">
<option value="">{% trans "Все категории" %}</option>
{% for cat in it_work_catalog.categories %}
<option value="{{ cat.id }}" {% if worklog_category_id == cat.id %}selected{% endif %}>{{ cat.name }}</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 "Как руководитель вы можете править записи прямо в таблице — изменения сохраняются при смене поля." %}
@@ -681,6 +693,7 @@
<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>
{{ it_work_catalog|json_script:"it-work-catalog-data" }}
<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>
@@ -688,14 +701,22 @@
<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>
<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 id="it-worklog-category" class="input" style="width:100%;" onchange="itWorklogCategoryChanged()">
<option value="">{% trans "— Выберите категорию —" %}</option>
{% for cat in it_work_catalog.categories %}
<option value="{{ cat.id }}">{{ cat.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>
<select name="catalog_work_id" id="it-worklog-work" required class="input" style="width:100%;" onchange="itWorklogWorkChanged()">
<option value="">{% trans "— Сначала выберите категорию —" %}</option>
</select>
<div id="it-worklog-work-notes" style="display:none; font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.375rem;"></div>
</div>
<div id="it-worklog-details" style="display:none; flex-direction:column; gap:0.75rem;"></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 'Поиск заявки по номеру или теме...' %}">
@@ -737,8 +758,8 @@
</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>
<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="description" id="it-worklog-description" 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>
<label style="display:block; font-size:0.8125rem; font-weight:500; color:var(--color-foreground); margin-bottom:0.375rem;">{% trans "Результат" %}</label>
@@ -772,8 +793,8 @@
<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>
<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 %}
@@ -810,17 +831,39 @@
title="{{ log.ticket.title }}">{{ log.ticket.number }}</button>
{% else %}—{% endif %}
</td>
<td style="padding:0.5rem 0.75rem;">
<td style="padding:0.5rem 0.75rem; min-width:12rem; max-width:16rem;">
{% if log.catalog_work %}
<input type="hidden" form="worklog-form-{{ log.pk }}" name="catalog_work_id" value="{{ log.catalog_work_id }}">
<div style="font-size:0.8125rem; color:var(--color-foreground); font-weight:500;">{{ log.catalog_work.name }}</div>
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{{ log.catalog_work.parent.name|default:"" }}</div>
{% with req=log.catalog_work.extra_data.required_details %}
{% if req %}
<div style="display:flex; flex-direction:column; gap:0.25rem; margin-top:0.375rem;">
{% for key in req %}
<label style="font-size:0.6875rem; color:var(--color-muted-foreground);">
{{ worklog_detail_labels|get_item:key|default:key }}
<input form="worklog-form-{{ log.pk }}" type="text" name="detail_{{ key }}"
value="{{ log.details|get_item:key }}"
class="input" style="width:100%; font-size:0.75rem; padding:0.2rem 0.35rem; margin-top:0.1rem;"
onchange="this.form.requestSubmit()">
</label>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% else %}
<span class="badge badge-muted" style="margin-bottom:0.25rem;">{% trans "без справочника" %}</span>
<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;"
style="min-width:8rem; max-width:11rem; font-size:0.8125rem; padding:0.3rem 0.4rem; display:block;"
onchange="this.form.requestSubmit()">
{% for value, label in worklog_types %}
<option value="{{ value }}" {% if log.work_type.code == value %}selected{% endif %}>{{ label }}</option>
<option value="{{ value }}" {% if log.work_type_id and log.work_type.code == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
{% endif %}
</td>
<td style="padding:0.5rem 0.75rem; min-width:14rem;">
<textarea form="worklog-form-{{ log.pk }}" name="description" rows="2" required
<textarea form="worklog-form-{{ log.pk }}" name="description" rows="2"
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>
@@ -860,8 +903,27 @@
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-foreground); max-width:16rem;">
{% if log.catalog_work %}
<div>{{ log.catalog_work.name }}</div>
<div style="font-size:0.75rem; color:var(--color-muted-foreground);">{{ log.catalog_work.parent.name|default:"" }}</div>
{% if log.details %}
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.2rem;">
{% for k, v in log.details.items %}{% if v %}{{ worklog_detail_labels|get_item:k|default:k }}: {{ v }}{% if not forloop.last %}; {% endif %}{% endif %}{% endfor %}
</div>
{% endif %}
{% else %}
<span class="badge badge-muted">{% trans "без справочника" %}</span>
<div style="font-size:0.8125rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{% if log.work_type %}{{ log.work_type.name }}{% endif %}</div>
{% endif %}
</td>
<td style="padding:0.625rem 1rem; color:var(--color-foreground); max-width:22rem;">
{% if log.catalog_work %}
{{ log.description|truncatechars:120|default:"—" }}
{% else %}
{{ log.description|truncatechars:120 }}
{% endif %}
</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>