1862 lines
94 KiB
HTML
1862 lines
94 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||
<meta name="theme-color" content="#2196F3">
|
||
<meta name="msapplication-navbutton-color" content="#2196F3">
|
||
<title>Учёт кормов</title>
|
||
<script src="/static/js/wesp-theme-boot.js"></script>
|
||
<script>document.documentElement.classList.add('wesp-page-enter-pending');</script>
|
||
<script src="/static/js/wesp-page-enter.js?v=2"></script>
|
||
<script src="/static/js/wesp-nav-logo-shimmer.js?v=2"></script>
|
||
<script src="/static/js/wesp-zootech-nav.js?v=5"></script>
|
||
|
||
<style>
|
||
html { background: #f5f7fa; }
|
||
html[data-theme="dark"] { background: #12181b; }
|
||
html[data-theme="organic"] { background: #f6f6f4; }
|
||
</style>
|
||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="/static/css/wesp-zootech-shell.css?v=3" rel="stylesheet">
|
||
<link href="/static/css/wesp-feed-quality-settings.css?v=3" rel="stylesheet">
|
||
<link href="/static/css/inter-fonts.css" rel="stylesheet">
|
||
<link href="/static/css/wesp-zootech-layout.css?v=16" rel="stylesheet"> <link href="/static/css/wesp-zootech-components.css?v=8" rel="stylesheet">
|
||
<link href="/static/css/wesp-zootech-shell-modals.css?v=4" rel="stylesheet">
|
||
<link href="/static/css/wesp-zootech-k-hub.css?v=28" rel="stylesheet">
|
||
<link href="/static/css/font-awesome.min.css" rel="stylesheet">
|
||
<script src="/static/js/wesp-zootech-notification-messages.js?v=1"></script>
|
||
<script src="/static/js/wesp-user-messages.js?v=1"></script>
|
||
<script src="/static/js/wesp-zootech-notify.js?v=6"></script>
|
||
<script src="/static/js/wesp-dialog.js"></script>
|
||
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||
<script src="/static/js/wesp-zootech-bootstrap-modals.js?v=3"></script>
|
||
<script src="/static/js/modules/daily-plan-skip-duration.js?v=2"></script>
|
||
<script src="/static/js/modules/daily-plan-replace-component-modal.js?v=1"></script>
|
||
<script src="/static/js/pages/daily-plan-panel.js?v=11"></script>
|
||
<script src="/static/js/pages/lab-master-panel.js?v=2"></script>
|
||
<script src="/static/js/pages/lab-animal-profiles-modal.js?v=1"></script>
|
||
<script src="/static/js/pages/daily-plan-component-norms-modal.js?v=23"></script>
|
||
<script src="/static/js/wesp-zootech-notification-center.js?v=13"></script>
|
||
<script src="/static/js/pages/analytics-copy.js?v=1"></script>
|
||
<script src="/static/js/pages/stock-forecast.js?v=1"></script>
|
||
<script src="/static/js/chart.umd.min.js"></script>
|
||
<style>
|
||
/* Модальное окно «Добавить на склад»: прокрутка списка */
|
||
#addToSkladModal .settings-modal-content {
|
||
max-height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
#addToSkladModal .settings-modal-body {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
#addToSkladModal #addToSkladList {
|
||
max-height: min(50vh, 320px);
|
||
overflow-y: auto;
|
||
margin: 0;
|
||
}
|
||
#addToSkladModal #addToSkladEmpty,
|
||
#addToSkladModal #addToSkladNoMatch {
|
||
flex-shrink: 0;
|
||
}
|
||
#addToSkladModal .add-to-sklad-search-wrap {
|
||
flex-shrink: 0;
|
||
}
|
||
#addToSkladModal .add-to-sklad-search-wrap .form-control {
|
||
border-radius: 8px;
|
||
padding: 0.5rem 0.75rem;
|
||
}
|
||
|
||
/* Тёмная тема: модальное окно «Добавить на склад» */
|
||
html[data-theme="dark"] #addToSkladModal .settings-modal-body p.text-muted {
|
||
color: var(--dm-muted) !important;
|
||
}
|
||
html[data-theme="dark"] #addToSkladModal #addToSkladList.list-group {
|
||
background-color: var(--dm-surface) !important;
|
||
border-color: var(--dm-border) !important;
|
||
}
|
||
html[data-theme="dark"] #addToSkladModal #addToSkladList .list-group-item {
|
||
background-color: var(--dm-surface) !important;
|
||
color: var(--dm-text) !important;
|
||
border-color: var(--dm-border) !important;
|
||
}
|
||
html[data-theme="dark"] #addToSkladModal #addToSkladList .list-group-item:hover,
|
||
html[data-theme="dark"] #addToSkladModal #addToSkladList .list-group-item-action:hover {
|
||
background-color: var(--dm-field) !important;
|
||
color: var(--dm-text) !important;
|
||
}
|
||
html[data-theme="dark"] #addToSkladModal #addToSkladEmpty.text-muted,
|
||
html[data-theme="dark"] #addToSkladModal #addToSkladNoMatch.text-muted {
|
||
color: var(--dm-muted) !important;
|
||
}
|
||
html[data-theme="dark"] #addToSkladModal #addToSkladSearch.form-control {
|
||
background-color: var(--dm-field) !important;
|
||
color: var(--dm-text) !important;
|
||
border-color: var(--dm-border) !important;
|
||
}
|
||
html[data-theme="dark"] #addToSkladModal #addToSkladSearch.form-control::placeholder {
|
||
color: var(--dm-muted);
|
||
}
|
||
html[data-theme="dark"] #addToSkladModal .settings-modal-body .text-center.text-muted,
|
||
html[data-theme="dark"] #addToSkladModal .settings-modal-body .text-center.text-danger {
|
||
color: var(--dm-muted) !important;
|
||
}
|
||
html[data-theme="dark"] #addToSkladModal .settings-modal-body .text-danger {
|
||
color: #f88 !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .settings-user-card,
|
||
html[data-theme="dark"] .settings-theme-card,
|
||
html[data-theme="dark"] .settings-credentials-form {
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border-color: var(--dm-border);
|
||
}
|
||
|
||
html[data-theme="dark"] .settings-user-card--clickable:hover {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border-color: rgba(255, 255, 255, 0.25);
|
||
}
|
||
|
||
html[data-theme="dark"] .settings-card-title {
|
||
color: var(--dm-text);
|
||
}
|
||
|
||
html[data-theme="dark"] .theme-label {
|
||
color: var(--dm-muted);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container { padding: 0 0.5rem; }
|
||
.desktop-menu {
|
||
display: none !important;
|
||
}
|
||
.hamburger-menu {
|
||
display: flex !important;
|
||
}
|
||
.user-section {
|
||
display: none !important;
|
||
}
|
||
}
|
||
|
||
/* Футер */
|
||
body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.page-footer {
|
||
margin-top: auto;
|
||
padding: 2rem 0;
|
||
background-color: transparent;
|
||
}
|
||
|
||
html[data-theme="dark"] .page-footer {
|
||
border: none;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.page-footer {
|
||
padding: 1.5rem 0;
|
||
}
|
||
}
|
||
|
||
/* Модальное окно настроек */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 2000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: #fff;
|
||
margin: 5% auto;
|
||
padding: 0;
|
||
border-radius: 12px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
box-shadow: 0 8px 18px rgba(17, 24, 39, 0.16), 0 0 0 1px rgba(17, 24, 39, 0.06);
|
||
animation: modalSlideIn 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes modalSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-50px) scale(0.9);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0) scale(1);
|
||
}
|
||
}
|
||
|
||
.modal-header {
|
||
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
|
||
color: white;
|
||
padding: 1.5rem;
|
||
border-radius: 12px 12px 0 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
font-size: 1.3rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.modal-header h3 i {
|
||
margin-right: 0.5rem;
|
||
color: #ffd700;
|
||
}
|
||
|
||
.close {
|
||
color: white;
|
||
font-size: 2rem;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.close:hover {
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 2rem;
|
||
}
|
||
|
||
.modal-body .form-group {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.modal-body label {
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 0.5rem;
|
||
display: block;
|
||
}
|
||
|
||
.modal-body .form-control {
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
padding: 0.75rem;
|
||
font-size: 1rem;
|
||
transition: all 0.3s ease;
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.modal-body .form-control:focus {
|
||
border-color: var(--primary-color);
|
||
box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.1);
|
||
background-color: white;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 1.5rem 2rem;
|
||
border-top: 1px solid #e0e0e0;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 1rem;
|
||
background-color: #f8f9fa;
|
||
border-radius: 0 0 12px 12px;
|
||
}
|
||
|
||
.modal-footer .btn {
|
||
padding: 0.75rem 1.5rem;
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.modal-footer .btn-secondary {
|
||
background-color: #6c757d;
|
||
border-color: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.modal-footer .btn-secondary:hover {
|
||
background-color: #5a6268;
|
||
border-color: #545b62;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.modal-footer .btn-primary {
|
||
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
|
||
border: none;
|
||
color: white;
|
||
}
|
||
|
||
.modal-footer .btn-primary:hover {
|
||
background: linear-gradient(135deg, var(--primary-dark), var(--primary-darker));
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.3);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.modal-content {
|
||
margin: 10% auto;
|
||
width: 95%;
|
||
max-width: none;
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 1rem 1.5rem;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.modal-footer .btn {
|
||
width: 100%;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
}
|
||
|
||
html[data-theme="dark"] .modal-body label {
|
||
color: var(--dm-text) !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .modal-footer {
|
||
background-color: var(--dm-surface) !important;
|
||
border-top-color: var(--dm-border) !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .modal-footer .btn-secondary {
|
||
background-color: var(--dm-field) !important;
|
||
border-color: var(--dm-border) !important;
|
||
color: var(--dm-text) !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .modal-footer .btn-secondary:hover {
|
||
background-color: #5a5959 !important;
|
||
border-color: rgba(255, 255, 255, 0.25) !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .modal-footer .btn-primary {
|
||
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)) !important;
|
||
border: none !important;
|
||
color: white !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .modal-footer .btn-primary:hover {
|
||
background: linear-gradient(135deg, var(--primary-dark), var(--primary-darker)) !important;
|
||
}
|
||
|
||
/* Модальное окно на всю страницу */
|
||
.full-page-modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 3000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.9);
|
||
backdrop-filter: blur(10px);
|
||
overflow-y: auto;
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.full-page-modal.active {
|
||
display: block;
|
||
opacity: 1;
|
||
}
|
||
|
||
.full-page-modal-content {
|
||
background-color: #fff;
|
||
margin: 0;
|
||
padding: 0;
|
||
min-height: 100vh;
|
||
transform: scale(0.98);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.full-page-modal.active .full-page-modal-content {
|
||
transform: scale(1);
|
||
}
|
||
|
||
html[data-theme="dark"] .full-page-modal-content {
|
||
background-color: var(--dm-bg);
|
||
color: var(--dm-text);
|
||
}
|
||
|
||
.full-page-modal-header {
|
||
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
|
||
color: white;
|
||
padding: 2rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.full-page-modal-header h2 {
|
||
margin: 0;
|
||
font-size: 2rem;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.full-page-modal-header h2 i {
|
||
color: #ffd700;
|
||
font-size: 2.2rem;
|
||
}
|
||
|
||
.full-page-modal-header h2 img {
|
||
height: 50px;
|
||
width: auto;
|
||
filter: brightness(0) invert(1);
|
||
}
|
||
|
||
.close-full-modal {
|
||
color: white;
|
||
font-size: 2.5rem;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
width: 50px;
|
||
height: 50px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.close-full-modal:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.full-page-modal-body {
|
||
padding: 2rem;
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.full-page-modal-header {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.full-page-modal-header h2 {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.full-page-modal-header h2 i {
|
||
font-size: 1.8rem;
|
||
}
|
||
|
||
.full-page-modal-body {
|
||
padding: 1rem;
|
||
}
|
||
}
|
||
|
||
/* Отдельный блок с компонентами (как в Components), одна карточка на ширину блока */
|
||
.consumption-section {
|
||
margin: 2rem 0;
|
||
}
|
||
.consumption-section .consumption-block {
|
||
background: #fff;
|
||
border-radius: 1rem;
|
||
padding: 2rem;
|
||
margin-bottom: 2rem;
|
||
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
html[data-theme="dark"] .consumption-section .consumption-block {
|
||
background: var(--dm-surface);
|
||
border-color: var(--dm-border);
|
||
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3), 0 2px 4px -2px rgba(0,0,0,0.2);
|
||
}
|
||
.consumption-section .section-title {
|
||
font-size: 1.5rem;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 1.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
.consumption-filters {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.75rem;
|
||
align-items: center;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.consumption-filters label { margin: 0; font-weight: 500; color: #555; }
|
||
.consumption-filters input[type="date"] {
|
||
padding: 0.5rem 0.75rem;
|
||
border-radius: 8px;
|
||
border: 1px solid #ddd;
|
||
}
|
||
.consumption-filters-add-btn {
|
||
margin-left: auto;
|
||
padding: 0.5rem 0.75rem;
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
line-height: 1.5;
|
||
}
|
||
.fa-tab-bar {
|
||
display: flex;
|
||
gap: 0.35rem;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.fa-tab-btn {
|
||
border: 1px solid var(--dm-border, #d1d5db);
|
||
background: transparent;
|
||
border-radius: 8px;
|
||
padding: 0.4rem 0.75rem;
|
||
cursor: pointer;
|
||
font-size: 0.85rem;
|
||
}
|
||
.fa-tab-btn.active {
|
||
background: var(--primary-color, #2196F3);
|
||
color: #fff;
|
||
border-color: var(--primary-color, #2196F3);
|
||
}
|
||
.fa-org-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0.65rem 1rem;
|
||
}
|
||
.fa-org-grid .fa-field--wide {
|
||
grid-column: 1 / -1;
|
||
}
|
||
.fa-org-grid .fa-field--wide .form-control {
|
||
min-width: 0;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.fa-org-grid { grid-template-columns: 1fr; }
|
||
}
|
||
.fa-sig-actions {
|
||
margin-top: 0.5rem;
|
||
}
|
||
.fa-modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1rem 1rem;
|
||
border-top: 1px solid var(--dm-border, #e5e7eb);
|
||
}
|
||
.fa-sig-block {
|
||
margin-bottom: 1rem;
|
||
padding: 0.75rem;
|
||
border: 1px solid var(--dm-border, #e5e7eb);
|
||
border-radius: 8px;
|
||
}
|
||
.fa-sig-canvas {
|
||
width: 100%;
|
||
max-width: 320px;
|
||
height: 80px;
|
||
border: 1px dashed #9ca3af;
|
||
border-radius: 6px;
|
||
touch-action: none;
|
||
background: #fff;
|
||
}
|
||
html[data-theme="dark"] .fa-sig-canvas { background: #2c3338; }
|
||
.fa-export-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0.5rem;
|
||
}
|
||
@media (max-width: 480px) {
|
||
.fa-export-grid { grid-template-columns: 1fr; }
|
||
}
|
||
#feedAccountingModal .settings-modal-content {
|
||
max-width: 720px;
|
||
max-height: 92vh;
|
||
overflow: auto;
|
||
}
|
||
/* Одна карточка на всю ширину блока (как в Components) */
|
||
.component-cards-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
.component-card {
|
||
background: white;
|
||
border: 2px solid transparent;
|
||
border-radius: 1rem;
|
||
padding: 1.5rem;
|
||
margin-bottom: 0;
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||
box-shadow: 0 6px 14px rgba(17, 24, 39, 0.08);
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
.component-card:hover {
|
||
border-color: var(--primary-color);
|
||
box-shadow: 0 6px 14px rgba(17, 24, 39, 0.08), 0 0 0 2px rgba(var(--primary-rgb), 0.2);
|
||
}
|
||
.component-card .component-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.component-card .component-name {
|
||
font-size: 1.25rem;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin: 0;
|
||
}
|
||
.component-card .component-info {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
.component-card .info-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
.component-card .info-label {
|
||
font-weight: 500;
|
||
color: #64748b;
|
||
min-width: 100px;
|
||
}
|
||
.component-card .info-value {
|
||
color: #1e293b;
|
||
font-weight: 600;
|
||
}
|
||
html[data-theme="dark"] .consumption-section .section-title { color: var(--dm-text); }
|
||
html[data-theme="dark"] .consumption-filters label { color: var(--dm-muted); }
|
||
html[data-theme="dark"] .consumption-filters input[type="date"] {
|
||
background: var(--dm-field);
|
||
color: var(--dm-text);
|
||
border-color: var(--dm-border);
|
||
}
|
||
html[data-theme="dark"] .component-card {
|
||
background: var(--dm-surface);
|
||
box-shadow: 0 6px 14px rgba(0,0,0,0.2);
|
||
border-color: transparent !important;
|
||
}
|
||
html[data-theme="dark"] .component-card:hover {
|
||
border-color: rgba(255, 255, 255, 0.25) !important;
|
||
box-shadow: 0 6px 14px rgba(0,0,0,0.2), 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||
}
|
||
html[data-theme="dark"] .component-card .component-name { color: var(--dm-text); }
|
||
html[data-theme="dark"] .component-card .info-label { color: var(--dm-muted); }
|
||
html[data-theme="dark"] .component-card .info-value { color: var(--dm-text); }
|
||
.component-card.component-card--clickable { cursor: pointer; }
|
||
.component-card.component-card--low-stock {
|
||
border: 2px solid rgba(220, 53, 69, 0.4);
|
||
animation: component-card-low-stock-pulse 2s ease-in-out infinite;
|
||
}
|
||
@keyframes component-card-low-stock-pulse {
|
||
0%, 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.15); border-color: rgba(220, 53, 69, 0.4); }
|
||
50% { box-shadow: 0 0 0 4px rgba(220, 53, 69, 0.2); border-color: rgba(220, 53, 69, 0.65); }
|
||
}
|
||
html[data-theme="dark"] .component-card.component-card--low-stock {
|
||
border-color: rgba(220, 53, 69, 0.5);
|
||
}
|
||
html[data-theme="dark"] .component-card.component-card--low-stock {
|
||
animation-name: component-card-low-stock-pulse-dark;
|
||
}
|
||
@keyframes component-card-low-stock-pulse-dark {
|
||
0%, 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.2); border-color: rgba(220, 53, 69, 0.5); }
|
||
50% { box-shadow: 0 0 0 4px rgba(220, 53, 69, 0.25); border-color: rgba(220, 53, 69, 0.75); }
|
||
}
|
||
.component-card .component-header { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
|
||
.component-card .component-header .component-name { margin: 0; flex: 1; }
|
||
.component-card-actions { display: flex; align-items: center; gap: 0.25rem; flex-shrink: 0; }
|
||
.component-cards-grid.components-move-pending .component-card-move-up,
|
||
.component-cards-grid.components-move-pending .component-card-move-down { pointer-events: none; opacity: 0.6; }
|
||
.component-card-remove-btn { flex-shrink: 0; padding: 0.35rem 0.5rem; color: #dc3545; border-color: #dc3545; }
|
||
.component-card-remove-btn:hover { color: #fff; background-color: #dc3545; }
|
||
#componentCardModal { opacity: 0; transition: opacity 0.2s ease; pointer-events: none; }
|
||
#componentCardModal.component-card-modal-show { opacity: 1; pointer-events: auto; }
|
||
#componentCardModal .settings-modal-content { transform: scale(0.95); opacity: 0; transition: transform 0.2s ease, opacity 0.2s ease; }
|
||
#componentCardModal.component-card-modal-show .settings-modal-content { transform: scale(1); opacity: 1; }
|
||
.component-card-modal-consumed-row { text-align: center; padding: 0.75rem 1rem; font-size: 0.95rem; color: #64748b; border-top: 1px solid #e2e8f0; }
|
||
html[data-theme="dark"] .component-card-modal-consumed-row { color: var(--dm-muted); border-top-color: var(--dm-border); }
|
||
.component-card-modal-consumed { background: transparent; cursor: default; border: none; }
|
||
html[data-theme="dark"] .component-card-modal-consumed { background: transparent !important; color: var(--dm-muted) !important; border: none !important; }
|
||
|
||
/* Правая закладка и панель «Выбранные компоненты» (как в recipes.html) */
|
||
.sync-panel-tab {
|
||
position: fixed;
|
||
top: 50%;
|
||
right: 0;
|
||
transform: translateY(-50%);
|
||
z-index: 1001;
|
||
width: 32px;
|
||
height: 72px;
|
||
padding: 0;
|
||
border: none;
|
||
border-radius: 12px 0 0 12px;
|
||
background: var(--primary-color, #2196F3);
|
||
color: #fff;
|
||
cursor: pointer;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
box-shadow: -2px 0 8px rgba(0,0,0,0.15);
|
||
transition: background 0.2s, width 0.2s;
|
||
}
|
||
.sync-panel-tab:hover {
|
||
background: var(--primary-dark, #1976D2);
|
||
width: 36px;
|
||
}
|
||
.sync-panel-tab span {
|
||
display: block;
|
||
width: 14px;
|
||
height: 2px;
|
||
background: currentColor;
|
||
border-radius: 1px;
|
||
}
|
||
html[data-theme="dark"] .sync-panel-tab {
|
||
background: var(--dm-field, #5a5959);
|
||
color: var(--dm-text);
|
||
}
|
||
html[data-theme="dark"] .sync-panel-tab:hover {
|
||
background: var(--dm-border);
|
||
}
|
||
.sync-panel-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1001;
|
||
background: rgba(0, 0, 0, 0.25);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
.sync-panel-overlay.open {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
html[data-theme="dark"] .sync-panel-overlay {
|
||
background: rgba(0, 0, 0, 0.5);
|
||
}
|
||
.sync-panel {
|
||
position: fixed;
|
||
top: 0;
|
||
right: 0;
|
||
width: min(400px, 90vw);
|
||
max-width: 100%;
|
||
height: 100vh;
|
||
background: #fff;
|
||
box-shadow: -4px 0 20px rgba(0,0,0,0.12);
|
||
z-index: 1002;
|
||
transform: translateX(100%);
|
||
transition: transform 0.3s ease;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.sync-panel.open {
|
||
transform: translateX(0);
|
||
}
|
||
/* В обёртке панель и график — соседние блоки, без перекрытия */
|
||
.components-panel-wrap .sync-panel {
|
||
position: relative;
|
||
right: auto;
|
||
top: auto;
|
||
height: 100%;
|
||
transform: none;
|
||
flex-shrink: 0;
|
||
}
|
||
html[data-theme="dark"] .sync-panel {
|
||
background: var(--dm-bg);
|
||
color: var(--dm-text);
|
||
box-shadow: -4px 0 20px rgba(0,0,0,0.4);
|
||
}
|
||
.sync-panel-header {
|
||
flex-shrink: 0;
|
||
padding: 1rem 1.25rem;
|
||
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #f8f9fa;
|
||
}
|
||
html[data-theme="dark"] .sync-panel-header {
|
||
border-color: var(--dm-border);
|
||
background: var(--dm-surface);
|
||
}
|
||
.sync-panel-header h3 {
|
||
margin: 0;
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
}
|
||
.sync-panel-close {
|
||
width: 36px;
|
||
height: 36px;
|
||
padding: 0;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #666;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.25rem;
|
||
transition: background 0.2s, color 0.2s;
|
||
}
|
||
.sync-panel-close:hover {
|
||
background: rgba(0,0,0,0.06);
|
||
color: #333;
|
||
}
|
||
html[data-theme="dark"] .sync-panel-close {
|
||
color: var(--dm-muted);
|
||
}
|
||
html[data-theme="dark"] .sync-panel-close:hover {
|
||
background: var(--dm-border);
|
||
color: var(--dm-text);
|
||
}
|
||
.sync-panel-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 1rem;
|
||
}
|
||
.components-panel-item {
|
||
padding: 0.75rem 1rem;
|
||
margin-bottom: 0.5rem;
|
||
border: 1px solid rgba(0,0,0,0.1);
|
||
border-radius: 10px;
|
||
background: #f8fafc;
|
||
font-weight: 500;
|
||
}
|
||
.components-panel-item--clickable {
|
||
cursor: pointer;
|
||
}
|
||
.components-panel-item--clickable:hover {
|
||
background: #e2e8f0;
|
||
border-color: var(--primary-color, #2196F3);
|
||
}
|
||
html[data-theme="dark"] .components-panel-item {
|
||
background: var(--dm-surface);
|
||
border-color: var(--dm-border);
|
||
color: var(--dm-text);
|
||
}
|
||
html[data-theme="dark"] .components-panel-item--clickable:hover {
|
||
background: var(--dm-border);
|
||
border-color: var(--primary-color);
|
||
}
|
||
.components-panel-wrap {
|
||
position: fixed;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
z-index: 1002;
|
||
display: flex;
|
||
flex-direction: row-reverse;
|
||
pointer-events: none;
|
||
}
|
||
.components-panel-wrap .sync-panel,
|
||
.components-panel-wrap .components-chart-area {
|
||
pointer-events: auto;
|
||
}
|
||
.components-chart-area {
|
||
width: 0;
|
||
overflow: hidden;
|
||
transition: width 0.2s ease;
|
||
background: #fff;
|
||
box-shadow: -4px 0 20px rgba(0,0,0,0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-right: 1px solid rgba(0,0,0,0.08);
|
||
}
|
||
.components-chart-area.open {
|
||
width: min(1200px, 90vw);
|
||
}
|
||
html[data-theme="dark"] .components-chart-area {
|
||
background: var(--dm-bg);
|
||
border-right-color: var(--dm-border);
|
||
box-shadow: -4px 0 20px rgba(0,0,0,0.3);
|
||
}
|
||
.components-chart-area-header {
|
||
flex-shrink: 0;
|
||
padding: 1rem 1.25rem;
|
||
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #f8f9fa;
|
||
}
|
||
html[data-theme="dark"] .components-chart-area-header {
|
||
background: var(--dm-surface);
|
||
border-color: var(--dm-border);
|
||
}
|
||
.components-chart-area-header h3 { margin: 0; font-size: 1.1rem; font-weight: 600; }
|
||
.components-chart-area-close { width: 36px; height: 36px; padding: 0; border: none; border-radius: 8px; background: transparent; color: #666; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; }
|
||
.components-chart-area-close:hover { background: rgba(0,0,0,0.06); color: #333; }
|
||
html[data-theme="dark"] .components-chart-area-close { color: var(--dm-muted); }
|
||
html[data-theme="dark"] .components-chart-area-close:hover { background: var(--dm-border); color: var(--dm-text); }
|
||
.components-chart-area-body { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; }
|
||
.components-panel-chart-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-bottom: 1rem; }
|
||
.components-panel-chart-filters label { margin: 0; font-size: 0.9rem; }
|
||
.components-panel-chart-filters input[type="date"] { padding: 0.35rem 0.5rem; border-radius: 6px; border: 1px solid #ddd; }
|
||
.components-panel-chart-canvas-wrap { flex: 1; min-height: 280px; position: relative; }
|
||
.components-panel-item--selected { border-color: var(--primary-color, #2196F3); background: rgba(var(--primary-rgb), 0.08); }
|
||
html[data-theme="dark"] .components-panel-item--selected { background: rgba(var(--primary-rgb), 0.15); border-color: var(--primary-color); }
|
||
html[data-theme="dark"] .components-chart-area-header h3 { color: var(--dm-text); }
|
||
html[data-theme="dark"] .components-chart-area-body input[type="date"] { background: var(--dm-field); color: var(--dm-text); border-color: var(--dm-border); }
|
||
.components-panel-empty {
|
||
text-align: center;
|
||
padding: 1.5rem 1rem;
|
||
color: #64748b;
|
||
font-size: 0.95rem;
|
||
}
|
||
html[data-theme="dark"] .components-panel-empty {
|
||
color: var(--dm-muted);
|
||
}
|
||
</style>
|
||
<link href="/static/css/wesp-zootech-pages-polish.css?v=1" rel="stylesheet">
|
||
<link href="/static/css/wesp-zootech-theme-organic.css?v=6" rel="stylesheet">
|
||
</head>
|
||
<body class="zootech-app-body">
|
||
<div id="wespZootechNavMount" data-active-page="feed_consumption"></div>
|
||
|
||
<div class="page-content wesp-page-main">
|
||
<div class="container">
|
||
<div class="consumption-section">
|
||
<div class="consumption-block zt-section" data-wesp-enter>
|
||
<h2 class="section-title">
|
||
<i class="fas fa-list" style="color: var(--primary-color);"></i>
|
||
Склад и учёт кормов
|
||
</h2>
|
||
<div class="consumption-filters">
|
||
<label for="consumptionDateFrom">С:</label>
|
||
<input type="date" id="consumptionDateFrom" aria-label="Дата начала">
|
||
<label for="consumptionDateTo">По:</label>
|
||
<input type="date" id="consumptionDateTo" aria-label="Дата окончания">
|
||
<button type="button" class="btn btn-outline-secondary consumption-filters-add-btn" id="openFeedAccountingBtn">
|
||
<i class="fas fa-file-alt"></i> Учётные документы
|
||
</button>
|
||
<button type="button" class="btn btn-outline-primary consumption-filters-add-btn" id="addToSkladBtn">
|
||
<i class="fas fa-plus"></i> Добавить на склад
|
||
</button>
|
||
</div>
|
||
<div id="stockForecastBanner" class="stock-forecast-banner" hidden role="status"></div>
|
||
<div id="consumptionComponentsList" class="component-cards-grid">
|
||
<!-- Карточки подгружаются через /api/sklad (остатки — recipes.db, расход — reports.db) -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно: добавить компонент на склад -->
|
||
<div id="addToSkladModal" class="settings-modal" style="display: none;">
|
||
<div class="settings-modal-content">
|
||
<div class="settings-modal-header">
|
||
<h3><i class="fas fa-plus"></i> Добавить компонент на склад</h3>
|
||
<span class="settings-close" onclick="closeAddToSkladModal()">×</span>
|
||
</div>
|
||
<div class="settings-modal-body">
|
||
<p class="text-muted small mb-2">Выберите компонент из списка:</p>
|
||
<div class="add-to-sklad-search-wrap mb-2">
|
||
<input type="text" id="addToSkladSearch" class="form-control" placeholder="Поиск по названию или типу..." aria-label="Поиск компонента" autocomplete="off">
|
||
</div>
|
||
<div id="addToSkladList" class="list-group"></div>
|
||
<div id="addToSkladNoMatch" class="text-muted py-3 text-center" style="display: none;">Нет компонентов по запросу</div>
|
||
<div id="addToSkladEmpty" class="text-muted py-3 text-center" style="display: none;">Нет доступных компонентов для добавления</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно: карточка компонента (Всего кг, Израсходовано внизу) -->
|
||
<div id="componentCardModal" class="settings-modal" style="display: none;">
|
||
<div class="settings-modal-content">
|
||
<div class="settings-modal-header">
|
||
<h3><i class="fas fa-box-open"></i> <span id="componentCardModalTitle">—</span></h3>
|
||
<span class="settings-close" onclick="closeComponentCardModal()">×</span>
|
||
</div>
|
||
<div class="settings-modal-body">
|
||
<div class="form-group mb-2">
|
||
<label class="form-label">Всего на складе (кг)</label>
|
||
<input type="number" step="0.1" min="0" id="componentCardModalTotalKg" class="form-control" placeholder="0">
|
||
</div>
|
||
<div class="form-group mb-0">
|
||
<label class="form-label">Поступление (кг)</label>
|
||
<input type="number" step="0.1" min="0" id="componentCardModalInflowKg" class="form-control" placeholder="0">
|
||
</div>
|
||
</div>
|
||
<div class="component-card-modal-consumed-row">
|
||
<span id="componentCardModalConsumed" class="component-card-modal-consumed">Израсходовано: —</span>
|
||
</div>
|
||
<p id="componentCardModalExplanation" class="text-muted small px-3 mb-0" hidden></p>
|
||
<div class="settings-modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeComponentCardModal()">Закрыть</button>
|
||
<button type="button" class="btn btn-primary" id="componentCardModalSaveBtn"><i class="fas fa-save"></i> Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модалка: учётные документы (СП-20, журнал, экспорт) -->
|
||
<div id="feedAccountingModal" class="settings-modal" style="display: none;">
|
||
<div class="settings-modal-content">
|
||
<div class="settings-modal-header">
|
||
<h3><i class="fas fa-file-alt"></i> Учётные документы</h3>
|
||
<span class="settings-close" onclick="closeFeedAccountingModal()">×</span>
|
||
</div>
|
||
<div class="settings-modal-body">
|
||
<div class="fa-tab-bar">
|
||
<button type="button" class="fa-tab-btn zt-tab active" data-tab="requisites">Реквизиты СП-20</button>
|
||
<button type="button" class="fa-tab-btn zt-tab" data-tab="signatures">Подписи</button>
|
||
<button type="button" class="fa-tab-btn zt-tab" data-tab="export">Экспорт</button>
|
||
</div>
|
||
<div class="fa-tab-panel" data-tab="requisites">
|
||
<p class="text-muted small">Данные для шапки формы СП-20 (ОКУД 0325020). Сохраняются один раз и подставляются в каждый документ.</p>
|
||
<div class="fa-org-grid">
|
||
<div class="fa-field--wide"><label class="form-label" for="faOrgName">Организация</label><input id="faOrgName" class="form-control" type="text" autocomplete="organization"></div>
|
||
<div><label class="form-label" for="faOkpo">ОКПО</label><input id="faOkpo" class="form-control" type="text"></div>
|
||
<div><label class="form-label" for="faDocPrefix">Префикс №</label><input id="faDocPrefix" class="form-control" type="text"></div>
|
||
<div class="fa-field--wide"><label class="form-label" for="faFarm">Ферма</label><input id="faFarm" class="form-control" type="text" list="faFarmList" placeholder="Подтягивается из базы"><datalist id="faFarmList"></datalist></div>
|
||
<div><label class="form-label" for="faDepartment">Отделение</label><input id="faDepartment" class="form-control" type="text"></div>
|
||
<div><label class="form-label" for="faBrigade">Бригада</label><input id="faBrigade" class="form-control" type="text"></div>
|
||
<div><label class="form-label" for="faAnimalType">Вид скота</label><input id="faAnimalType" class="form-control" type="text"></div>
|
||
<div><label class="form-label" for="faResponsible">Ответственный</label><input id="faResponsible" class="form-control" type="text"></div>
|
||
<div><label class="form-label" for="faZootech">Зоотехник (ФИО)</label><input id="faZootech" class="form-control" type="text"></div>
|
||
<div><label class="form-label" for="faWarehouse">Кладовщик (ФИО)</label><input id="faWarehouse" class="form-control" type="text"></div>
|
||
</div>
|
||
</div>
|
||
<div class="fa-tab-panel" data-tab="signatures" hidden>
|
||
<p class="text-muted small">Нарисуйте подписи пальцем или стилусом. Сохранение — одной кнопкой «Сохранить всё» внизу окна.</p>
|
||
<div class="fa-sig-block">
|
||
<label class="form-label">Зоотехник</label>
|
||
<canvas id="faSigZootech" class="fa-sig-canvas" width="320" height="80"></canvas>
|
||
<div class="fa-sig-actions"><button type="button" class="btn btn-sm btn-outline-secondary" id="clear_faSigZootech">Очистить</button></div>
|
||
</div>
|
||
<div class="fa-sig-block">
|
||
<label class="form-label">Кладовщик</label>
|
||
<canvas id="faSigWarehouse" class="fa-sig-canvas" width="320" height="80"></canvas>
|
||
<div class="fa-sig-actions"><button type="button" class="btn btn-sm btn-outline-secondary" id="clear_faSigWarehouse">Очистить</button></div>
|
||
</div>
|
||
<div class="fa-sig-block">
|
||
<label class="form-label">Получатель</label>
|
||
<canvas id="faSigRecipient" class="fa-sig-canvas" width="320" height="80"></canvas>
|
||
<div class="fa-sig-actions"><button type="button" class="btn btn-sm btn-outline-secondary" id="clear_faSigRecipient">Очистить</button></div>
|
||
</div>
|
||
</div>
|
||
<div class="fa-tab-panel" data-tab="export" hidden>
|
||
<div class="fa-field mb-3">
|
||
<label class="form-label" for="faExportMonth">Месяц для СП-20 и журнала</label>
|
||
<input id="faExportMonth" class="form-control" type="month" style="max-width: 14rem;">
|
||
</div>
|
||
<p id="faSp20Status" class="text-muted small mb-3">Загрузка сводки…</p>
|
||
<p class="text-muted small">Остатки и потребление — за период «С / По» на странице.</p>
|
||
<div class="fa-export-grid">
|
||
<button type="button" class="btn btn-outline-success" id="faExportConsumptionXlsx"><i class="fas fa-file-excel"></i> Excel: потребление</button>
|
||
<button type="button" class="btn btn-outline-danger" id="faExportConsumptionPdf"><i class="fas fa-file-pdf"></i> PDF: потребление</button>
|
||
<button type="button" class="btn btn-outline-success" id="faExportStock"><i class="fas fa-file-excel"></i> Excel: остатки</button>
|
||
<button type="button" class="btn btn-outline-success" id="faExportSp20Xlsx"><i class="fas fa-file-excel"></i> Excel: СП-20</button>
|
||
<button type="button" class="btn btn-outline-danger" id="faExportSp20Pdf"><i class="fas fa-file-pdf"></i> PDF: СП-20</button>
|
||
<button type="button" class="btn btn-outline-success" id="faExportJournalXlsx"><i class="fas fa-file-excel"></i> Excel: журнал</button>
|
||
<button type="button" class="btn btn-outline-danger" id="faExportJournalPdf"><i class="fas fa-file-pdf"></i> PDF: журнал</button>
|
||
<button type="button" class="btn btn-primary" id="faExportZip"><i class="fas fa-file-archive"></i> Скачать всё (ZIP)</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="fa-modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeFeedAccountingModal()">Закрыть</button>
|
||
<button type="button" class="btn btn-primary" id="faSaveAllBtn"><i class="fas fa-save"></i> Сохранить всё</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/static/js/feed-accounting-modal.js?v=5"></script>
|
||
<script>
|
||
const notyf = window.WespZootechNotify.createAdapter();
|
||
|
||
function getDefaultMonthDates() {
|
||
var now = new Date();
|
||
var from = new Date(now.getFullYear(), now.getMonth(), 1);
|
||
var to = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||
return { from: from.toISOString().slice(0, 10), to: to.toISOString().slice(0, 10) };
|
||
}
|
||
function setDefaultConsumptionDates() {
|
||
var d = getDefaultMonthDates();
|
||
var elFrom = document.getElementById('consumptionDateFrom');
|
||
var elTo = document.getElementById('consumptionDateTo');
|
||
if (elFrom && !elFrom.value) elFrom.value = d.from;
|
||
if (elTo && !elTo.value) elTo.value = d.to;
|
||
}
|
||
|
||
// Дебаунс для загрузки списка при смене дат — один запрос вместо нескольких при быстром изменении или инициализации
|
||
var loadConsumptionComponentsDebounceTimer = null;
|
||
function loadConsumptionComponentsDebounced() {
|
||
if (loadConsumptionComponentsDebounceTimer) clearTimeout(loadConsumptionComponentsDebounceTimer);
|
||
loadConsumptionComponentsDebounceTimer = setTimeout(function() {
|
||
loadConsumptionComponentsDebounceTimer = null;
|
||
loadConsumptionComponents();
|
||
}, 150);
|
||
}
|
||
|
||
// Потребление по компонентам через модуль склад: остатки из recipes.db, расход из reports.db (считаем, не пишем)
|
||
function loadConsumptionComponents() {
|
||
var dateFrom = document.getElementById('consumptionDateFrom') && document.getElementById('consumptionDateFrom').value;
|
||
var dateTo = document.getElementById('consumptionDateTo') && document.getElementById('consumptionDateTo').value;
|
||
var url = '/api/sklad';
|
||
if (dateFrom && dateTo) {
|
||
url += '?date_from=' + encodeURIComponent(dateFrom) + '&date_to=' + encodeURIComponent(dateTo);
|
||
}
|
||
fetch(url)
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
var list = document.getElementById('consumptionComponentsList');
|
||
if (!list) return;
|
||
var items = data.items;
|
||
if (data.error || !Array.isArray(items)) {
|
||
list.innerHTML = '<div class="text-danger py-3"><i class="fas fa-exclamation-triangle"></i> ' + (data.error || data.message || 'Ошибка загрузки') + '</div>';
|
||
return;
|
||
}
|
||
if (items.length === 0) {
|
||
list.innerHTML = '<div class="text-muted py-5 text-center"><i class="fas fa-box-open fa-3x mb-3"></i><p>Нет данных о расходе за выбранный период.</p></div>';
|
||
return;
|
||
}
|
||
function fmtKg(x) {
|
||
var n = Number(x);
|
||
return (Number.isFinite(n) ? n.toFixed(1) : '—') + ' кг';
|
||
}
|
||
function fmtDaysLabel(label, daysVal) {
|
||
if (label) return label;
|
||
if (daysVal != null && Number.isFinite(Number(daysVal))) {
|
||
var d = Number(daysVal);
|
||
if (d >= 9999) return '≥9999';
|
||
if (d < 1) return '~' + Math.round(d * 24) + ' ч';
|
||
return '~' + (d === Math.floor(d) ? String(Math.floor(d)) : d.toFixed(1)) + ' дн';
|
||
}
|
||
return '—';
|
||
}
|
||
list.innerHTML = items.map(function(item, index) {
|
||
var rawName = (item.name || '—');
|
||
var name = rawName.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
var cid = (item.component_id || '').replace(/"/g, '"');
|
||
var forecastKey = (item.forecast_key || item.component_id || item.name_key || '').replace(/"/g, '"');
|
||
var totalVal = Number(item.total_kg);
|
||
var totalStr = Number.isFinite(totalVal) ? totalVal.toFixed(1) : '';
|
||
var consumedVal = item.consumed_total_kg != null ? Number(item.consumed_total_kg) : Number(item.consumed_kg);
|
||
if (!Number.isFinite(consumedVal)) consumedVal = 0;
|
||
var consumed = fmtKg(consumedVal);
|
||
var remainingVal = Number(item.remaining_kg);
|
||
if (!Number.isFinite(remainingVal)) remainingVal = 0;
|
||
var remaining = fmtKg(remainingVal);
|
||
var plannedPerDay = Number(item.planned_consumption_per_day_kg);
|
||
var plannedPerDayStr = (Number.isFinite(plannedPerDay) && plannedPerDay > 0) ? (plannedPerDay.toFixed(2) + ' кг/сут') : '—';
|
||
var daysLeftPlan = item.days_left_plan != null ? Number(item.days_left_plan) : null;
|
||
var daysLeftStr = (daysLeftPlan != null && Number.isFinite(daysLeftPlan)) ? (daysLeftPlan >= 9999 ? '≥9999' : daysLeftPlan.toFixed(1) + ' дн.') : '—';
|
||
var daysLeftAdj = item.days_left_adjusted != null ? Number(item.days_left_adjusted) : (item.daysLeftAdjusted != null ? Number(item.daysLeftAdjusted) : null);
|
||
var daysLeftAdjStr = fmtDaysLabel(item.days_left_adjusted_label || item.daysLeftAdjustedLabel, daysLeftAdj);
|
||
var adjWarnClass = (daysLeftAdj != null && Number.isFinite(daysLeftAdj) && daysLeftAdj <= 2) ? ' info-item--warn' : '';
|
||
var explanation = (item.explanation || '').replace(/</g, '<').replace(/>/g, '>');
|
||
var explanationHidden = explanation ? '' : ' hidden';
|
||
var lowStockClass = ((daysLeftPlan != null && Number.isFinite(daysLeftPlan) && daysLeftPlan <= 1.5) || (daysLeftAdj != null && Number.isFinite(daysLeftAdj) && daysLeftAdj <= 1.5)) ? ' component-card--low-stock' : '';
|
||
var moveUpDisabled = index === 0 ? ' disabled' : '';
|
||
var moveDownDisabled = index === items.length - 1 ? ' disabled' : '';
|
||
var inflowVal = item.inflow_kg != null ? Number(item.inflow_kg) : 0;
|
||
var inflowStr = Number.isFinite(inflowVal) ? inflowVal.toFixed(1) : '0';
|
||
var hasStock = item.has_stock !== false;
|
||
var cardClass = 'component-card component-card--clickable' + (hasStock ? '' : ' component-card--report-only') + lowStockClass;
|
||
var actionsHtml = hasStock
|
||
? '<div class="component-card-actions">' +
|
||
'<button type="button" class="btn btn-sm btn-outline-secondary component-card-move-up"' + moveUpDisabled + ' title="Выше" aria-label="Выше"><i class="fas fa-arrow-up"></i></button>' +
|
||
'<button type="button" class="btn btn-sm btn-outline-secondary component-card-move-down"' + moveDownDisabled + ' title="Ниже" aria-label="Ниже"><i class="fas fa-arrow-down"></i></button>' +
|
||
'<button type="button" class="btn btn-sm btn-outline-danger component-card-remove-btn" data-component-id="' + cid + '" data-component-name="' + rawName.replace(/"/g, '"') + '" title="Убрать со склада" aria-label="Убрать со склада"><i class="fas fa-trash"></i></button>' +
|
||
'</div>'
|
||
: '<span class="badge bg-secondary">расход из отчётов</span>';
|
||
return '<div class="' + cardClass + '" data-component-id="' + cid + '" data-forecast-key="' + forecastKey + '" data-has-stock="' + (hasStock ? '1' : '0') + '" data-sort-index="' + index + '" data-component-name="' + rawName.replace(/"/g, '"') + '" data-consumed="' + consumed.replace(/"/g, '"') + '" data-total-kg="' + totalStr + '" data-inflow-kg="' + inflowStr + '">' +
|
||
'<div class="component-header">' +
|
||
'<h3 class="component-name">' + name + '</h3>' +
|
||
actionsHtml +
|
||
'</div>' +
|
||
'<div class="component-info">' +
|
||
'<div class="info-item"><span class="info-label"><i class="fas fa-box-open me-1"></i> Осталось:</span><span class="info-value">' + remaining + '</span></div>' +
|
||
'<div class="info-item"><span class="info-label"><i class="fas fa-history me-1"></i> Расход /24ч (план):</span><span class="info-value">' + plannedPerDayStr + '</span></div>' +
|
||
'<div class="info-item"><span class="info-label"><i class="fas fa-calendar-alt me-1"></i> Хватит по рецепту:</span><span class="info-value">' + daysLeftStr + '</span></div>' +
|
||
'<div class="info-item' + adjWarnClass + '"><span class="info-label"><i class="fas fa-calendar-check me-1"></i> Хватит (с учётом плана):</span><span class="info-value" data-stock-days-adjusted>' + daysLeftAdjStr + '</span></div>' +
|
||
'<p class="stock-card-explanation text-muted small mb-0" data-stock-explanation' + explanationHidden + '>' + explanation + '</p>' +
|
||
'</div></div>';
|
||
}).join('');
|
||
if (window.WespStockForecast && typeof window.WespStockForecast.refresh === 'function') {
|
||
window.WespStockForecast.refresh();
|
||
}
|
||
})
|
||
.catch(function(err) {
|
||
var list = document.getElementById('consumptionComponentsList');
|
||
if (list) list.innerHTML = '<div class="text-danger py-3"><i class="fas fa-exclamation-triangle"></i> Ошибка загрузки данных</div>';
|
||
console.error('loadConsumptionComponents:', err);
|
||
});
|
||
}
|
||
|
||
function moveComponent(componentId, toIndex) {
|
||
var list = document.getElementById('consumptionComponentsList');
|
||
if (list) list.classList.add('components-move-pending');
|
||
return fetch('/api/sklad/' + encodeURIComponent(componentId) + '/move', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ to_index: toIndex })
|
||
}).then(function(r) { return r.json(); }).then(function(payload) {
|
||
if (payload.success) {
|
||
loadConsumptionComponents();
|
||
} else {
|
||
notyf.error(payload.error, { fallback: 'Ошибка перемещения' });
|
||
}
|
||
}).catch(function() {
|
||
notyf.error('Ошибка перемещения');
|
||
}).finally(function() {
|
||
if (list) list.classList.remove('components-move-pending');
|
||
});
|
||
}
|
||
function moveComponentUp(componentId, currentIndex) {
|
||
if (!Number.isFinite(currentIndex) || currentIndex <= 0) return;
|
||
moveComponent(componentId, currentIndex - 1);
|
||
}
|
||
function moveComponentDown(componentId, currentIndex) {
|
||
var list = document.getElementById('consumptionComponentsList');
|
||
var total = list ? list.querySelectorAll('.component-card').length : 0;
|
||
if (!Number.isFinite(currentIndex) || currentIndex >= total - 1) return;
|
||
moveComponent(componentId, currentIndex + 1);
|
||
}
|
||
|
||
function filterAddToSkladList() {
|
||
var searchEl = document.getElementById('addToSkladSearch');
|
||
var listEl = document.getElementById('addToSkladList');
|
||
var noMatchEl = document.getElementById('addToSkladNoMatch');
|
||
if (!searchEl || !listEl) return;
|
||
var q = (searchEl.value || '').trim().toLowerCase();
|
||
var buttons = listEl.querySelectorAll('.list-group-item');
|
||
var visibleCount = 0;
|
||
buttons.forEach(function(btn) {
|
||
var searchText = (btn.dataset.search || btn.textContent || '').toLowerCase();
|
||
var show = !q || searchText.indexOf(q) !== -1;
|
||
btn.style.display = show ? '' : 'none';
|
||
if (show) visibleCount++;
|
||
});
|
||
if (noMatchEl) noMatchEl.style.display = (q && buttons.length > 0 && visibleCount === 0) ? 'block' : 'none';
|
||
}
|
||
|
||
function openAddToSkladModal() {
|
||
var modal = document.getElementById('addToSkladModal');
|
||
var listEl = document.getElementById('addToSkladList');
|
||
var emptyEl = document.getElementById('addToSkladEmpty');
|
||
var noMatchEl = document.getElementById('addToSkladNoMatch');
|
||
var searchEl = document.getElementById('addToSkladSearch');
|
||
if (!modal || !listEl) return;
|
||
listEl.innerHTML = '<div class="text-center text-muted py-3">Загрузка...</div>';
|
||
if (emptyEl) emptyEl.style.display = 'none';
|
||
if (noMatchEl) noMatchEl.style.display = 'none';
|
||
if (searchEl) searchEl.value = '';
|
||
modal.style.display = 'block';
|
||
document.body.style.overflow = 'hidden';
|
||
Promise.all([fetch('/api/sklad').then(function(r) { return r.json(); }), fetch('/api/components').then(function(r) { return r.json(); })])
|
||
.then(function(results) {
|
||
var skladData = results[0];
|
||
var components = results[1];
|
||
var items = Array.isArray(skladData.items) ? skladData.items : [];
|
||
var existingIds = new Set(items.map(function(it) { return String(it.component_id || ''); }));
|
||
var available = Array.isArray(components) ? components.filter(function(c) { return c && c.id && !existingIds.has(String(c.id)); }) : [];
|
||
listEl.innerHTML = '';
|
||
if (available.length === 0) {
|
||
if (emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = existingIds.length ? 'Все компоненты уже на складе' : 'Нет компонентов'; }
|
||
return;
|
||
}
|
||
available.forEach(function(c) {
|
||
var label = (c.name || '') + (c.type ? ' (' + c.type + ')' : '');
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'list-group-item list-group-item-action text-start';
|
||
btn.textContent = label;
|
||
btn.dataset.componentId = c.id;
|
||
btn.dataset.componentName = c.name || '';
|
||
btn.dataset.search = label.toLowerCase();
|
||
listEl.appendChild(btn);
|
||
});
|
||
listEl.querySelectorAll('button').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var id = btn.dataset.componentId;
|
||
if (!id) return;
|
||
btn.disabled = true;
|
||
fetch('/api/sklad/add', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ component_id: id })
|
||
}).then(function(r) { return r.json(); }).then(function(payload) {
|
||
if (payload.success) {
|
||
closeAddToSkladModal();
|
||
loadConsumptionComponents();
|
||
} else {
|
||
notyf.error(payload.error, { fallback: 'Ошибка добавления' });
|
||
btn.disabled = false;
|
||
}
|
||
}).catch(function() {
|
||
notyf.error('Ошибка добавления');
|
||
btn.disabled = false;
|
||
});
|
||
});
|
||
});
|
||
filterAddToSkladList();
|
||
})
|
||
.catch(function(e) {
|
||
listEl.innerHTML = '<div class="text-center text-danger py-3">Ошибка загрузки</div>';
|
||
if (searchEl) searchEl.style.display = 'none';
|
||
});
|
||
}
|
||
function closeAddToSkladModal() {
|
||
var modal = document.getElementById('addToSkladModal');
|
||
if (modal) { modal.style.display = 'none'; document.body.style.overflow = ''; }
|
||
}
|
||
|
||
function openComponentCardModal(card) {
|
||
var modal = document.getElementById('componentCardModal');
|
||
var titleEl = document.getElementById('componentCardModalTitle');
|
||
var consumedEl = document.getElementById('componentCardModalConsumed');
|
||
var totalInput = document.getElementById('componentCardModalTotalKg');
|
||
var inflowInput = document.getElementById('componentCardModalInflowKg');
|
||
if (!modal || !titleEl || !consumedEl || !totalInput) return;
|
||
var name = (card.dataset.componentName || '').replace(/"/g, '"');
|
||
var consumed = (card.dataset.consumed || '—').replace(/"/g, '"');
|
||
var totalKg = card.dataset.totalKg || '';
|
||
var inflowKg = card.dataset.inflowKg || '0';
|
||
modal.dataset.componentId = card.dataset.componentId || '';
|
||
modal.dataset.hasStock = card.dataset.hasStock || '1';
|
||
modal.dataset.forecastKey = card.dataset.forecastKey || card.dataset.componentId || '';
|
||
titleEl.textContent = name || '—';
|
||
consumedEl.textContent = 'Израсходовано: ' + consumed;
|
||
totalInput.value = totalKg;
|
||
if (inflowInput) inflowInput.value = inflowKg;
|
||
var explEl = document.getElementById('componentCardModalExplanation');
|
||
var fc = window.WespStockForecast && window.WespStockForecast.getForecast
|
||
? window.WespStockForecast.getForecast(card.dataset.forecastKey || card.dataset.componentId || '')
|
||
: null;
|
||
if (explEl) {
|
||
if (fc && fc.explanation) {
|
||
explEl.textContent = fc.explanation;
|
||
explEl.hidden = false;
|
||
} else {
|
||
explEl.textContent = '';
|
||
explEl.hidden = true;
|
||
}
|
||
}
|
||
modal.style.display = 'block';
|
||
document.body.style.overflow = 'hidden';
|
||
requestAnimationFrame(function() { modal.classList.add('component-card-modal-show'); });
|
||
totalInput.focus();
|
||
}
|
||
|
||
function closeComponentCardModal() {
|
||
var modal = document.getElementById('componentCardModal');
|
||
if (!modal) return;
|
||
modal.classList.remove('component-card-modal-show');
|
||
setTimeout(function() {
|
||
modal.style.display = 'none';
|
||
document.body.style.overflow = '';
|
||
}, 200);
|
||
}
|
||
|
||
// Один делегированный обработчик клика по списку карточек (не добавлять при каждой загрузке — иначе накапливаются запросы)
|
||
function bindConsumptionComponentsListClick() {
|
||
var list = document.getElementById('consumptionComponentsList');
|
||
if (!list || list._componentsClickBound) return;
|
||
list._componentsClickBound = true;
|
||
list.addEventListener('click', function(e) {
|
||
var removeBtn = e.target && e.target.closest && e.target.closest('.component-card-remove-btn');
|
||
if (removeBtn) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
var cid = removeBtn.dataset.componentId;
|
||
var name = (removeBtn.dataset.componentName || '').replace(/"/g, '"');
|
||
void (async function() {
|
||
if (!cid || !(await WespDialog.confirm('Убрать «' + (name || cid) + '» со склада?'))) return;
|
||
removeBtn.disabled = true;
|
||
fetch('/api/sklad/' + encodeURIComponent(cid), { method: 'DELETE' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(payload) {
|
||
removeBtn.disabled = false;
|
||
if (payload.success) { notyf.success('Убрано со склада'); loadConsumptionComponents(); }
|
||
else notyf.error(payload.error, { fallback: 'Ошибка' });
|
||
})
|
||
.catch(function() { removeBtn.disabled = false; notyf.error('Ошибка'); });
|
||
})();
|
||
return;
|
||
}
|
||
var moveUp = e.target && e.target.closest && e.target.closest('.component-card-move-up');
|
||
if (moveUp && !moveUp.disabled) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
var card = moveUp.closest('.component-card');
|
||
if (card) moveComponentUp(card.dataset.componentId, parseInt(card.dataset.sortIndex, 10));
|
||
return;
|
||
}
|
||
var moveDown = e.target && e.target.closest && e.target.closest('.component-card-move-down');
|
||
if (moveDown && !moveDown.disabled) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
var card = moveDown.closest('.component-card');
|
||
if (card) moveComponentDown(card.dataset.componentId, parseInt(card.dataset.sortIndex, 10));
|
||
return;
|
||
}
|
||
var card = e.target && e.target.closest && e.target.closest('.component-card.component-card--clickable');
|
||
if (!card) return;
|
||
e.preventDefault();
|
||
openComponentCardModal(card);
|
||
});
|
||
}
|
||
|
||
// Проверяем авторизацию при загрузке страницы
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
setDefaultConsumptionDates();
|
||
bindConsumptionComponentsListClick();
|
||
loadConsumptionComponents();
|
||
var elFrom = document.getElementById('consumptionDateFrom');
|
||
var elTo = document.getElementById('consumptionDateTo');
|
||
if (elFrom) elFrom.addEventListener('change', loadConsumptionComponentsDebounced);
|
||
if (elTo) elTo.addEventListener('change', loadConsumptionComponentsDebounced);
|
||
var addBtn = document.getElementById('addToSkladBtn');
|
||
if (addBtn) addBtn.addEventListener('click', openAddToSkladModal);
|
||
var addToSkladSearchEl = document.getElementById('addToSkladSearch');
|
||
if (addToSkladSearchEl) addToSkladSearchEl.addEventListener('input', filterAddToSkladList);
|
||
var addModal = document.getElementById('addToSkladModal');
|
||
if (addModal) addModal.addEventListener('click', function(e) { if (e.target === addModal) closeAddToSkladModal(); });
|
||
|
||
var cardModal = document.getElementById('componentCardModal');
|
||
if (cardModal) cardModal.addEventListener('click', function(e) { if (e.target === cardModal) closeComponentCardModal(); });
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
var m = document.getElementById('componentCardModal');
|
||
if (m && m.style.display === 'block') closeComponentCardModal();
|
||
var panel = document.getElementById('componentsPanel');
|
||
if (panel && panel.classList.contains('open')) closeComponentsPanel();
|
||
}
|
||
});
|
||
|
||
var componentsPanelTab = document.getElementById('componentsPanelTab');
|
||
var componentsPanelOverlay = document.getElementById('componentsPanelOverlay');
|
||
var componentsPanel = document.getElementById('componentsPanel');
|
||
var componentsPanelClose = document.getElementById('componentsPanelClose');
|
||
if (componentsPanelTab) componentsPanelTab.addEventListener('click', openComponentsPanel);
|
||
if (componentsPanelClose) componentsPanelClose.addEventListener('click', closeComponentsPanel);
|
||
if (componentsPanelOverlay) componentsPanelOverlay.addEventListener('click', closeComponentsPanel);
|
||
|
||
var listEl = document.getElementById('componentsPanelList');
|
||
if (listEl) listEl.addEventListener('click', function(e) {
|
||
var item = e.target && e.target.closest && e.target.closest('.components-panel-item--clickable');
|
||
if (!item) return;
|
||
var id = item.dataset.componentId;
|
||
var name = item.dataset.componentName || '—';
|
||
if (id) openComponentsChartView(id, name, item);
|
||
});
|
||
var chartAreaCloseBtn = document.getElementById('componentsChartAreaClose');
|
||
if (chartAreaCloseBtn) chartAreaCloseBtn.addEventListener('click', closeComponentsChartView);
|
||
var chartBuildBtn = document.getElementById('componentsPanelChartBuild');
|
||
if (chartBuildBtn) chartBuildBtn.addEventListener('click', function() { loadComponentsChart(); });
|
||
|
||
var saveBtn = document.getElementById('componentCardModalSaveBtn');
|
||
if (saveBtn) saveBtn.addEventListener('click', function() {
|
||
var modal = document.getElementById('componentCardModal');
|
||
var cid = modal && modal.dataset.componentId;
|
||
var hasStock = modal && modal.dataset.hasStock !== '0';
|
||
var totalInput = document.getElementById('componentCardModalTotalKg');
|
||
var inflowInput = document.getElementById('componentCardModalInflowKg');
|
||
if (!totalInput) return;
|
||
var v = Number(totalInput.value);
|
||
if (!Number.isFinite(v) || v < 0) { notyf.error('Введите число ≥ 0'); return; }
|
||
var inflowVal = inflowInput ? Math.max(0, Number(inflowInput.value) || 0) : 0;
|
||
function saveStockTotal(componentId) {
|
||
saveBtn.disabled = true;
|
||
return fetch('/api/sklad/' + encodeURIComponent(componentId), {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ total_kg: v, inflow_kg: inflowVal })
|
||
}).then(function(r) { return r.json(); }).then(function(payload) {
|
||
saveBtn.disabled = false;
|
||
if (payload.success) { notyf.success('Сохранено'); closeComponentCardModal(); loadConsumptionComponents(); }
|
||
else notyf.error(payload.error, { fallback: 'Ошибка' });
|
||
}).catch(function() { saveBtn.disabled = false; notyf.error('Ошибка сохранения'); });
|
||
}
|
||
if (!hasStock) {
|
||
if (!cid) { notyf.error('Компонент не найден в справочнике — добавьте его в «Компоненты»'); return; }
|
||
saveBtn.disabled = true;
|
||
fetch('/api/sklad/add', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ component_id: cid })
|
||
}).then(function(r) { return r.json(); }).then(function(payload) {
|
||
if (!payload.success) {
|
||
saveBtn.disabled = false;
|
||
notyf.error(payload.error, { fallback: 'Ошибка добавления на склад' });
|
||
return;
|
||
}
|
||
return saveStockTotal(cid);
|
||
}).catch(function() { saveBtn.disabled = false; notyf.error('Ошибка добавления на склад'); });
|
||
return;
|
||
}
|
||
if (!cid) return;
|
||
saveStockTotal(cid);
|
||
});
|
||
});
|
||
|
||
function openComponentsPanel() {
|
||
var overlay = document.getElementById('componentsPanelOverlay');
|
||
var wrap = document.getElementById('componentsPanelWrap');
|
||
var panel = document.getElementById('componentsPanel');
|
||
if (overlay) overlay.classList.add('open');
|
||
if (wrap) wrap.style.display = 'flex';
|
||
if (panel) panel.classList.add('open');
|
||
loadComponentsPanelList();
|
||
}
|
||
|
||
function closeComponentsPanel() {
|
||
var overlay = document.getElementById('componentsPanelOverlay');
|
||
var wrap = document.getElementById('componentsPanelWrap');
|
||
var panel = document.getElementById('componentsPanel');
|
||
if (overlay) overlay.classList.remove('open');
|
||
if (wrap) wrap.style.display = 'none';
|
||
if (panel) panel.classList.remove('open');
|
||
closeComponentsChartView();
|
||
}
|
||
|
||
function loadComponentsPanelList() {
|
||
var listEl = document.getElementById('componentsPanelList');
|
||
var emptyEl = document.getElementById('componentsPanelEmpty');
|
||
var errorEl = document.getElementById('componentsPanelError');
|
||
if (!listEl) return;
|
||
listEl.innerHTML = '';
|
||
if (emptyEl) emptyEl.style.display = 'none';
|
||
if (errorEl) { errorEl.style.display = 'none'; errorEl.textContent = ''; }
|
||
fetch('/api/sklad').then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
var items = data.items;
|
||
if (data.error || !Array.isArray(items)) {
|
||
if (errorEl) { errorEl.textContent = window.WespUserMessages?.userFacingMessage?.(data.error || data.message, 'Не удалось загрузить прогноз') || 'Не удалось загрузить прогноз'; errorEl.style.display = 'block'; }
|
||
return;
|
||
}
|
||
if (items.length === 0) {
|
||
if (emptyEl) emptyEl.style.display = 'block';
|
||
return;
|
||
}
|
||
items.forEach(function(item) {
|
||
var div = document.createElement('div');
|
||
div.className = 'components-panel-item components-panel-item--clickable';
|
||
div.textContent = item.name || '—';
|
||
div.dataset.componentId = item.component_id || '';
|
||
div.dataset.componentName = item.name || '—';
|
||
listEl.appendChild(div);
|
||
});
|
||
})
|
||
.catch(function(err) {
|
||
if (errorEl) { errorEl.textContent = 'Ошибка загрузки данных'; errorEl.style.display = 'block'; }
|
||
});
|
||
}
|
||
|
||
var componentsPanelChartInstance = null;
|
||
|
||
function openComponentsChartView(componentId, componentName, listItem) {
|
||
var chartArea = document.getElementById('componentsChartArea');
|
||
var titleEl = document.getElementById('componentsChartAreaTitle');
|
||
var chartFrom = document.getElementById('chartDateFrom');
|
||
var chartTo = document.getElementById('chartDateTo');
|
||
var mainFrom = document.getElementById('consumptionDateFrom');
|
||
var mainTo = document.getElementById('consumptionDateTo');
|
||
if (!chartArea) return;
|
||
chartArea.dataset.chartComponentId = componentId || '';
|
||
if (titleEl) titleEl.textContent = componentName || 'График остатка';
|
||
chartArea.classList.add('open');
|
||
var to = new Date();
|
||
var from = new Date(to);
|
||
from.setDate(from.getDate() - 30);
|
||
if (mainFrom && mainTo && mainFrom.value && mainTo.value) {
|
||
if (chartFrom) chartFrom.value = mainFrom.value;
|
||
if (chartTo) chartTo.value = mainTo.value;
|
||
} else {
|
||
if (chartFrom) chartFrom.value = from.toISOString().slice(0, 10);
|
||
if (chartTo) chartTo.value = to.toISOString().slice(0, 10);
|
||
}
|
||
var list = document.getElementById('componentsPanelList');
|
||
if (list) {
|
||
list.querySelectorAll('.components-panel-item--selected').forEach(function(el) { el.classList.remove('components-panel-item--selected'); });
|
||
if (listItem) listItem.classList.add('components-panel-item--selected');
|
||
}
|
||
loadComponentsChart();
|
||
}
|
||
|
||
function closeComponentsChartView() {
|
||
var chartArea = document.getElementById('componentsChartArea');
|
||
if (!chartArea) return;
|
||
chartArea.classList.remove('open');
|
||
chartArea.removeAttribute('data-chart-component-id');
|
||
var list = document.getElementById('componentsPanelList');
|
||
if (list) list.querySelectorAll('.components-panel-item--selected').forEach(function(el) { el.classList.remove('components-panel-item--selected'); });
|
||
if (componentsPanelChartInstance) {
|
||
componentsPanelChartInstance.destroy();
|
||
componentsPanelChartInstance = null;
|
||
}
|
||
}
|
||
|
||
function loadComponentsChart() {
|
||
var chartArea = document.getElementById('componentsChartArea');
|
||
var componentId = chartArea && chartArea.dataset.chartComponentId;
|
||
var chartFrom = document.getElementById('chartDateFrom');
|
||
var chartTo = document.getElementById('chartDateTo');
|
||
var canvas = document.getElementById('componentsPanelChartCanvas');
|
||
var emptyEl = document.getElementById('componentsPanelChartEmpty');
|
||
if (!componentId || !canvas) return;
|
||
var dateFrom = chartFrom && chartFrom.value ? chartFrom.value : '';
|
||
var dateTo = chartTo && chartTo.value ? chartTo.value : '';
|
||
var url = '/api/sklad/' + encodeURIComponent(componentId) + '/chart';
|
||
if (dateFrom && dateTo) url += '?date_from=' + encodeURIComponent(dateFrom) + '&date_to=' + encodeURIComponent(dateTo);
|
||
if (emptyEl) emptyEl.style.display = 'none';
|
||
fetch(url).then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (componentsPanelChartInstance) { componentsPanelChartInstance.destroy(); componentsPanelChartInstance = null; }
|
||
if (data.error) {
|
||
if (emptyEl) { emptyEl.textContent = data.error; emptyEl.style.display = 'block'; }
|
||
return;
|
||
}
|
||
var points = data.points || [];
|
||
if (!points || points.length === 0) {
|
||
if (emptyEl) { emptyEl.textContent = 'Нет данных за выбранный период. Добавьте отчёты загрузки с расходом этого компонента.'; emptyEl.style.display = 'block'; }
|
||
return;
|
||
}
|
||
var labels = points.map(function(p) { return p.date; });
|
||
var values = points.map(function(p) { return p.remaining_kg; });
|
||
if (componentsPanelChartInstance) componentsPanelChartInstance.destroy();
|
||
var ctx = canvas.getContext('2d');
|
||
var isDark = document.documentElement.dataset.theme === 'dark';
|
||
componentsPanelChartInstance = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [{
|
||
label: 'Остаток (кг)',
|
||
data: values,
|
||
borderColor: isDark ? 'rgb(33, 150, 243)' : 'rgb(33, 150, 243)',
|
||
backgroundColor: isDark ? 'rgba(var(--primary-rgb), 0.1)' : 'rgba(var(--primary-rgb), 0.1)',
|
||
fill: true,
|
||
tension: 0.2
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: {
|
||
x: {
|
||
title: { display: true, text: 'Дата' },
|
||
ticks: { maxRotation: 45, minRotation: 0 }
|
||
},
|
||
y: {
|
||
title: { display: true, text: 'Остаток (кг)' },
|
||
beginAtZero: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
})
|
||
.catch(function(err) {
|
||
if (componentsPanelChartInstance) { componentsPanelChartInstance.destroy(); componentsPanelChartInstance = null; }
|
||
if (emptyEl) { emptyEl.textContent = 'Ошибка загрузки графика. Проверьте подключение.'; emptyEl.style.display = 'block'; }
|
||
});
|
||
}
|
||
|
||
</script>
|
||
|
||
<!-- Настройки: сверху карточка пользователя, снизу выбор темы (общий стиль на всех страницах) -->
|
||
<div id="settingsModal" class="settings-modal" style="display: none;">
|
||
<div class="settings-modal-content">
|
||
<div class="settings-modal-header">
|
||
<h3><i class="fas fa-cog"></i> Настройки</h3>
|
||
<span class="settings-close" data-action="close-settings">×</span>
|
||
</div>
|
||
<div class="settings-modal-body">
|
||
<div class="settings-user-card settings-user-card--clickable" data-action="toggle-credentials-form">
|
||
<h4 class="settings-card-title"><i class="fas fa-user"></i> Сменить логин/пароль <i class="fas fa-chevron-down settings-chevron"></i></h4>
|
||
</div>
|
||
<div class="settings-credentials-form" id="credentialsForm" style="display: none;">
|
||
<form id="settingsForm">
|
||
<div class="form-group">
|
||
<label for="oldLogin">Текущий логин:</label>
|
||
<input type="text" id="oldLogin" class="form-control" readonly>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="oldPassword">Текущий пароль:</label>
|
||
<input type="password" id="oldPassword" class="form-control">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="newLogin">Новый логин:</label>
|
||
<input type="text" id="newLogin" class="form-control" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="newPassword">Новый пароль:</label>
|
||
<input type="password" id="newPassword" class="form-control" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="confirmPassword">Подтвердите новый пароль:</label>
|
||
<input type="password" id="confirmPassword" class="form-control" required>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="settings-sync-card settings-sync-card--clickable" data-action="toggle-sync-form">
|
||
<h4 class="settings-card-title"><i class="fas fa-sync-alt"></i> Настройка синхронизации <i class="fas fa-chevron-down settings-chevron"></i></h4>
|
||
</div>
|
||
<div class="settings-sync-form" id="syncSettingsForm" style="display: none;">
|
||
<div id="syncClientsListSettings"></div>
|
||
<div id="syncClientsEmptySettings" class="text-muted small" style="display: none;">Нет подключенных клиентов</div>
|
||
</div>
|
||
<div class="settings-feed-quality-card settings-feed-quality-card--clickable" data-action="open-feed-quality-settings">
|
||
<h4 class="settings-card-title"><i class="fas fa-triangle-exclamation"></i> Контроль отклонений <i class="fas fa-chevron-right settings-chevron"></i></h4>
|
||
</div>
|
||
<div class="settings-theme-card">
|
||
<h4 class="settings-card-title"><i class="fas fa-palette"></i> Тема оформления</h4>
|
||
<div id="wespThemePicker" class="theme-picker-mount" role="radiogroup" aria-label="Тема оформления"></div>
|
||
<div class="settings-contrast-row">
|
||
<span class="settings-contrast-row__label">Усиленный контраст</span>
|
||
<label class="theme-switch" for="wespHighContrastToggle">
|
||
<input type="checkbox" id="wespHighContrastToggle" role="switch" aria-label="Усиленный контраст">
|
||
<span class="theme-slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="text-muted small mt-2">Версия системы 2.1</div>
|
||
</div>
|
||
<div class="settings-modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-action="close-settings">Отмена</button>
|
||
<button type="button" class="btn btn-primary" data-action="change-credentials">
|
||
<i class="fas fa-save"></i> Сохранить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module">
|
||
import { createRecipesAuthSettingsController } from '/static/js/pages/recipes-auth-settings.js';
|
||
import { initFeedQualitySettingsModal } from '/static/js/pages/feed-quality-settings-modal.js?v=4';
|
||
import { SyncPanel } from '/static/js/modules/recipes/sync-panel.js';
|
||
|
||
const notyf = window.WespZootechNotify.createAdapter();
|
||
|
||
const authSettingsController = createRecipesAuthSettingsController({ notyf });
|
||
const syncPanelModule = new SyncPanel({
|
||
openSettings: (event) => authSettingsController.openSettings(event),
|
||
closeSettings: () => authSettingsController.closeSettings(),
|
||
toggleCredentialsForm: () => authSettingsController.toggleCredentialsForm(),
|
||
toggleSyncForm: () => authSettingsController.toggleSyncForm(),
|
||
changeCredentials: () => authSettingsController.changeCredentials(),
|
||
editSyncClientSettings: (btn) => authSettingsController.editSyncClientSettings(btn),
|
||
deleteSyncClientSettings: (btn) => authSettingsController.deleteSyncClientSettings(btn),
|
||
saveSyncClientDisplayName: (nodeId) => authSettingsController.saveSyncClientDisplayName(nodeId),
|
||
loadSyncClientsSettings: () => authSettingsController.loadSyncClientsSettings(),
|
||
});
|
||
|
||
window.openSettings = (event) => authSettingsController.openSettings(event);
|
||
window.closeSettings = () => authSettingsController.closeSettings();
|
||
window.logout = () => authSettingsController.logout();
|
||
|
||
document.addEventListener('click', async (event) => {
|
||
const actionEl = event.target.closest('[data-action]');
|
||
if (!actionEl) return;
|
||
const action = actionEl.dataset.action;
|
||
if (await syncPanelModule.handleAction(action, event, actionEl)) return;
|
||
if (action === 'logout') {
|
||
event.preventDefault();
|
||
authSettingsController.logout();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initFeedQualitySettingsModal(notyf);
|
||
authSettingsController.checkAuth();
|
||
authSettingsController.setupMobileMenu();
|
||
});
|
||
</script>
|
||
|
||
<!-- Правая закладка «Выбранные компоненты»: слева от панели — график, в панели — список -->
|
||
<button type="button" class="sync-panel-tab" id="componentsPanelTab" title="Выбранные компоненты" aria-label="Открыть панель выбранных компонентов">
|
||
<span></span><span></span><span></span>
|
||
</button>
|
||
<div class="sync-panel-overlay" id="componentsPanelOverlay"></div>
|
||
<div class="components-panel-wrap" id="componentsPanelWrap" style="display: none;">
|
||
<div class="sync-panel" id="componentsPanel">
|
||
<div class="sync-panel-header">
|
||
<h3 id="componentsPanelTitle">Выбранные компоненты</h3>
|
||
<button type="button" class="sync-panel-close" id="componentsPanelClose" aria-label="Закрыть">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
<div class="sync-panel-body">
|
||
<div id="componentsPanelList"></div>
|
||
<div id="componentsPanelEmpty" class="components-panel-empty" style="display: none;">Нет компонентов на складе</div>
|
||
<div id="componentsPanelError" class="components-panel-empty" style="display: none;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="components-chart-area" id="componentsChartArea">
|
||
<div class="components-chart-area-header">
|
||
<h3 id="componentsChartAreaTitle">График остатка</h3>
|
||
<button type="button" class="components-chart-area-close" id="componentsChartAreaClose" aria-label="Закрыть график"><i class="fas fa-times"></i></button>
|
||
</div>
|
||
<div class="components-chart-area-body">
|
||
<div class="components-panel-chart-filters">
|
||
<label for="chartDateFrom">С:</label>
|
||
<input type="date" id="chartDateFrom" aria-label="Дата начала">
|
||
<label for="chartDateTo">По:</label>
|
||
<input type="date" id="chartDateTo" aria-label="Дата окончания">
|
||
<button type="button" class="btn btn-sm btn-primary" id="componentsPanelChartBuild"><i class="fas fa-chart-line"></i> Построить</button>
|
||
</div>
|
||
<div class="components-panel-chart-canvas-wrap">
|
||
<canvas id="componentsPanelChartCanvas"></canvas>
|
||
</div>
|
||
<div id="componentsPanelChartEmpty" class="components-panel-empty" style="display: none;">Нет данных за период</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer class="app-footer" data-wesp-enter>
|
||
<span class="text-muted small">Комптон® является зарегистрированным товарным знаком</span>
|
||
</footer>
|
||
<link href="/static/css/wesp-update-banner.css" rel="stylesheet">
|
||
<script src="/static/js/wesp-update-notifier.js"></script>
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
if (window.WespUpdateNotifier && window.WespDialog) {
|
||
WespUpdateNotifier.init({ dialog: window.WespDialog });
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|