Files
MES_Core/shiftflow/templates/shiftflow/task_create.html
2026-04-03 01:10:05 +03:00

649 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html' %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<div class="card shadow border-secondary">
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
<h3 class="text-accent mb-0"><i class="bi bi-plus-circle me-2"></i>Новое задание</h3>
{% if request.GET.next %}
<a class="btn btn-outline-secondary btn-sm" href="{{ request.GET.next }}">
<i class="bi bi-arrow-left me-1"></i>Назад
</a>
{% else %}
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
<i class="bi bi-arrow-left me-1"></i>Назад
</a>
{% endif %}
</div>
<form method="post" enctype="multipart/form-data" class="card-body p-4">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.GET.next }}">
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for e in form.non_field_errors %}<div>{{ e }}</div>{% endfor %}
</div>
{% endif %}
<div class="row g-3">
<div class="col-12">
<div class="card border-secondary">
<div class="card-header border-secondary py-2">
<strong>Деталь</strong>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label small text-muted">{{ form.drawing_name.label }}</label>
{{ form.drawing_name }}
{% for e in form.drawing_name.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-4">
<label class="form-label small text-muted">{{ form.quantity_ordered.label }}</label>
{{ form.quantity_ordered }}
{% for e in form.quantity_ordered.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-4">
<label class="form-label small text-muted">{{ form.size_value.label }}</label>
{{ form.size_value }}
{% for e in form.size_value.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check form-switch">
{{ form.is_bend }}
<label class="form-check-label ms-2">{{ form.is_bend.label }}</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-secondary">
<div class="card-header border-secondary py-2">
<strong>Файлы</strong>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small text-muted">{{ form.drawing_file.label }}</label>
{{ form.drawing_file }}
<button type="button" id="fillNameFromSource" class="btn btn-outline-secondary btn-sm mt-2" disabled>
в название (из исходника)
</button>
</div>
<div class="col-md-6">
<label class="form-label small text-muted">{{ form.extra_drawing.label }}</label>
{{ form.extra_drawing }}
<button type="button" id="fillNameFromPdf" class="btn btn-outline-secondary btn-sm mt-2" disabled>
в название (из чертежа)
</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-secondary">
<div class="card-header border-secondary py-2">
<strong>Сделка и материал</strong>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small text-muted">{{ form.deal.label }}</label>
<div class="d-flex gap-2">
<div class="flex-grow-1">{{ form.deal }}</div>
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealModal" data-mode="create">Создать</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="dealEditBtn" data-bs-toggle="modal" data-bs-target="#dealModal" data-mode="edit" disabled>Редакт.</button>
</div>
{% for e in form.deal.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-6">
<label class="form-label small text-muted">{{ form.material.label }}</label>
<div class="d-flex gap-2">
<div class="flex-grow-1">{{ form.material }}</div>
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#materialModal" data-mode="create">Создать</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="materialEditBtn" data-bs-toggle="modal" data-bs-target="#materialModal" data-mode="edit" disabled>Редакт.</button>
</div>
{% for e in form.material.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-outline-accent px-4 fw-bold">
<i class="bi bi-save me-2"></i>Создать
</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="dealModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Сделка</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="dealId">
<div class="mb-3">
<label class="form-label small text-muted">№ Сделки</label>
<input type="text" class="form-control border-secondary" id="dealNumber">
</div>
<div class="mb-3">
<label class="form-label small text-muted">Статус</label>
<select class="form-select border-secondary" id="dealStatus">
<option value="lead">Зашла</option>
<option value="work" selected>В работе</option>
<option value="done">Завершена</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small text-muted">Компания</label>
<div class="d-flex gap-2">
<select class="form-select border-secondary" id="dealCompany">
<option value="">— не выбрано —</option>
{% for c in companies %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-outline-accent btn-sm" id="openCompanyModalBtn">
Создать
</button>
</div>
</div>
<div class="mb-0">
<label class="form-label small text-muted">Описание</label>
<textarea class="form-control border-secondary" rows="3" id="dealDescription"></textarea>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="dealSaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="materialModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Материал</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="materialId">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small text-muted">Категория</label>
<div class="d-flex gap-2">
<select class="form-select border-secondary" id="materialCategory">
<option value="">— выбрать —</option>
{% for c in material_categories %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-outline-accent btn-sm" id="openCategoryModalBtn">
Создать
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label small text-muted">Марка стали</label>
<div class="d-flex gap-2">
<select class="form-select border-secondary" id="materialGrade">
<option value="">— не выбрано —</option>
{% for g in steel_grades %}
<option value="{{ g.id }}">{{ g.name }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-outline-accent btn-sm" id="openGradeModalBtn">
Создать
</button>
</div>
</div>
<div class="col-12">
<label class="form-label small text-muted">Наименование (размер/характеристики)</label>
<input type="text" class="form-control border-secondary" id="materialName">
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="materialSaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="companyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Компания</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="companyId">
<div class="mb-3">
<label class="form-label small text-muted">Название</label>
<input type="text" class="form-control border-secondary" id="companyName">
</div>
<div class="mb-0">
<label class="form-label small text-muted">Примечание</label>
<textarea class="form-control border-secondary" rows="3" id="companyDescription"></textarea>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="companySaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Категория материала</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="categoryId">
<div class="mb-3">
<label class="form-label small text-muted">Название</label>
<input type="text" class="form-control border-secondary" id="categoryName">
</div>
<div class="mb-0">
<label class="form-label small text-muted">ГОСТ</label>
<input type="text" class="form-control border-secondary" id="categoryGost">
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="categorySaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="gradeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Марка стали</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="gradeId">
<div class="mb-3">
<label class="form-label small text-muted">Марка стали</label>
<input type="text" class="form-control border-secondary" id="gradeName">
</div>
<div class="mb-0">
<label class="form-label small text-muted">ГОСТ/ТУ</label>
<input type="text" class="form-control border-secondary" id="gradeGost">
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="gradeSaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const drawingFile = document.getElementById('id_drawing_file');
const extraDrawing = document.getElementById('id_extra_drawing');
const drawingName = document.getElementById('id_drawing_name');
const qtyOrdered = document.getElementById('id_quantity_ordered');
const sizeValue = document.getElementById('id_size_value');
const fillFromSource = document.getElementById('fillNameFromSource');
const fillFromPdf = document.getElementById('fillNameFromPdf');
const dealSelect = document.getElementById('id_deal');
const materialSelect = document.getElementById('id_material');
const dealEditBtn = document.getElementById('dealEditBtn');
const materialEditBtn = document.getElementById('materialEditBtn');
const dealModal = document.getElementById('dealModal');
const dealId = document.getElementById('dealId');
const dealNumber = document.getElementById('dealNumber');
const dealCompany = document.getElementById('dealCompany');
const dealDescription = document.getElementById('dealDescription');
const dealStatus = document.getElementById('dealStatus');
const dealSaveBtn = document.getElementById('dealSaveBtn');
const companyModal = document.getElementById('companyModal');
const companyId = document.getElementById('companyId');
const companyName = document.getElementById('companyName');
const companyDescription = document.getElementById('companyDescription');
const companySaveBtn = document.getElementById('companySaveBtn');
const openCompanyModalBtn = document.getElementById('openCompanyModalBtn');
const materialModal = document.getElementById('materialModal');
const materialId = document.getElementById('materialId');
const materialCategory = document.getElementById('materialCategory');
const materialGrade = document.getElementById('materialGrade');
const materialName = document.getElementById('materialName');
const materialSaveBtn = document.getElementById('materialSaveBtn');
const categoryModal = document.getElementById('categoryModal');
const categoryId = document.getElementById('categoryId');
const categoryName = document.getElementById('categoryName');
const categoryGost = document.getElementById('categoryGost');
const categorySaveBtn = document.getElementById('categorySaveBtn');
const openCategoryModalBtn = document.getElementById('openCategoryModalBtn');
const gradeModal = document.getElementById('gradeModal');
const gradeId = document.getElementById('gradeId');
const gradeName = document.getElementById('gradeName');
const gradeGost = document.getElementById('gradeGost');
const gradeSaveBtn = document.getElementById('gradeSaveBtn');
const openGradeModalBtn = document.getElementById('openGradeModalBtn');
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return '';
}
function filenameBase(path) {
if (!path) return '';
const justName = path.split('\\').pop().split('/').pop();
const idx = justName.lastIndexOf('.');
return idx > 0 ? justName.slice(0, idx) : justName;
}
function updateFileButtons() {
const hasSource = drawingFile && drawingFile.files && drawingFile.files.length > 0;
const hasPdf = extraDrawing && extraDrawing.files && extraDrawing.files.length > 0;
if (fillFromSource) fillFromSource.disabled = !hasSource;
if (fillFromPdf) fillFromPdf.disabled = !hasPdf;
}
function tryFillFromDxfFilename(filename) {
if (!filename) return false;
const isDxf = filename.toLowerCase().endsWith('.dxf');
if (!isDxf) return false;
// Поддерживаем паттерн в имени файла вида "-s2n45".
// На практике встречаются варианты: "s2n45", "-s2_n45", "_s2-n45" и т.п.
// Поэтому ищем "s<число> ... n<целое>" в любом месте имени, разрешая разделители между ними.
const base = filenameBase(filename);
const normalized = base.replace(/\s+/g, '');
const m = normalized.match(/s([0-9]+(?:[\.,][0-9]+)?)[^0-9a-zA-Z]*n([0-9]+)/);
if (!m) return false;
const s = (m[1] || '').replace(',', '.');
const n = (m[2] || '').trim();
// По твоей просьбе: перезатираем значения, даже если пользователь что-то уже вводил.
if (sizeValue) {
sizeValue.value = s;
sizeValue.dispatchEvent(new Event('input', { bubbles: true }));
}
if (qtyOrdered) {
qtyOrdered.value = n;
qtyOrdered.dispatchEvent(new Event('input', { bubbles: true }));
}
return true;
}
if (drawingFile) drawingFile.addEventListener('change', function () {
updateFileButtons();
if (!drawingFile.files || drawingFile.files.length === 0) return;
const fname = drawingFile.files[0].name;
// По твоей просьбе: при выборе исходника перезаполняем «Наименование» из имени файла.
if (drawingName) drawingName.value = filenameBase(fname);
// Пытаемся распарсить «Размер» и «Требуется» из имени DXF.
tryFillFromDxfFilename(fname);
});
if (extraDrawing) extraDrawing.addEventListener('change', updateFileButtons);
updateFileButtons();
if (fillFromSource) {
fillFromSource.addEventListener('click', function () {
if (!drawingFile || !drawingFile.files || drawingFile.files.length === 0) return;
const fname = drawingFile.files[0].name;
const base = filenameBase(fname);
// Кнопка также перезаполняет значения из имени исходника.
if (drawingName) drawingName.value = base;
tryFillFromDxfFilename(fname);
});
}
if (fillFromPdf) {
fillFromPdf.addEventListener('click', function () {
if (!extraDrawing || !extraDrawing.files || extraDrawing.files.length === 0) return;
const base = filenameBase(extraDrawing.files[0].name);
if (drawingName) drawingName.value = base;
});
}
function updateEditButtons() {
if (dealEditBtn) dealEditBtn.disabled = !(dealSelect && dealSelect.value);
if (materialEditBtn) materialEditBtn.disabled = !(materialSelect && materialSelect.value);
}
function openChildModal(parentEl, childEl) {
if (!parentEl || !childEl) return;
childEl.dataset.returnTo = parentEl.id;
const parent = bootstrap.Modal.getOrCreateInstance(parentEl);
const child = bootstrap.Modal.getOrCreateInstance(childEl);
parentEl.addEventListener('hidden.bs.modal', function () {
child.show();
}, { once: true });
parent.hide();
}
function returnToParent(childEl) {
const returnId = childEl ? childEl.dataset.returnTo : '';
if (!returnId) return;
delete childEl.dataset.returnTo;
const parentEl = document.getElementById(returnId);
if (parentEl) bootstrap.Modal.getOrCreateInstance(parentEl).show();
}
if (companyModal) companyModal.addEventListener('hidden.bs.modal', function () { returnToParent(companyModal); });
if (categoryModal) categoryModal.addEventListener('hidden.bs.modal', function () { returnToParent(categoryModal); });
if (gradeModal) gradeModal.addEventListener('hidden.bs.modal', function () { returnToParent(gradeModal); });
if (openCompanyModalBtn) openCompanyModalBtn.addEventListener('click', function () { openChildModal(dealModal, companyModal); });
if (openCategoryModalBtn) openCategoryModalBtn.addEventListener('click', function () { openChildModal(materialModal, categoryModal); });
if (openGradeModalBtn) openGradeModalBtn.addEventListener('click', function () { openChildModal(materialModal, gradeModal); });
if (dealSelect) dealSelect.addEventListener('change', updateEditButtons);
if (materialSelect) materialSelect.addEventListener('change', updateEditButtons);
updateEditButtons();
async function postForm(url, data) {
const csrftoken = getCookie('csrftoken');
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'X-CSRFToken': csrftoken,
},
body: new URLSearchParams(data).toString(),
});
if (!res.ok) throw new Error('request_failed');
return await res.json();
}
async function getJson(url) {
const res = await fetch(url, { method: 'GET' });
if (!res.ok) throw new Error('request_failed');
return await res.json();
}
function upsertSelectOption(select, id, label) {
if (!select) return;
let opt = select.querySelector(`option[value="${id}"]`);
if (!opt) {
opt = document.createElement('option');
opt.value = String(id);
select.appendChild(opt);
}
opt.textContent = label;
select.value = String(id);
select.dispatchEvent(new Event('change'));
}
if (dealModal) {
dealModal.addEventListener('show.bs.modal', async function (event) {
if (!event.relatedTarget) return;
const mode = event.relatedTarget.getAttribute('data-mode') || 'create';
dealId.value = '';
dealNumber.value = '';
dealCompany.value = '';
dealDescription.value = '';
if (dealStatus) dealStatus.value = 'work';
if (mode === 'edit' && dealSelect && dealSelect.value) {
const data = await getJson(`/planning/deal/${dealSelect.value}/json/`);
dealId.value = data.id;
dealNumber.value = data.number || '';
dealCompany.value = data.company_id ? String(data.company_id) : '';
dealDescription.value = data.description || '';
if (dealStatus) dealStatus.value = data.status || 'work';
}
});
}
if (dealSaveBtn) {
dealSaveBtn.addEventListener('click', async function () {
const payload = {
id: dealId.value,
number: dealNumber.value,
company_id: dealCompany.value,
status: dealStatus ? dealStatus.value : 'work',
description: dealDescription.value,
};
const data = await postForm('{% url "deal_upsert" %}', payload);
upsertSelectOption(dealSelect, data.id, data.label);
bootstrap.Modal.getInstance(dealModal).hide();
});
}
if (companyModal) {
companyModal.addEventListener('show.bs.modal', function () {
companyId.value = '';
companyName.value = '';
companyDescription.value = '';
});
}
if (companySaveBtn) {
companySaveBtn.addEventListener('click', async function () {
const payload = {
id: companyId.value,
name: companyName.value,
description: companyDescription.value,
};
const data = await postForm('{% url "company_upsert" %}', payload);
upsertSelectOption(dealCompany, data.id, data.label);
bootstrap.Modal.getInstance(companyModal).hide();
});
}
if (categoryModal) {
categoryModal.addEventListener('show.bs.modal', function () {
categoryId.value = '';
categoryName.value = '';
categoryGost.value = '';
});
}
if (categorySaveBtn) {
categorySaveBtn.addEventListener('click', async function () {
const payload = {
id: categoryId.value,
name: categoryName.value,
gost_standard: categoryGost.value,
};
const data = await postForm('{% url "material_category_upsert" %}', payload);
upsertSelectOption(materialCategory, data.id, data.label);
bootstrap.Modal.getInstance(categoryModal).hide();
});
}
if (gradeModal) {
gradeModal.addEventListener('show.bs.modal', function () {
gradeId.value = '';
gradeName.value = '';
gradeGost.value = '';
});
}
if (gradeSaveBtn) {
gradeSaveBtn.addEventListener('click', async function () {
const payload = {
id: gradeId.value,
name: gradeName.value,
gost_standard: gradeGost.value,
};
const data = await postForm('{% url "steel_grade_upsert" %}', payload);
upsertSelectOption(materialGrade, data.id, data.label);
bootstrap.Modal.getInstance(gradeModal).hide();
});
}
if (materialModal) {
materialModal.addEventListener('show.bs.modal', async function (event) {
if (!event.relatedTarget) return;
const mode = event.relatedTarget.getAttribute('data-mode') || 'create';
materialId.value = '';
materialCategory.value = '';
materialGrade.value = '';
materialName.value = '';
if (mode === 'edit' && materialSelect && materialSelect.value) {
const data = await getJson(`/planning/material/${materialSelect.value}/json/`);
materialId.value = data.id;
materialCategory.value = data.category_id ? String(data.category_id) : '';
materialGrade.value = data.steel_grade_id ? String(data.steel_grade_id) : '';
materialName.value = data.name || '';
}
});
}
if (materialSaveBtn) {
materialSaveBtn.addEventListener('click', async function () {
const payload = {
id: materialId.value,
category_id: materialCategory.value,
steel_grade_id: materialGrade.value,
name: materialName.value,
};
const data = await postForm('{% url "material_upsert" %}', payload);
upsertSelectOption(materialSelect, data.id, data.label);
bootstrap.Modal.getInstance(materialModal).hide();
});
}
});
</script>
{% endblock %}