""" Appreciation UI views - Server-rendered templates for appreciation management """ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType from django.core.paginator import Paginator from django.db.models import Q, Count from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.views.decorators.http import require_http_methods from apps.accounts.models import User from apps.core.services import AuditService from apps.organizations.models import Department, Hospital, Staff from .models import ( Appreciation, AppreciationBadge, AppreciationCategory, AppreciationStatus, AppreciationVisibility, AppreciationStats, UserBadge, ) # ============================================================================ # APPRECIATION LIST & DETAIL VIEWS # ============================================================================ @login_required def appreciation_list(request): """ Appreciations list view with advanced filters and pagination. Features: - Server-side pagination - Advanced filters (status, visibility, category, hospital, department) - Search by message - Tab-based navigation (Received, Sent, Leaderboard, Badges) """ # Base queryset with optimizations queryset = Appreciation.objects.select_related( 'sender', 'hospital', 'department', 'category' ) # Apply RBAC filters user = request.user if user.is_px_admin(): pass # See all elif user.is_hospital_admin() and user.hospital: queryset = queryset.filter(hospital=user.hospital) elif user.is_department_manager() and user.department: queryset = queryset.filter( Q(department=user.department) | Q(department__isnull=True) ) elif user.hospital: queryset = queryset.filter(hospital=user.hospital) else: queryset = queryset.none() # Apply visibility filter based on user from django.contrib.contenttypes.models import ContentType user_content_type = ContentType.objects.get_for_model(user) visibility_filter = ( Q(sender=user) | Q(recipient_content_type=user_content_type, recipient_object_id=user.id) ) if user.department: visibility_filter |= Q(visibility=AppreciationVisibility.DEPARTMENT, department=user.department) if user.hospital: visibility_filter |= Q(visibility=AppreciationVisibility.HOSPITAL, hospital=user.hospital) visibility_filter |= Q(visibility=AppreciationVisibility.PUBLIC) queryset = queryset.filter(visibility_filter) # Apply filters from request tab = request.GET.get('tab', 'received') if tab == 'received': # Show appreciations received by user queryset = queryset.filter( recipient_content_type=user_content_type, recipient_object_id=user.id ) elif tab == 'sent': # Show appreciations sent by user queryset = queryset.filter(sender=user) # 'leaderboard' and 'badges' tabs are handled separately status_filter = request.GET.get('status') if status_filter: queryset = queryset.filter(status=status_filter) visibility_filter_req = request.GET.get('visibility') if visibility_filter_req: queryset = queryset.filter(visibility=visibility_filter_req) category_filter = request.GET.get('category') if category_filter: queryset = queryset.filter(category_id=category_filter) hospital_filter = request.GET.get('hospital') if hospital_filter: queryset = queryset.filter(hospital_id=hospital_filter) department_filter = request.GET.get('department') if department_filter: queryset = queryset.filter(department_id=department_filter) # Search search_query = request.GET.get('search') if search_query: queryset = queryset.filter( Q(message_en__icontains=search_query) | Q(message_ar__icontains=search_query) ) # Date range filters date_from = request.GET.get('date_from') if date_from: queryset = queryset.filter(sent_at__gte=date_from) date_to = request.GET.get('date_to') if date_to: queryset = queryset.filter(sent_at__lte=date_to) # Ordering order_by = request.GET.get('order_by', '-sent_at') queryset = queryset.order_by(order_by) # Pagination page_size = int(request.GET.get('page_size', 25)) paginator = Paginator(queryset, page_size) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) # Get filter options hospitals = Hospital.objects.filter(status='active') if not user.is_px_admin() and user.hospital: hospitals = hospitals.filter(id=user.hospital.id) departments = Department.objects.filter(status='active') if not user.is_px_admin() and user.hospital: departments = departments.filter(hospital=user.hospital) categories = AppreciationCategory.objects.filter(is_active=True) if not user.is_px_admin() and user.hospital: categories = categories.filter(Q(hospital_id=user.hospital.id) | Q(hospital__isnull=True)) # Statistics user_content_type = ContentType.objects.get_for_model(user) stats = { 'received': Appreciation.objects.filter( recipient_content_type=user_content_type, recipient_object_id=user.id ).count(), 'sent': Appreciation.objects.filter(sender=user).count(), 'badges_earned': UserBadge.objects.filter( recipient_content_type=user_content_type, recipient_object_id=user.id ).count(), } context = { 'page_obj': page_obj, 'appreciations': page_obj.object_list, 'stats': stats, 'hospitals': hospitals, 'departments': departments, 'categories': categories, 'status_choices': AppreciationStatus.choices, 'visibility_choices': AppreciationVisibility.choices, 'current_tab': tab, 'filters': request.GET, } return render(request, 'appreciation/appreciation_list.html', context) @login_required def appreciation_detail(request, pk): """ Appreciation detail view. Features: - Full appreciation details - Acknowledge action for recipients - Related appreciations """ appreciation = get_object_or_404( Appreciation.objects.select_related( 'sender', 'hospital', 'department', 'category' ), pk=pk ) # Check access user = request.user from django.contrib.contenttypes.models import ContentType user_content_type = ContentType.objects.get_for_model(user) can_view = ( appreciation.sender == user or (appreciation.recipient_content_type == user_content_type and appreciation.recipient_object_id == user.id) or user.is_px_admin() or (user.is_hospital_admin() and appreciation.hospital == user.hospital) ) if not can_view: messages.error(request, "You don't have permission to view this appreciation.") return redirect('appreciation:appreciation_list') # Check if user is recipient is_recipient = ( appreciation.recipient_content_type == user_content_type and appreciation.recipient_object_id == user.id ) # Get related appreciations related = Appreciation.objects.filter( recipient_content_type=appreciation.recipient_content_type, recipient_object_id=appreciation.recipient_object_id ).exclude(pk=appreciation.pk)[:5] context = { 'appreciation': appreciation, 'is_recipient': is_recipient, 'related': related, 'status_choices': AppreciationStatus.choices, 'visibility_choices': AppreciationVisibility.choices, } return render(request, 'appreciation/appreciation_detail.html', context) @login_required @require_http_methods(["GET", "POST"]) def appreciation_send(request): """Send appreciation form""" if request.method == 'POST': try: # Get form data recipient_type = request.POST.get('recipient_type') recipient_id = request.POST.get('recipient_id') category_id = request.POST.get('category_id', None) message_en = request.POST.get('message_en') message_ar = request.POST.get('message_ar', '') visibility = request.POST.get('visibility', AppreciationVisibility.PRIVATE) is_anonymous = request.POST.get('is_anonymous') == 'on' hospital_id = request.POST.get('hospital_id') department_id = request.POST.get('department_id', None) # Validate required fields if not all([recipient_type, recipient_id, message_en, hospital_id]): messages.error(request, "Please fill in all required fields.") return redirect('appreciation:appreciation_send') # Get recipient if recipient_type == 'user': recipient = User.objects.get(id=recipient_id) recipient_content_type = ContentType.objects.get_for_model(User) else: # staff recipient = Staff.objects.get(id=recipient_id) recipient_content_type = ContentType.objects.get_for_model(Staff) # Get hospital and department hospital = Hospital.objects.get(id=hospital_id) department = None if department_id: department = Department.objects.get(id=department_id) # Get category category = None if category_id: category = AppreciationCategory.objects.get(id=category_id) # Create appreciation appreciation = Appreciation.objects.create( sender=request.user, recipient_content_type=recipient_content_type, recipient_object_id=recipient_id, hospital=hospital, department=department, category=category, message_en=message_en, message_ar=message_ar, visibility=visibility, is_anonymous=is_anonymous, ) # Send appreciation appreciation.send() # Log audit AuditService.log_event( event_type='appreciation_sent', description=f"Appreciation sent to {str(recipient)}", user=request.user, content_object=appreciation, metadata={ 'visibility': visibility, 'category': category.name_en if category else None, 'anonymous': is_anonymous } ) messages.success(request, "Appreciation sent successfully!") return redirect('appreciation:appreciation_detail', pk=appreciation.id) except User.DoesNotExist: messages.error(request, "User not found.") except Staff.DoesNotExist: messages.error(request, "Staff not found.") except Exception as e: messages.error(request, f"Error sending appreciation: {str(e)}") return redirect('appreciation:appreciation_send') # GET request - show form hospitals = Hospital.objects.filter(status='active') if not request.user.is_px_admin() and request.user.hospital: hospitals = hospitals.filter(id=request.user.hospital.id) categories = AppreciationCategory.objects.filter(is_active=True) if not request.user.is_px_admin() and request.user.hospital: categories = categories.filter(Q(hospital_id=request.user.hospital.id) | Q(hospital__isnull=True)) context = { 'hospitals': hospitals, 'categories': categories, 'visibility_choices': AppreciationVisibility.choices, } return render(request, 'appreciation/appreciation_send_form.html', context) @login_required @require_http_methods(["POST"]) def appreciation_acknowledge(request, pk): """Acknowledge appreciation""" appreciation = get_object_or_404(Appreciation, pk=pk) # Check if user is recipient user_content_type = ContentType.objects.get_for_model(request.user) if not ( appreciation.recipient_content_type == user_content_type and appreciation.recipient_object_id == request.user.id ): messages.error(request, "You can only acknowledge appreciations sent to you.") return redirect('appreciation:appreciation_detail', pk=pk) # Acknowledge appreciation.acknowledge() messages.success(request, "Appreciation acknowledged successfully.") return redirect('appreciation:appreciation_detail', pk=pk) # ============================================================================ # LEADERBOARD VIEWS # ============================================================================ @login_required def leaderboard_view(request): """ Appreciation leaderboard view. Features: - Monthly rankings - Hospital and department filters - Top recipients with badges """ user = request.user # Get date range now = timezone.now() year = int(request.GET.get('year', now.year)) month = int(request.GET.get('month', now.month)) # Build base query queryset = AppreciationStats.objects.filter(year=year, month=month) # Apply RBAC if not user.is_px_admin() and user.hospital: queryset = queryset.filter(hospital=user.hospital) # Apply filters hospital_filter = request.GET.get('hospital') if hospital_filter: queryset = queryset.filter(hospital_id=hospital_filter) department_filter = request.GET.get('department') if department_filter: queryset = queryset.filter(department_id=department_filter) # Order by received count queryset = queryset.order_by('-received_count') # Pagination page_size = int(request.GET.get('page_size', 50)) paginator = Paginator(queryset, page_size) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) # Get filter options hospitals = Hospital.objects.filter(status='active') if not user.is_px_admin() and user.hospital: hospitals = hospitals.filter(id=user.hospital.id) departments = Department.objects.filter(status='active') if not user.is_px_admin() and user.hospital: departments = departments.filter(hospital=user.hospital) # Get months for filter months = [(i, timezone.datetime(year=year, month=i, day=1).strftime('%B')) for i in range(1, 13)] years = range(now.year - 1, now.year + 2) context = { 'page_obj': page_obj, 'leaderboard': page_obj.object_list, 'hospitals': hospitals, 'departments': departments, 'months': months, 'years': years, 'selected_year': year, 'selected_month': month, 'filters': request.GET, } return render(request, 'appreciation/leaderboard.html', context) @login_required def my_badges_view(request): """ User's badges view. Features: - All earned badges - Badge details and criteria - Progress toward next badges """ user = request.user user_content_type = ContentType.objects.get_for_model(user) # Get user's badges queryset = UserBadge.objects.filter( recipient_content_type=user_content_type, recipient_object_id=user.id ).select_related('badge').order_by('-earned_at') # Pagination page_size = int(request.GET.get('page_size', 20)) paginator = Paginator(queryset, page_size) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) # Get available badges for progress tracking available_badges = AppreciationBadge.objects.filter(is_active=True) if not request.user.is_px_admin() and request.user.hospital: available_badges = available_badges.filter(Q(hospital_id=request.user.hospital.id) | Q(hospital__isnull=True)) # Calculate progress for each badge badge_progress = [] total_received = Appreciation.objects.filter( recipient_content_type=user_content_type, recipient_object_id=user.id ).count() for badge in available_badges: earned = queryset.filter(badge=badge).exists() progress = 0 if badge.criteria_type == 'count': progress = min(100, int((total_received / badge.criteria_value) * 100)) badge_progress.append({ 'badge': badge, 'earned': earned, 'progress': progress, }) context = { 'page_obj': page_obj, 'badges': page_obj.object_list, 'total_received': total_received, 'badge_progress': badge_progress, } return render(request, 'appreciation/my_badges.html', context) # ============================================================================ # ADMIN: CATEGORY MANAGEMENT # ============================================================================ @login_required def category_list(request): """List and manage appreciation categories""" user = request.user # Check permission if not (user.is_px_admin() or user.is_hospital_admin()): messages.error(request, "You don't have permission to manage categories.") return redirect('appreciation:appreciation_list') # Base queryset queryset = AppreciationCategory.objects.all() # Apply RBAC if not user.is_px_admin() and user.hospital: queryset = queryset.filter(Q(hospital_id=user.hospital.id) | Q(hospital__isnull=True)) # Search search_query = request.GET.get('search') if search_query: queryset = queryset.filter( Q(name_en__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(code__icontains=search_query) ) # Ordering queryset = queryset.order_by('order', 'code') # Pagination page_size = int(request.GET.get('page_size', 25)) paginator = Paginator(queryset, page_size) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) context = { 'page_obj': page_obj, 'categories': page_obj.object_list, } return render(request, 'appreciation/admin/category_list.html', context) @login_required @require_http_methods(["GET", "POST"]) def category_create(request): """Create appreciation category""" user = request.user # Check permission if not (user.is_px_admin() or user.is_hospital_admin()): messages.error(request, "You don't have permission to create categories.") return redirect('appreciation:appreciation_list') if request.method == 'POST': try: code = request.POST.get('code') name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar', '') description_en = request.POST.get('description_en', '') description_ar = request.POST.get('description_ar', '') icon = request.POST.get('icon', 'fa-heart') color = request.POST.get('color', '#FF5733') order = request.POST.get('order', 0) is_active = request.POST.get('is_active') == 'on' # Get hospital hospital = None if user.is_hospital_admin() and user.hospital: hospital = user.hospital # Validate if not all([code, name_en]): messages.error(request, "Please fill in all required fields.") return redirect('appreciation:category_create') # Create category AppreciationCategory.objects.create( code=code, name_en=name_en, name_ar=name_ar, description_en=description_en, description_ar=description_ar, icon=icon, color=color, order=order, is_active=is_active, hospital=hospital, ) messages.success(request, "Category created successfully.") return redirect('appreciation:category_list') except Exception as e: messages.error(request, f"Error creating category: {str(e)}") return redirect('appreciation:category_create') context = {} return render(request, 'appreciation/admin/category_form.html', context) @login_required @require_http_methods(["GET", "POST"]) def category_edit(request, pk): """Edit appreciation category""" user = request.user # Check permission if not (user.is_px_admin() or user.is_hospital_admin()): messages.error(request, "You don't have permission to edit categories.") return redirect('appreciation:appreciation_list') category = get_object_or_404(AppreciationCategory, pk=pk) # Check access if not user.is_px_admin() and category.hospital != user.hospital: messages.error(request, "You don't have permission to edit this category.") return redirect('appreciation:category_list') if request.method == 'POST': try: category.code = request.POST.get('code') category.name_en = request.POST.get('name_en') category.name_ar = request.POST.get('name_ar', '') category.description_en = request.POST.get('description_en', '') category.description_ar = request.POST.get('description_ar', '') category.icon = request.POST.get('icon', 'fa-heart') category.color = request.POST.get('color', '#FF5733') category.order = request.POST.get('order', 0) category.is_active = request.POST.get('is_active') == 'on' category.save() messages.success(request, "Category updated successfully.") return redirect('appreciation:category_list') except Exception as e: messages.error(request, f"Error updating category: {str(e)}") return redirect('appreciation:category_edit', pk=pk) context = { 'category': category, } return render(request, 'appreciation/admin/category_form.html', context) @login_required @require_http_methods(["POST"]) def category_delete(request, pk): """Delete appreciation category""" user = request.user # Check permission if not (user.is_px_admin() or user.is_hospital_admin()): messages.error(request, "You don't have permission to delete categories.") return redirect('appreciation:appreciation_list') category = get_object_or_404(AppreciationCategory, pk=pk) # Check if category is in use if Appreciation.objects.filter(category=category).exists(): messages.error(request, "Cannot delete category that is in use.") return redirect('appreciation:category_list') # Log audit AuditService.log_event( event_type='category_deleted', description=f"Appreciation category deleted: {category.name_en}", user=request.user, metadata={'category_code': category.code} ) category.delete() messages.success(request, "Category deleted successfully.") return redirect('appreciation:category_list') # ============================================================================ # ADMIN: BADGE MANAGEMENT # ============================================================================ @login_required def badge_list(request): """List and manage appreciation badges""" user = request.user # Check permission if not (user.is_px_admin() or user.is_hospital_admin()): messages.error(request, "You don't have permission to manage badges.") return redirect('appreciation:appreciation_list') # Base queryset queryset = AppreciationBadge.objects.all() # Apply RBAC if not user.is_px_admin() and user.hospital: queryset = queryset.filter(Q(hospital_id=user.hospital.id) | Q(hospital__isnull=True)) # Search search_query = request.GET.get('search') if search_query: queryset = queryset.filter( Q(name_en__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(code__icontains=search_query) ) # Ordering queryset = queryset.order_by('order', 'code') # Pagination page_size = int(request.GET.get('page_size', 25)) paginator = Paginator(queryset, page_size) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) context = { 'page_obj': page_obj, 'badges': page_obj.object_list, } return render(request, 'appreciation/admin/badge_list.html', context) @login_required @require_http_methods(["GET", "POST"]) def badge_create(request): """Create appreciation badge""" user = request.user # Check permission if not (user.is_px_admin() or user.is_hospital_admin()): messages.error(request, "You don't have permission to create badges.") return redirect('appreciation:appreciation_list') if request.method == 'POST': try: code = request.POST.get('code') name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar', '') description_en = request.POST.get('description_en', '') description_ar = request.POST.get('description_ar', '') icon = request.POST.get('icon', 'fa-award') color = request.POST.get('color', '#FFD700') criteria_type = request.POST.get('criteria_type', 'count') criteria_value = request.POST.get('criteria_value', 5) order = request.POST.get('order', 0) is_active = request.POST.get('is_active') == 'on' # Get hospital hospital = None if user.is_hospital_admin() and user.hospital: hospital = user.hospital # Validate if not all([code, name_en, criteria_value]): messages.error(request, "Please fill in all required fields.") return redirect('appreciation:badge_create') # Create badge AppreciationBadge.objects.create( code=code, name_en=name_en, name_ar=name_ar, description_en=description_en, description_ar=description_ar, icon=icon, color=color, criteria_type=criteria_type, criteria_value=int(criteria_value), order=order, is_active=is_active, hospital=hospital, ) messages.success(request, "Badge created successfully.") return redirect('appreciation:badge_list') except Exception as e: messages.error(request, f"Error creating badge: {str(e)}") return redirect('appreciation:badge_create') context = {} return render(request, 'appreciation/admin/badge_form.html', context) @login_required @require_http_methods(["GET", "POST"]) def badge_edit(request, pk): """Edit appreciation badge""" user = request.user # Check permission if not (user.is_px_admin() or user.is_hospital_admin()): messages.error(request, "You don't have permission to edit badges.") return redirect('appreciation:appreciation_list') badge = get_object_or_404(AppreciationBadge, pk=pk) # Check access if not user.is_px_admin() and badge.hospital != user.hospital: messages.error(request, "You don't have permission to edit this badge.") return redirect('appreciation:badge_list') if request.method == 'POST': try: badge.code = request.POST.get('code') badge.name_en = request.POST.get('name_en') badge.name_ar = request.POST.get('name_ar', '') badge.description_en = request.POST.get('description_en', '') badge.description_ar = request.POST.get('description_ar', '') badge.icon = request.POST.get('icon', 'fa-award') badge.color = request.POST.get('color', '#FFD700') badge.criteria_type = request.POST.get('criteria_type', 'count') badge.criteria_value = request.POST.get('criteria_value', 5) badge.order = request.POST.get('order', 0) badge.is_active = request.POST.get('is_active') == 'on' badge.save() messages.success(request, "Badge updated successfully.") return redirect('appreciation:badge_list') except Exception as e: messages.error(request, f"Error updating badge: {str(e)}") return redirect('appreciation:badge_edit', pk=pk) context = { 'badge': badge, } return render(request, 'appreciation/admin/badge_form.html', context) @login_required @require_http_methods(["POST"]) def badge_delete(request, pk): """Delete appreciation badge""" user = request.user # Check permission if not (user.is_px_admin() or user.is_hospital_admin()): messages.error(request, "You don't have permission to delete badges.") return redirect('appreciation:appreciation_list') badge = get_object_or_404(AppreciationBadge, pk=pk) # Check if badge is in use if UserBadge.objects.filter(badge=badge).exists(): messages.error(request, "Cannot delete badge that has been earned.") return redirect('appreciation:badge_list') # Log audit AuditService.log_event( event_type='badge_deleted', description=f"Appreciation badge deleted: {badge.name_en}", user=request.user, metadata={'badge_code': badge.code} ) badge.delete() messages.success(request, "Badge deleted successfully.") return redirect('appreciation:badge_list') # ============================================================================ # AJAX/API HELPERS # ============================================================================ @login_required def get_users_by_hospital(request): """Get users for a hospital (AJAX)""" hospital_id = request.GET.get('hospital_id') if not hospital_id: return JsonResponse({'users': []}) users = User.objects.filter( hospital_id=hospital_id, is_active=True ).values('id', 'first_name', 'last_name') results = [ { 'id': str(u['id']), 'name': f"{u['first_name']} {u['last_name']}", } for u in users ] return JsonResponse({'users': results}) @login_required def get_staff_by_hospital(request): """Get staff for a hospital (AJAX)""" hospital_id = request.GET.get('hospital_id') if not hospital_id: return JsonResponse({'staff': []}) staff = Staff.objects.filter( hospital_id=hospital_id, status='active' ).values('id', 'user__first_name', 'user__last_name') results = [ { 'id': str(s['id']), 'name': f"{s['user__first_name']} {s['user__last_name']}", } for s in staff ] return JsonResponse({'staff': results}) @login_required def get_departments_by_hospital(request): """Get departments for a hospital (AJAX)""" hospital_id = request.GET.get('hospital_id') if not hospital_id: return JsonResponse({'departments': []}) departments = Department.objects.filter( hospital_id=hospital_id, status='active' ).values('id', 'name', 'name_ar') results = [ { 'id': str(d['id']), 'name': d['name'], } for d in departments ] return JsonResponse({'departments': results}) @login_required def appreciation_summary_ajax(request): """Get appreciation summary for current user (AJAX)""" user = request.user user_content_type = ContentType.objects.get_for_model(user) now = timezone.now() current_year = now.year current_month = now.month summary = { 'total_received': Appreciation.objects.filter( recipient_content_type=user_content_type, recipient_object_id=user.id ).count(), 'total_sent': Appreciation.objects.filter(sender=user).count(), 'this_month_received': Appreciation.objects.filter( recipient_content_type=user_content_type, recipient_object_id=user.id, sent_at__year=current_year, sent_at__month=current_month ).count(), 'this_month_sent': Appreciation.objects.filter( sender=user, sent_at__year=current_year, sent_at__month=current_month ).count(), 'badges_earned': UserBadge.objects.filter( recipient_content_type=user_content_type, recipient_object_id=user.id ).count(), } # Get hospital rank if user.hospital: stats = AppreciationStats.objects.filter( recipient_content_type=user_content_type, recipient_object_id=user.id, year=current_year, month=current_month ).first() summary['hospital_rank'] = stats.hospital_rank if stats else None return JsonResponse(summary)