From 5328c34229f2e3cf2aa42a0f4a641e69cae8b656 Mon Sep 17 00:00:00 2001 From: fomichev Date: Wed, 22 Apr 2026 10:03:22 +0300 Subject: [PATCH] fix producer --- erp24/views/producer/index.php | 107 +++++-- erp24/web/js/budget/index.js | 399 ++++++++++++++------------ erp24/web/js/product-mapping/index.js | 46 +-- 3 files changed, 329 insertions(+), 223 deletions(-) diff --git a/erp24/views/producer/index.php b/erp24/views/producer/index.php index 78bacc26..1193f566 100644 --- a/erp24/views/producer/index.php +++ b/erp24/views/producer/index.php @@ -104,10 +104,14 @@ $producers = $dataProvider->getModels(); getStatusBadge() ?> - - - - is_active): ?> + is_active && $producer->is_active): ?> + + + + + + + is_active && $producer->is_active): ?> ' + messages[0] + ''); }); + } else { + \$saveBtn.prop('disabled', false); + alert(resp.message || 'Ошибка сохранения'); } }, - error: function() { alert('Ошибка сервера'); } + error: function() { + \$saveBtn.prop('disabled', false); + alert('Ошибка сервера'); + } }); }); - $(document).on('click', '.btn-producer-delete', function(e) { + $(document).on('click.producertab', '.btn-producer-delete', function(e) { e.preventDefault(); var id = $(this).data('id'); var name = $(this).data('name'); @@ -263,9 +303,10 @@ $js = <<' + messages[0] + ''); }); + } else { + \$saveBtn.prop('disabled', false); + alert(resp.message || 'Ошибка сохранения'); } }, - error: function() { alert('Ошибка сервера'); } + error: function() { + \$saveBtn.prop('disabled', false); + alert('Ошибка сервера'); + } }); }); - $(document).on('click', '.btn-plantation-delete', function(e) { + $(document).on('click.producertab', '.btn-plantation-delete', function(e) { e.preventDefault(); var id = $(this).data('id'); var name = $(this).data('name'); @@ -343,7 +396,7 @@ $js = <<
' + - '
' + escapeHtml(b.category) + '   ' + b.period_start + ' — ' + b.period_end + '
' + - '
' + - '
' + - pct + '%
' + - '
' + - formatNumber(b.used_amount) + ' ₽ / ' + formatNumber(b.limit_amount) + ' ₽ | Остаток: ' + formatNumber(b.remaining) + ' ₽
' + - '
'; - - container.innerHTML += html; - }); -} - -function renderBudgetTable(budgets) { - var tbody = document.querySelector('#budget-table tbody'); - tbody.innerHTML = ''; - - budgets.forEach(function (b) { - var statusBadge; - if (b.is_exceeded) { - statusBadge = 'Превышен'; - } else if (b.is_alert) { - statusBadge = 'Предупреждение'; - } else { - statusBadge = 'Норма'; - } - var row = '' + - '' + b.period_start + ' — ' + b.period_end + '' + - '' + escapeHtml(b.category) + '' + - '' + formatNumber(b.limit_amount) + '' + - '' + formatNumber(b.used_amount) + '' + - '' + b.usage_pct + '%' + - '' + b.alert_threshold_pct + '%' + - '' + statusBadge + '' + - ''; - - tbody.innerHTML += row; - }); -} - -function saveBudget() { - var data = { - category: document.getElementById('form-category').value, - period_start: document.getElementById('form-period-start').value, - limit_amount: document.getElementById('form-limit').value, - alert_threshold_pct: document.getElementById('form-threshold').value - }; + function formatNumber(n) { + return new Intl.NumberFormat('ru-RU', {minimumFractionDigits: 0, maximumFractionDigits: 0}).format(n); + } + + function csrfHeader() { + var token = document.querySelector('meta[name="csrf-token"]'); + return token ? {'X-CSRF-Token': token.getAttribute('content')} : {}; + } - if (!data.category || !data.period_start || !data.limit_amount) { - alert('Заполните все обязательные поля'); - return; + function getModalInstance(id) { + var el = document.getElementById(id); + if (!el) return null; + return bootstrap.Modal.getOrCreateInstance(el); } - // Проверка: понедельник - var d = new Date(data.period_start); - if (d.getDay() !== 1) { - alert('Период должен начинаться с понедельника'); - return; + /* ── Rendering ───────────────────────────────── */ + + function renderProgressBars(budgets) { + var container = document.getElementById('progress-container'); + if (!container) return; + + var parts = []; + budgets.forEach(function (b) { + var pct = b.usage_pct; + var color = b.is_exceeded ? '#dc3545' : b.is_alert ? '#ffc107' : '#28a745'; + parts.push( + '
' + + '
' + + '
' + escapeHtml(b.category) + '   ' + escapeHtml(b.period_start) + ' — ' + escapeHtml(b.period_end) + '
' + + '
' + + '
' + + escapeHtml(pct) + '%
' + + '
' + + formatNumber(b.used_amount) + ' ₽ / ' + formatNumber(b.limit_amount) + ' ₽ | Остаток: ' + formatNumber(b.remaining) + ' ₽
' + + '
' + ); + }); + container.innerHTML = parts.join(''); } - $.ajax({ - url: '/budget/save', - type: 'POST', - data: data, - dataType: 'json', - success: function (response) { - if (response.success) { - var modal = bootstrap.Modal.getInstance(document.getElementById('budgetFormModal')); - if (modal) modal.hide(); - loadProgress(); - } else { - alert(response.message || 'Ошибка сохранения'); + function renderBudgetTable(budgets) { + var tbody = document.querySelector('#budget-table tbody'); + if (!tbody) return; + + var rows = []; + budgets.forEach(function (b) { + var statusBadge = b.is_exceeded + ? 'Превышен' + : b.is_alert + ? 'Предупреждение' + : 'Норма'; + + rows.push( + '' + + '' + escapeHtml(b.period_start) + ' — ' + escapeHtml(b.period_end) + '' + + '' + escapeHtml(b.category) + '' + + '' + formatNumber(b.limit_amount) + '' + + '' + formatNumber(b.used_amount) + '' + + '' + escapeHtml(b.usage_pct) + '%' + + '' + escapeHtml(b.alert_threshold_pct) + '%' + + '' + statusBadge + '' + + '' + ); + }); + tbody.innerHTML = rows.join(''); + } + + /* ── Core ────────────────────────────────────── */ + + function loadProgress() { + var categoryEl = document.getElementById('filter-category'); + var periodEl = document.getElementById('filter-period'); + if (!categoryEl || !periodEl) return; + + var params = {}; + if (categoryEl.value) params.category = categoryEl.value; + if (periodEl.value) params.period_start = periodEl.value; + + $.ajax({ + url: urls.getProgress, + type: 'GET', + data: params, + dataType: 'json', + success: function (response) { + if (response.success) { + renderProgressBars(response.data); + renderBudgetTable(response.data); + } + }, + error: function () { + console.error('Ошибка загрузки прогресса бюджета'); } - }, - error: function () { - alert('Ошибка сервера'); + }); + } + + function saveBudget() { + var data = { + category: document.getElementById('form-category').value, + period_start: document.getElementById('form-period-start').value, + limit_amount: document.getElementById('form-limit').value, + alert_threshold_pct: document.getElementById('form-threshold').value + }; + + if (!data.category || !data.period_start || !data.limit_amount) { + alert('Заполните все обязательные поля'); + return; } - }); -} -function requestApproval() { - var budgetId = document.getElementById('approval-budget-id').value; - var amount = document.getElementById('approval-amount').value; - var reason = document.getElementById('approval-reason').value; + var d = new Date(data.period_start); + if (d.getDay() !== 1) { + alert('Период должен начинаться с понедельника'); + return; + } - if (!amount || !reason) { - alert('Заполните все поля'); - return; + $.ajax({ + url: urls.save, + type: 'POST', + headers: csrfHeader(), + data: data, + dataType: 'json', + success: function (response) { + if (response.success) { + var modal = getModalInstance('budgetFormModal'); + if (modal) modal.hide(); + loadProgress(); + } else { + alert(response.message || 'Ошибка сохранения'); + } + }, + error: function () { alert('Ошибка сервера'); } + }); } - $.ajax({ - url: '/budget/request-approval', - type: 'POST', - data: {budget_id: budgetId, amount: amount, reason: reason}, - dataType: 'json', - success: function (response) { - if (response.success) { - var modal = bootstrap.Modal.getInstance(document.getElementById('approvalRequestModal')); - if (modal) modal.hide(); - location.reload(); - } else { - alert(response.message || 'Ошибка'); - } - }, - error: function () { - alert('Ошибка сервера'); + function requestApproval() { + var budgetId = document.getElementById('approval-budget-id').value; + var amount = document.getElementById('approval-amount').value; + var reason = document.getElementById('approval-reason').value; + + if (!amount || !reason) { + alert('Заполните все поля'); + return; } - }); -} -function resolveApproval(approvalId, status) { - var action = status === 'approved' ? 'одобрить' : 'отклонить'; - if (!confirm('Вы уверены, что хотите ' + action + ' запрос #' + approvalId + '?')) { - return; + $.ajax({ + url: urls.requestApproval, + type: 'POST', + headers: csrfHeader(), + data: {budget_id: budgetId, amount: amount, reason: reason}, + dataType: 'json', + success: function (response) { + if (response.success) { + var modal = getModalInstance('approvalRequestModal'); + if (modal) modal.hide(); + location.reload(); + } else { + alert(response.message || 'Ошибка'); + } + }, + error: function () { alert('Ошибка сервера'); } + }); } - $.ajax({ - url: '/budget/resolve-approval', - type: 'POST', - data: {approval_id: approvalId, status: status}, - dataType: 'json', - success: function (response) { - if (response.success) { - location.reload(); - } else { - alert(response.message || 'Ошибка'); - } - }, - error: function () { - alert('Ошибка сервера'); + function resolveApproval(approvalId, status) { + var action = status === 'approved' ? 'одобрить' : 'отклонить'; + if (!confirm('Вы уверены, что хотите ' + action + ' запрос #' + approvalId + '?')) return; + + $.ajax({ + url: urls.resolveApproval, + type: 'POST', + headers: csrfHeader(), + data: {approval_id: approvalId, status: status}, + dataType: 'json', + success: function (response) { + if (response.success) { + location.reload(); + } else { + alert(response.message || 'Ошибка'); + } + }, + error: function () { alert('Ошибка сервера'); } + }); + } + + function openApprovalModal(budgetId) { + document.getElementById('approval-budget-id').value = budgetId; + document.getElementById('approval-amount').value = ''; + document.getElementById('approval-reason').value = ''; + var modal = getModalInstance('approvalRequestModal'); + if (modal) modal.show(); + } + + /* ── Auto-refresh ────────────────────────────── */ + + function startAutoRefresh() { + if (_autoRefreshTimer) return; + _autoRefreshTimer = setInterval(loadProgress, 60000); + } + + /* ── Init ────────────────────────────────────── */ + + $(function () { + // Запускаем авто-обновление только если DOM бюджета присутствует + if (document.getElementById('progress-container')) { + loadProgress(); + startAutoRefresh(); } }); -} - -function openApprovalModal(budgetId) { - document.getElementById('approval-budget-id').value = budgetId; - document.getElementById('approval-amount').value = ''; - document.getElementById('approval-reason').value = ''; - var modal = new bootstrap.Modal(document.getElementById('approvalRequestModal')); - modal.show(); -} - -// Helpers -function formatNumber(n) { - return new Intl.NumberFormat('ru-RU', {minimumFractionDigits: 0, maximumFractionDigits: 0}).format(n); -} - -function escapeHtml(text) { - var div = document.createElement('div'); - div.appendChild(document.createTextNode(text)); - return div.innerHTML; -} - -// Auto-refresh every 60 seconds -setInterval(loadProgress, 60000); + + /* ── Public API ──────────────────────────────── */ + + window.budgetApp = { + saveBudget: saveBudget, + requestApproval: requestApproval, + resolveApproval: resolveApproval, + openApprovalModal: openApprovalModal, + loadProgress: loadProgress + }; +}()); diff --git a/erp24/web/js/product-mapping/index.js b/erp24/web/js/product-mapping/index.js index 83feb61e..3d511aa4 100644 --- a/erp24/web/js/product-mapping/index.js +++ b/erp24/web/js/product-mapping/index.js @@ -13,12 +13,13 @@ (function () { // Единое состояние модуля — живёт пока жива страница var state = { - cfg: null, // URL-ы эндпоинтов - modal: null, // Bootstrap Modal instance - editingId: null, - cascadeXhr: null, // in-flight cascade AJAX - activeXhr: null, // in-flight reloadList AJAX - searchTimer: null + cfg: null, // URL-ы эндпоинтов + modal: null, // Bootstrap Modal instance + editingId: null, + shouldReload: false, // флаг: перезагрузить список после hidden.bs.modal + cascadeXhr: null, // in-flight cascade AJAX + activeXhr: null, // in-flight reloadList AJAX + searchTimer: null }; /* ─────────────────────────────────────────────── @@ -75,11 +76,6 @@ }); } - function getModal() { - // Всегда берём актуальный экземпляр Bootstrap Modal - return state.modal; - } - /* ─────────────────────────────────────────────── pmSetup — вызывается ОДИН РАЗ при загрузке страницы ─────────────────────────────────────────────── */ @@ -185,7 +181,7 @@ $btn.prop('disabled', true); state.editingId = null; - var modal = getModal(); + var modal = state.modal; if (!modal) { $btn.prop('disabled', false); return; } $('#pm-modal-title').text('Добавить поставщика'); @@ -205,7 +201,7 @@ $btn.prop('disabled', true); state.editingId = $btn.data('id'); - var modal = getModal(); + var modal = state.modal; if (!modal) { $btn.prop('disabled', false); return; } $('#pm-modal-title').text('Редактировать маппинг'); @@ -219,6 +215,10 @@ $(document).on('click' + NS, '#btn-mapping-save', function () { if (!state.cfg) return; + var $saveBtn = $(this); + if ($saveBtn.prop('disabled')) return; + $saveBtn.prop('disabled', true); + var $form = $('#mapping-form'); var url = state.editingId ? state.cfg.urls.update + '?id=' + state.editingId @@ -234,10 +234,10 @@ dataType: 'json', success: function (resp) { if (resp.success) { - var modal = getModal(); - if (modal) modal.hide(); - reloadList(); + state.shouldReload = true; + if (state.modal) state.modal.hide(); } else if (resp.errors) { + $saveBtn.prop('disabled', false); $.each(resp.errors, function (field, messages) { var $input = $form.find('[name="ProductMapping[' + field + ']"]'); if (!$input.length) $input = $form.find('[name="ProductMapping[' + field + '][]"]'); @@ -245,7 +245,10 @@ }); } }, - error: function () { alert('Ошибка сервера'); } + error: function () { + $saveBtn.prop('disabled', false); + alert('Ошибка сервера'); + } }); }); @@ -281,6 +284,7 @@ window.pmReinit = function (cfg) { state.cfg = cfg; + state.shouldReload = false; // Корректно уничтожаем старый Modal, чтобы Bootstrap не плодил глобальные слушатели if (state.modal) { @@ -291,6 +295,14 @@ var el = document.getElementById('pm-modal'); if (el) { state.modal = new bootstrap.Modal(el); + // Перезагружаем список только после полного закрытия модала, + // иначе Bootstrap не успевает убрать backdrop до замены DOM. + el.addEventListener('hidden.bs.modal', function () { + if (state.shouldReload) { + state.shouldReload = false; + reloadList(); + } + }); } }; }()); -- 2.39.5