USER
Сделай чтобы скрипт ARM3 работал также как и ARM1 по принципу, а именно отображал пик на графике sparkline когда процесс пошел.
АРМ1 скрипт копирование файлов делает
import os
import time
import RPi.GPIO as GPIO
import requests
from datetime import datetime
import threading
import shutil
from concurrent.futures import ThreadPoolExecutor, as_completed
# URL сервера Flask
FLASK_SERVER_URL = "http://192.168.50.99:80"
worker_name = 'ARM1'
# Подключаем GPIO-пин для кнопки
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# Путь к файлам, которые нужно скопировать (локальные файлы на RPi)
source_files = ['/home/moon/force_upgrade', '/home/moon/ModelC-A_FW_V2.0.9.zip']
# Интервал для отправки heartbeat в секундах
HEARTBEAT_INTERVAL = 1
# Глобальный флаг для отслеживания процесса копирования
is_copying = False # <--- Добавлено
def check_internet(url='https://www.google.com/', timeout=5):
try:
_ = requests.get(url, timeout=timeout)
return True
except requests.RequestException:
return False
def send_heartbeat():
global is_copying # <--- Добавлено для доступа к глобальной переменной
while True:
internet_up = check_internet()
try:
response = requests.post(
f"{FLASK_SERVER_URL}/register_user",
json={
'worker_name': worker_name,
'internet_up': internet_up,
'copying': is_copying # <--- Добавлено
},
headers={'Content-Type': 'application/json'},
timeout=10
)
if response.status_code == 200:
response_data = response.json()
if response_data.get('status') == 'success':
status = 'Онлайн' if internet_up else 'Нет интернета'
print(f"[{datetime.now()}] Успешная регистрация/обновление статуса на сервере: {status}")
else:
error_message = response_data.get('message', 'Неизвестная ошибка')
print(f"[{datetime.now()}] Ошибка на сервере: {error_message}")
else:
print(f"[{datetime.now()}] Некорректный статус-код от сервера: {response.status_code}")
except requests.exceptions.ConnectionError:
print(f"[{datetime.now()}] Ошибка подключения к серверу. Сервер недоступен.")
except Exception as e:
print(f"[{datetime.now()}] Ошибка при регистрации: {e}")
time.sleep(HEARTBEAT_INTERVAL)
def copy_files_to_usb(destination, files):
global is_copying # <--- Добавлено для доступа к глобальной переменной
for file in files:
filename = os.path.basename(file)
dest_path = os.path.join(destination, filename)
try:
if os.path.abspath(file) == os.path.abspath(dest_path):
print(f"[{datetime.now()}] Файл {filename} уже находится в целевой директории {destination}.")
continue
if not os.path.exists(file):
print(f"[{datetime.now()}] Исходный файл {file} не существует.")
continue
shutil.copy(file, destination)
print(f"[{datetime.now()}] Файл {filename} скопирован в {destination}!")
except Exception as e:
print(f"[{datetime.now()}] Ошибка при копировании файла {filename} в {destination}: {e}")
def copy_files(destinations, files):
global is_copying # <--- Добавлено для доступа к глобальной переменной
try:
# Устанавливаем флаг копирования перед началом
is_copying = True
print(f"[{datetime.now()}] Начало копирования файлов на USB накопители.")
# Используем ThreadPoolExecutor для параллельного копирования на разные накопители
with ThreadPoolExecutor(max_workers=len(destinations)) as executor:
futures = {executor.submit(copy_files_to_usb, dest, files): dest for dest in destinations}
for future in as_completed(futures):
dest = futures[future]
try:
future.result()
except Exception as e:
print(f"[{datetime.now()}] Ошибка при копировании на {dest}: {e}")
finally:
# Сбрасываем флаг копирования после завершения
is_copying = False
print(f"[{datetime.now()}] Копирование файлов завершено.")
# Запуск потока heartbeat
heartbeat_thread = threading.Thread(target=send_heartbeat, daemon=True)
heartbeat_thread.start()
while True:
# Проверяем состояние кнопки
if GPIO.input(17) == False:
print(f"[{datetime.now()}] Кнопка нажата. Начинаем копирование файлов.")
# Находим подключенные USB накопители
usb_drives = [f'/media/moon/{d}' for d in os.listdir('/media/moon/') if os.path.isdir(f'/media/moon/{d}')]
if not usb_drives:
print(f"[{datetime.now()}] Нет подключенных USB накопителей!")
else:
# Копируем файлы на все USB накопители параллельно
copy_files(usb_drives, source_files)
# Подготовка данных для отправки на сервер
selected_date = datetime.now().strftime("%Y-%m-%d")
current_datetime = datetime.now()
arma_type = 'CFA' # Или другой тип по необходимости
data = {
'date': selected_date,
'worker_name': worker_name,
'timestamp': current_datetime.strftime("%Y-%m-%d %H:%M:%S"),
'arma_type': arma_type,
}
# Отправка данных на сервер
try:
response = requests.post(
f"{FLASK_SERVER_URL}/update_table",
json=data,
headers={'Content-Type': 'application/json'},
timeout=10
)
print(f"[{datetime.now()}] Ответ от сервера: {response.status_code}, {response.text}")
if response.status_code == 200:
response_data = response.json()
if response_data.get('status') == 'success':
print(f"[{datetime.now()}] Данные успешно отправлены на сервер для даты {selected_date}")
else:
error_message = response_data.get('message', 'Неизвестная ошибка')
print(f"[{datetime.now()}] Ошибка на сервере: {error_message}")
else:
print(f"[{datetime.now()}] Некорректный статус-код: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"[{datetime.now()}] Ошибка при отправке данных: {e}")
# Предотвращаем многократные срабатывания при удержании кнопки
while GPIO.input(17) == False:
time.sleep(0.1)
time.sleep(0.1)
ARM3 запускает скрипт , пик происходит на графике в момент запуска скрипта activationGlaz.py
import os
import time
import RPi.GPIO as GPIO
import requests
import subprocess
from datetime import datetime
# URL сервера Flask
FLASK_SERVER_URL = "http://192.168.50.99:80" # Адрес сервера
worker_name = "ARM_3"
try:
response = requests.post(
f"{FLASK_SERVER_URL}/register_user",
json={'worker_name': worker_name},
headers={'Content-Type': 'application/json'}
)
if response.status_code == 200:
print("Успешная регистрация на сервере")
else:
print("Ошибка при регистрации на сервере")
except Exception as e:
print(f"Ошибка при регистрации: {e}")
# Настройка GPIO порта
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # подключаем кнопку к GPIO 17
def run_script():
print("Кнопка нажата! Запускаем скрипт...")
subprocess.call(["python", "/home/moon/Desktop/activationGlaz.py"]) # запускаем питоновский скрипт
try:
while True:
if GPIO.input(17) == False: # если кнопка нажата
run_script()
time.sleep(0.1) # ждем 0.1 секунды
except KeyboardInterrupt:
GPIO.cleanup() # очищаем GPIO порты
Мой код
from flask import Flask, render_template, request, redirect, make_response, jsonify, send_file, Response
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.sql import func
from datetime import datetime, timedelta, date
import pandas as pd
from io import BytesIO
import json
import queue
import logging
from sqlalchemy import cast, Integer, or_
import os
from collections import deque
import random
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///table_data.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
ACTIVITY_HISTORY_LENGTH = 400 # Количество последних значений для графика
activity_data = {}
class TableRow(db.Model):
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.String(10), nullable=False)
row_number = db.Column(db.String(10))
timestamp = db.Column(db.String(50))
number = db.Column(db.String(50))
serial = db.Column(db.String(50))
dl_number = db.Column(db.String(50))
worker_name = db.Column(db.String(100))
pack_time = db.Column(db.String(50))
arma_type = db.Column(db.String(50))
class DLCounter(db.Model):
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.String(10), unique=True, nullable=False)
counter = db.Column(db.Integer, nullable=False)
def __repr__(self):
return f'<DLCounter {self.date}: {self.counter}>'
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
login_history = db.relationship('LoginHistory', backref='user', lazy=True)
class LoginHistory(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
login_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
class Statistics(db.Model):
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.String(10), unique=True, nullable=False)
generate_clicks = db.Column(db.Integer, default=0)
pack_clicks = db.Column(db.Integer, default=0)
def __repr__(self):
return f'<Statistics {self.date}: Generate={self.generate_clicks}, Pack={self.pack_clicks}>'
# Добавьте это в раздел моделей вашего Flask-приложения
class Device(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(10), unique=True, nullable=False) # Имена: ARM1, ARM2 и т.д.
arma_type = db.Column(db.String(10), nullable=False, default='Other') # Тип АРМА
last_active = db.Column(db.DateTime, nullable=True) # Время последней активности
internet_up = db.Column(db.Boolean, nullable=False, default=True) # Статус интернет-соединения
copying = db.Column(db.Boolean, nullable=False, default=False) # Статус копирования
previous_copying = db.Column(db.Boolean, nullable=False, default=False) # Новое поле
sending_number = db.Column(db.Boolean, nullable=False, default=False) # Статус отправки номера
def to_dict(self, current_time=None):
if current_time is None:
current_time = datetime.utcnow()
if self.copying:
status = 'copying'
elif self.sending_number:
status = 'sending_number'
else:
status = 'offline'
if self.last_active:
delta = current_time - self.last_active
if delta <= timedelta(seconds=60):
if self.internet_up:
status = 'online'
else:
status = 'no_internet'
return {
"id": self.id,
"name": self.name,
"arma_type": self.arma_type,
"last_active": self.last_active.isoformat() if self.last_active else None,
"internet_up": self.internet_up,
"copying": self.copying,
"sending_number": self.sending_number,
"status": status
}
tables = {}
table = [['№', 'Время отправки', 'Номер ', 'Serial', 'Графа', 'Имя отправителя','Время упаковки' ,'Тип АРМА', 'Действие']]
# Создаем очередь для событий
update_queue = queue.Queue()
@app.route('/get_device_status', methods=['GET'])
def get_device_status():
devices = Device.query.all()
device_statuses = {}
threshold = timedelta(seconds=5) # Уменьшенный порог времени для быстрого определения оффлайн
now = datetime.utcnow()
for device in devices:
name = device.name
if device.sending_number:
status = 'sending_number'
# Используем более высокие значения для создания импульсов
activity_value = random.choice([2, 3, 2, 3, 4, 3, 2])
elif device.copying:
status = 'copying'
activity_value = random.choice([0, 1, 0, 1, 2, 1, 0])
else:
if device.last_active and now - device.last_active <= threshold:
if device.internet_up:
status = 'online'
activity_value = 1
else:
status = 'no_internet'
activity_value = 0
else:
status = 'offline'
activity_value = 0
device_statuses[name] = status
activity_data[name].append(activity_value)
logging.debug(f"Устройство {name}: статус {status}, активность {activity_value}")
# Преобразуем deque в список для JSON
activity_data_serializable = {name: list(data) for name, data in activity_data.items()}
logging.debug(f"Activity data: {activity_data_serializable}") # Добавьте эту строку для отладки
return jsonify({
'status': 'success',
'devices': device_statuses,
'activity': activity_data_serializable
})
def initialize_activity_data():
device_names = ['ARM1', 'ARM2', 'ARM3', 'ARM4', 'ARM5', 'ARM6']
for name in device_names:
activity_data[name] = deque([0]*ACTIVITY_HISTORY_LENGTH, maxlen=ACTIVITY_HISTORY_LENGTH)
def create_devices():
device_types = {
'ARM1': 'CFA',
'ARM2': 'VND',
'ARM3': 'SRC',
'ARM4': 'SRC',
'ARM5': 'RPA',
'ARM6': 'RPA' # Убедитесь, что ARM6 тоже имеет тип 'RPA'
}
for name, arma_type in device_types.items():
device = Device.query.filter_by(name=name).first()
if not device:
device = Device(name=name, arma_type=arma_type, sending_number=False)
db.session.add(device)
db.session.commit()
initialize_activity_data()
@app.route('/register_user', methods=['POST'])
def register_user():
data = request.json
worker_name = data.get('worker_name')
internet_up = data.get('internet_up', False)
copying = data.get('copying', False)
sending_number = data.get('sending_number', False)
if not worker_name:
return jsonify({"status": "error", "message": "worker_name is required"}), 400
device = Device.query.filter_by(name=worker_name).first()
if not device:
return jsonify({"status": "error", "message": "Device not found"}), 404
device.last_active = datetime.utcnow()
device.internet_up = internet_up
device.copying = copying
device.sending_number = sending_number
db.session.commit()
return jsonify({"status": "success"})
@app.route('/get_unpacked_days', methods=['GET'])
def get_unpacked_days():
year = request.args.get('year')
month = request.args.get('month')
if not year or not month:
return jsonify({"status": "error", "message": "Необходимо указать год и месяц"}), 400
try:
year = int(year)
month = int(month)
except ValueError:
return jsonify({"status": "error", "message": "Год и месяц должны быть числами"}), 400
# Формируем формат даты для фильтрации
month_str = f"{month:02d}"
# Запрашиваем уникальные дни, где pack_time отсутствует и arma_type равен 'RPA'
unpacked_dates = TableRow.query.filter(
TableRow.date.like(f"{year}-{month_str}-%"),
TableRow.arma_type == 'RPA',
or_(TableRow.pack_time == None, TableRow.pack_time == '')
).with_entities(TableRow.date).distinct().all()
# Извлекаем дни из дат
unpacked_days = set()
for date_str, in unpacked_dates:
try:
day = int(date_str.split('-')[2])
unpacked_days.add(day)
except (IndexError, ValueError):
continue # Пропускаем некорректные форматы дат
return jsonify({'status': 'success', 'unpacked_days': list(unpacked_days)})
@app.route('/stream')
def stream():
def event_stream():
while True:
try:
data = update_queue.get(timeout=20)
yield f'data: {data}\n\n'
except queue.Empty:
yield ': keep-alive\n\n'
return Response(event_stream(), mimetype='text/event-stream')
# Удален маршрут /ping, так как он использовал ActiveUser
@app.route('/get_table_data')
def get_table_data():
date = request.args.get('date')
filter_arma = request.args.get('filter_arma') # Новый параметр фильтра
app.logger.debug(f"Получен запрос таблицы для даты: {date}, фильтр: {filter_arma}")
# Начинаем построение запроса
query = TableRow.query.filter_by(date=date)
# Если фильтр активирован, добавляем условие
if filter_arma == 'RPA':
query = query.filter_by(arma_type='RPA')
rows = query.all()
app.logger.debug(f"Найдено записей: {len(rows)} для даты {date} с фильтром {filter_arma}")
data = []
numbers = {}
serials = {}
dl_numbers = {}
for row in rows:
data.append({
'id': row.id,
'row_number': row.row_number,
'timestamp': row.timestamp,
'number': row.number,
'serial': row.serial,
'dl_number': row.dl_number,
'worker_name': row.worker_name,
'pack_time': row.pack_time,
'arma_type': row.arma_type,
})
# Собираем информацию о дубликатах
if row.number:
clean_number = ''.join(filter(str.isdigit, row.number))
if clean_number in numbers:
numbers[clean_number].append(len(data) - 1)
else:
numbers[clean_number] = [len(data) - 1]
if row.serial:
clean_serial = ''.join(filter(str.isdigit, row.serial))
if clean_serial in serials:
serials[clean_serial].append(len(data) - 1)
else:
serials[clean_serial] = [len(data) - 1]
if row.dl_number:
clean_dl_number = ''.join(filter(str.isdigit, row.dl_number))
if clean_dl_number in dl_numbers:
dl_numbers[clean_dl_number].append(len(data) - 1)
else:
dl_numbers[clean_dl_number] = [len(data) - 1]
# Фильтруем только реальные дубликаты
duplicate_numbers = {k: v for k, v in numbers.items() if len(v) > 1}
duplicate_serials = {k: v for k, v in serials.items() if len(v) > 1}
duplicate_dl_numbers = {k: v for k, v in dl_numbers.items() if len(v) > 1}
return jsonify({
"data": data,
"duplicates": {
"numbers": duplicate_numbers,
"serials": duplicate_serials,
"dl_numbers": duplicate_dl_numbers
}
})
@app.route('/update_table', methods=['POST'])
def update_table():
data = request.json
print(data)
date = data['date']
worker_name = data['worker_name']
arma_type = data['arma_type']
# Получаем количество существующих записей для этой даты
row_count = TableRow.query.filter_by(date=date).count() + 1
# Создаём новую строку в таблице
if not data.get('number') and not data.get('serial'):
new_row = TableRow(
date=date,
row_number=str(row_count),
timestamp=data['timestamp'],
number='', # Пустое значение для номера
serial='', # Пустое значение для серийного номера
dl_number='', # Пустое значение для графы
worker_name=worker_name,
arma_type=arma_type,
)
elif not data.get('number'):
new_row = TableRow(
date=date,
row_number=str(row_count),
timestamp=data['timestamp'],
number='', # Пустое значение для номера
serial=data['serial'],
dl_number='', # Пустое значение для графы
worker_name=worker_name,
arma_type=arma_type,
)
else:
new_row = TableRow(
date=date,
row_number=str(row_count),
timestamp=data['timestamp'],
number=data['number'],
serial=data['serial'],
dl_number=data.get('dl_number', ''),
worker_name=worker_name,
arma_type=arma_type,
)
# Сохраняем запись в базе данных
db.session.add(new_row)
device = Device.query.filter_by(name=worker_name).first()
if device:
device.last_active = datetime.utcnow()
db.session.commit()
# Отправляем сообщение в очередь для обновления клиентов
update_queue.put(json.dumps({"type": "update", "date": date}))
# Добавляем импульсивное значение для sparkline, если запись отправлена ARM5 или ARM6
if worker_name in ['ARM5', 'ARM6']:
activity_data[worker_name].append(5) # Значение импульса (можно настроить по необходимости)
return jsonify({"status": "success"})
@app.route('/update_graf', methods=['POST'])
def update_graf():
try:
data = request.json
row_id = int(data.get('id'))
new_value = data.get('new_value')
date = data.get('date')
if not row_id or not date:
return jsonify({"status": "error", "message": "Missing 'id' or 'date' in request"}), 400
# Находим запись по id и дате
row = TableRow.query.filter_by(id=row_id, date=date).first()
if row:
row.dl_number = new_value
db.session.commit()
return jsonify({"status": "success"})
else:
return jsonify({"status": "error", "message": "Row not found"}), 404
except (ValueError, TypeError) as e:
return jsonify({"status": "error", "message": f"Invalid input: {str(e)}"}), 400
except Exception as e:
db.session.rollback()
return jsonify({"status": "error", "message": str(e)}), 500
@app.route('/update_arma_type', methods=['POST'])
def update_arma_type():
try:
data = request.json
date = data['date']
serial = data['serial']
arma_type = data['arma_type']
row = TableRow.query.filter_by(date=date, serial=serial).first()
if row:
row.arma_type = arma_type
db.session.commit()
return jsonify({"status": "success"})
else:
return jsonify({"status": "error", "message": "Row not found"})
except Exception as e:
return jsonify({"status": "error", "message": str(e)})
@app.route('/update_number', methods=['POST'])
def update_number():
try:
data = request.json
row_id = int(data['id']) # Используем 'id' вместо 'row_index'
new_value = data['new_value']
date = data['date']
# Находим запись по id и дате
row = TableRow.query.filter_by(id=row_id, date=date).first()
if row:
row.number = new_value
db.session.commit()
return jsonify({"status": "success"})
return jsonify({"status": "error", "message": "Row not found"})
except Exception as e:
db.session.rollback()
return jsonify({"status": "error", "message": str(e)})
@app.route('/pack_item', methods=['POST'])
def pack_item():
try:
data = request.json
row_id = data['id']
date = data['date']
rows = TableRow.query.filter_by(id=row_id, date=date).all()
if rows:
row = rows[0]
pack_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
row.pack_time = pack_time
db.session.commit()
# Обновляем статистику упаковки
statistics = Statistics.query.filter_by(date=date).first()
if statistics:
statistics.pack_clicks += 1
else:
statistics = Statistics(date=date, generate_clicks=0, pack_clicks=1) # pack_clicks=1
db.session.add(statistics)
db.session.commit()
# Отправляем сообщение в очередь для обновления клиентов
update_queue.put(json.dumps({"type": "update", "date": date}))
return jsonify({"status": "success", "pack_time": pack_time})
return jsonify({"status": "error", "message": "Строка не найдена"}), 404
except Exception as e:
print(f"Ошибка в pack_item: {str(e)}")
return jsonify({"status": "error", "message": str(e)}), 500
@app.route('/set_initial_dl', methods=['POST'])
def set_initial_dl():
try:
data = request.json
if not data:
return jsonify({"status": "error", "message": "No data received"})
date = data.get('date')
initial_number = data.get('initial_number')
if not date or not initial_number:
return jsonify({
"status": "error",
"message": "Missing required fields",
"received": {"date": date, "initial_number": initial_number}
})
try:
initial_number = int(initial_number)
except ValueError:
return jsonify({
"status": "error",
"message": "Initial number must be an integer"
})
print(f"Setting initial DL for date {date}: {initial_number}") # Отладочный вывод
dl_counter = DLCounter.query.filter_by(date=date).first()
if dl_counter:
dl_counter.counter = initial_number
print(f"Updated existing counter for {date}") # Отладочный вывод
else:
dl_counter = DLCounter(date=date, counter=initial_number)
db.session.add(dl_counter)
print(f"Created new counter for {date}") # Отладочный вывод
db.session.commit()
return jsonify({
"status": "success",
"message": f"DL counter set to {initial_number} for date {date}"
})
except Exception as e:
print(f"Error in set_initial_dl: {str(e)}") # Отладочный вывод
db.session.rollback()
return jsonify({
"status": "error",
"message": str(e)
})
@app.route('/generate_dl', methods=['POST'])
def generate_dl():
try:
data = request.json
row_id = data['id']
date = data['date']
# Получаем или создаем счетчик DL для данной даты
dl_counter = DLCounter.query.filter_by(date=date).first()
if not dl_counter:
return jsonify({"status": "error", "message": "DL counter не инициализирован"}), 400
# Генерируем новый DL номер
dl_number = f"DL{dl_counter.counter:06d}"
# Находим и обновляем нужную запись по id
row = TableRow.query.filter_by(id=row_id, date=date).first()
if row:
row.dl_number = dl_number
dl_counter.counter += 1 # Увеличиваем счетчик
# Обновляем статистику генерации
statistics = Statistics.query.filter_by(date=date).first()
if statistics:
statistics.generate_clicks += 1
else:
statistics = Statistics(date=date, generate_clicks=1, pack_clicks=0)
db.session.add(statistics)
db.session.commit()
return jsonify({"status": "success", "dl_number": dl_number, "id": row_id})
return jsonify({"status": "error", "message": "Строка не найдена"}), 404
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 500
@app.route('/export_excel')
def export_excel():
try:
date = request.args.get('date')
sort_column = request.args.get('sort_column')
sort_order = request.args.get('sort_order')
if not date:
return jsonify({"error": "Date parameter is required"}), 400
# Начинаем построение запроса с фильтрации по дате и типу АРМА "RPA"
query = TableRow.query.filter_by(date=date, arma_type='RPA')
# Добавляем дополнительные условия фильтрации:
# - dl_number не пустой
# - pack_time не пустой
query = query.filter(
TableRow.dl_number.isnot(None),
TableRow.dl_number != '',
TableRow.pack_time.isnot(None),
TableRow.pack_time != ''
)
# Применяем сортировку, если указана
if sort_column and sort_order:
column_mapping = {
'0': TableRow.row_number,
'1': TableRow.timestamp,
'2': TableRow.number,
'3': TableRow.serial,
'4': TableRow.dl_number,
'5': TableRow.worker_name,
'6': TableRow.pack_time, # Время упаковки
'7': TableRow.arma_type, # Тип АРМА
}
if sort_column in column_mapping:
column = column_mapping[sort_column]
# Явно приводим столбец "Номер" к целому числу для корректной сортировки
if sort_column == '2':
column = cast(column, Integer)
if sort_order == 'desc':
query = query.order_by(column.desc())
else:
query = query.order_by(column.asc())
else:
return jsonify({"error": "Invalid sort column"}), 400
rows = query.all()
# Создаем DataFrame только с отфильтрованными данными
data = []
for row in rows:
# Безопасное преобразование номера
try:
number = int(row.number) if row.number and row.number.strip() else None
except ValueError:
number = None
data.append({
'№': row.row_number,
'Время отправки': row.timestamp,
'Номер': number,
'Serial': row.serial,
'Графа': row.dl_number,
'Имя отправителя': row.worker_name,
'Время упаковки': row.pack_time, # Время упаковки
'Тип АРМА': row.arma_type, # Тип АРМА
})
if not data:
return jsonify({"error": "Нет записей для экспорта с заданными критериями."}), 400
df = pd.DataFrame(data)
# Создаем Excel файл
output = BytesIO()
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
df.to_excel(writer, index=False, sheet_name='Sheet1')
# Настраиваем форматирование
workbook = writer.book
worksheet = writer.sheets['Sheet1']
# Автоматическая ширина столбцов
for idx, col in enumerate(df.columns):
series = df[col]
max_len = max(
series.astype(str).map(len).max(),
len(str(col))
) + 2
worksheet.set_column(idx, idx, max_len)
# Форматирование числовых столбцов
if col in ['Номер', 'Графа']:
num_format = workbook.add_format({'num_format': '0'})
worksheet.set_column(idx, idx, max_len, num_format)
output.seek(0)
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=f'export_{date}.xlsx'
)
except Exception as e:
print(f"Export error: {str(e)}")
return jsonify({"error": str(e)}), 500
@app.route('/update_serial', methods=['POST'])
def update_serial():
try:
data = request.json
row_index = int(data['row_index'])
new_value = data['new_value']
date = data['date']
rows = TableRow.query.filter_by(date=date).all()
if 0 <= row_index < len(rows):
row = rows[row_index]
row.serial = new_value
db.session.commit()
return jsonify({"status": "success"})
return jsonify({"status": "error", "message": "Invalid row index"})
except Exception as e:
db.session.rollback()
return jsonify({"status": "error", "message": str(e)})
def initialize_table_for_date(date):
# Проверяем, существуют ли уже записи для этой даты
existing = TableRow.query.filter_by(date=date).first()
if not existing:
# Создаем новую запись только в случае отсутствия данных
new_row = TableRow(
date=date,
row_number=str(1),
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
number='',
serial='',
dl_number='',
worker_name='',
arma_type='',
pack_time='',
)
db.session.add(new_row)
# Инициализируем статистику для новой даты
statistics = Statistics(date=date, generate_clicks=0, pack_clicks=0)
db.session.add(statistics)
db.session.commit()
@app.route('/update_serial_arma', methods=['POST'])
def update_serial_arma():
try:
data = request.json
date = data['date']
serial = data['serial']
arma_type = data['arma_type']
# Создаем новую запись в таблице
new_row = TableRow(
date=date,
serial=serial,
arma_type=arma_type,
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
db.session.add(new_row)
db.session.commit()
return jsonify({"status": "success"})
except Exception as e:
db.session.rollback()
return jsonify({"status": "error", "message": str(e)}), 500
@app.route('/get_statistics')
def get_statistics():
try:
date = request.args.get('date')
app.logger.debug(f"Получен запрос статистики для даты: {date}")
# Получаем статистику генерации и упаковки
statistics = Statistics.query.filter_by(date=date).first()
if not statistics:
generate_clicks = 0
pack_clicks = 0
app.logger.debug(f"Статистика для даты {date} не найдена. Установлено значение 0.")
else:
generate_clicks = statistics.generate_clicks
pack_clicks = statistics.pack_clicks
app.logger.debug(f"Статистика для даты {date}: Генераций = {generate_clicks}, Упаковок = {pack_clicks}")
# Получаем количество записей типа "RPA"
rpa_records = TableRow.query.filter_by(date=date, arma_type='RPA').count()
app.logger.debug(f"Количество записей типа RPA для даты {date}: {rpa_records}")
return jsonify({
"generate_clicks": generate_clicks,
"pack_clicks": pack_clicks,
"rpa_records": rpa_records
})
except Exception as e:
app.logger.error(f"Ошибка в get_statistics: {str(e)}")
return jsonify({
"generate_clicks": 0,
"pack_clicks": 0,
"rpa_records": 0,
"error": str(e)
})
@app.route('/delete_row', methods=['POST'])
def delete_row():
try:
data = request.json
row_id = data.get('id')
if not row_id:
return jsonify({'status': 'error', 'message': 'ID записи не предоставлен'}), 400
row = TableRow.query.get(row_id)
if not row:
return jsonify({'status': 'error', 'message': 'Запись не найдена'}), 404
date = row.date # Получаем дату записи для обновления статистики
# Получаем запись статистики для этой даты
statistics = Statistics.query.filter_by(date=date).first()
if statistics:
# Если запись имела сгенерированный DL номер, уменьшаем generate_clicks
if row.dl_number:
if statistics.generate_clicks > 0:
statistics.generate_clicks -= 1
else:
app.logger.warning(f"Attempting to decrement generate_clicks below zero for date {date}")
# Если запись была упакована, уменьшаем pack_clicks
if row.pack_time:
if statistics.pack_clicks > 0:
statistics.pack_clicks -= 1
else:
app.logger.warning(f"Attempting to decrement pack_clicks below zero for date {date}")
db.session.commit()
else:
app.logger.warning(f"No Statistics entry found for date {date} when deleting row {row_id}")
# Удаляем запись из таблицы
db.session.delete(row)
db.session.commit()
# Отправляем сообщение в очередь для обновления клиентов
update_queue.put(json.dumps({"type": "delete", "date": date, "id": row.id}))
return jsonify({'status': 'success'})
except Exception as e:
db.session.rollback()
app.logger.error(f"Ошибка в delete_row: {str(e)}")
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/', methods=['GET', 'POST'])
def main():
if request.method == 'POST':
worker = request.form.get('worker')
name = request.form.get('name')
# Сохраняем информацию о входе
user = User.query.filter_by(username=name).first()
if not user:
user = User(username=name)
db.session.add(user)
login_history = LoginHistory(user=user)
db.session.add(login_history)
db.session.commit()
resp = make_response(redirect('/welcome'))
resp.set_cookie('worker', worker)
resp.set_cookie('name', name)
return resp
return render_template('main.html')
@app.route('/get_current_dl', methods=['GET'])
def get_current_dl():
date = request.args.get('date')
if not date:
return jsonify({"status": "error", "message": "Дата обязательна"}), 400
dl_counter = DLCounter.query.filter_by(date=date).first()
if dl_counter:
return jsonify({"status": "success", "current_dl": dl_counter.counter})
else:
return jsonify({"status": "error", "message": "Счетчик DL не инициализирован для этой даты"}), 404
@app.route('/welcome')
def welcome():
worker = request.cookies.get('worker')
name = request.cookies.get('name')
if not worker or not name:
return redirect('/')
current_time = datetime.utcnow()
devices = [device.to_dict(current_time=current_time) for device in Device.query.order_by(Device.name).all()]
current_time_str = current_time.isoformat()
return render_template('welcome.html', name=name, worker=worker, table=table, devices=devices, current_time=current_time_str)
@app.route('/logout')
def logout():
resp = make_response(redirect('/'))
resp.delete_cookie('worker')
resp.delete_cookie('name')
return resp
if __name__ == '__main__':
with app.app_context():
db.create_all()
create_devices()
logging.disable(logging.CRITICAL)
os.system('title СЕРВЕР')
app.run(host='192.168.50.99', port=80, debug=True) #192.168.50.99
let isEditing = false;
let searchTimeout;
let lastUpdateTime = 0;
const updateInterval = 1000;
let eventSource;
let lastData = null;
let isDeleting = false;
let fpInstance;
let unpackedDaysCache = {}; // Ключи будут в формате 'YYYY-MM', значение - Set дней
const deviceSparklines = {};
// Глобальные переменные для отслеживания сортировки
let globalSortColumn = null;
let globalSortOrder = 'asc';
function getSelectedDate() {
return $('#datepicker').val();
}
// Функция для получения состояния фильтра
function getArmaFilter() {
return $('#armaFilter').is(':checked') ? 'RPA' : '';
}
function updateTable(force = false, highlightNew = false) {
return new Promise((resolve, reject) => {
const now = Date.now();
if (!force && now - lastUpdateTime < updateInterval) {
resolve();
return;
}
if (isEditing) {
resolve();
return;
}
if (isEditing || isDeleting) { // Добавляем проверку на isDeleting
resolve();
return;
}
$.ajax({
url: '/get_table_data',
type: 'GET',
data: {
date: getSelectedDate(),
filter_arma: getArmaFilter()
},
success: function(response) {
var tableBody = $('#tableBody');
// Обновляем содержимое таблицы
updateTableContent(response.data, tableBody, highlightNew);
// Применяем текущую сортировку
if (globalSortColumn !== null) {
sortTable(globalSortColumn, globalSortOrder);
}
updateStatistics();
lastUpdateTime = now;
// Проверяем и подсвечиваем дубликаты
highlightDuplicates(response.duplicates);
resolve();
},
error: function(xhr, status, error) {
console.error('Error updating table:', error);
reject(error);
}
});
});
}
function initializeSparklines(activity) {
for (const [name, data] of Object.entries(activity)) {
const sparklineSpan = $(`#${name}-sparkline`);
sparklineSpan.sparkline(data, {
type: 'line',
width: '300px', // Увеличенная ширина
height: '50px', // Также можно увеличить высоту для лучшей видимости
lineColor: 'green',
fillColor: 'transparent',
spotColor: 'red', // Отображение пиков
minSpotColor: 'blue', // Минимальные пики
maxSpotColor: 'orange', // Максимальные пики
highlightSpotColor: 'blue',
highlightLineColor: 'blue'
});
sparklineSpan.data('sparkline-initialized', true);
}
}
function fetchDeviceStatuses() {
$.ajax({
url: '/get_device_status',
type: 'GET',
success: function(response) {
if (response.status === 'success') {
const devices = response.devices;
const activity = response.activity;
console.log("Received devices:", devices);
console.log("Received activity data:", activity);
for (const [name, status] of Object.entries(devices)) {
// Обновляем кружочек статуса
const statusCircle = $(`#${name}-status`);
const statusText = $(`#${name}-status-text`);
statusCircle.removeClass('online offline no_internet copying sending_number processing');
if (status === 'sending_number') {
statusCircle.addClass('sending_number');
statusText.text('Отправка номера');
} else if (status === 'copying') {
statusCircle.addClass('copying');
statusText.text('Копирование');
} else if (status === 'processing') {
statusCircle.addClass('processing');
statusText.text('Процесс идет');
} else if (status === 'online') {
statusCircle.addClass('online');
statusText.text('Онлайн');
} else if (status === 'no_internet') {
statusCircle.addClass('no_internet');
statusText.text('Нет интернета');
} else {
statusCircle.addClass('offline');
statusText.text('Нет соединения');
}
}
// Обновляем Sparkline
updateSparklines(devices, activity);
}
},
error: function(error) {
console.error('Ошибка при получении статусов устройств:', error);
}
});
}
function updateSparklines(devices, activity) {
for (const [name, status] of Object.entries(devices)) {
const sparklineSpan = $(`#${name}-sparkline`);
const data = activity[name];
// Определяем цвет линии в зависимости от статуса устройства и последнего значения активности
let lineColor = 'green'; // Стандартный цвет
if (status === 'processing') {
lineColor = 'purple'; // Цвет для статуса "processing"
} else if (status === 'copying') {
lineColor = 'orange';
} else if (status === 'sending_number') {
lineColor = 'blue';
} else if (status === 'no_internet') {
lineColor = 'red';
}
// Проверяем, был ли добавлен импульс (например, значение > 4)
const lastValue = data[data.length - 1];
if (lastValue > 4) {
lineColor = 'purple'; // Цвет импульса
}
if (sparklineSpan.data('sparkline-initialized')) {
// Обновляем существующий Sparkline
sparklineSpan.sparkline(data, {
type: 'line',
width: '100%',
height: '20px',
lineColor: lineColor,
fillColor: 'transparent',
spotColor: 'red',
minSpotColor: 'blue',
maxSpotColor: 'orange',
highlightSpotColor: 'blue',
highlightLineColor: 'blue'
});
} else {
// Инициализируем Sparkline, если он ещё не инициализирован
sparklineSpan.sparkline(data, {
type: 'line',
width: '100%',
height: '20px',
lineColor: lineColor,
fillColor: 'transparent',
spotColor: 'red',
minSpotColor: 'blue',
maxSpotColor: 'orange',
highlightSpotColor: 'blue',
highlightLineColor: 'blue'
});
sparklineSpan.data('sparkline-initialized', true);
}
}
}
function highlightDuplicates(duplicates) {
// Очищаем предыдущие выделения
$('#tableBody td').removeClass('duplicate-highlight');
// Подсвечиваем дубликаты номеров
for (let number in duplicates.numbers) {
duplicates.numbers[number].forEach(index => {
$(`#tableBody tr:eq(${index}) td:nth-child(3)`).addClass('duplicate-highlight');
});
}
// Подсвечиваем дубликаты серийных номеров
for (let serial in duplicates.serials) {
duplicates.serials[serial].forEach(index => {
$(`#tableBody tr:eq(${index}) td:nth-child(4)`).addClass('duplicate-highlight');
});
}
// Подсвечиваем дубликаты "Графа" (dl_number)
for (let dl_number in duplicates.dl_numbers) {
duplicates.dl_numbers[dl_number].forEach(index => {
$(`#tableBody tr:eq(${index}) td:nth-child(5)`).addClass('duplicate-highlight');
});
}
}
function updateCell(cell, newValue, rowId, cellIndex, rowData) {
console.log(`Updating cell: index=${cellIndex}, value=${newValue}, rowId=${rowId}`);
const excludedArmaTypes = ["CFA", "VND", "SRC"];
if (cellIndex === 4) { // Столбец "Графа"
if (rowData.arma_type === 'RPA' && !excludedArmaTypes.includes(rowData.arma_type)) {
if (newValue && newValue.startsWith('DL')) {
cell.text(newValue)
.addClass('clickable')
.css('cursor', 'pointer')
.off('click') // Удаляем предыдущие обработчики
.on('click', function() {
copyToClipboard(newValue);
$(this).addClass('copied');
setTimeout(() => {
$(this).removeClass('copied');
}, 200);
});
// Обновляем data-value
let numericValue = parseDL(newValue) || 0;
cell.attr('data-value', numericValue);
} else {
cell.html('<button class="generate-button">Генерировать</button>')
.find('button')
.click(function() {
generateDL(rowId);
});
// Обновляем data-value
cell.attr('data-value', 0);
}
} else if (!excludedArmaTypes.includes(rowData.arma_type)) {
if (newValue && newValue.startsWith('DL')) {
cell.text(newValue)
.addClass('clickable')
.css('cursor', 'pointer')
.off('click')
.on('click', function() {
copyToClipboard(newValue);
$(this).addClass('copied');
setTimeout(() => {
$(this).removeClass('copied');
}, 200);
});
// Обновляем data-value
let numericValue = parseDL(newValue) || 0;
cell.attr('data-value', numericValue);
} else {
cell.text(newValue || '');
// Обновляем data-value
let numericValue = parseDL(newValue) || 0;
cell.attr('data-value', numericValue);
}
} else {
cell.text(newValue || '');
// Обновляем data-value
let numericValue = parseDL(newValue) || 0;
cell.attr('data-value', numericValue);
}
} else if (cellIndex === 6) { // Столбец "Время упаковки"
const dlCell = cell.parent().find('td').eq(4);
const dlValue = dlCell.text().trim();
const armaType = rowData.arma_type;
if (dlValue.startsWith('DL') && !newValue && !excludedArmaTypes.includes(armaType)) {
cell.html('<button class="pack-button">Упаковать</button>')
.find('button')
.click(function() {
packItem(rowId);
});
} else {
cell.text(newValue);
}
} else if (cellIndex === 7) { // Столбец "Тип АРМА"
if (!newValue || newValue.trim() === '') {
newValue = 'RPA';
}
cell.text(newValue)
.toggleClass('editable', true)
.attr('data-column', cellIndex);
} else {
cell.text(newValue)
.toggleClass('editable', cellIndex === 2 || cellIndex === 3)
.attr('data-column', cellIndex);
}
}
function sortTable(column, order) {
const rows = $('#tableBody > tr').get();
rows.sort(function(a, b) {
let A = $(a).children('td').eq(column).text().trim();
let B = $(b).children('td').eq(column).text().trim();
// Обработка пустых значений
if (A === '' && B === '') return 0;
if (A === '') return order === 'asc' ? 1 : -1;
if (B === '') return order === 'asc' ? -1 : 1;
// Специальная обработка для столбца "Графа" (dl_number) с использованием data-value
if (column === 4) { // Индекс столбца "Графа"
let numA = parseInt($(a).children('td').eq(column).attr('data-value')) || 0;
let numB = parseInt($(b).children('td').eq(column).attr('data-value')) || 0;
// Отладочные сообщения
console.log(`Сортировка Графа: numA = ${numA}; numB = ${numB}`);
return order === 'asc' ? numA - numB : numB - numA;
}
// Обработка дат и времени для соответствующих столбцов
if (column === 1 || column === 6) { // Столбцы "Время отправки" и "Время упаковки"
let dateA = Date.parse(A);
let dateB = Date.parse(B);
if (isNaN(dateA)) dateA = 0;
if (isNaN(dateB)) dateB = 0;
return order === 'asc' ? dateA - dateB : dateB - dateA;
}
// Обработка числовых столбцов
if (column === 0 || column === 2) { // Столбцы с числами (№ и Номер)
let numA = parseFloat(A) || 0;
let numB = parseFloat(B) || 0;
return order === 'asc' ? numA - numB : numB - numA;
}
// Обработка строковых столбцов
if (column === 5) { // Столбец с именем отправителя
A = A.toLowerCase();
B = B.toLowerCase();
}
// Строковое сравнение для остальных столбцов
A = A.toUpperCase();
B = B.toUpperCase();
if (A < B) return order === 'asc' ? -1 : 1;
if (A > B) return order === 'asc' ? 1 : -1;
return 0;
});
// Очистка тела таблицы
$('#tableBody').empty();
// Добавление отсортированных строк
$.each(rows, function(index, row) {
$('#tableBody').append(row);
});
checkDuplicates();
bindEventHandlers();
}
function parseDL(dl_number) {
if (typeof dl_number !== 'string') return null;
dl_number = dl_number.trim();
let match = dl_number.match(/^DL[-\s]?(\d+)$/i);
if (match && match[1]) {
return parseInt(match[1], 10);
}
return null;
}
function generateDL(rowId) {
console.log(`Generating DL for row ID ${rowId}`);
const selectedDate = getSelectedDate();
console.log("Selected date:", selectedDate);
$.ajax({
url: '/generate_dl',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
id: rowId,
date: selectedDate
}),
success: function(response) {
if (response.status === 'success') {
console.log('DL успешно сгенерирован:', response.dl_number);
var row = $(`#tableBody tr[data-id='${rowId}']`);
var dlCell = row.find('td:eq(4)');
var packCell = row.find('td:eq(6)'); // Столбец "Время упаковки"
var armaType = row.find('td:eq(7)').text().trim();
dlCell.text(response.dl_number)
.addClass('clickable')
.css('cursor', 'pointer')
.off('click') // Удаляем предыдущие обработчики
.on('click', function() {
copyToClipboard(response.dl_number);
$(this).addClass('copied');
setTimeout(() => {
$(this).removeClass('copied');
}, 200);
});
// Добавляем кнопку упаковки только если arma_type не исключен
const excludedArmaTypes = ["CFA", "VND", "SRC"];
if (!excludedArmaTypes.includes(armaType)) {
packCell.html('<button class="pack-button">Упаковать</button>')
.find('button')
.click(function() {
packItem(rowId);
});
}
// Обновляем поле initial-dl с текущим значением DL
loadCurrentDL();
} else {
alert('Ошибка при генерации DL: ' + response.message);
}
},
error: function(xhr, status, error) {
console.error('Ошибка при генерации DL:', status, error);
alert('Ошибка при генерации DL: ' + error);
}
});
}
function packItem(rowId) {
console.log(`Упаковка элемента для строки ID ${rowId}`);
const selectedDate = getSelectedDate();
console.log("Selected date:", selectedDate);
$.ajax({
url: '/pack_item',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
id: rowId,
date: selectedDate
}),
success: function(response) {
console.log("Ответ сервера:", response);
if (response.status === 'success') {
var row = $(`#tableBody tr[data-id='${rowId}']`);
row.addClass('packed-row');
var cell = row.find('td:eq(6)'); // Столбец "Время упаковки"
cell.text(response.pack_time);
updateStatistics();
// Удаляем кнопку "Упаковать", так как запись уже упакована
cell.find('button.pack-button').remove();
// Определяем год и месяц из `selectedDate`
const [year, month, day] = selectedDate.split('-').map(Number);
// Принудительно обновляем неупакованные дни для этого месяца
markUnpackedDays(year, month, true);
} else {
alert('Ошибка при упаковке: ' + response.message);
}
},
error: function(xhr, status, error) {
console.error("Ошибка AJAX:", xhr, status, error);
alert('Ошибка при упаковке: ' + error);
}
});
}
function areSimilarNumbers(num1, num2) {
// Очищаем строки от нецифровых символов и пробелов
const clean1 = num1.toString().replace(/\D/g, '').trim();
const clean2 = num2.toString().replace(/\D/g, '').trim();
// Если строки пустые или разной длины - не дубликаты
if (!clean1 || !clean2 || clean1.length !== clean2.length) {
return false;
}
// Проверяем на точное совпадение
return clean1 === clean2;
}
function checkDuplicates() {
console.log("Checking for duplicates...");
const rows = $('#tableBody tr');
// Очищаем предыдущие выделения
rows.find('td').removeClass('duplicate-highlight');
// Создаем массивы для хранения номеров и их индексов
const numbers = new Map();
const dl_numbers = new Map();
const serials = new Map();
// Собираем все номера и серийные номера
rows.each(function(index) {
const number = $(this).find('td:nth-child(3)').text().trim();
const serial = $(this).find('td:nth-child(4)').text().trim();
const dl_number = $(this).find('td:nth-child(5)').text().trim();
if (number) {
if (!numbers.has(number)) {
numbers.set(number, []);
}
numbers.get(number).push(index);
}
if (dl_number) {
if (!dl_numbers.has(dl_number)) {
dl_numbers.set(dl_number, []);
}
dl_numbers.get(dl_number).push(index);
}
if (serial) {
if (!serials.has(serial)) {
serials.set(serial, []);
}
serials.get(serial).push(index);
}
});
// Подсвечиваем дубликаты
let duplicatesFound = false;
dl_numbers.forEach((indices, dl_number) => {
if (dl_number.startsWith("DL")) {
if (indices.length > 1) {
indices.forEach(index => {
rows.eq(index).find('td:nth-child(5)').addClass('duplicate-highlight');
});
duplicatesFound = true;
}
}
});
// Проверяем номера
numbers.forEach((indices, number) => {
if (indices.length > 1) {
indices.forEach(index => {
rows.eq(index).find('td:nth-child(3)').addClass('duplicate-highlight');
});
duplicatesFound = true;
}
});
// // Проверяем грайвера
// dl_numbers.forEach((indices, dl_number) => {
// if (indices.length > 1) {
// indices.forEach(index => {
// rows.eq(index).find('td:nth-child(5)').addClass('duplicate-highlight');
// });
// duplicatesFound = true;
// }
// });
// Проверяем серийные номера
serials.forEach((indices, serial) => {
if (indices.length > 1) {
indices.forEach(index => {
rows.eq(index).find('td:nth-child(4)').addClass('duplicate-highlight');
});
duplicatesFound = true;
}
});
return duplicatesFound;
}
function appendNewRow(tableBody, rowData) {
console.log("Добавление новой строки:", rowData);
// Создаем элемент строки и присваиваем уникальный data-id
var tr = $('<tr>').attr('data-id', rowData.id);
// Если есть время упаковки и это не заголовок, добавляем класс 'packed-row'
if (rowData.pack_time && rowData.pack_time !== "Время упаковки") {
tr.addClass('packed-row');
}
// Добавляем ячейки в строку
tr.append($('<td>').text(rowData.row_number));
tr.append($('<td>').text(rowData.timestamp));
// Столбец "Номер" (Третий столбец, индекс 2)
var numberCell = $('<td>')
.text(rowData.number)
.addClass('editable') // Добавляем класс для редактирования
.attr('data-column', '2'); // Указываем индекс столбца
tr.append(numberCell);
tr.append($('<td>').text(rowData.serial));
// Столбец "Графа" (Пятый столбец, индекс 4)
var dlCell = $('<td>')
.text(rowData.dl_number || '')
.addClass('editable') // Добавляем класс для редактирования
.attr('data-column', '4'); // Указываем индекс столбца "Графа"
tr.append(dlCell);
// Добавляем data-value
var numericValue = parseDL(rowData.dl_number) || 0;
dlCell.attr('data-value', numericValue);
// Проверка и добавление кнопок или кликабельного текста для "Графа"
const excludedArmaTypes = ["CFA", "VND", "SRC"];
if (rowData.arma_type === 'RPA' && !excludedArmaTypes.includes(rowData.arma_type)) {
// Для типа АРМА 'RPA', проверяем наличие и формат dl_number
if (!rowData.dl_number || !rowData.dl_number.startsWith('DL')) {
// Если dl_number отсутствует или не начинается с 'DL', отображаем кнопку "Генерировать"
dlCell.html('<button class="generate-button">Генерировать</button>')
.find('button')
.click(function() {
generateDL(rowData.id); // Передаем уникальный ID строки
});
} else {
// Если dl_number присутствует и начинается с 'DL', делаем его кликабельным для копирования
dlCell.text(rowData.dl_number)
.addClass('clickable')
.css('cursor', 'pointer')
.on('click', function() {
copyToClipboard(rowData.dl_number);
$(this).addClass('copied');
setTimeout(() => {
$(this).removeClass('copied');
}, 200);
});
}
} else if (!excludedArmaTypes.includes(rowData.arma_type)) {
// Для других типов АРМА, которые не исключены, отображаем или скрываем кнопку
if (rowData.dl_number && rowData.dl_number.startsWith('DL')) {
dlCell.text(rowData.dl_number)
.addClass('clickable')
.css('cursor', 'pointer')
.on('click', function() {
copyToClipboard(rowData.dl_number);
$(this).addClass('copied');
setTimeout(() => {
$(this).removeClass('copied');
}, 200);
});
} else {
dlCell.text(rowData.dl_number || '');
}
} else {
// Для типов АРМА "CFA", "VND", "SRC" оставляем ячейку пустой или с текущим значением
dlCell.text(rowData.dl_number || '');
}
// Добавляем ячейку с именем отправителя
tr.append($('<td>').text(rowData.worker_name));
/**
* Обработка ячейки "Время упаковки" (pack_time)
*/
var packCell = $('<td>');
if (rowData.dl_number && rowData.dl_number.startsWith('DL') && !rowData.pack_time && !excludedArmaTypes.includes(rowData.arma_type)) {
// Если dl_number с 'DL', pack_time отсутствует и arma_type не исключен, показываем кнопку "Упаковать"
packCell.html('<button class="pack-button">Упаковать</button>')
.find('button')
.click(function() {
packItem(rowData.id); // Передаем уникальный ID строки
});
} else {
// Иначе отображаем время упаковки или оставляем пустым
packCell.text(rowData.pack_time || '');
}
tr.append(packCell);
// Добавляем ячейку с типом АРМА
tr.append($('<td>').text(rowData.arma_type || ''));
/**
* Добавление ячейки "Действие" с кнопкой "Удалить"
*/
var actionTd = $('<td>');
var deleteButton = $('<button>').text('Удалить').addClass('delete-button btn btn-danger btn-sm');
deleteButton.click(function() {
initiateDeleteConfirmation(rowData.id); // Используем новую функцию
});
actionTd.append(deleteButton);
tr.append(actionTd);
// Добавляем строку в тело таблицы
tableBody.append(tr);
/**
* Дополнительная обработка после добавления строки
*/
setTimeout(() => {
const dlValue = dlCell.text().trim();
const packValue = packCell.text().trim();
if (dlValue.startsWith('DL') && !packValue && !excludedArmaTypes.includes(rowData.arma_type)) {
console.log('Добавляем кнопку упаковки для DL:', dlValue);
packCell.html('<button class="pack-button">Упаковать</button>')
.find('button')
.click(function() {
packItem(rowData.id); // Передаем уникальный ID строки
});
}
// Проверяем дубликаты после добавления новой строки
checkDuplicates();
}, 100);
}
function bindEventHandlers() {
// Привязка обработчика для кнопки "Генерировать"
$('.generate-button').off('click').on('click', function() {
var rowId = $(this).closest('tr').data('id');
generateDL(rowId);
});
// Привязка обработчика для кнопки "Упаковать"
$('.pack-button').off('click').on('click', function() {
var rowId = $(this).closest('tr').data('id');
packItem(rowId);
});
// Привязка обработчика для кнопки "Удалить"
$('.delete-button').off('click').on('click', function() {
var rowId = $(this).closest('tr').data('id');
initiateDeleteConfirmation(rowId);
});
}
function updateTableContent(data, tableBody, highlightNew = false) {
tableBody.empty(); // Очищаем текущее содержимое таблицы
data.forEach(function(rowData, index) {
appendNewRow(tableBody, rowData);
if (highlightNew && index === data.length - 1) {
// Можно добавить подсветку новой строки, если нужно
}
});
checkDuplicates();
bindEventHandlers(); // Проверяем дубликаты после обновления таблицы
}
function scheduleNextUpdate() {
setTimeout(() => {
if (!isEditing) {
updateTable(false, false).then(() => {
scheduleNextUpdate();
});
} else {
scheduleNextUpdate();
}
}, 3000);
}
function initializeEventSource() {
if (eventSource) {
eventSource.close();
}
eventSource = new EventSource('/stream');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'update' && data.date === getSelectedDate()) {
// Обновляем таблицу
updateTable(true).then(() => {
// После обновления таблицы, обновляем подсветку дней
const currentDate = new Date(fpInstance.selectedDates[0]);
markUnpackedDays(currentDate.getFullYear(), currentDate.getMonth() + 1);
fetchDeviceStatuses();
});
}
};
eventSource.onerror = function(error) {
console.error('EventSource failed:', error);
eventSource.close();
setTimeout(initializeEventSource, 5000); // Попытка переподключения через 5 секунд
};
}
// Обработчики событий для таблицы
$(document).ready(function() {
updateTable();
updateStatistics(); // Убедитесь, что эта функция определена
initializeEventSource();
scheduleNextUpdate();
fetchDeviceStatuses();
loadCurrentDL();
// Обработчик сортировки
$('.sortable').click(function() {
const column = $(this).data('column');
const order = $(this).hasClass('asc') ? 'desc' : 'asc';
// Удаляем классы сортировки со всех заголовков
$('.sortable').removeClass('asc desc');
// Добавляем класс текущей сортировки
$(this).addClass(order);
// Сохраняем текущие параметры сортировки
globalSortColumn = column;
globalSortOrder = order;
// Сортируем таблицу
sortTable(column, order);
});
// Обработчик поиска
$('#searchButton').click(function() {
searchNumber();
});
$('#searchInput').keypress(function(e) {
if (e.which === 13) {
searchNumber();
}
});
$('#armaFilter').on('change', function() {
updateTable();
});
});
// Обработчик изменения даты
$('#datepicker').on('change', function() {
initializeEventSource();
updateTable();
});
// Обработчик экспорта в Excel
$(document).ready(function() {
$('.export-button').on('click', function(e) {
e.preventDefault();
// Получаем информацию о сортировке
const sortableColumn = $('.sortable.asc, .sortable.desc');
const sortColumn = sortableColumn.length ? sortableColumn.data('column') : '';
const sortOrder = $('.sortable.asc').length ? 'asc' : 'desc';
// Формируем URL
const date = getSelectedDate();
const queryParams = new URLSearchParams({
date: date,
sort_column: sortColumn || '',
sort_order: sortOrder
});
// Создаем и отправляем запрос
fetch(`/export_excel?${queryParams.toString()}`)
.then(response => {
if (response.ok) {
return response.blob();
}
throw new Error('Network response was not ok.');
})
.then(blob => {
// Создаем ссылку для скачивания
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `export_${date}.xlsx`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при экспорте файла');
});
});
});
$(document).ready(function() {
fpInstance = flatpickr("#datepicker", {
locale: "ru",
dateFormat: "Y-m-d",
defaultDate: "today",
onChange: function(selectedDates, dateStr, instance) {
updateTable();
const selected = selectedDates[0];
if (selected) {
markUnpackedDays(selected.getFullYear(), selected.getMonth() + 1);
}
},
onMonthChange: function(selectedDates, dateStr, instance) {
const selected = instance.currentYear && instance.currentMonth !== undefined
? new Date(instance.currentYear, instance.currentMonth, 1)
: new Date();
markUnpackedDays(selected.getFullYear(), selected.getMonth() + 1);
},
onDayCreate: function(dObj, dStr, fp, dayElem) {
const year = dayElem.dateObj.getFullYear();
const month = dayElem.dateObj.getMonth() + 1; // getMonth() возвращает 0-11
const day = dayElem.dateObj.getDate();
const key = `${year}-${String(month).padStart(2, '0')}`;
if (unpackedDaysCache[key] && unpackedDaysCache[key].has(day)) {
dayElem.style.backgroundColor = 'lightcoral';
dayElem.style.color = 'white';
} else {
// Сброс стилей для других дней
dayElem.style.backgroundColor = '';
dayElem.style.color = '';
}
}
});
// Инициализируем для текущего месяца
const today = new Date();
markUnpackedDays(today.getFullYear(), today.getMonth() + 1);
initializeEventSource();
updateTable();
updateStatistics();
fetchDeviceStatuses();
setInterval(fetchDeviceStatuses, 1000); // Интервал 1 секунду (скорректируйте при необходимости)
});
// Обработчик двойного клика для редактирования ячеек
$(document).on('dblclick', '.editable', function() {
if (isEditing) return; // Предотвращаем многократное редактирование
isEditing = true;
var td = $(this);
var currentValue = td.text();
var input = $('<input type="text">')
.val(currentValue)
.addClass('edit-input');
td.html(input);
input.focus();
input.on('blur', function() {
var newValue = $(this).val().trim();
var rowId = td.closest('tr').data('id');
var columnIndex = td.attr('data-column');
// Определяем URL для обновления в зависимости от столбца
let url;
let updateData = {
id: rowId,
new_value: newValue,
date: getSelectedDate()
};
if (columnIndex === '2') { // Столбец "Номер"
url = '/update_number';
} else if (columnIndex === '4') { // Столбец "Графа"
url = '/update_graf';
} else {
alert('Неподдерживаемый столбец для редактирования');
td.text(currentValue);
isEditing = false;
return;
}
$.ajax({
url: url,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(updateData),
success: function(response) {
if (response.status === 'success') {
td.text(newValue);
checkDuplicates();
} else {
alert('Ошибка при обновлении значения: ' + response.message);
td.text(currentValue);
}
isEditing = false;
},
error: function(xhr, status, error) {
alert('Ошибка при отправке данных: ' + error);
td.text(currentValue);
isEditing = false;
}
});
});
input.on('keypress', function(e) {
if (e.which === 13) { // Нажатие Enter
$(this).blur();
}
});
input.on('keydown', function(e) {
if (e.key === 'Escape') { // Нажатие Esc
td.text(currentValue);
isEditing = false;
}
});
});
function searchNumber() {
const searchValue = $('#searchInput').val().trim();
if (!searchValue) return;
// Удаляем предыдущую подсветку
$('.highlight-search').removeClass('highlight-search');
const rows = $('#tableBody tr');
let found = false;
rows.each(function() {
const numberCell = $(this).find('td:nth-child(3)');
const cellText = numberCell.text().trim();
if (cellText === searchValue) {
numberCell.addClass('highlight-search');
// Прокрутка к найденной строке
$('.table-container').animate({
scrollTop: $(this).offset().top - $('.table-container').offset().top + $('.table-container').scrollTop() - 100
}, 500);
found = true;
// Устанавливаем таймер на удаление подсветки через 3 секунды
setTimeout(() => {
numberCell.removeClass('highlight-search');
}, 3000);
// Выводим сообщение в консоль для отладки
console.log('Найдено совпадение:', cellText);
return false; // Прерываем цикл, так как нашли нужную строку
}
});
if (!found) {
alert('Точное совпадение номера не найдено');
}
}
function initiateDeleteConfirmation(rowId) {
isDeleting = true; // Устанавливаем флаг подтверждения удаления
var deleteCell = $(`tr[data-id='${rowId}'] td:last-child`);
// Проверяем, не находится ли уже в состоянии подтверждения
if (deleteCell.data('confirming')) {
return; // Если подтверждение уже запрошено, ничего не делаем
}
// Устанавливаем флаг подтверждения
deleteCell.data('confirming', true);
// Создаем контейнер для кнопок "Да" и "Нет"
var buttonsContainer = $('<div>').addClass('confirm-buttons');
var yesButton = $('<button>')
.text('Да')
.addClass('confirm-yes-button btn btn-success btn-sm me-2')
.click(function() {
deleteConfirmed(rowId);
});
var noButton = $('<button>')
.text('Нет')
.addClass('confirm-no-button btn btn-secondary btn-sm')
.click(function() {
cancelDeleteConfirmation(rowId);
});
buttonsContainer.append(yesButton);
buttonsContainer.append(noButton);
// Заменяем содержимое ячейки на контейнер с кнопками
deleteCell.empty();
deleteCell.append(buttonsContainer);
}
function deleteConfirmed(rowId) {
$.ajax({
url: '/delete_row',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ id: rowId }),
success: function(response) {
if (response.status === 'success') {
// Удаляем строку из таблицы
$(`tr[data-id='${rowId}']`).remove();
} else {
console.error('Ошибка при удалении записи:', response.message);
// Можно добавить уведомление пользователю о неудаче
}
// Восстанавливаем кнопку "Удалить" и сбрасываем флаг
cancelDeleteConfirmation(rowId);
},
error: function(xhr, status, error) {
console.error('Произошла ошибка при удалении записи:', error);
// Можно добавить уведомление пользователю о неудаче
// Восстанавливаем кнопку "Удалить" и сбрасываем флаг
cancelDeleteConfirmation(rowId);
}
});
}
function cancelDeleteConfirmation(rowId) {
var deleteCell = $(`tr[data-id='${rowId}'] td:last-child`);
// Сбрасываем флаг подтверждения
deleteCell.data('confirming', false);
// Восстанавливаем кнопку "Удалить"
deleteCell.empty();
var deleteButton = $('<button>')
.text('Удалить')
.addClass('delete-button btn btn-danger btn-sm')
.click(function() {
initiateDeleteConfirmation(rowId);
});
deleteCell.append(deleteButton);
isDeleting = false; // Сбрасываем глобальный флаг
}
function loadCurrentDL() {
const selectedDate = getSelectedDate();
$.ajax({
url: '/get_current_dl',
type: 'GET',
data: { date: selectedDate },
success: function(response) {
if (response.status === 'success') {
$('#initial-dl').val(response.current_dl);
} else {
$('#initial-dl').val(''); // Можно установить значение по умолчанию или показать сообщение
console.warn(response.message);
}
},
error: function(xhr, status, error) {
console.error('Ошибка при получении текущего DL:', error);
$('#initial-dl').val('');
}
});
}
function markUnpackedDays(year, month, force = false) {
const key = `${year}-${String(month).padStart(2, '0')}`;
if (!force && unpackedDaysCache[key] !== undefined) {
fpInstance.redraw(); // Перерисовываем календарь для применения стилей
return;
}
// Запрашиваем неупакованные дни для выбранного месяца и года
fetch(`/get_unpacked_days?year=${year}&month=${month}`)
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
unpackedDaysCache[key] = new Set(data.unpacked_days);
console.log(`Неупакованные дни для ${key}:`, unpackedDaysCache[key]);
} else {
console.error('Ошибка при получении неупакованных дней:', data.message);
unpackedDaysCache[key] = new Set(); // Инициализируем пустым множеством
}
fpInstance.redraw(); // Перерисовываем календарь после обновления данных
})
.catch(error => {
console.error('Ошибка при запросе неупакованных дней:', error);
unpackedDaysCache[key] = new Set(); // Инициализируем пустым множеством
fpInstance.redraw();
});
}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Рабочая таблица</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles_welcome.css') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
<script src="{{ url_for('static', filename='js/date.js') }}"></script>
<script src="{{ url_for('static', filename='js/statistics.js') }}"></script>
<!-- Убедитесь, что вы подключили jQuery UI для перетаскивания -->
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ru.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-sparklines/2.1.2/jquery.sparkline.min.js"></script>
</head>
<body>
<div class="header">
<h1>Рабочая таблица</h1>
<div class="user-info">
<span>Пользователь: {{ name }}</span>
<button onclick="toggleDeviceStatusWindow()" class="status-button">Статусы Устройств</button>
<a href="/logout" class="logout-button">Выход</a>
</div>
</div>
<div class="content-wrapper">
<!-- Основной контент -->
<div class="main-content">
<div class="statistics-container">
<div class="date-selector">
<input id="datepicker" type="text" placeholder="Выберите дату" class="flatpickr-input" readonly="readonly">
</div>
<div class="stat-box">
<span class="stat-label">Генераций:</span>
<span class="stat-value" id="generateClicks">0</span>
</div>
<div class="stat-box">
<span class="stat-label">Упаковано:</span>
<span class="stat-value" id="packClicks">0</span>
</div>
<div class="stat-box">
<span class="stat-label">Записей "RPA":</span>
<span class="stat-value" id="rpaRecords">0</span>
</div>
</div>
<div class="table-container">
<table border="1" id="dataTable">
<thead>
<tr>
<th class="sortable" data-column="0">№</th>
<th class="sortable" data-column="1">Время отправки</th>
<th class="sortable" data-column="2">Номер</th>
<th class="sortable" data-column="3">Serial</th>
<th class="sortable" data-column="4">Графа</th>
<th class="sortable" data-column="5">Имя отправителя</th>
<th class="sortable" data-column="6">Время упаковки</th>
<th class="sortable" data-column="7">Тип АРМА</th>
<th data-column="8">Действие</th>
</tr>
</thead>
<tbody id="tableBody">
{% for row in table[1:] %}
<tr data-id="{{ row.id }}">
{% for cell in row %}
<td class="{% if loop.index0 == 2 or loop.index0 ==3 or loop.index0 ==4 %}editable{% endif %}">{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="controls-wrapper">
<div class="search-container">
<input type="text" id="searchInput" placeholder="Поиск по номеру...">
<button id="searchButton">Найти</button>
</div>
<div class="controls-container">
<div class="dl-control">
<div class="dl-row">
<label for="initial-dl">Начать DL:</label>
<input type="text" id="initial-dl" placeholder="Значение">
</div>
<button onclick="setInitialDL()">Установить</button>
</div>
<div class="filter-container">
<label>
<input type="checkbox" id="armaFilter">
Показать только RPA
</label>
</div>
<div class="export-container">
<button class="export-button">Экспорт в Excel</button>
</div>
</div>
</div>
<div class="notification" id="notification"></div>
</div>
</div>
<button onclick="toggleDeviceStatusWindow()" class="status-button">Статусы Устройств</button>
<!-- Окно статусов устройств -->
<div id="deviceStatusWindow" class="device-status-window">
<div class="device-status-header ui-widget-header">
<h4>Статус устройств</h4>
<button onclick="toggleDeviceStatusWindow()" class="close-button">×</button>
</div>
<ul id="deviceStatusList">
{% for device in devices %}
<li>
<span class="status-circle" id="{{ device.name }}-status"></span>
<span class="device-name">{{ device.name }}</span>
<span class="status-text" id="{{ device.name }}-status-text">
{% if device.status == 'processing' %}
Процесс идет
{% elif device.status == 'copying' %}
Копирование
{% elif device.status == 'sending_number' %}
Отправка номера
{% elif device.status == 'online' %}
Онлайн
{% elif device.status == 'no_internet' %}
Нет интернета
{% else %}
Нет соединения
{% endif %}
</span>
<!-- Элемент для Sparkline -->
<span id="{{ device.name }}-sparkline" class="sparkline"></span>
</li>
{% endfor %}
</ul>
</div>
<script>
// Функция для отображения/скрытия мини-окна
function toggleDeviceStatusWindow() {
$('#deviceStatusWindow').toggleClass('show');
}
// Делает окно перетаскиваемым
$( function() {
$("#deviceStatusWindow").draggable({
handle: ".device-status-header"
});
} );
</script>
</body>
</html>