1
0
mirror of https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git synced 2025-08-14 00:25:46 +02:00

Initial commit

This commit is contained in:
MoonTestUse1
2024-12-23 19:27:44 +06:00
commit e81df4c87e
4952 changed files with 1705479 additions and 0 deletions

1
backend/app/__init__.py Normal file
View File

@@ -0,0 +1 @@
# FastAPI application package

81
backend/app/bot.py Normal file
View File

@@ -0,0 +1,81 @@
# from logging import getLogger
# from aiogram import Bot, Dispatcher, types
# from aiogram.filters import CommandStart
# from sqlalchemy.orm import Session
# from .database import get_db
# from .crud import requests
# from .utils.telegram import STATUS_LABELS, create_status_keyboard, format_request_message
# # Initialize logger
# logger = getLogger(__name__)
# # Initialize bot and dispatcher
# bot = Bot(token="7677506032:AAHEqNUr1lIUfNVbLwaWIaPeKKShsCyz3eo")
# dp = Dispatcher()
# @dp.callback_query(lambda c: c.data and c.data.startswith('status_'))
# async def process_status_update(callback: types.CallbackQuery):
# """Handle status update button clicks"""
# try:
# # Parse callback data using underscore as separator
# parts = callback.data.split('_')
# logger.info(f"Parsed callback parts: {parts}")
# if len(parts) < 3:
# logger.warning(f"Invalid callback data format: {parts}")
# return
# request_id = int(parts[1])
# # Handle 'in_progress' case where we have an extra underscore
# 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}")
# # Get database session
# db = next(get_db())
# try:
# # Update request status in database
# 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
# # Update message with new status
# 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)
# @dp.message(CommandStart())
# async def start_command(message: types.Message):
# """Handle /start command"""
# await message.answer(
# "👋 Привет! Я бот технической поддержки.\n"
# "Я буду отправлять уведомления о новых заявках и позволю менять их статус."
# )
# async def start_bot():
# """Start the bot"""
# try:
# await dp.start_polling(bot)
# finally:
# await bot.session.close()

View File

@@ -0,0 +1 @@
from .bot import dp, start_bot

14
backend/app/bot/bot.py Normal file
View File

@@ -0,0 +1,14 @@
from aiogram import Bot, Dispatcher
from app.bot.config import settings
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
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()

12
backend/app/bot/config.py Normal file
View File

@@ -0,0 +1,12 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
TELEGRAM_BOT_TOKEN: str = "7677506032:AAHoVqFJs3IZKNK2NVzGnzKUn1hjVtU5Ryk"
TELEGRAM_CHAT_ID: str = "5057752127"
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -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: "📝",
}

View File

@@ -0,0 +1,2 @@
from .start import dp
from .status import dp

View File

@@ -0,0 +1,12 @@
from aiogram import types
from aiogram.filters import CommandStart
from ..bot import dp
@dp.message(CommandStart())
async def start_command(message: types.Message):
"""Handle /start command"""
await message.answer(
"👋 Привет! Я бот технической поддержки.\n"
"Я буду отправлять уведомления о новых заявках и позволю менять их статус."
)

View File

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

View File

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

View File

@@ -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"📋 <b>Заявка #{request_data['id']}</b>\n\n"
f"👤 <b>Сотрудник:</b> {request_data['employee_last_name']} {request_data['employee_first_name']}\n"
f"🏢 <b>Отдел:</b> {department}\n"
f"🚪 <b>Кабинет:</b> {request_data['office']}\n"
f"{REQUEST_TYPE_EMOJI.get(request_data['request_type'], '📝')} <b>Тип заявки:</b> {request_type}\n"
f"{PRIORITY_EMOJI.get(request_data['priority'], '')} <b>Приоритет:</b> {priority}\n\n"
f"📝 <b>Описание:</b>\n<blockquote>{request_data['description']}</blockquote>\n\n"
f"🕒 <b>Создана:</b> {created_at}\n"
f"📊 <b>Статус:</b> {status}"
)

View File

@@ -0,0 +1,41 @@
import asyncio
from logging import getLogger
from aiogram.client.session.aiohttp import AiohttpSession
from .bot import bot
from .keyboards import create_status_keyboard
from .messages import format_request_message
from .config import settings
logger = getLogger(__name__)
async def send_request_notification(request_data: dict):
try:
message = format_request_message(request_data)
keyboard = create_status_keyboard(
request_data["id"], request_data.get("status", "new")
)
async with AiohttpSession() as session:
bot.session = session
await bot.send_message(
chat_id=settings.TELEGRAM_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):
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(send_request_notification(request_data))
loop.close()
except Exception as e:
logger.error(f"Failed to send notification: {e}", exc_info=True)
raise

21
backend/app/crud/auth.py Normal file
View File

@@ -0,0 +1,21 @@
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 employee
def authenticate_admin(db: Session, username: str, password: str):
# Здесь можно добавить логику для админа, пока используем хардкод
if username == "admin" and password == "admin66":
return True
return False

View File

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

View File

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

View File

@@ -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 ""
# 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],
}

20
backend/app/database.py Normal file
View File

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

176
backend/app/main.py Normal file
View File

@@ -0,0 +1,176 @@
from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from typing import List
from .models import employee as employee_models
from .models import request as request_models
from .schemas import tables
from .crud import employees, requests, auth, statistics
from .database import engine, get_db
from .models.request import StatusUpdate
from .bot.notifications import send_notification
from .bot import start_bot
import threading
import asyncio
tables.Base.metadata.create_all(bind=engine)
app = FastAPI()
def run_bot():
asyncio.run(start_bot())
bot_thread = threading.Thread(target=run_bot, daemon=True)
bot_thread.start()
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Auth endpoints
@app.post("/api/auth/login")
def login(credentials: dict, db: Session = Depends(get_db)):
employee = auth.authenticate_employee(
db, credentials["lastName"], credentials["password"]
)
if not employee:
raise HTTPException(status_code=401, detail="Invalid credentials")
return {
"id": employee.id,
"firstName": employee.first_name,
"lastName": employee.last_name,
"department": employee.department,
"createdAt": employee.created_at,
}
@app.post("/api/auth/admin")
def admin_login(credentials: dict, db: Session = Depends(get_db)):
if not auth.authenticate_admin(
db, credentials["username"], credentials["password"]
):
raise HTTPException(status_code=401, detail="Неверные учетные данные")
return {"success": True}
# Employee endpoints
@app.post("/api/employees/", response_model=employee_models.Employee)
def create_employee(
employee: employee_models.EmployeeCreate, db: Session = Depends(get_db)
):
db_employee = employees.get_employee_by_lastname(db, employee.last_name)
if db_employee:
raise HTTPException(status_code=400, detail="Last name already registered")
return employees.create_employee(db=db, employee=employee)
@app.get("/api/employees/", response_model=List[employee_models.Employee])
def read_employees(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
return employees.get_employees(db, skip=skip, limit=limit)
@app.patch("/api/employees/{employee_id}")
def update_employee(employee_id: int, data: dict, db: Session = Depends(get_db)):
return employees.update_employee(db, employee_id, data)
# Request endpoints
@app.post("/api/requests/")
def create_request(
request: request_models.RequestCreate, db: Session = Depends(get_db)
):
# Create request in database
new_request = requests.create_request(db=db, request=request)
# Get employee details for the notification
employee = employees.get_employee(db, new_request.employee_id)
# Prepare notification data
notification_data = {
"id": new_request.id,
"employee_last_name": employee.last_name,
"employee_first_name": employee.first_name,
"department": new_request.department,
"office": employee.office,
"request_type": new_request.request_type,
"priority": new_request.priority,
"description": new_request.description,
"created_at": new_request.created_at.isoformat(),
}
# Send notification to Telegram (non-blocking)
try:
send_notification(notification_data)
except Exception as e:
print(f"Failed to send Telegram notification: {e}")
return new_request
@app.patch("/api/requests/{request_id}/status")
def update_request_status(
request_id: int,
status_update: request_models.StatusUpdate,
db: Session = Depends(get_db),
):
try:
request = requests.update_request_status(db, request_id, status_update.status)
if request is None:
raise HTTPException(status_code=404, detail="Request not found")
return request
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/requests/", response_model=request_models.Request)
def create_request(request_data: dict, db: Session = Depends(get_db)):
return requests.create_request(db=db, request_data=request_data)
@app.get("/api/requests/", response_model=List[request_models.RequestWithEmployee])
def read_requests(
skip: int = 0,
limit: int = 100,
last_name: str = None,
db: Session = Depends(get_db),
):
if last_name:
return requests.get_requests_by_employee_lastname(db, last_name)
return requests.get_requests(db, skip=skip, limit=limit)
@app.patch("/api/requests/{request_id}/status")
def update_request_status(request_id: int, status: str, db: Session = Depends(get_db)):
request = requests.update_request_status(db, request_id, status)
if request is None:
raise HTTPException(status_code=404, detail="Request not found")
return request
@app.patch("/api/requests/{request_id}/status")
def update_request_status(
request_id: int, status_update: StatusUpdate, db: Session = Depends(get_db)
):
try:
request = requests.update_request_status(db, request_id, status_update.status)
if request is None:
raise HTTPException(status_code=404, detail="Request not found")
return request
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail="Internal server error")
@app.get("/api/statistics")
def get_statistics(period: str = "week", db: Session = Depends(get_db)):
return statistics.get_statistics(db, period)

View File

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

View File

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

View File

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

11
backend/app/utils/auth.py Normal file
View File

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

View File

@@ -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': '📝'
}

View File

@@ -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"📋 <b>Заявка #{request_data['id']}</b>\n\n"
f"👤 <b>Сотрудник:</b> {request_data['employee_last_name']} {request_data['employee_first_name']}\n"
f"🏢 <b>Отдел:</b> {department}\n"
f"🚪 <b>Кабинет:</b> {request_data['office']}\n"
f"{REQUEST_TYPE_EMOJI.get(request_data['request_type'], '📝')} <b>Тип заявки:</b> {request_type}\n"
f"{PRIORITY_EMOJI.get(request_data['priority'], '')} <b>Приоритет:</b> {priority}\n\n"
f"📝 <b>Описание:</b>\n{request_data['description']}\n\n"
f"🕒 <b>Создана:</b> {created_at}\n"
f"📊 <b>Статус:</b> {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