487 lines
15 KiB
Python
487 lines
15 KiB
Python
"""
|
|
RCA (Root Cause Analysis) views
|
|
"""
|
|
from django.contrib import messages
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db.models import Q, Count
|
|
from django.http import JsonResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.urls import reverse_lazy
|
|
from django.utils import timezone
|
|
from django.views import View
|
|
from django.views.generic import (
|
|
CreateView,
|
|
DeleteView,
|
|
DetailView,
|
|
ListView,
|
|
UpdateView,
|
|
)
|
|
|
|
from .forms import (
|
|
RCAAttachmentForm,
|
|
RCAClosureForm,
|
|
RCACorrectiveActionForm,
|
|
RCANoteForm,
|
|
RCARootCauseForm,
|
|
RCAApprovalForm,
|
|
RCAStatusChangeForm,
|
|
RootCauseAnalysisForm,
|
|
)
|
|
from .models import (
|
|
RCAActionStatus,
|
|
RCAStatus,
|
|
RCACorrectiveAction,
|
|
RCARootCause,
|
|
RootCauseAnalysis,
|
|
)
|
|
|
|
|
|
class RCAListView(LoginRequiredMixin, ListView):
|
|
"""List view for Root Cause Analyses"""
|
|
model = RootCauseAnalysis
|
|
template_name = 'rca/rca_list.html'
|
|
context_object_name = 'rcas'
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
queryset = RootCauseAnalysis.objects.filter(
|
|
is_deleted=False
|
|
).select_related(
|
|
'hospital',
|
|
'department',
|
|
'assigned_to',
|
|
'created_by'
|
|
).prefetch_related(
|
|
'root_causes',
|
|
'corrective_actions'
|
|
)
|
|
|
|
# Get filter parameters
|
|
status = self.request.GET.get('status')
|
|
severity = self.request.GET.get('severity')
|
|
priority = self.request.GET.get('priority')
|
|
hospital = self.request.GET.get('hospital')
|
|
search = self.request.GET.get('search')
|
|
date_from = self.request.GET.get('date_from')
|
|
date_to = self.request.GET.get('date_to')
|
|
|
|
# Apply filters
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
if severity:
|
|
queryset = queryset.filter(severity=severity)
|
|
if priority:
|
|
queryset = queryset.filter(priority=priority)
|
|
if hospital:
|
|
queryset = queryset.filter(hospital_id=hospital)
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search) |
|
|
Q(description__icontains=search)
|
|
)
|
|
if date_from:
|
|
queryset = queryset.filter(created_at__gte=date_from)
|
|
if date_to:
|
|
queryset = queryset.filter(created_at__lte=date_to)
|
|
|
|
# Filter by user's hospital (if not admin)
|
|
if not self.request.user.is_superuser:
|
|
from apps.organizations.models import Hospital
|
|
user_hospitals = Hospital.objects.filter(
|
|
staff__user=self.request.user
|
|
)
|
|
queryset = queryset.filter(hospital__in=user_hospitals)
|
|
|
|
return queryset.order_by('-created_at')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
# Add filter form
|
|
from .forms import RCAFilterForm
|
|
context['filter_form'] = RCAFilterForm(self.request.GET)
|
|
|
|
# Add counts
|
|
context['total_count'] = self.get_queryset().count()
|
|
context['draft_count'] = self.get_queryset().filter(
|
|
status=RCAStatus.DRAFT
|
|
).count()
|
|
context['in_progress_count'] = self.get_queryset().filter(
|
|
status=RCAStatus.IN_PROGRESS
|
|
).count()
|
|
context['review_count'] = self.get_queryset().filter(
|
|
status=RCAStatus.REVIEW
|
|
).count()
|
|
context['approved_count'] = self.get_queryset().filter(
|
|
status=RCAStatus.APPROVED
|
|
).count()
|
|
context['closed_count'] = self.get_queryset().filter(
|
|
status=RCAStatus.CLOSED
|
|
).count()
|
|
|
|
return context
|
|
|
|
|
|
class RCADetailView(LoginRequiredMixin, DetailView):
|
|
"""Detail view for Root Cause Analysis"""
|
|
model = RootCauseAnalysis
|
|
template_name = 'rca/rca_detail.html'
|
|
context_object_name = 'rca'
|
|
|
|
def get_queryset(self):
|
|
return RootCauseAnalysis.objects.filter(
|
|
is_deleted=False
|
|
).select_related(
|
|
'hospital',
|
|
'department',
|
|
'assigned_to',
|
|
'created_by',
|
|
'approved_by',
|
|
'closed_by'
|
|
).prefetch_related(
|
|
'root_causes__verified_by',
|
|
'corrective_actions__root_cause',
|
|
'attachments',
|
|
'notes__created_by',
|
|
'status_logs__changed_by'
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
# Add forms
|
|
context['root_cause_form'] = RCARootCauseForm()
|
|
context['corrective_action_form'] = RCACorrectiveActionForm(
|
|
rca=self.object
|
|
)
|
|
context['attachment_form'] = RCAAttachmentForm()
|
|
context['note_form'] = RCANoteForm()
|
|
context['status_change_form'] = RCAStatusChangeForm()
|
|
context['approval_form'] = RCAApprovalForm()
|
|
context['closure_form'] = RCAClosureForm()
|
|
|
|
# Calculate progress
|
|
total_actions = self.object.corrective_actions.count()
|
|
completed_actions = self.object.corrective_actions.filter(
|
|
status=RCAActionStatus.COMPLETED
|
|
).count()
|
|
context['progress_percentage'] = (
|
|
(completed_actions / total_actions * 100)
|
|
if total_actions > 0 else 0
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class RCACreateView(LoginRequiredMixin, CreateView):
|
|
"""Create view for Root Cause Analysis"""
|
|
model = RootCauseAnalysis
|
|
form_class = RootCauseAnalysisForm
|
|
template_name = 'rca/rca_form.html'
|
|
success_url = reverse_lazy('rca:rca_list')
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['user'] = self.request.user
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
rca = form.save(commit=False)
|
|
rca.created_by = self.request.user
|
|
|
|
# Handle linking to related item (if provided)
|
|
related_model = self.request.POST.get('related_model')
|
|
related_id = self.request.POST.get('related_id')
|
|
|
|
if related_model and related_id:
|
|
try:
|
|
content_type = ContentType.objects.get(model=related_model)
|
|
rca.content_type = content_type
|
|
rca.object_id = related_id
|
|
except ContentType.DoesNotExist:
|
|
pass
|
|
|
|
rca.save()
|
|
|
|
# Create status log
|
|
rca.status_logs.create(
|
|
old_status='',
|
|
new_status=rca.status,
|
|
changed_by=self.request.user,
|
|
notes='RCA created'
|
|
)
|
|
|
|
messages.success(
|
|
self.request,
|
|
'Root Cause Analysis created successfully!'
|
|
)
|
|
return redirect('rca:rca_detail', pk=rca.pk)
|
|
|
|
|
|
class RCAUpdateView(LoginRequiredMixin, UpdateView):
|
|
"""Update view for Root Cause Analysis"""
|
|
model = RootCauseAnalysis
|
|
form_class = RootCauseAnalysisForm
|
|
template_name = 'rca/rca_form.html'
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['user'] = self.request.user
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('rca:rca_detail', kwargs={'pk': self.object.pk})
|
|
|
|
def form_valid(self, form):
|
|
rca = form.save()
|
|
messages.success(
|
|
self.request,
|
|
'Root Cause Analysis updated successfully!'
|
|
)
|
|
return redirect(self.get_success_url())
|
|
|
|
|
|
class RCADeleteView(LoginRequiredMixin, View):
|
|
"""Delete (soft delete) view for Root Cause Analysis"""
|
|
|
|
def post(self, request, pk):
|
|
rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False)
|
|
rca.soft_delete(user=request.user)
|
|
messages.success(
|
|
request,
|
|
'Root Cause Analysis deleted successfully!'
|
|
)
|
|
return redirect('rca:rca_list')
|
|
|
|
|
|
class RCAStatusChangeView(LoginRequiredMixin, View):
|
|
"""View to change RCA status"""
|
|
|
|
def post(self, request, pk):
|
|
rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False)
|
|
form = RCAStatusChangeForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
old_status = rca.status
|
|
new_status = form.cleaned_data['new_status']
|
|
notes = form.cleaned_data['notes']
|
|
|
|
rca.status = new_status
|
|
|
|
# Set timestamps based on status
|
|
if new_status == RCAStatus.IN_PROGRESS:
|
|
rca.assigned_to = request.user
|
|
rca.assigned_at = timezone.now()
|
|
elif new_status == RCAStatus.APPROVED:
|
|
rca.approved_by = request.user
|
|
rca.approved_at = timezone.now()
|
|
|
|
rca.save()
|
|
|
|
# Create status log
|
|
rca.status_logs.create(
|
|
old_status=old_status,
|
|
new_status=new_status,
|
|
changed_by=request.user,
|
|
notes=notes
|
|
)
|
|
|
|
messages.success(
|
|
request,
|
|
f'Status changed from {old_status} to {new_status}'
|
|
)
|
|
else:
|
|
messages.error(request, 'Invalid status change')
|
|
|
|
return redirect('rca:rca_detail', pk=rca.pk)
|
|
|
|
|
|
class RCAApprovalView(LoginRequiredMixin, View):
|
|
"""View to approve RCA"""
|
|
|
|
def post(self, request, pk):
|
|
rca = get_object_or_404(
|
|
RootCauseAnalysis,
|
|
pk=pk,
|
|
is_deleted=False,
|
|
status=RCAStatus.REVIEW
|
|
)
|
|
form = RCAApprovalForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
rca.status = RCAStatus.APPROVED
|
|
rca.approved_by = request.user
|
|
rca.approved_at = timezone.now()
|
|
rca.approval_notes = form.cleaned_data['approval_notes']
|
|
rca.save()
|
|
|
|
# Create status log
|
|
rca.status_logs.create(
|
|
old_status=RCAStatus.REVIEW,
|
|
new_status=RCAStatus.APPROVED,
|
|
changed_by=request.user,
|
|
notes=rca.approval_notes
|
|
)
|
|
|
|
messages.success(request, 'RCA approved successfully!')
|
|
else:
|
|
messages.error(request, 'Invalid approval data')
|
|
|
|
return redirect('rca:rca_detail', pk=rca.pk)
|
|
|
|
|
|
class RCAClosureView(LoginRequiredMixin, View):
|
|
"""View to close RCA"""
|
|
|
|
def post(self, request, pk):
|
|
rca = get_object_or_404(
|
|
RootCauseAnalysis,
|
|
pk=pk,
|
|
is_deleted=False,
|
|
status__in=[RCAStatus.APPROVED, RCAStatus.IN_PROGRESS]
|
|
)
|
|
form = RCAClosureForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
rca.status = RCAStatus.CLOSED
|
|
rca.closed_by = request.user
|
|
rca.closed_at = timezone.now()
|
|
rca.closure_notes = form.cleaned_data['closure_notes']
|
|
rca.actual_completion_date = form.cleaned_data[
|
|
'actual_completion_date'
|
|
]
|
|
rca.save()
|
|
|
|
# Create status log
|
|
rca.status_logs.create(
|
|
old_status=RCAStatus.APPROVED,
|
|
new_status=RCAStatus.CLOSED,
|
|
changed_by=request.user,
|
|
notes=rca.closure_notes
|
|
)
|
|
|
|
messages.success(request, 'RCA closed successfully!')
|
|
else:
|
|
messages.error(request, 'Invalid closure data')
|
|
|
|
return redirect('rca:rca_detail', pk=rca.pk)
|
|
|
|
|
|
class RCARootCauseCreateView(LoginRequiredMixin, View):
|
|
"""View to add a root cause to RCA"""
|
|
|
|
def post(self, request, pk):
|
|
rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False)
|
|
form = RCARootCauseForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
root_cause = form.save(commit=False)
|
|
root_cause.rca = rca
|
|
root_cause.save()
|
|
messages.success(
|
|
request,
|
|
'Root cause added successfully!'
|
|
)
|
|
else:
|
|
messages.error(request, 'Invalid root cause data')
|
|
|
|
return redirect('rca:rca_detail', pk=rca.pk)
|
|
|
|
|
|
class RCARootCauseDeleteView(LoginRequiredMixin, View):
|
|
"""View to delete a root cause"""
|
|
|
|
def post(self, request, pk, root_cause_pk):
|
|
rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False)
|
|
root_cause = get_object_or_404(
|
|
RCARootCause,
|
|
pk=root_cause_pk,
|
|
rca=rca
|
|
)
|
|
root_cause.delete()
|
|
messages.success(request, 'Root cause deleted successfully!')
|
|
return redirect('rca:rca_detail', pk=rca.pk)
|
|
|
|
|
|
class RCACorrectiveActionCreateView(LoginRequiredMixin, View):
|
|
"""View to add a corrective action to RCA"""
|
|
|
|
def post(self, request, pk):
|
|
rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False)
|
|
form = RCACorrectiveActionForm(request.POST, rca=rca)
|
|
|
|
if form.is_valid():
|
|
action = form.save(commit=False)
|
|
action.rca = rca
|
|
action.save()
|
|
messages.success(
|
|
request,
|
|
'Corrective action added successfully!'
|
|
)
|
|
else:
|
|
messages.error(request, 'Invalid corrective action data')
|
|
|
|
return redirect('rca:rca_detail', pk=rca.pk)
|
|
|
|
|
|
class RCACorrectiveActionDeleteView(LoginRequiredMixin, View):
|
|
"""View to delete a corrective action"""
|
|
|
|
def post(self, request, pk, action_pk):
|
|
rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False)
|
|
action = get_object_or_404(
|
|
RCACorrectiveAction,
|
|
pk=action_pk,
|
|
rca=rca
|
|
)
|
|
action.delete()
|
|
messages.success(
|
|
request,
|
|
'Corrective action deleted successfully!'
|
|
)
|
|
return redirect('rca:rca_detail', pk=rca.pk)
|
|
|
|
|
|
class RCAAttachmentCreateView(LoginRequiredMixin, View):
|
|
"""View to add an attachment to RCA"""
|
|
|
|
def post(self, request, pk):
|
|
rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False)
|
|
form = RCAAttachmentForm(request.POST, request.FILES)
|
|
|
|
if form.is_valid():
|
|
attachment = form.save(commit=False)
|
|
attachment.rca = rca
|
|
attachment.uploaded_by = request.user
|
|
attachment.filename = request.FILES['file'].name
|
|
attachment.file_type = request.FILES['file'].content_type
|
|
attachment.file_size = request.FILES['file'].size
|
|
attachment.save()
|
|
messages.success(
|
|
request,
|
|
'Attachment added successfully!'
|
|
)
|
|
else:
|
|
messages.error(request, 'Invalid attachment data')
|
|
|
|
return redirect('rca:rca_detail', pk=rca.pk)
|
|
|
|
|
|
class RCANoteCreateView(LoginRequiredMixin, View):
|
|
"""View to add a note to RCA"""
|
|
|
|
def post(self, request, pk):
|
|
rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False)
|
|
form = RCANoteForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
note = form.save(commit=False)
|
|
note.rca = rca
|
|
note.created_by = request.user
|
|
note.save()
|
|
messages.success(request, 'Note added successfully!')
|
|
else:
|
|
messages.error(request, 'Invalid note data')
|
|
|
|
return redirect('rca:rca_detail', pk=rca.pk) |