1630 lines
61 KiB
HTML
1630 lines
61 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<script>
|
||
(function () {
|
||
try {
|
||
var t = (localStorage.getItem("theme") || "").trim().toLowerCase();
|
||
document.documentElement.setAttribute("data-theme", t === "light" ? "light" : "dark");
|
||
} catch (e) {}
|
||
})();
|
||
</script>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>Выгрузка кормосмеси</title>
|
||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="/static/css/font-awesome.min.css" rel="stylesheet">
|
||
<link href="/static/css/inter-fonts.css" rel="stylesheet">
|
||
<link href="/static/css/wesp-recipes-skeleton.css" rel="stylesheet">
|
||
<link href="/static/css/wesp-kiosk-skeleton.css" rel="stylesheet">
|
||
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||
<style>
|
||
:root {
|
||
--primary: #2563eb;
|
||
--primary-light: #3b82f6;
|
||
--success: #10b981;
|
||
--danger: #ef4444;
|
||
--warning: #f59e0b;
|
||
--gray-50: #f9fafb;
|
||
--gray-100: #f3f4f6;
|
||
--gray-200: #e5e7eb;
|
||
--gray-300: #d1d5db;
|
||
--gray-600: #4b5563;
|
||
--gray-700: #374151;
|
||
--gray-900: #111827;
|
||
--white: #ffffff;
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
font-family: 'Inter', sans-serif;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background: var(--gray-50);
|
||
color: var(--gray-900);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Кнопка главная */
|
||
.home-btn {
|
||
position: fixed;
|
||
top: 15px;
|
||
left: 15px;
|
||
padding: 10px 16px;
|
||
background: var(--primary);
|
||
color: var(--white);
|
||
text-decoration: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
transition: all 0.2s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
z-index: 1000;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.home-btn:hover {
|
||
background: var(--primary-light);
|
||
color: var(--white);
|
||
text-decoration: none;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 14px 16px 16px;
|
||
gap: 12px;
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
height: 100vh;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* Адаптивная компоновка */
|
||
.content-wrapper {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex: 1 1 0;
|
||
min-height: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.content-wrapper.no-groups {
|
||
justify-content: center;
|
||
}
|
||
|
||
.weight-section {
|
||
flex: 1 1 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
gap: 12px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.content-wrapper.no-groups .weight-section {
|
||
max-width: 720px;
|
||
width: 100%;
|
||
}
|
||
|
||
.groups-section {
|
||
flex: 0 0 min(36vw, 380px);
|
||
min-height: 0;
|
||
display: flex;
|
||
transition: all 0.3s ease;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.groups-section.hidden {
|
||
display: none;
|
||
}
|
||
|
||
/* Дисплей весов */
|
||
.weight-display {
|
||
position: relative;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 0;
|
||
padding: 0;
|
||
text-align: center;
|
||
box-shadow: none;
|
||
margin: 0;
|
||
flex: 1 1 0;
|
||
min-height: 0;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.weight-hero-meta {
|
||
position: relative;
|
||
z-index: 2;
|
||
width: 100%;
|
||
padding-bottom: 4px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.weight-label {
|
||
font-size: 18px;
|
||
color: var(--gray-700);
|
||
margin: 0 0 6px 0;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.remaining-label {
|
||
font-size: 13px;
|
||
color: var(--gray-600);
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
margin: 0;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.weight-value-area,
|
||
.mixer-weight-info {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
z-index: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.mixer-weight-info {
|
||
display: none;
|
||
}
|
||
|
||
.current-weight {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.12em;
|
||
font-size: 48px;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
margin: 0;
|
||
transition: color 0.3s ease;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
}
|
||
|
||
.current-weight.overloaded {
|
||
color: #dc3545;
|
||
animation: pulse-red 1s infinite;
|
||
}
|
||
|
||
@keyframes pulse-red {
|
||
0%, 100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
.weight-unit {
|
||
font-size: 0.32em;
|
||
color: var(--gray-600);
|
||
margin-left: 0;
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Общий прогресс */
|
||
.progress-section {
|
||
flex-shrink: 0;
|
||
background: var(--gray-50);
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 14px 16px;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.progress-label {
|
||
font-size: 14px;
|
||
color: var(--gray-900);
|
||
margin-bottom: 8px;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.progress-container {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: var(--gray-200);
|
||
border-radius: 5px;
|
||
overflow: hidden;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 100%;
|
||
background: var(--primary);
|
||
width: 0%;
|
||
transition: width 0.3s ease;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.progress-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
color: var(--gray-600);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.progress-info + .progress-info {
|
||
margin-top: 6px;
|
||
}
|
||
|
||
/* Группы выгрузки - упрощенные */
|
||
.unloading-groups {
|
||
background: var(--gray-50);
|
||
border: none;
|
||
border-radius: 8px;
|
||
box-shadow: none;
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.groups-header {
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid var(--gray-200);
|
||
background: transparent;
|
||
border-radius: 0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.groups-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--gray-900);
|
||
margin: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.groups-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
}
|
||
|
||
.groups-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.group-item {
|
||
background: var(--white);
|
||
border: 1px solid var(--gray-200);
|
||
border-radius: 8px;
|
||
padding: 12px 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
text-align: left;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
min-height: 56px;
|
||
}
|
||
|
||
.group-item:hover {
|
||
background: var(--gray-100);
|
||
}
|
||
|
||
.group-item.active {
|
||
background: var(--primary);
|
||
color: var(--white);
|
||
border-color: var(--primary);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||
}
|
||
|
||
.group-item.completed {
|
||
background: var(--success);
|
||
color: var(--white);
|
||
border-color: var(--success);
|
||
}
|
||
|
||
.group-name {
|
||
font-weight: 600;
|
||
font-size: 15px;
|
||
margin: 0;
|
||
}
|
||
|
||
.group-weight-info {
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
opacity: 0.95;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Стили кнопок */
|
||
.buttons-container {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-shrink: 0;
|
||
margin: 0;
|
||
padding: 4px 0 0;
|
||
}
|
||
|
||
.btn {
|
||
padding: 16px 20px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
min-height: 50px;
|
||
white-space: nowrap;
|
||
flex: 1;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
transform: none !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
.btn i {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--primary);
|
||
color: var(--white);
|
||
}
|
||
|
||
.btn-success {
|
||
background: var(--success);
|
||
color: var(--white);
|
||
}
|
||
|
||
.btn-danger {
|
||
background: var(--danger);
|
||
color: var(--white);
|
||
}
|
||
|
||
/* Оптимизация для планшетов в горизонтальном положении */
|
||
@media (max-width: 1024px) and (orientation: landscape) {
|
||
.main-content {
|
||
padding: 10px;
|
||
height: 100vh;
|
||
}
|
||
|
||
.content-wrapper {
|
||
gap: 10px;
|
||
}
|
||
|
||
.groups-section {
|
||
flex: 0 0 min(34vw, 320px);
|
||
}
|
||
|
||
.progress-section {
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.progress-label {
|
||
font-size: 14px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 12px 18px;
|
||
font-size: 14px;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.buttons-container {
|
||
margin-top: 0px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.groups-list {
|
||
gap: 6px;
|
||
padding: 6px;
|
||
}
|
||
|
||
.group-item {
|
||
padding: 10px 12px;
|
||
min-height: 52px;
|
||
}
|
||
|
||
.group-name {
|
||
font-size: 12px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.group-weight-info {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.group-nav-buttons .btn {
|
||
padding: 10px 15px;
|
||
font-size: 13px;
|
||
min-height: 42px;
|
||
}
|
||
}
|
||
|
||
/* Кнопки навигации между группами под группами выгрузки */
|
||
.group-nav-buttons {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 15px;
|
||
margin: 15px 0 10px 0;
|
||
padding: 15px;
|
||
border-top: 1px solid var(--gray-200);
|
||
background: var(--gray-50);
|
||
}
|
||
|
||
.group-nav-buttons .btn {
|
||
flex: 1;
|
||
max-width: 160px;
|
||
padding: 12px 18px;
|
||
font-size: 14px;
|
||
min-height: 46px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--gray-600);
|
||
color: var(--white);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: var(--gray-700);
|
||
}
|
||
|
||
.btn-secondary:disabled {
|
||
background: var(--gray-300);
|
||
color: var(--gray-500);
|
||
}
|
||
|
||
/* Для маленьких планшетов */
|
||
@media (max-width: 768px) {
|
||
.content-wrapper {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.weight-section,
|
||
.groups-section {
|
||
flex: 1 1 0;
|
||
width: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
.groups-section {
|
||
flex: 0 1 38%;
|
||
}
|
||
|
||
.groups-list {
|
||
gap: 6px;
|
||
padding: 6px;
|
||
}
|
||
|
||
.group-item {
|
||
padding: 10px 12px;
|
||
min-height: 48px;
|
||
}
|
||
|
||
.group-name {
|
||
font-size: 11px;
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.group-weight-info {
|
||
font-size: 10px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 16px;
|
||
font-size: 13px;
|
||
min-height: 42px;
|
||
}
|
||
|
||
.group-nav-buttons .btn {
|
||
padding: 8px 12px;
|
||
font-size: 11px;
|
||
min-height: 36px;
|
||
}
|
||
}
|
||
|
||
/* Анимации */
|
||
.fade-in {
|
||
animation: fadeIn 0.5s ease-out;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* Стили для уведомлений */
|
||
.toast-container {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
z-index: 9999;
|
||
}
|
||
|
||
.toast {
|
||
background: var(--white);
|
||
border-radius: 8px;
|
||
padding: 16px 24px;
|
||
margin-bottom: 10px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
transform: translateX(120%);
|
||
transition: transform 0.3s ease;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.toast.show {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.toast.success {
|
||
border-left: 4px solid var(--success);
|
||
}
|
||
|
||
.toast.error {
|
||
border-left: 4px solid var(--danger);
|
||
}
|
||
|
||
.toast-icon {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.toast.success .toast-icon {
|
||
color: var(--success);
|
||
}
|
||
|
||
.toast.error .toast-icon {
|
||
color: var(--danger);
|
||
}
|
||
|
||
.toast-message {
|
||
color: var(--gray-900);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* --- Светлая тема (явно, согласовано с kiosk-theme) --- */
|
||
html[data-theme="light"] body {
|
||
background: #f9fafb !important;
|
||
color: #111827 !important;
|
||
}
|
||
|
||
/* --- Тёмная тема: карточки и панели, не только фон body --- */
|
||
html[data-theme="dark"] body {
|
||
background: #1a1a1a !important;
|
||
color: #e6e6e6 !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .weight-label,
|
||
html[data-theme="dark"] .remaining-label {
|
||
color: #bdbdbd !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .current-weight {
|
||
color: #60a5fa !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .current-weight.overloaded {
|
||
color: #f87171 !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .weight-unit {
|
||
color: #9ca3af !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .progress-section {
|
||
background: #1f1f1f !important;
|
||
border: none !important;
|
||
color: #e6e6e6 !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .progress-label {
|
||
color: #f3f4f6 !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .progress-container {
|
||
background: #374151 !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .progress-info {
|
||
color: #9ca3af !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .progress-info strong {
|
||
color: #e5e7eb !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .unloading-groups {
|
||
background: #1f1f1f !important;
|
||
border: none !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .groups-header {
|
||
background: transparent !important;
|
||
border-bottom-color: #3a3a3a !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .groups-title {
|
||
color: #f3f4f6 !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .group-item {
|
||
background: #242424 !important;
|
||
border-color: #3a3a3a !important;
|
||
color: #e6e6e6 !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .group-item:hover {
|
||
background: #374151 !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .group-item.active {
|
||
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.45) !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .group-nav-buttons {
|
||
background: #1f1f1f !important;
|
||
border-top-color: #3a3a3a !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .toast {
|
||
background: #2d2d2d !important;
|
||
color: #f3f4f6 !important;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5) !important;
|
||
}
|
||
|
||
html[data-theme="dark"] .toast-message {
|
||
color: #e5e7eb !important;
|
||
}
|
||
|
||
#wespThemeToggleWrap {
|
||
display: none !important;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Удалена кнопка Главная -->
|
||
|
||
<!-- Основной контент -->
|
||
<div class="main-content">
|
||
<div class="content-wrapper" id="contentWrapper">
|
||
<!-- Секция весов -->
|
||
<div class="weight-section">
|
||
<!-- Дисплей основных весов -->
|
||
<div class="weight-display fade-in">
|
||
<div class="weight-hero-meta">
|
||
<div class="weight-label">
|
||
<i class="fas fa-layer-group" id="weightIcon"></i>
|
||
<span id="activeGroupName">Выберите группу</span>
|
||
</div>
|
||
<div class="remaining-label" id="remainingLabel">Осталось по группе</div>
|
||
</div>
|
||
<div class="weight-value-area" id="groupInfo">
|
||
<div class="current-weight" id="groupRemainingWeight">
|
||
0<span class="weight-unit"> кг</span>
|
||
</div>
|
||
</div>
|
||
<div class="mixer-weight-info" id="mixerWeightInfo">
|
||
<div class="current-weight" id="mixerWeight">
|
||
0<span class="weight-unit"> кг</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Общий прогресс -->
|
||
<div class="progress-section fade-in">
|
||
<div class="progress-label">
|
||
<i class="fas fa-chart-line"></i>
|
||
Прогресс выгрузки
|
||
</div>
|
||
<div class="progress-container">
|
||
<div class="progress-bar" id="progressBar"></div>
|
||
</div>
|
||
<div class="progress-info">
|
||
<span>Выгружено: <strong id="unloadedWeight">0</strong> кг</span>
|
||
<span>В смесителе: <strong id="mixerCurrentWeight">0</strong> кг</span>
|
||
</div>
|
||
<div class="progress-info">
|
||
<span>Общий вес: <strong id="totalWeight">0</strong> кг</span>
|
||
<span></span>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Секция групп выгрузки - упрощенная -->
|
||
<div class="groups-section hidden" id="groupsSection">
|
||
<div class="unloading-groups" id="unloadingGroups">
|
||
<div class="groups-header">
|
||
<div class="groups-title">
|
||
<i class=" "></i>
|
||
Группы выгрузки
|
||
</div>
|
||
</div>
|
||
<div class="groups-content">
|
||
<div class="groups-list" id="groupsList">
|
||
<!-- Группы будут добавлены динамически -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Навигация между группами под группами выгрузки -->
|
||
<div class="group-nav-buttons" id="groupNavButtons" style="display:none;">
|
||
<button class="btn btn-primary" id="prevGroupBtn">
|
||
<i class="fas fa-chevron-left"></i>
|
||
Предыдущая
|
||
</button>
|
||
<button class="btn btn-primary" id="nextGroupBtn">
|
||
Следующая
|
||
<i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Кнопки управления -->
|
||
<div class="buttons-container">
|
||
<button class="btn btn-primary" id="finishBtn" disabled>
|
||
<i class="fas fa-check"></i>
|
||
Завершить выгрузку
|
||
</button>
|
||
<button class="btn btn-danger" id="forceFinishBtn">
|
||
<i class="fas fa-exclamation-triangle"></i>
|
||
Завершить досрочно
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Контейнер для уведомлений -->
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
|
||
<script src="/static/js/kiosk-dialog.js"></script>
|
||
<script type="module" src="/static/js/modules/kiosk/kiosk-skeleton.js"></script>
|
||
<script>
|
||
let totalWeight = 0;
|
||
let currentWeight = 0;
|
||
let initialTotalWeight = 0;
|
||
let isUnloadingComplete = false;
|
||
let recipeId = null;
|
||
let startTime = null;
|
||
let mixingStartTime = null;
|
||
let components = [];
|
||
let unloadingGroups = [];
|
||
let activeGroupIndex = -1;
|
||
let weightAtGroupStart = 0;
|
||
let scaleInitialized = false;
|
||
let isSaving = false;
|
||
let kioskPaired = false;
|
||
let unloadingInitialized = false;
|
||
const DEVICE_TOKEN_KEY = 'wespDeviceToken';
|
||
|
||
function getDeviceToken() {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const tokenFromQuery = (params.get('device_token') || '').trim();
|
||
if (tokenFromQuery) {
|
||
localStorage.setItem(DEVICE_TOKEN_KEY, tokenFromQuery);
|
||
return tokenFromQuery;
|
||
}
|
||
return (localStorage.getItem(DEVICE_TOKEN_KEY) || '').trim();
|
||
}
|
||
|
||
function buildHeaders(extra = {}) {
|
||
const headers = { ...extra };
|
||
const token = getDeviceToken();
|
||
if (token) headers['X-Device-Token'] = token;
|
||
return headers;
|
||
}
|
||
|
||
function todayIso() {
|
||
return new Date().toISOString().slice(0, 10);
|
||
}
|
||
|
||
function buildKioskHeaders(extra = {}) {
|
||
return buildHeaders({
|
||
Accept: 'application/json',
|
||
'X-Wesp-Kiosk': '1',
|
||
...extra,
|
||
});
|
||
}
|
||
|
||
function applyPlanTotalWeight(planTotal) {
|
||
const value = parseFloat(planTotal);
|
||
if (!Number.isFinite(value) || value < 0) return;
|
||
totalWeight = value;
|
||
initialTotalWeight = value;
|
||
const totalEl = document.getElementById('totalWeight');
|
||
if (totalEl) totalEl.textContent = String(Math.round(value));
|
||
}
|
||
|
||
const UNLOADING_WEIGHT_FIT_BASE_PX = 240;
|
||
|
||
function getActiveWeightDisplayNodes() {
|
||
const groupInfo = document.getElementById('groupInfo');
|
||
const mixerInfo = document.getElementById('mixerWeightInfo');
|
||
if (groupInfo && groupInfo.style.display !== 'none') {
|
||
return {
|
||
el: document.getElementById('groupRemainingWeight'),
|
||
area: groupInfo,
|
||
};
|
||
}
|
||
if (mixerInfo && mixerInfo.style.display !== 'none') {
|
||
return {
|
||
el: document.getElementById('mixerWeight'),
|
||
area: mixerInfo,
|
||
};
|
||
}
|
||
return { el: null, area: null };
|
||
}
|
||
|
||
function fitUnloadingWeightDisplay() {
|
||
const { el, area } = getActiveWeightDisplayNodes();
|
||
if (!el || !area || area.clientWidth <= 0 || area.clientHeight <= 0) return;
|
||
|
||
const maxWidth = Math.max(48, area.clientWidth);
|
||
const maxHeight = Math.max(48, area.clientHeight);
|
||
const minSize = 18;
|
||
const maxSize = UNLOADING_WEIGHT_FIT_BASE_PX;
|
||
|
||
el.style.left = '50%';
|
||
el.style.top = '50%';
|
||
el.style.transform = 'translate(-50%, -50%)';
|
||
el.style.whiteSpace = 'nowrap';
|
||
const unit = el.querySelector('.weight-unit');
|
||
if (unit) unit.style.fontSize = '';
|
||
|
||
let lo = minSize;
|
||
let hi = maxSize;
|
||
let best = minSize;
|
||
while (lo <= hi) {
|
||
const mid = Math.floor((lo + hi) / 2);
|
||
el.style.fontSize = `${mid}px`;
|
||
if (el.scrollWidth <= maxWidth && el.scrollHeight <= maxHeight) {
|
||
best = mid;
|
||
lo = mid + 1;
|
||
} else {
|
||
hi = mid - 1;
|
||
}
|
||
}
|
||
el.style.fontSize = `${best}px`;
|
||
el.style.transform = 'translate(-50%, -50%)';
|
||
}
|
||
|
||
function resolveGroupTargetWeight(group, recipe, mixerTotal) {
|
||
const planWeight = parseFloat(group.weight);
|
||
if (Number.isFinite(planWeight) && planWeight >= 0) {
|
||
return planWeight;
|
||
}
|
||
const headsPerTrip = recipe.headsPerTrip || recipe.heads_per_trip || 1;
|
||
if (group.distributionType === 'heads') {
|
||
const totalHeads = group.value || 0;
|
||
return headsPerTrip > 0 ? (mixerTotal / headsPerTrip) * totalHeads : 0;
|
||
}
|
||
const percent = group.value || 0;
|
||
return (mixerTotal * percent) / 100;
|
||
}
|
||
|
||
function withDeviceToken(url) {
|
||
const token = getDeviceToken();
|
||
if (!token) return url;
|
||
const u = new URL(url, window.location.origin);
|
||
u.searchParams.set('device_token', token);
|
||
return `${u.pathname}${u.search}`;
|
||
}
|
||
|
||
async function initUnloadingPage() {
|
||
if (unloadingInitialized || !kioskPaired) return;
|
||
unloadingInitialized = true;
|
||
// Получаем параметры из URL
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
recipeId = urlParams.get('recipe_id');
|
||
totalWeight = parseFloat(urlParams.get('total_weight')) || 0;
|
||
|
||
// Проверяем наличие времени смешивания
|
||
const savedMixingTime = localStorage.getItem('targetMixingTime');
|
||
console.log('Initial mixing time check:', {
|
||
savedMixingTime,
|
||
parsed: parseInt(savedMixingTime)
|
||
});
|
||
|
||
if (!savedMixingTime || isNaN(parseInt(savedMixingTime))) {
|
||
console.error('Invalid or missing mixing time:', savedMixingTime);
|
||
void (async () => {
|
||
await WespKioskDialog.alert('Ошибка: некорректное или отсутствующее время смешивания', { variant: 'danger' });
|
||
window.location.href = '/recipes_content';
|
||
})();
|
||
return;
|
||
}
|
||
|
||
mixingStartTime = new Date();
|
||
console.log('Mixing start time set:', mixingStartTime.toISOString());
|
||
|
||
// Загружаем сохраненные данные о компонентах
|
||
const savedComponentData = localStorage.getItem('componentData');
|
||
console.log('Saved component data:', savedComponentData);
|
||
|
||
if (savedComponentData) {
|
||
components = JSON.parse(savedComponentData);
|
||
console.log('Parsed components:', components);
|
||
} else {
|
||
console.log('No saved component data found');
|
||
}
|
||
|
||
// Веса рейса и групп — из плана на день (с запасным вариантом из URL)
|
||
await loadRecipeInfo();
|
||
applyPlanTotalWeight(totalWeight);
|
||
startTime = new Date();
|
||
fitUnloadingWeightDisplay();
|
||
|
||
let unloadWeightFitDebounce;
|
||
const observeWeightFit = (node) => {
|
||
if (!node || typeof ResizeObserver === 'undefined') return;
|
||
const observer = new ResizeObserver(() => {
|
||
clearTimeout(unloadWeightFitDebounce);
|
||
unloadWeightFitDebounce = setTimeout(fitUnloadingWeightDisplay, 60);
|
||
});
|
||
observer.observe(node);
|
||
};
|
||
observeWeightFit(document.querySelector('.weight-display'));
|
||
|
||
let resizeLayoutDebounce;
|
||
window.addEventListener('resize', () => {
|
||
clearTimeout(resizeLayoutDebounce);
|
||
resizeLayoutDebounce = setTimeout(fitUnloadingWeightDisplay, 120);
|
||
});
|
||
|
||
// Подключаем SSE для получения веса
|
||
const eventSource = new EventSource(withDeviceToken('/stream_weight'));
|
||
eventSource.onmessage = e => {
|
||
const payload = JSON.parse(e.data || '{}');
|
||
currentWeight = parseFloat(payload.weight) || 0;
|
||
if (!scaleInitialized) {
|
||
scaleInitialized = true;
|
||
if (activeGroupIndex >= 0) {
|
||
weightAtGroupStart = currentWeight;
|
||
}
|
||
}
|
||
updateDisplay();
|
||
};
|
||
|
||
// Обработчик кнопки принудительного завершения
|
||
document.getElementById('forceFinishBtn').addEventListener('click', function() {
|
||
if (!isSaving) {
|
||
saveReport();
|
||
}
|
||
});
|
||
|
||
// Обработчик обычной кнопки завершения
|
||
document.getElementById('finishBtn').addEventListener('click', function() {
|
||
if (!isSaving) {
|
||
saveReport();
|
||
}
|
||
});
|
||
|
||
// Обработчики кнопок навигации групп
|
||
const prevBtn = document.getElementById('prevGroupBtn');
|
||
const nextBtn = document.getElementById('nextGroupBtn');
|
||
|
||
prevBtn.addEventListener('click', () => {
|
||
if (activeGroupIndex > 0) {
|
||
selectGroup(activeGroupIndex - 1);
|
||
}
|
||
});
|
||
|
||
nextBtn.addEventListener('click', () => {
|
||
if (activeGroupIndex < unloadingGroups.length - 1) {
|
||
selectGroup(activeGroupIndex + 1);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Обновление компоновки в зависимости от наличия групп
|
||
function updateLayout() {
|
||
const contentWrapper = document.getElementById('contentWrapper');
|
||
const groupsSection = document.getElementById('groupsSection');
|
||
const activeGroupName = document.getElementById('activeGroupName');
|
||
const weightIcon = document.getElementById('weightIcon');
|
||
const groupInfo = document.getElementById('groupInfo');
|
||
const mixerWeightInfo = document.getElementById('mixerWeightInfo');
|
||
|
||
if (unloadingGroups.length === 0) {
|
||
// Нет групп - показываем вес в кормосмесителе
|
||
contentWrapper.classList.add('no-groups');
|
||
groupsSection.classList.add('hidden');
|
||
|
||
// Меняем заголовок и иконку
|
||
activeGroupName.textContent = 'Вес в кормосмесителе';
|
||
weightIcon.className = 'fas fa-weight-hanging';
|
||
|
||
// Скрываем информацию о группах и показываем общий вес
|
||
groupInfo.style.display = 'none';
|
||
mixerWeightInfo.style.display = 'block';
|
||
} else {
|
||
// Есть группы - показываем обычный интерфейс
|
||
contentWrapper.classList.remove('no-groups');
|
||
groupsSection.classList.remove('hidden');
|
||
|
||
// Возвращаем обычные элементы
|
||
weightIcon.className = 'fas fa-layer-group';
|
||
groupInfo.style.display = 'block';
|
||
mixerWeightInfo.style.display = 'none';
|
||
}
|
||
fitUnloadingWeightDisplay();
|
||
}
|
||
|
||
// Функция загрузки информации о рецепте
|
||
async function loadRecipeInfo() {
|
||
if (!recipeId) return;
|
||
|
||
const groupsList = document.getElementById('groupsList');
|
||
const groupsSection = document.getElementById('groupsSection');
|
||
let groupsStartedAt = null;
|
||
if (groupsList && window.WespKioskSkeleton) {
|
||
groupsSection?.classList.remove('hidden');
|
||
groupsStartedAt = window.WespKioskSkeleton.mountKioskSkeleton(
|
||
groupsList,
|
||
window.WespKioskSkeleton.buildUnloadingGroupsSkeletonHtml(3)
|
||
);
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`/api/recipes/${encodeURIComponent(recipeId)}?date=${encodeURIComponent(todayIso())}`,
|
||
{ headers: buildKioskHeaders() },
|
||
);
|
||
if (!response.ok) throw new Error('Ошибка загрузки рецепта');
|
||
|
||
const recipe = await response.json();
|
||
const planTotal = recipe.total_weight ?? recipe.totalWeightKg;
|
||
if (planTotal != null) {
|
||
applyPlanTotalWeight(planTotal);
|
||
}
|
||
|
||
const groups = recipe.unloadingGroups || recipe.unloading_groups || [];
|
||
if (groups.length > 0) {
|
||
unloadingGroups = groups.slice().sort((a, b) => (a.order || 0) - (b.order || 0));
|
||
|
||
// Инициализируем группы весами из плана на день
|
||
unloadingGroups.forEach(group => {
|
||
group.targetWeight = resolveGroupTargetWeight(group, recipe, totalWeight);
|
||
group.unloadedWeight = 0;
|
||
group.remainingWeight = group.targetWeight;
|
||
group.originalWeight = group.weight || group.targetWeight;
|
||
group.weight = group.targetWeight;
|
||
});
|
||
|
||
await displayGroups(groupsStartedAt);
|
||
updateLayout();
|
||
selectGroup(0);
|
||
|
||
// Показываем кнопки навигации если групп больше одной
|
||
if (unloadingGroups.length > 1) {
|
||
const navButtons = document.getElementById('groupNavButtons');
|
||
if (navButtons) {
|
||
navButtons.style.display = 'flex';
|
||
updateNavigationButtons();
|
||
}
|
||
}
|
||
} else {
|
||
if (groupsList && window.WespKioskSkeleton) {
|
||
window.WespKioskSkeleton.clearStaleKioskSkeleton(groupsList);
|
||
}
|
||
updateLayout();
|
||
}
|
||
|
||
fitUnloadingWeightDisplay();
|
||
|
||
// Проверяем тип оборудования - если кормоцех и нет групп выгрузки, автоматически формируем отчет
|
||
const equipmentType = localStorage.getItem('equipmentType') || 'dispenser';
|
||
if ((equipmentType === 'mill' || equipmentType === 'кормоцех') && unloadingGroups.length === 0) {
|
||
console.log('Обнаружен кормоцех без групп выгрузки, автоматически формируем отчет без выгрузки');
|
||
// Скрываем интерфейс выгрузки
|
||
const mainContent = document.querySelector('.main-content');
|
||
if (mainContent) {
|
||
mainContent.style.display = 'none';
|
||
}
|
||
|
||
// Создаем и показываем сообщение о формировании отчета
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.style.cssText = 'display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; gap: 20px; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--gray-50); z-index: 10000;';
|
||
messageDiv.innerHTML = `
|
||
<div style="font-size: 24px; font-weight: 600; color: var(--gray-900);">Формирование отчета...</div>
|
||
<div class="spinner" style="border: 4px solid #f3f3f3; border-top: 4px solid #2563eb; border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite;"></div>
|
||
`;
|
||
document.body.appendChild(messageDiv);
|
||
|
||
// Добавляем стили для анимации
|
||
if (!document.getElementById('spinner-styles')) {
|
||
const style = document.createElement('style');
|
||
style.id = 'spinner-styles';
|
||
style.textContent = `
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
// Автоматически формируем отчет после небольшой задержки (чтобы данные успели загрузиться)
|
||
setTimeout(() => {
|
||
saveReport();
|
||
}, 500);
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки информации о рецепте:', error);
|
||
if (groupsList && window.WespKioskSkeleton) {
|
||
window.WespKioskSkeleton.clearStaleKioskSkeleton(groupsList);
|
||
}
|
||
updateLayout();
|
||
}
|
||
}
|
||
|
||
function buildGroupsListHtml() {
|
||
return unloadingGroups.map((group, index) => {
|
||
if (!group) return '';
|
||
const unloadedWeight = group.unloadedWeight || 0;
|
||
const targetWeight = group.targetWeight || 0;
|
||
return `
|
||
<div class="group-item" id="group-${index}" onclick="selectGroup(${index})">
|
||
<div class="group-name">${group.name || 'Без названия'}</div>
|
||
<div class="group-weight-info">
|
||
<span class="group-weight">${Math.round(unloadedWeight)} / ${Math.round(targetWeight)} кг</span>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// Упрощенное отображение групп - только название и прогресс
|
||
async function displayGroups(startedAt) {
|
||
const groupsList = document.getElementById('groupsList');
|
||
if (!groupsList) return;
|
||
|
||
const html = buildGroupsListHtml();
|
||
if (startedAt != null && window.WespKioskSkeleton) {
|
||
await window.WespKioskSkeleton.replaceKioskContent(groupsList, html, { startedAt });
|
||
} else {
|
||
groupsList.innerHTML = html;
|
||
}
|
||
}
|
||
|
||
// Выбор группы для выгрузки
|
||
function selectGroup(index) {
|
||
if (index < 0 || index >= unloadingGroups.length) return;
|
||
|
||
// Сохраняем состояние предыдущей группы только при реальном переключении
|
||
if (activeGroupIndex >= 0 && activeGroupIndex !== index && unloadingGroups[activeGroupIndex]) {
|
||
const activeGroup = unloadingGroups[activeGroupIndex];
|
||
const unloadedFromGroup = Math.max(0, weightAtGroupStart - currentWeight);
|
||
activeGroup.unloadedWeight += unloadedFromGroup;
|
||
activeGroup.remainingWeight = Math.max(0, activeGroup.targetWeight - activeGroup.unloadedWeight);
|
||
}
|
||
|
||
activeGroupIndex = index;
|
||
weightAtGroupStart = currentWeight;
|
||
|
||
const activeGroup = unloadingGroups[activeGroupIndex];
|
||
if (!activeGroup) return;
|
||
|
||
const activeGroupNameElement = document.getElementById('activeGroupName');
|
||
if (activeGroupNameElement) {
|
||
activeGroupNameElement.textContent = activeGroup.name || 'Без названия';
|
||
}
|
||
|
||
// Прокручиваем к выбранной группе
|
||
scrollToActiveGroup();
|
||
|
||
updateDisplay();
|
||
updateNavigationButtons();
|
||
fitUnloadingWeightDisplay();
|
||
}
|
||
|
||
// Обновление отображения основного дисплея
|
||
function updateMainDisplay() {
|
||
if (activeGroupIndex < 0 || !unloadingGroups[activeGroupIndex]) return;
|
||
|
||
const activeGroup = unloadingGroups[activeGroupIndex];
|
||
const currentUnloading = Math.max(0, weightAtGroupStart - currentWeight);
|
||
const totalUnloaded = (activeGroup.unloadedWeight || 0) + currentUnloading;
|
||
const remaining = (activeGroup.targetWeight || 0) - totalUnloaded; // Убираем Math.max для перевыгрузки
|
||
|
||
const groupRemainingWeightElement = document.getElementById('groupRemainingWeight');
|
||
|
||
if (groupRemainingWeightElement) {
|
||
const remainingText = remaining.toFixed(0);
|
||
const isOverloaded = remaining < 0;
|
||
groupRemainingWeightElement.innerHTML =
|
||
`${remainingText}<span class="weight-unit"></span>`;
|
||
fitUnloadingWeightDisplay();
|
||
|
||
// Добавляем визуальное выделение для перевыгрузки
|
||
if (isOverloaded) {
|
||
groupRemainingWeightElement.classList.add('overloaded');
|
||
} else {
|
||
groupRemainingWeightElement.classList.remove('overloaded');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Обновление отображения групп
|
||
function updateGroupsDisplay() {
|
||
let cumulativeWeight = 0;
|
||
const unloadedWeight = totalWeight - currentWeight;
|
||
|
||
unloadingGroups.forEach((group, index) => {
|
||
const groupItem = document.getElementById(`group-${index}`);
|
||
if (!groupItem) return;
|
||
|
||
const groupWeightElement = groupItem.querySelector('.group-weight');
|
||
|
||
// Вычисляем текущий статус группы
|
||
let currentGroupUnloaded = group.unloadedWeight;
|
||
if (index === activeGroupIndex) {
|
||
const additionalUnloading = Math.max(0, weightAtGroupStart - currentWeight);
|
||
currentGroupUnloaded += additionalUnloading;
|
||
}
|
||
|
||
const remaining = group.targetWeight - currentGroupUnloaded; // Убираем Math.max для перевыгрузки
|
||
|
||
// Обновляем отображение
|
||
groupWeightElement.textContent = `${Math.round(currentGroupUnloaded)} / ${Math.round(group.targetWeight)} кг`;
|
||
|
||
// Обновляем классы
|
||
groupItem.className = 'group-item';
|
||
|
||
if (currentGroupUnloaded >= group.targetWeight) {
|
||
groupItem.classList.add('completed');
|
||
} else if (index === activeGroupIndex) {
|
||
groupItem.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Функция для показа уведомлений
|
||
function showToast(message, type = 'success') {
|
||
const container = document.getElementById('toastContainer');
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
|
||
const icon = type === 'success' ? '✓' : '✕';
|
||
|
||
toast.innerHTML = `
|
||
<div class="toast-icon">${icon}</div>
|
||
<div class="toast-message">${message}</div>
|
||
`;
|
||
|
||
container.appendChild(toast);
|
||
|
||
// Показываем уведомление
|
||
setTimeout(() => toast.classList.add('show'), 100);
|
||
|
||
// Удаляем уведомление через 3 секунды
|
||
setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
setTimeout(() => container.removeChild(toast), 300);
|
||
}, 3000);
|
||
}
|
||
|
||
// Выносим сохранение отчета в отдельную функцию
|
||
async function saveReport() {
|
||
if (!kioskPaired) return;
|
||
// Защита от повторного нажатия
|
||
if (isSaving) {
|
||
return;
|
||
}
|
||
isSaving = true;
|
||
|
||
// Отключаем кнопки
|
||
document.getElementById('finishBtn').disabled = true;
|
||
document.getElementById('forceFinishBtn').disabled = true;
|
||
|
||
// Меняем текст кнопок
|
||
const finishBtn = document.getElementById('finishBtn');
|
||
const forceFinishBtn = document.getElementById('forceFinishBtn');
|
||
const originalFinishText = finishBtn.innerHTML;
|
||
const originalForceText = forceFinishBtn.innerHTML;
|
||
|
||
finishBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Сохранение...';
|
||
forceFinishBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Сохранение...';
|
||
|
||
try {
|
||
if (!recipeId) {
|
||
throw new Error('Не указан ID рецепта');
|
||
}
|
||
|
||
// Получаем сохраненные данные
|
||
const savedComponentData = localStorage.getItem('componentData');
|
||
const savedLoadingTimes = localStorage.getItem('componentLoadingTimes');
|
||
const savedTargetMixingTime = localStorage.getItem('targetMixingTime');
|
||
const savedActualMixingTime = localStorage.getItem('actualMixingTime');
|
||
|
||
console.log('Saved mixing times:', {
|
||
target: savedTargetMixingTime,
|
||
actual: savedActualMixingTime
|
||
});
|
||
|
||
if (!savedTargetMixingTime || !savedActualMixingTime) {
|
||
throw new Error('Не найдены данные о времени смешивания');
|
||
}
|
||
|
||
const targetMixingTime = parseInt(savedTargetMixingTime);
|
||
const actualMixingTime = parseInt(savedActualMixingTime);
|
||
|
||
if (isNaN(targetMixingTime) || isNaN(actualMixingTime)) {
|
||
throw new Error('Некорректные данные о времени смешивания');
|
||
}
|
||
|
||
const equipmentType = localStorage.getItem('equipmentType') || 'dispenser';
|
||
const dispenserType = (equipmentType === 'mill' || equipmentType === 'кормоцех') ? 'mill' : 'dispenser';
|
||
// Формируем данные для отчета о загрузке
|
||
const loadingReportData = {
|
||
recipe_id: recipeId, // UUID строка, не число
|
||
total_weight: parseFloat(totalWeight) || 0,
|
||
target_mixing_time: targetMixingTime,
|
||
actual_mixing_time: actualMixingTime,
|
||
components: JSON.parse(savedComponentData || '[]'),
|
||
component_loading_times: JSON.parse(savedLoadingTimes || '[]'),
|
||
dispenser_type: dispenserType
|
||
};
|
||
|
||
// Сохраняем отчет о загрузке
|
||
const loadingResponse = await fetch('/api/save_report', {
|
||
method: 'POST',
|
||
headers: buildHeaders({ 'Content-Type': 'application/json' }),
|
||
body: JSON.stringify(loadingReportData)
|
||
});
|
||
|
||
if (!loadingResponse.ok) {
|
||
const loadingError = await loadingResponse.json();
|
||
throw new Error(`Ошибка сохранения отчета о загрузке: ${loadingError.message || 'неизвестная ошибка'}`);
|
||
}
|
||
|
||
const loadingResult = await loadingResponse.json();
|
||
const loadingReportId = loadingResult.report_id; // Получаем ID отчета о загрузке
|
||
|
||
// Сохраняем финальное состояние активной группы
|
||
if (activeGroupIndex >= 0 && unloadingGroups[activeGroupIndex]) {
|
||
const activeGroup = unloadingGroups[activeGroupIndex];
|
||
const finalUnloading = Math.max(0, weightAtGroupStart - currentWeight);
|
||
activeGroup.unloadedWeight += finalUnloading;
|
||
activeGroup.remainingWeight = activeGroup.targetWeight - activeGroup.unloadedWeight;
|
||
}
|
||
|
||
// Вычисляем общий выгруженный вес
|
||
const totalUnloadedWeight = initialTotalWeight - currentWeight;
|
||
|
||
// Формируем данные для отчета о выгрузке (если в рецепте нет групп — отправляем пустой массив, в отчёте будет «Групп нет»)
|
||
const reportUnloadingGroups = unloadingGroups.length > 0
|
||
? unloadingGroups.map(group => ({
|
||
name: group.name,
|
||
target_weight: group.originalWeight || group.targetWeight,
|
||
unloaded_weight: group.unloadedWeight,
|
||
remaining_weight: group.remainingWeight,
|
||
distribution_type: group.distributionType,
|
||
distribution_value: group.value,
|
||
order: group.order
|
||
}))
|
||
: [];
|
||
|
||
const unloadingReportData = {
|
||
recipe_id: recipeId, // UUID строка, не число
|
||
loading_report_id: loadingReportId, // Добавляем ID отчета о загрузке
|
||
total_weight: parseFloat(initialTotalWeight) || 0,
|
||
total_unloaded_weight: totalUnloadedWeight,
|
||
remaining_weight: currentWeight,
|
||
unloading_groups: reportUnloadingGroups
|
||
};
|
||
|
||
console.log('Отправляем отчет о выгрузке:', unloadingReportData);
|
||
|
||
// Сохраняем отчет о выгрузке
|
||
const unloadingResponse = await fetch('/api/save_unloading_report', {
|
||
method: 'POST',
|
||
headers: buildHeaders({ 'Content-Type': 'application/json' }),
|
||
body: JSON.stringify(unloadingReportData)
|
||
});
|
||
|
||
if (!unloadingResponse.ok) {
|
||
const unloadingError = await unloadingResponse.json();
|
||
throw new Error(`Ошибка сохранения отчета о выгрузке: ${unloadingError.message || 'неизвестная ошибка'}`);
|
||
}
|
||
|
||
const unloadingResult = await unloadingResponse.json();
|
||
console.log('Результат сохранения отчета о выгрузке:', unloadingResult);
|
||
|
||
// Очищаем localStorage после успешного сохранения
|
||
localStorage.removeItem('componentData');
|
||
localStorage.removeItem('componentLoadingTimes');
|
||
localStorage.removeItem('targetMixingTime');
|
||
localStorage.removeItem('actualMixingTime');
|
||
|
||
showToast('Отчеты успешно сохранены');
|
||
|
||
// Перенаправляем на страницу весов через небольшую задержку
|
||
setTimeout(() => {
|
||
window.location.href = '/scales';
|
||
}, 1500);
|
||
|
||
} catch (error) {
|
||
console.error('Detailed error:', {
|
||
message: error.message,
|
||
stack: error.stack,
|
||
error: error
|
||
});
|
||
showToast(error.message, 'error');
|
||
|
||
// Восстанавливаем кнопки при ошибке
|
||
isSaving = false;
|
||
finishBtn.disabled = false;
|
||
forceFinishBtn.disabled = false;
|
||
finishBtn.innerHTML = originalFinishText;
|
||
forceFinishBtn.innerHTML = originalForceText;
|
||
}
|
||
}
|
||
|
||
function updateDisplay() {
|
||
if (!kioskPaired) return;
|
||
// Вычисляем выгруженный вес и процент выгрузки
|
||
const unloadedWeight = initialTotalWeight - currentWeight;
|
||
const unloadPercentage = Math.min((unloadedWeight / initialTotalWeight) * 100, 100);
|
||
|
||
// Обновляем прогресс-бар
|
||
document.getElementById('progressBar').style.width = `${unloadPercentage}%`;
|
||
|
||
// Обновляем информацию о весе
|
||
document.getElementById('unloadedWeight').textContent = String(Math.round(unloadedWeight));
|
||
const mixerCurrentEl = document.getElementById('mixerCurrentWeight');
|
||
if (mixerCurrentEl) mixerCurrentEl.textContent = String(Math.round(currentWeight));
|
||
|
||
// Обновляем отображение в зависимости от наличия групп
|
||
if (unloadingGroups.length > 0) {
|
||
// Есть группы - обновляем групповое отображение
|
||
updateGroupsDisplay();
|
||
updateMainDisplay();
|
||
} else {
|
||
// Нет групп - показываем текущий вес кормосмесителя
|
||
const mixerWeight = document.getElementById('mixerWeight');
|
||
if (mixerWeight) {
|
||
mixerWeight.innerHTML = `${currentWeight.toFixed(0)}<span class="weight-unit"> кг</span>`;
|
||
fitUnloadingWeightDisplay();
|
||
}
|
||
}
|
||
|
||
// Проверяем завершение выгрузки (когда осталось менее 5% от начального веса)
|
||
if (!isUnloadingComplete && currentWeight <= initialTotalWeight * 0.05) {
|
||
isUnloadingComplete = true;
|
||
document.getElementById('finishBtn').disabled = false;
|
||
}
|
||
}
|
||
|
||
// Функция обновления кнопок навигации
|
||
function updateNavigationButtons() {
|
||
const prevBtn = document.getElementById('prevGroupBtn');
|
||
const nextBtn = document.getElementById('nextGroupBtn');
|
||
|
||
if (activeGroupIndex > 0) {
|
||
prevBtn.disabled = false;
|
||
} else {
|
||
prevBtn.disabled = true;
|
||
}
|
||
|
||
if (activeGroupIndex < unloadingGroups.length - 1) {
|
||
nextBtn.disabled = false;
|
||
} else {
|
||
nextBtn.disabled = true;
|
||
}
|
||
}
|
||
|
||
// Функция прокрутки к активной группе
|
||
function scrollToActiveGroup() {
|
||
const activeGroupElement = document.getElementById(`group-${activeGroupIndex}`);
|
||
if (activeGroupElement) {
|
||
const groupsContent = document.querySelector('.groups-content');
|
||
if (groupsContent) {
|
||
// Вычисляем позицию для центрирования элемента
|
||
const containerHeight = groupsContent.clientHeight;
|
||
const elementHeight = activeGroupElement.offsetHeight;
|
||
const elementTop = activeGroupElement.offsetTop;
|
||
|
||
// Центрируем элемент в контейнере
|
||
const scrollTop = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
||
|
||
// Плавная прокрутка
|
||
groupsContent.scrollTo({
|
||
top: Math.max(0, scrollTop),
|
||
behavior: 'smooth'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<script src="/static/js/kiosk-theme.js"></script>
|
||
<script src="/static/js/kiosk-pairing.js"></script>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
window.WespKioskTheme.setup({ withToggle: false });
|
||
window.WespKioskPairing.setup({
|
||
onPaired: function () {
|
||
kioskPaired = true;
|
||
initUnloadingPage();
|
||
},
|
||
onUnpaired: function () {
|
||
kioskPaired = false;
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
<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) {
|
||
WespUpdateNotifier.init({ dialog: window.WespKioskDialog || window.WespDialog });
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |