605 lines
22 KiB
HTML
605 lines
22 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Blood Bank Inventory Dashboard{% endblock %}
|
|
|
|
{% block css %}
|
|
|
|
<style>
|
|
.inventory-card {
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.inventory-card:hover {
|
|
transform: translateY(-5px);
|
|
}
|
|
|
|
.blood-group-card {
|
|
border-left: 4px solid;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.blood-group-o { border-left-color: #dc3545; }
|
|
.blood-group-a { border-left-color: #007bff; }
|
|
.blood-group-b { border-left-color: #28a745; }
|
|
.blood-group-ab { border-left-color: #ffc107; }
|
|
|
|
.expiry-alert {
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { opacity: 1; }
|
|
50% { opacity: 0.7; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
.temperature-normal { color: #28a745; }
|
|
.temperature-warning { color: #ffc107; }
|
|
.temperature-critical { color: #dc3545; }
|
|
|
|
.chart-container {
|
|
position: relative;
|
|
height: 300px;
|
|
}
|
|
</style>
|
|
{% 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">Inventory Dashboard</li>
|
|
</ol>
|
|
<!-- END breadcrumb -->
|
|
|
|
<!-- BEGIN page-header -->
|
|
<h1 class="page-header">Blood Bank Inventory <small>real-time inventory monitoring</small></h1>
|
|
<!-- END page-header -->
|
|
|
|
<!-- BEGIN summary cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="card inventory-card bg-primary text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Total Units</h6>
|
|
<h2 class="mb-0">{{ total_units }}</h2>
|
|
<small>Available for transfusion</small>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fa fa-tint fa-3x opacity-75"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="card inventory-card bg-success text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Fresh Units</h6>
|
|
<h2 class="mb-0">{{ fresh_units }}</h2>
|
|
<small>Less than 7 days old</small>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fa fa-check-circle fa-3x opacity-75"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="card inventory-card bg-warning text-white expiry-alert">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Expiring Soon</h6>
|
|
<h2 class="mb-0">{{ expiring_units }}</h2>
|
|
<small>Within 3 days</small>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fa fa-exclamation-triangle fa-3x opacity-75"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="card inventory-card bg-danger text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Critical Low</h6>
|
|
<h2 class="mb-0">{{ critical_low_count }}</h2>
|
|
<small>Below minimum levels</small>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fa fa-exclamation-circle fa-3x opacity-75"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END summary cards -->
|
|
|
|
<!-- BEGIN row -->
|
|
<div class="row">
|
|
<!-- BEGIN col-8 -->
|
|
<div class="col-xl-8">
|
|
<!-- BEGIN blood group inventory -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Blood Group Inventory</h4>
|
|
<div class="panel-heading-btn">
|
|
<button type="button" class="btn btn-info btn-sm" onclick="refreshInventory()">
|
|
<i class="fa fa-refresh"></i> Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="row">
|
|
{% for group in blood_groups %}
|
|
<div class="col-md-6">
|
|
<div class="blood-group-card card blood-group-{{ group.abo_type|lower }}">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h5 class="card-title mb-1">{{ group.display_name }}</h5>
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<small class="text-muted">Whole Blood</small>
|
|
<h6 class="mb-0">{{ group.whole_blood_count }}</h6>
|
|
</div>
|
|
<div class="col-6">
|
|
<small class="text-muted">RBC</small>
|
|
<h6 class="mb-0">{{ group.rbc_count }}</h6>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<small class="text-muted">Plasma</small>
|
|
<h6 class="mb-0">{{ group.plasma_count }}</h6>
|
|
</div>
|
|
<div class="col-6">
|
|
<small class="text-muted">Platelets</small>
|
|
<h6 class="mb-0">{{ group.platelet_count }}</h6>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="text-end">
|
|
<h3 class="mb-0">{{ group.total_units }}</h3>
|
|
<small class="text-muted">Total Units</small>
|
|
{% if group.is_critical_low %}
|
|
<br><span class="badge bg-danger">Critical</span>
|
|
{% elif group.is_low %}
|
|
<br><span class="badge bg-warning">Low</span>
|
|
{% else %}
|
|
<br><span class="badge bg-success">Good</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END blood group inventory -->
|
|
|
|
<!-- BEGIN inventory trends -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Inventory Trends</h4>
|
|
<div class="panel-heading-btn">
|
|
<select class="form-select form-select-sm" id="trendPeriod" onchange="updateTrendChart()">
|
|
<option value="7">Last 7 Days</option>
|
|
<option value="30">Last 30 Days</option>
|
|
<option value="90">Last 3 Months</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="chart-container">
|
|
<canvas id="inventoryTrendChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END inventory trends -->
|
|
|
|
<!-- BEGIN component distribution -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Component Distribution</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="chart-container">
|
|
<canvas id="componentChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="chart-container">
|
|
<canvas id="bloodGroupChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END component distribution -->
|
|
</div>
|
|
<!-- END col-8 -->
|
|
|
|
<!-- BEGIN col-4 -->
|
|
<div class="col-xl-4">
|
|
<!-- BEGIN storage locations -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Storage Locations</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
{% for location in storage_locations %}
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title mb-1">{{ location.name }}</h6>
|
|
<small class="text-muted">{{ location.location_type }}</small>
|
|
</div>
|
|
<div class="text-end">
|
|
<h5 class="mb-0">{{ location.unit_count }}</h5>
|
|
<small class="text-muted">Units</small>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2">
|
|
<div class="d-flex justify-content-between">
|
|
<small>Temperature:</small>
|
|
<small class="{% if location.temperature_status == 'normal' %}temperature-normal{% elif location.temperature_status == 'warning' %}temperature-warning{% else %}temperature-critical{% endif %}">
|
|
{{ location.current_temperature }}°C
|
|
<i class="fa fa-thermometer-half"></i>
|
|
</small>
|
|
</div>
|
|
<div class="progress mt-1" style="height: 5px;">
|
|
<div class="progress-bar {% if location.capacity_percentage > 90 %}bg-danger{% elif location.capacity_percentage > 75 %}bg-warning{% else %}bg-success{% endif %}"
|
|
style="width: {{ location.capacity_percentage }}%"></div>
|
|
</div>
|
|
<small class="text-muted">{{ location.capacity_percentage }}% capacity</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<!-- END storage locations -->
|
|
|
|
<!-- BEGIN expiry alerts -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Expiry Alerts</h4>
|
|
<div class="panel-heading-btn">
|
|
<span class="badge bg-warning">{{ expiring_units }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
{% for unit in expiring_units_list %}
|
|
<div class="alert alert-warning d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>{{ unit.unit_number }}</strong><br>
|
|
<small>{{ unit.blood_group.display_name }} - {{ unit.component.get_name_display }}</small>
|
|
</div>
|
|
<div class="text-end">
|
|
<span class="badge bg-danger">{{ unit.days_to_expiry }} days</span><br>
|
|
<small class="text-muted">{{ unit.expiry_date|date:"M d" }}</small>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-3">
|
|
<i class="fa fa-check-circle fa-2x text-success mb-2"></i>
|
|
<p class="text-muted mb-0">No units expiring soon</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<!-- END expiry alerts -->
|
|
|
|
<!-- BEGIN recent activity -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Recent Activity</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="timeline">
|
|
{% for activity in recent_activities %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-time">{{ activity.timestamp|date:"H:i" }}</div>
|
|
<div class="timeline-body">
|
|
<div class="timeline-content">
|
|
<i class="fa {{ activity.icon }} text-{{ activity.color }}"></i>
|
|
{{ activity.description }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-3">
|
|
<i class="fa fa-clock fa-2x text-muted mb-2"></i>
|
|
<p class="text-muted mb-0">No recent activity</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END recent activity -->
|
|
|
|
<!-- BEGIN quick actions -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Quick Actions</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="d-grid gap-2">
|
|
<a href="{% url 'blood_bank:blood_unit_create' %}" class="btn btn-primary">
|
|
<i class="fa fa-plus"></i> Register Blood Unit
|
|
</a>
|
|
<a href="{% url 'blood_bank:blood_request_list' %}" class="btn btn-info">
|
|
<i class="fa fa-clipboard-list"></i> View Requests
|
|
</a>
|
|
<a href="{% url 'blood_bank:donor_list' %}" class="btn btn-success">
|
|
<i class="fa fa-users"></i> Manage Donors
|
|
</a>
|
|
<button type="button" class="btn btn-warning" onclick="generateInventoryReport()">
|
|
<i class="fa fa-file-pdf"></i> Generate Report
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END quick actions -->
|
|
</div>
|
|
<!-- END col-4 -->
|
|
</div>
|
|
<!-- END row -->
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'plugins/chart.js/dist/chart.js' %}"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
initializeCharts();
|
|
|
|
// Auto-refresh every 5 minutes
|
|
setInterval(function() {
|
|
if (document.visibilityState === 'visible') {
|
|
refreshInventory();
|
|
}
|
|
}, 300000);
|
|
});
|
|
|
|
function initializeCharts() {
|
|
// Inventory Trend Chart
|
|
var trendCtx = document.getElementById('inventoryTrendChart').getContext('2d');
|
|
var trendChart = new Chart(trendCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: {{ trend_labels|safe }},
|
|
datasets: [{
|
|
label: 'Total Units',
|
|
data: {{ trend_data|safe }},
|
|
borderColor: '#007bff',
|
|
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Component Distribution Chart
|
|
var componentCtx = document.getElementById('componentChart').getContext('2d');
|
|
var componentChart = new Chart(componentCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: {{ component_labels|safe }},
|
|
datasets: [{
|
|
data: {{ component_data|safe }},
|
|
backgroundColor: [
|
|
'#dc3545',
|
|
'#007bff',
|
|
'#28a745',
|
|
'#ffc107',
|
|
'#6f42c1'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'By Component'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Blood Group Distribution Chart
|
|
var bloodGroupCtx = document.getElementById('bloodGroupChart').getContext('2d');
|
|
var bloodGroupChart = new Chart(bloodGroupCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: {{ blood_group_labels|safe }},
|
|
datasets: [{
|
|
data: {{ blood_group_data|safe }},
|
|
backgroundColor: [
|
|
'#dc3545',
|
|
'#007bff',
|
|
'#28a745',
|
|
'#ffc107'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'By Blood Group'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function refreshInventory() {
|
|
// Show loading indicator
|
|
Swal.fire({
|
|
title: 'Refreshing Inventory...',
|
|
allowOutsideClick: false,
|
|
didOpen: () => {
|
|
Swal.showLoading();
|
|
}
|
|
});
|
|
|
|
// Simulate refresh (in real implementation, this would be an AJAX call)
|
|
setTimeout(function() {
|
|
Swal.close();
|
|
location.reload();
|
|
}, 2000);
|
|
}
|
|
|
|
function updateTrendChart() {
|
|
var period = document.getElementById('trendPeriod').value;
|
|
|
|
// Here you would make an AJAX call to get new data
|
|
// For now, we'll just show a message
|
|
Swal.fire({
|
|
icon: 'info',
|
|
title: 'Updating Chart',
|
|
text: `Loading data for last ${period} days...`,
|
|
timer: 1500,
|
|
showConfirmButton: false
|
|
});
|
|
}
|
|
|
|
function generateInventoryReport() {
|
|
Swal.fire({
|
|
title: 'Generate Inventory Report',
|
|
html: `
|
|
<div class="text-start">
|
|
<div class="mb-3">
|
|
<label class="form-label">Report Type</label>
|
|
<select class="form-select" id="reportType">
|
|
<option value="summary">Inventory Summary</option>
|
|
<option value="detailed">Detailed Inventory</option>
|
|
<option value="expiry">Expiry Report</option>
|
|
<option value="usage">Usage Analysis</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Date Range</label>
|
|
<select class="form-select" id="dateRange">
|
|
<option value="current">Current Inventory</option>
|
|
<option value="week">Last Week</option>
|
|
<option value="month">Last Month</option>
|
|
<option value="quarter">Last Quarter</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Format</label>
|
|
<select class="form-select" id="reportFormat">
|
|
<option value="pdf">PDF</option>
|
|
<option value="excel">Excel</option>
|
|
<option value="csv">CSV</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
`,
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Generate Report',
|
|
cancelButtonText: 'Cancel',
|
|
preConfirm: () => {
|
|
const reportType = document.getElementById('reportType').value;
|
|
const dateRange = document.getElementById('dateRange').value;
|
|
const format = document.getElementById('reportFormat').value;
|
|
|
|
return { reportType, dateRange, format };
|
|
}
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: 'Report Generated',
|
|
text: `${result.value.reportType} report in ${result.value.format} format is being prepared.`,
|
|
confirmButtonText: 'OK'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Timeline styles
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var style = document.createElement('style');
|
|
style.textContent = `
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 10px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: #e9ecef;
|
|
}
|
|
|
|
.timeline-item {
|
|
position: relative;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.timeline-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -15px;
|
|
top: 5px;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #007bff;
|
|
}
|
|
|
|
.timeline-time {
|
|
font-size: 0.8em;
|
|
color: #6c757d;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.timeline-content {
|
|
font-size: 0.9em;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|