Spaces:
Sleeping
Sleeping
feat: user preferences support
Browse files- src/app/api/v1/auth.py +160 -0
- src/app/models/user_preference.py +54 -0
- src/app/schemas/user_preferences.py +207 -0
src/app/api/v1/auth.py
CHANGED
|
@@ -13,6 +13,11 @@ from app.schemas.user import (
|
|
| 13 |
UserCreate, UserResponse, UserUpdate, UserProfile,
|
| 14 |
AdminOTPRequest, AdminRegistrationRequest
|
| 15 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
from app.models.user import User
|
| 17 |
from app.core.supabase_auth import supabase_auth
|
| 18 |
from app.services.audit_service import AuditService
|
|
@@ -794,3 +799,158 @@ async def logout(
|
|
| 794 |
logger.info(f"User logged out: {current_user.email}")
|
| 795 |
|
| 796 |
return MessageResponse(message="Logged out successfully")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
UserCreate, UserResponse, UserUpdate, UserProfile,
|
| 14 |
AdminOTPRequest, AdminRegistrationRequest
|
| 15 |
)
|
| 16 |
+
from app.schemas.user_preferences import (
|
| 17 |
+
UserPreferencesUpdate, UserPreferencesResponse,
|
| 18 |
+
DEFAULT_FAVORITE_APPS, AVAILABLE_APPS, DEFAULT_DASHBOARD_WIDGETS
|
| 19 |
+
)
|
| 20 |
+
from app.models.user_preference import UserPreference
|
| 21 |
from app.models.user import User
|
| 22 |
from app.core.supabase_auth import supabase_auth
|
| 23 |
from app.services.audit_service import AuditService
|
|
|
|
| 799 |
logger.info(f"User logged out: {current_user.email}")
|
| 800 |
|
| 801 |
return MessageResponse(message="Logged out successfully")
|
| 802 |
+
|
| 803 |
+
|
| 804 |
+
# ============================================
|
| 805 |
+
# USER PREFERENCES ENDPOINTS
|
| 806 |
+
# ============================================
|
| 807 |
+
|
| 808 |
+
@router.get("/me/preferences", response_model=UserPreferencesResponse)
|
| 809 |
+
async def get_my_preferences(
|
| 810 |
+
current_user: User = Depends(get_current_active_user),
|
| 811 |
+
db: Session = Depends(get_db)
|
| 812 |
+
):
|
| 813 |
+
"""
|
| 814 |
+
Get current user's preferences from user_preferences table
|
| 815 |
+
|
| 816 |
+
Returns user preferences with role-based defaults if preferences don't exist yet.
|
| 817 |
+
Automatically creates preferences record if it doesn't exist (via database trigger).
|
| 818 |
+
"""
|
| 819 |
+
# Get or create preferences
|
| 820 |
+
preferences = db.query(UserPreference).filter(
|
| 821 |
+
UserPreference.user_id == current_user.id,
|
| 822 |
+
UserPreference.deleted_at == None
|
| 823 |
+
).first()
|
| 824 |
+
|
| 825 |
+
# If no preferences exist, create with role-based defaults
|
| 826 |
+
if not preferences:
|
| 827 |
+
preferences = UserPreference(
|
| 828 |
+
user_id=current_user.id,
|
| 829 |
+
favorite_apps=DEFAULT_FAVORITE_APPS.get(current_user.role, ['dashboard', 'tickets', 'projects', 'maps']),
|
| 830 |
+
dashboard_widgets=DEFAULT_DASHBOARD_WIDGETS.get(current_user.role, ['recent_tickets', 'team_performance', 'sla_metrics']),
|
| 831 |
+
theme='light',
|
| 832 |
+
language='en'
|
| 833 |
+
)
|
| 834 |
+
db.add(preferences)
|
| 835 |
+
db.commit()
|
| 836 |
+
db.refresh(preferences)
|
| 837 |
+
logger.info(f"Created default preferences for user: {current_user.email}")
|
| 838 |
+
|
| 839 |
+
return UserPreferencesResponse.from_orm(preferences)
|
| 840 |
+
|
| 841 |
+
|
| 842 |
+
@router.put("/me/preferences", response_model=UserPreferencesResponse)
|
| 843 |
+
async def update_my_preferences(
|
| 844 |
+
preferences_data: UserPreferencesUpdate,
|
| 845 |
+
request: Request,
|
| 846 |
+
current_user: User = Depends(get_current_active_user),
|
| 847 |
+
db: Session = Depends(get_db)
|
| 848 |
+
):
|
| 849 |
+
"""
|
| 850 |
+
Update current user's preferences in user_preferences table
|
| 851 |
+
|
| 852 |
+
Updates user preferences including favorite apps, theme, language, and notification settings.
|
| 853 |
+
Favorite apps are validated against role-specific available apps (max 6).
|
| 854 |
+
"""
|
| 855 |
+
# Get or create preferences
|
| 856 |
+
preferences = db.query(UserPreference).filter(
|
| 857 |
+
UserPreference.user_id == current_user.id,
|
| 858 |
+
UserPreference.deleted_at == None
|
| 859 |
+
).first()
|
| 860 |
+
|
| 861 |
+
if not preferences:
|
| 862 |
+
# Create new preferences record
|
| 863 |
+
preferences = UserPreference(
|
| 864 |
+
user_id=current_user.id,
|
| 865 |
+
favorite_apps=DEFAULT_FAVORITE_APPS.get(current_user.role, []),
|
| 866 |
+
dashboard_widgets=DEFAULT_DASHBOARD_WIDGETS.get(current_user.role, [])
|
| 867 |
+
)
|
| 868 |
+
db.add(preferences)
|
| 869 |
+
db.flush()
|
| 870 |
+
|
| 871 |
+
# Track changes for audit
|
| 872 |
+
changes = {'old': {}, 'new': {}}
|
| 873 |
+
|
| 874 |
+
# Update only provided fields
|
| 875 |
+
update_data = preferences_data.dict(exclude_unset=True)
|
| 876 |
+
|
| 877 |
+
# Validate favorite apps if provided
|
| 878 |
+
if 'favorite_apps' in update_data and update_data['favorite_apps'] is not None:
|
| 879 |
+
# Check max limit (6 apps)
|
| 880 |
+
if len(update_data['favorite_apps']) > 6:
|
| 881 |
+
raise HTTPException(
|
| 882 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 883 |
+
detail="Maximum 6 favorite apps allowed"
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
# Validate against role-specific available apps
|
| 887 |
+
available_apps = AVAILABLE_APPS.get(current_user.role, [])
|
| 888 |
+
invalid_apps = [app for app in update_data['favorite_apps'] if app not in available_apps]
|
| 889 |
+
|
| 890 |
+
if invalid_apps:
|
| 891 |
+
raise HTTPException(
|
| 892 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 893 |
+
detail=f"Invalid apps for {current_user.role}: {', '.join(invalid_apps)}. "
|
| 894 |
+
f"Available apps: {', '.join(available_apps)}"
|
| 895 |
+
)
|
| 896 |
+
|
| 897 |
+
changes['old']['favorite_apps'] = preferences.favorite_apps
|
| 898 |
+
changes['new']['favorite_apps'] = update_data['favorite_apps']
|
| 899 |
+
preferences.favorite_apps = update_data['favorite_apps']
|
| 900 |
+
|
| 901 |
+
# Update other fields
|
| 902 |
+
for field, value in update_data.items():
|
| 903 |
+
if field != 'favorite_apps' and value is not None:
|
| 904 |
+
old_value = getattr(preferences, field, None)
|
| 905 |
+
if old_value != value:
|
| 906 |
+
changes['old'][field] = old_value
|
| 907 |
+
changes['new'][field] = value
|
| 908 |
+
setattr(preferences, field, value)
|
| 909 |
+
|
| 910 |
+
db.commit()
|
| 911 |
+
db.refresh(preferences)
|
| 912 |
+
|
| 913 |
+
# Audit log
|
| 914 |
+
if changes['old']:
|
| 915 |
+
AuditService.log_action(
|
| 916 |
+
db=db,
|
| 917 |
+
action='update',
|
| 918 |
+
entity_type='user_preferences',
|
| 919 |
+
entity_id=str(preferences.id),
|
| 920 |
+
description=f"User updated preferences: {current_user.email}",
|
| 921 |
+
user=current_user,
|
| 922 |
+
request=request,
|
| 923 |
+
changes=changes
|
| 924 |
+
)
|
| 925 |
+
|
| 926 |
+
logger.info(f"Preferences updated for user: {current_user.email}")
|
| 927 |
+
|
| 928 |
+
return UserPreferencesResponse.from_orm(preferences)
|
| 929 |
+
|
| 930 |
+
|
| 931 |
+
@router.get("/me/preferences/available-apps", response_model=dict)
|
| 932 |
+
async def get_available_apps(
|
| 933 |
+
current_user: User = Depends(get_current_active_user),
|
| 934 |
+
db: Session = Depends(get_db)
|
| 935 |
+
):
|
| 936 |
+
"""
|
| 937 |
+
Get list of apps available for user to favorite based on their role
|
| 938 |
+
|
| 939 |
+
Returns currently favorited apps, all available apps for the role,
|
| 940 |
+
and role-based default favorites.
|
| 941 |
+
"""
|
| 942 |
+
# Get current preferences
|
| 943 |
+
preferences = db.query(UserPreference).filter(
|
| 944 |
+
UserPreference.user_id == current_user.id,
|
| 945 |
+
UserPreference.deleted_at == None
|
| 946 |
+
).first()
|
| 947 |
+
|
| 948 |
+
current_favorites = preferences.favorite_apps if preferences else DEFAULT_FAVORITE_APPS.get(current_user.role, [])
|
| 949 |
+
|
| 950 |
+
return {
|
| 951 |
+
"role": current_user.role,
|
| 952 |
+
"current_favorites": current_favorites,
|
| 953 |
+
"available_apps": AVAILABLE_APPS.get(current_user.role, []),
|
| 954 |
+
"default_favorites": DEFAULT_FAVORITE_APPS.get(current_user.role, []),
|
| 955 |
+
"max_favorites": 6
|
| 956 |
+
}
|
src/app/models/user_preference.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
User Preferences Model
|
| 3 |
+
Stores user-specific UI preferences, favorites, theme, and settings
|
| 4 |
+
"""
|
| 5 |
+
from sqlalchemy import Column, String, Boolean, Integer, Float, ForeignKey, ARRAY, Text
|
| 6 |
+
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
| 7 |
+
from app.models.base import BaseModel
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class UserPreference(BaseModel):
|
| 11 |
+
"""
|
| 12 |
+
User Preferences model - One row per user
|
| 13 |
+
Stores UI preferences, favorites, theme, and settings
|
| 14 |
+
"""
|
| 15 |
+
__tablename__ = "user_preferences"
|
| 16 |
+
|
| 17 |
+
# Link to user
|
| 18 |
+
user_id = Column(UUID(as_uuid=True), ForeignKey('users.id', ondelete='CASCADE'), nullable=False, unique=True)
|
| 19 |
+
|
| 20 |
+
# Favorite Apps (for 9-dot app launcher)
|
| 21 |
+
# Array of app codes that appear in the top navigation bar
|
| 22 |
+
# Max: 6 favorites (enforced in application logic)
|
| 23 |
+
favorite_apps = Column(ARRAY(Text), default=['dashboard', 'tickets', 'projects', 'maps'])
|
| 24 |
+
|
| 25 |
+
# UI Preferences
|
| 26 |
+
theme = Column(String(50), default='light') # 'light', 'dark', 'auto'
|
| 27 |
+
language = Column(String(10), default='en') # 'en', 'sw' (Swahili), 'fr', etc.
|
| 28 |
+
|
| 29 |
+
# Notification Preferences
|
| 30 |
+
email_notifications = Column(Boolean, default=True)
|
| 31 |
+
push_notifications = Column(Boolean, default=True)
|
| 32 |
+
sms_notifications = Column(Boolean, default=False)
|
| 33 |
+
|
| 34 |
+
# Dashboard Layout (which widgets to show)
|
| 35 |
+
# Array of widget codes: ['recent_tickets', 'team_performance', 'sla_metrics', 'map_view']
|
| 36 |
+
dashboard_widgets = Column(ARRAY(Text), default=['recent_tickets', 'team_performance', 'sla_metrics'])
|
| 37 |
+
|
| 38 |
+
# Table/List Preferences
|
| 39 |
+
default_tickets_view = Column(String(50), default='list') # 'list', 'kanban', 'calendar'
|
| 40 |
+
tickets_per_page = Column(Integer, default=25) # Pagination size
|
| 41 |
+
default_sort_field = Column(String(100), default='created_at') # Default sort field
|
| 42 |
+
default_sort_order = Column(String(10), default='desc') # 'asc', 'desc'
|
| 43 |
+
|
| 44 |
+
# Map Preferences (for field agents and dispatchers)
|
| 45 |
+
default_map_zoom = Column(Integer, default=12)
|
| 46 |
+
default_map_center_lat = Column(Float, nullable=True) # User's preferred map center
|
| 47 |
+
default_map_center_lng = Column(Float, nullable=True)
|
| 48 |
+
|
| 49 |
+
# Additional settings (flexible JSONB for future expansion)
|
| 50 |
+
# Can store: sidebar_collapsed, compact_mode, color_blind_mode, etc.
|
| 51 |
+
additional_settings = Column(JSONB, default={})
|
| 52 |
+
|
| 53 |
+
def __repr__(self):
|
| 54 |
+
return f"<UserPreference(user_id='{self.user_id}', theme='{self.theme}')>"
|
src/app/schemas/user_preferences.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
User Preferences Schemas
|
| 3 |
+
Handles user-specific settings like favorite apps, theme, notifications, etc.
|
| 4 |
+
Maps to user_preferences table in database
|
| 5 |
+
"""
|
| 6 |
+
from pydantic import BaseModel, Field, validator
|
| 7 |
+
from typing import Optional, List, Dict, Any
|
| 8 |
+
from uuid import UUID
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class UserPreferencesUpdate(BaseModel):
|
| 12 |
+
"""Update user preferences"""
|
| 13 |
+
favorite_apps: Optional[List[str]] = Field(
|
| 14 |
+
None,
|
| 15 |
+
max_items=6,
|
| 16 |
+
description="List of favorite app codes (max 6) e.g., ['dashboard', 'tickets', 'projects']"
|
| 17 |
+
)
|
| 18 |
+
theme: Optional[str] = Field(
|
| 19 |
+
None,
|
| 20 |
+
description="UI theme: 'light', 'dark', 'auto'"
|
| 21 |
+
)
|
| 22 |
+
language: Optional[str] = Field(
|
| 23 |
+
None,
|
| 24 |
+
description="Preferred language code (e.g., 'en', 'sw')"
|
| 25 |
+
)
|
| 26 |
+
email_notifications: Optional[bool] = Field(
|
| 27 |
+
None,
|
| 28 |
+
description="Enable/disable email notifications"
|
| 29 |
+
)
|
| 30 |
+
push_notifications: Optional[bool] = Field(
|
| 31 |
+
None,
|
| 32 |
+
description="Enable/disable push notifications"
|
| 33 |
+
)
|
| 34 |
+
sms_notifications: Optional[bool] = Field(
|
| 35 |
+
None,
|
| 36 |
+
description="Enable/disable SMS notifications"
|
| 37 |
+
)
|
| 38 |
+
dashboard_widgets: Optional[List[str]] = Field(
|
| 39 |
+
None,
|
| 40 |
+
description="Array of widget codes to display on dashboard"
|
| 41 |
+
)
|
| 42 |
+
default_tickets_view: Optional[str] = Field(
|
| 43 |
+
None,
|
| 44 |
+
description="Default view for tickets: 'list', 'kanban', 'calendar'"
|
| 45 |
+
)
|
| 46 |
+
tickets_per_page: Optional[int] = Field(
|
| 47 |
+
None,
|
| 48 |
+
ge=10,
|
| 49 |
+
le=100,
|
| 50 |
+
description="Pagination size (10-100)"
|
| 51 |
+
)
|
| 52 |
+
default_sort_field: Optional[str] = Field(
|
| 53 |
+
None,
|
| 54 |
+
description="Default sort field"
|
| 55 |
+
)
|
| 56 |
+
default_sort_order: Optional[str] = Field(
|
| 57 |
+
None,
|
| 58 |
+
description="Default sort order: 'asc', 'desc'"
|
| 59 |
+
)
|
| 60 |
+
default_map_zoom: Optional[int] = Field(
|
| 61 |
+
None,
|
| 62 |
+
description="Default map zoom level"
|
| 63 |
+
)
|
| 64 |
+
default_map_center_lat: Optional[float] = Field(
|
| 65 |
+
None,
|
| 66 |
+
description="Default map center latitude"
|
| 67 |
+
)
|
| 68 |
+
default_map_center_lng: Optional[float] = Field(
|
| 69 |
+
None,
|
| 70 |
+
description="Default map center longitude"
|
| 71 |
+
)
|
| 72 |
+
additional_settings: Optional[Dict[str, Any]] = Field(
|
| 73 |
+
None,
|
| 74 |
+
description="Additional settings (sidebar_collapsed, compact_mode, etc.)"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
@validator('theme')
|
| 78 |
+
def validate_theme(cls, v):
|
| 79 |
+
if v and v not in ['light', 'dark', 'auto']:
|
| 80 |
+
raise ValueError('Theme must be one of: light, dark, auto')
|
| 81 |
+
return v
|
| 82 |
+
|
| 83 |
+
@validator('default_tickets_view')
|
| 84 |
+
def validate_tickets_view(cls, v):
|
| 85 |
+
if v and v not in ['list', 'kanban', 'calendar']:
|
| 86 |
+
raise ValueError('Tickets view must be one of: list, kanban, calendar')
|
| 87 |
+
return v
|
| 88 |
+
|
| 89 |
+
@validator('default_sort_order')
|
| 90 |
+
def validate_sort_order(cls, v):
|
| 91 |
+
if v and v not in ['asc', 'desc']:
|
| 92 |
+
raise ValueError('Sort order must be: asc or desc')
|
| 93 |
+
return v
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class UserPreferencesResponse(BaseModel):
|
| 97 |
+
"""User preferences response"""
|
| 98 |
+
id: UUID
|
| 99 |
+
user_id: UUID
|
| 100 |
+
favorite_apps: List[str] = Field(
|
| 101 |
+
default_factory=lambda: ['dashboard', 'tickets', 'projects', 'maps'],
|
| 102 |
+
description="List of favorite app codes"
|
| 103 |
+
)
|
| 104 |
+
theme: str = Field(default="light", description="UI theme")
|
| 105 |
+
language: str = Field(default="en", description="Preferred language")
|
| 106 |
+
email_notifications: bool = Field(default=True, description="Email notifications enabled")
|
| 107 |
+
push_notifications: bool = Field(default=True, description="Push notifications enabled")
|
| 108 |
+
sms_notifications: bool = Field(default=False, description="SMS notifications enabled")
|
| 109 |
+
dashboard_widgets: List[str] = Field(
|
| 110 |
+
default_factory=lambda: ['recent_tickets', 'team_performance', 'sla_metrics'],
|
| 111 |
+
description="Dashboard widgets"
|
| 112 |
+
)
|
| 113 |
+
default_tickets_view: str = Field(default="list", description="Default tickets view")
|
| 114 |
+
tickets_per_page: int = Field(default=25, description="Tickets per page")
|
| 115 |
+
default_sort_field: str = Field(default="created_at", description="Default sort field")
|
| 116 |
+
default_sort_order: str = Field(default="desc", description="Default sort order")
|
| 117 |
+
default_map_zoom: int = Field(default=12, description="Default map zoom")
|
| 118 |
+
default_map_center_lat: Optional[float] = Field(None, description="Map center latitude")
|
| 119 |
+
default_map_center_lng: Optional[float] = Field(None, description="Map center longitude")
|
| 120 |
+
additional_settings: Dict[str, Any] = Field(default_factory=dict, description="Additional settings")
|
| 121 |
+
|
| 122 |
+
class Config:
|
| 123 |
+
from_attributes = True
|
| 124 |
+
json_schema_extra = {
|
| 125 |
+
"example": {
|
| 126 |
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
| 127 |
+
"user_id": "550e8400-e29b-41d4-a716-446655440001",
|
| 128 |
+
"favorite_apps": ["dashboard", "organizations", "users", "activity"],
|
| 129 |
+
"theme": "light",
|
| 130 |
+
"language": "en",
|
| 131 |
+
"email_notifications": True,
|
| 132 |
+
"push_notifications": True,
|
| 133 |
+
"sms_notifications": False,
|
| 134 |
+
"dashboard_widgets": ["recent_tickets", "team_performance", "sla_metrics"],
|
| 135 |
+
"default_tickets_view": "list",
|
| 136 |
+
"tickets_per_page": 25,
|
| 137 |
+
"default_sort_field": "created_at",
|
| 138 |
+
"default_sort_order": "desc",
|
| 139 |
+
"default_map_zoom": 12,
|
| 140 |
+
"default_map_center_lat": None,
|
| 141 |
+
"default_map_center_lng": None,
|
| 142 |
+
"additional_settings": {}
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# Default favorite apps by role (using database app codes - lowercase with underscores)
|
| 148 |
+
DEFAULT_FAVORITE_APPS = {
|
| 149 |
+
"platform_admin": ["dashboard", "organizations", "users", "activity"],
|
| 150 |
+
"client_admin": ["dashboard", "projects", "tickets", "team"],
|
| 151 |
+
"contractor_admin": ["dashboard", "projects", "tickets", "team"],
|
| 152 |
+
"sales_manager": ["dashboard", "sales_orders", "customers", "reports"],
|
| 153 |
+
"project_manager": ["dashboard", "projects", "tickets", "team"],
|
| 154 |
+
"dispatcher": ["dashboard", "tickets", "maps", "team"],
|
| 155 |
+
"field_agent": ["tickets", "maps", "timesheets", "profile"],
|
| 156 |
+
"sales_agent": ["dashboard", "sales_orders", "customers", "maps"]
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
# Available apps by role (what they can favorite - max 6)
|
| 160 |
+
AVAILABLE_APPS = {
|
| 161 |
+
"platform_admin": [
|
| 162 |
+
# Core apps
|
| 163 |
+
"dashboard", "organizations", "users", "activity",
|
| 164 |
+
# Management
|
| 165 |
+
"settings", "billing", "notifications", "help"
|
| 166 |
+
],
|
| 167 |
+
"client_admin": [
|
| 168 |
+
"dashboard", "projects", "tickets", "team", "sales_orders",
|
| 169 |
+
"customers", "contractors", "reports", "settings", "help"
|
| 170 |
+
],
|
| 171 |
+
"contractor_admin": [
|
| 172 |
+
"dashboard", "projects", "tickets", "team", "timesheets",
|
| 173 |
+
"payroll", "reports", "settings", "help"
|
| 174 |
+
],
|
| 175 |
+
"sales_manager": [
|
| 176 |
+
"dashboard", "sales_orders", "customers", "reports",
|
| 177 |
+
"team", "maps", "settings", "help"
|
| 178 |
+
],
|
| 179 |
+
"project_manager": [
|
| 180 |
+
"dashboard", "projects", "tickets", "team", "reports",
|
| 181 |
+
"maps", "settings", "help"
|
| 182 |
+
],
|
| 183 |
+
"dispatcher": [
|
| 184 |
+
"dashboard", "tickets", "maps", "team", "projects",
|
| 185 |
+
"reports", "settings", "help"
|
| 186 |
+
],
|
| 187 |
+
"field_agent": [
|
| 188 |
+
"tickets", "maps", "timesheets", "profile", "expenses",
|
| 189 |
+
"documents", "help"
|
| 190 |
+
],
|
| 191 |
+
"sales_agent": [
|
| 192 |
+
"dashboard", "sales_orders", "customers", "maps",
|
| 193 |
+
"profile", "reports", "help"
|
| 194 |
+
]
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
# Available dashboard widgets by role
|
| 198 |
+
DEFAULT_DASHBOARD_WIDGETS = {
|
| 199 |
+
"platform_admin": ["recent_tickets", "team_performance", "sla_metrics", "organizations_overview"],
|
| 200 |
+
"client_admin": ["recent_tickets", "team_performance", "sla_metrics", "project_status"],
|
| 201 |
+
"contractor_admin": ["recent_tickets", "team_performance", "payroll_summary", "project_status"],
|
| 202 |
+
"sales_manager": ["sales_pipeline", "revenue_metrics", "team_performance", "conversion_rates"],
|
| 203 |
+
"project_manager": ["project_status", "team_performance", "sla_metrics", "map_view"],
|
| 204 |
+
"dispatcher": ["recent_tickets", "map_view", "team_availability", "sla_metrics"],
|
| 205 |
+
"field_agent": ["my_tickets", "earnings_summary", "attendance_summary"],
|
| 206 |
+
"sales_agent": ["my_sales", "customer_pipeline", "earnings_summary"]
|
| 207 |
+
}
|