""" QI Projects Console UI views Provides full CRUD functionality for Quality Improvement projects, task management, and template handling. """ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext_lazy as _ from apps.core.decorators import block_source_user from apps.organizations.models import Hospital from apps.px_action_center.models import PXAction from .forms import ConvertToProjectForm, QIProjectForm, QIProjectTaskForm, QIProjectTemplateForm from .models import QIProject, QIProjectTask @block_source_user @login_required def project_list(request): """QI Projects list view with filtering and pagination""" # Exclude templates from the list queryset = QIProject.objects.filter(is_template=False).select_related( 'hospital', 'department', 'project_lead' ).prefetch_related('team_members', 'related_actions') # Apply RBAC filters user = request.user # Get selected hospital for PX Admins (from middleware) selected_hospital = getattr(request, 'tenant_hospital', None) if user.is_px_admin(): # PX Admins see all, but filter by selected hospital if set if selected_hospital: queryset = queryset.filter(hospital=selected_hospital) elif user.hospital: queryset = queryset.filter(hospital=user.hospital) # Apply filters status_filter = request.GET.get('status') if status_filter: queryset = queryset.filter(status=status_filter) hospital_filter = request.GET.get('hospital') if hospital_filter: queryset = queryset.filter(hospital_id=hospital_filter) # Search search_query = request.GET.get('search') if search_query: queryset = queryset.filter( Q(name__icontains=search_query) | Q(description__icontains=search_query) | Q(name_ar__icontains=search_query) ) # Ordering queryset = queryset.order_by('-created_at') # 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 hospitals for filter hospitals = Hospital.objects.filter(status='active') if not user.is_px_admin() and user.hospital: hospitals = hospitals.filter(id=user.hospital.id) # Statistics stats = { 'total': queryset.count(), 'active': queryset.filter(status='active').count(), 'completed': queryset.filter(status='completed').count(), 'pending': queryset.filter(status='pending').count(), } context = { 'page_obj': page_obj, 'projects': page_obj.object_list, 'hospitals': hospitals, 'stats': stats, 'filters': request.GET, } return render(request, 'projects/project_list.html', context) @block_source_user @login_required def project_detail(request, pk): """QI Project detail view with task management""" project = get_object_or_404( QIProject.objects.filter(is_template=False).select_related( 'hospital', 'department', 'project_lead' ).prefetch_related( 'team_members', 'related_actions', 'tasks' ), pk=pk ) # Check permission user = request.user if not user.is_px_admin() and user.hospital and project.hospital != user.hospital: messages.error(request, _("You don't have permission to view this project.")) return redirect('projects:project_list') # Get tasks tasks = project.tasks.all().order_by('order', 'created_at') # Get related actions related_actions = project.related_actions.all() context = { 'project': project, 'tasks': tasks, 'related_actions': related_actions, 'can_edit': user.is_px_admin() or user.is_hospital_admin or user.is_department_manager, } return render(request, 'projects/project_detail.html', context) @block_source_user @login_required def project_create(request, template_pk=None): """Create a new QI Project""" user = request.user # Check permission (PX Admin, Hospital Admin, or Department Manager) if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager): messages.error(request, _("You don't have permission to create projects.")) return redirect('projects:project_list') # Check for template parameter (from URL or GET) template_id = template_pk or request.GET.get('template') initial_data = {} template = None if template_id: try: template = QIProject.objects.get(pk=template_id, is_template=True) initial_data = { 'name': template.name, 'name_ar': template.name_ar, 'description': template.description, 'department': template.department, 'target_completion_date': template.target_completion_date, } if not user.is_px_admin() and user.hospital: initial_data['hospital'] = user.hospital except QIProject.DoesNotExist: pass # Check for PX Action parameter (convert action to project) action_id = request.GET.get('action') if action_id: try: action = PXAction.objects.get(pk=action_id) initial_data['name'] = f"QI Project: {action.title}" initial_data['description'] = action.description if not user.is_px_admin() and user.hospital: initial_data['hospital'] = user.hospital else: initial_data['hospital'] = action.hospital except PXAction.DoesNotExist: pass if request.method == 'POST': form = QIProjectForm(request.POST, user=user) # Check for template in POST data (hidden field) template_id_post = request.POST.get('template_id') if template_id_post: try: template = QIProject.objects.get(pk=template_id_post, is_template=True) except QIProject.DoesNotExist: template = None if form.is_valid(): project = form.save(commit=False) project.created_by = user project.save() form.save_m2m() # Save many-to-many relationships # If created from template, copy tasks task_count = 0 if template: for template_task in template.tasks.all(): QIProjectTask.objects.create( project=project, title=template_task.title, description=template_task.description, order=template_task.order, status='pending' ) task_count += 1 # If created from action, link it if action_id: try: action = PXAction.objects.get(pk=action_id) project.related_actions.add(action) except PXAction.DoesNotExist: pass if template and task_count > 0: messages.success( request, _('QI Project created successfully with %(count)d task(s) from template.') % {'count': task_count} ) else: messages.success(request, _("QI Project created successfully.")) return redirect('projects:project_detail', pk=project.pk) else: form = QIProjectForm(user=user, initial=initial_data) context = { 'form': form, 'is_create': True, 'template': template, } return render(request, 'projects/project_form.html', context) @block_source_user @login_required def project_edit(request, pk): """Edit an existing QI Project""" user = request.user project = get_object_or_404(QIProject, pk=pk, is_template=False) # Check permission if not user.is_px_admin() and user.hospital and project.hospital != user.hospital: messages.error(request, _("You don't have permission to edit this project.")) return redirect('projects:project_list') # Check edit permission (PX Admin, Hospital Admin, or Department Manager) if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager): messages.error(request, _("You don't have permission to edit projects.")) return redirect('projects:project_detail', pk=project.pk) if request.method == 'POST': form = QIProjectForm(request.POST, instance=project, user=user) if form.is_valid(): form.save() messages.success(request, _("QI Project updated successfully.")) return redirect('projects:project_detail', pk=project.pk) else: form = QIProjectForm(instance=project, user=user) context = { 'form': form, 'project': project, 'is_create': False, } return render(request, 'projects/project_form.html', context) @block_source_user @login_required def project_delete(request, pk): """Delete a QI Project""" user = request.user project = get_object_or_404(QIProject, pk=pk, is_template=False) # Check permission (only PX Admin or Hospital Admin can delete) if not (user.is_px_admin() or user.is_hospital_admin): messages.error(request, _("You don't have permission to delete projects.")) return redirect('projects:project_detail', pk=project.pk) if not user.is_px_admin() and user.hospital and project.hospital != user.hospital: messages.error(request, _("You don't have permission to delete this project.")) return redirect('projects:project_list') if request.method == 'POST': project_name = project.name project.delete() messages.success(request, _('Project "%(name)s" deleted successfully.') % {'name': project_name}) return redirect('projects:project_list') context = { 'project': project, } return render(request, 'projects/project_delete_confirm.html', context) @block_source_user @login_required def project_save_as_template(request, pk): """Save an existing project and its tasks as a template""" user = request.user project = get_object_or_404(QIProject, pk=pk, is_template=False) # Check permission (only PX Admin or Hospital Admin can create templates) if not (user.is_px_admin() or user.is_hospital_admin): messages.error(request, _("You don't have permission to create templates.")) return redirect('projects:project_detail', pk=project.pk) # Check hospital access if not user.is_px_admin() and user.hospital and project.hospital != user.hospital: messages.error(request, _("You don't have permission to create templates from this project.")) return redirect('projects:project_list') if request.method == 'POST': template_name = request.POST.get('template_name', '').strip() template_description = request.POST.get('template_description', '').strip() make_global = request.POST.get('make_global') == 'on' if not template_name: messages.error(request, _('Please provide a template name.')) return redirect('projects:project_save_as_template', pk=project.pk) # Create template from project template = QIProject.objects.create( name=template_name, name_ar=template_name, # Can be edited later description=template_description or project.description, is_template=True, # If global, hospital is None; otherwise use project's hospital hospital=None if make_global else project.hospital, department=project.department, status='pending', # Default status for templates created_by=user ) # Copy tasks from project to template for task in project.tasks.all(): QIProjectTask.objects.create( project=template, title=task.title, description=task.description, order=task.order, status='pending' # Reset status for template ) messages.success( request, _('Template "%(name)s" created successfully with %(count)d task(s).') % { 'name': template_name, 'count': project.tasks.count() } ) return redirect('projects:template_list') context = { 'project': project, 'suggested_name': f"Template: {project.name}", } return render(request, 'projects/project_save_as_template.html', context) # ============================================================================= # Task Management Views # ============================================================================= @block_source_user @login_required def task_create(request, project_pk): """Add a new task to a project""" user = request.user project = get_object_or_404(QIProject, pk=project_pk, is_template=False) # Check permission if not user.is_px_admin() and user.hospital and project.hospital != user.hospital: messages.error(request, _("You don't have permission to add tasks to this project.")) return redirect('projects:project_detail', pk=project.pk) if request.method == 'POST': form = QIProjectTaskForm(request.POST, project=project) if form.is_valid(): task = form.save(commit=False) task.project = project task.save() messages.success(request, _("Task added successfully.")) return redirect('projects:project_detail', pk=project.pk) else: form = QIProjectTaskForm(project=project) context = { 'form': form, 'project': project, 'is_create': True, } return render(request, 'projects/task_form.html', context) @block_source_user @login_required def task_edit(request, project_pk, task_pk): """Edit an existing task""" user = request.user project = get_object_or_404(QIProject, pk=project_pk, is_template=False) task = get_object_or_404(QIProjectTask, pk=task_pk, project=project) # Check permission if not user.is_px_admin() and user.hospital and project.hospital != user.hospital: messages.error(request, _("You don't have permission to edit tasks in this project.")) return redirect('projects:project_detail', pk=project.pk) if request.method == 'POST': form = QIProjectTaskForm(request.POST, instance=task, project=project) if form.is_valid(): task = form.save() # If status changed to completed, set completed_date if task.status == 'completed' and not task.completed_date: from django.utils import timezone task.completed_date = timezone.now().date() task.save() messages.success(request, _("Task updated successfully.")) return redirect('projects:project_detail', pk=project.pk) else: form = QIProjectTaskForm(instance=task, project=project) context = { 'form': form, 'project': project, 'task': task, 'is_create': False, } return render(request, 'projects/task_form.html', context) @block_source_user @login_required def task_delete(request, project_pk, task_pk): """Delete a task""" user = request.user project = get_object_or_404(QIProject, pk=project_pk, is_template=False) task = get_object_or_404(QIProjectTask, pk=task_pk, project=project) # Check permission if not user.is_px_admin() and user.hospital and project.hospital != user.hospital: messages.error(request, _("You don't have permission to delete tasks in this project.")) return redirect('projects:project_detail', pk=project.pk) if request.method == 'POST': task.delete() messages.success(request, _("Task deleted successfully.")) return redirect('projects:project_detail', pk=project.pk) context = { 'project': project, 'task': task, } return render(request, 'projects/task_delete_confirm.html', context) @block_source_user @login_required def task_toggle_status(request, project_pk, task_pk): """Quick toggle task status between pending and completed""" user = request.user project = get_object_or_404(QIProject, pk=project_pk, is_template=False) task = get_object_or_404(QIProjectTask, pk=task_pk, project=project) # Check permission if not user.is_px_admin() and user.hospital and project.hospital != user.hospital: messages.error(request, _("You don't have permission to update tasks in this project.")) return redirect('projects:project_detail', pk=project.pk) from django.utils import timezone if task.status == 'completed': task.status = 'pending' task.completed_date = None else: task.status = 'completed' task.completed_date = timezone.now().date() task.save() messages.success(request, _("Task status updated.")) return redirect('projects:project_detail', pk=project.pk) # ============================================================================= # Template Management Views # ============================================================================= @block_source_user @login_required def template_list(request): """List QI Project templates""" user = request.user # Only admins can manage templates if not (user.is_px_admin() or user.is_hospital_admin): messages.error(request, _("You don't have permission to view templates.")) return redirect('projects:project_list') queryset = QIProject.objects.filter(is_template=True).select_related('hospital', 'department') # Apply RBAC filters if not user.is_px_admin(): from django.db.models import Q queryset = queryset.filter( Q(hospital=user.hospital) | Q(hospital__isnull=True) ) # Search search_query = request.GET.get('search') if search_query: queryset = queryset.filter( Q(name__icontains=search_query) | Q(description__icontains=search_query) | Q(name_ar__icontains=search_query) ) queryset = queryset.order_by('name') context = { 'templates': queryset, 'can_create': user.is_px_admin() or user.is_hospital_admin, 'can_edit': user.is_px_admin() or user.is_hospital_admin, } return render(request, 'projects/template_list.html', context) @block_source_user @login_required def template_detail(request, pk): """View template details with tasks""" user = request.user # Only admins can view templates if not (user.is_px_admin() or user.is_hospital_admin): messages.error(request, _("You don't have permission to view templates.")) return redirect('projects:project_list') template = get_object_or_404( QIProject.objects.filter(is_template=True).select_related('hospital', 'department'), pk=pk ) # Check permission for hospital-specific templates if not user.is_px_admin(): from django.db.models import Q if template.hospital and template.hospital != user.hospital: messages.error(request, _("You don't have permission to view this template.")) return redirect('projects:template_list') # Get tasks tasks = template.tasks.all().order_by('order', 'created_at') context = { 'template': template, 'tasks': tasks, } return render(request, 'projects/template_detail.html', context) @block_source_user @login_required def template_create(request): """Create a new project template""" user = request.user # Only admins can create templates if not (user.is_px_admin() or user.is_hospital_admin): messages.error(request, _("You don't have permission to create templates.")) return redirect('projects:project_list') if request.method == 'POST': form = QIProjectTemplateForm(request.POST, user=user) if form.is_valid(): template = form.save(commit=False) template.is_template = True template.created_by = user template.save() messages.success(request, _("Project template created successfully.")) return redirect('projects:template_list') else: form = QIProjectTemplateForm(user=user) context = { 'form': form, 'is_create': True, } return render(request, 'projects/template_form.html', context) @block_source_user @login_required def template_edit(request, pk): """Edit an existing project template""" user = request.user template = get_object_or_404(QIProject, pk=pk, is_template=True) # Check permission if not user.is_px_admin(): if template.hospital and template.hospital != user.hospital: messages.error(request, _("You don't have permission to edit this template.")) return redirect('projects:template_list') if request.method == 'POST': form = QIProjectTemplateForm(request.POST, instance=template, user=user) if form.is_valid(): form.save() messages.success(request, _("Project template updated successfully.")) return redirect('projects:template_list') else: form = QIProjectTemplateForm(instance=template, user=user) context = { 'form': form, 'template': template, 'is_create': False, } return render(request, 'projects/template_form.html', context) @block_source_user @login_required def template_delete(request, pk): """Delete a project template""" user = request.user template = get_object_or_404(QIProject, pk=pk, is_template=True) # Check permission if not user.is_px_admin(): if template.hospital and template.hospital != user.hospital: messages.error(request, _("You don't have permission to delete this template.")) return redirect('projects:template_list') if request.method == 'POST': template_name = template.name template.delete() messages.success(request, _('Template "%(name)s" deleted successfully.') % {'name': template_name}) return redirect('projects:template_list') context = { 'template': template, } return render(request, 'projects/template_delete_confirm.html', context) # ============================================================================= # PX Action Conversion View # ============================================================================= @block_source_user @login_required def convert_action_to_project(request, action_pk): """Convert a PX Action to a QI Project""" user = request.user # Check permission if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager): messages.error(request, _("You don't have permission to create projects.")) return redirect('px_action_center:action_detail', pk=action_pk) action = get_object_or_404(PXAction, pk=action_pk) # Check hospital access if not user.is_px_admin() and user.hospital and action.hospital != user.hospital: messages.error(request, _("You don't have permission to convert this action.")) return redirect('px_action_center:action_detail', pk=action_pk) if request.method == 'POST': form = ConvertToProjectForm(request.POST, user=user, action=action) if form.is_valid(): # Create project from template or blank template = form.cleaned_data.get('template') if template: # Copy from template project = QIProject.objects.create( name=form.cleaned_data['project_name'], name_ar=template.name_ar, description=template.description, hospital=action.hospital, department=template.department, project_lead=form.cleaned_data['project_lead'], target_completion_date=form.cleaned_data['target_completion_date'], status='pending', created_by=user ) # Copy tasks from template for template_task in template.tasks.all(): QIProjectTask.objects.create( project=project, title=template_task.title, description=template_task.description, order=template_task.order, status='pending' ) else: # Create blank project project = QIProject.objects.create( name=form.cleaned_data['project_name'], description=action.description, hospital=action.hospital, project_lead=form.cleaned_data['project_lead'], target_completion_date=form.cleaned_data['target_completion_date'], status='pending', created_by=user ) # Link to the action project.related_actions.add(action) messages.success(request, _("PX Action converted to QI Project successfully.")) return redirect('projects:project_detail', pk=project.pk) else: initial_data = { 'project_name': f"QI Project: {action.title}", } form = ConvertToProjectForm(user=user, action=action, initial=initial_data) context = { 'form': form, 'action': action, } return render(request, 'projects/convert_action.html', context)