Initial commit: Базовая структура сайта
This commit is contained in:
0
backend/app/api/__init__.py
Executable file
0
backend/app/api/__init__.py
Executable file
15
backend/app/api/v1/__init__.py
Executable file
15
backend/app/api/v1/__init__.py
Executable file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
API v1 роутер
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from app.api.v1 import auth, summary, timeline, metrics, leave, admin
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(summary.router, prefix="/summary", tags=["summary"])
|
||||
api_router.include_router(timeline.router, prefix="/timeline", tags=["timeline"])
|
||||
api_router.include_router(metrics.router, prefix="/metrics", tags=["metrics"])
|
||||
api_router.include_router(leave.router, prefix="/leave", tags=["leave"])
|
||||
api_router.include_router(admin.router, prefix="/admin", tags=["admin"])
|
||||
|
||||
120
backend/app/api/v1/admin.py
Executable file
120
backend/app/api/v1/admin.py
Executable file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Модуль Панель Администратора
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.core.database import get_service_db
|
||||
from app.core.security import get_admin_user, get_password_hash
|
||||
from app.models.service_db import AppUser, AppRole, AppConfiguration
|
||||
from app.schemas.user import UserCreate, UserResponse
|
||||
from app.schemas.config import ConfigResponse, ConfigUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/users", response_model=List[UserResponse])
|
||||
async def get_users(
|
||||
admin_user: AppUser = Depends(get_admin_user),
|
||||
db: Session = Depends(get_service_db)
|
||||
):
|
||||
"""Получение списка всех пользователей сервиса"""
|
||||
users = db.query(AppUser).all()
|
||||
result = []
|
||||
for user in users:
|
||||
result.append(UserResponse(
|
||||
id=user.id,
|
||||
login=user.login,
|
||||
role_id=user.role_id,
|
||||
role_name=user.role.role_name if user.role else None,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
user_data: UserCreate,
|
||||
admin_user: AppUser = Depends(get_admin_user),
|
||||
db: Session = Depends(get_service_db)
|
||||
):
|
||||
"""Создание нового пользователя сервиса"""
|
||||
# Проверка существования логина
|
||||
existing_user = db.query(AppUser).filter(AppUser.login == user_data.login).first()
|
||||
if existing_user:
|
||||
raise HTTPException(status_code=400, detail="Пользователь с таким логином уже существует")
|
||||
|
||||
# Проверка существования роли
|
||||
role = db.query(AppRole).filter(AppRole.id == user_data.role_id).first()
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="Роль не найдена")
|
||||
|
||||
new_user = AppUser(
|
||||
login=user_data.login,
|
||||
hashed_password=get_password_hash(user_data.password),
|
||||
role_id=user_data.role_id,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
|
||||
return UserResponse(
|
||||
id=new_user.id,
|
||||
login=new_user.login,
|
||||
role_id=new_user.role_id,
|
||||
role_name=new_user.role.role_name if new_user.role else None,
|
||||
is_active=new_user.is_active,
|
||||
created_at=new_user.created_at
|
||||
)
|
||||
|
||||
|
||||
@router.get("/config", response_model=List[ConfigResponse])
|
||||
async def get_config(
|
||||
admin_user: AppUser = Depends(get_admin_user),
|
||||
db: Session = Depends(get_service_db)
|
||||
):
|
||||
"""Получение конфигурации (нечувствительные данные)"""
|
||||
config_items = db.query(AppConfiguration).filter(
|
||||
AppConfiguration.key.in_(["manictime_host", "manictime_port", "manictime_dbname"])
|
||||
).all()
|
||||
|
||||
result = []
|
||||
for item in config_items:
|
||||
result.append(ConfigResponse(key=item.key, value=item.value))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/config", response_model=ConfigResponse)
|
||||
async def update_config(
|
||||
config_data: ConfigUpdate,
|
||||
admin_user: AppUser = Depends(get_admin_user),
|
||||
db: Session = Depends(get_service_db)
|
||||
):
|
||||
"""Обновление конфигурации (нечувствительные данные)"""
|
||||
allowed_keys = ["manictime_host", "manictime_port", "manictime_dbname"]
|
||||
|
||||
if config_data.key not in allowed_keys:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Ключ '{config_data.key}' не разрешен для редактирования"
|
||||
)
|
||||
|
||||
config_item = db.query(AppConfiguration).filter(
|
||||
AppConfiguration.key == config_data.key
|
||||
).first()
|
||||
|
||||
if not config_item:
|
||||
config_item = AppConfiguration(key=config_data.key, value=config_data.value)
|
||||
db.add(config_item)
|
||||
else:
|
||||
config_item.value = config_data.value
|
||||
|
||||
db.commit()
|
||||
db.refresh(config_item)
|
||||
|
||||
return ConfigResponse(key=config_item.key, value=config_item.value)
|
||||
|
||||
43
backend/app/api/v1/auth.py
Executable file
43
backend/app/api/v1/auth.py
Executable file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Модуль аутентификации
|
||||
"""
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_service_db
|
||||
from app.core.security import verify_password, create_access_token
|
||||
from app.core.config import settings
|
||||
from app.models.service_db import AppUser
|
||||
from app.schemas.auth import LoginRequest, Token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/token", response_model=Token)
|
||||
async def login(
|
||||
login_data: LoginRequest,
|
||||
db: Session = Depends(get_service_db)
|
||||
):
|
||||
"""Аутентификация пользователя и получение JWT токена"""
|
||||
user = db.query(AppUser).filter(AppUser.login == login_data.login).first()
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверный логин или пароль"
|
||||
)
|
||||
|
||||
if not verify_password(login_data.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверный логин или пароль"
|
||||
)
|
||||
|
||||
access_token_expires = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.id, "role": user.role.role_name},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
99
backend/app/api/v1/leave.py
Executable file
99
backend/app/api/v1/leave.py
Executable file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Модуль Отпуска/Больничные
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import date
|
||||
from typing import List
|
||||
from app.core.database import get_service_db
|
||||
from app.core.security import get_current_user, get_admin_user
|
||||
from app.models.service_db import AppUser, LeaveEvent
|
||||
from app.schemas.leave import LeaveEventCreate, LeaveEventResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/events", response_model=List[LeaveEventResponse])
|
||||
async def get_leave_events(
|
||||
start_date: date = Query(..., description="Начальная дата"),
|
||||
end_date: date = Query(..., description="Конечная дата"),
|
||||
current_user: AppUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_service_db)
|
||||
):
|
||||
"""
|
||||
Получение списка событий отпусков и больничных за период
|
||||
"""
|
||||
if start_date > end_date:
|
||||
raise HTTPException(status_code=400, detail="Начальная дата должна быть раньше конечной")
|
||||
|
||||
events = db.query(LeaveEvent).filter(
|
||||
LeaveEvent.start_date <= end_date,
|
||||
LeaveEvent.end_date >= start_date
|
||||
).all()
|
||||
|
||||
result = []
|
||||
for event in events:
|
||||
result.append(LeaveEventResponse(
|
||||
id=event.id,
|
||||
user_id=event.user_id,
|
||||
user_login=event.user.login if event.user else None,
|
||||
start_date=event.start_date,
|
||||
end_date=event.end_date,
|
||||
leave_type=event.leave_type,
|
||||
created_at=event.created_at.isoformat() if event.created_at else None
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/events", response_model=LeaveEventResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_leave_event(
|
||||
event_data: LeaveEventCreate,
|
||||
admin_user: AppUser = Depends(get_admin_user),
|
||||
db: Session = Depends(get_service_db)
|
||||
):
|
||||
"""
|
||||
Создание нового события отпуска/больничного (только для администраторов)
|
||||
"""
|
||||
if event_data.start_date > event_data.end_date:
|
||||
raise HTTPException(status_code=400, detail="Начальная дата должна быть раньше конечной")
|
||||
|
||||
# Проверка существования пользователя
|
||||
user = db.query(AppUser).filter(AppUser.id == event_data.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Проверка на пересечение с существующими событиями
|
||||
overlapping = db.query(LeaveEvent).filter(
|
||||
LeaveEvent.user_id == event_data.user_id,
|
||||
LeaveEvent.start_date <= event_data.end_date,
|
||||
LeaveEvent.end_date >= event_data.start_date
|
||||
).first()
|
||||
|
||||
if overlapping:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Уже существует событие в этом периоде для данного пользователя"
|
||||
)
|
||||
|
||||
new_event = LeaveEvent(
|
||||
user_id=event_data.user_id,
|
||||
start_date=event_data.start_date,
|
||||
end_date=event_data.end_date,
|
||||
leave_type=event_data.leave_type
|
||||
)
|
||||
|
||||
db.add(new_event)
|
||||
db.commit()
|
||||
db.refresh(new_event)
|
||||
|
||||
return LeaveEventResponse(
|
||||
id=new_event.id,
|
||||
user_id=new_event.user_id,
|
||||
user_login=new_event.user.login if new_event.user else None,
|
||||
start_date=new_event.start_date,
|
||||
end_date=new_event.end_date,
|
||||
leave_type=new_event.leave_type,
|
||||
created_at=new_event.created_at.isoformat() if new_event.created_at else None
|
||||
)
|
||||
|
||||
98
backend/app/api/v1/metrics.py
Executable file
98
backend/app/api/v1/metrics.py
Executable file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Модуль Метрика - числовые показатели эффективности
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from typing import Literal
|
||||
from app.core.database import get_manictime_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.service_db import AppUser
|
||||
from app.schemas.metrics import MetricsAggregateResponse, MetricsRow
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/aggregate", response_model=MetricsAggregateResponse)
|
||||
async def get_metrics_aggregate(
|
||||
period: Literal["week", "month", "quarter", "year"] = Query(..., description="Период агрегации"),
|
||||
year: int = Query(..., description="Год"),
|
||||
current_user: AppUser = Depends(get_current_user),
|
||||
manictime_db: Session = Depends(get_manictime_db)
|
||||
):
|
||||
"""
|
||||
Получение агрегированных метрик за указанный период
|
||||
"""
|
||||
try:
|
||||
if year < 2000 or year > 2100:
|
||||
raise HTTPException(status_code=400, detail="Неверный год")
|
||||
|
||||
# Определение функции агрегации в зависимости от периода
|
||||
date_trunc_func = {
|
||||
"week": "week",
|
||||
"month": "month",
|
||||
"quarter": "quarter",
|
||||
"year": "year"
|
||||
}.get(period, "quarter")
|
||||
|
||||
# SQL запрос для агрегации активного времени
|
||||
query = text(f"""
|
||||
SELECT
|
||||
u."DisplayName" AS user_name,
|
||||
DATE_TRUNC('{date_trunc_func}', a."StartLocalTime") AS period_start,
|
||||
SUM(EXTRACT(EPOCH FROM (a."EndLocalTime" - a."StartLocalTime"))) AS total_seconds
|
||||
FROM "Ar_Activity" a
|
||||
JOIN "Ar_Timeline" t ON a."ReportId" = t."ReportId"
|
||||
JOIN "Ar_User" u ON t."OwnerId" = u."UserId"
|
||||
WHERE
|
||||
t."SchemaName" = 'ManicTime/Computer usage'
|
||||
AND a."Name" = 'Active'
|
||||
AND EXTRACT(YEAR FROM a."StartLocalTime") = :year
|
||||
GROUP BY
|
||||
u."DisplayName",
|
||||
DATE_TRUNC('{date_trunc_func}', a."StartLocalTime")
|
||||
ORDER BY
|
||||
u."DisplayName",
|
||||
period_start
|
||||
""")
|
||||
|
||||
result = manictime_db.execute(query, {"year": year})
|
||||
rows = result.fetchall()
|
||||
|
||||
# Группировка данных по пользователям
|
||||
user_data = {}
|
||||
for row in rows:
|
||||
user_name = row.user_name
|
||||
if user_name not in user_data:
|
||||
user_data[user_name] = {}
|
||||
|
||||
# Формирование ключа периода
|
||||
period_start = row.period_start
|
||||
if period == "week":
|
||||
period_key = f"W{period_start.isocalendar()[1]}"
|
||||
elif period == "month":
|
||||
period_key = f"M{period_start.month}"
|
||||
elif period == "quarter":
|
||||
quarter = (period_start.month - 1) // 3 + 1
|
||||
period_key = f"Q{quarter}"
|
||||
else: # year
|
||||
period_key = "Y1"
|
||||
|
||||
# Конвертация секунд в часы
|
||||
hours = float(row.total_seconds) / 3600
|
||||
user_data[user_name][period_key] = round(hours, 2)
|
||||
|
||||
# Формирование ответа
|
||||
metrics_rows = []
|
||||
for user_name, periods in user_data.items():
|
||||
metrics_rows.append(MetricsRow(user=user_name, data=periods))
|
||||
|
||||
return MetricsAggregateResponse(
|
||||
period_type=period,
|
||||
year=year,
|
||||
data=metrics_rows
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка при получении данных: {str(e)}")
|
||||
|
||||
123
backend/app/api/v1/summary.py
Executable file
123
backend/app/api/v1/summary.py
Executable file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Модуль Сводка - агрегированная гистограмма активности
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from app.core.database import get_manictime_db, get_service_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.service_db import AppUser, AppConfiguration
|
||||
from app.schemas.summary import SummaryHistogramResponse, SummaryDataset
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get("/histogram", response_model=SummaryHistogramResponse)
|
||||
async def get_summary_histogram(
|
||||
start_date: str = Query(..., description="Начальная дата (YYYY-MM-DD)"),
|
||||
end_date: str = Query(..., description="Конечная дата (YYYY-MM-DD)"),
|
||||
current_user: AppUser = Depends(get_current_user),
|
||||
manictime_db: Session = Depends(get_manictime_db)
|
||||
):
|
||||
"""
|
||||
Получение данных для гистограммы активности по дням
|
||||
"""
|
||||
try:
|
||||
# Валидация дат
|
||||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
|
||||
if start_dt > end_dt:
|
||||
raise HTTPException(status_code=400, detail="Начальная дата должна быть раньше конечной")
|
||||
|
||||
# SQL запрос для агрегации данных
|
||||
query = text("""
|
||||
WITH computer_usage AS (
|
||||
SELECT
|
||||
a."StartLocalTime",
|
||||
a."EndLocalTime",
|
||||
a."Name",
|
||||
t."OwnerId",
|
||||
DATE_TRUNC('day', a."StartLocalTime") AS "day"
|
||||
FROM "Ar_Activity" a
|
||||
JOIN "Ar_Timeline" t ON a."ReportId" = t."ReportId"
|
||||
WHERE t."SchemaName" = 'ManicTime/Computer usage'
|
||||
AND a."StartLocalTime" >= :start_date
|
||||
AND a."EndLocalTime" <= :end_date
|
||||
),
|
||||
productive_time AS (
|
||||
SELECT
|
||||
a."StartLocalTime",
|
||||
a."EndLocalTime",
|
||||
t."OwnerId",
|
||||
DATE_TRUNC('day', a."StartLocalTime") AS "day"
|
||||
FROM "Ar_Activity" a
|
||||
JOIN "Ar_Timeline" t ON a."ReportId" = t."ReportId"
|
||||
JOIN "Ar_CommonGroup" cg ON a."CommonGroupId" = cg."CommonId"
|
||||
JOIN "Ar_CategoryGroup" cag ON cg."CommonId" = cag."CommonGroupId"
|
||||
JOIN "Ar_Category" c ON cag."CategoryId" = c."CategoryId"
|
||||
WHERE c."Name" = 'Productive'
|
||||
AND a."StartLocalTime" >= :start_date
|
||||
AND a."EndLocalTime" <= :end_date
|
||||
)
|
||||
SELECT
|
||||
cu."day",
|
||||
SUM(CASE WHEN cu."Name" = 'Active'
|
||||
THEN EXTRACT(EPOCH FROM (cu."EndLocalTime" - cu."StartLocalTime"))
|
||||
ELSE 0 END) AS active_seconds,
|
||||
SUM(CASE WHEN cu."Name" = 'Away'
|
||||
THEN EXTRACT(EPOCH FROM (cu."EndLocalTime" - cu."StartLocalTime"))
|
||||
ELSE 0 END) AS away_seconds,
|
||||
SUM(CASE WHEN cu."Name" IN ('Session Locked', 'Power Off')
|
||||
THEN EXTRACT(EPOCH FROM (cu."EndLocalTime" - cu."StartLocalTime"))
|
||||
ELSE 0 END) AS afk_seconds,
|
||||
COALESCE(SUM(EXTRACT(EPOCH FROM (pt."EndLocalTime" - pt."StartLocalTime"))), 0) AS productive_seconds
|
||||
FROM computer_usage cu
|
||||
LEFT JOIN productive_time pt ON cu."day" = pt."day"
|
||||
AND cu."OwnerId" = pt."OwnerId"
|
||||
AND pt."StartLocalTime" < cu."EndLocalTime"
|
||||
AND pt."EndLocalTime" > cu."StartLocalTime"
|
||||
GROUP BY cu."day"
|
||||
ORDER BY cu."day"
|
||||
""")
|
||||
|
||||
result = manictime_db.execute(
|
||||
query,
|
||||
{"start_date": start_date, "end_date": end_date}
|
||||
)
|
||||
|
||||
rows = result.fetchall()
|
||||
|
||||
# Формирование ответа
|
||||
labels = []
|
||||
active_data = []
|
||||
away_data = []
|
||||
afk_data = []
|
||||
productive_data = []
|
||||
|
||||
for row in rows:
|
||||
day = row.day.strftime("%Y-%m-%d")
|
||||
labels.append(day)
|
||||
active_data.append(float(row.active_seconds or 0))
|
||||
away_data.append(float(row.away_seconds or 0))
|
||||
afk_data.append(float(row.afk_seconds or 0))
|
||||
productive_data.append(float(row.productive_seconds or 0))
|
||||
|
||||
datasets = [
|
||||
SummaryDataset(label="Активный", color="green", data=active_data),
|
||||
SummaryDataset(label="Неактивный", color="red", data=away_data),
|
||||
SummaryDataset(label="Не у ПК", color="yellow", data=afk_data),
|
||||
SummaryDataset(label="Продуктивность", color="orange", data=productive_data),
|
||||
]
|
||||
|
||||
return SummaryHistogramResponse(labels=labels, datasets=datasets)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Неверный формат даты: {str(e)}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка при получении данных: {str(e)}")
|
||||
|
||||
146
backend/app/api/v1/timeline.py
Executable file
146
backend/app/api/v1/timeline.py
Executable file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Модуль Хронология - индивидуальные линейки активности
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from app.core.database import get_manictime_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.service_db import AppUser
|
||||
from app.schemas.timeline import TimelineActivityResponse, UserActivity, ActivitySegment
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/user-activity", response_model=TimelineActivityResponse)
|
||||
async def get_user_activity(
|
||||
date: str = Query(..., description="Дата (YYYY-MM-DD)"),
|
||||
user_ids: List[int] = Query(..., description="Список ID пользователей"),
|
||||
current_user: AppUser = Depends(get_current_user),
|
||||
manictime_db: Session = Depends(get_manictime_db)
|
||||
):
|
||||
"""
|
||||
Получение активности пользователей за указанную дату для построения линеек
|
||||
"""
|
||||
try:
|
||||
# Валидация даты
|
||||
date_dt = datetime.strptime(date, "%Y-%m-%d")
|
||||
date_start = date_dt.strftime("%Y-%m-%d 00:00:00")
|
||||
date_end = date_dt.strftime("%Y-%m-%d 23:59:59")
|
||||
|
||||
if not user_ids:
|
||||
raise HTTPException(status_code=400, detail="Необходимо указать хотя бы одного пользователя")
|
||||
|
||||
# Получение информации о пользователях
|
||||
users_query = text("""
|
||||
SELECT "UserId", "DisplayName"
|
||||
FROM "Ar_User"
|
||||
WHERE "UserId" = ANY(:user_ids)
|
||||
""")
|
||||
|
||||
users_result = manictime_db.execute(users_query, {"user_ids": user_ids})
|
||||
users_map = {row.UserId: row.DisplayName for row in users_result}
|
||||
|
||||
if not users_map:
|
||||
raise HTTPException(status_code=404, detail="Пользователи не найдены")
|
||||
|
||||
# Получение активности с шкалы Computer usage
|
||||
activity_query = text("""
|
||||
SELECT
|
||||
t."OwnerId",
|
||||
a."Name",
|
||||
a."StartLocalTime",
|
||||
a."EndLocalTime"
|
||||
FROM "Ar_Activity" a
|
||||
JOIN "Ar_Timeline" t ON a."ReportId" = t."ReportId"
|
||||
WHERE t."SchemaName" = 'ManicTime/Computer usage'
|
||||
AND t."OwnerId" = ANY(:user_ids)
|
||||
AND a."StartLocalTime" >= :date_start
|
||||
AND a."EndLocalTime" <= :date_end
|
||||
ORDER BY t."OwnerId", a."StartLocalTime"
|
||||
""")
|
||||
|
||||
activity_result = manictime_db.execute(
|
||||
activity_query,
|
||||
{"user_ids": user_ids, "date_start": date_start, "date_end": date_end}
|
||||
)
|
||||
|
||||
# Получение продуктивного времени
|
||||
productive_query = text("""
|
||||
SELECT
|
||||
t."OwnerId",
|
||||
a."StartLocalTime",
|
||||
a."EndLocalTime"
|
||||
FROM "Ar_Activity" a
|
||||
JOIN "Ar_Timeline" t ON a."ReportId" = t."ReportId"
|
||||
JOIN "Ar_CommonGroup" cg ON a."CommonGroupId" = cg."CommonId"
|
||||
JOIN "Ar_CategoryGroup" cag ON cg."CommonId" = cag."CommonGroupId"
|
||||
JOIN "Ar_Category" c ON cag."CategoryId" = c."CategoryId"
|
||||
WHERE c."Name" = 'Productive'
|
||||
AND t."OwnerId" = ANY(:user_ids)
|
||||
AND a."StartLocalTime" >= :date_start
|
||||
AND a."EndLocalTime" <= :date_end
|
||||
ORDER BY t."OwnerId", a."StartLocalTime"
|
||||
""")
|
||||
|
||||
productive_result = manictime_db.execute(
|
||||
productive_query,
|
||||
{"user_ids": user_ids, "date_start": date_start, "date_end": date_end}
|
||||
)
|
||||
|
||||
# Группировка по пользователям
|
||||
user_activities = {}
|
||||
|
||||
for row in activity_result:
|
||||
user_id = row.OwnerId
|
||||
if user_id not in user_activities:
|
||||
user_activities[user_id] = {
|
||||
"user_id": user_id,
|
||||
"display_name": users_map.get(user_id, f"User {user_id}"),
|
||||
"segments": []
|
||||
}
|
||||
|
||||
# Определение типа сегмента
|
||||
segment_type = "Active"
|
||||
if row.Name == "Away":
|
||||
segment_type = "Away"
|
||||
elif row.Name == "Session Locked":
|
||||
segment_type = "Session Locked"
|
||||
elif row.Name == "Power Off":
|
||||
segment_type = "Power Off"
|
||||
|
||||
user_activities[user_id]["segments"].append(
|
||||
{
|
||||
"type": segment_type,
|
||||
"start": row.StartLocalTime.isoformat(),
|
||||
"end": row.EndLocalTime.isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
# Добавление продуктивного времени
|
||||
for row in productive_result:
|
||||
user_id = row.OwnerId
|
||||
if user_id in user_activities:
|
||||
user_activities[user_id]["segments"].append(
|
||||
{
|
||||
"type": "Productive",
|
||||
"start": row.StartLocalTime.isoformat(),
|
||||
"end": row.EndLocalTime.isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
# Формирование ответа
|
||||
activities = []
|
||||
for user_id in user_ids:
|
||||
if user_id in user_activities:
|
||||
activities.append(UserActivity(**user_activities[user_id]))
|
||||
|
||||
return TimelineActivityResponse(date=date, activities=activities)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Неверный формат даты: {str(e)}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка при получении данных: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user