Files
2026-07-17 12:57:18 +03:00

1630 lines
61 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>