468 lines
19 KiB
HTML
468 lines
19 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Blood Unit Management{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- BEGIN breadcrumb -->
|
|
<ol class="breadcrumb float-xl-end">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
|
|
<li class="breadcrumb-item active">Blood Units</li>
|
|
</ol>
|
|
<!-- END breadcrumb -->
|
|
|
|
<!-- BEGIN page-header -->
|
|
<h1 class="page-header">Blood Unit Management <small>track and manage blood inventory</small></h1>
|
|
<!-- END page-header -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Blood Unit Inventory</h4>
|
|
<div class="panel-heading-btn">
|
|
<a href="{% url 'blood_bank:blood_unit_create' %}" class="btn btn-primary btn-sm">
|
|
<i class="fa fa-plus"></i> Register Blood Unit
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
<!-- BEGIN search and filter form -->
|
|
<form method="get" class="mb-4">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label for="blood_group">Blood Group</label>
|
|
{{ form.blood_group }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label for="component">Component</label>
|
|
{{ form.component }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="form-group">
|
|
<label for="status">Status</label>
|
|
{{ form.status }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="form-group">
|
|
<label for="expiry_days">Expiring in (days)</label>
|
|
{{ form.expiry_days }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="form-group">
|
|
<label> </label>
|
|
<div class="d-grid">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fa fa-search"></i> Filter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
<!-- END search and filter form -->
|
|
|
|
<!-- BEGIN summary cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card bg-primary text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Available Units</h6>
|
|
<h3 class="mb-0">{{ page_obj.paginator.count }}</h3>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fa fa-tint fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-success text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Fresh Units</h6>
|
|
<h3 class="mb-0">
|
|
{{ page_obj.object_list|length }}
|
|
</h3>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fa fa-check-circle fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-warning text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Expiring Soon</h6>
|
|
<h3 class="mb-0" id="expiringSoonCount">0</h3>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fa fa-exclamation-triangle fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-danger text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Expired Units</h6>
|
|
<h3 class="mb-0" id="expiredCount">0</h3>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fa fa-times-circle fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END summary cards -->
|
|
|
|
<!-- BEGIN table -->
|
|
<div class="table-responsive">
|
|
<table id="bloodUnitTable" class="table table-striped table-bordered align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Unit Number</th>
|
|
<th>Donor</th>
|
|
<th>Blood Group</th>
|
|
<th>Component</th>
|
|
<th>Collection Date</th>
|
|
<th>Expiry Date</th>
|
|
<th>Volume (ml)</th>
|
|
<th>Status</th>
|
|
<th>Location</th>
|
|
<th>Days to Expiry</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for unit in page_obj %}
|
|
<tr data-unit-id="{{ unit.id }}" data-days-to-expiry="{{ unit.days_to_expiry }}" data-status="{{ unit.status }}">
|
|
<td>
|
|
<a href="{% url 'blood_bank:blood_unit_detail' unit.id %}" class="text-decoration-none">
|
|
<strong>{{ unit.unit_number }}</strong>
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<a href="{% url 'blood_bank:donor_detail' unit.donor.id %}" class="text-decoration-none">
|
|
{{ unit.donor.full_name }}
|
|
</a>
|
|
<br>
|
|
<small class="text-muted">{{ unit.donor.donor_id }}</small>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-primary fs-6">{{ unit.blood_group.display_name }}</span>
|
|
</td>
|
|
<td>{{ unit.component.get_name_display }}</td>
|
|
<td>{{ unit.collection_date|date:"M d, Y H:i" }}</td>
|
|
<td>
|
|
{{ unit.expiry_date|date:"M d, Y" }}
|
|
{% if unit.days_to_expiry <= 3 and unit.status == 'available' %}
|
|
<br><span class="badge bg-danger">Expiring Soon</span>
|
|
{% elif unit.is_expired %}
|
|
<br><span class="badge bg-danger">Expired</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ unit.volume_ml }}</td>
|
|
<td>
|
|
{% if unit.status == 'available' %}
|
|
<span class="badge bg-success">{{ unit.get_status_display }}</span>
|
|
{% elif unit.status == 'expired' %}
|
|
<span class="badge bg-danger">{{ unit.get_status_display }}</span>
|
|
{% elif unit.status == 'issued' %}
|
|
<span class="badge bg-info">{{ unit.get_status_display }}</span>
|
|
{% elif unit.status == 'transfused' %}
|
|
<span class="badge bg-primary">{{ unit.get_status_display }}</span>
|
|
{% elif unit.status == 'discarded' %}
|
|
<span class="badge bg-dark">{{ unit.get_status_display }}</span>
|
|
{% else %}
|
|
<span class="badge bg-warning">{{ unit.get_status_display }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ unit.location }}</td>
|
|
<td>
|
|
{% if unit.is_expired %}
|
|
<span class="text-danger fw-bold">Expired</span>
|
|
{% elif unit.days_to_expiry <= 3 %}
|
|
<span class="text-warning fw-bold">{{ unit.days_to_expiry }} days</span>
|
|
{% else %}
|
|
<span class="text-success">{{ unit.days_to_expiry }} days</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group" role="group">
|
|
<a href="{% url 'blood_bank:blood_unit_detail' unit.id %}"
|
|
class="btn btn-outline-primary btn-sm" title="View Details">
|
|
<i class="fa fa-eye"></i>
|
|
</a>
|
|
{% if unit.status == 'collected' or unit.status == 'testing' %}
|
|
<a href="{% url 'blood_bank:blood_test_create' unit.id %}"
|
|
class="btn btn-outline-success btn-sm" title="Add Test">
|
|
<i class="fa fa-flask"></i>
|
|
</a>
|
|
{% endif %}
|
|
{% if unit.status == 'available' %}
|
|
<button type="button" class="btn btn-outline-warning btn-sm"
|
|
onclick="moveUnit({{ unit.id }})" title="Move Location">
|
|
<i class="fa fa-arrows-alt"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="11" class="text-center">
|
|
<div class="py-4">
|
|
<i class="fa fa-tint fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No blood units found</h5>
|
|
<p class="text-muted">Try adjusting your search criteria or register a new blood unit.</p>
|
|
<a href="{% url 'blood_bank:blood_unit_create' %}" class="btn btn-primary">
|
|
<i class="fa fa-plus"></i> Register Blood Unit
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
|
|
</div>
|
|
<!-- END table -->
|
|
|
|
<!-- BEGIN pagination -->
|
|
{% if is_paginated %}
|
|
{% include 'partial/pagination.html' %}
|
|
{% endif %}
|
|
<!-- END pagination -->
|
|
|
|
<!-- BEGIN summary -->
|
|
<div class="row mt-3">
|
|
|
|
<div class="col-md-6 text-end">
|
|
<div class="btn-group" role="group">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
|
<i class="fa fa-print"></i> Print
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="exportToCSV()">
|
|
<i class="fa fa-download"></i> Export
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="showExpiryReport()">
|
|
<i class="fa fa-chart-bar"></i> Expiry Report
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END summary -->
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN move unit modal -->
|
|
<div class="modal fade" id="moveUnitModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Move Blood Unit</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="moveUnitForm">
|
|
<div class="mb-3">
|
|
<label for="newLocation" class="form-label">New Location</label>
|
|
<select class="form-select" id="newLocation" required>
|
|
<option value="">Select Location</option>
|
|
<option value="Refrigerator A">Refrigerator A</option>
|
|
<option value="Refrigerator B">Refrigerator B</option>
|
|
<option value="Freezer 1">Freezer 1</option>
|
|
<option value="Platelet Agitator">Platelet Agitator</option>
|
|
<option value="Quarantine">Quarantine</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="moveReason" class="form-label">Reason for Move</label>
|
|
<textarea class="form-control" id="moveReason" rows="3"></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="confirmMoveUnit()">Move Unit</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END move unit modal -->
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize Select2 for filters
|
|
$('.form-select').select2({
|
|
theme: 'bootstrap-5',
|
|
width: '100%'
|
|
});
|
|
|
|
// Initialize DataTable
|
|
$('#bloodUnitTable').DataTable({
|
|
responsive: true,
|
|
pageLength: 25,
|
|
order: [[4, 'desc']], // Sort by collection date
|
|
columnDefs: [
|
|
{ orderable: false, targets: [10] } // Actions column
|
|
]
|
|
});
|
|
|
|
// Calculate summary statistics
|
|
calculateSummaryStats();
|
|
});
|
|
|
|
function calculateSummaryStats() {
|
|
var expiringSoon = 0;
|
|
var expired = 0;
|
|
|
|
$('tr[data-unit-id]').each(function() {
|
|
var daysToExpiry = parseInt($(this).data('days-to-expiry'));
|
|
var status = $(this).data('status');
|
|
|
|
if (status === 'expired' || daysToExpiry === 0) {
|
|
expired++;
|
|
} else if (daysToExpiry <= 7 && status === 'available') {
|
|
expiringSoon++;
|
|
}
|
|
});
|
|
|
|
$('#expiringSoonCount').text(expiringSoon);
|
|
$('#expiredCount').text(expired);
|
|
}
|
|
|
|
var currentUnitId = null;
|
|
|
|
function moveUnit(unitId) {
|
|
currentUnitId = unitId;
|
|
$('#moveUnitModal').modal('show');
|
|
}
|
|
|
|
function confirmMoveUnit() {
|
|
var newLocation = $('#newLocation').val();
|
|
var reason = $('#moveReason').val();
|
|
|
|
if (!newLocation) {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: 'Missing Information',
|
|
text: 'Please select a new location.',
|
|
confirmButtonText: 'OK'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Here you would make an AJAX call to update the unit location
|
|
// For now, we'll just show a success message
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: 'Unit Moved',
|
|
text: 'Blood unit has been moved to ' + newLocation,
|
|
confirmButtonText: 'OK'
|
|
}).then(() => {
|
|
$('#moveUnitModal').modal('hide');
|
|
location.reload(); // Refresh the page
|
|
});
|
|
}
|
|
|
|
function exportToCSV() {
|
|
var csv = [];
|
|
var rows = document.querySelectorAll("#bloodUnitTable tr");
|
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
var row = [], cols = rows[i].querySelectorAll("td, th");
|
|
|
|
for (var j = 0; j < cols.length - 1; j++) { // Exclude actions column
|
|
row.push(cols[j].innerText.replace(/,/g, ';'));
|
|
}
|
|
|
|
csv.push(row.join(","));
|
|
}
|
|
|
|
var csvFile = new Blob([csv.join("\n")], {type: "text/csv"});
|
|
var downloadLink = document.createElement("a");
|
|
downloadLink.download = "blood_units.csv";
|
|
downloadLink.href = window.URL.createObjectURL(csvFile);
|
|
downloadLink.style.display = "none";
|
|
document.body.appendChild(downloadLink);
|
|
downloadLink.click();
|
|
}
|
|
|
|
function showExpiryReport() {
|
|
var expiryData = {};
|
|
|
|
$('tr[data-unit-id]').each(function() {
|
|
var daysToExpiry = parseInt($(this).data('days-to-expiry'));
|
|
var status = $(this).data('status');
|
|
|
|
if (status === 'available') {
|
|
if (daysToExpiry <= 0) {
|
|
expiryData['Expired'] = (expiryData['Expired'] || 0) + 1;
|
|
} else if (daysToExpiry <= 3) {
|
|
expiryData['1-3 days'] = (expiryData['1-3 days'] || 0) + 1;
|
|
} else if (daysToExpiry <= 7) {
|
|
expiryData['4-7 days'] = (expiryData['4-7 days'] || 0) + 1;
|
|
} else if (daysToExpiry <= 14) {
|
|
expiryData['8-14 days'] = (expiryData['8-14 days'] || 0) + 1;
|
|
} else {
|
|
expiryData['15+ days'] = (expiryData['15+ days'] || 0) + 1;
|
|
}
|
|
}
|
|
});
|
|
|
|
var reportHtml = '<table class="table table-sm"><thead><tr><th>Expiry Range</th><th>Count</th></tr></thead><tbody>';
|
|
|
|
Object.keys(expiryData).forEach(function(range) {
|
|
reportHtml += '<tr><td>' + range + '</td><td>' + expiryData[range] + '</td></tr>';
|
|
});
|
|
|
|
reportHtml += '</tbody></table>';
|
|
|
|
Swal.fire({
|
|
title: 'Blood Unit Expiry Report',
|
|
html: reportHtml,
|
|
width: '500px',
|
|
confirmButtonText: 'Close'
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|