1227 lines
42 KiB
Plaintext
1227 lines
42 KiB
Plaintext
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 .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')[:10]
|
|
|
|
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
|
|
@login_required
|
|
def donor_list(request):
|
|
"""List all donors with filtering and search"""
|
|
donors = Donor.objects.select_related('blood_group').order_by('-registration_date')
|
|
|
|
# Search functionality
|
|
search_query = request.GET.get('search')
|
|
if search_query:
|
|
donors = donors.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 = request.GET.get('status')
|
|
if status_filter:
|
|
donors = donors.filter(status=status_filter)
|
|
|
|
# Filter by blood group
|
|
blood_group_filter = request.GET.get('blood_group')
|
|
if blood_group_filter:
|
|
donors = donors.filter(blood_group_id=blood_group_filter)
|
|
|
|
paginator = Paginator(donors, 25)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'blood_groups': BloodGroup.objects.all(),
|
|
'status_choices': Donor.STATUS_CHOICES,
|
|
'search_query': search_query,
|
|
'status_filter': status_filter,
|
|
'blood_group_filter': blood_group_filter,
|
|
}
|
|
|
|
return render(request, 'blood_bank/donor_list.html', context)
|
|
|
|
|
|
@login_required
|
|
def donor_detail(request, donor_id):
|
|
"""Donor detail view with donation history"""
|
|
donor = get_object_or_404(Donor, id=donor_id)
|
|
|
|
# Get donation history
|
|
blood_units = BloodUnit.objects.filter(donor=donor).select_related(
|
|
'component', 'blood_group'
|
|
).order_by('-collection_date')
|
|
|
|
context = {
|
|
'donor': donor,
|
|
'blood_units': blood_units,
|
|
'total_donations': blood_units.count(),
|
|
'last_donation': blood_units.first(),
|
|
}
|
|
|
|
return render(request, 'blood_bank/donor_detail.html', context)
|
|
|
|
|
|
@login_required
|
|
@permission_required('blood_bank.add_donor')
|
|
def donor_create(request):
|
|
"""Create new donor"""
|
|
if request.method == 'POST':
|
|
form = DonorForm(request.POST)
|
|
if form.is_valid():
|
|
donor = form.save(commit=False)
|
|
donor.created_by = request.user
|
|
donor.save()
|
|
messages.success(request, f'Donor {donor.donor_id} created successfully.')
|
|
return redirect('blood_bank:donor_detail', donor_id=donor.id)
|
|
else:
|
|
form = DonorForm()
|
|
|
|
return render(request, 'blood_bank/donor_form.html', {'form': form, 'title': 'Add New Donor'})
|
|
|
|
|
|
@login_required
|
|
@permission_required('blood_bank.change_donor')
|
|
def donor_update(request, donor_id):
|
|
"""Update donor information"""
|
|
donor = get_object_or_404(Donor, id=donor_id)
|
|
|
|
if request.method == 'POST':
|
|
form = DonorForm(request.POST, instance=donor)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, f'Donor {donor.donor_id} updated successfully.')
|
|
return redirect('blood_bank:donor_detail', donor_id=donor.id)
|
|
else:
|
|
form = DonorForm(instance=donor)
|
|
|
|
return render(request, 'blood_bank/donor_form.html', {
|
|
'form': form, 'donor': donor, 'title': 'Update Donor'
|
|
})
|
|
|
|
|
|
@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/donor_eligibility.html', context)
|
|
|
|
|
|
# Blood Unit Management Views
|
|
@login_required
|
|
def blood_unit_list(request):
|
|
"""List all blood units with filtering"""
|
|
form = BloodInventorySearchForm(request.GET)
|
|
blood_units = BloodUnit.objects.select_related(
|
|
'donor', 'component', 'blood_group'
|
|
).order_by('-collection_date')
|
|
|
|
if form.is_valid():
|
|
if form.cleaned_data['blood_group']:
|
|
blood_units = blood_units.filter(blood_group=form.cleaned_data['blood_group'])
|
|
|
|
if form.cleaned_data['component']:
|
|
blood_units = blood_units.filter(component=form.cleaned_data['component'])
|
|
|
|
if form.cleaned_data['status']:
|
|
blood_units = blood_units.filter(status=form.cleaned_data['status'])
|
|
|
|
if form.cleaned_data['expiry_days']:
|
|
expiry_date = timezone.now() + timedelta(days=form.cleaned_data['expiry_days'])
|
|
blood_units = blood_units.filter(expiry_date__lte=expiry_date)
|
|
|
|
paginator = Paginator(blood_units, 25)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'form': form,
|
|
}
|
|
|
|
return render(request, 'blood_bank/blood_unit_list.html', context)
|
|
|
|
|
|
@login_required
|
|
def blood_unit_detail(request, unit_id):
|
|
"""Blood unit detail view with test results"""
|
|
blood_unit = get_object_or_404(BloodUnit, id=unit_id)
|
|
|
|
# Get test results
|
|
tests = BloodTest.objects.filter(blood_unit=blood_unit).select_related('tested_by')
|
|
crossmatches = CrossMatch.objects.filter(blood_unit=blood_unit).select_related(
|
|
'recipient', 'tested_by'
|
|
)
|
|
|
|
context = {
|
|
'blood_unit': blood_unit,
|
|
'tests': tests,
|
|
'crossmatches': crossmatches,
|
|
}
|
|
|
|
return render(request, 'blood_bank/blood_unit_detail.html', context)
|
|
|
|
|
|
@login_required
|
|
@permission_required('blood_bank.add_bloodunit')
|
|
def blood_unit_create(request, donor_id=None):
|
|
"""Create new blood unit"""
|
|
donor = None
|
|
if donor_id:
|
|
donor = get_object_or_404(Donor, id=donor_id)
|
|
|
|
if request.method == 'POST':
|
|
form = BloodUnitForm(request.POST)
|
|
if form.is_valid():
|
|
blood_unit = form.save(commit=False)
|
|
blood_unit.collected_by = request.user
|
|
blood_unit.save()
|
|
|
|
# Update donor's last donation date and total donations
|
|
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(request, f'Blood unit {blood_unit.unit_number} created successfully.')
|
|
return redirect('blood_bank:blood_unit_detail', unit_id=blood_unit.id)
|
|
else:
|
|
initial_data = {}
|
|
if donor:
|
|
initial_data['donor'] = donor
|
|
initial_data['blood_group'] = donor.blood_group
|
|
form = BloodUnitForm(initial=initial_data)
|
|
|
|
return render(request, 'blood_bank/blood_unit_form.html', {
|
|
'form': form, 'donor': donor, 'title': 'Register Blood Unit'
|
|
})
|
|
|
|
|
|
# Blood Request Management Views
|
|
@login_required
|
|
def blood_request_list(request):
|
|
"""List all blood requests"""
|
|
requests = BloodRequest.objects.select_related(
|
|
'patient', 'requesting_department', 'requesting_physician', 'component_requested'
|
|
).order_by('-request_date')
|
|
|
|
# Filter by status
|
|
status_filter = request.GET.get('status')
|
|
if status_filter:
|
|
requests = requests.filter(status=status_filter)
|
|
|
|
# Filter by urgency
|
|
urgency_filter = request.GET.get('urgency')
|
|
if urgency_filter:
|
|
requests = requests.filter(urgency=urgency_filter)
|
|
|
|
paginator = Paginator(requests, 25)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'status_choices': BloodRequest.STATUS_CHOICES,
|
|
'urgency_choices': BloodRequest.URGENCY_CHOICES,
|
|
'status_filter': status_filter,
|
|
'urgency_filter': urgency_filter,
|
|
}
|
|
|
|
return render(request, 'blood_bank/blood_request_list.html', context)
|
|
|
|
|
|
@login_required
|
|
def blood_request_detail(request, request_id):
|
|
"""Blood request detail view"""
|
|
blood_request = get_object_or_404(BloodRequest, id=request_id)
|
|
|
|
# Get issued units for this request
|
|
issues = BloodIssue.objects.filter(blood_request=blood_request).select_related(
|
|
'blood_unit', 'issued_by', 'issued_to'
|
|
)
|
|
|
|
context = {
|
|
'blood_request': blood_request,
|
|
'issues': issues,
|
|
}
|
|
|
|
return render(request, 'blood_bank/blood_request_detail.html', context)
|
|
|
|
|
|
@login_required
|
|
@permission_required('blood_bank.add_bloodrequest')
|
|
def blood_request_create(request):
|
|
"""Create new blood request"""
|
|
if request.method == 'POST':
|
|
form = BloodRequestForm(request.POST)
|
|
if form.is_valid():
|
|
blood_request = form.save(commit=False)
|
|
blood_request.requesting_physician = 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(request, f'Blood request {blood_request.request_number} created successfully.')
|
|
return redirect('blood_bank:blood_request_detail', request_id=blood_request.id)
|
|
else:
|
|
form = BloodRequestForm()
|
|
|
|
return render(request, 'blood_bank/blood_request_form.html', {
|
|
'form': form, 'title': 'Create Blood Request'
|
|
})
|
|
|
|
|
|
# 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/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/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/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/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/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_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_overview.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_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_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)
|
|
|