Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/profile_dashboard_widgets_service.py

364 lines
11 KiB
Python
Executable file

from __future__ import annotations
from typing import Any, Dict, List, Callable
from datetime import datetime
from sqlalchemy.orm import Session
from adapters.db.models.profile_dashboard_layout import ProfileUserDashboardLayout
from app.services.business_service import get_user_businesses
from app.services.announcement_service import user_list as list_announcements
from adapters.db.repositories.support.ticket_repository import TicketRepository
from adapters.api.v1.schemas import QueryInfo
from app.services.system_settings_service import is_support_tickets_enabled_for_users
# ----------------------------
# Responsive columns per breakpoint
# ----------------------------
COLUMNS_BY_BREAKPOINT: Dict[str, int] = {
"xs": 1,
"sm": 4,
"md": 8,
"lg": 12,
"xl": 12,
}
_VALID_BREAKPOINTS = frozenset(COLUMNS_BY_BREAKPOINT.keys())
def _normalize_layout_breakpoint(breakpoint: str) -> str:
bp = (breakpoint or "md").lower()
if bp not in _VALID_BREAKPOINTS:
return "md"
return bp
# ----------------------------
# Profile Widget Definitions
# ----------------------------
PROFILE_WIDGET_DEFINITIONS: List[Dict[str, Any]] = [
{
"key": "profile_recent_businesses",
"title": "کسب‌وکارهای شما",
"icon": "business",
"version": 1,
"permissions_required": [],
"defaults": {
"xs": {"colSpan": 1, "rowSpan": 2},
"sm": {"colSpan": 4, "rowSpan": 2},
"md": {"colSpan": 8, "rowSpan": 2},
"lg": {"colSpan": 4, "rowSpan": 2},
"xl": {"colSpan": 4, "rowSpan": 2},
},
"cache_ttl": 60,
},
{
"key": "profile_announcements",
"title": "اعلان‌ها",
"icon": "notifications",
"version": 1,
"permissions_required": [],
"defaults": {
"xs": {"colSpan": 1, "rowSpan": 2},
"sm": {"colSpan": 4, "rowSpan": 2},
"md": {"colSpan": 8, "rowSpan": 2},
"lg": {"colSpan": 4, "rowSpan": 2},
"xl": {"colSpan": 4, "rowSpan": 2},
},
"cache_ttl": 30,
},
{
"key": "profile_support_tickets",
"title": "تیکت‌های پشتیبانی",
"icon": "support_agent",
"version": 1,
"permissions_required": [],
"defaults": {
"xs": {"colSpan": 1, "rowSpan": 2},
"sm": {"colSpan": 4, "rowSpan": 2},
"md": {"colSpan": 8, "rowSpan": 2},
"lg": {"colSpan": 4, "rowSpan": 2},
"xl": {"colSpan": 4, "rowSpan": 2},
},
"cache_ttl": 30,
},
]
def get_profile_widget_definitions(db: Session, user_id: int) -> Dict[str, Any]:
"""
Returns available widgets for profile dashboard along with responsive columns map.
"""
return {
"columns": COLUMNS_BY_BREAKPOINT,
"items": PROFILE_WIDGET_DEFINITIONS,
}
def _default_layout_items(bp: str) -> List[Dict[str, Any]]:
columns = COLUMNS_BY_BREAKPOINT[bp]
items: List[Dict[str, Any]] = []
order = 1
for d in PROFILE_WIDGET_DEFINITIONS:
defaults = (d.get("defaults") or {}).get(bp) or {}
items.append({
"key": d["key"],
"order": order,
"colSpan": int(defaults.get("colSpan", max(1, columns // 2))),
"rowSpan": int(defaults.get("rowSpan", 2)),
"hidden": False,
})
order += 1
return items
def _layout_profile_dict(
bp: str,
items: List[Dict[str, Any]],
updated_at: datetime,
) -> Dict[str, Any]:
columns = COLUMNS_BY_BREAKPOINT[bp]
return {
"breakpoint": bp,
"columns": columns,
"items": items,
"version": 2,
"updated_at": updated_at.isoformat() + "Z",
}
def get_profile_dashboard_layout_profile(
db: Session,
user_id: int,
breakpoint: str,
) -> Dict[str, Any]:
"""
Returns a profile for the requested breakpoint:
{ breakpoint, columns, items: [{ key, order, colSpan, rowSpan, hidden }] }
"""
bp = _normalize_layout_breakpoint(breakpoint)
row = (
db.query(ProfileUserDashboardLayout)
.filter(
ProfileUserDashboardLayout.user_id == user_id,
ProfileUserDashboardLayout.breakpoint == bp,
)
.first()
)
if row is not None and row.items is not None:
stored = list(row.items) # type: ignore[arg-type]
return _layout_profile_dict(
bp,
sorted(stored, key=lambda x: int(x.get("order", 1))),
row.updated_at,
)
now = datetime.utcnow()
return _layout_profile_dict(bp, _default_layout_items(bp), now)
def save_profile_dashboard_layout_profile(
db: Session,
user_id: int,
breakpoint: str,
items: List[Dict[str, Any]],
) -> Dict[str, Any]:
bp = _normalize_layout_breakpoint(breakpoint)
columns = COLUMNS_BY_BREAKPOINT[bp]
sanitized: List[Dict[str, Any]] = []
for it in (items or []):
try:
key = str(it.get("key"))
order = int(it.get("order", 1))
col_span = max(1, min(columns, int(it.get("colSpan", 1))))
row_span = int(it.get("rowSpan", 1))
hidden = bool(it.get("hidden", False))
sanitized.append({
"key": key,
"order": order,
"colSpan": col_span,
"rowSpan": row_span,
"hidden": hidden,
})
except Exception:
continue
sanitized = sorted(sanitized, key=lambda x: x.get("order", 1))
now = datetime.utcnow()
row = (
db.query(ProfileUserDashboardLayout)
.filter(
ProfileUserDashboardLayout.user_id == user_id,
ProfileUserDashboardLayout.breakpoint == bp,
)
.first()
)
if row is None:
row = ProfileUserDashboardLayout(
user_id=user_id,
breakpoint=bp,
items=sanitized,
)
db.add(row)
else:
row.items = sanitized
row.updated_at = now
db.flush()
return _layout_profile_dict(bp, sanitized, row.updated_at)
# ----------------------------
# Data resolvers (Batch)
# ----------------------------
WidgetResolver = Callable[[Session, int, Dict[str, Any]], Any]
def _resolve_profile_recent_businesses(
db: Session, user_id: int, filters: Dict[str, Any]
) -> Dict[str, Any]:
"""
Returns user's businesses (owner + member).
"""
limit_raw = filters.get("limit", 10)
try:
limit = max(1, min(50, int(limit_raw)))
except Exception:
limit = 10
query_dict = {
"take": limit,
"skip": 0,
"sort_by": "created_at",
"sort_desc": True,
"search": None,
}
# برای داشبورد پروفایل، کسب‌وکارهای حذف‌شده را نشان نمی‌دهیم
result = get_user_businesses(db, user_id, query_dict, include_deleted_for_owner=False)
items = result.get("items", [])
# فیلتر کردن کسب و کارهای حذف شده (در حال حذف یا حذف شده)
filtered_items = []
for item in items:
deleted_at = item.get("deleted_at")
is_deleted = item.get("is_deleted", False)
is_deletion_pending = item.get("is_deletion_pending", False)
has_deleted_at = deleted_at is not None and deleted_at != ""
if not has_deleted_at and not is_deleted and not is_deletion_pending:
filtered_items.append(item)
items = filtered_items
formatted_items = []
for item in items:
formatted_items.append({
"id": item.get("id"),
"name": item.get("name"),
"role": item.get("role", "عضو"),
"is_owner": item.get("is_owner", False),
})
return {"items": formatted_items}
def _resolve_profile_announcements(
db: Session, user_id: int, filters: Dict[str, Any]
) -> Dict[str, Any]:
"""
Returns user's announcements.
"""
limit_raw = filters.get("limit", 5)
try:
limit = max(1, min(50, int(limit_raw)))
except Exception:
limit = 5
only_unread = filters.get("only_unread", False)
try:
result = list_announcements(
db=db,
user_id=user_id,
page=1,
limit=limit,
only_unread=only_unread,
level=None,
locale=None,
)
items = result.get("items", [])
return {"items": items}
except Exception:
return {"items": []}
def _resolve_profile_support_tickets(
db: Session, user_id: int, filters: Dict[str, Any]
) -> Dict[str, Any]:
"""
Returns user's support tickets.
"""
if not is_support_tickets_enabled_for_users(db):
return {"items": []}
limit_raw = filters.get("limit", 5)
try:
limit = max(1, min(50, int(limit_raw)))
except Exception:
limit = 5
try:
ticket_repo = TicketRepository(db)
query_info = QueryInfo(
take=limit,
skip=0,
sort_by="last_message_at",
sort_desc=True,
search=None,
search_fields=None,
)
tickets, total = ticket_repo.get_user_tickets(user_id, query_info)
formatted_items = []
for ticket in tickets:
from app.services.support.ticket_engagement_service import ticket_last_activity_at
activity_at = ticket_last_activity_at(ticket)
formatted_items.append({
"id": ticket.id,
"subject": ticket.title,
"status": ticket.status.name if ticket.status else "",
"updated_at": activity_at.isoformat() if activity_at else "",
"last_message_at": ticket.last_message_at.isoformat() if ticket.last_message_at else "",
})
return {"items": formatted_items}
except Exception:
return {"items": []}
PROFILE_WIDGET_RESOLVERS: Dict[str, WidgetResolver] = {
"profile_recent_businesses": _resolve_profile_recent_businesses,
"profile_announcements": _resolve_profile_announcements,
"profile_support_tickets": _resolve_profile_support_tickets,
}
def get_profile_widgets_batch_data(
db: Session,
user_id: int,
widget_keys: List[str],
filters: Dict[str, Any],
) -> Dict[str, Any]:
"""
Returns a map: { widget_key: data or error } for requested widget_keys.
"""
result: Dict[str, Any] = {}
for key in widget_keys:
resolver = PROFILE_WIDGET_RESOLVERS.get(key)
if not resolver:
result[key] = {"error": "UNKNOWN_WIDGET"}
continue
try:
result[key] = resolver(db, user_id, filters or {})
except Exception as ex:
result[key] = {"error": str(ex)}
return result