diff --git a/shiftflow/templates/shiftflow/partials/_filter.html b/shiftflow/templates/shiftflow/partials/_filter.html
index 60a9b31..76b0ff1 100644
--- a/shiftflow/templates/shiftflow/partials/_filter.html
+++ b/shiftflow/templates/shiftflow/partials/_filter.html
@@ -1,6 +1,6 @@
-
-
-
+
+
+
+
+
+
@@ -80,10 +93,12 @@
function saveFilters(){
if (!form) return;
+ const qEl = form.querySelector('input[name="q"]');
const data = {
statuses: Array.from(form.querySelectorAll('input[name="statuses"]:checked')).map(i=>i.value),
m_ids: Array.from(form.querySelectorAll('input[name="m_ids"]:checked')).map(i=>i.value),
- start_date: s ? s.value : ''
+ start_date: s ? s.value : '',
+ q: qEl ? (qEl.value || '') : ''
};
try { localStorage.setItem('registry_filters', JSON.stringify(data)); } catch(_){}
}
@@ -107,6 +122,9 @@
if (Array.isArray(data.m_ids)){
form.querySelectorAll('input[name="m_ids"]').forEach(i=>{ i.checked = data.m_ids.includes(i.value); });
}
+ const qEl = form.querySelector('input[name="q"]');
+ if (qEl) qEl.value = data.q || '';
+
if (s) s.value = data.start_date || weekAgo;
if (e) e.value = today;
const filtered = form.querySelector('input[name="filtered"]');
@@ -116,18 +134,31 @@
}
if (form){
- form.addEventListener('change', saveFilters, true);
+ form.addEventListener('change', saveFilters, true);
- const resetBtn = document.getElementById('registryResetBtn');
- if (resetBtn) {
- resetBtn.addEventListener('click', function () {
- try { localStorage.removeItem('registry_filters'); } catch(_) {}
- });
+ const resetBtn = document.getElementById('registryResetBtn');
+ if (resetBtn) {
+ resetBtn.addEventListener('click', function () {
+ try { localStorage.removeItem('registry_filters'); } catch(_) {}
+ });
+ }
+
+ document.addEventListener('keydown', function (e) {
+ if (e.key !== 'Escape') return;
+ if (e.defaultPrevented) return;
+ const active = document.activeElement;
+ if (active && (active.tagName || '').toLowerCase() === 'textarea') return;
+ if (document.querySelector('.modal.show')) return;
+
+ if (resetBtn) {
+ e.preventDefault();
+ resetBtn.click();
+ }
+ }, true);
+
+ restoreFilters();
}
-
- restoreFilters();
- }
- });
+ });
\ No newline at end of file
diff --git a/shiftflow/views.py b/shiftflow/views.py
index c5c6b91..67d87ee 100644
--- a/shiftflow/views.py
+++ b/shiftflow/views.py
@@ -435,6 +435,8 @@ class RegistryView(LoginRequiredMixin, ListView):
# Диапазон дат, задаваемый пользователем. Если фильтры не активны или явно указан reset=1 — используем дефолты
start_date = self.request.GET.get('start_date')
end_date = self.request.GET.get('end_date')
+
+ q = (self.request.GET.get('q') or '').strip()
# Дефолтный режим: последние 7 дней и только статус "В работе"
is_default = (not filtered) or bool(reset)
@@ -451,6 +453,16 @@ class RegistryView(LoginRequiredMixin, ListView):
# Ограничения по ролям
+ if q:
+ queryset = queryset.filter(
+ Q(task__deal__number__icontains=q)
+ | Q(task__drawing_name__icontains=q)
+ | Q(task__entity__name__icontains=q)
+ | Q(task__entity__drawing_number__icontains=q)
+ | Q(task__material__name__icontains=q)
+ | Q(task__material__full_name__icontains=q)
+ )
+
if role == 'operator':
user_machines = profile.machines.all() if profile else Machine.objects.none()
queryset = queryset.filter(machine__in=user_machines)
@@ -470,6 +482,7 @@ class RegistryView(LoginRequiredMixin, ListView):
context['user_role'] = role
context['user_roles'] = sorted(roles)
context['is_readonly'] = bool(getattr(profile, 'is_readonly', False)) if profile else False
+ context['q'] = (self.request.GET.get('q') or '').strip()
allowed_ws = list(profile.allowed_workshops.values_list('id', flat=True)) if profile else []
context['allowed_workshop_ids'] = allowed_ws
@@ -511,6 +524,16 @@ class RegistryView(LoginRequiredMixin, ListView):
work_qs = WorkItem.objects.select_related('deal', 'deal__company', 'entity', 'entity__planned_material', 'operation', 'machine', 'workshop')
+ q = (self.request.GET.get('q') or '').strip()
+ if q:
+ work_qs = work_qs.filter(
+ Q(deal__number__icontains=q)
+ | Q(entity__name__icontains=q)
+ | Q(entity__drawing_number__icontains=q)
+ | Q(entity__planned_material__name__icontains=q)
+ | Q(entity__planned_material__full_name__icontains=q)
+ )
+
m_ids = [int(i) for i in self.request.GET.getlist('m_ids') if str(i).isdigit()]
if m_ids:
work_qs = work_qs.filter(Q(machine_id__in=m_ids) | Q(machine_id__isnull=True))
diff --git a/static/css/style.css b/static/css/style.css
index 23414a2..740abe9 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -161,6 +161,16 @@ body {
width: 120px;
}
+.registry-filter-q {
+ width: 220px;
+}
+
+@media (max-width: 575.98px) {
+ .registry-filter-q {
+ width: 100%;
+ }
+}
+
/* Специальный класс для центрирования окна логина (вернем его только там) */
.sf-attention {
animation: sfAttentionPulse 1.6s ease-in-out infinite;