""" Quality app views with healthcare-focused CRUD operations. Implements appropriate access patterns for quality management and compliance workflows. """ from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.views.generic import ( ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView ) from django.http import JsonResponse, HttpResponse from django.db.models import Q, Count, Avg, Sum, F, Case, When, IntegerField from django.utils import timezone from django.contrib import messages from django.urls import reverse_lazy, reverse from django.core.paginator import Paginator from django.template.loader import render_to_string from datetime import datetime, timedelta, date import json from core.utils import AuditLogger from .models import ( QualityIndicator, QualityMeasurement, IncidentReport, RiskAssessment, AuditPlan, AuditFinding, ImprovementProject ) from .forms import ( QualityIndicatorForm, QualityMeasurementForm, IncidentReportForm, RiskAssessmentForm, AuditPlanForm, AuditFindingForm, ImprovementProjectForm ) # ============================================================================ # DASHBOARD AND OVERVIEW VIEWS # ============================================================================ class QualityDashboardView(LoginRequiredMixin, TemplateView): """ Main quality dashboard with key metrics and recent activity. """ template_name = 'quality/dashboard.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) tenant = self.request.user.tenant today = timezone.now().date() this_month = timezone.now().replace(day=1).date() # Dashboard statistics context.update({ 'total_indicators': QualityIndicator.objects.filter( tenant=tenant, is_active=True ).count(), 'indicators_meeting_target': QualityIndicator.objects.filter( tenant=tenant, is_active=True, current_value__gte=F('target_value') ).count(), 'indicators_below_target': QualityIndicator.objects.filter( tenant=tenant, is_active=True, current_value__lt=F('target_value') ).count(), 'measurements_this_month': QualityMeasurement.objects.filter( tenant=tenant, measurement_date__gte=this_month ).count(), 'incidents_this_month': IncidentReport.objects.filter( tenant=tenant, incident_date__gte=this_month ).count(), 'incidents_open': IncidentReport.objects.filter( tenant=tenant, status__in=['REPORTED', 'INVESTIGATING', 'PENDING_REVIEW'] ).count(), 'high_risk_incidents': IncidentReport.objects.filter( tenant=tenant, severity__in=['HIGH', 'CRITICAL'], status__in=['REPORTED', 'INVESTIGATING', 'PENDING_REVIEW'] ).count(), 'risk_assessments_pending': RiskAssessment.objects.filter( tenant=tenant, status='PENDING' ).count(), 'audits_active': AuditPlan.objects.filter( tenant=tenant, status='IN_PROGRESS' ).count(), 'audit_findings_open': AuditFinding.objects.filter( tenant=tenant, status__in=['OPEN', 'IN_PROGRESS'] ).count(), 'improvement_projects_active': ImprovementProject.objects.filter( tenant=tenant, status='IN_PROGRESS' ).count(), }) # Recent incidents context['recent_incidents'] = IncidentReport.objects.filter( tenant=tenant ).select_related('reported_by').order_by('-incident_date')[:10] # Recent measurements context['recent_measurements'] = QualityMeasurement.objects.filter( tenant=tenant ).select_related('indicator', 'created_by').order_by('-measurement_date')[:10] # Active improvement projects context['active_projects'] = ImprovementProject.objects.filter( tenant=tenant, status='IN_PROGRESS' ).select_related('project_manager').order_by('-actual_start_date')[:5] # Quality indicators performance context['indicator_performance'] = QualityIndicator.objects.filter( tenant=tenant, is_active=True ).annotate( performance_percentage=Case( When(target_value__gt=0, then=F('current_value') * 100 / F('target_value')), default=0, output_field=IntegerField() ) ).order_by('-performance_percentage')[:10] return context # ============================================================================ # QUALITY INDICATOR VIEWS (FULL CRUD - Master Data) # ============================================================================ class QualityIndicatorListView(LoginRequiredMixin, ListView): """ List all quality indicators with filtering and search. """ model = QualityIndicator template_name = 'quality/indicators/quality_indicator_list.html' context_object_name = 'quality_indicators' paginate_by = 25 def get_queryset(self): queryset = QualityIndicator.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(indicator_name__icontains=search) | Q(description__icontains=search) | Q(category__icontains=search) ) # Filter by category category = self.request.GET.get('category') if category: queryset = queryset.filter(category=category) # Filter by frequency frequency = self.request.GET.get('frequency') if frequency: queryset = queryset.filter(measurement_frequency=frequency) # Filter by active status active_only = self.request.GET.get('active_only') if active_only: queryset = queryset.filter(is_active=True) # Filter by performance performance = self.request.GET.get('performance') if performance == 'meeting_target': queryset = queryset.filter(current_value__gte=F('target_value')) elif performance == 'below_target': queryset = queryset.filter(current_value__lt=F('target_value')) return queryset.order_by('category', 'name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'categories': QualityIndicator._meta.get_field('category').choices, 'frequencies': QualityIndicator._meta.get_field('frequency').choices, 'search_query': self.request.GET.get('search', ''), }) return context class QualityIndicatorDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a quality indicator. """ model = QualityIndicator template_name = 'quality/indicators/quality_indicator_detail.html' context_object_name = 'quality_indicator' def get_queryset(self): return QualityIndicator.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) indicator = self.object # Get recent measurements for this indicator context['recent_measurements'] = indicator.measurements.all().select_related( 'measured_by' ).order_by('-measurement_date')[:20] # Calculate performance statistics measurements = indicator.measurements.all() if measurements.exists(): context['measurement_stats'] = { 'total_measurements': measurements.count(), 'average_value': measurements.aggregate(avg=Avg('value'))['avg'], 'latest_value': measurements.first().value if measurements.first() else None, 'trend_direction': self._calculate_trend(measurements[:5]) if measurements.count() >= 5 else 'stable', } # Performance percentage if indicator.target_value and indicator.target_value > 0: context['performance_percentage'] = ( indicator.current_value / indicator.target_value * 100 ) return context def _calculate_trend(self, recent_measurements): """Calculate trend direction based on recent measurements.""" if len(recent_measurements) < 2: return 'stable' values = [m.value for m in recent_measurements if m.value is not None] if len(values) < 2: return 'stable' # Simple trend calculation first_half_avg = sum(values[:len(values)//2]) / (len(values)//2) second_half_avg = sum(values[len(values)//2:]) / (len(values) - len(values)//2) if second_half_avg > first_half_avg * 1.05: return 'improving' elif second_half_avg < first_half_avg * 0.95: return 'declining' else: return 'stable' class QualityIndicatorCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new quality indicator. """ model = QualityIndicator form_class = QualityIndicatorForm template_name = 'quality/indicators/quality_indicator_form.html' permission_required = 'quality.add_qualityindicator' success_url = reverse_lazy('quality:quality_indicator_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.created_by = self.request.user response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='QUALITY_INDICATOR_CREATED', model='QualityIndicator', object_id=str(self.object.id), details={ 'indicator_name': self.object.indicator_name, 'category': self.object.category, 'target_value': str(self.object.target_value) } ) messages.success(self.request, f'Quality indicator "{self.object.indicator_name}" created successfully.') return response class QualityIndicatorUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update an existing quality indicator. """ model = QualityIndicator form_class = QualityIndicatorForm template_name = 'quality/indicators/quality_indicator_form.html' permission_required = 'quality.change_qualityindicator' def get_queryset(self): return QualityIndicator.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('quality:quality_indicator_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log the action AuditLogger.log_event( user=self.request.user, action='QUALITY_INDICATOR_UPDATED', model='QualityIndicator', object_id=str(self.object.id), details={ 'indicator_name': self.object.indicator_name, 'changes': form.changed_data } ) messages.success(self.request, f'Quality indicator "{self.object.indicator_name}" updated successfully.') return response class QualityIndicatorDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): """ Delete a quality indicator (soft delete by deactivating). """ model = QualityIndicator template_name = 'quality/indicators/quality_indicator_confirm_delete.html' permission_required = 'quality.delete_qualityindicator' success_url = reverse_lazy('quality:quality_indicator_list') def get_queryset(self): return QualityIndicator.objects.filter(tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): self.object = self.get_object() # Check if indicator has measurements measurements_count = self.object.measurements.count() if measurements_count > 0: messages.warning( request, f'Quality indicator "{self.object.indicator_name}" has {measurements_count} measurements. ' f'It will be deactivated instead of deleted.' ) # Soft delete by deactivating self.object.is_active = False self.object.save() # Log the action AuditLogger.log_event( user=request.user, action='QUALITY_INDICATOR_DEACTIVATED', model='QualityIndicator', object_id=str(self.object.id), details={'indicator_name': self.object.indicator_name} ) messages.success(request, f'Quality indicator "{self.object.indicator_name}" deactivated successfully.') return redirect(self.success_url) # ============================================================================ # IMPROVEMENT PROJECT VIEWS (FULL CRUD - Operational Data) # ============================================================================ class ImprovementProjectListView(LoginRequiredMixin, ListView): """ List all improvement projects with filtering and search. """ model = ImprovementProject template_name = 'quality/projects/project_list.html' context_object_name = 'improvement_projects' paginate_by = 25 def get_queryset(self): queryset = ImprovementProject.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(project_name__icontains=search) | Q(description__icontains=search) | Q(methodology__icontains=search) ) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by methodology methodology = self.request.GET.get('methodology') if methodology: queryset = queryset.filter(methodology=methodology) # Filter by project manager manager_id = self.request.GET.get('manager') if manager_id: queryset = queryset.filter(project_manager_id=manager_id) return queryset.select_related('project_manager').order_by('-actual_start_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'statuses': ImprovementProject._meta.get_field('status').choices, 'methodologies': ImprovementProject._meta.get_field('methodology').choices, 'search_query': self.request.GET.get('search', ''), }) return context class ImprovementProjectDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about an improvement project. """ model = ImprovementProject template_name = 'quality/projects/project_detail.html' context_object_name = 'improvement_project' def get_queryset(self): return ImprovementProject.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) project = self.object # Calculate project progress if project.start_date and project.target_completion_date: total_days = (project.target_completion_date - project.start_date).days if project.status == 'COMPLETED' and project.actual_completion_date: elapsed_days = (project.actual_completion_date - project.start_date).days else: elapsed_days = (timezone.now().date() - project.start_date).days context['progress_percentage'] = min(100, max(0, (elapsed_days / total_days * 100))) if total_days > 0 else 0 # Calculate ROI if applicable if project.estimated_cost and project.estimated_savings: context['estimated_roi'] = ( (project.estimated_savings - project.estimated_cost) / project.estimated_cost * 100 ) return context class ImprovementProjectCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new improvement project. """ model = ImprovementProject form_class = ImprovementProjectForm template_name = 'quality/projects/project_form.html' permission_required = 'quality.add_improvementproject' success_url = reverse_lazy('quality:improvement_project_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='IMPROVEMENT_PROJECT_CREATED', model='ImprovementProject', object_id=str(self.object.id), details={ 'project_name': self.object.project_name, 'methodology': self.object.methodology, 'project_manager': f"{self.object.project_manager.first_name} {self.object.project_manager.last_name}" if self.object.project_manager else None } ) messages.success(self.request, f'Improvement project "{self.object.project_name}" created successfully.') return response class ImprovementProjectUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update an existing improvement project. """ model = ImprovementProject form_class = ImprovementProjectForm template_name = 'quality/projects/project_form.html' permission_required = 'quality.change_improvementproject' def get_queryset(self): return ImprovementProject.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('quality:improvement_project_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='IMPROVEMENT_PROJECT_UPDATED', model='ImprovementProject', object_id=str(self.object.id), details={ 'project_name': self.object.project_name, 'changes': form.changed_data } ) messages.success(self.request, f'Improvement project "{self.object.project_name}" updated successfully.') return response class ImprovementProjectDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): """ Delete an improvement project. """ model = ImprovementProject template_name = 'quality/projects/project_confirm_delete.html' permission_required = 'quality.delete_improvementproject' success_url = reverse_lazy('quality:improvement_project_list') def get_queryset(self): return ImprovementProject.objects.filter(tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): self.object = self.get_object() project_name = self.object.project_name # Log the action before deletion AuditLogger.log_event( user=request.user, action='IMPROVEMENT_PROJECT_DELETED', model='ImprovementProject', object_id=str(self.object.id), details={'project_name': project_name} ) response = super().delete(request, *args, **kwargs) messages.success(request, f'Improvement project "{project_name}" deleted successfully.') return response # ============================================================================ # AUDIT PLAN VIEWS (LIMITED CRUD - Operational Data) # ============================================================================ class AuditPlanListView(LoginRequiredMixin, ListView): """ List all audit plans with filtering and search. """ model = AuditPlan template_name = 'quality/audits/audit_list.html' context_object_name = 'audit_plans' paginate_by = 25 def get_queryset(self): queryset = AuditPlan.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(audit_name__icontains=search) | Q(audit_scope__icontains=search) | Q(department__icontains=search) ) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by audit type audit_type = self.request.GET.get('audit_type') if audit_type: queryset = queryset.filter(audit_type=audit_type) # Filter by date range date_from = self.request.GET.get('date_from') date_to = self.request.GET.get('date_to') if date_from: queryset = queryset.filter(planned_start_date__gte=date_from) if date_to: queryset = queryset.filter(planned_end_date__lte=date_to) return queryset.select_related('lead_auditor').order_by('-planned_start_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'statuses': AuditPlan._meta.get_field('status').choices, 'audit_types': AuditPlan._meta.get_field('audit_type').choices, 'search_query': self.request.GET.get('search', ''), }) return context class AuditPlanDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about an audit plan. """ model = AuditPlan template_name = 'quality/audits/audit_detail.html' context_object_name = 'audit_plan' def get_queryset(self): return AuditPlan.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) audit_plan = self.object # Get audit findings for this plan context['audit_findings'] = audit_plan.audit_findings.all().order_by('-finding_date') # Calculate audit progress if audit_plan.planned_start_date and audit_plan.planned_end_date: total_days = (audit_plan.planned_end_date - audit_plan.planned_start_date).days if audit_plan.status == 'COMPLETED' and audit_plan.actual_end_date: elapsed_days = (audit_plan.actual_end_date - audit_plan.planned_start_date).days elif audit_plan.status == 'IN_PROGRESS': elapsed_days = (timezone.now().date() - audit_plan.planned_start_date).days else: elapsed_days = 0 context['progress_percentage'] = min(100, max(0, (elapsed_days / total_days * 100))) if total_days > 0 else 0 return context class AuditPlanCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new audit plan. """ model = AuditPlan form_class = AuditPlanForm template_name = 'quality/audits/audit_form.html' permission_required = 'quality.add_auditplan' success_url = reverse_lazy('quality:audit_plan_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant response = super().form_valid(form) # Log the action AuditLogger.log_event( user=self.request.user, action='AUDIT_PLAN_CREATED', model='AuditPlan', object_id=str(self.object.id), details={ 'audit_name': self.object.audit_name, 'audit_type': self.object.audit_type, 'department': self.object.department } ) messages.success(self.request, f'Audit plan "{self.object.audit_name}" created successfully.') return response class AuditPlanUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update an audit plan (limited to status and notes after start). """ model = AuditPlan template_name = 'quality/audits/audit_form.html' permission_required = 'quality.change_auditplan' def get_queryset(self): return AuditPlan.objects.filter(tenant=self.request.user.tenant) def get_form_class(self): # Limit fields based on audit status if self.object.status in ['IN_PROGRESS', 'COMPLETED']: # Limited fields for audits that have started class RestrictedAuditPlanForm(AuditPlanForm): class Meta(AuditPlanForm.Meta): fields = ['status', 'actual_start_date', 'actual_end_date', 'notes'] return RestrictedAuditPlanForm else: return AuditPlanForm def get_success_url(self): return reverse('quality:audit_plan_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log the action AuditLogger.log_event( user=self.request.user, action='AUDIT_PLAN_UPDATED', model='AuditPlan', object_id=str(self.object.id), details={ 'audit_name': self.object.audit_name, 'changes': form.changed_data } ) messages.success(self.request, f'Audit plan "{self.object.audit_name}" updated successfully.') return response # ============================================================================ # QUALITY MEASUREMENT VIEWS (LIMITED CRUD - Operational Data) # ============================================================================ class QualityMeasurementListView(LoginRequiredMixin, ListView): """ List all quality measurements with filtering and search. """ model = QualityMeasurement template_name = 'quality/measurements/measurement_list.html' context_object_name = 'quality_measurements' paginate_by = 25 def get_queryset(self): queryset = QualityMeasurement.objects.filter(tenant=self.request.user.tenant) # Filter by indicator indicator_id = self.request.GET.get('indicator') if indicator_id: queryset = queryset.filter(indicator_id=indicator_id) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by date range date_from = self.request.GET.get('date_from') date_to = self.request.GET.get('date_to') if date_from: queryset = queryset.filter(measurement_date__gte=date_from) if date_to: queryset = queryset.filter(measurement_date__lte=date_to) return queryset.select_related( 'indicator', 'measured_by', 'verified_by' ).order_by('-measurement_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'indicators': QualityIndicator.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('name'), 'statuses': QualityMeasurement._meta.get_field('status').choices, }) return context class QualityMeasurementDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a quality measurement. """ model = QualityMeasurement template_name = 'quality/measurements/measurement_detail.html' context_object_name = 'quality_measurement' def get_queryset(self): return QualityMeasurement.objects.filter(tenant=self.request.user.tenant) class QualityMeasurementCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new quality measurement. """ model = QualityMeasurement form_class = QualityMeasurementForm template_name = 'quality/measurements/measurement_form.html' permission_required = 'quality.add_qualitymeasurement' success_url = reverse_lazy('quality:quality_measurement_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.measured_by = self.request.user response = super().form_valid(form) # Update indicator current value indicator = self.object.indicator indicator.current_value = self.object.value indicator.last_measured_date = self.object.measurement_date indicator.save() # Log the action AuditLogger.log_event( user=self.request.user, action='QUALITY_MEASUREMENT_CREATED', model='QualityMeasurement', object_id=str(self.object.id), details={ 'indicator_name': self.object.indicator.indicator_name, 'value': str(self.object.value), 'measurement_date': str(self.object.measurement_date) } ) messages.success(self.request, 'Quality measurement recorded successfully.') return response class QualityMeasurementUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update quality measurement (limited to notes and verification). """ model = QualityMeasurement fields = ['notes', 'status'] # Restricted fields template_name = 'quality/measurements/measurement_form.html' permission_required = 'quality.change_qualitymeasurement' def get_queryset(self): return QualityMeasurement.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('quality:quality_measurement_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): # Set verification details if status is being changed to verified if form.cleaned_data.get('status') == 'VERIFIED' and self.object.status != 'VERIFIED': form.instance.verified_by = self.request.user form.instance.verification_date = timezone.now().date() response = super().form_valid(form) # Log the action AuditLogger.log_event( user=self.request.user, action='QUALITY_MEASUREMENT_UPDATED', model='QualityMeasurement', object_id=str(self.object.id), details={ 'indicator_name': self.object.indicator.indicator_name, 'changes': form.changed_data } ) messages.success(self.request, 'Quality measurement updated successfully.') return response # ============================================================================ # RISK ASSESSMENT VIEWS (RESTRICTED CRUD - Clinical/Operational Data) # ============================================================================ class RiskAssessmentListView(LoginRequiredMixin, ListView): """ List all risk assessments with filtering and search. """ model = RiskAssessment template_name = 'quality/risk_assessments/risk_assessment_list.html' context_object_name = 'risk_assessments' paginate_by = 25 def get_queryset(self): queryset = RiskAssessment.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(risk_title__icontains=search) | Q(risk_description__icontains=search) | Q(department__icontains=search) ) # Filter by risk level risk_level = self.request.GET.get('risk_level') if risk_level: queryset = queryset.filter(risk_level=risk_level) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by department department = self.request.GET.get('department') if department: queryset = queryset.filter(department=department) return queryset.select_related('assessed_by', 'approved_by').order_by('-assessment_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'risk_levels': RiskAssessment._meta.get_field('risk_level').choices, 'statuses': RiskAssessment._meta.get_field('status').choices, 'search_query': self.request.GET.get('search', ''), }) return context class RiskAssessmentDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a risk assessment. """ model = RiskAssessment template_name = 'quality/risk_assessments/risk_assessment_detail.html' context_object_name = 'risk_assessment' def get_queryset(self): return RiskAssessment.objects.filter(tenant=self.request.user.tenant) class RiskAssessmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new risk assessment. """ model = RiskAssessment form_class = RiskAssessmentForm template_name = 'quality/risk_assessments/risk_assessment_form.html' permission_required = 'quality.add_riskassessment' success_url = reverse_lazy('quality:risk_assessment_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.assessed_by = self.request.user response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='RISK_ASSESSMENT_CREATED', model='RiskAssessment', object_id=str(self.object.id), details={ 'risk_title': self.object.risk_title, 'risk_level': self.object.risk_level, 'department': self.object.department } ) messages.success(self.request, f'Risk assessment "{self.object.risk_title}" created successfully.') return response class RiskAssessmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update risk assessment (limited after approval). """ model = RiskAssessment template_name = 'quality/risk_assessments/risk_assessment_form.html' permission_required = 'quality.change_riskassessment' def get_queryset(self): return RiskAssessment.objects.filter(tenant=self.request.user.tenant) def get_form_class(self): # Limit fields based on approval status if self.object.status == 'APPROVED': # Very limited fields for approved assessments class RestrictedRiskAssessmentForm(RiskAssessmentForm): class Meta(RiskAssessmentForm.Meta): fields = ['mitigation_actions', 'review_notes'] return RestrictedRiskAssessmentForm else: return RiskAssessmentForm def get_success_url(self): return reverse('quality:risk_assessment_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='RISK_ASSESSMENT_UPDATED', model='RiskAssessment', object_id=str(self.object.id), details={ 'risk_title': self.object.risk_title, 'changes': form.changed_data } ) messages.success(self.request, f'Risk assessment "{self.object.risk_title}" updated successfully.') return response # ============================================================================ # INCIDENT REPORT VIEWS (APPEND-ONLY - Clinical Records) # ============================================================================ class IncidentReportListView(LoginRequiredMixin, ListView): """ List all incident reports with filtering and search. """ model = IncidentReport template_name = 'quality/incident_reports/incident_report_list.html' context_object_name = 'incident_reports' paginate_by = 25 def get_queryset(self): queryset = IncidentReport.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(incident_title__icontains=search) | Q(incident_description__icontains=search) | Q(location__icontains=search) ) # Filter by severity severity = self.request.GET.get('severity') if severity: queryset = queryset.filter(severity=severity) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by incident type incident_type = self.request.GET.get('incident_type') if incident_type: queryset = queryset.filter(incident_type=incident_type) # Filter by date range date_from = self.request.GET.get('date_from') date_to = self.request.GET.get('date_to') if date_from: queryset = queryset.filter(incident_date__gte=date_from) if date_to: queryset = queryset.filter(incident_date__lte=date_to) return queryset.select_related('reported_by').order_by('-incident_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'severities': IncidentReport._meta.get_field('severity').choices, 'statuses': IncidentReport._meta.get_field('status').choices, 'incident_types': IncidentReport._meta.get_field('incident_type').choices, 'search_query': self.request.GET.get('search', ''), }) return context class IncidentReportDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about an incident report. """ model = IncidentReport template_name = 'quality/incident_reports/incident_report_detail.html' context_object_name = 'incident_report' def get_queryset(self): return IncidentReport.objects.filter(tenant=self.request.user.tenant) class IncidentReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new incident report. """ model = IncidentReport form_class = IncidentReportForm template_name = 'quality/incident_reports/incident_report_form.html' permission_required = 'quality.add_incidentreport' success_url = reverse_lazy('quality:incident_report_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.reported_by = self.request.user response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='INCIDENT_REPORT_CREATED', model='IncidentReport', object_id=str(self.object.id), details={ 'incident_title': self.object.incident_title, 'severity': self.object.severity, 'incident_type': self.object.incident_type } ) messages.success(self.request, f'Incident report "{self.object.incident_title}" created successfully.') return response # Note: No UpdateView or DeleteView for IncidentReport - Append-only for patient safety # ============================================================================ # AUDIT FINDING VIEWS (APPEND-ONLY - Audit Records) # ============================================================================ class AuditFindingListView(LoginRequiredMixin, ListView): """ List all audit findings with filtering and search. """ model = AuditFinding template_name = 'quality/findings/finding_list.html' context_object_name = 'audit_findings' paginate_by = 25 def get_queryset(self): queryset = AuditFinding.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(finding_title__icontains=search) | Q(finding_description__icontains=search) | Q(audit_plan__audit_name__icontains=search) ) # Filter by severity severity = self.request.GET.get('severity') if severity: queryset = queryset.filter(severity=severity) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by audit plan audit_plan_id = self.request.GET.get('audit_plan') if audit_plan_id: queryset = queryset.filter(audit_plan_id=audit_plan_id) return queryset.select_related('audit_plan', 'identified_by').order_by('-finding_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'severities': AuditFinding._meta.get_field('severity').choices, 'statuses': AuditFinding._meta.get_field('status').choices, 'audit_plans': AuditPlan.objects.filter( tenant=self.request.user.tenant ).order_by('-planned_start_date'), 'search_query': self.request.GET.get('search', ''), }) return context class AuditFindingDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about an audit finding. """ model = AuditFinding template_name = 'quality/findings/finding_detail.html' context_object_name = 'audit_finding' def get_queryset(self): return AuditFinding.objects.filter(tenant=self.request.user.tenant) class AuditFindingCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new audit finding. """ model = AuditFinding form_class = AuditFindingForm template_name = 'quality/findings/finding_form.html' permission_required = 'quality.add_auditfinding' success_url = reverse_lazy('quality:audit_finding_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.identified_by = self.request.user response = super().form_valid(form) # Log the action AuditLogger.log_event( user=self.request.user, action='AUDIT_FINDING_CREATED', model='AuditFinding', object_id=str(self.object.id), details={ 'finding_title': self.object.finding_title, 'severity': self.object.severity, 'audit_plan': self.object.audit_plan.audit_name } ) messages.success(self.request, f'Audit finding "{self.object.finding_title}" created successfully.') return response # Note: No UpdateView or DeleteView for AuditFinding - Append-only for compliance # ============================================================================ # HTMX VIEWS FOR REAL-TIME UPDATES # ============================================================================ @login_required def quality_stats(request): """ HTMX endpoint for quality dashboard statistics. """ tenant = request.user.tenant today = timezone.now().date() this_month = timezone.now().replace(day=1).date() stats = { 'indicators_meeting_target': QualityIndicator.objects.filter( tenant=tenant, is_active=True, current_value__gte=F('target_value') ).count(), 'incidents_open': IncidentReport.objects.filter( tenant=tenant, status__in=['REPORTED', 'INVESTIGATING', 'PENDING_REVIEW'] ).count(), 'high_risk_incidents': IncidentReport.objects.filter( tenant=tenant, severity__in=['HIGH', 'CRITICAL'], status__in=['REPORTED', 'INVESTIGATING', 'PENDING_REVIEW'] ).count(), 'improvement_projects_active': ImprovementProject.objects.filter( tenant=tenant, status='IN_PROGRESS' ).count(), } return render(request, 'quality/partials/quality_metrics.html', {'stats': stats}) @login_required def incident_search(request): """ HTMX endpoint for incident report search. """ search = request.GET.get('search', '') severity = request.GET.get('severity', '') status = request.GET.get('status', '') queryset = IncidentReport.objects.filter(tenant=request.user.tenant) if search: queryset = queryset.filter( Q(incident_title__icontains=search) | Q(incident_description__icontains=search) | Q(location__icontains=search) ) if severity: queryset = queryset.filter(severity=severity) if status: queryset = queryset.filter(status=status) incidents = queryset.select_related('reported_by').order_by('-incident_date')[:20] return render(request, 'quality/partials/incident_list.html', {'incidents': incidents}) # ============================================================================ # ACTION VIEWS # ============================================================================ @login_required def approve_risk_assessment(request, assessment_id): """ Approve a risk assessment. """ if request.method == 'POST': assessment = get_object_or_404( RiskAssessment, id=assessment_id, tenant=request.user.tenant ) # Only allow approval if assessment is pending if assessment.status != 'PENDING': messages.error(request, 'Only pending risk assessments can be approved.') return redirect('quality:risk_assessment_detail', pk=assessment.pk) assessment.status = 'APPROVED' assessment.approved_by = request.user assessment.approval_date = timezone.now().date() assessment.save() # Log the action AuditLogger.log_event( user=request.user, action='RISK_ASSESSMENT_APPROVED', model='RiskAssessment', object_id=str(assessment.id), details={'risk_title': assessment.risk_title} ) messages.success(request, f'Risk assessment "{assessment.risk_title}" approved successfully.') if request.headers.get('HX-Request'): return render(request, 'quality/partials/risk_assessment_status.html', {'assessment': assessment}) return redirect('quality:risk_assessment_detail', pk=assessment.pk) return JsonResponse({'success': False}) @login_required def verify_measurement(request, measurement_id): """ Verify a quality measurement. """ if request.method == 'POST': measurement = get_object_or_404( QualityMeasurement, id=measurement_id, tenant=request.user.tenant ) # Only allow verification if measurement is pending if measurement.status != 'PENDING': messages.error(request, 'Only pending measurements can be verified.') return redirect('quality:quality_measurement_detail', pk=measurement.pk) measurement.status = 'VERIFIED' measurement.verified_by = request.user measurement.verification_date = timezone.now().date() measurement.save() # Log the action AuditLogger.log_action( user=request.user, action='QUALITY_MEASUREMENT_VERIFIED', model='QualityMeasurement', object_id=str(measurement.id), details={ 'indicator_name': measurement.indicator.indicator_name, 'value': str(measurement.value) } ) messages.success(request, 'Quality measurement verified successfully.') if request.headers.get('HX-Request'): return render(request, 'quality/partials/measurement_status.html', {'measurement': measurement}) return redirect('quality:quality_measurement_detail', pk=measurement.pk) return JsonResponse({'success': False}) @login_required def close_incident(request, incident_id): """ Close an incident report. """ if request.method == 'POST': incident = get_object_or_404( IncidentReport, id=incident_id, tenant=request.user.tenant ) # Only allow closing if incident is not already closed if incident.status == 'CLOSED': messages.error(request, 'Incident is already closed.') return redirect('quality:incident_report_detail', pk=incident.pk) incident.status = 'CLOSED' incident.resolution_date = timezone.now().date() incident.save() # Log the action AuditLogger.log_action( user=request.user, action='INCIDENT_REPORT_CLOSED', model='IncidentReport', object_id=str(incident.id), details={'incident_title': incident.incident_title} ) messages.success(request, f'Incident report "{incident.incident_title}" closed successfully.') if request.headers.get('HX-Request'): return render(request, 'quality/partials/incident_status.html', {'incident': incident}) return redirect('quality:incident_report_detail', pk=incident.pk) return JsonResponse({'success': False}) # # """ # Quality app views with healthcare-focused CRUD operations. # Implements appropriate access patterns for quality management and compliance workflows. # """ # # from django.shortcuts import render, get_object_or_404, redirect # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.views.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView # ) # from django.http import JsonResponse, HttpResponse # from django.db.models import Q, Count, Avg, Sum, F, Case, When, IntegerField # from django.utils import timezone # from django.contrib import messages # from django.urls import reverse_lazy, reverse # from django.core.paginator import Paginator # from django.template.loader import render_to_string # from datetime import datetime, timedelta, date # import json # # from core.utils import AuditLogger # from .models import ( # QualityIndicator, QualityMeasurement, IncidentReport, RiskAssessment, # AuditPlan, AuditFinding, ImprovementProject # ) # from .forms import ( # QualityIndicatorForm, QualityMeasurementForm, IncidentReportForm, # RiskAssessmentForm, AuditPlanForm, AuditFindingForm, ImprovementProjectForm # ) # # # # ============================================================================ # # DASHBOARD AND OVERVIEW VIEWS # # ============================================================================ # # class QualityDashboardView(LoginRequiredMixin, TemplateView): # """ # Main quality dashboard with key metrics and recent activity. # """ # template_name = 'quality/dashboard.html' # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # today = timezone.now().date() # this_month = timezone.now().replace(day=1).date() # # # Dashboard statistics # context.update({ # 'total_indicators': QualityIndicator.objects.filter( # tenant=tenant, # is_active=True # ).count(), # 'indicators_meeting_target': QualityIndicator.objects.filter( # tenant=tenant, # is_active=True, # current_value__gte=F('target_value') # ).count(), # 'indicators_below_target': QualityIndicator.objects.filter( # tenant=tenant, # is_active=True, # current_value__lt=F('target_value') # ).count(), # 'measurements_this_month': QualityMeasurement.objects.filter( # tenant=tenant, # measurement_date__gte=this_month # ).count(), # 'incidents_this_month': IncidentReport.objects.filter( # tenant=tenant, # incident_date__gte=this_month # ).count(), # 'incidents_open': IncidentReport.objects.filter( # tenant=tenant, # status__in=['REPORTED', 'INVESTIGATING', 'PENDING_REVIEW'] # ).count(), # 'high_risk_incidents': IncidentReport.objects.filter( # tenant=tenant, # severity__in=['HIGH', 'CRITICAL'], # status__in=['REPORTED', 'INVESTIGATING', 'PENDING_REVIEW'] # ).count(), # 'risk_assessments_pending': RiskAssessment.objects.filter( # tenant=tenant, # status='PENDING' # ).count(), # 'audits_active': AuditPlan.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'audit_findings_open': AuditFinding.objects.filter( # tenant=tenant, # status__in=['OPEN', 'IN_PROGRESS'] # ).count(), # 'improvement_projects_active': ImprovementProject.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # }) # # # Recent incidents # context['recent_incidents'] = IncidentReport.objects.filter( # tenant=tenant # ).select_related('reported_by').order_by('-incident_date')[:10] # # # Recent measurements # context['recent_measurements'] = QualityMeasurement.objects.filter( # tenant=tenant # ).select_related('indicator', 'measured_by').order_by('-measurement_date')[:10] # # # Active improvement projects # context['active_projects'] = ImprovementProject.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).select_related('project_manager').order_by('-start_date')[:5] # # # Quality indicators performance # context['indicator_performance'] = QualityIndicator.objects.filter( # tenant=tenant, # is_active=True # ).annotate( # performance_percentage=Case( # When(target_value__gt=0, then=F('current_value') * 100 / F('target_value')), # default=0, # output_field=IntegerField() # ) # ).order_by('-performance_percentage')[:10] # # return context # # # # ============================================================================ # # QUALITY INDICATOR VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class QualityIndicatorListView(LoginRequiredMixin, ListView): # """ # List all quality indicators with filtering and search. # """ # model = QualityIndicator # template_name = 'quality/quality_indicator_list.html' # context_object_name = 'quality_indicators' # paginate_by = 25 # # def get_queryset(self): # queryset = QualityIndicator.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(indicator_name__icontains=search) | # Q(description__icontains=search) | # Q(category__icontains=search) # ) # # # Filter by category # category = self.request.GET.get('category') # if category: # queryset = queryset.filter(category=category) # # # Filter by frequency # frequency = self.request.GET.get('frequency') # if frequency: # queryset = queryset.filter(measurement_frequency=frequency) # # # Filter by active status # active_only = self.request.GET.get('active_only') # if active_only: # queryset = queryset.filter(is_active=True) # # # Filter by performance # performance = self.request.GET.get('performance') # if performance == 'meeting_target': # queryset = queryset.filter(current_value__gte=F('target_value')) # elif performance == 'below_target': # queryset = queryset.filter(current_value__lt=F('target_value')) # # return queryset.order_by('category', 'indicator_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'categories': QualityIndicator._meta.get_field('category').choices, # 'frequencies': QualityIndicator._meta.get_field('measurement_frequency').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class QualityIndicatorDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a quality indicator. # """ # model = QualityIndicator # template_name = 'quality/quality_indicator_detail.html' # context_object_name = 'quality_indicator' # # def get_queryset(self): # return QualityIndicator.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # indicator = self.object # # # Get recent measurements for this indicator # context['recent_measurements'] = indicator.measurements.all().select_related( # 'measured_by' # ).order_by('-measurement_date')[:20] # # # Calculate performance statistics # measurements = indicator.measurements.all() # if measurements.exists(): # context['measurement_stats'] = { # 'total_measurements': measurements.count(), # 'average_value': measurements.aggregate(avg=Avg('value'))['avg'], # 'latest_value': measurements.first().value if measurements.first() else None, # 'trend_direction': self._calculate_trend(measurements[:5]) if measurements.count() >= 5 else 'stable', # } # # # Performance percentage # if indicator.target_value and indicator.target_value > 0: # context['performance_percentage'] = ( # indicator.current_value / indicator.target_value * 100 # ) # # return context # # def _calculate_trend(self, recent_measurements): # """Calculate trend direction based on recent measurements.""" # if len(recent_measurements) < 2: # return 'stable' # # values = [m.value for m in recent_measurements if m.value is not None] # if len(values) < 2: # return 'stable' # # # Simple trend calculation # first_half_avg = sum(values[:len(values) // 2]) / (len(values) // 2) # second_half_avg = sum(values[len(values) // 2:]) / (len(values) - len(values) // 2) # # if second_half_avg > first_half_avg * 1.05: # return 'improving' # elif second_half_avg < first_half_avg * 0.95: # return 'declining' # else: # return 'stable' # # # class QualityIndicatorCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new quality indicator. # """ # model = QualityIndicator # form_class = QualityIndicatorForm # template_name = 'quality/quality_indicator_form.html' # permission_required = 'quality.add_qualityindicator' # success_url = reverse_lazy('quality:quality_indicator_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='QUALITY_INDICATOR_CREATED', # model='QualityIndicator', # object_id=str(self.object.id), # details={ # 'indicator_name': self.object.indicator_name, # 'category': self.object.category, # 'target_value': str(self.object.target_value) # } # ) # # messages.success(self.request, f'Quality indicator "{self.object.indicator_name}" created successfully.') # return response # # # class QualityIndicatorUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an existing quality indicator. # """ # model = QualityIndicator # form_class = QualityIndicatorForm # template_name = 'quality/quality_indicator_form.html' # permission_required = 'quality.change_qualityindicator' # # def get_queryset(self): # return QualityIndicator.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('quality:quality_indicator_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='QUALITY_INDICATOR_UPDATED', # model='QualityIndicator', # object_id=str(self.object.id), # details={ # 'indicator_name': self.object.indicator_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, f'Quality indicator "{self.object.indicator_name}" updated successfully.') # return response # # # class QualityIndicatorDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete a quality indicator (soft delete by deactivating). # """ # model = QualityIndicator # template_name = 'quality/quality_indicator_confirm_delete.html' # permission_required = 'quality.delete_qualityindicator' # success_url = reverse_lazy('quality:quality_indicator_list') # # def get_queryset(self): # return QualityIndicator.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Check if indicator has measurements # measurements_count = self.object.measurements.count() # if measurements_count > 0: # messages.warning( # request, # f'Quality indicator "{self.object.indicator_name}" has {measurements_count} measurements. ' # f'It will be deactivated instead of deleted.' # ) # # # Soft delete by deactivating # self.object.is_active = False # self.object.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='QUALITY_INDICATOR_DEACTIVATED', # model='QualityIndicator', # object_id=str(self.object.id), # details={'indicator_name': self.object.indicator_name} # ) # # messages.success(request, f'Quality indicator "{self.object.indicator_name}" deactivated successfully.') # return redirect(self.success_url) # # # # ============================================================================ # # IMPROVEMENT PROJECT VIEWS (FULL CRUD - Operational Data) # # ============================================================================ # # class ImprovementProjectListView(LoginRequiredMixin, ListView): # """ # List all improvement projects with filtering and search. # """ # model = ImprovementProject # template_name = 'quality/improvement_project_list.html' # context_object_name = 'improvement_projects' # paginate_by = 25 # # def get_queryset(self): # queryset = ImprovementProject.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(project_name__icontains=search) | # Q(description__icontains=search) | # Q(methodology__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by methodology # methodology = self.request.GET.get('methodology') # if methodology: # queryset = queryset.filter(methodology=methodology) # # # Filter by project manager # manager_id = self.request.GET.get('manager') # if manager_id: # queryset = queryset.filter(project_manager_id=manager_id) # # return queryset.select_related('project_manager').order_by('-start_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': ImprovementProject._meta.get_field('status').choices, # 'methodologies': ImprovementProject._meta.get_field('methodology').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class ImprovementProjectDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an improvement project. # """ # model = ImprovementProject # template_name = 'quality/improvement_project_detail.html' # context_object_name = 'improvement_project' # # def get_queryset(self): # return ImprovementProject.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # project = self.object # # # Calculate project progress # if project.start_date and project.target_completion_date: # total_days = (project.target_completion_date - project.start_date).days # if project.status == 'COMPLETED' and project.actual_completion_date: # elapsed_days = (project.actual_completion_date - project.start_date).days # else: # elapsed_days = (timezone.now().date() - project.start_date).days # # context['progress_percentage'] = min(100, # max(0, (elapsed_days / total_days * 100))) if total_days > 0 else 0 # # # Calculate ROI if applicable # if project.estimated_cost and project.estimated_savings: # context['estimated_roi'] = ( # (project.estimated_savings - project.estimated_cost) / project.estimated_cost * 100 # ) # # return context # # # class ImprovementProjectCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new improvement project. # """ # model = ImprovementProject # form_class = ImprovementProjectForm # template_name = 'quality/improvement_project_form.html' # permission_required = 'quality.add_improvementproject' # success_url = reverse_lazy('quality:improvement_project_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMPROVEMENT_PROJECT_CREATED', # model='ImprovementProject', # object_id=str(self.object.id), # details={ # 'project_name': self.object.project_name, # 'methodology': self.object.methodology, # 'project_manager': f"{self.object.project_manager.first_name} {self.object.project_manager.last_name}" if self.object.project_manager else None # } # ) # # messages.success(self.request, f'Improvement project "{self.object.project_name}" created successfully.') # return response # # # class ImprovementProjectUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an existing improvement project. # """ # model = ImprovementProject # form_class = ImprovementProjectForm # template_name = 'quality/improvement_project_form.html' # permission_required = 'quality.change_improvementproject' # # def get_queryset(self): # return ImprovementProject.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('quality:improvement_project_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMPROVEMENT_PROJECT_UPDATED', # model='ImprovementProject', # object_id=str(self.object.id), # details={ # 'project_name': self.object.project_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, f'Improvement project "{self.object.project_name}" updated successfully.') # return response # # # class ImprovementProjectDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete an improvement project. # """ # model = ImprovementProject # template_name = 'quality/improvement_project_confirm_delete.html' # permission_required = 'quality.delete_improvementproject' # success_url = reverse_lazy('quality:improvement_project_list') # # def get_queryset(self): # return ImprovementProject.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # project_name = self.object.project_name # # # Log the action before deletion # AuditLogger.log_action( # user=request.user, # action='IMPROVEMENT_PROJECT_DELETED', # model='ImprovementProject', # object_id=str(self.object.id), # details={'project_name': project_name} # ) # # response = super().delete(request, *args, **kwargs) # messages.success(request, f'Improvement project "{project_name}" deleted successfully.') # return response # # # # ============================================================================ # # AUDIT PLAN VIEWS (LIMITED CRUD - Operational Data) # # ============================================================================ # # class AuditPlanListView(LoginRequiredMixin, ListView): # """ # List all audit plans with filtering and search. # """ # model = AuditPlan # template_name = 'quality/audit_plan_list.html' # context_object_name = 'audit_plans' # paginate_by = 25 # # def get_queryset(self): # queryset = AuditPlan.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(audit_name__icontains=search) | # Q(audit_scope__icontains=search) | # Q(department__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by audit type # audit_type = self.request.GET.get('audit_type') # if audit_type: # queryset = queryset.filter(audit_type=audit_type) # # # Filter by date range # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # if date_from: # queryset = queryset.filter(planned_start_date__gte=date_from) # if date_to: # queryset = queryset.filter(planned_end_date__lte=date_to) # # return queryset.select_related('lead_auditor').order_by('-planned_start_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': AuditPlan._meta.get_field('status').choices, # 'audit_types': AuditPlan._meta.get_field('audit_type').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class AuditPlanDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an audit plan. # """ # model = AuditPlan # template_name = 'quality/audit_plan_detail.html' # context_object_name = 'audit_plan' # # def get_queryset(self): # return AuditPlan.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # audit_plan = self.object # # # Get audit findings for this plan # context['audit_findings'] = audit_plan.audit_findings.all().order_by('-finding_date') # # # Calculate audit progress # if audit_plan.planned_start_date and audit_plan.planned_end_date: # total_days = (audit_plan.planned_end_date - audit_plan.planned_start_date).days # if audit_plan.status == 'COMPLETED' and audit_plan.actual_end_date: # elapsed_days = (audit_plan.actual_end_date - audit_plan.planned_start_date).days # elif audit_plan.status == 'IN_PROGRESS': # elapsed_days = (timezone.now().date() - audit_plan.planned_start_date).days # else: # elapsed_days = 0 # # context['progress_percentage'] = min(100, # max(0, (elapsed_days / total_days * 100))) if total_days > 0 else 0 # # return context # # # class AuditPlanCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new audit plan. # """ # model = AuditPlan # form_class = AuditPlanForm # template_name = 'quality/audit_plan_form.html' # permission_required = 'quality.add_auditplan' # success_url = reverse_lazy('quality:audit_plan_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='AUDIT_PLAN_CREATED', # model='AuditPlan', # object_id=str(self.object.id), # details={ # 'audit_name': self.object.audit_name, # 'audit_type': self.object.audit_type, # 'department': self.object.department # } # ) # # messages.success(self.request, f'Audit plan "{self.object.audit_name}" created successfully.') # return response # # # class AuditPlanUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an audit plan (limited to status and notes after start). # """ # model = AuditPlan # template_name = 'quality/audit_plan_update_form.html' # permission_required = 'quality.change_auditplan' # # def get_queryset(self): # return AuditPlan.objects.filter(tenant=self.request.user.tenant) # # def get_form_class(self): # # Limit fields based on audit status # if self.object.status in ['IN_PROGRESS', 'COMPLETED']: # # Limited fields for audits that have started # class RestrictedAuditPlanForm(AuditPlanForm): # class Meta(AuditPlanForm.Meta): # fields = ['status', 'actual_start_date', 'actual_end_date', 'notes'] # # return RestrictedAuditPlanForm # else: # return AuditPlanForm # # def get_success_url(self): # return reverse('quality:audit_plan_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='AUDIT_PLAN_UPDATED', # model='AuditPlan', # object_id=str(self.object.id), # details={ # 'audit_name': self.object.audit_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, f'Audit plan "{self.object.audit_name}" updated successfully.') # return response # # # # ============================================================================ # # QUALITY MEASUREMENT VIEWS (LIMITED CRUD - Operational Data) # # ============================================================================ # # class QualityMeasurementListView(LoginRequiredMixin, ListView): # """ # List all quality measurements with filtering and search. # """ # model = QualityMeasurement # template_name = 'quality/quality_measurement_list.html' # context_object_name = 'quality_measurements' # paginate_by = 25 # # def get_queryset(self): # queryset = QualityMeasurement.objects.filter(tenant=self.request.user.tenant) # # # Filter by indicator # indicator_id = self.request.GET.get('indicator') # if indicator_id: # queryset = queryset.filter(indicator_id=indicator_id) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by date range # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # if date_from: # queryset = queryset.filter(measurement_date__gte=date_from) # if date_to: # queryset = queryset.filter(measurement_date__lte=date_to) # # return queryset.select_related( # 'indicator', 'measured_by', 'verified_by' # ).order_by('-measurement_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'indicators': QualityIndicator.objects.filter( # tenant=self.request.user.tenant, # is_active=True # ).order_by('indicator_name'), # 'statuses': QualityMeasurement._meta.get_field('status').choices, # }) # return context # # # class QualityMeasurementDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a quality measurement. # """ # model = QualityMeasurement # template_name = 'quality/quality_measurement_detail.html' # context_object_name = 'quality_measurement' # # def get_queryset(self): # return QualityMeasurement.objects.filter(tenant=self.request.user.tenant) # # # class QualityMeasurementCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new quality measurement. # """ # model = QualityMeasurement # form_class = QualityMeasurementForm # template_name = 'quality/quality_measurement_form.html' # permission_required = 'quality.add_qualitymeasurement' # success_url = reverse_lazy('quality:quality_measurement_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.measured_by = self.request.user # response = super().form_valid(form) # # # Update indicator current value # indicator = self.object.indicator # indicator.current_value = self.object.value # indicator.last_measured_date = self.object.measurement_date # indicator.save() # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='QUALITY_MEASUREMENT_CREATED', # model='QualityMeasurement', # object_id=str(self.object.id), # details={ # 'indicator_name': self.object.indicator.indicator_name, # 'value': str(self.object.value), # 'measurement_date': str(self.object.measurement_date) # } # ) # # messages.success(self.request, 'Quality measurement recorded successfully.') # return response # # # class QualityMeasurementUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update quality measurement (limited to notes and verification). # """ # model = QualityMeasurement # fields = ['notes', 'status'] # Restricted fields # template_name = 'quality/quality_measurement_update_form.html' # permission_required = 'quality.change_qualitymeasurement' # # def get_queryset(self): # return QualityMeasurement.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('quality:quality_measurement_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # # Set verification details if status is being changed to verified # if form.cleaned_data.get('status') == 'VERIFIED' and self.object.status != 'VERIFIED': # form.instance.verified_by = self.request.user # form.instance.verification_date = timezone.now().date() # # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='QUALITY_MEASUREMENT_UPDATED', # model='QualityMeasurement', # object_id=str(self.object.id), # details={ # 'indicator_name': self.object.indicator.indicator_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Quality measurement updated successfully.') # return response # # # # ============================================================================ # # RISK ASSESSMENT VIEWS (RESTRICTED CRUD - Clinical/Operational Data) # # ============================================================================ # # class RiskAssessmentListView(LoginRequiredMixin, ListView): # """ # List all risk assessments with filtering and search. # """ # model = RiskAssessment # template_name = 'quality/risk_assessment_list.html' # context_object_name = 'risk_assessments' # paginate_by = 25 # # def get_queryset(self): # queryset = RiskAssessment.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(risk_title__icontains=search) | # Q(risk_description__icontains=search) | # Q(department__icontains=search) # ) # # # Filter by risk level # risk_level = self.request.GET.get('risk_level') # if risk_level: # queryset = queryset.filter(risk_level=risk_level) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by department # department = self.request.GET.get('department') # if department: # queryset = queryset.filter(department=department) # # return queryset.select_related('assessed_by', 'approved_by').order_by('-assessment_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'risk_levels': RiskAssessment._meta.get_field('risk_level').choices, # 'statuses': RiskAssessment._meta.get_field('status').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class RiskAssessmentDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a risk assessment. # """ # model = RiskAssessment # template_name = 'quality/risk_assessment_detail.html' # context_object_name = 'risk_assessment' # # def get_queryset(self): # return RiskAssessment.objects.filter(tenant=self.request.user.tenant) # # # class RiskAssessmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new risk assessment. # """ # model = RiskAssessment # form_class = RiskAssessmentForm # template_name = 'quality/risk_assessment_form.html' # permission_required = 'quality.add_riskassessment' # success_url = reverse_lazy('quality:risk_assessment_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.assessed_by = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='RISK_ASSESSMENT_CREATED', # model='RiskAssessment', # object_id=str(self.object.id), # details={ # 'risk_title': self.object.risk_title, # 'risk_level': self.object.risk_level, # 'department': self.object.department # } # ) # # messages.success(self.request, f'Risk assessment "{self.object.risk_title}" created successfully.') # return response # # # class RiskAssessmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update risk assessment (limited after approval). # """ # model = RiskAssessment # template_name = 'quality/risk_assessment_update_form.html' # permission_required = 'quality.change_riskassessment' # # def get_queryset(self): # return RiskAssessment.objects.filter(tenant=self.request.user.tenant) # # def get_form_class(self): # # Limit fields based on approval status # if self.object.status == 'APPROVED': # # Very limited fields for approved assessments # class RestrictedRiskAssessmentForm(RiskAssessmentForm): # class Meta(RiskAssessmentForm.Meta): # fields = ['mitigation_actions', 'review_notes'] # # return RestrictedRiskAssessmentForm # else: # return RiskAssessmentForm # # def get_success_url(self): # return reverse('quality:risk_assessment_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='RISK_ASSESSMENT_UPDATED', # model='RiskAssessment', # object_id=str(self.object.id), # details={ # 'risk_title': self.object.risk_title, # 'changes': form.changed_data # } # ) # # messages.success(self.request, f'Risk assessment "{self.object.risk_title}" updated successfully.') # return response # # # # ============================================================================ # # INCIDENT REPORT VIEWS (APPEND-ONLY - Clinical Records) # # ============================================================================ # # class IncidentReportListView(LoginRequiredMixin, ListView): # """ # List all incident reports with filtering and search. # """ # model = IncidentReport # template_name = 'quality/incident_report_list.html' # context_object_name = 'incident_reports' # paginate_by = 25 # # def get_queryset(self): # queryset = IncidentReport.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(incident_title__icontains=search) | # Q(incident_description__icontains=search) | # Q(location__icontains=search) # ) # # # Filter by severity # severity = self.request.GET.get('severity') # if severity: # queryset = queryset.filter(severity=severity) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by incident type # incident_type = self.request.GET.get('incident_type') # if incident_type: # queryset = queryset.filter(incident_type=incident_type) # # # Filter by date range # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # if date_from: # queryset = queryset.filter(incident_date__gte=date_from) # if date_to: # queryset = queryset.filter(incident_date__lte=date_to) # # return queryset.select_related('reported_by').order_by('-incident_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'severities': IncidentReport._meta.get_field('severity').choices, # 'statuses': IncidentReport._meta.get_field('status').choices, # 'incident_types': IncidentReport._meta.get_field('incident_type').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class IncidentReportDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an incident report. # """ # model = IncidentReport # template_name = 'quality/incident_report_detail.html' # context_object_name = 'incident_report' # # def get_queryset(self): # return IncidentReport.objects.filter(tenant=self.request.user.tenant) # # # class IncidentReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new incident report. # """ # model = IncidentReport # form_class = IncidentReportForm # template_name = 'quality/incident_report_form.html' # permission_required = 'quality.add_incidentreport' # success_url = reverse_lazy('quality:incident_report_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.reported_by = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='INCIDENT_REPORT_CREATED', # model='IncidentReport', # object_id=str(self.object.id), # details={ # 'incident_title': self.object.incident_title, # 'severity': self.object.severity, # 'incident_type': self.object.incident_type # } # ) # # messages.success(self.request, f'Incident report "{self.object.incident_title}" created successfully.') # return response # # # # Note: No UpdateView or DeleteView for IncidentReport - Append-only for patient safety # # # # ============================================================================ # # AUDIT FINDING VIEWS (APPEND-ONLY - Audit Records) # # ============================================================================ # # class AuditFindingListView(LoginRequiredMixin, ListView): # """ # List all audit findings with filtering and search. # """ # model = AuditFinding # template_name = 'quality/audit_finding_list.html' # context_object_name = 'audit_findings' # paginate_by = 25 # # def get_queryset(self): # queryset = AuditFinding.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(finding_title__icontains=search) | # Q(finding_description__icontains=search) | # Q(audit_plan__audit_name__icontains=search) # ) # # # Filter by severity # severity = self.request.GET.get('severity') # if severity: # queryset = queryset.filter(severity=severity) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by audit plan # audit_plan_id = self.request.GET.get('audit_plan') # if audit_plan_id: # queryset = queryset.filter(audit_plan_id=audit_plan_id) # # return queryset.select_related('audit_plan', 'identified_by').order_by('-finding_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'severities': AuditFinding._meta.get_field('severity').choices, # 'statuses': AuditFinding._meta.get_field('status').choices, # 'audit_plans': AuditPlan.objects.filter( # tenant=self.request.user.tenant # ).order_by('-planned_start_date'), # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class AuditFindingDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an audit finding. # """ # model = AuditFinding # template_name = 'quality/audit_finding_detail.html' # context_object_name = 'audit_finding' # # def get_queryset(self): # return AuditFinding.objects.filter(tenant=self.request.user.tenant) # # # class AuditFindingCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new audit finding. # """ # model = AuditFinding # form_class = AuditFindingForm # template_name = 'quality/audit_finding_form.html' # permission_required = 'quality.add_auditfinding' # success_url = reverse_lazy('quality:audit_finding_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.identified_by = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='AUDIT_FINDING_CREATED', # model='AuditFinding', # object_id=str(self.object.id), # details={ # 'finding_title': self.object.finding_title, # 'severity': self.object.severity, # 'audit_plan': self.object.audit_plan.audit_name # } # ) # # messages.success(self.request, f'Audit finding "{self.object.finding_title}" created successfully.') # return response # # # # Note: No UpdateView or DeleteView for AuditFinding - Append-only for compliance # # # # ============================================================================ # # HTMX VIEWS FOR REAL-TIME UPDATES # # ============================================================================ # # @login_required # def quality_stats(request): # """ # HTMX endpoint for quality dashboard statistics. # """ # tenant = request.user.tenant # today = timezone.now().date() # this_month = timezone.now().replace(day=1).date() # # stats = { # 'indicators_meeting_target': QualityIndicator.objects.filter( # tenant=tenant, # is_active=True, # current_value__gte=F('target_value') # ).count(), # 'incidents_open': IncidentReport.objects.filter( # tenant=tenant, # status__in=['REPORTED', 'INVESTIGATING', 'PENDING_REVIEW'] # ).count(), # 'high_risk_incidents': IncidentReport.objects.filter( # tenant=tenant, # severity__in=['HIGH', 'CRITICAL'], # status__in=['REPORTED', 'INVESTIGATING', 'PENDING_REVIEW'] # ).count(), # 'improvement_projects_active': ImprovementProject.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # } # # return render(request, 'quality/partials/quality_metrics.html', {'stats': stats}) # # # @login_required # def incident_search(request): # """ # HTMX endpoint for incident report search. # """ # search = request.GET.get('search', '') # severity = request.GET.get('severity', '') # status = request.GET.get('status', '') # # queryset = IncidentReport.objects.filter(tenant=request.user.tenant) # # if search: # queryset = queryset.filter( # Q(incident_title__icontains=search) | # Q(incident_description__icontains=search) | # Q(location__icontains=search) # ) # # if severity: # queryset = queryset.filter(severity=severity) # # if status: # queryset = queryset.filter(status=status) # # incidents = queryset.select_related('reported_by').order_by('-incident_date')[:20] # # return render(request, 'quality/partials/incident_list.html', {'incidents': incidents}) # # # # ============================================================================ # # ACTION VIEWS # # ============================================================================ # # @login_required # def approve_risk_assessment(request, assessment_id): # """ # Approve a risk assessment. # """ # if request.method == 'POST': # assessment = get_object_or_404( # RiskAssessment, # id=assessment_id, # tenant=request.user.tenant # ) # # # Only allow approval if assessment is pending # if assessment.status != 'PENDING': # messages.error(request, 'Only pending risk assessments can be approved.') # return redirect('quality:risk_assessment_detail', pk=assessment.pk) # # assessment.status = 'APPROVED' # assessment.approved_by = request.user # assessment.approval_date = timezone.now().date() # assessment.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='RISK_ASSESSMENT_APPROVED', # model='RiskAssessment', # object_id=str(assessment.id), # details={'risk_title': assessment.risk_title} # ) # # messages.success(request, f'Risk assessment "{assessment.risk_title}" approved successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'quality/partials/risk_assessment_status.html', {'assessment': assessment}) # # return redirect('quality:risk_assessment_detail', pk=assessment.pk) # # return JsonResponse({'success': False}) # # # @login_required # def verify_measurement(request, measurement_id): # """ # Verify a quality measurement. # """ # if request.method == 'POST': # measurement = get_object_or_404( # QualityMeasurement, # id=measurement_id, # tenant=request.user.tenant # ) # # # Only allow verification if measurement is pending # if measurement.status != 'PENDING': # messages.error(request, 'Only pending measurements can be verified.') # return redirect('quality:quality_measurement_detail', pk=measurement.pk) # # measurement.status = 'VERIFIED' # measurement.verified_by = request.user # measurement.verification_date = timezone.now().date() # measurement.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='QUALITY_MEASUREMENT_VERIFIED', # model='QualityMeasurement', # object_id=str(measurement.id), # details={ # 'indicator_name': measurement.indicator.indicator_name, # 'value': str(measurement.value) # } # ) # # messages.success(request, 'Quality measurement verified successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'quality/partials/measurement_status.html', {'measurement': measurement}) # # return redirect('quality:quality_measurement_detail', pk=measurement.pk) # # return JsonResponse({'success': False}) # # # @login_required # def close_incident(request, incident_id): # """ # Close an incident report. # """ # if request.method == 'POST': # incident = get_object_or_404( # IncidentReport, # id=incident_id, # tenant=request.user.tenant # ) # # # Only allow closing if incident is not already closed # if incident.status == 'CLOSED': # messages.error(request, 'Incident is already closed.') # return redirect('quality:incident_report_detail', pk=incident.pk) # # incident.status = 'CLOSED' # incident.resolution_date = timezone.now().date() # incident.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='INCIDENT_REPORT_CLOSED', # model='IncidentReport', # object_id=str(incident.id), # details={'incident_title': incident.incident_title} # ) # # messages.success(request, f'Incident report "{incident.incident_title}" closed successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'quality/partials/incident_status.html', {'incident': incident}) # # return redirect('quality:incident_report_detail', pk=incident.pk) # # return JsonResponse({'success': False}) # # #