2772 lines
102 KiB
Python
2772 lines
102 KiB
Python
"""
|
|
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})
|
|
#
|
|
#
|
|
#
|