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

add websockets support

This commit is contained in:
MoonTestUse1
2025-01-05 01:23:12 +06:00
parent 787d19528b
commit 7997184442
4 changed files with 307 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
"""Requests router""" """Requests router"""
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List, Optional from typing import List, Optional
from ..database import get_db from ..database import get_db
@@ -8,9 +8,32 @@ from ..schemas.request import Request, RequestCreate, RequestUpdate
from ..models.request import RequestStatus from ..models.request import RequestStatus
from ..utils.auth import get_current_employee, get_current_admin from ..utils.auth import get_current_employee, get_current_admin
from ..utils.telegram import notify_new_request from ..utils.telegram import notify_new_request
from ..websockets.notifications import notification_manager
router = APIRouter() router = APIRouter()
@router.websocket("/ws/admin")
async def websocket_admin_endpoint(websocket: WebSocket):
"""WebSocket endpoint для админов"""
await notification_manager.connect(websocket, "admin")
try:
while True:
data = await websocket.receive_text()
# Здесь можно добавить обработку сообщений от админа
except WebSocketDisconnect:
notification_manager.disconnect(websocket, "admin")
@router.websocket("/ws/employee/{employee_id}")
async def websocket_employee_endpoint(websocket: WebSocket, employee_id: int):
"""WebSocket endpoint для сотрудников"""
await notification_manager.connect(websocket, "employee")
try:
while True:
data = await websocket.receive_text()
# Здесь можно добавить обработку сообщений от сотрудника
except WebSocketDisconnect:
notification_manager.disconnect(websocket, "employee")
@router.post("/", response_model=Request) @router.post("/", response_model=Request)
async def create_request( async def create_request(
request: RequestCreate, request: RequestCreate,
@@ -21,6 +44,17 @@ async def create_request(
db_request = requests.create_request(db, request, current_employee["id"]) db_request = requests.create_request(db, request, current_employee["id"])
# Отправляем уведомление в Telegram # Отправляем уведомление в Telegram
await notify_new_request(db_request.id) await notify_new_request(db_request.id)
# Отправляем уведомление через WebSocket всем админам
await notification_manager.broadcast_to_admins({
"type": "new_request",
"data": {
"id": db_request.id,
"employee_id": current_employee["id"],
"status": db_request.status,
"priority": db_request.priority,
"created_at": db_request.created_at.isoformat()
}
})
return db_request return db_request
@router.get("/my", response_model=List[Request]) @router.get("/my", response_model=List[Request])
@@ -43,7 +77,7 @@ def get_all_requests(
return requests.get_requests(db, status=status, skip=skip, limit=limit) return requests.get_requests(db, status=status, skip=skip, limit=limit)
@router.patch("/{request_id}/status", response_model=Request) @router.patch("/{request_id}/status", response_model=Request)
def update_request_status( async def update_request_status(
request_id: int, request_id: int,
request_update: RequestUpdate, request_update: RequestUpdate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
@@ -53,6 +87,15 @@ def update_request_status(
db_request = requests.update_request_status(db, request_id, request_update.status) db_request = requests.update_request_status(db, request_id, request_update.status)
if db_request is None: if db_request is None:
raise HTTPException(status_code=404, detail="Request not found") raise HTTPException(status_code=404, detail="Request not found")
# Отправляем уведомление через WebSocket
await notification_manager.broadcast_to_admins({
"type": "status_update",
"data": {
"id": request_id,
"status": request_update.status
}
})
return db_request return db_request
@router.get("/statistics") @router.get("/statistics")

View File

@@ -0,0 +1,36 @@
from fastapi import WebSocket
from typing import Dict, List
import json
class NotificationManager:
def __init__(self):
self.active_connections: Dict[str, List[WebSocket]] = {
"admin": [], # Подключения админов
"employee": [] # Подключения сотрудников
}
async def connect(self, websocket: WebSocket, client_type: str):
await websocket.accept()
self.active_connections[client_type].append(websocket)
def disconnect(self, websocket: WebSocket, client_type: str):
self.active_connections[client_type].remove(websocket)
async def broadcast_to_admins(self, message: dict):
"""Отправка сообщения всем подключенным админам"""
for connection in self.active_connections["admin"]:
try:
await connection.send_json(message)
except:
# Если не удалось отправить сообщение, пропускаем
continue
async def broadcast_to_employees(self, employee_id: int, message: dict):
"""Отправка сообщения конкретному сотруднику"""
for connection in self.active_connections["employee"]:
try:
await connection.send_json(message)
except:
continue
notification_manager = NotificationManager()

View File

@@ -0,0 +1,74 @@
import { ref } from 'vue'
class WebSocketClient {
private socket: WebSocket | null = null
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectTimeout = 3000
private messageHandlers: ((data: any) => void)[] = []
// Состояние подключения
isConnected = ref(false)
connect(type: 'admin' | 'employee', id?: number) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const baseUrl = `${protocol}//${window.location.host}`
const url = type === 'admin' ?
`${baseUrl}/api/requests/ws/admin` :
`${baseUrl}/api/requests/ws/employee/${id}`
this.socket = new WebSocket(url)
this.socket.onopen = () => {
console.log('WebSocket connected')
this.isConnected.value = true
this.reconnectAttempts = 0
}
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data)
this.messageHandlers.forEach(handler => handler(data))
}
this.socket.onclose = () => {
console.log('WebSocket disconnected')
this.isConnected.value = false
this.tryReconnect()
}
this.socket.onerror = (error) => {
console.error('WebSocket error:', error)
this.isConnected.value = false
}
}
private tryReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
setTimeout(() => {
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
this.connect('admin')
}, this.reconnectTimeout)
}
}
addMessageHandler(handler: (data: any) => void) {
this.messageHandlers.push(handler)
}
removeMessageHandler(handler: (data: any) => void) {
const index = this.messageHandlers.indexOf(handler)
if (index > -1) {
this.messageHandlers.splice(index, 1)
}
}
disconnect() {
if (this.socket) {
this.socket.close()
this.socket = null
}
}
}
export const wsClient = new WebSocketClient()

View File

@@ -0,0 +1,152 @@
<template>
<div class="p-6">
<div class="mb-6">
<h1 class="text-2xl font-bold mb-4">Панель администратора</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-white p-4 rounded-lg shadow">
<h3 class="text-lg font-semibold mb-2">Новые заявки</h3>
<p class="text-3xl font-bold text-blue-600">{{ statistics.by_status?.NEW || 0 }}</p>
</div>
<div class="bg-white p-4 rounded-lg shadow">
<h3 class="text-lg font-semibold mb-2">В работе</h3>
<p class="text-3xl font-bold text-yellow-600">{{ statistics.by_status?.IN_PROGRESS || 0 }}</p>
</div>
<div class="bg-white p-4 rounded-lg shadow">
<h3 class="text-lg font-semibold mb-2">Завершено</h3>
<p class="text-3xl font-bold text-green-600">{{ statistics.by_status?.COMPLETED || 0 }}</p>
</div>
<div class="bg-white p-4 rounded-lg shadow">
<h3 class="text-lg font-semibold mb-2">Отклонено</h3>
<p class="text-3xl font-bold text-red-600">{{ statistics.by_status?.REJECTED || 0 }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold mb-4">Последние заявки</h2>
<div class="overflow-x-auto">
<table class="min-w-full table-auto">
<thead>
<tr class="bg-gray-100">
<th class="px-4 py-2 text-left">ID</th>
<th class="px-4 py-2 text-left">Сотрудник</th>
<th class="px-4 py-2 text-left">Тип</th>
<th class="px-4 py-2 text-left">Приоритет</th>
<th class="px-4 py-2 text-left">Статус</th>
<th class="px-4 py-2 text-left">Дата создания</th>
<th class="px-4 py-2 text-left">Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="request in requests" :key="request.id" class="border-b">
<td class="px-4 py-2">#{{ request.id }}</td>
<td class="px-4 py-2">{{ request.employee_name }}</td>
<td class="px-4 py-2">{{ request.request_type }}</td>
<td class="px-4 py-2">
<span :class="getPriorityClass(request.priority)">
{{ request.priority }}
</span>
</td>
<td class="px-4 py-2">
<span :class="getStatusClass(request.status)">
{{ request.status }}
</span>
</td>
<td class="px-4 py-2">{{ formatDate(request.created_at) }}</td>
<td class="px-4 py-2">
<button @click="openRequestDetails(request)" class="text-blue-600 hover:text-blue-800">
Подробнее
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import axios from '@/plugins/axios'
import { wsClient } from '@/plugins/websocket'
import { formatDate } from '@/utils/date'
const requests = ref([])
const statistics = ref({
total: 0,
by_status: {}
})
// Загрузка данных
const fetchData = async () => {
try {
const [requestsResponse, statsResponse] = await Promise.all([
axios.get('/api/requests/admin'),
axios.get('/api/requests/statistics')
])
requests.value = requestsResponse.data
statistics.value = statsResponse.data
} catch (error) {
console.error('Error fetching data:', error)
}
}
// Обработчик WebSocket сообщений
const handleWebSocketMessage = (data: any) => {
if (data.type === 'new_request') {
// Обновляем статистику при получении новой заявки
statistics.value.total++
statistics.value.by_status.NEW = (statistics.value.by_status.NEW || 0) + 1
// Добавляем новую заявку в начало списка
requests.value.unshift(data.data)
} else if (data.type === 'status_update') {
// Обновляем статус заявки в списке
const request = requests.value.find(r => r.id === data.data.id)
if (request) {
const oldStatus = request.status
request.status = data.data.status
// Обновляем статистику
statistics.value.by_status[oldStatus]--
statistics.value.by_status[data.data.status] = (statistics.value.by_status[data.data.status] || 0) + 1
}
}
}
// Подключение к WebSocket при монтировании компонента
onMounted(() => {
fetchData()
wsClient.connect('admin')
wsClient.addMessageHandler(handleWebSocketMessage)
})
// Отключение от WebSocket при размонтировании компонента
onUnmounted(() => {
wsClient.removeMessageHandler(handleWebSocketMessage)
wsClient.disconnect()
})
const getPriorityClass = (priority: string) => {
const classes = {
HIGH: 'text-red-600',
MEDIUM: 'text-yellow-600',
LOW: 'text-green-600'
}
return classes[priority] || ''
}
const getStatusClass = (status: string) => {
const classes = {
NEW: 'text-blue-600',
IN_PROGRESS: 'text-yellow-600',
COMPLETED: 'text-green-600',
REJECTED: 'text-red-600'
}
return classes[status] || ''
}
const openRequestDetails = (request: any) => {
// Здесь можно добавить логику открытия модального окна с деталями заявки
console.log('Opening request details:', request)
}
</script>