исправь экспорт в excel  по номеру работает но по колонке Графа где DL генерируется не работает экспорт и вся таблица тоже когда хочу экспортировать не работает. Сделай что то и исправь по своему
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_, func
import os, re
import logging
from logging.handlers import RotatingFileHandler


app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///table_data.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)



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))
    last_graf = 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)


# Удалена модель ActiveUser


tables = {}
table = [['№', 'Время отправки', 'Номер ', 'Serial', 'Графа', 'Имя отправителя','Время упаковки' ,'Тип АРМА', 'last_graf', 'Действие']]
# Добавляем счетчик для номеров записей
row_counter = 1
# Создаем очередь для событий
update_queue = queue.Queue()

@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:
                # Ожидаем новое сообщение с таймаутом 20 секунд
                data = update_queue.get(timeout=20)
                yield f'data: {data}\n\n'
            except queue.Empty:
                # Отправляем keep-alive комментарий, чтобы соединение не разорвалось
                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')  # Новый параметр фильтра

    # Начинаем построение запроса
    query = TableRow.query.filter_by(date=date)

    # Если фильтр активирован, добавляем условие
    if filter_arma == 'RPA':
        query = query.filter_by(arma_type='RPA')

    rows = query.all()
    data = []
    numbers = {}
    serials = {}
    dl_numbers = {}  # Добавляем словарь для dl_numbers

    for row in rows:
        data.append({
            'id': row.id,  # Добавляем 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,
            'last_graf': row.last_graf
        })

        # Собираем информацию о дубликатах
        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  # Добавляем 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)

    # Ранее здесь была логика обновления ActiveUser, удалим ее

    db.session.commit()

    # Отправляем сообщение в очередь для обновления клиентов
    update_queue.put(json.dumps({"type": "update", "date": date}))

    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()
            
            # Отправляем сообщение в очередь для обновления клиентов
            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  # Увеличиваем счетчик
            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('/get_statistics')
def get_statistics():
    try:
        date = request.args.get('date')
        current_time = datetime.now()
        hour_ago = current_time - timedelta(hours=1)

        # Общее количество упакованных записей
        total_records = TableRow.query.filter_by(date=date).filter(TableRow.pack_time.isnot(None)).count()

        # Количество записей за последний час
        last_hour_records = TableRow.query.filter_by(date=date)\
            .filter(TableRow.pack_time.isnot(None))\
            .filter(TableRow.pack_time >= hour_ago.strftime("%Y-%m-%d %H:%M:%S"))\
            .count()

        return jsonify({
            "total_records": total_records,
            "last_hour_records": last_hour_records
        })
    except Exception as e:
        return jsonify({
            "total_records": 0,
            "last_hour_records": 0,
            "error": str(e)
        })

import re
from sqlalchemy import func

@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')
        export_type = request.args.get('export_type')

        app.logger.debug(f"Получен запрос на экспорт: date={date}, sort_column={sort_column}, sort_order={sort_order}, export_type={export_type}")

        # Проверка обязательных параметров
        if not date:
            app.logger.error("Отсутствует обязательный параметр: date")
            return jsonify({"error": "Date parameter is required"}), 400

        # Начинаем построение запроса с фильтрации по дате
        query = TableRow.query.filter_by(date=date)
        app.logger.debug(f"Применён фильтр по дате: {date}")

        # Для типов экспорта 1 и 2 добавляем фильтр по arma_type='RPA'
        if export_type in ['1', '2']:
            query = query.filter_by(arma_type='RPA')
            app.logger.debug("Применён фильтр по arma_type='RPA'")

        # Обработка типа экспорта
        if export_type == '1':
            # Экспорт по диапазону номеров
            number_start = request.args.get('number_start')
            number_end = request.args.get('number_end')
            app.logger.debug(f"Тип экспорта 1: number_start={number_start}, number_end={number_end}")
            if not number_start or not number_end:
                app.logger.error("Не указаны начальный и конечный номера для экспорта по диапазону номеров")
                return jsonify({"error": "Необходимо указать начальный и конечный номера для экспорта по диапазону номеров."}), 400
            try:
                number_start = int(number_start)
                number_end = int(number_end)
                app.logger.debug(f"Преобразование диапазона номеров: start={number_start}, end={number_end}")
            except ValueError:
                app.logger.error("Номера должны быть целыми числами")
                return jsonify({"error": "Номера должны быть целыми числами."}), 400
            query = query.filter(
                cast(TableRow.number, Integer) >= number_start,
                cast(TableRow.number, Integer) <= number_end
            )
            app.logger.debug("Применён фильтр по диапазону номеров")
        elif export_type == '2':
            # Экспорт по диапазону DL (Граф)
            dl_start = request.args.get('dl_start')
            dl_end = request.args.get('dl_end')
            app.logger.debug(f"Тип экспорта 2: dl_start={dl_start}, dl_end={dl_end}")
            if not dl_start or not dl_end:
                app.logger.error("Не указаны начальный и конечный DL для экспорта по диапазону DL")
                return jsonify({"error": "Необходимо указать начальный и конечный DL для экспорта по диапазону DL."}), 400

            # Гибкое регулярное выражение для DL номеров
            pattern = r'^DL[-\s]?(\d{1,})$'
            match_start = re.match(pattern, dl_start, re.IGNORECASE)
            match_end = re.match(pattern, dl_end, re.IGNORECASE)
            if not match_start or not match_end:
                app.logger.error("DL номера не соответствуют формату DLXXXXXX или имеют неверный формат")
                return jsonify({"error": "DL номера должны начинаться с DL и содержать цифры."}), 400

            dl_start_num = int(match_start.group(1))
            dl_end_num = int(match_end.group(1))
            app.logger.debug(f"Преобразование DL диапазона: start_num={dl_start_num}, end_num={dl_end_num}")

            # Применяем фильтр по диапазону DL
            # Заменяем 'DL', '-', ' ' на пустую строку и приводим к Integer
            dl_number_clean = cast(
                func.replace(
                    func.replace(
                        func.replace(TableRow.dl_number, 'DL', ''),
                        '-', ''
                    ), 
                    ' ', ''
                ), 
                Integer
            )

            query = query.filter(
                dl_number_clean >= dl_start_num,
                dl_number_clean <= dl_end_num
            )
            app.logger.debug("Применён фильтр по диапазону DL")
        elif export_type == '3':
            # Экспорт всей таблицы для данной даты без дополнительных фильтров
            app.logger.debug("Тип экспорта 3: Экспорт всей таблицы")
            pass
        else:
            app.logger.error("Неверный тип экспорта")
            return jsonify({"error": "Неверный тип экспорта."}), 400

        # Для типов экспорта 1 и 2 добавляем дополнительные фильтры
        if export_type in ['1', '2']:
            query = query.filter(
                TableRow.dl_number.isnot(None),
                TableRow.dl_number != '',
                TableRow.pack_time.isnot(None),
                TableRow.pack_time != ''
            )
            app.logger.debug("Применены дополнительные фильтры по dl_number и 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,
                '8': TableRow.last_graf
            }
            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())
                app.logger.debug(f"Применён порядок сортировки: column={sort_column}, order={sort_order}")
            else:
                app.logger.error("Invalid sort column")
                return jsonify({"error": "Invalid sort column"}), 400

        rows = query.all()
        app.logger.info(f"Количество записей после фильтрации: {len(rows)}")

        # Формируем данные для экспорта
        data = []
        if export_type == '3':
            # Полный экспорт всех строк для даты
            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,
                    'last_graf': row.last_graf
                })
            app.logger.debug("Сформированы данные для полного экспорта")
        else:
            # Экспорт по диапазону номеров или DL с фильтрацией по dl_number и pack_time
            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,
                    'last_graf': row.last_graf
                })
            app.logger.debug("Сформированы данные для экспорта по диапазону")

        if not data:
            app.logger.warning("Нет записей для экспорта с заданными критериями")
            return jsonify({"error": "Нет записей для экспорта с заданными критериями."}), 400

        df = pd.DataFrame(data)
        app.logger.debug(f"Данные преобразованы в DataFrame с {len(df)} строками и {len(df.columns)} столбцами")

        # Создаём Excel файл
        output = BytesIO()
        with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
            df.to_excel(writer, index=False, sheet_name='Sheet1')
            app.logger.debug("Данные записаны в Excel файл")

            # Настраиваем форматирование
            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)
                app.logger.debug(f"Установлена ширина столбца '{col}' на {max_len} символов")

                # Форматирование числовых столбцов
                if col in ['Номер', 'Графа']:
                    num_format = workbook.add_format({'num_format': '0'})
                    worksheet.set_column(idx, idx, max_len, num_format)
                    app.logger.debug(f"Применён числовой формат для столбца '{col}'")

        output.seek(0)
        app.logger.info("Excel файл успешно создан и готов к отправке")

        return send_file(
            output,
            mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            as_attachment=True,
            download_name=f'export_{date}.xlsx'
        )

    except Exception as e:
        app.logger.exception("Произошла ошибка при экспорте в Excel")
        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='',
            last_graf=''
        )
        db.session.add(new_row)
        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


# Удален маршрут /register_user, так как он использовал ActiveUser


# Удален маршрут /get_active_users, так как он использовал ActiveUser


@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

        db.session.delete(row)
        db.session.commit()

        # Отправляем сообщение в очередь для обновления клиентов
        update_queue.put(json.dumps({"type": "delete", "date": row.date, "id": row.id}))

        return jsonify({'status': 'success'})
    except Exception as e:
        db.session.rollback()
        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('/')

    return render_template('welcome.html', name=name, worker=worker, table=table)


@app.route('/logout')
def logout():
    resp = make_response(redirect('/'))
    resp.delete_cookie('worker')
    resp.delete_cookie('name')
    return resp


file_handler = RotatingFileHandler('C:\\Users\\B-ZONE\\Desktop\\myprojectV2\\app.log', maxBytes=10240, backupCount=10)
file_handler.setLevel(logging.DEBUG)  # Уровень логирования
formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)

app.logger.setLevel(logging.DEBUG)
app.logger.info('Flask-приложение запущено')

if __name__ == '__main__':
    with app.app_context():
        db.create_all()

    logging.disable(logging.CRITICAL)
    os.system('title СЕРВЕР')
    app.run(host='192.168.0.234', 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 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 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();
    // Регулярное выражение, поддерживающее DL123456, DL-123456, DL 123456
    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();
            } 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) {
   

    // Создаем элемент строки и присваиваем уникальный 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 || ''));

    // Добавляем ячейку с последней графой (last_graf)
    tr.append($('<td>').text(rowData.last_graf || ''));

    /**
     * Добавление ячейки "Действие" с кнопкой "Удалить"
     */
    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) {
            // Можно добавить подсветку новой строки, если нужно
            // Например:
            // $('tr:last').addClass('highlight-new').fadeOut(2000, function() {
            //     $(this).removeClass('highlight-new');
            // });
        }
    });
    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(() => {
                // После обновления таблицы, обновляем подсветку дней
                markUnpackedDays();
            });
        }
    };

    eventSource.onerror = function(error) {
        console.error('EventSource failed:', error);
        eventSource.close();
        setTimeout(initializeEventSource, 5000);  // Попытка переподключения через 5 секунд
    };
}
// Обработчики событий для таблицы
$(document).ready(function() {
    initializeDateSelectors(); // Убедитесь, что эта функция определена
    updateTable();
    updateStatistics(); // Убедитесь, что эта функция определена
    initializeEventSource();
    scheduleNextUpdate();
    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('Ошибка при экспорте файла');
        });
    });
});
*/
// Инициализация выбора даты с помощью flatpickr
let currentDate;

$(document).ready(function() {
    flatpickr("#datepicker", {
        dateFormat: "Y-m-d",
        defaultDate: "today",
        onChange: function(selectedDates, dateStr, instance) {
            currentDate = dateStr;
            updateTable();
        }
    });

    currentDate = new Date().toISOString().split('T')[0];
    initializeEventSource();
    updateTable();
    updateStatistics();
});

// Обработчик двойного клика для редактирования ячеек
$(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);

    // Заменяем кнопку "Удалить" на кнопки "Да" и "Нет"
    deleteCell.empty();

    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);
        });

    deleteCell.append(yesButton);
    deleteCell.append(noButton);
}


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('');
        }
    });
}

var rowId = td.closest('tr').data('id');  // Получаем 'id' строки

let updateData = {
    id: rowId,  // ОТПРАВЛЯЕМ 'id'
    new_value: newValue,
    date: getSelectedDate()
};

$.ajax({
    url: url,
    type: 'POST',
    contentType: 'application/json',
    data: JSON.stringify(updateData),
});
function updateStatistics() {
    const selectedDate = getSelectedDate();
    $.ajax({
        url: '/get_statistics',
        type: 'GET',
        data: { date: selectedDate },
        success: function(response) {
            $('#totalRecords').text(response.total_records);
            $('#lastHourRecords').text(response.last_hour_records);
        },
        error: function(xhr, status, error) {
            console.error("Error fetching statistics:", error);
            $('#totalRecords').text('Error');
            $('#lastHourRecords').text('Error');
        }
    });
}

function setInitialDL() {
    const initialNumber = document.getElementById('initial-dl').value;
    const selectedDate = getSelectedDate();
    
    console.log("Setting initial DL:", {
        initialNumber: initialNumber,
        date: selectedDate
    });

    if (!initialNumber) {
        alert('Пожалуйста, введите начальное значение DL');
        return;
    }

    $.ajax({
        url: '/set_initial_dl',
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify({
            initial_number: initialNumber,
            date: selectedDate
        }),
        success: function(response) {
            console.log("Server response:", response);
            if (response.status === 'success') {
                alert('Начальное значение DL успешно установлено');
                loadCurrentDL(); // Обновляем отображение текущего DL
            } else {
                alert('Ошибка: ' + (response.message || 'Неизвестная ошибка'));
            }
        },
        error: function(xhr, status, error) {
            console.error("AJAX error:", {xhr, status, error});
            alert('Ошибка при отправке данных: ' + error);
        }
    });
}


$(document).ready(function() {
    // Открытие модального окна
    $('#openExportModal').on('click', function() {
        $('#exportModal').css('display', 'block');
    });

    // Закрытие модального окна при нажатии на кнопку "Отмена" или "x"
    $('.close, .cancel-button').on('click', function() {
        $('#exportModal').css('display', 'none');
        $('#exportForm')[0].reset();
        $('.export-options').hide();
        // Показать по умолчанию первый набор полей
        $('#numberRangeInputs').show();
    });

    // Переключение видимости полей ввода в зависимости от выбранного типа экспорта
    $('input[name="exportType"]').on('change', function() {
        const selectedType = $(this).val();
        $('.export-options').hide();
        if (selectedType === '1') {
            $('#numberRangeInputs').show();
        } else if (selectedType === '2') {
            $('#dlRangeInputs').show();
        } else if (selectedType === '3') {
            // Нет дополнительных полей для экспорта всей таблицы
        }
    });

    // Обработка отправки формы экспорта
    $('#exportForm').on('submit', function(e) {
        e.preventDefault();
    
        const exportType = $('input[name="exportType"]:checked').val();
        const date = getSelectedDate();
        let queryParams = {
            date: date,
            sort_column: globalSortColumn || '',
            sort_order: globalSortOrder || 'asc',
            export_type: exportType
        };
    
        if (exportType === '1') {
            const numberStart = $('#numberStart').val();
            const numberEnd = $('#numberEnd').val();
            if (numberStart && numberEnd) {
                queryParams.number_start = numberStart;
                queryParams.number_end = numberEnd;
            }
        } else if (exportType === '2') {
            const dlStart = $('#dlStart').val();
            const dlEnd = $('#dlEnd').val();
            if (dlStart && dlEnd) {
                queryParams.dl_start = dlStart;
                queryParams.dl_end = dlEnd;
            }
        }
    
        // Формируем URL с параметрами
        const url = `/export_excel?${new URLSearchParams(queryParams).toString()}`;
    
        // Создаём и отправляем запрос для скачивания файла
        window.location.href = url;
    
        // Закрываем модальное окно
        $('#exportModal').css('display', 'none');
        $('#exportForm')[0].reset();
        $('.export-options').hide();
        // Показать по умолчанию первый набор полей
        $('#numberRangeInputs').show();
    });

    // Закрытие модального окна при клике вне его содержимого
    $(window).on('click', function(event) {
        if ($(event.target).is('#exportModal')) {
            $('#exportModal').css('display', 'none');
            $('#exportForm')[0].reset();
            $('.export-options').hide();
            // Показать по умолчанию первый набор полей
            $('#numberRangeInputs').show();
        }
    });
});
<!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>
    <script src="{{ url_for('static', filename='js/connectedUser.js') }}"></script>
</head>
<body>
    <div class="header">
        <h1>Рабочая таблица</h1>
        <div class="user-info">
            <span>Пользователь: {{ name }}</span>
            <a href="/logout" class="logout-button">Выход</a>
        </div>
    </div>

    <div class="content-wrapper">
        <div class="active-users-box">
            <h4>Активные пользователи:</h4>
            <table id="activeUsersTable">
                <thead>
                    <tr>
                        <th>Имя</th>
                        <th>Статус</th>
                        <th>Записей сегодня</th>
                    </tr>
                </thead>
                <tbody id="activeUsersBody">
                </tbody>
            </table>
        </div>
        
        <div class="date-selector">
            <select id="day">
             
            </select>
            <select id="month">
                <option value="01">Январь</option>
                <option value="02">Февраль</option>
                <option value="03">Март</option>
                <option value="04">Апрель</option>
                <option value="05">Май</option>
                <option value="06">Июнь</option>
                <option value="07">Июль</option>
                <option value="08">Август</option>
                <option value="09">Сентябрь</option>
                <option value="10">Октябрь</option>
                <option value="11">Ноябрь</option>
                <option value="12">Декабрь</option>
            </select>
            <select id="year">
                <!-- JavaScript заполнит годы -->
            </select>
        </div>
        

        <div class="statistics-container">
            <div class="stat-box">
                <span class="stat-label">Всего записей:</span>
                <span class="stat-value" id="totalRecords">0</span>
            </div>
            <div class="stat-box">
                <span class="stat-label">Записей за последний час:</span>
                <span class="stat-value" id="lastHourRecords">0</span>
            </div>
        </div>
        <div class="search-container">
            <input type="text" id="searchInput" placeholder="Поиск по номеру...">
            <button id="searchButton">Найти</button>
        </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 class="sortable" data-column="8">last_graf</th>
                        <th>Действие</th>
                    </tr>
                </thead>
                
                
                
                
                <tbody id="tableBody">
                    {% for row in table[1:] %}
                        <tr>
                            {% for cell in row %}
                                <td class="{% if loop.index0 == 2 %}editable{% endif %}">{{ cell }}</td>
                            {% endfor %}
                        </tr>
                    {% endfor %}
                </tbody>
            </table>
        </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="export-container">
                <button class="export-button" id="openExportModal">Экспорт в Excel</button>
            </div>
            
        </div>
        <div class="notification" id="notification"></div>
        <div class="filter-container">
            <label>
                <input type="checkbox" id="armaFilter">
                Показать только RPA
            </label>
        </div>
    </div>
    <!-- Модальное окно для экспорта в Excel -->
    <div id="exportModal" class="modal">
        <div class="modal-content">
            <span class="close">&times;</span>
            <h2>Экспортировать в Excel</h2>
            <form id="exportForm">
                <div>
                    <input type="radio" id="exportType1" name="exportType" value="1" checked>
                    <label for="exportType1">Экспорт по диапазону номеров</label>
                </div>
                <div id="numberRangeInputs" class="export-options">
                    <label for="numberStart">Начальный номер:</label>
                    <input type="number" id="numberStart" name="numberStart" required>
                    <label for="numberEnd">Конечный номер:</label>
                    <input type="number" id="numberEnd" name="numberEnd" required>
                </div>
                <div>
                    <input type="radio" id="exportType2" name="exportType" value="2">
                    <label for="exportType2">Экспорт по диапазону DL (Граф)</label>
                </div>
                <div id="dlRangeInputs" class="export-options" style="display:none;">
                    <label for="dlStart">Начальный DL:</label>
                    <input type="text" id="dlStart" name="dlStart" placeholder="Например, DL000001">
                    <label for="dlEnd">Конечный DL:</label>
                    <input type="text" id="dlEnd" name="dlEnd" placeholder="Например, DL000100">
                </div>
                <div>
                    <input type="radio" id="exportType3" name="exportType" value="3">
                    <label for="exportType3">Экспорт всей таблицы</label>
                </div>
                <div class="modal-actions">
                    <button type="submit" class="confirm-export-button">Экспортировать</button>
                    <button type="button" class="cancel-button">Отмена</button>
                </div>
            </form>
        </div>
    </div>
</body>
</html>