commit f9543463c49c5cdd29b9e4278e08f589b5a6aaec Author: MoonTestUse1 Date: Sat Dec 28 05:32:33 2024 +0600 Проверка diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24449e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Dependencies +node_modules +__pycache__ +*.pyc + +# Environment +.env +.env.local +.env.*.local + +# Build +dist +build + +# IDE +.vscode +.idea + +# Database + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# System +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..002a7f8 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# AdministrationItDepartmens +Site for teh support diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..09c3ba8 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +# FastAPI application package diff --git a/backend/app/bot.py b/backend/app/bot.py new file mode 100644 index 0000000..8cdea92 --- /dev/null +++ b/backend/app/bot.py @@ -0,0 +1,68 @@ +from aiogram import Bot, Dispatcher, types +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.filters import Command +from sqlalchemy.orm import Session +from ..database import get_db +from .. import models + +# Создаем роутер для обработки callback'ов +from aiogram import Router +router = Router() + +# Обработчик нажатия кнопки +@router.callback_query(lambda c: c.data.startswith('status_')) +async def process_status_button(callback_query: types.CallbackQuery): + try: + print(f"Hello world: {callback_query.data}") + # Получаем ID заявки и новый статус из callback_data + _, request_id, new_status = callback_query.data.split('_') + request_id = int(request_id) + + # Получаем сессию базы данных + db = next(get_db()) + + # Обновляем статус в базе данных + request = db.query(models.Request).filter(models.Request.id == request_id).first() + if request: + request.status = new_status + db.commit() + + # Обновляем сообщение в боте + await callback_query.message.edit_text( + f"Заявка №{request_id}\nСтатус: {new_status}", + reply_markup=get_updated_keyboard(request_id, new_status) + ) + + # Отправляем уведомление о успешном обновлении + await callback_query.answer("Статус успешно обновлен!") + else: + await callback_query.answer("Заявка не найдена!", show_alert=True) + + except Exception as e: + print(f"Error in process_status_button: {e}") + await callback_query.answer("Произошла ошибка при обновлении статуса", show_alert=True) + +def get_updated_keyboard(request_id: int, current_status: str) -> InlineKeyboardMarkup: + keyboard = InlineKeyboardMarkup(inline_keyboard=[]) + + if current_status != "in_progress": + keyboard.inline_keyboard.append([ + InlineKeyboardButton( + text="В работе", + callback_data=f"status_{request_id}_in_progress" + ) + ]) + + if current_status != "completed": + keyboard.inline_keyboard.append([ + InlineKeyboardButton( + text="Завершено", + callback_data=f"status_{request_id}_completed" + ) + ]) + + return keyboard + +# В основном файле бота (где создается диспетчер) +dp = Dispatcher() +dp.include_router(router) \ No newline at end of file diff --git a/backend/app/bot/__init__.py b/backend/app/bot/__init__.py new file mode 100644 index 0000000..8109bc3 --- /dev/null +++ b/backend/app/bot/__init__.py @@ -0,0 +1,19 @@ +"""Bot initialization module""" +from aiogram import Bot, Dispatcher +from .config import settings +from .handlers import callbacks_router, start_router + +# Initialize bot and dispatcher +bot = Bot(token=settings.bot_token) +dp = Dispatcher() + +# Include routers only once during initialization +dp.include_router(callbacks_router) +dp.include_router(start_router) + +async def start_bot(): + """Start the bot""" + try: + await dp.start_polling(bot, skip_updates=True) + except Exception as e: + print(f"Error starting bot: {e}") \ No newline at end of file diff --git a/backend/app/bot/bot.py b/backend/app/bot/bot.py new file mode 100644 index 0000000..5fb38f6 --- /dev/null +++ b/backend/app/bot/bot.py @@ -0,0 +1,69 @@ +from aiogram import Bot, Dispatcher +from app.bot.config import settings + +bot = Bot(token="7677506032:AAHduD5EePz3bE23DKlo35KoOp2_9lZuS34") +dp = Dispatcher() +from .handlers import start, status + + +async def start_bot(): + """Start the bot""" + try: + await dp.start_polling(bot, skip_updates=True) + finally: + await bot.session.close() + +from aiogram import Bot, Dispatcher, types +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from sqlalchemy.orm import Session +from ..database import get_db +from .. import models + +# Создаем обработчик нажатия кнопки +@dp.callback_query_handler(lambda c: c.data.startswith('status_')) +async def process_status_button(callback_query: types.CallbackQuery): + try: + # Получаем ID заявки и новый статус из callback_data + _, request_id, new_status = callback_query.data.split('_') + request_id = int(request_id) + + # Получаем сессию базы данных + db = next(get_db()) + + # Обновляем статус в базе данных + request = db.query(models.Request).filter(models.Request.id == request_id).first() + if request: + request.status = new_status + db.commit() + + # Обновляем сообщение в боте + await callback_query.message.edit_text( + f"Заявка №{request_id}\nСтатус: {new_status}", + reply_markup=get_updated_keyboard(request_id, new_status) + ) + + # Отправляем уведомление о успешном обновлении + await callback_query.answer("Статус успешно обновлен!") + else: + await callback_query.answer("Заявка не найдена!", show_alert=True) + + except Exception as e: + print(f"Error in process_status_button: {e}") + await callback_query.answer("Произошла ошибка при обновлении статуса", show_alert=True) + +def get_updated_keyboard(request_id: int, current_status: str) -> InlineKeyboardMarkup: + keyboard = InlineKeyboardMarkup() + + if current_status != "in_progress": + keyboard.add(InlineKeyboardButton( + "В работе", + callback_data=f"status_{request_id}_in_progress" + )) + + if current_status != "completed": + keyboard.add(InlineKeyboardButton( + "Завершено", + callback_data=f"status_{request_id}_completed" + )) + + return keyboard \ No newline at end of file diff --git a/backend/app/bot/config.py b/backend/app/bot/config.py new file mode 100644 index 0000000..2b36e8b --- /dev/null +++ b/backend/app/bot/config.py @@ -0,0 +1,26 @@ +""" +Configuration module for the Telegram bot. +Contains all necessary settings and constants. +""" +from pydantic_settings import BaseSettings +from pydantic import Field + +class Settings(BaseSettings): + """Bot configuration settings""" + bot_token: str = Field("7677506032:AAHduD5EePz3bE23DKlo35KoOp2_9lZuS34", env="TELEGRAM_BOT_TOKEN") + chat_id: str = Field("5057752127", env="TELEGRAM_CHAT_ID") + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + +# Create settings instance +settings = Settings() + +# Request status constants +class RequestStatus: + """Constants for request statuses""" + NEW = "new" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + CANCELLED = "cancelled" \ No newline at end of file diff --git a/backend/app/bot/constants.py b/backend/app/bot/constants.py new file mode 100644 index 0000000..0506868 --- /dev/null +++ b/backend/app/bot/constants.py @@ -0,0 +1,73 @@ +from enum import Enum + + +class RequestStatus(str, Enum): + NEW = "new" + IN_PROGRESS = "in_progress" + RESOLVED = "resolved" + CLOSED = "closed" + + +class RequestPriority(str, Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + + +class Department(str, Enum): + AHO = "aho" + GKH = "gkh" + GENERAL = "general" + + +class RequestType(str, Enum): + HARDWARE = "hardware" + SOFTWARE = "software" + NETWORK = "network" + ACCESS = "access" + OTHER = "other" + + +STATUS_LABELS = { + RequestStatus.NEW: "Новая", + RequestStatus.IN_PROGRESS: "В работе", + RequestStatus.RESOLVED: "Решена", + RequestStatus.CLOSED: "Закрыта", +} + +PRIORITY_LABELS = { + RequestPriority.LOW: "Низкий", + RequestPriority.MEDIUM: "Средний", + RequestPriority.HIGH: "Высокий", + RequestPriority.CRITICAL: "Критический", +} + +PRIORITY_EMOJI = { + RequestPriority.LOW: "🟢", + RequestPriority.MEDIUM: "🟡", + RequestPriority.HIGH: "🟠", + RequestPriority.CRITICAL: "🔴", +} + +DEPARTMENT_LABELS = { + Department.AHO: "Административно-хозяйственный отдел", + Department.GKH: "Жилищно-коммунальное хозяйство", + Department.GENERAL: "Общий отдел", +} + +REQUEST_TYPE_LABELS = { + RequestType.HARDWARE: "Проблемы с оборудованием", + RequestType.SOFTWARE: "Проблемы с ПО", + RequestType.NETWORK: "Проблемы с сетью", + RequestType.ACCESS: "Доступ к системам", + RequestType.OTHER: "Другое", +} + +REQUEST_TYPE_EMOJI = { + RequestType.HARDWARE: "🖥️", + RequestType.SOFTWARE: "💿", + RequestType.NETWORK: "🌐", + RequestType.ACCESS: "🔑", + RequestType.OTHER: "📝", +} diff --git a/backend/app/bot/handlers/__init__.py b/backend/app/bot/handlers/__init__.py new file mode 100644 index 0000000..3e057f0 --- /dev/null +++ b/backend/app/bot/handlers/__init__.py @@ -0,0 +1,6 @@ +"""Handlers initialization""" +from .callbacks import router as callbacks_router +from .start import router as start_router +from .callbacks import get_updated_keyboard + +__all__ = ['callbacks_router', 'start_router', 'get_updated_keyboard'] \ No newline at end of file diff --git a/backend/app/bot/handlers/callbacks.py b/backend/app/bot/handlers/callbacks.py new file mode 100644 index 0000000..1cd8bfe --- /dev/null +++ b/backend/app/bot/handlers/callbacks.py @@ -0,0 +1,70 @@ +"""Handlers for callback queries""" +from aiogram import Router, types +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from sqlalchemy.orm import Session +from ...database import get_db +from ... import models +from ..config import RequestStatus + +router = Router() + +@router.callback_query(lambda c: c.data and c.data.startswith('status_')) +async def process_status_button(callback_query: types.CallbackQuery): + """ + Handle status update button clicks. + Updates request status in database and updates message in Telegram. + """ + try: + # Parse callback data + _, request_id, new_status = callback_query.data.split('_') + request_id = int(request_id) + + # Get database session + db = next(get_db()) + + # Update request status + request = db.query(models.Request).filter(models.Request.id == request_id).first() + if request: + request.status = new_status + db.commit() + + # Update message in Telegram + await callback_query.message.edit_text( + f"Заявка №{request_id}\n" + f"Статус: {new_status}\n" + f"Описание: {request.description}", + reply_markup=get_updated_keyboard(request_id, new_status) + ) + + await callback_query.answer("Статус успешно обновлен!") + else: + await callback_query.answer("Заявка не найдена!", show_alert=True) + + except Exception as e: + print(f"Error in process_status_button: {e}") + await callback_query.answer( + "Произошла ошибка при обновлении статуса", + show_alert=True + ) + +def get_updated_keyboard(request_id: int, current_status: str) -> InlineKeyboardMarkup: + """Create keyboard with status update buttons""" + keyboard = InlineKeyboardMarkup(inline_keyboard=[]) + + if current_status != RequestStatus.IN_PROGRESS: + keyboard.inline_keyboard.append([ + InlineKeyboardButton( + text="В работе", + callback_data=f"status_{request_id}_{RequestStatus.IN_PROGRESS}" + ) + ]) + + if current_status != RequestStatus.COMPLETED: + keyboard.inline_keyboard.append([ + InlineKeyboardButton( + text="Завершено", + callback_data=f"status_{request_id}_{RequestStatus.COMPLETED}" + ) + ]) + + return keyboard \ No newline at end of file diff --git a/backend/app/bot/handlers/start.py b/backend/app/bot/handlers/start.py new file mode 100644 index 0000000..8cb37ba --- /dev/null +++ b/backend/app/bot/handlers/start.py @@ -0,0 +1,11 @@ +"""Handler for start command and other basic commands""" +from aiogram import Router, types +from aiogram.filters import Command +from ..config import settings + +router = Router() + +@router.message(Command("start")) +async def cmd_start(message: types.Message): + """Handle /start command""" + await message.answer("Бот для обработки заявок запущен!") \ No newline at end of file diff --git a/backend/app/bot/handlers/status.py b/backend/app/bot/handlers/status.py new file mode 100644 index 0000000..bd200ff --- /dev/null +++ b/backend/app/bot/handlers/status.py @@ -0,0 +1,61 @@ +from aiogram import types, F +from logging import getLogger +from sqlalchemy.orm import Session +from ...database import get_db +from ...crud import requests +from ..bot import dp +from ..keyboards import create_status_keyboard +from ..messages import format_request_message +from ..constants import STATUS_LABELS + +logger = getLogger(__name__) + +@dp.callback_query(F.data.startswith("status_")) +async def process_status_update(callback: types.CallbackQuery): + try: + parts = callback.data.split("_") + logger.info(f"Received callback data: {callback.data}") + + if len(parts) < 3: + logger.error(f"Invalid callback data format: {parts}") + await callback.answer("Неверный формат данных", show_alert=True) + return + + request_id = int(parts[1]) + new_status = "_".join(parts[2:]) if len(parts) > 3 else parts[2] + + logger.info( + f"Processing status update: request_id={request_id}, new_status={new_status}" + ) + + db = next(get_db()) + try: + updated_request = requests.update_request_status(db, request_id, new_status) + if not updated_request: + logger.warning(f"Request not found: {request_id}") + await callback.answer("Заявка не найдена", show_alert=True) + return + + new_message = format_request_message(updated_request) + new_keyboard = create_status_keyboard(request_id, new_status) + + await callback.message.edit_text( + text=new_message, parse_mode="HTML", reply_markup=new_keyboard + ) + + await callback.answer(f"Статус обновлен: {STATUS_LABELS[new_status]}") + logger.info( + f"Successfully updated request {request_id} to status {new_status}" + ) + + except ValueError as e: + logger.error(f"Value error while updating status: {e}") + await callback.answer(str(e), show_alert=True) + finally: + db.close() + + except Exception as e: + logger.error(f"Error processing callback: {e}", exc_info=True) + await callback.answer( + "Произошла ошибка при обновлении статуса", show_alert=True + ) diff --git a/backend/app/bot/keyboards.py b/backend/app/bot/keyboards.py new file mode 100644 index 0000000..3f5cedf --- /dev/null +++ b/backend/app/bot/keyboards.py @@ -0,0 +1,33 @@ +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from logging import getLogger +from .constants import STATUS_LABELS + +logger = getLogger(__name__) + +def create_status_keyboard( + request_id: int, current_status: str +) -> InlineKeyboardMarkup: + status_transitions = { + "new": ["in_progress"], + "in_progress": ["resolved"], + "resolved": ["closed"], + "closed": [], + } + + buttons = [] + available_statuses = status_transitions.get(current_status, []) + + for status in available_statuses: + callback_data = f"status_{request_id}_{status}" + logger.debug(f"Creating button with callback_data: {callback_data}") + buttons.append( + [ + InlineKeyboardButton( + text=STATUS_LABELS[status], callback_data=callback_data + ) + ] + ) + + keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) + logger.debug(f"Created keyboard: {keyboard}") + return keyboard diff --git a/backend/app/bot/messages.py b/backend/app/bot/messages.py new file mode 100644 index 0000000..0f951ed --- /dev/null +++ b/backend/app/bot/messages.py @@ -0,0 +1,37 @@ +from datetime import datetime +from .constants import ( + STATUS_LABELS, + PRIORITY_LABELS, + PRIORITY_EMOJI, + DEPARTMENT_LABELS, + REQUEST_TYPE_LABELS, + REQUEST_TYPE_EMOJI, +) + + +def format_request_message(request_data: dict) -> str: + created_at = datetime.fromisoformat(request_data["created_at"]).strftime( + "%d.%m.%Y %H:%M" + ) + + # Get translated values + department = DEPARTMENT_LABELS.get( + request_data["department"], request_data["department"] + ) + request_type = REQUEST_TYPE_LABELS.get( + request_data["request_type"], request_data["request_type"] + ) + priority = PRIORITY_LABELS.get(request_data["priority"], request_data["priority"]) + status = STATUS_LABELS.get(request_data.get("status", "new"), "Неизвестно") + + return ( + f"📋 Заявка #{request_data['id']}\n\n" + f"👤 Сотрудник: {request_data['employee_last_name']} {request_data['employee_first_name']}\n" + f"🏢 Отдел: {department}\n" + f"🚪 Кабинет: {request_data['office']}\n" + f"{REQUEST_TYPE_EMOJI.get(request_data['request_type'], '📝')} Тип заявки: {request_type}\n" + f"{PRIORITY_EMOJI.get(request_data['priority'], '⚪')} Приоритет: {priority}\n\n" + f"📝 Описание:\n
{request_data['description']}
\n\n" + f"🕒 Создана: {created_at}\n" + f"📊 Статус: {status}" + ) diff --git a/backend/app/bot/notifications.py b/backend/app/bot/notifications.py new file mode 100644 index 0000000..397f5a4 --- /dev/null +++ b/backend/app/bot/notifications.py @@ -0,0 +1,23 @@ +"""Notifications module for the Telegram bot""" +from .config import settings +from . import bot +from .handlers import get_updated_keyboard + +async def send_notification(request_data: dict): + """Send notification about new request to Telegram chat""" + message_text = ( + f"Новая заявка №{request_data['id']}\n" + f"Отдел: {request_data['department']}\n" + f"Тип: {request_data['request_type']}\n" + f"Приоритет: {request_data['priority']}\n" + f"Описание: {request_data['description']}" + ) + + try: + await bot.send_message( + chat_id=settings.chat_id, + text=message_text, + reply_markup=get_updated_keyboard(request_data['id'], "new") + ) + except Exception as e: + print(f"Error sending notification: {e}") \ No newline at end of file diff --git a/backend/app/crud/auth.py b/backend/app/crud/auth.py new file mode 100644 index 0000000..696d7de --- /dev/null +++ b/backend/app/crud/auth.py @@ -0,0 +1,26 @@ +from sqlalchemy.orm import Session +from ..schemas import tables +from ..utils.auth import verify_password + + +def authenticate_employee(db: Session, last_name: str, password: str): + employee = db.query(tables.Employee).filter(tables.Employee.last_name == last_name).first() + if not employee: + return None + if not verify_password(password, employee.password): + return None + return { + "id": employee.id, + "firstName": employee.first_name, + "lastName": employee.last_name, + "department": employee.department, + "office": employee.office, + "createdAt": employee.created_at + } + + +def authenticate_admin(db: Session, username: str, password: str): + # Здесь можно добавить логику для админа, пока используем хардкод + if username == "admin" and password == "admin66": + return True + return False diff --git a/backend/app/crud/employees.py b/backend/app/crud/employees.py new file mode 100644 index 0000000..641f493 --- /dev/null +++ b/backend/app/crud/employees.py @@ -0,0 +1,45 @@ +from sqlalchemy.orm import Session +from ..models import employee as models +from ..schemas import tables +from ..utils.auth import get_password_hash + + +def get_employee(db: Session, employee_id: int): + return db.query(tables.Employee).filter(tables.Employee.id == employee_id).first() + + +def get_employee_by_lastname(db: Session, last_name: str): + return ( + db.query(tables.Employee).filter(tables.Employee.last_name == last_name).first() + ) + + +def get_employees(db: Session, skip: int = 0, limit: int = 100): + return db.query(tables.Employee).offset(skip).limit(limit).all() + + +def create_employee(db: Session, employee: models.EmployeeCreate): + hashed_password = get_password_hash(employee.password) + db_employee = tables.Employee( + first_name=employee.first_name, + last_name=employee.last_name, + department=employee.department, + office=employee.office, + password=hashed_password, + ) + db.add(db_employee) + db.commit() + db.refresh(db_employee) + return db_employee + + +def update_employee(db: Session, employee_id: int, data: dict): + db_employee = get_employee(db, employee_id) + if db_employee: + for key, value in data.items(): + if key == "password": + value = get_password_hash(value) + setattr(db_employee, key, value) + db.commit() + db.refresh(db_employee) + return db_employee diff --git a/backend/app/crud/requests.py b/backend/app/crud/requests.py new file mode 100644 index 0000000..9a547de --- /dev/null +++ b/backend/app/crud/requests.py @@ -0,0 +1,207 @@ +from sqlalchemy.orm import Session +from ..models import request as models +from ..schemas import tables +from sqlalchemy import select +from sqlalchemy.orm import Session +from ..schemas import tables +from datetime import datetime + + +def create_request(db: Session, request: models.RequestCreate): + db_request = tables.Request( + employee_id=request.employee_id, + department=request.department, + request_type=request.request_type, + priority=request.priority, + description=request.description, + status="new", + ) + db.add(db_request) + db.commit() + db.refresh(db_request) + return db_request + + +def get_requests(db: Session, skip: int = 0, limit: int = 100): + requests = ( + db.query( + tables.Request, + tables.Employee.last_name.label("employee_last_name"), + tables.Employee.first_name.label("employee_first_name"), + ) + .join(tables.Employee) + .offset(skip) + .limit(limit) + .all() + ) + + return [ + { + "id": req[0].id, + "employee_id": req[0].employee_id, + "department": req[0].department, + "request_type": req[0].request_type, + "priority": req[0].priority, + "status": req[0].status, + "description": req[0].description, + "created_at": req[0].created_at, + "employee_last_name": req[1], + "employee_first_name": req[2], + } + for req in requests + ] + + +def get_requests_by_employee_lastname(db: Session, last_name: str): + requests = ( + db.query( + tables.Request, + tables.Employee.last_name.label("employee_last_name"), + tables.Employee.first_name.label("employee_first_name"), + ) + .join(tables.Employee) + .filter(tables.Employee.last_name.ilike(f"%{last_name}%")) + .all() + ) + + return [ + { + "id": req[0].id, + "employee_id": req[0].employee_id, + "department": req[0].department, + "request_type": req[0].request_type, + "priority": req[0].priority, + "status": req[0].status, + "description": req[0].description, + "created_at": req[0].created_at, + "employee_last_name": req[1], + "employee_first_name": req[2], + } + for req in requests + ] + + +def update_request_status( + db: Session, request_id: int, new_status: models.RequestStatus +): + try: + db_request = ( + db.query(tables.Request).filter(tables.Request.id == request_id).first() + ) + if not db_request: + return None + + # Define valid status transitions + valid_transitions = { + models.RequestStatus.NEW: [models.RequestStatus.IN_PROGRESS], + models.RequestStatus.IN_PROGRESS: [models.RequestStatus.RESOLVED], + models.RequestStatus.RESOLVED: [models.RequestStatus.CLOSED], + models.RequestStatus.CLOSED: [], + } + + current_status = models.RequestStatus(db_request.status) + if new_status not in valid_transitions[current_status]: + raise ValueError( + f"Invalid status transition from {current_status} to {new_status}" + ) + + db_request.status = new_status + db.commit() + db.refresh(db_request) + return db_request + + except Exception as e: + db.rollback() + raise e + + +def get_request_details(db: Session, request_id: int): + """Get detailed request information including employee details""" + request = ( + db.query(tables.Request) + .join(tables.Employee) + .filter(tables.Request.id == request_id) + .first() + ) + + if not request: + return None + + return { + "id": request.id, + "employee_last_name": request.employee.last_name, + "employee_first_name": request.employee.first_name, + "department": request.department, + "office": request.employee.office, + "request_type": request.request_type, + "priority": request.priority, + "description": request.description, + "status": request.status, + "created_at": request.created_at.isoformat(), + } + + +from sqlalchemy.orm import Session +from ..schemas import tables +from datetime import datetime + + +def get_request_details(db: Session, request_id: int): + """Get detailed request information including employee details""" + request = ( + db.query(tables.Request) + .join(tables.Employee) + .filter(tables.Request.id == request_id) + .first() + ) + + if not request: + return None + + return { + "id": request.id, + "employee_last_name": request.employee.last_name, + "employee_first_name": request.employee.first_name, + "department": request.department, + "office": request.employee.office, + "request_type": request.request_type, + "priority": request.priority, + "description": request.description, + "status": request.status, + "created_at": request.created_at.isoformat(), + } + + +def update_request_status(db: Session, request_id: int, new_status: str): + """Update request status with validation""" + try: + # Define valid status transitions + valid_transitions = { + "new": ["in_progress"], + "in_progress": ["resolved"], + "resolved": ["closed"], + "closed": [], + } + + db_request = ( + db.query(tables.Request).filter(tables.Request.id == request_id).first() + ) + if not db_request: + return None + + current_status = db_request.status + if new_status not in valid_transitions.get(current_status, []): + raise ValueError( + f"Invalid status transition from {current_status} to {new_status}" + ) + + db_request.status = new_status + db.commit() + db.refresh(db_request) + + # Get full request details after update + return get_request_details(db, request_id) + + except Exception as e: + db.rollback() + raise e diff --git a/backend/app/crud/statistics.py b/backend/app/crud/statistics.py new file mode 100644 index 0000000..d7d8ae5 --- /dev/null +++ b/backend/app/crud/statistics.py @@ -0,0 +1,89 @@ +from sqlalchemy import func, text +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +from ..schemas import tables +from ..models.request import RequestStatus + + +def get_statistics(db: Session, period: str = "week"): + # Calculate date range based on period + now = datetime.now() + if period == "day": + start_date = now - timedelta(days=1) + elif period == "week": + start_date = now - timedelta(weeks=1) + elif period == "month": + start_date = now - timedelta(days=30) + else: # all time + start_date = datetime.min + + # Total requests + total_requests = db.query(func.count(tables.Request.id)).scalar() or 0 + + # Resolved requests in period + resolved_requests = ( + db.query(func.count(tables.Request.id)) + .filter(tables.Request.status == RequestStatus.RESOLVED) + .filter(tables.Request.created_at >= start_date) + .scalar() + or 0 + ) + + # Average resolution time (in hours) + avg_resolution = ( + db.query( + func.avg(func.julianday("now") - func.julianday(tables.Request.created_at)) + * 24 + ) + .filter( + tables.Request.status == RequestStatus.RESOLVED, + tables.Request.created_at >= start_date, + ) + .scalar() + ) + + avg_resolution_time = f"{int(avg_resolution or 0)}ч" if avg_resolution else "0ч" + + # Request volume over time + volume_data = ( + db.query( + func.date(tables.Request.created_at).label("date"), + func.count(tables.Request.id).label("count"), + ) + .filter(tables.Request.created_at >= start_date) + .group_by(text("date")) + .all() + ) + + # Request types distribution + type_distribution = ( + db.query(tables.Request.request_type, func.count(tables.Request.id)) + .group_by(tables.Request.request_type) + .all() + ) + + # Status distribution + status_distribution = ( + db.query(tables.Request.status, func.count(tables.Request.id)) + .group_by(tables.Request.status) + .all() + ) + + # Ensure all statuses are represented + all_statuses = {status.value: 0 for status in RequestStatus} + for status, count in status_distribution: + all_statuses[status] = count + + status_data = [(status, count) for status, count in all_statuses.items()] + + return { + "totalRequests": total_requests, + "resolvedRequests": resolved_requests, + "averageResolutionTime": avg_resolution_time, + "volumeLabels": [str(d[0]) for d in volume_data], + "volumeData": [d[1] for d in volume_data], + "typeLabels": [t[0] for t in type_distribution], + "typeData": [t[1] for t in type_distribution], + "statusLabels": [s[0] for s in status_data], + "statusData": [s[1] for s in status_data], + } diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..c0d1878 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,20 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/logging_config.py b/backend/app/logging_config.py new file mode 100644 index 0000000..519ab94 --- /dev/null +++ b/backend/app/logging_config.py @@ -0,0 +1,56 @@ +"""Logging configuration for the application""" + +logging_config = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S" + }, + "access": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(client_addr)s - %(request_line)s - %(status_code)s", + "datefmt": "%Y-%m-%d %H:%M:%S" + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "INFO", + "formatter": "default", + "stream": "ext://sys.stdout" + }, + "file": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "default", + "filename": "logs/app.log", + "maxBytes": 10485760, # 10MB + "backupCount": 5 + }, + "access_file": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "access", + "filename": "logs/access.log", + "maxBytes": 10485760, # 10MB + "backupCount": 5 + } + }, + "loggers": { + "": { # Root logger + "handlers": ["console", "file"], + "level": "INFO" + }, + "app": { # Application logger + "handlers": ["console", "file"], + "level": "INFO", + "propagate": False + }, + "app.access": { # Access logger + "handlers": ["access_file"], + "level": "INFO", + "propagate": False + } + } +} \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..f10bfdb --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI, Depends, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.openapi.docs import get_swagger_ui_html +from fastapi.openapi.utils import get_openapi +import logging +from logging.config import dictConfig +from .logging_config import logging_config + +# Configure logging +dictConfig(logging_config) +logger = logging.getLogger(__name__) + +app = FastAPI( + title="Support Portal API", + description="API for managing support requests and employees", + version="1.0.0", + docs_url=None, + redoc_url=None +) + +# Custom OpenAPI documentation +@app.get("/api/docs", include_in_schema=False) +async def custom_swagger_ui_html(): + return get_swagger_ui_html( + openapi_url="/api/openapi.json", + title="Support Portal API Documentation", + swagger_favicon_url="/favicon.ico" + ) + +@app.get("/api/openapi.json", include_in_schema=False) +async def get_open_api_endpoint(): + return get_openapi( + title="Support Portal API", + version="1.0.0", + description="API for managing support requests and employees", + routes=app.routes + ) + +# Existing middleware and routes... \ No newline at end of file diff --git a/backend/app/middleware/__init__.py b/backend/app/middleware/__init__.py new file mode 100644 index 0000000..ad73acc --- /dev/null +++ b/backend/app/middleware/__init__.py @@ -0,0 +1,3 @@ +from .logging import LoggingMiddleware + +__all__ = ['LoggingMiddleware'] \ No newline at end of file diff --git a/backend/app/middleware/logging.py b/backend/app/middleware/logging.py new file mode 100644 index 0000000..6189630 --- /dev/null +++ b/backend/app/middleware/logging.py @@ -0,0 +1,39 @@ +"""Logging middleware for request/response tracking""" +import time +from fastapi import Request +from starlette.middleware.base import BaseHTTPMiddleware +import logging + +logger = logging.getLogger("app.access") + +class LoggingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + start_time = time.time() + + # Log request + logger.info( + "Request started", + extra={ + "client_addr": request.client.host, + "request_line": f"{request.method} {request.url.path}", + "status_code": "PENDING" + } + ) + + response = await call_next(request) + + # Calculate processing time + process_time = time.time() - start_time + + # Log response + logger.info( + "Request completed", + extra={ + "client_addr": request.client.host, + "request_line": f"{request.method} {request.url.path}", + "status_code": response.status_code, + "process_time": f"{process_time:.2f}s" + } + ) + + return response \ No newline at end of file diff --git a/backend/app/models/employee.py b/backend/app/models/employee.py new file mode 100644 index 0000000..76d0e2a --- /dev/null +++ b/backend/app/models/employee.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel +from datetime import datetime + + +class EmployeeBase(BaseModel): + last_name: str + first_name: str + department: str + office: str + + +class EmployeeCreate(EmployeeBase): + password: str + + +class Employee(EmployeeBase): + id: int + created_at: datetime + + class Config: + from_attributes = True diff --git a/backend/app/models/request.py b/backend/app/models/request.py new file mode 100644 index 0000000..419573b --- /dev/null +++ b/backend/app/models/request.py @@ -0,0 +1,50 @@ +from pydantic import BaseModel +from datetime import datetime +from enum import Enum + + +class RequestStatus(str, Enum): + NEW = "new" + IN_PROGRESS = "in_progress" + RESOLVED = "resolved" + CLOSED = "closed" + + +class RequestPriority(str, Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + + +class StatusUpdate(BaseModel): + status: RequestStatus + + +class RequestBase(BaseModel): + department: str + request_type: str + priority: RequestPriority + description: str + + +class RequestCreate(RequestBase): + employee_id: int + + +class Request(RequestBase): + id: int + status: RequestStatus + created_at: datetime + employee_id: int + + class Config: + from_attributes = True + + +class RequestWithEmployee(Request): + employee_last_name: str + employee_first_name: str + + class Config: + from_attributes = True diff --git a/backend/app/schemas/tables.py b/backend/app/schemas/tables.py new file mode 100644 index 0000000..83fff96 --- /dev/null +++ b/backend/app/schemas/tables.py @@ -0,0 +1,34 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from ..database import Base +from ..models.request import RequestStatus, RequestPriority + + +class Employee(Base): + __tablename__ = "employees" + + id = Column(Integer, primary_key=True, index=True) + first_name = Column(String, nullable=False) + last_name = Column(String, nullable=False) + department = Column(String, nullable=False) + office = Column(String, nullable=False) + password = Column(String, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + requests = relationship("Request", back_populates="employee") + + +class Request(Base): + __tablename__ = "requests" + + id = Column(Integer, primary_key=True, index=True) + employee_id = Column(Integer, ForeignKey("employees.id")) + department = Column(String, nullable=False) + request_type = Column(String, nullable=False) + priority = Column(Enum(RequestPriority), nullable=False) + status = Column(Enum(RequestStatus), default=RequestStatus.NEW) + description = Column(String) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + employee = relationship("Employee", back_populates="requests") diff --git a/backend/app/utils/auth.py b/backend/app/utils/auth.py new file mode 100644 index 0000000..73b4920 --- /dev/null +++ b/backend/app/utils/auth.py @@ -0,0 +1,11 @@ +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) diff --git a/backend/app/utils/constants.py b/backend/app/utils/constants.py new file mode 100644 index 0000000..a11a177 --- /dev/null +++ b/backend/app/utils/constants.py @@ -0,0 +1,45 @@ +STATUS_LABELS = { + 'new': 'Новая', + 'in_progress': 'В работе', + 'resolved': 'Решена', + 'closed': 'Закрыта' +} + +# Priority translations and emoji +PRIORITY_LABELS = { + 'low': 'Низкий', + 'medium': 'Средний', + 'high': 'Высокий', + 'critical': 'Критический' +} + +PRIORITY_EMOJI = { + 'low': '🟢', + 'medium': '🟡', + 'high': '🟠', + 'critical': '🔴' +} + +# Department translations +DEPARTMENT_LABELS = { + 'aho': 'Административно-хозяйственный отдел', + 'gkh': 'Жилищно-коммунальное хозяйство', + 'general': 'Общий отдел' +} + +# Request type translations and emoji +REQUEST_TYPE_LABELS = { + 'hardware': 'Проблемы с оборудованием', + 'software': 'Проблемы с ПО', + 'network': 'Проблемы с сетью', + 'access': 'Доступ к системам', + 'other': 'Другое' +} + +REQUEST_TYPE_EMOJI = { + 'hardware': '🖥️', + 'software': '💿', + 'network': '🌐', + 'access': '🔑', + 'other': '📝' +} \ No newline at end of file diff --git a/backend/app/utils/telegram.py b/backend/app/utils/telegram.py new file mode 100644 index 0000000..9e7ee24 --- /dev/null +++ b/backend/app/utils/telegram.py @@ -0,0 +1,90 @@ +from aiogram import Bot +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +import asyncio +from datetime import datetime +from logging import getLogger +from .constants import ( + STATUS_LABELS, PRIORITY_LABELS, PRIORITY_EMOJI, + DEPARTMENT_LABELS, REQUEST_TYPE_LABELS, REQUEST_TYPE_EMOJI +) + +# Initialize logger +logger = getLogger(__name__) + +# Initialize bot with token +bot = Bot(token="7677506032:AAHEqNUr1lIUfNVbLwaWIaPeKKShsCyz3eo") + +# Chat ID for notifications +CHAT_ID = "5057752127" + +def create_status_keyboard(request_id: int, current_status: str) -> InlineKeyboardMarkup: + """Create inline keyboard with status buttons""" + status_transitions = { + 'new': ['in_progress'], + 'in_progress': ['resolved'], + 'resolved': ['closed'], + 'closed': [] + } + + buttons = [] + available_statuses = status_transitions.get(current_status, []) + + for status in available_statuses: + callback_data = f"status_{request_id}_{status}" + logger.debug(f"Creating button with callback_data: {callback_data}") + buttons.append([ + InlineKeyboardButton( + text=STATUS_LABELS[status], + callback_data=callback_data + ) + ]) + + keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) + logger.debug(f"Created keyboard: {keyboard}") + return keyboard + +def format_request_message(request_data: dict) -> str: + """Format request data into a message""" + created_at = datetime.fromisoformat(request_data['created_at']).strftime('%d.%m.%Y %H:%M') + + # Get translated values + department = DEPARTMENT_LABELS.get(request_data['department'], request_data['department']) + request_type = REQUEST_TYPE_LABELS.get(request_data['request_type'], request_data['request_type']) + priority = PRIORITY_LABELS.get(request_data['priority'], request_data['priority']) + status = STATUS_LABELS.get(request_data.get('status', 'new'), 'Неизвестно') + + return ( + f"📋 Заявка #{request_data['id']}\n\n" + f"👤 Сотрудник: {request_data['employee_last_name']} {request_data['employee_first_name']}\n" + f"🏢 Отдел: {department}\n" + f"🚪 Кабинет: {request_data['office']}\n" + f"{REQUEST_TYPE_EMOJI.get(request_data['request_type'], '📝')} Тип заявки: {request_type}\n" + f"{PRIORITY_EMOJI.get(request_data['priority'], '⚪')} Приоритет: {priority}\n\n" + f"📝 Описание:\n{request_data['description']}\n\n" + f"🕒 Создана: {created_at}\n" + f"📊 Статус: {status}" + ) + +async def send_request_notification(request_data: dict): + """Send notification about request to Telegram""" + try: + message = format_request_message(request_data) + keyboard = create_status_keyboard(request_data['id'], request_data.get('status', 'new')) + + await bot.send_message( + chat_id=CHAT_ID, + text=message, + parse_mode="HTML", + reply_markup=keyboard + ) + except Exception as e: + logger.error(f"Error sending Telegram notification: {e}", exc_info=True) + raise + +def send_notification(request_data: dict): + """Wrapper to run async notification in sync context""" + try: + asyncio.run(send_request_notification(request_data)) + except Exception as e: + logger.error(f"Failed to send notification: {e}", exc_info=True) + raise \ No newline at end of file diff --git a/backend/crud.py b/backend/crud.py new file mode 100644 index 0000000..8f4f02b --- /dev/null +++ b/backend/crud.py @@ -0,0 +1,55 @@ +from sqlalchemy.orm import Session +from models import request as models +from schemas import tables +from typing import List + + +def create_request(db: Session, request_data: dict): + db_request = tables.Request( + employee_id=request_data["employee_id"], + department=request_data["department"], + request_type=request_data["request_type"], + priority=request_data["priority"], + description=request_data["description"], + status="new", + ) + db.add(db_request) + db.commit() + db.refresh(db_request) + return db_request + + +def get_requests(db: Session, skip: int = 0, limit: int = 100) -> List[tables.Request]: + return ( + db.query(tables.Request) + .join(tables.Employee) + .add_columns( + tables.Employee.last_name.label("employee_last_name"), + tables.Employee.first_name.label("employee_first_name"), + ) + .offset(skip) + .limit(limit) + .all() + ) + + +def get_requests_by_employee_lastname( + db: Session, last_name: str +) -> List[tables.Request]: + return ( + db.query(tables.Request) + .join(tables.Employee) + .filter(tables.Employee.last_name.ilike(f"%{last_name}%")) + .all() + ) + + +def update_request_status(db: Session, request_id: int, new_status: str): + db_request = ( + db.query(tables.Request).filter(tables.Request.id == request_id).first() + ) + if db_request: + db_request.status = new_status + db.commit() + db.refresh(db_request) + return db_request diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..c0d1878 --- /dev/null +++ b/backend/database.py @@ -0,0 +1,20 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..9b270ca --- /dev/null +++ b/backend/models.py @@ -0,0 +1,23 @@ +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship +from .database import Base + + +class Employee(Base): + __tablename__ = "employees" + + id = Column(Integer, primary_key=True, index=True) + last_name = Column(String, index=True) + # другие поля... + + requests = relationship("Request", back_populates="employee") + + +class Request(Base): + __tablename__ = "requests" + + id = Column(Integer, primary_key=True, index=True) + employee_id = Column(Integer, ForeignKey("employees.id")) + # другие поля... + + employee = relationship("Employee", back_populates="requests") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..b721d3d --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,11 @@ +fastapi==0.110.0 +uvicorn==0.27.1 +sqlalchemy==2.0.27 +pydantic==2.6.3 +pydantic-settings==2.2.1 +python-multipart==0.0.9 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]>=1.7.4 +bcrypt>=4.0.1 +aiogram>=3.4.1 +python-dotenv==1.0.1 \ No newline at end of file diff --git a/backend/run.py b/backend/run.py new file mode 100644 index 0000000..63cb99a --- /dev/null +++ b/backend/run.py @@ -0,0 +1,38 @@ +""" +Main application entry point. +Runs both the FastAPI application and Telegram bot. +""" +import asyncio +import uvicorn +from app.main import app +from app.bot import start_bot +from logging import getLogger + +logger = getLogger(__name__) + +async def run_api(): + """Run FastAPI application""" + config = uvicorn.Config( + app, + host="0.0.0.0", + port=8000, + reload=True + ) + server = uvicorn.Server(config) + await server.serve() + +async def main(): + """Run both bot and API in the main thread""" + try: + # Создаем задачи для бота и API + bot_task = asyncio.create_task(start_bot()) + api_task = asyncio.create_task(run_api()) + + # Запускаем обе задачи + await asyncio.gather(bot_task, api_task) + except Exception as e: + logger.error(f"Application crashed: {e}", exc_info=True) + +if __name__ == "__main__": + # Запускаем в основном потоке + asyncio.run(main()) \ No newline at end of file diff --git a/backend/schemas.py b/backend/schemas.py new file mode 100644 index 0000000..d07d247 --- /dev/null +++ b/backend/schemas.py @@ -0,0 +1,44 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional +from models import RequestStatus + + +class EmployeeBase(BaseModel): + last_name: str + first_name: str + department: str + office: str + + +class EmployeeCreate(EmployeeBase): + password: str + + +class Employee(EmployeeBase): + id: int + created_at: datetime + + class Config: + from_attributes = True + + +class RequestBase(BaseModel): + department: str + request_type: str + priority: str + description: str + + +class RequestCreate(RequestBase): + employee_id: int + + +class Request(RequestBase): + id: int + status: RequestStatus + created_at: datetime + employee_id: int + + class Config: + from_attributes = True diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2606d12 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +version: '3.8' + +services: + frontend: + build: + context: . + dockerfile: docker/frontend/Dockerfile + container_name: support-frontend + volumes: + - frontend_build:/app/dist + networks: + - app-network + + backend: + build: + context: . + dockerfile: docker/backend/Dockerfile + container_name: support-backend + environment: + - TELEGRAM_BOT_TOKEN=7677506032:AAHEqNUr1lIUfNVbLwaWIaPeKKShsCyz3eo + - TELEGRAM_CHAT_ID=-1002037023574 + ports: + - "8000:8000" + volumes: + - ./backend:/app + - ./sql_app.db:/app/sql_app.db:rw + - ./logs:/app/logs:rw + networks: + - app-network + restart: unless-stopped + + nginx: + build: + context: . + dockerfile: docker/nginx/Dockerfile + container_name: support-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - frontend_build:/usr/share/nginx/html + - ./docker/nginx/conf.d:/etc/nginx/conf.d + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + networks: + - app-network + depends_on: + - frontend + - backend + command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + + certbot: + image: certbot/certbot + container_name: support-certbot + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + +networks: + app-network: + driver: bridge + +volumes: + frontend_build: \ No newline at end of file diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile new file mode 100644 index 0000000..cd835d4 --- /dev/null +++ b/docker/backend/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Create logs directory +RUN mkdir -p /app/logs + +# Copy requirements first to leverage Docker cache +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY backend/ . + +# Create volume for logs +VOLUME ["/app/logs"] + +# Expose the port the app runs on +EXPOSE 8000 + +CMD ["python", "run.py"] \ No newline at end of file diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile new file mode 100644 index 0000000..e668995 --- /dev/null +++ b/docker/frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:18-alpine + +WORKDIR /app + +# Копируем package.json и package-lock.json +COPY frontend/package*.json ./ + +# Устанавливаем зависимости +RUN npm install + +# Копируем исходный код +COPY frontend/ ./ + +# Собираем приложение +RUN npm run build + +# Держим контейнер запущенным +CMD ["tail", "-f", "/dev/null"] \ No newline at end of file diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 0000000..53005fe --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,14 @@ +FROM nginx:alpine + +# Копируем конфигурацию nginx +COPY docker/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf + +# Копируем собранные файлы фронтенда +COPY frontend/dist /usr/share/nginx/html/ + +# Удаляем дефолтную страницу nginx +RUN rm -rf /usr/share/nginx/html/50x.html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf new file mode 100644 index 0000000..49d8b5f --- /dev/null +++ b/docker/nginx/conf.d/default.conf @@ -0,0 +1,41 @@ +upstream backend_upstream { + server support-backend:8000; +} + +server { + listen 80 default_server; + server_name itformhelp.ru www.itformhelp.ru; + + root /usr/share/nginx/html; + index index.html; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # SPA routes + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + # API proxy + location /api/ { + proxy_pass http://backend_upstream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# Редирект с IP на домен +server { + listen 80; + server_name 185.139.70.62; + return 301 http://itformhelp.ru$request_uri; +} \ No newline at end of file diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 0000000..2dc3089 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,25 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..027507f --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Администрация КАО + + +
+ + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..74917be --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2908 @@ +{ + "name": "admin-portal", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "admin-portal", + "version": "0.0.0", + "dependencies": { + "@supabase/supabase-js": "^2.39.7", + "@vueuse/core": "^10.9.0", + "chart.js": "^4.4.1", + "lucide-vue-next": "^0.344.0", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "autoprefixer": "^10.4.18", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "^5.2.2", + "vue-tsc": "^2.0.6" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "dependencies": { + "@babel/types": "^7.26.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@supabase/auth-js": { + "version": "2.67.3", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.67.3.tgz", + "integrity": "sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.17.7.tgz", + "integrity": "sha512-aOzOYaTADm/dVTNksyqv9KsbhVa1gHz1Hoxb2ZEF2Ed9H7qlWOfptECQWmkEmrrFjtNaiPrgiSaPECvzI/seDA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.47.10", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.47.10.tgz", + "integrity": "sha512-vJfPF820Ho5WILYHfKiBykDQ1SB9odTHrRZ0JxHfuLMC8GRvv21YLkUZQK7/rSVCkLvD6/ZwMWaOAfdUd//guw==", + "dependencies": { + "@supabase/auth-js": "2.67.3", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.17.7", + "@supabase/realtime-js": "2.11.2", + "@supabase/storage-js": "2.7.1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "peer": true + }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", + "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.11.tgz", + "integrity": "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==", + "dev": true, + "dependencies": { + "@volar/source-map": "2.4.11" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.11.tgz", + "integrity": "sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==", + "dev": true + }, + "node_modules/@volar/typescript": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.11.tgz", + "integrity": "sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==", + "dev": true, + "dependencies": { + "@volar/language-core": "2.4.11", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/language-core": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.10.tgz", + "integrity": "sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==", + "dev": true, + "dependencies": { + "@volar/language-core": "~2.4.8", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^0.2.0", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/alien-signals": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.2.tgz", + "integrity": "sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==", + "dev": true + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.75", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", + "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/lucide-vue-next": { + "version": "0.344.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.344.0.tgz", + "integrity": "sha512-YvexWTTzcRScmOKUvbQvexDmIK63x6NT8lV3Cot9TShQyy3PpFxfx5i2wZNhaZfV8bKqTMRXgCs372rnrtPYKQ==", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz", + "integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "dev": true, + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", + "integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz", + "integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==", + "dev": true, + "dependencies": { + "@volar/typescript": "~2.4.8", + "@vue/language-core": "2.1.10", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0b24ef2 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "admin-portal", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@supabase/supabase-js": "^2.39.7", + "@vueuse/core": "^10.9.0", + "chart.js": "^4.4.1", + "lucide-vue-next": "^0.344.0", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "autoprefixer": "^10.4.18", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "^5.2.2", + "vue-tsc": "^2.0.6" + } +} \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..e99ebc2 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..9bac9cf --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Footer.vue b/frontend/src/components/Footer.vue new file mode 100644 index 0000000..838bd26 --- /dev/null +++ b/frontend/src/components/Footer.vue @@ -0,0 +1,45 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Header.vue b/frontend/src/components/Header.vue new file mode 100644 index 0000000..d58d461 --- /dev/null +++ b/frontend/src/components/Header.vue @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/EmployeeForm.vue b/frontend/src/components/admin/EmployeeForm.vue new file mode 100644 index 0000000..9c25fc9 --- /dev/null +++ b/frontend/src/components/admin/EmployeeForm.vue @@ -0,0 +1,190 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/EmployeeList.vue b/frontend/src/components/admin/EmployeeList.vue new file mode 100644 index 0000000..cebd0bf --- /dev/null +++ b/frontend/src/components/admin/EmployeeList.vue @@ -0,0 +1,125 @@ + + + + diff --git a/frontend/src/components/admin/RequestDescriptionModal.vue b/frontend/src/components/admin/RequestDescriptionModal.vue new file mode 100644 index 0000000..aa1fcb4 --- /dev/null +++ b/frontend/src/components/admin/RequestDescriptionModal.vue @@ -0,0 +1,64 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/RequestList.vue b/frontend/src/components/admin/RequestList.vue new file mode 100644 index 0000000..b6f56f8 --- /dev/null +++ b/frontend/src/components/admin/RequestList.vue @@ -0,0 +1,198 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/RequestPriorityBadge.vue b/frontend/src/components/admin/RequestPriorityBadge.vue new file mode 100644 index 0000000..708f552 --- /dev/null +++ b/frontend/src/components/admin/RequestPriorityBadge.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/RequestStatusBadge.vue b/frontend/src/components/admin/RequestStatusBadge.vue new file mode 100644 index 0000000..5e9f805 --- /dev/null +++ b/frontend/src/components/admin/RequestStatusBadge.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/RequestStatusModal.vue b/frontend/src/components/admin/RequestStatusModal.vue new file mode 100644 index 0000000..8b07404 --- /dev/null +++ b/frontend/src/components/admin/RequestStatusModal.vue @@ -0,0 +1,64 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/StatisticsPanel.vue b/frontend/src/components/admin/StatisticsPanel.vue new file mode 100644 index 0000000..b63f489 --- /dev/null +++ b/frontend/src/components/admin/StatisticsPanel.vue @@ -0,0 +1,95 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/charts/StatusChart.vue b/frontend/src/components/admin/charts/StatusChart.vue new file mode 100644 index 0000000..f2e9b73 --- /dev/null +++ b/frontend/src/components/admin/charts/StatusChart.vue @@ -0,0 +1,87 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/charts/TypesChart.vue b/frontend/src/components/admin/charts/TypesChart.vue new file mode 100644 index 0000000..e1aadf6 --- /dev/null +++ b/frontend/src/components/admin/charts/TypesChart.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/charts/VolumeChart.vue b/frontend/src/components/admin/charts/VolumeChart.vue new file mode 100644 index 0000000..178f684 --- /dev/null +++ b/frontend/src/components/admin/charts/VolumeChart.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/admin/employee/EmployeeFormModal.vue b/frontend/src/components/admin/employee/EmployeeFormModal.vue new file mode 100644 index 0000000..3f59f75 --- /dev/null +++ b/frontend/src/components/admin/employee/EmployeeFormModal.vue @@ -0,0 +1,151 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/request/RequestForm.vue b/frontend/src/components/request/RequestForm.vue new file mode 100644 index 0000000..cecbac9 --- /dev/null +++ b/frontend/src/components/request/RequestForm.vue @@ -0,0 +1,162 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/ui/FormField.vue b/frontend/src/components/ui/FormField.vue new file mode 100644 index 0000000..c1a3ba8 --- /dev/null +++ b/frontend/src/components/ui/FormField.vue @@ -0,0 +1,60 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/ui/Notification.vue b/frontend/src/components/ui/Notification.vue new file mode 100644 index 0000000..043730b --- /dev/null +++ b/frontend/src/components/ui/Notification.vue @@ -0,0 +1,61 @@ + + + \ No newline at end of file diff --git a/frontend/src/composables/useNotification.ts b/frontend/src/composables/useNotification.ts new file mode 100644 index 0000000..061b407 --- /dev/null +++ b/frontend/src/composables/useNotification.ts @@ -0,0 +1,31 @@ +import { ref } from 'vue'; + +export function useNotification() { + const show = ref(false); + const message = ref(''); + const type = ref<'success' | 'error'>('success'); + + function showNotification(newMessage: string, newType: 'success' | 'error' = 'success', duration = 3000) { + message.value = newMessage; + type.value = newType; + show.value = true; + + if (duration > 0) { + setTimeout(() => { + show.value = false; + }, duration); + } + } + + function hideNotification() { + show.value = false; + } + + return { + show, + message, + type, + showNotification, + hideNotification + }; +} \ No newline at end of file diff --git a/frontend/src/composables/utils/constants.ts b/frontend/src/composables/utils/constants.ts new file mode 100644 index 0000000..0d9db15 --- /dev/null +++ b/frontend/src/composables/utils/constants.ts @@ -0,0 +1,17 @@ +export const requestTypes = [ + { value: 'hardware', label: 'Проблемы с оборудованием' }, + { value: 'software', label: 'Проблемы с программным обеспечением' }, + { value: 'network', label: 'Проблемы с сетью' }, + { value: 'access', label: 'Доступ к системам' }, + { value: 'other', label: 'Другое' } +] as const; + +export const departments = [ + { value: 'aho', label: 'Административно-хозяйственный отдел' }, + { value: 'gkh', label: 'Жилищно-коммунальное хозяйство' }, + { value: 'general', label: 'Общий отдел' } +] as const; + +export function getRequestTypeLabel(value: string): string { + return requestTypes.find(type => type.value === value)?.label || value; +} \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..854aa5f --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import App from './App.vue'; +import router from './router'; +import './index.css'; + +const app = createApp(App); +const pinia = createPinia(); + +app.use(pinia); +app.use(router); +app.mount('#app'); \ No newline at end of file diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..86bd1da --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,48 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import { useAuthStore } from '@/stores/auth'; +import LoginView from '../views/LoginView.vue'; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + name: 'login', + component: LoginView + }, + { + path: '/support', + name: 'support', + // Ошибка: Не найден модуль '../views/SupportView.vue' + component: () => import('../views/SupportView.vue'), + meta: { requiresAuth: true } + }, + { + path: '/admin', + name: 'admin-login', + // Ошибка: Не найден модуль '../views/admin/AdminLoginView.vue' + component: () => import('../views/admin/AdminLoginView.vue') + }, + { + path: '/admin/dashboard', + name: 'admin-dashboard', + // Ошибка: Не найден модуль '../views/admin/DashboardView.vue' + component: () => import('../views/admin/DashboardView.vue'), + meta: { requiresAdmin: true } + } + ] +}); + +router.beforeEach((to, _, next) => { + const authStore = useAuthStore(); + + if (to.meta.requiresAuth && !authStore.isAuthenticated) { + next({ name: 'login' }); + } else if (to.meta.requiresAdmin && !authStore.isAdmin) { + next({ name: 'admin-login' }); + } else { + next(); + } +}); + +export default router; \ No newline at end of file diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..d45506d --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -0,0 +1,79 @@ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; +import type { User } from '@/types/auth'; + +export const useAuthStore = defineStore('auth', () => { + const user = ref(null); + const isAdmin = ref(false); + + const isAuthenticated = computed(() => !!user.value); + + function setUser(newUser: User | null) { + user.value = newUser; + } + + function setAdmin(value: boolean) { + isAdmin.value = value; + } + + async function login(lastName: string, password: string): Promise { + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ lastName, password }), + }); + + if (!response.ok) { + return false; + } + + const userData = await response.json(); + setUser(userData); + return true; + } catch (error) { + console.error('Login error:', error); + return false; + } + } + + async function adminLogin(username: string, password: string): Promise { + try { + const response = await fetch('/api/auth/admin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }); + + if (!response.ok) { + return false; + } + + setAdmin(true); + return true; + } catch (error) { + console.error('Admin login error:', error); + return false; + } + } + + function logout() { + user.value = null; + isAdmin.value = false; + } + + return { + user, + isAdmin, + isAuthenticated, + setUser, + setAdmin, + login, + adminLogin, + logout + }; +}); \ No newline at end of file diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts new file mode 100644 index 0000000..d119333 --- /dev/null +++ b/frontend/src/types/auth.ts @@ -0,0 +1,36 @@ +export interface User { + id: string; + firstName: string; + lastName: string; + department: string; + createdAt: string; +} + +export interface LoginCredentials { + lastName: string; + password: string; +} + +export interface AdminCredentials { + username: string; + password: string; +} + +export interface Employee { + id: number; + first_name: string; + last_name: string; + department: string; + office: string; +} + +export interface Request { + id: number; + employee_last_name: string; + employee_first_name: string; + employee_office: string; + request_type: string; + priority: 'low' | 'medium' | 'high' | 'critical'; + status: 'new' | 'in_progress' | 'resolved' | 'closed'; + created_at: string; +} \ No newline at end of file diff --git a/frontend/src/types/employee.ts b/frontend/src/types/employee.ts new file mode 100644 index 0000000..59c8724 --- /dev/null +++ b/frontend/src/types/employee.ts @@ -0,0 +1,17 @@ +export interface Employee { + id: number; + first_name: string; + last_name: string; + department: string; + office: string; + created_at?: string; + } + + export interface EmployeeFormData { + first_name: string; + last_name: string; + department: string; + office: string; + password?: string; // Делаем password опциональным + } + \ No newline at end of file diff --git a/frontend/src/types/request.ts b/frontend/src/types/request.ts new file mode 100644 index 0000000..93982d1 --- /dev/null +++ b/frontend/src/types/request.ts @@ -0,0 +1,14 @@ +export type RequestStatus = 'new' | 'in_progress' | 'resolved' | 'closed'; + +export interface Request { + id: number; + status: RequestStatus; + created_at: string; + employee_last_name: string; + employee_first_name: string; + employee_office: string; + request_type: string; + priority: 'low' | 'medium' | 'high' | 'critical'; + description: string; + } + \ No newline at end of file diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue new file mode 100644 index 0000000..dd76400 --- /dev/null +++ b/frontend/src/views/LoginView.vue @@ -0,0 +1,98 @@ + + + diff --git a/frontend/src/views/SupportView.vue b/frontend/src/views/SupportView.vue new file mode 100644 index 0000000..02c0e65 --- /dev/null +++ b/frontend/src/views/SupportView.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/admin/AdminLoginView.vue b/frontend/src/views/admin/AdminLoginView.vue new file mode 100644 index 0000000..e7995c4 --- /dev/null +++ b/frontend/src/views/admin/AdminLoginView.vue @@ -0,0 +1,97 @@ + + + diff --git a/frontend/src/views/admin/DashboardView.vue b/frontend/src/views/admin/DashboardView.vue new file mode 100644 index 0000000..2e472af --- /dev/null +++ b/frontend/src/views/admin/DashboardView.vue @@ -0,0 +1,64 @@ + + + \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..ff48818 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..cff8d71 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Paths */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..099658c --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..972c4b0 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { fileURLToPath, URL } from 'url'; + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true + } + } + } +}); \ No newline at end of file diff --git a/init-letsencrypt.sh b/init-letsencrypt.sh new file mode 100644 index 0000000..7dcd132 --- /dev/null +++ b/init-letsencrypt.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Остановить все контейнеры +docker compose down + +# Создать временную директорию для webroot +mkdir -p ./docker/certbot/www + +# Запустить nginx +docker compose up -d frontend + +# Подождать, пока nginx запустится +echo "Waiting for nginx to start..." +sleep 5 + +# Получить тестовый сертификат +docker compose run --rm certbot + +# Если тестовый сертификат получен успешно, получить боевой сертификат +if [ $? -eq 0 ]; then + echo "Test certificate obtained successfully. Getting production certificate..." + docker compose run --rm certbot certonly --webroot --webroot-path=/var/www/html --email admin@itformhelp.ru --agree-tos --no-eff-email --force-renewal -d itformhelp.ru -d www.itformhelp.ru +fi + +# Перезапустить все сервисы +docker compose down +docker compose up -d diff --git a/sql_app.db b/sql_app.db new file mode 100644 index 0000000..c216915 Binary files /dev/null and b/sql_app.db differ diff --git a/ssl-init.sh b/ssl-init.sh new file mode 100644 index 0000000..3c78630 --- /dev/null +++ b/ssl-init.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Остановить все контейнеры +docker compose down -v + +# Создать необходимые директории +mkdir -p ./certbot/www +mkdir -p ./certbot/conf + +# Запустить только nginx для первичной проверки +docker compose up -d frontend + +# Подождать, пока nginx запустится +sleep 5 + +# Запустить certbot для получения сертификата +docker compose run --rm certbot + +# Перезапустить все сервисы +docker compose down +docker compose up -d