Marwan Alwali ab2c4a36c5 update
2025-10-02 10:13:03 +03:00

1234 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 donors 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)