175 lines
4.9 KiB
Python
175 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Iterable
|
|
|
|
from sqlalchemy import Select, func, select, update
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.modules.alerts.models import UserAlert
|
|
from app.modules.core.iam.models import User
|
|
from app.modules.email_integration.event_service import send_alert_created_email
|
|
|
|
ALERT_PRIORITIES = ("low", "normal", "high", "critical")
|
|
ALERT_TYPES = (
|
|
"general",
|
|
"task_assigned",
|
|
"task_due",
|
|
"task_overdue",
|
|
"task_review",
|
|
"document_uploaded",
|
|
"clarification",
|
|
"attendance",
|
|
"leave",
|
|
"payroll",
|
|
"consultant",
|
|
"client",
|
|
)
|
|
|
|
|
|
def normalize_priority(priority: str | None) -> str:
|
|
value = (priority or "normal").strip().lower()
|
|
return value if value in ALERT_PRIORITIES else "normal"
|
|
|
|
|
|
def normalize_alert_type(alert_type: str | None) -> str:
|
|
value = (alert_type or "general").strip().lower()
|
|
return value or "general"
|
|
|
|
|
|
def create_alert(
|
|
db: Session,
|
|
*,
|
|
user_id: int,
|
|
title: str,
|
|
message: str | None = None,
|
|
tenant_id: int | None = None,
|
|
branch_id: int | None = None,
|
|
role_context: str | None = None,
|
|
alert_type: str = "general",
|
|
priority: str = "normal",
|
|
target_url: str | None = None,
|
|
created_by_user_id: int | None = None,
|
|
commit: bool = True,
|
|
) -> UserAlert:
|
|
alert = UserAlert(
|
|
tenant_id=tenant_id,
|
|
branch_id=branch_id,
|
|
user_id=user_id,
|
|
role_context=(role_context or None),
|
|
alert_type=normalize_alert_type(alert_type),
|
|
priority=normalize_priority(priority),
|
|
title=(title or "Alert").strip()[:255],
|
|
message=(message or None),
|
|
target_url=(target_url or None),
|
|
created_by_user_id=created_by_user_id,
|
|
)
|
|
db.add(alert)
|
|
db.flush()
|
|
try:
|
|
send_alert_created_email(db, alert)
|
|
except Exception:
|
|
# Email notification must never block in-app alert creation.
|
|
pass
|
|
if commit:
|
|
db.commit()
|
|
db.refresh(alert)
|
|
return alert
|
|
|
|
|
|
def create_bulk_alerts(
|
|
db: Session,
|
|
*,
|
|
user_ids: Iterable[int],
|
|
title: str,
|
|
message: str | None = None,
|
|
tenant_id: int | None = None,
|
|
branch_id: int | None = None,
|
|
role_context: str | None = None,
|
|
alert_type: str = "general",
|
|
priority: str = "normal",
|
|
target_url: str | None = None,
|
|
created_by_user_id: int | None = None,
|
|
) -> list[UserAlert]:
|
|
rows: list[UserAlert] = []
|
|
for user_id in sorted({int(uid) for uid in user_ids if uid}):
|
|
rows.append(
|
|
create_alert(
|
|
db,
|
|
user_id=user_id,
|
|
title=title,
|
|
message=message,
|
|
tenant_id=tenant_id,
|
|
branch_id=branch_id,
|
|
role_context=role_context,
|
|
alert_type=alert_type,
|
|
priority=priority,
|
|
target_url=target_url,
|
|
created_by_user_id=created_by_user_id,
|
|
commit=False,
|
|
)
|
|
)
|
|
db.commit()
|
|
for row in rows:
|
|
db.refresh(row)
|
|
return rows
|
|
|
|
|
|
def _user_alert_query(current_user: User) -> Select:
|
|
return select(UserAlert).where(UserAlert.user_id == current_user.id)
|
|
|
|
|
|
def list_my_alerts(
|
|
db: Session,
|
|
current_user: User,
|
|
*,
|
|
status: str = "all",
|
|
priority: str = "all",
|
|
limit: int = 100,
|
|
) -> list[UserAlert]:
|
|
q = _user_alert_query(current_user)
|
|
if status == "unread":
|
|
q = q.where(UserAlert.is_read.is_(False))
|
|
elif status == "read":
|
|
q = q.where(UserAlert.is_read.is_(True))
|
|
if priority in ALERT_PRIORITIES:
|
|
q = q.where(UserAlert.priority == priority)
|
|
q = q.order_by(UserAlert.is_read.asc(), UserAlert.created_at_utc.desc()).limit(max(1, min(limit, 500)))
|
|
return list(db.execute(q).scalars().all())
|
|
|
|
|
|
def count_unread_alerts(db: Session, current_user: User | None) -> int:
|
|
if not current_user:
|
|
return 0
|
|
value = db.execute(
|
|
select(func.count(UserAlert.id)).where(UserAlert.user_id == current_user.id, UserAlert.is_read.is_(False))
|
|
).scalar_one()
|
|
return int(value or 0)
|
|
|
|
|
|
def get_my_alert_or_404(db: Session, current_user: User, alert_id: int) -> UserAlert | None:
|
|
return db.execute(
|
|
select(UserAlert).where(UserAlert.id == alert_id, UserAlert.user_id == current_user.id)
|
|
).scalar_one_or_none()
|
|
|
|
|
|
def mark_alert_read(db: Session, current_user: User, alert_id: int) -> bool:
|
|
alert = get_my_alert_or_404(db, current_user, alert_id)
|
|
if not alert:
|
|
return False
|
|
if not alert.is_read:
|
|
alert.is_read = True
|
|
alert.read_at_utc = datetime.now(timezone.utc)
|
|
db.commit()
|
|
return True
|
|
|
|
|
|
def mark_all_alerts_read(db: Session, current_user: User) -> int:
|
|
result = db.execute(
|
|
update(UserAlert)
|
|
.where(UserAlert.user_id == current_user.id, UserAlert.is_read.is_(False))
|
|
.values(is_read=True, read_at_utc=datetime.now(timezone.utc))
|
|
)
|
|
db.commit()
|
|
return int(result.rowcount or 0)
|