1234 lines
43 KiB
Python
1234 lines
43 KiB
Python
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||
from django.shortcuts import render, get_object_or_404, redirect
|
||
from django.contrib.auth.decorators import login_required, permission_required
|
||
from django.contrib import messages
|
||
from django.http import JsonResponse, HttpResponse
|
||
from django.core.paginator import Paginator
|
||
from django.db.models import Q, Count, Sum, Avg
|
||
from django.utils import timezone
|
||
from django.views.decorators.http import require_http_methods
|
||
from django.contrib.auth.models import User
|
||
from datetime import timedelta
|
||
import json
|
||
|
||
from django.views.generic import ListView, CreateView, DetailView, DeleteView, UpdateView
|
||
|
||
from .models import (
|
||
BloodGroup, Donor, BloodComponent, BloodUnit, BloodTest, CrossMatch,
|
||
BloodRequest, BloodIssue, Transfusion, AdverseReaction, InventoryLocation,
|
||
QualityControl
|
||
)
|
||
from .forms import (
|
||
DonorForm, BloodUnitForm, BloodTestForm, CrossMatchForm, BloodRequestForm,
|
||
BloodIssueForm, TransfusionForm, AdverseReactionForm, InventoryLocationForm,
|
||
QualityControlForm, DonorEligibilityForm, BloodInventorySearchForm
|
||
)
|
||
|
||
|
||
@login_required
|
||
def dashboard(request):
|
||
"""Blood bank dashboard with key metrics"""
|
||
context = {
|
||
'total_donors': Donor.objects.filter(status='active').count(),
|
||
'total_units': BloodUnit.objects.filter(status='available').count(),
|
||
'pending_requests': BloodRequest.objects.filter(status='pending').count(),
|
||
'expiring_soon': BloodUnit.objects.filter(
|
||
expiry_date__lte=timezone.now() + timedelta(days=7),
|
||
status='available'
|
||
).count(),
|
||
'recent_donations': BloodUnit.objects.filter(
|
||
collection_date__gte=timezone.now() - timedelta(days=7)
|
||
).count(),
|
||
'active_transfusions': Transfusion.objects.filter(
|
||
status__in=['started', 'in_progress']
|
||
).count(),
|
||
}
|
||
|
||
# Blood group distribution
|
||
blood_group_stats = BloodUnit.objects.filter(status='available').values(
|
||
'blood_group__abo_type', 'blood_group__rh_factor'
|
||
).annotate(count=Count('id'))
|
||
|
||
context['blood_group_stats'] = blood_group_stats
|
||
|
||
# Recent activities
|
||
context['recent_units'] = BloodUnit.objects.select_related('donor', 'component', 'blood_group').order_by('-collection_date')
|
||
|
||
context['urgent_requests'] = BloodRequest.objects.filter(
|
||
urgency='emergency', status__in=['pending', 'processing']
|
||
).select_related('patient', 'component_requested')[:5]
|
||
|
||
return render(request, 'blood_bank/dashboard.html', context)
|
||
|
||
|
||
# Donor Management Views
|
||
class DonorListView(LoginRequiredMixin, ListView):
|
||
model = Donor
|
||
template_name = 'blood_bank/donors/donor_list.html'
|
||
context_object_name = 'page_obj'
|
||
paginate_by = 25
|
||
|
||
def get_queryset(self):
|
||
queryset = Donor.objects.select_related('blood_group').order_by('-registration_date')
|
||
|
||
# Search functionality
|
||
search_query = self.request.GET.get('search')
|
||
if search_query:
|
||
queryset = queryset.filter(
|
||
Q(donor_id__icontains=search_query) |
|
||
Q(first_name__icontains=search_query) |
|
||
Q(last_name__icontains=search_query) |
|
||
Q(phone__icontains=search_query)
|
||
)
|
||
|
||
# Filter by status
|
||
status_filter = self.request.GET.get('status')
|
||
if status_filter:
|
||
queryset = queryset.filter(status=status_filter)
|
||
|
||
# Filter by blood group
|
||
blood_group_filter = self.request.GET.get('blood_group')
|
||
if blood_group_filter:
|
||
queryset = queryset.filter(blood_group_id=blood_group_filter)
|
||
|
||
return queryset
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
|
||
context.update({
|
||
'blood_groups': BloodGroup.objects.all(),
|
||
'status_choices': Donor.DonorStatus.choices,
|
||
'search_query': self.request.GET.get('search'),
|
||
'status_filter': self.request.GET.get('status'),
|
||
'blood_group_filter': self.request.GET.get('blood_group'),
|
||
})
|
||
|
||
return context
|
||
|
||
|
||
|
||
class DonorDetailView(LoginRequiredMixin, DetailView):
|
||
model = Donor
|
||
template_name = 'blood_bank/donors/donor_detail.html'
|
||
context_object_name = 'donor'
|
||
pk_url_kwarg = 'donor_id'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
ctx = super().get_context_data(**kwargs)
|
||
donor = self.object
|
||
blood_units = (
|
||
BloodUnit.objects
|
||
.filter(donor=donor)
|
||
.select_related('component', 'blood_group')
|
||
.order_by('-collection_date')
|
||
)
|
||
ctx.update({
|
||
'blood_units': blood_units,
|
||
'total_donations': blood_units.count(),
|
||
'last_donation': blood_units.first(),
|
||
})
|
||
return ctx
|
||
|
||
|
||
class DonorCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
model = Donor
|
||
form_class = DonorForm
|
||
template_name = 'blood_bank/donors/donor_form.html'
|
||
permission_required = 'blood_bank.add_donor'
|
||
|
||
def form_valid(self, form):
|
||
donor = form.save(commit=False)
|
||
donor.created_by = self.request.user
|
||
donor.save()
|
||
messages.success(self.request, f'Donor {donor.donor_id} created successfully.')
|
||
return redirect('blood_bank:donor_detail', donor_id=donor.id)
|
||
|
||
def get_context_data(self, **kwargs):
|
||
ctx = super().get_context_data(**kwargs)
|
||
ctx['title'] = 'Add New Donor'
|
||
return ctx
|
||
|
||
|
||
class DonorUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
model = Donor
|
||
form_class = DonorForm
|
||
template_name = 'blood_bank/donors/donor_form.html'
|
||
permission_required = 'blood_bank.change_donor'
|
||
pk_url_kwarg = 'donor_id'
|
||
context_object_name = 'donor'
|
||
|
||
def form_valid(self, form):
|
||
donor = form.save()
|
||
messages.success(self.request, f'Donor {donor.donor_id} updated successfully.')
|
||
return redirect('blood_bank:donor_detail', donor_id=donor.id)
|
||
|
||
def get_context_data(self, **kwargs):
|
||
ctx = super().get_context_data(**kwargs)
|
||
ctx['title'] = 'Update Donor'
|
||
return ctx
|
||
|
||
|
||
@login_required
|
||
def donor_eligibility_check(request, donor_id):
|
||
"""Check donor eligibility for donation"""
|
||
donor = get_object_or_404(Donor, id=donor_id)
|
||
|
||
if request.method == 'POST':
|
||
form = DonorEligibilityForm(request.POST)
|
||
if form.is_valid():
|
||
# Process eligibility check
|
||
messages.success(request, f'Donor {donor.donor_id} is eligible for donation.')
|
||
return redirect('blood_bank:blood_unit_create', donor_id=donor.id)
|
||
else:
|
||
form = DonorEligibilityForm()
|
||
|
||
context = {
|
||
'donor': donor,
|
||
'form': form,
|
||
'is_eligible': donor.is_eligible_for_donation,
|
||
'next_eligible_date': donor.next_eligible_date,
|
||
}
|
||
|
||
return render(request, 'blood_bank/donors/donor_eligibility.html', context)
|
||
|
||
|
||
class BloodUnitListView(LoginRequiredMixin, ListView):
|
||
model = BloodUnit
|
||
template_name = 'blood_bank/units/blood_unit_list.html'
|
||
context_object_name = 'blood_units' # you'll still get page_obj automatically
|
||
paginate_by = 25
|
||
|
||
def get_queryset(self):
|
||
# base queryset
|
||
qs = BloodUnit.objects.select_related('donor', 'component', 'blood_group') \
|
||
.order_by('-collection_date')
|
||
|
||
# bind/validate the filter form
|
||
self.form = BloodInventorySearchForm(self.request.GET)
|
||
if self.form.is_valid():
|
||
cd = self.form.cleaned_data
|
||
|
||
if cd.get('blood_group'):
|
||
qs = qs.filter(blood_group=cd['blood_group'])
|
||
|
||
if cd.get('component'):
|
||
qs = qs.filter(component=cd['component'])
|
||
|
||
if cd.get('status'):
|
||
qs = qs.filter(status=cd['status'])
|
||
|
||
if cd.get('expiry_days') is not None:
|
||
expiry_date = timezone.now() + timedelta(days=cd['expiry_days'])
|
||
qs = qs.filter(expiry_date__lte=expiry_date)
|
||
|
||
return qs
|
||
|
||
def get_context_data(self, **kwargs):
|
||
ctx = super().get_context_data(**kwargs)
|
||
# expose the (bound) form to the template
|
||
ctx['form'] = getattr(self, 'form', BloodInventorySearchForm(self.request.GET))
|
||
return ctx
|
||
|
||
|
||
class BloodUnitDetailView(LoginRequiredMixin, DetailView):
|
||
model = BloodUnit
|
||
template_name = 'blood_bank/units/blood_unit_detail.html'
|
||
context_object_name = 'blood_unit'
|
||
pk_url_kwarg = 'unit_id'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
ctx = super().get_context_data(**kwargs)
|
||
blood_unit = self.object
|
||
|
||
# related objects
|
||
ctx['tests'] = BloodTest.objects.filter(blood_unit=blood_unit) \
|
||
.select_related('tested_by')
|
||
ctx['crossmatches'] = CrossMatch.objects.filter(blood_unit=blood_unit) \
|
||
.select_related('recipient', 'tested_by')
|
||
return ctx
|
||
|
||
|
||
class BloodUnitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
model = BloodUnit
|
||
form_class = BloodUnitForm
|
||
template_name = 'blood_bank/units/blood_unit_form.html'
|
||
permission_required = 'blood_bank.add_bloodunit'
|
||
|
||
def get_initial(self):
|
||
initial = super().get_initial()
|
||
donor_id = self.kwargs.get('donor_id')
|
||
if donor_id:
|
||
donor = get_object_or_404(Donor, id=donor_id)
|
||
initial['donor'] = donor
|
||
initial['blood_group'] = donor.blood_group
|
||
self.donor = donor # store for use in context
|
||
return initial
|
||
|
||
def form_valid(self, form):
|
||
blood_unit = form.save(commit=False)
|
||
blood_unit.collected_by = self.request.user
|
||
blood_unit.save()
|
||
|
||
# Update donor’s donation stats
|
||
if blood_unit.donor:
|
||
blood_unit.donor.last_donation_date = blood_unit.collection_date
|
||
blood_unit.donor.total_donations += 1
|
||
blood_unit.donor.save()
|
||
|
||
messages.success(self.request, f'Blood unit {blood_unit.unit_number} created successfully.')
|
||
return redirect('blood_bank:blood_unit_detail', unit_id=blood_unit.id)
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['donor'] = getattr(self, 'donor', None)
|
||
context['title'] = 'Register Blood Unit'
|
||
return context
|
||
|
||
|
||
class BloodRequestListView(LoginRequiredMixin, ListView):
|
||
model = BloodRequest
|
||
template_name = 'blood_bank/requests/blood_request_list.html'
|
||
context_object_name = 'page_obj'
|
||
paginate_by = 25
|
||
|
||
def get_queryset(self):
|
||
qs = BloodRequest.objects.select_related(
|
||
'patient', 'requesting_department', 'requesting_physician', 'component_requested'
|
||
).order_by('-request_date')
|
||
|
||
status_filter = self.request.GET.get('status')
|
||
urgency_filter = self.request.GET.get('urgency')
|
||
|
||
if status_filter:
|
||
qs = qs.filter(status=status_filter)
|
||
if urgency_filter:
|
||
qs = qs.filter(urgency=urgency_filter)
|
||
|
||
return qs
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['status_choices'] = BloodRequest.STATUS_CHOICES
|
||
context['urgency_choices'] = BloodRequest.URGENCY_CHOICES
|
||
context['status_filter'] = self.request.GET.get('status')
|
||
context['urgency_filter'] = self.request.GET.get('urgency')
|
||
return context
|
||
|
||
|
||
class BloodRequestDetailView(LoginRequiredMixin, DetailView):
|
||
model = BloodRequest
|
||
pk_url_kwarg = 'request_id'
|
||
template_name = 'blood_bank/requests/blood_request_detail.html'
|
||
context_object_name = 'blood_request'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['issues'] = BloodIssue.objects.filter(
|
||
blood_request=self.object
|
||
).select_related('blood_unit', 'issued_by', 'issued_to')
|
||
return context
|
||
|
||
|
||
class BloodRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
model = BloodRequest
|
||
form_class = BloodRequestForm
|
||
template_name = 'blood_bank/requests/blood_request_form.html'
|
||
permission_required = 'blood_bank.add_bloodrequest'
|
||
|
||
def form_valid(self, form):
|
||
blood_request = form.save(commit=False)
|
||
blood_request.requesting_physician = self.request.user
|
||
# Generate request number
|
||
blood_request.request_number = f"BR{timezone.now().strftime('%Y%m%d')}{BloodRequest.objects.count() + 1:04d}"
|
||
blood_request.save()
|
||
|
||
messages.success(self.request, f'Blood request {blood_request.request_number} created successfully.')
|
||
return redirect('blood_bank:blood_request_detail', request_id=blood_request.id)
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['title'] = 'Create Blood Request'
|
||
return context
|
||
|
||
|
||
# Blood Issue and Transfusion Views
|
||
@login_required
|
||
@permission_required('blood_bank.add_bloodissue')
|
||
def blood_issue_create(request, request_id):
|
||
"""Issue blood unit for a request"""
|
||
blood_request = get_object_or_404(BloodRequest, id=request_id)
|
||
|
||
if request.method == 'POST':
|
||
form = BloodIssueForm(request.POST)
|
||
if form.is_valid():
|
||
blood_issue = form.save(commit=False)
|
||
blood_issue.issued_by = request.user
|
||
blood_issue.save()
|
||
|
||
# Update blood unit status
|
||
blood_issue.blood_unit.status = 'issued'
|
||
blood_issue.blood_unit.save()
|
||
|
||
# Update request status
|
||
blood_request.status = 'issued'
|
||
blood_request.processed_by = request.user
|
||
blood_request.processed_at = timezone.now()
|
||
blood_request.save()
|
||
|
||
messages.success(request, f'Blood unit {blood_issue.blood_unit.unit_number} issued successfully.')
|
||
return redirect('blood_bank:blood_request_detail', request_id=blood_request.id)
|
||
else:
|
||
# Filter compatible blood units
|
||
compatible_units = BloodUnit.objects.filter(
|
||
blood_group=blood_request.patient_blood_group,
|
||
component=blood_request.component_requested,
|
||
status='available'
|
||
)
|
||
|
||
form = BloodIssueForm(initial={
|
||
'blood_request': blood_request,
|
||
'expiry_time': timezone.now() + timedelta(hours=4)
|
||
})
|
||
form.fields['blood_unit'].queryset = compatible_units
|
||
|
||
context = {
|
||
'form': form,
|
||
'blood_request': blood_request,
|
||
'title': 'Issue Blood Unit'
|
||
}
|
||
|
||
return render(request, 'blood_bank/issues/blood_issue_form.html', context)
|
||
|
||
|
||
@login_required
|
||
def transfusion_list(request):
|
||
"""List all transfusions"""
|
||
transfusions = Transfusion.objects.select_related(
|
||
'blood_issue__blood_unit', 'blood_issue__blood_request__patient',
|
||
'administered_by'
|
||
).order_by('-start_time')
|
||
|
||
paginator = Paginator(transfusions, 25)
|
||
page_number = request.GET.get('page')
|
||
page_obj = paginator.get_page(page_number)
|
||
|
||
return render(request, 'blood_bank/transfusions/transfusion_list.html', {'page_obj': page_obj})
|
||
|
||
|
||
@login_required
|
||
def transfusion_detail(request, transfusion_id):
|
||
"""Transfusion detail view"""
|
||
transfusion = get_object_or_404(Transfusion, id=transfusion_id)
|
||
|
||
# Get adverse reactions
|
||
reactions = AdverseReaction.objects.filter(transfusion=transfusion)
|
||
|
||
context = {
|
||
'transfusion': transfusion,
|
||
'reactions': reactions,
|
||
}
|
||
|
||
return render(request, 'blood_bank/transfusions/transfusion_detail.html', context)
|
||
|
||
|
||
@login_required
|
||
@permission_required('blood_bank.add_transfusion')
|
||
def transfusion_create(request, issue_id):
|
||
"""Start transfusion"""
|
||
blood_issue = get_object_or_404(BloodIssue, id=issue_id)
|
||
|
||
if request.method == 'POST':
|
||
form = TransfusionForm(request.POST)
|
||
if form.is_valid():
|
||
transfusion = form.save(commit=False)
|
||
transfusion.administered_by = request.user
|
||
transfusion.save()
|
||
|
||
# Update blood unit status
|
||
blood_issue.blood_unit.status = 'transfused'
|
||
blood_issue.blood_unit.save()
|
||
|
||
messages.success(request, 'Transfusion started successfully.')
|
||
return redirect('blood_bank:transfusion_detail', transfusion_id=transfusion.id)
|
||
else:
|
||
form = TransfusionForm(initial={
|
||
'blood_issue': blood_issue,
|
||
'start_time': timezone.now()
|
||
})
|
||
|
||
context = {
|
||
'form': form,
|
||
'blood_issue': blood_issue,
|
||
'title': 'Start Transfusion'
|
||
}
|
||
|
||
return render(request, 'blood_bank/transfusions/transfusion_form.html', context)
|
||
|
||
|
||
# Testing Views
|
||
@login_required
|
||
@permission_required('blood_bank.add_bloodtest')
|
||
def blood_test_create(request, unit_id):
|
||
"""Add test result for blood unit"""
|
||
blood_unit = get_object_or_404(BloodUnit, id=unit_id)
|
||
|
||
if request.method == 'POST':
|
||
form = BloodTestForm(request.POST)
|
||
if form.is_valid():
|
||
test = form.save(commit=False)
|
||
test.tested_by = request.user
|
||
test.save()
|
||
|
||
# Update blood unit status based on test results
|
||
if test.result == 'positive' and test.test_type in ['hiv', 'hbv', 'hcv', 'syphilis']:
|
||
blood_unit.status = 'discarded'
|
||
blood_unit.save()
|
||
|
||
messages.success(request, f'Test result for {test.get_test_type_display()} added successfully.')
|
||
return redirect('blood_bank:blood_unit_detail', unit_id=blood_unit.id)
|
||
else:
|
||
form = BloodTestForm(initial={
|
||
'blood_unit': blood_unit,
|
||
'test_date': timezone.now()
|
||
})
|
||
|
||
context = {
|
||
'form': form,
|
||
'blood_unit': blood_unit,
|
||
'title': 'Add Test Result'
|
||
}
|
||
|
||
return render(request, 'blood_bank/tests/blood_test_form.html', context)
|
||
|
||
|
||
@login_required
|
||
@permission_required('blood_bank.add_crossmatch')
|
||
def crossmatch_create(request, unit_id, patient_id):
|
||
"""Create crossmatch test"""
|
||
blood_unit = get_object_or_404(BloodUnit, id=unit_id)
|
||
|
||
if request.method == 'POST':
|
||
form = CrossMatchForm(request.POST)
|
||
if form.is_valid():
|
||
crossmatch = form.save(commit=False)
|
||
crossmatch.tested_by = request.user
|
||
crossmatch.save()
|
||
|
||
messages.success(request, 'Crossmatch test created successfully.')
|
||
return redirect('blood_bank:blood_unit_detail', unit_id=blood_unit.id)
|
||
else:
|
||
form = CrossMatchForm(initial={
|
||
'blood_unit': blood_unit,
|
||
'test_date': timezone.now()
|
||
})
|
||
|
||
context = {
|
||
'form': form,
|
||
'blood_unit': blood_unit,
|
||
'title': 'Crossmatch Test'
|
||
}
|
||
|
||
return render(request, 'blood_bank/crossmatch/crossmatch_form.html', context)
|
||
|
||
|
||
# Inventory Management Views
|
||
@login_required
|
||
def inventory_overview(request):
|
||
"""Blood inventory overview"""
|
||
# Get inventory by blood group and component
|
||
inventory_data = BloodUnit.objects.filter(status='available').values(
|
||
'blood_group__abo_type', 'blood_group__rh_factor', 'component__name'
|
||
).annotate(count=Count('id')).order_by(
|
||
'blood_group__abo_type', 'blood_group__rh_factor', 'component__name'
|
||
)
|
||
|
||
# Get expiring units
|
||
expiring_units = BloodUnit.objects.filter(
|
||
status='available',
|
||
expiry_date__lte=timezone.now() + timedelta(days=7)
|
||
).select_related('component', 'blood_group').order_by('expiry_date')
|
||
|
||
# Get location utilization
|
||
locations = InventoryLocation.objects.filter(is_active=True)
|
||
|
||
context = {
|
||
'inventory_data': inventory_data,
|
||
'expiring_units': expiring_units,
|
||
'locations': locations,
|
||
}
|
||
|
||
return render(request, 'blood_bank/inventory/inventory_dashboard.html', context)
|
||
|
||
|
||
# Quality Control Views
|
||
@login_required
|
||
def quality_control_list(request):
|
||
"""List quality control tests"""
|
||
qc_tests = QualityControl.objects.select_related(
|
||
'performed_by', 'reviewed_by'
|
||
).order_by('-test_date')
|
||
|
||
paginator = Paginator(qc_tests, 25)
|
||
page_number = request.GET.get('page')
|
||
page_obj = paginator.get_page(page_number)
|
||
|
||
return render(request, 'blood_bank/quality_control/quality_control_list.html', {'page_obj': page_obj})
|
||
|
||
|
||
@login_required
|
||
@permission_required('blood_bank.add_qualitycontrol')
|
||
def quality_control_create(request):
|
||
"""Create quality control test"""
|
||
if request.method == 'POST':
|
||
form = QualityControlForm(request.POST)
|
||
if form.is_valid():
|
||
qc_test = form.save(commit=False)
|
||
qc_test.performed_by = request.user
|
||
qc_test.save()
|
||
messages.success(request, 'Quality control test created successfully.')
|
||
return redirect('blood_bank:quality_control_list')
|
||
else:
|
||
form = QualityControlForm(initial={'test_date': timezone.now()})
|
||
|
||
return render(request, 'blood_bank/quality_control/quality_control_form.html', {
|
||
'form': form, 'title': 'Create QC Test'
|
||
})
|
||
|
||
|
||
# API Views for AJAX requests
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def api_blood_availability(request):
|
||
"""API endpoint for checking blood availability"""
|
||
blood_group_id = request.GET.get('blood_group')
|
||
component_id = request.GET.get('component')
|
||
|
||
if not blood_group_id or not component_id:
|
||
return JsonResponse({'error': 'Missing parameters'}, status=400)
|
||
|
||
available_units = BloodUnit.objects.filter(
|
||
blood_group_id=blood_group_id,
|
||
component_id=component_id,
|
||
status='available'
|
||
).count()
|
||
|
||
return JsonResponse({'available_units': available_units})
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def api_donor_search(request):
|
||
"""API endpoint for donor search"""
|
||
query = request.GET.get('q', '')
|
||
|
||
if len(query) < 2:
|
||
return JsonResponse({'donors': []})
|
||
|
||
donors = Donor.objects.filter(
|
||
Q(donor_id__icontains=query) |
|
||
Q(first_name__icontains=query) |
|
||
Q(last_name__icontains=query)
|
||
).filter(status='active')[:10]
|
||
|
||
donor_data = [{
|
||
'id': donor.id,
|
||
'donor_id': donor.donor_id,
|
||
'name': donor.full_name,
|
||
'blood_group': str(donor.blood_group),
|
||
'is_eligible': donor.is_eligible_for_donation
|
||
} for donor in donors]
|
||
|
||
return JsonResponse({'donors': donor_data})
|
||
|
||
|
||
# Reports Views
|
||
@login_required
|
||
def reports_dashboard(request):
|
||
"""Blood bank reports dashboard"""
|
||
# Get date range from request
|
||
start_date = request.GET.get('start_date')
|
||
end_date = request.GET.get('end_date')
|
||
|
||
if not start_date:
|
||
start_date = timezone.now() - timedelta(days=30)
|
||
else:
|
||
start_date = timezone.datetime.strptime(start_date, '%Y-%m-%d').date()
|
||
|
||
if not end_date:
|
||
end_date = timezone.now().date()
|
||
else:
|
||
end_date = timezone.datetime.strptime(end_date, '%Y-%m-%d').date()
|
||
|
||
# Collection statistics
|
||
collections = BloodUnit.objects.filter(
|
||
collection_date__date__range=[start_date, end_date]
|
||
)
|
||
|
||
# Transfusion statistics
|
||
transfusions = Transfusion.objects.filter(
|
||
start_time__date__range=[start_date, end_date]
|
||
)
|
||
|
||
# Adverse reactions
|
||
reactions = AdverseReaction.objects.filter(
|
||
onset_time__date__range=[start_date, end_date]
|
||
)
|
||
|
||
context = {
|
||
'start_date': start_date,
|
||
'end_date': end_date,
|
||
'total_collections': collections.count(),
|
||
'total_transfusions': transfusions.count(),
|
||
'total_reactions': reactions.count(),
|
||
'collections_by_type': collections.values('component__name').annotate(count=Count('id')),
|
||
'transfusions_by_type': transfusions.values(
|
||
'blood_issue__blood_unit__component__name'
|
||
).annotate(count=Count('id')),
|
||
'reactions_by_type': reactions.values('reaction_type').annotate(count=Count('id')),
|
||
}
|
||
|
||
return render(request, 'blood_bank/reports_dashboard.html', context)
|
||
|
||
|
||
# Additional API Views for JavaScript functions
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def api_move_unit(request, unit_id):
|
||
"""API endpoint for moving blood unit to different location"""
|
||
try:
|
||
unit = BloodUnit.objects.get(id=unit_id)
|
||
new_location_id = request.POST.get('location_id')
|
||
|
||
if not new_location_id:
|
||
return JsonResponse({'error': 'Location ID required'}, status=400)
|
||
|
||
try:
|
||
new_location = InventoryLocation.objects.get(id=new_location_id)
|
||
except InventoryLocation.DoesNotExist:
|
||
return JsonResponse({'error': 'Invalid location'}, status=400)
|
||
|
||
# Check location capacity
|
||
current_units = BloodUnit.objects.filter(
|
||
current_location=new_location,
|
||
status__in=['available', 'quarantined']
|
||
).count()
|
||
|
||
if current_units >= new_location.capacity:
|
||
return JsonResponse({'error': 'Location at capacity'}, status=400)
|
||
|
||
old_location = unit.current_location
|
||
unit.current_location = new_location
|
||
unit.save()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': f'Unit moved from {old_location} to {new_location}',
|
||
'new_location': str(new_location)
|
||
})
|
||
|
||
except BloodUnit.DoesNotExist:
|
||
return JsonResponse({'error': 'Blood unit not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=500)
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def api_expiry_report(request):
|
||
"""API endpoint for expiry report"""
|
||
days_ahead = int(request.GET.get('days', 7))
|
||
|
||
expiring_units = BloodUnit.objects.filter(
|
||
expiry_date__lte=timezone.now() + timedelta(days=days_ahead),
|
||
status='available'
|
||
).select_related('blood_group', 'component', 'current_location')
|
||
|
||
report_data = []
|
||
for unit in expiring_units:
|
||
days_to_expiry = (unit.expiry_date - timezone.now().date()).days
|
||
report_data.append({
|
||
'unit_number': unit.unit_number,
|
||
'blood_group': str(unit.blood_group),
|
||
'component': unit.component.get_name_display(),
|
||
'expiry_date': unit.expiry_date.strftime('%Y-%m-%d'),
|
||
'days_to_expiry': days_to_expiry,
|
||
'location': str(unit.current_location),
|
||
'urgency': 'critical' if days_to_expiry <= 2 else 'warning' if days_to_expiry <= 5 else 'normal'
|
||
})
|
||
|
||
return JsonResponse({
|
||
'units': report_data,
|
||
'total_expiring': len(report_data),
|
||
'critical_count': len([u for u in report_data if u['urgency'] == 'critical']),
|
||
'warning_count': len([u for u in report_data if u['urgency'] == 'warning'])
|
||
})
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def api_cancel_request(request, request_id):
|
||
"""API endpoint for cancelling blood request"""
|
||
try:
|
||
blood_request = BloodRequest.objects.get(id=request_id)
|
||
|
||
if blood_request.status in ['completed', 'cancelled']:
|
||
return JsonResponse({'error': 'Request cannot be cancelled'}, status=400)
|
||
|
||
cancellation_reason = request.POST.get('reason', '')
|
||
if not cancellation_reason:
|
||
return JsonResponse({'error': 'Cancellation reason required'}, status=400)
|
||
|
||
# Release any reserved units
|
||
reserved_units = BloodUnit.objects.filter(
|
||
reserved_for_request=blood_request,
|
||
status='reserved'
|
||
)
|
||
reserved_units.update(status='available', reserved_for_request=None)
|
||
|
||
blood_request.status = 'cancelled'
|
||
blood_request.cancellation_reason = cancellation_reason
|
||
blood_request.cancelled_by = request.user
|
||
blood_request.cancellation_date = timezone.now()
|
||
blood_request.save()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': 'Blood request cancelled successfully',
|
||
'released_units': reserved_units.count()
|
||
})
|
||
|
||
except BloodRequest.DoesNotExist:
|
||
return JsonResponse({'error': 'Blood request not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=500)
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def api_check_availability(request):
|
||
"""API endpoint for real-time blood availability checking"""
|
||
blood_group_id = request.GET.get('blood_group')
|
||
component_id = request.GET.get('component')
|
||
quantity = int(request.GET.get('quantity', 1))
|
||
|
||
if not blood_group_id or not component_id:
|
||
return JsonResponse({'error': 'Missing parameters'}, status=400)
|
||
|
||
available_units = BloodUnit.objects.filter(
|
||
blood_group_id=blood_group_id,
|
||
component_id=component_id,
|
||
status='available'
|
||
).order_by('expiry_date')
|
||
|
||
total_available = available_units.count()
|
||
expiring_soon = available_units.filter(
|
||
expiry_date__lte=timezone.now() + timedelta(days=7)
|
||
).count()
|
||
|
||
# Get units by location
|
||
location_breakdown = {}
|
||
for unit in available_units:
|
||
location = str(unit.current_location)
|
||
if location not in location_breakdown:
|
||
location_breakdown[location] = 0
|
||
location_breakdown[location] += 1
|
||
|
||
return JsonResponse({
|
||
'total_available': total_available,
|
||
'requested_quantity': quantity,
|
||
'can_fulfill': total_available >= quantity,
|
||
'expiring_soon': expiring_soon,
|
||
'location_breakdown': location_breakdown,
|
||
'oldest_unit_expiry': available_units.first().expiry_date.strftime(
|
||
'%Y-%m-%d') if available_units.exists() else None
|
||
})
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def api_urgency_report(request):
|
||
"""API endpoint for urgency statistics report"""
|
||
# Get requests by urgency level
|
||
urgency_stats = BloodRequest.objects.values('urgency').annotate(
|
||
count=Count('id'),
|
||
pending=Count('id', filter=Q(status='pending')),
|
||
completed=Count('id', filter=Q(status='completed'))
|
||
)
|
||
|
||
# Get emergency requests from last 24 hours
|
||
emergency_requests = BloodRequest.objects.filter(
|
||
urgency='emergency',
|
||
created_at__gte=timezone.now() - timedelta(hours=24)
|
||
).count()
|
||
|
||
# Average response time for urgent requests
|
||
urgent_completed = BloodRequest.objects.filter(
|
||
urgency__in=['urgent', 'emergency'],
|
||
status='completed',
|
||
completed_at__isnull=False
|
||
)
|
||
|
||
avg_response_times = {}
|
||
for request in urgent_completed:
|
||
urgency = request.urgency
|
||
response_time = (request.completed_at - request.created_at).total_seconds() / 60 # minutes
|
||
if urgency not in avg_response_times:
|
||
avg_response_times[urgency] = []
|
||
avg_response_times[urgency].append(response_time)
|
||
|
||
# Calculate averages
|
||
for urgency in avg_response_times:
|
||
times = avg_response_times[urgency]
|
||
avg_response_times[urgency] = sum(times) / len(times) if times else 0
|
||
|
||
return JsonResponse({
|
||
'urgency_breakdown': list(urgency_stats),
|
||
'emergency_last_24h': emergency_requests,
|
||
'avg_response_times': avg_response_times,
|
||
'total_pending': BloodRequest.objects.filter(status='pending').count()
|
||
})
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def api_initiate_capa(request):
|
||
"""API endpoint for initiating CAPA (Corrective and Preventive Action)"""
|
||
qc_record_id = request.POST.get('qc_record_id')
|
||
priority = request.POST.get('priority', 'medium')
|
||
assessment = request.POST.get('assessment', '')
|
||
|
||
if not qc_record_id:
|
||
return JsonResponse({'error': 'QC record ID required'}, status=400)
|
||
|
||
try:
|
||
qc_record = QualityControl.objects.get(id=qc_record_id)
|
||
|
||
if qc_record.capa_initiated:
|
||
return JsonResponse({'error': 'CAPA already initiated'}, status=400)
|
||
|
||
# Generate CAPA number
|
||
capa_number = f"CAPA-{qc_record.id}-{timezone.now().strftime('%Y%m%d')}"
|
||
|
||
qc_record.capa_initiated = True
|
||
qc_record.capa_number = capa_number
|
||
qc_record.capa_priority = priority
|
||
qc_record.capa_initiated_by = request.user
|
||
qc_record.capa_date = timezone.now()
|
||
qc_record.capa_assessment = assessment
|
||
qc_record.capa_status = 'open'
|
||
qc_record.save()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'capa_number': capa_number,
|
||
'message': f'CAPA {capa_number} initiated successfully'
|
||
})
|
||
|
||
except QualityControl.DoesNotExist:
|
||
return JsonResponse({'error': 'QC record not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=500)
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def api_review_results(request):
|
||
"""API endpoint for reviewing QC results"""
|
||
qc_record_id = request.POST.get('qc_record_id')
|
||
review_notes = request.POST.get('review_notes', '')
|
||
|
||
if not qc_record_id:
|
||
return JsonResponse({'error': 'QC record ID required'}, status=400)
|
||
|
||
try:
|
||
qc_record = QualityControl.objects.get(id=qc_record_id)
|
||
|
||
if qc_record.reviewed_by:
|
||
return JsonResponse({'error': 'Results already reviewed'}, status=400)
|
||
|
||
qc_record.reviewed_by = request.user
|
||
qc_record.review_date = timezone.now()
|
||
qc_record.review_notes = review_notes
|
||
qc_record.save()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'reviewed_by': request.user.get_full_name(),
|
||
'review_date': qc_record.review_date.strftime('%Y-%m-%d %H:%M'),
|
||
'message': 'QC results reviewed successfully'
|
||
})
|
||
|
||
except QualityControl.DoesNotExist:
|
||
return JsonResponse({'error': 'QC record not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=500)
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def api_record_vital_signs(request):
|
||
"""API endpoint for recording vital signs during transfusion"""
|
||
transfusion_id = request.POST.get('transfusion_id')
|
||
|
||
if not transfusion_id:
|
||
return JsonResponse({'error': 'Transfusion ID required'}, status=400)
|
||
|
||
try:
|
||
transfusion = Transfusion.objects.get(id=transfusion_id)
|
||
|
||
vital_signs = {
|
||
'blood_pressure': request.POST.get('blood_pressure'),
|
||
'heart_rate': request.POST.get('heart_rate'),
|
||
'temperature': request.POST.get('temperature'),
|
||
'respiratory_rate': request.POST.get('respiratory_rate'),
|
||
'oxygen_saturation': request.POST.get('oxygen_saturation'),
|
||
'recorded_at': timezone.now().isoformat(),
|
||
'recorded_by': request.user.get_full_name()
|
||
}
|
||
|
||
# Add to existing vital signs history
|
||
if transfusion.vital_signs_history:
|
||
vital_signs_list = json.loads(transfusion.vital_signs_history)
|
||
else:
|
||
vital_signs_list = []
|
||
|
||
vital_signs_list.append(vital_signs)
|
||
transfusion.vital_signs_history = json.dumps(vital_signs_list)
|
||
|
||
# Update current vital signs
|
||
transfusion.current_blood_pressure = vital_signs['blood_pressure']
|
||
transfusion.current_heart_rate = vital_signs['heart_rate']
|
||
transfusion.current_temperature = vital_signs['temperature']
|
||
transfusion.current_respiratory_rate = vital_signs['respiratory_rate']
|
||
transfusion.current_oxygen_saturation = vital_signs['oxygen_saturation']
|
||
transfusion.last_vitals_check = timezone.now()
|
||
|
||
transfusion.save()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': 'Vital signs recorded successfully',
|
||
'vital_signs': vital_signs
|
||
})
|
||
|
||
except Transfusion.DoesNotExist:
|
||
return JsonResponse({'error': 'Transfusion not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=500)
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def api_stop_transfusion(request):
|
||
"""API endpoint for emergency stop of transfusion"""
|
||
transfusion_id = request.POST.get('transfusion_id')
|
||
stop_reason = request.POST.get('stop_reason', '')
|
||
|
||
if not transfusion_id:
|
||
return JsonResponse({'error': 'Transfusion ID required'}, status=400)
|
||
|
||
try:
|
||
transfusion = Transfusion.objects.get(id=transfusion_id)
|
||
|
||
if transfusion.status not in ['started', 'in_progress']:
|
||
return JsonResponse({'error': 'Transfusion cannot be stopped'}, status=400)
|
||
|
||
transfusion.status = 'stopped'
|
||
transfusion.end_time = timezone.now()
|
||
transfusion.stop_reason = stop_reason
|
||
transfusion.stopped_by = request.user
|
||
|
||
# Calculate volume transfused based on time elapsed
|
||
if transfusion.start_time:
|
||
elapsed_minutes = (timezone.now() - transfusion.start_time).total_seconds() / 60
|
||
rate_ml_per_min = transfusion.transfusion_rate / 60 if transfusion.transfusion_rate else 0
|
||
volume_transfused = min(elapsed_minutes * rate_ml_per_min, transfusion.blood_unit.volume_ml)
|
||
transfusion.volume_transfused = volume_transfused
|
||
|
||
transfusion.save()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': 'Transfusion stopped successfully',
|
||
'volume_transfused': transfusion.volume_transfused,
|
||
'stop_time': transfusion.end_time.strftime('%Y-%m-%d %H:%M:%S')
|
||
})
|
||
|
||
except Transfusion.DoesNotExist:
|
||
return JsonResponse({'error': 'Transfusion not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=500)
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def api_complete_transfusion(request):
|
||
"""API endpoint for completing transfusion"""
|
||
transfusion_id = request.POST.get('transfusion_id')
|
||
completion_notes = request.POST.get('completion_notes', '')
|
||
|
||
if not transfusion_id:
|
||
return JsonResponse({'error': 'Transfusion ID required'}, status=400)
|
||
|
||
try:
|
||
transfusion = Transfusion.objects.get(id=transfusion_id)
|
||
|
||
if transfusion.status not in ['started', 'in_progress']:
|
||
return JsonResponse({'error': 'Transfusion cannot be completed'}, status=400)
|
||
|
||
transfusion.status = 'completed'
|
||
transfusion.end_time = timezone.now()
|
||
transfusion.completion_notes = completion_notes
|
||
transfusion.completed_by = request.user
|
||
|
||
# Set volume transfused to full unit if not already set
|
||
if not transfusion.volume_transfused:
|
||
transfusion.volume_transfused = transfusion.blood_unit.volume_ml
|
||
|
||
transfusion.save()
|
||
|
||
# Update blood unit status
|
||
blood_unit = transfusion.blood_unit
|
||
blood_unit.status = 'transfused'
|
||
blood_unit.save()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': 'Transfusion completed successfully',
|
||
'completion_time': transfusion.end_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'total_volume': transfusion.volume_transfused
|
||
})
|
||
|
||
except Transfusion.DoesNotExist:
|
||
return JsonResponse({'error': 'Transfusion not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=500)
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def api_export_csv(request):
|
||
"""API endpoint for exporting data to CSV"""
|
||
import csv
|
||
from django.http import HttpResponse
|
||
|
||
export_type = request.GET.get('type', 'donors')
|
||
|
||
response = HttpResponse(content_type='text/csv')
|
||
response['Content-Disposition'] = f'attachment; filename="{export_type}_{timezone.now().strftime("%Y%m%d")}.csv"'
|
||
|
||
writer = csv.writer(response)
|
||
|
||
if export_type == 'donors':
|
||
writer.writerow(['Donor ID', 'Name', 'Blood Group', 'Status', 'Last Donation', 'Total Donations'])
|
||
donors = Donor.objects.select_related('blood_group').all()
|
||
for donor in donors:
|
||
last_donation = donor.blood_units.order_by('-collection_date').first()
|
||
writer.writerow([
|
||
donor.donor_id,
|
||
donor.full_name,
|
||
str(donor.blood_group),
|
||
donor.get_status_display(),
|
||
last_donation.collection_date.strftime('%Y-%m-%d') if last_donation else 'Never',
|
||
donor.blood_units.count()
|
||
])
|
||
|
||
elif export_type == 'units':
|
||
writer.writerow(
|
||
['Unit Number', 'Blood Group', 'Component', 'Status', 'Collection Date', 'Expiry Date', 'Location'])
|
||
units = BloodUnit.objects.select_related('blood_group', 'component', 'current_location').all()
|
||
for unit in units:
|
||
writer.writerow([
|
||
unit.unit_number,
|
||
str(unit.blood_group),
|
||
unit.component.get_name_display(),
|
||
unit.get_status_display(),
|
||
unit.collection_date.strftime('%Y-%m-%d'),
|
||
unit.expiry_date.strftime('%Y-%m-%d'),
|
||
str(unit.current_location)
|
||
])
|
||
|
||
elif export_type == 'requests':
|
||
writer.writerow(
|
||
['Request Number', 'Patient', 'Blood Group', 'Component', 'Quantity', 'Urgency', 'Status', 'Created Date'])
|
||
requests = BloodRequest.objects.select_related('patient', 'component').all()
|
||
for req in requests:
|
||
writer.writerow([
|
||
req.request_number,
|
||
req.patient.full_name,
|
||
str(req.patient.blood_group),
|
||
req.component.get_name_display(),
|
||
req.quantity_requested,
|
||
req.get_urgency_display(),
|
||
req.get_status_display(),
|
||
req.created_at.strftime('%Y-%m-%d %H:%M')
|
||
])
|
||
|
||
return response
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def api_inventory_locations(request):
|
||
"""API endpoint for inventory location management"""
|
||
locations = InventoryLocation.objects.all()
|
||
|
||
location_data = []
|
||
for location in locations:
|
||
current_units = BloodUnit.objects.filter(
|
||
current_location=location,
|
||
status__in=['available', 'quarantined']
|
||
).count()
|
||
|
||
location_data.append({
|
||
'id': location.id,
|
||
'name': location.name,
|
||
'location_type': location.get_location_type_display(),
|
||
'capacity': location.capacity,
|
||
'current_units': current_units,
|
||
'available_space': location.capacity - current_units,
|
||
'temperature': location.temperature,
|
||
'is_active': location.is_active,
|
||
'utilization_percent': round((current_units / location.capacity) * 100, 1) if location.capacity > 0 else 0
|
||
})
|
||
|
||
return JsonResponse({'locations': location_data})
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def api_update_location(request, location_id):
|
||
"""API endpoint for updating inventory location"""
|
||
try:
|
||
location = InventoryLocation.objects.get(id=location_id)
|
||
|
||
# Update fields if provided
|
||
if 'temperature' in request.POST:
|
||
location.temperature = float(request.POST['temperature'])
|
||
|
||
if 'is_active' in request.POST:
|
||
location.is_active = request.POST['is_active'].lower() == 'true'
|
||
|
||
if 'notes' in request.POST:
|
||
location.notes = request.POST['notes']
|
||
|
||
location.save()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': 'Location updated successfully',
|
||
'location': {
|
||
'id': location.id,
|
||
'name': location.name,
|
||
'temperature': location.temperature,
|
||
'is_active': location.is_active
|
||
}
|
||
})
|
||
|
||
except InventoryLocation.DoesNotExist:
|
||
return JsonResponse({'error': 'Location not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=500)
|
||
|