feat: Add mobile responsive layout, open room access, and admin room management
Mobile Responsive Layout: - Add useMediaQuery, useIsMobile, useIsTablet, useIsDesktop hooks for device detection - Create MobileHeader component with hamburger menu and action drawer - Create BottomToolbar for mobile navigation (Files, Members) - Create SlidePanel component for full-screen mobile sidebars - Update RoomDetail.tsx with mobile/desktop conditional rendering - Update RoomList.tsx with single-column grid and touch-friendly buttons - Add CSS custom properties for safe areas and touch targets (min 44px) - Add mobile viewport meta tags for notched devices Open Room Access: - All authenticated users can view all rooms (not just their own) - Users can join active rooms they're not members of - Add is_member field to room responses - Update room list API to return all rooms by default Admin Room Management: - Add permanent delete functionality for system admins - Add delete confirmation dialog with room title verification - Broadcast room deletion via WebSocket to connected users - Add users search API for adding members 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,16 @@ Handles business logic for room CRUD operations
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, and_, func
|
||||
from typing import List, Optional, Dict
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from app.modules.chat_room.models import IncidentRoom, RoomMember, RoomStatus, MemberRole
|
||||
from app.modules.chat_room.schemas import CreateRoomRequest, UpdateRoomRequest, RoomFilterParams
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RoomService:
|
||||
"""Service for room management operations"""
|
||||
@@ -111,6 +114,10 @@ class RoomService:
|
||||
) -> List[IncidentRoom]:
|
||||
"""List rooms accessible to user with filters
|
||||
|
||||
All authenticated users can see all rooms by default.
|
||||
Use my_rooms=true to filter to only rooms where user is a member.
|
||||
Non-admin users cannot see ARCHIVED rooms.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
user_id: User ID
|
||||
@@ -118,13 +125,16 @@ class RoomService:
|
||||
is_admin: Whether user is system admin
|
||||
|
||||
Returns:
|
||||
List of accessible rooms
|
||||
List of rooms and total count
|
||||
"""
|
||||
# Non-admin requesting archived rooms explicitly - return empty
|
||||
if not is_admin and filters.status == RoomStatus.ARCHIVED:
|
||||
return [], 0
|
||||
|
||||
query = db.query(IncidentRoom)
|
||||
|
||||
# Access control: admin sees all, others see only their rooms
|
||||
if not is_admin or not filters.all:
|
||||
# Join with room_members to filter by membership
|
||||
# Filter to user's rooms only if my_rooms=true
|
||||
if filters.my_rooms:
|
||||
query = query.join(RoomMember).filter(
|
||||
and_(
|
||||
RoomMember.user_id == user_id,
|
||||
@@ -132,6 +142,10 @@ class RoomService:
|
||||
)
|
||||
)
|
||||
|
||||
# Hide archived rooms from non-admin users
|
||||
if not is_admin:
|
||||
query = query.filter(IncidentRoom.status != RoomStatus.ARCHIVED)
|
||||
|
||||
# Apply filters
|
||||
if filters.status:
|
||||
query = query.filter(IncidentRoom.status == filters.status)
|
||||
@@ -381,6 +395,86 @@ class RoomService:
|
||||
room.last_activity_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
def permanent_delete_room(
|
||||
self,
|
||||
db: Session,
|
||||
room_id: str
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""Permanently delete a room and all associated data (admin only)
|
||||
|
||||
This is an irreversible operation that:
|
||||
1. Deletes all files from MinIO storage
|
||||
2. Deletes all report documents from MinIO storage
|
||||
3. Cascades delete to all related database records
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
room_id: Room ID to permanently delete
|
||||
|
||||
Returns:
|
||||
Tuple of (success, error_message)
|
||||
"""
|
||||
# Late imports to avoid circular dependency
|
||||
from app.modules.file_storage.models import RoomFile
|
||||
from app.modules.report_generation.models import GeneratedReport
|
||||
from app.modules.file_storage.services import minio_service
|
||||
from app.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Check room exists
|
||||
room = db.query(IncidentRoom).filter(
|
||||
IncidentRoom.room_id == room_id
|
||||
).first()
|
||||
|
||||
if not room:
|
||||
return False, "Room not found"
|
||||
|
||||
try:
|
||||
# Step 1: Delete room files from MinIO
|
||||
room_files = db.query(RoomFile).filter(
|
||||
RoomFile.room_id == room_id
|
||||
).all()
|
||||
|
||||
for rf in room_files:
|
||||
if rf.minio_object_path:
|
||||
success = minio_service.delete_file(
|
||||
rf.minio_bucket or settings.MINIO_BUCKET,
|
||||
rf.minio_object_path
|
||||
)
|
||||
if not success:
|
||||
logger.warning(
|
||||
f"Failed to delete MinIO file: {rf.minio_object_path}"
|
||||
)
|
||||
|
||||
# Step 2: Delete generated report documents from MinIO
|
||||
reports = db.query(GeneratedReport).filter(
|
||||
GeneratedReport.room_id == room_id
|
||||
).all()
|
||||
|
||||
for report in reports:
|
||||
if report.docx_storage_path:
|
||||
success = minio_service.delete_file(
|
||||
settings.MINIO_BUCKET,
|
||||
report.docx_storage_path
|
||||
)
|
||||
if not success:
|
||||
logger.warning(
|
||||
f"Failed to delete report file: {report.docx_storage_path}"
|
||||
)
|
||||
|
||||
# Step 3: Delete room from database (CASCADE handles related tables)
|
||||
db.delete(room)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Permanently deleted room {room_id} and all associated data")
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to permanently delete room {room_id}: {e}")
|
||||
return False, str(e)
|
||||
|
||||
|
||||
# Create singleton instance
|
||||
room_service = RoomService()
|
||||
Reference in New Issue
Block a user