сделай мне календарь мой красивым 
фласк код
from flask import Flask, render_template, request, redirect, make_response, jsonify, send_file, Response
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, timedelta
import pandas as pd
from io import BytesIO
import json
import queue

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


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)



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



@app.route('/stream')
def stream():
    def event_stream():
        while True:
            message = update_queue.get()  # Блокирует до получения сообщения
            yield f"data: {message}\n\n"
    
    return Response(event_stream(), content_type='text/event-stream')


@app.route('/get_table_data')
def get_table_data():
    date = request.args.get('date')
    rows = TableRow.query.filter_by(date=date).all()
    data = []
    numbers = {}
    serials = {}

    for row in rows:
        data.append([
            row.row_number,
            row.timestamp,
            row.number,
            row.serial,
            row.dl_number,
            row.worker_name,
            row.pack_time,
            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]

    # Фильтруем только реальные дубликаты
    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}

    return jsonify({
        "data": data,
        "duplicates": {
            "numbers": duplicate_numbers,
            "serials": duplicate_serials
        }
    })




@app.route('/update_table', methods=['POST'])
def update_table():
    data = request.json
    date = data['date']
    
    # Получаем количество существующих записей для этой даты
    row_count = TableRow.query.filter_by(date=date).count() + 1
    
    new_row = TableRow(
        date=date,
        row_number=str(row_count),
        timestamp=data['timestamp'],
        number=data['number'],
        serial=data['serial'],
        worker_name=data['worker_name'],
        last_graf=data.get('last_graf', '')
    )
    
    db.session.add(new_row)
    db.session.commit()
    
    # Отправляем сообщение в очередь для обновления клиентов
    update_queue.put(json.dumps({"type": "update", "date": date}))
    
    return jsonify({"status": "success"})

@app.route('/update_number', methods=['POST'])
def update_number():
    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.number = 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)})

@app.route('/pack_item', methods=['POST'])
def pack_item():
    try:
        data = request.json
        row_index = data['row_index']
        date = data['date']
        
        print(f"Received date: {date}")  # Отладочный вывод
        print(f"Received row_index: {row_index}")  # Отладочный вывод
        
        row = TableRow.query.filter_by(date=date).offset(row_index).first()
        if row:
            pack_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            row.pack_time = pack_time
            db.session.commit()
            return jsonify({"status": "success", "pack_time": pack_time})
        
        return jsonify({"status": "error", "message": "Row not found"})
    except Exception as e:
        print(f"Error in pack_item: {str(e)}")  # Отладочный вывод
        return jsonify({"status": "error", "message": str(e)})


@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_index = data['row_index']
        date = data['date']
        
        # Получаем или создаем счетчик DL для данной даты
        dl_counter = DLCounter.query.filter_by(date=date).first()
        if not dl_counter:
            return jsonify({"status": "error", "message": "DL counter not initialized"})
        
        # Генерируем новый DL номер
        dl_number = f"DL{dl_counter.counter:06d}"
        
        # Находим и обновляем нужную запись
        row = TableRow.query.filter_by(date=date).offset(row_index).first()
        if row:
            row.dl_number = dl_number
            dl_counter.counter += 1  # Увеличиваем счетчик
            db.session.commit()
            return jsonify({"status": "success", "dl_number": dl_number})
        
        return jsonify({"status": "error", "message": "Row not found"})
    except Exception as e:
        return jsonify({"status": "error", "message": str(e)})

    

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



@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
        
        # Получаем все записи для выбранной даты
        query = TableRow.query.filter_by(date=date)
        
        # Применяем сортировку, если указана
        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.last_graf
            }
            if sort_column in column_mapping:
                column = column_mapping[sort_column]
                if sort_order == 'desc':
                    query = query.order_by(column.desc())
                else:
                    query = query.order_by(column.asc())
        
        rows = query.all()
        
        # Создаем DataFrame
        data = []
        for row in rows:
            data.append({
                '№': row.row_number,
                'Время отправки': row.timestamp,
                'Номер': row.number,
                'Serial': row.serial,
                'Графа': row.dl_number,
                'Имя отправителя': row.worker_name,
                'Время упаковки': row.pack_time,
                'last_graf': row.last_graf
            })
        
        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)
        
        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:
        # Создаем первую запись с заголовками
        first_row = TableRow(
            date=date,
            row_number="№",
            timestamp="Время отправки",
            number="Номер",
            serial="Serial",
            dl_number="Графа",
            worker_name="Имя отправителя",
            pack_time="Время упаковки",
            last_graf="last_graf"
        )
        db.session.add(first_row)
        db.session.commit()



@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('/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

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

    app.debug = True
    app.run(host='0.0.0.0', port=80, debug=True)
html код
<!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') }}">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></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>
</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="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="search-container">
            <input type="text" id="searchInput" placeholder="Поиск по номеру...">
            <button id="searchButton">Найти</button>
        </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="table-container">
            <table border="1" id="dataTable">
                <thead>
                    <tr>
                        {% for header in table[0] %}
                            {% if header in ["Имя отправителя", "Время создания", "Время упаковки"] %}
                                <th class="sortable" data-column="{{ loop.index0 }}">{{ header }}</th>
                            {% elif loop.index0 == 1 %}
                                <th class="sortable" data-column="1">{{ header }}</th>
                            {% elif loop.index0 == 2 %}
                                <th class="sortable" data-column="2">{{ header }}</th>
                            {% elif loop.index0 == 6 %}  <!-- Индекс столбца "Время упаковки" -->
                                <th class="sortable" data-column="6">{{ header }}</th>
                            {% else %}
                                <th>{{ header }}</th>
                            {% endif %}
                        {% endfor %}
                    </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">
                <label for="initial-dl">Начать DL:</label>
                <input type="text" id="initial-dl">
                <button onclick="setInitialDL()">Установить</button>
            </div>
            <div class="export-container">
                <button class="export-button">Экспорт в Excel</button>
            </div>
            
        </div>
    </div>
</body>
</html>
css код
:root {
    --primary-color: #5c5c5c;
    --secondary-color: #2b2b2b;
    --background-color: #ecf0f1;
    --text-color: #34495e;
    --hover-color: #e74c3c;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
/* Центрирование контейнера */
body {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0 auto;
    font-family: Arial, sans-serif;
    background-color: #f4f4f9;
}

.content-wrapper {
    max-width: 1200px;
    margin: 80px auto 20px;
    padding: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
}

.table-container {
    text-align: center;
    margin-top: -120px;
    margin-right: 120px;
    height: 600px;
    overflow-y: auto;
    overflow-x: hidden;
}

/* Оформление заголовка */
h3 {
    margin: 0;
    padding-bottom: 60px;
    font-size: 30px;
    color: #333;
}

/* Стиль таблицы */
table {
    border-collapse: collapse;
    width: 100%;
    max-width: 1100px;
    margin: 0 auto;
    background-color: #fff;
    box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
}

th {
    width: 400px;
    padding: 10px;
    border: 1px solid #ddd;
    text-align: center;
    background-color: #71d693;
    color: rgb(0, 0, 0);
    height: 30px;
}

td {
    height: 50px;
    padding: 10px;
    border: 1px solid #ddd;
    text-align: center;
    vertical-align: middle;
    min-width: 120px;
}


tbody tr:nth-child(even) {
    background-color: #f9f9f9;
}

tbody tr:hover {
    background-color: #e0f7fa;
}

.editable {
    cursor: pointer;
}

.editable:hover {
    background-color: #f0f0f0;
}

.edit-input {
    width: 90%;
    height: 80%;
    padding: 5px;
    border: 1px solid #4CAF50;
    border-radius: 4px;
    text-align: center;
}

.edit-input:focus {
    outline: none;
    border-color: #45a049;
    box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
}

.dl-control {
    position: relative;
    left: 800px;
    background-color: #f8f8f8;
    padding: 15px;
    border-radius: 4px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}


.dl-control input {
    width: 100%;
    padding: 8px;
    margin: 8px 0;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.dl-control button, .export-button {
    width: 100%;
    padding: 8px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.dl-control button:hover, .export-button:hover {
    background-color: #45a049;
}

.dl-control label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
}

.controls-container {
    display: flex;
    justify-content: space-between;
    width: 100%;
    gap: 20px;
    
}


/* Стиль для эффекта копирования */
.copied {
    background-color: #e8f5e9 !important;
    transition: background-color 0.2s;
}

/* Стили для уведомления */
.notification {
    position: fixed;
    bottom: 20px;
    right: 20px;
    background-color: #333;
    color: white;
    padding: 10px 20px;
    border-radius: 4px;
    z-index: 1000;
    animation: fadeIn 0.3s, fadeOut 0.3s 1.7s;
}

@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes fadeOut {
    from {
        opacity: 1;
        transform: translateY(0);
    }
    to {
        opacity: 0;
        transform: translateY(20px);
    }
}

/* Стиль для ячеек с DL номером */
td[onclick] {
    cursor: pointer;
}

td[onclick]:hover {
    background-color: #f5f5f5;
}


.statistics-container {
    display: flex;
    position: fixed;
    right: 0;
    top: 60px;
    justify-content: flex-end;
    gap: 20px;
    padding: 15px;
    margin-bottom: 20px;
}

.stat-box {
    background-color: #f8f9fa;
    padding: 10px 15px;
    border-radius: 6px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.stat-label {
    font-weight: 500;
    color: #666;
    margin-right: 8px;
}

.stat-value {
    font-weight: 600;
    color: #2196F3;
}

.export-container {
    text-align: center;
    display: fixed;
    position: absolute;
    top: 740px;
    right: 1150px;
    margin-top: 20px;
}

.export-button {
    display: inline-block;
    padding: 10px 20px;
    background-color: #4CAF50;
    color: white;
    text-decoration: none;
    border-radius: 4px;
    font-weight: bold;
    transition: background-color 0.3s;
}

.export-button:hover {
    background-color: #45a049;
}

.duplicate-highlight {
    background-color: #ffcad1 !important;
    transition: background-color 0.3s;
}

.editable {
    cursor: pointer;
}


.date-selector {
    margin: 20px 0;
    padding: 10px;
    left: 0;
    top:50px;
    background-color: #f5f5f5;
    border-radius: 5px;
    display: flex;
    position: absolute;
    gap: 10px;
    justify-content: center;
}

.date-selector select {
    padding: 5px;
    font-size: 16px;
    border: 1px solid #ddd;
    border-radius: 4px;
    background-color: white;
}

.date-selector select:focus {
    outline: none;
    border-color: #007bff;
}


.packed-row td {
    background-color: #e8f5e9 !important; /* светло-зеленый цвет */
}


.sortable {
    cursor: pointer;
    position: relative;
}

.sortable:after {
    content: '↕️';
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 12px;
}

.sortable.asc:after {
    content: '↑';
}

.sortable.desc:after {
    content: '↓';
}


/* Стиль для кнопки "Генерировать" */
td button {
    padding: 8px 15px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-weight: 500;
    transition: background-color 0.3s;
}

/* Специфичный стиль для кнопки "Генерировать" */
td button:contains('Генерировать') {
    background-color: #007bff; /* синий цвет */
    color: white;
}

td button:contains('Генерировать'):hover {
    background-color: #0056b3; /* более темный синий при наведении */
}

/* Стиль для кнопки "Упаковать" остается зеленым */
td button:contains('Упаковать') {
    background-color: #4CAF50;
    color: white;
}

td button:contains('Упаковать'):hover {
    background-color: #45a049;
}


/* Стиль для кнопки "Генерировать" */
.generate-button {
    padding: 8px 15px;
    background-color: #007bff !important; /* синий цвет */
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-weight: 500;
    transition: background-color 0.3s;
}

.generate-button:hover {
    background-color: #0056b3 !important; /* более темный синий при наведении */
}

.header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    background-color: #fff;
    padding: 10px 20px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    display: flex;
    justify-content: space-between;
    align-items: center;
    z-index: 1000;
}

.header h1 {
    margin: 0;
    font-size: 24px;
    color: #333;
}

.user-info {
    display: flex;
    align-items: center;
    gap: 20px;
}

.logout-button {
    padding: 8px 15px;
    background-color: #dc3545;
    color: white;
    text-decoration: none;
    border-radius: 4px;
    transition: background-color 0.3s;
}

.logout-button:hover {
    background-color: #c82333;
}

.search-container {
    position: absolute;
    top: 750px;
    right: 780px;
    display: flex;
    gap: 10px;
    padding: 10px;
    background-color: #f5f5f5;
    border-radius: 5px;
}

#searchInput {
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
    width: 200px;
}

#searchButton {
    padding: 8px 15px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

#searchButton:hover {
    background-color: #45a049;
}

.highlight-search {
    background-color: yellow !important;
    transition: background-color 0.3s ease;
}



.pack-button {
    background-color: #4CAF50; /* Зеленый цвет */
    color: white; /* Белый текст */
    border: none; /* Без границы */
    padding: 10px 20px; /* Отступы */
    text-align: center; /* Центрирование текста */
    text-decoration: none; /* Без подчеркивания */
    display: inline-block; /* Инлайн-блок для правильного отображения */
    font-size: 12px; /* Размер шрифта */
    margin: 4px 2px; /* Отступы */
    cursor: pointer; /* Указатель при наведении */
    border-radius: 5px; /* Закругленные углы */
    transition: background-color 0.3s; /* Плавный переход цвета */
}

.pack-button:hover {
    background-color: #45a049; /* Темнее при наведении */
}


.new-highlight {
    animation: highlightFade 3s;
}

@keyframes highlightFade {
    from { background-color: #ffff99; }
    to { background-color: transparent; }
}

.duplicate-highlight {
    background-color: #ffcad1 !important; /* Светло-красный цвет для дубликатов */
    transition: background-color 0.3s;
}

table,js

let isEditing = false;
let searchTimeout;
let lastUpdateTime = 0;
const updateInterval = 1000;
let eventSource;
let lastData = null;




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

        $.ajax({
            url: '/get_table_data',
            type: 'GET',
            data: { date: getSelectedDate() },
            success: function(response) {
                var tableBody = $('#tableBody');
                var currentSortColumn = $('.sortable.asc, .sortable.desc').data('column');
                var currentSortOrder = $('.sortable.asc').length ? 'asc' : 'desc';
                
                // Обновляем данные таблицы
                updateTableContent(response.data, tableBody, highlightNew);

                // Восстанавливаем сортировку
                if (currentSortColumn !== undefined) {
                    sortTable(currentSortColumn, currentSortOrder);
                }

                updateStatistics();
                lastUpdateTime = now;

                // Проверяем дубликаты, используя данные с сервера
                highlightDuplicates(response.duplicates);
            }
        });
    })
    }

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





function updateCell(cell, newValue, rowIndex, cellIndex) {
    console.log(`Updating cell: index=${cellIndex}, value=${newValue}, rowIndex=${rowIndex}`);

    if (cellIndex === 4) { // Столбец "Графа"
        if (newValue && newValue.startsWith('DL')) {
            cell.text(newValue)
                .css('cursor', 'pointer')
                .off('click')
                .on('click', function() {
                    copyToClipboard(newValue);
                    $(this).addClass('copied');
                    setTimeout(() => {
                        $(this).removeClass('copied');
                    }, 200);
                });

            // После установки DL номера сразу проверяем и обновляем ячейку времени упаковки
            setTimeout(() => {
                const packTimeCell = cell.parent().find('td').eq(6);
                const packTime = packTimeCell.text().trim();
                if (!packTime) {
                    console.log('Adding pack button for DL:', newValue);
                    packTimeCell.html('<button class="pack-button">Упаковать</button>')
                        .find('button')
                        .click(function() {
                            packItem(rowIndex);
                        });
                }
            }, 0);
        } else {
            cell.html('<button class="generate-button">Генерировать</button>')
                .find('button')
                .click(function() {
                    generateDL(rowIndex);
                });
        }
    } else if (cellIndex === 6) { // Столбец "Время упаковки"
        const dlCell = cell.parent().find('td').eq(4);
        const dlValue = dlCell.text().trim();
        console.log(`Processing pack time cell. DL value: ${dlValue}, Current value: ${newValue}`);

        if (newValue && newValue !== '') {
            cell.text(newValue);
        } else if (dlValue && dlValue.startsWith('DL')) {
            console.log('Adding pack button for existing DL');
            cell.html('<button class="pack-button">Упаковать</button>')
                .find('button')
                .click(function() {
                    packItem(rowIndex);
                });
        } else {
            cell.empty(); // Очищаем ячейку, если нет DL номера
        }
    } else {
        cell.text(newValue)
            .toggleClass('editable', cellIndex === 2 || cellIndex === 3)
            .attr('data-column', cellIndex);
    }
}



function appendNewRow(tableBody, rowData) {
    console.log("Adding new row:", rowData);
    var tr = $('<tr>');
    
    // Проверяем наличие времени упаковки
    if (rowData[6]) {
        tr.addClass('packed-row');
    }
    
    // Добавляем все ячейки
    rowData.forEach(function(cellValue, index) {
        var td = $('<td>');
        tr.append(td);
        
        // Используем setTimeout, чтобы гарантировать, что строка уже добавлена в DOM
        setTimeout(() => {
            updateCell(td, cellValue, tr.index(), index);
        }, 0);
    });
    
    tableBody.append(tr);
    
    // Дополнительная проверка после добавления строки
    setTimeout(() => {
        const dlCell = tr.find('td').eq(4);
        const packCell = tr.find('td').eq(6);
        const dlValue = dlCell.text().trim();
        const packValue = packCell.text().trim();
        
        if (dlValue.startsWith('DL') && !packValue) {
            console.log('Post-check: Adding pack button for DL:', dlValue);
            packCell.html('<button class="pack-button">Упаковать</button>')
                .find('button')
                .click(function() {
                    packItem(tr.index());
                });
        }
    }, 100);
}





function sortTable(column, order) {
    const rows = $('#tableBody > tr').get();
    const originalNumbers = rows.map((row, index) => ({
        row: row,
        originalNumber: $(row).children('td').first().text()
    }));

    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;

        // Специальная обработка для столбцов с датами (время отправки и время упаковки)
        if (column === 1 || column === 6) {
            // Преобразуем строки дат в объекты Date
            A = new Date(A.replace(/(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2}):(\d{2})/, '\$3-\$2-\$1T\$4:\$5:\$6'));
            B = new Date(B.replace(/(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2}):(\d{2})/, '\$3-\$2-\$1T\$4:\$5:\$6'));
            return order === 'asc' ? A - B : B - A;
        }

        // Остальной код сортировки остается без изменений...
        if (column === 5) {
            A = A.toLowerCase();
            B = B.toLowerCase();
        }

        if (!isNaN(A) && !isNaN(B)) {
            A = parseFloat(A);
            B = parseFloat(B);
        } else {
            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();
    rows.forEach((row, index) => {
        const originalData = originalNumbers.find(item => item.row === row);
        $(row).children('td').first().text(originalData.originalNumber);
        $('#tableBody').append(row);
    });
    checkDuplicates();
}



function generateDL(rowIndex) {
    $.ajax({
        url: '/generate_dl',
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify({
            row_index: rowIndex,
            date: getSelectedDate()
        }),
        success: function(response) {
            if (response.status === 'success') {
                console.log('DL generated successfully:', response.dl_number);
                var row = $(`#tableBody tr:eq(${rowIndex})`);
                var dlCell = row.find('td:eq(4)');
                var packCell = row.find('td:eq(6)');
                
                dlCell.text(response.dl_number);
                
                // Добавляем кнопку упаковки
                console.log('Adding pack button after DL generation');
                packCell.html('<button class="pack-button">Упаковать</button>')
                    .find('button')
                    .click(function() {
                        packItem(rowIndex);
                    });
            } else {
                alert('Ошибка при генерации DL');
            }
        },
        error: function(xhr, status, error) {
            console.error('Error generating DL:', error);
            alert('Ошибка при генерации DL: ' + error);
        }
    });
}



function packItem(rowIndex) {
    const selectedDate = getSelectedDate();
            console.log("Selected date:", selectedDate);
            console.log("Row index:", rowIndex);

            $.ajax({
                url: '/pack_item',
                type: 'POST',
                contentType: 'application/json',
                data: JSON.stringify({
                    row_index: rowIndex,
                    date: selectedDate
                }),
                success: function(response) {
                    console.log("Server response:", response);
                    if (response.status === 'success') {
                        var row = $(`#tableBody tr:eq(${rowIndex})`);
                        row.addClass('packed-row');
                        var cell = row.find('td:eq(6)');
                        cell.text(response.pack_time);
                        updateStatistics();
                    } else {
                        alert('Ошибка при упаковке: ' + response.message);
                    }
                },
                error: function(xhr, status, error) {
                    console.error("AJAX error:", 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 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();
        
        if (number) {
            if (!numbers.has(number)) {
                numbers.set(number, []);
            }
            numbers.get(number).push(index);
        }
        
        if (serial) {
            if (!serials.has(serial)) {
                serials.set(serial, []);
            }
            serials.get(serial).push(index);
        }
    });
    
    // Подсвечиваем дубликаты
    let duplicatesFound = false;
    
    // Проверяем номера
    numbers.forEach((indices, number) => {
        if (indices.length > 1) {
            indices.forEach(index => {
                rows.eq(index).find('td:nth-child(3)').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("Adding new row:", rowData);
    var tr = $('<tr>');
    
    // Проверяем наличие времени упаковки
    if (rowData[6]) {
        tr.addClass('packed-row');
    }
    
    // Добавляем все ячейки
    rowData.forEach(function(cellValue, index) {
        var td = $('<td>');
        tr.append(td);
        
        // Используем setTimeout, чтобы гарантировать, что строка уже добавлена в DOM
        setTimeout(() => {
            updateCell(td, cellValue, tr.index(), index);
        }, 0);
    });
    
    tableBody.append(tr);
    
    // Дополнительная проверка после добавления строки
    setTimeout(() => {
        const dlCell = tr.find('td').eq(4);
        const packCell = tr.find('td').eq(6);
        const dlValue = dlCell.text().trim();
        const packValue = packCell.text().trim();
        
        if (dlValue.startsWith('DL') && !packValue) {
            console.log('Post-check: Adding pack button for DL:', dlValue);
            packCell.html('<button class="pack-button">Упаковать</button>')
                .find('button')
                .click(function() {
                    packItem(tr.index());
                });
        }
        
        // Проверяем дубликаты после добавления новой строки
        checkDuplicates();
    }, 100);
}







// Обработчики событий для таблицы
$(document).ready(function() {
    initializeDateSelectors();
    updateTable();
    updateStatistics();
    initializeEventSource();
    setInterval(updateStatistics, 20);
    scheduleNextUpdate();

    $('.sortable').click(function() {
        const column = $(this).data('column');
        const order = $(this).hasClass('asc') ? 'desc' : 'asc';
        
        // Удаляем классы сортировки со всех заголовков
        $('.sortable').removeClass('asc desc');
        // Добавляем класс текущей сортировки
        $(this).addClass(order);
        
        sortTable(column, order);
    });

    // Обработчик поиска
    $('#searchButton').click(function() {
        searchNumber();
    });
    $('#searchInput').keypress(function(e) {
        if (e.which === 13) {
            searchNumber();
        }
    });
});


$('#datepicker').on('change', function() {
    initializeEventSource();
    updateTable();
});

// В файле table.js или отдельном файле export.js
$(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('Ошибка при экспорте файла');
            });
    });
});


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();
    setInterval(updateStatistics, 20);
});


$(document).on('dblclick', '.editable', function() {
    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();
        var rowIndex = td.parent().index();
        var columnIndex = td.attr('data-column');
        
        const url = columnIndex === '2' ? '/update_number' : '/update_serial';
        
        $.ajax({
            url: url,
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify({
                row_index: rowIndex,
                new_value: newValue,
                date: getSelectedDate()
            }),
            success: function(response) {
                if (response.status === 'success') {
                    td.text(newValue);
                    const duplicatesFound = checkDuplicates();
                    console.log("Duplicates found after edit:", duplicatesFound)
                } else {
                    alert('Ошибка при обновлении значения');
                    td.text(currentValue);
                }
                isEditing = false;
            },
            error: function() {
                alert('Ошибка при отправке данных');
                td.text(currentValue);
                isEditing = false;
            }
        });
    });

    input.on('keypress', function(e) {
        if (e.which === 13) {
            $(this).blur();
        }
    });
});

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

    eventSource.onerror = function(error) {
        console.error('EventSource failed:', error);
        eventSource.close();
        setTimeout(initializeEventSource, 5000);  // Попытка переподключения через 5 секунд
    };
}



function updateTableContent(data, tableBody, highlightNew = false) {
    tableBody.empty();  // Очищаем текущее содержимое таблицы
    data.forEach(function(rowData, index) {
        const newRow = appendNewRow(tableBody, rowData);
        if (highlightNew && index === data.length - 1) {
            newRow.addClass('highlight-new').delay(2000).queue(function(next){
                $(this).removeClass('highlight-new');
                next();
            });
        }
    });
    checkDuplicates();  // Проверяем дубликаты после обновления таблицы
}


function scheduleNextUpdate() {
    setTimeout(() => {
        if (!isEditing) {
            updateTable(false, false).then(() => {
                scheduleNextUpdate();
            });
        } else {
            scheduleNextUpdate();
        }
    }, 3000);
}
date.js
function initializeDateSelectors() {
    const currentDate = new Date();
    
    // Заполняем дни
    const daySelect = document.getElementById('day');
    for (let i = 1; i <= 31; i++) {
        const option = document.createElement('option');
        option.value = i.toString().padStart(2, '0');
        option.text = i;
        daySelect.appendChild(option);
    }
    
    // Заполняем годы (текущий год и следующий)
    const yearSelect = document.getElementById('year');
    const currentYear = currentDate.getFullYear();
    for (let year = 2024; year <= 2030; year++) {
        const option = document.createElement('option');
        option.value = year;
        option.textContent = year;
        yearSelect.appendChild(option);
    }
    
    // Устанавливаем текущую дату
    daySelect.value = currentDate.getDate().toString().padStart(2, '0');
    document.getElementById('month').value = (currentDate.getMonth() + 1).toString().padStart(2, '0');
    yearSelect.value = currentDate.getFullYear();
    
    // Добавляем обработчики событий
    daySelect.addEventListener('change', updateTable);
    document.getElementById('month').addEventListener('change', updateTable);
    yearSelect.addEventListener('change', updateTable);
}

function getSelectedDate() {
    const day = document.getElementById('day').value.padStart(2, '0');
    const month = document.getElementById('month').value.padStart(2, '0');
    const year = document.getElementById('year').value;
    return `${year}-${month}-${day}`;
}