forked from hesabix/arc
364 lines
11 KiB
Python
Executable file
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
|