Marwan Alwali 2780a2dc7c update
2025-09-16 15:10:57 +03:00

1366 lines
43 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}Slot Availability - Appointment Management{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/fullcalendar/main.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/select2/css/select2.min.css' %}" rel="stylesheet" />
<style>
.availability-header {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
border-radius: 0.5rem;
color: white;
margin-bottom: 2rem;
padding: 2rem;
}
.filter-panel {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 2rem;
padding: 1.5rem;
}
.filter-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.availability-grid {
display: grid;
gap: 2rem;
grid-template-columns: 1fr 300px;
margin-bottom: 2rem;
}
.calendar-container {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
padding: 1.5rem;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.stats-card {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
padding: 1.5rem;
}
.stats-header {
align-items: center;
border-bottom: 2px solid #f8f9fa;
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 1rem;
}
.stats-title {
color: #495057;
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.stats-grid {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
.stat-item {
align-items: center;
display: flex;
justify-content: space-between;
padding: 0.75rem;
border-radius: 0.375rem;
transition: all 0.3s ease;
}
.stat-item:hover {
background: #f8f9fa;
}
.stat-label {
color: #6c757d;
font-size: 0.9rem;
font-weight: 500;
}
.stat-value {
color: #495057;
font-size: 1.25rem;
font-weight: 700;
}
.stat-value.available {
color: #28a745;
}
.stat-value.booked {
color: #007bff;
}
.stat-value.blocked {
color: #dc3545;
}
.provider-list {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
max-height: 400px;
overflow-y: auto;
padding: 1rem;
}
.provider-item {
align-items: center;
border-radius: 0.375rem;
cursor: pointer;
display: flex;
gap: 1rem;
margin-bottom: 0.75rem;
padding: 1rem;
transition: all 0.3s ease;
}
.provider-item:hover {
background: #f8f9fa;
}
.provider-item.selected {
background: #e7f3ff;
border: 1px solid #007bff;
}
.provider-avatar {
align-items: center;
background: #007bff;
border-radius: 50%;
color: white;
display: flex;
font-size: 0.9rem;
font-weight: 600;
height: 40px;
justify-content: center;
width: 40px;
}
.provider-info {
flex: 1;
}
.provider-name {
color: #495057;
font-size: 0.95rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.provider-specialty {
color: #6c757d;
font-size: 0.8rem;
}
.availability-indicator {
align-items: center;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.availability-count {
color: #495057;
font-size: 0.8rem;
font-weight: 600;
}
.availability-bar {
background: #e9ecef;
border-radius: 2px;
height: 4px;
overflow: hidden;
width: 40px;
}
.availability-fill {
height: 100%;
transition: width 0.3s ease;
}
.availability-fill.high {
background: #28a745;
}
.availability-fill.medium {
background: #ffc107;
}
.availability-fill.low {
background: #dc3545;
}
.time-slots-view {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 2rem;
padding: 1.5rem;
}
.time-grid {
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.time-slot {
align-items: center;
border: 2px solid #e9ecef;
border-radius: 0.375rem;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem;
text-align: center;
transition: all 0.3s ease;
}
.time-slot:hover {
border-color: #007bff;
transform: translateY(-1px);
}
.time-slot.available {
background: #d4edda;
border-color: #28a745;
color: #155724;
}
.time-slot.booked {
background: #cce5ff;
border-color: #007bff;
color: #004085;
}
.time-slot.blocked {
background: #f8d7da;
border-color: #dc3545;
color: #721c24;
}
.time-slot.partially-booked {
background: #fff3cd;
border-color: #ffc107;
color: #856404;
}
.slot-time {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
font-weight: 600;
}
.slot-status {
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
}
.slot-count {
font-size: 0.7rem;
opacity: 0.8;
}
.legend {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: center;
margin-bottom: 1.5rem;
}
.legend-item {
align-items: center;
display: flex;
gap: 0.5rem;
}
.legend-color {
border-radius: 0.25rem;
height: 16px;
width: 16px;
}
.legend-color.available {
background: #d4edda;
border: 1px solid #28a745;
}
.legend-color.booked {
background: #cce5ff;
border: 1px solid #007bff;
}
.legend-color.blocked {
background: #f8d7da;
border: 1px solid #dc3545;
}
.legend-color.partially-booked {
background: #fff3cd;
border: 1px solid #ffc107;
}
.legend-label {
color: #495057;
font-size: 0.85rem;
font-weight: 500;
}
.view-toggle {
align-items: center;
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.btn-toggle {
background: #e9ecef;
border: 1px solid #dee2e6;
color: #495057;
padding: 0.5rem 1rem;
transition: all 0.3s ease;
}
.btn-toggle.active {
background: #007bff;
border-color: #007bff;
color: white;
}
.quick-actions {
background: #f8f9fa;
border-radius: 0.375rem;
margin-bottom: 1.5rem;
padding: 1rem;
}
.action-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.btn-action {
align-items: center;
display: flex;
font-size: 0.85rem;
gap: 0.5rem;
padding: 0.5rem 1rem;
}
.utilization-chart {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 2rem;
padding: 1.5rem;
}
.chart-container {
height: 300px;
position: relative;
}
@media (max-width: 768px) {
.availability-header {
padding: 1.5rem;
text-align: center;
}
.availability-grid {
grid-template-columns: 1fr;
}
.filter-grid {
grid-template-columns: 1fr;
}
.time-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
.legend {
flex-direction: column;
gap: 1rem;
}
.view-toggle {
flex-direction: column;
align-items: stretch;
}
.action-buttons {
flex-direction: column;
}
}
.fade-in {
animation: fadeIn 0.6s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.loading-overlay {
align-items: center;
background: rgba(255, 255, 255, 0.9);
display: none;
height: 100%;
justify-content: center;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: 1000;
}
.loading-spinner {
align-items: center;
display: flex;
flex-direction: column;
gap: 1rem;
}
.spinner {
animation: spin 1s linear infinite;
border: 3px solid #f3f3f3;
border-radius: 50%;
border-top: 3px solid #007bff;
height: 40px;
width: 40px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-state {
align-items: center;
color: #6c757d;
display: flex;
flex-direction: column;
gap: 1rem;
padding: 3rem;
text-align: center;
}
.empty-icon {
font-size: 4rem;
opacity: 0.5;
}
.refresh-indicator {
align-items: center;
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 0.375rem;
color: #0066cc;
display: none;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.75rem;
}
.auto-refresh-toggle {
align-items: center;
display: flex;
gap: 0.5rem;
margin-left: auto;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="availability-header fade-in">
<div class="row align-items-center">
<div class="col-lg-8">
<h1 class="mb-2">
<i class="fas fa-calendar-check me-3"></i>
Slot Availability Overview
</h1>
<p class="mb-3 opacity-90">
Real-time view of appointment slot availability across all providers and departments
</p>
<div class="d-flex flex-wrap gap-3">
<div class="d-flex align-items-center gap-2">
<i class="fas fa-calendar"></i>
<span id="currentDate">{{ today|date:"F d, Y" }}</span>
</div>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-clock"></i>
<span id="currentTime">{{ now|time:"g:i A" }}</span>
</div>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-users"></i>
<span>{{ total_providers }} Providers</span>
</div>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-calendar-alt"></i>
<span>{{ total_slots }} Total Slots</span>
</div>
</div>
</div>
<div class="col-lg-4 text-lg-end">
<div class="d-flex flex-column gap-2">
<div class="auto-refresh-toggle">
<label class="form-check-label" for="autoRefresh">Auto Refresh</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="autoRefresh" checked>
</div>
</div>
<button class="btn btn-light" onclick="refreshData()">
<i class="fas fa-sync-alt me-2"></i>Refresh Now
</button>
</div>
</div>
</div>
</div>
<!-- Refresh Indicator -->
<div class="refresh-indicator" id="refreshIndicator">
<i class="fas fa-sync-alt fa-spin"></i>
<span>Updating availability data...</span>
</div>
<!-- Filter Panel -->
<div class="filter-panel fade-in">
<div class="row align-items-end">
<div class="col-lg-10">
<div class="filter-grid">
<div class="form-group">
<label class="form-label">Date Range</label>
<select class="form-select" id="dateRange">
<option value="today">Today</option>
<option value="tomorrow">Tomorrow</option>
<option value="week" selected>This Week</option>
<option value="month">This Month</option>
<option value="custom">Custom Range</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Department</label>
<select class="form-select" id="departmentFilter">
<option value="">All Departments</option>
{% for department in departments %}
<option value="{{ department.id }}">{{ department.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Provider</label>
<select class="form-select" id="providerFilter">
<option value="">All Providers</option>
{% for provider in providers %}
<option value="{{ provider.id }}">{{ provider.get_full_name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Appointment Type</label>
<select class="form-select" id="typeFilter">
<option value="">All Types</option>
<option value="consultation">Consultation</option>
<option value="follow_up">Follow-up</option>
<option value="procedure">Procedure</option>
<option value="emergency">Emergency</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select class="form-select" id="statusFilter">
<option value="">All Statuses</option>
<option value="available">Available</option>
<option value="booked">Booked</option>
<option value="blocked">Blocked</option>
</select>
</div>
</div>
</div>
<div class="col-lg-2">
<button class="btn btn-primary w-100" onclick="applyFilters()">
<i class="fas fa-filter me-2"></i>Apply Filters
</button>
</div>
</div>
</div>
<!-- View Toggle -->
<div class="view-toggle fade-in">
<label class="form-label me-3">View:</label>
<button class="btn btn-toggle active" data-view="calendar">
<i class="fas fa-calendar me-1"></i>Calendar
</button>
<button class="btn btn-toggle" data-view="grid">
<i class="fas fa-th me-1"></i>Grid
</button>
<button class="btn btn-toggle" data-view="list">
<i class="fas fa-list me-1"></i>List
</button>
</div>
<!-- Main Content -->
<div class="availability-grid fade-in">
<!-- Calendar/Grid View -->
<div class="main-view">
<!-- Calendar View -->
<div class="calendar-container" id="calendarView">
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner">
<div class="spinner"></div>
<span>Loading availability data...</span>
</div>
</div>
<div id="calendar"></div>
</div>
<!-- Grid View -->
<div class="time-slots-view" id="gridView" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
<i class="fas fa-th me-2"></i>
Time Slots Grid
</h5>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-primary" onclick="previousDay()">
<i class="fas fa-chevron-left"></i>
</button>
<span id="gridDate" class="align-self-center px-3">{{ today|date:"M d, Y" }}</span>
<button class="btn btn-sm btn-outline-primary" onclick="nextDay()">
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<!-- Legend -->
<div class="legend">
<div class="legend-item">
<div class="legend-color available"></div>
<span class="legend-label">Available</span>
</div>
<div class="legend-item">
<div class="legend-color booked"></div>
<span class="legend-label">Booked</span>
</div>
<div class="legend-item">
<div class="legend-color blocked"></div>
<span class="legend-label">Blocked</span>
</div>
<div class="legend-item">
<div class="legend-color partially-booked"></div>
<span class="legend-label">Partially Booked</span>
</div>
</div>
<div class="time-grid" id="timeGrid">
<!-- Time slots will be populated by JavaScript -->
</div>
</div>
<!-- List View -->
<div class="time-slots-view" id="listView" style="display: none;">
<h5 class="mb-3">
<i class="fas fa-list me-2"></i>
Detailed Slot List
</h5>
<div class="table-responsive">
<table class="table table-hover" id="slotsTable">
<thead>
<tr>
<th>Date</th>
<th>Time</th>
<th>Provider</th>
<th>Type</th>
<th>Status</th>
<th>Patient</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Table rows will be populated by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- Statistics Card -->
<div class="stats-card">
<div class="stats-header">
<h5 class="stats-title">
<i class="fas fa-chart-pie text-primary me-2"></i>
Availability Stats
</h5>
<button class="btn btn-sm btn-outline-secondary" onclick="refreshStats()">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Available Slots</span>
<span class="stat-value available" id="availableCount">{{ stats.available|default:0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Booked Slots</span>
<span class="stat-value booked" id="bookedCount">{{ stats.booked|default:0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Blocked Slots</span>
<span class="stat-value blocked" id="blockedCount">{{ stats.blocked|default:0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Utilization Rate</span>
<span class="stat-value" id="utilizationRate">{{ stats.utilization|default:0 }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">No-Show Rate</span>
<span class="stat-value" id="noShowRate">{{ stats.no_show_rate|default:0 }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">Avg. Duration</span>
<span class="stat-value" id="avgDuration">{{ stats.avg_duration|default:30 }} min</span>
</div>
</div>
</div>
<!-- Provider List -->
<div class="stats-card">
<div class="stats-header">
<h5 class="stats-title">
<i class="fas fa-user-md text-success me-2"></i>
Providers
</h5>
<span class="badge bg-secondary">{{ providers|length }}</span>
</div>
<div class="provider-list">
{% for provider in providers %}
<div class="provider-item" data-provider-id="{{ provider.id }}" onclick="selectProvider({{ provider.id }})">
{% if provider.profile_picture %}
<img src="{{ provider.profile_picture.url }}"
class="provider-avatar" alt="{{ provider.get_full_name }}">
{% else %}
<div class="provider-avatar">
{{ provider.first_name.0 }}{{ provider.last_name.0 }}
</div>
{% endif %}
<div class="provider-info">
<div class="provider-name">{{ provider.get_full_name }}</div>
<div class="provider-specialty">{{ provider.specialty|default:"General Practice" }}</div>
</div>
<div class="availability-indicator">
<div class="availability-count">{{ provider.available_slots|default:0 }}</div>
<div class="availability-bar">
<div class="availability-fill {{ provider.availability_level|default:'low' }}"
style="width: {{ provider.availability_percentage|default:0 }}%"></div>
</div>
</div>
</div>
{% empty %}
<div class="empty-state">
<i class="fas fa-user-md empty-icon"></i>
<h6>No Providers Found</h6>
<p>No providers match the current filters</p>
</div>
{% endfor %}
</div>
</div>
<!-- Quick Actions -->
<div class="stats-card">
<div class="stats-header">
<h5 class="stats-title">
<i class="fas fa-bolt text-warning me-2"></i>
Quick Actions
</h5>
</div>
<div class="action-buttons">
<a href="{% url 'appointments:slot_create' %}" class="btn btn-primary btn-action">
<i class="fas fa-plus"></i>Create Slot
</a>
<button class="btn btn-success btn-action" onclick="bulkCreateSlots()">
<i class="fas fa-layer-group"></i>Bulk Create
</button>
<button class="btn btn-info btn-action" onclick="exportAvailability()">
<i class="fas fa-download"></i>Export
</button>
<button class="btn btn-warning btn-action" onclick="manageBlocks()">
<i class="fas fa-ban"></i>Manage Blocks
</button>
<a href="{% url 'appointments:slot_list' %}" class="btn btn-secondary btn-action">
<i class="fas fa-list"></i>All Slots
</a>
</div>
</div>
</div>
</div>
<!-- Utilization Chart -->
<div class="utilization-chart fade-in">
<h5 class="mb-3">
<i class="fas fa-chart-line text-info me-2"></i>
Utilization Trends
</h5>
<div class="chart-container">
<canvas id="utilizationChart"></canvas>
</div>
</div>
</div>
<!-- Bulk Create Modal -->
<div class="modal fade" id="bulkCreateModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-layer-group me-2"></i>Bulk Create Slots
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="bulkCreateForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Provider *</label>
<select class="form-select" id="bulkProvider" required>
<option value="">Select Provider</option>
{% for provider in providers %}
<option value="{{ provider.id }}">{{ provider.get_full_name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Date Range *</label>
<div class="input-group">
<input type="date" class="form-control" id="bulkStartDate" required>
<span class="input-group-text">to</span>
<input type="date" class="form-control" id="bulkEndDate" required>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Start Time *</label>
<input type="time" class="form-control" id="bulkStartTime" value="09:00" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">End Time *</label>
<input type="time" class="form-control" id="bulkEndTime" value="17:00" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Slot Duration (minutes) *</label>
<select class="form-select" id="bulkDuration" required>
<option value="15">15 minutes</option>
<option value="30" selected>30 minutes</option>
<option value="45">45 minutes</option>
<option value="60">60 minutes</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Days of Week</label>
<div class="d-flex flex-wrap gap-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="1" id="mon" checked>
<label class="form-check-label" for="mon">Mon</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="2" id="tue" checked>
<label class="form-check-label" for="tue">Tue</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="3" id="wed" checked>
<label class="form-check-label" for="wed">Wed</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="4" id="thu" checked>
<label class="form-check-label" for="thu">Thu</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="5" id="fri" checked>
<label class="form-check-label" for="fri">Fri</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="6" id="sat">
<label class="form-check-label" for="sat">Sat</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="0" id="sun">
<label class="form-check-label" for="sun">Sun</label>
</div>
</div>
</div>
</div>
</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-success" onclick="createBulkSlots()">
<i class="fas fa-check me-1"></i>Create Slots
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/fullcalendar/main.min.js' %}"></script>
<script src="{% static 'assets/plugins/select2/js/select2.min.js' %}"></script>
<script src="{% static 'assets/plugins/chart.js/chart.min.js' %}"></script>
<script>
let calendar;
let autoRefreshInterval;
let currentView = 'calendar';
let selectedProvider = null;
$(document).ready(function() {
initializeCalendar();
initializeChart();
setupEventListeners();
startAutoRefresh();
// Initialize Select2
$('#providerFilter, #departmentFilter').select2({
placeholder: 'Select...',
allowClear: true
});
});
function initializeCalendar() {
const calendarEl = document.getElementById('calendar');
calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridWeek',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
events: '/api/slots/calendar-events/',
eventClick: function(info) {
window.location.href = `/appointments/slots/${info.event.id}/`;
},
eventClassNames: function(arg) {
return ['slot-' + arg.event.extendedProps.status];
},
loading: function(bool) {
$('#loadingOverlay').toggle(bool);
}
});
calendar.render();
}
function initializeChart() {
const ctx = document.getElementById('utilizationChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
datasets: [{
label: 'Utilization %',
data: [75, 82, 68, 90, 85, 45, 30],
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
function setupEventListeners() {
// View toggle
$('.btn-toggle').on('click', function() {
const view = $(this).data('view');
switchView(view);
});
// Auto refresh toggle
$('#autoRefresh').on('change', function() {
if ($(this).is(':checked')) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
});
// Filter changes
$('#dateRange, #departmentFilter, #providerFilter, #typeFilter, #statusFilter').on('change', function() {
applyFilters();
});
}
function switchView(view) {
currentView = view;
// Update toggle buttons
$('.btn-toggle').removeClass('active');
$(`.btn-toggle[data-view="${view}"]`).addClass('active');
// Show/hide views
$('#calendarView, #gridView, #listView').hide();
$(`#${view}View`).show();
// Load data for the view
switch(view) {
case 'calendar':
calendar.render();
break;
case 'grid':
loadGridView();
break;
case 'list':
loadListView();
break;
}
}
function loadGridView() {
const date = $('#gridDate').text();
$.ajax({
url: '/api/slots/grid-data/',
data: { date: date },
success: function(data) {
renderTimeGrid(data.slots);
}
});
}
function renderTimeGrid(slots) {
const grid = $('#timeGrid');
grid.empty();
if (slots.length === 0) {
grid.append(`
<div class="empty-state">
<i class="fas fa-calendar-times empty-icon"></i>
<h6>No Slots Found</h6>
<p>No slots available for the selected date and filters</p>
</div>
`);
return;
}
slots.forEach(slot => {
const slotEl = $(`
<div class="time-slot ${slot.status}" onclick="viewSlot(${slot.id})">
<div class="slot-time">${slot.start_time} - ${slot.end_time}</div>
<div class="slot-status">${slot.status}</div>
${slot.patient ? `<div class="slot-count">${slot.patient}</div>` : ''}
</div>
`);
grid.append(slotEl);
});
}
function loadListView() {
$.ajax({
url: '/api/slots/list-data/',
data: getFilterData(),
success: function(data) {
renderSlotTable(data.slots);
}
});
}
function renderSlotTable(slots) {
const tbody = $('#slotsTable tbody');
tbody.empty();
if (slots.length === 0) {
tbody.append(`
<tr>
<td colspan="7" class="text-center py-4">
<i class="fas fa-calendar-times text-muted fa-2x mb-2"></i>
<br>No slots found matching the current filters
</td>
</tr>
`);
return;
}
slots.forEach(slot => {
const row = $(`
<tr>
<td>${slot.date}</td>
<td>${slot.start_time} - ${slot.end_time}</td>
<td>${slot.provider}</td>
<td>${slot.appointment_type}</td>
<td><span class="badge bg-${getStatusColor(slot.status)}">${slot.status}</span></td>
<td>${slot.patient || '-'}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="/appointments/slots/${slot.id}/" class="btn btn-outline-primary">
<i class="fas fa-eye"></i>
</a>
<a href="/appointments/slots/${slot.id}/edit/" class="btn btn-outline-secondary">
<i class="fas fa-edit"></i>
</a>
</div>
</td>
</tr>
`);
tbody.append(row);
});
}
function getStatusColor(status) {
switch(status.toLowerCase()) {
case 'available': return 'success';
case 'booked': return 'primary';
case 'blocked': return 'danger';
default: return 'secondary';
}
}
function applyFilters() {
const filterData = getFilterData();
// Update calendar
if (currentView === 'calendar') {
calendar.refetchEvents();
}
// Update other views
if (currentView === 'grid') {
loadGridView();
} else if (currentView === 'list') {
loadListView();
}
// Update stats
refreshStats();
}
function getFilterData() {
return {
date_range: $('#dateRange').val(),
department: $('#departmentFilter').val(),
provider: $('#providerFilter').val(),
appointment_type: $('#typeFilter').val(),
status: $('#statusFilter').val()
};
}
function refreshData() {
$('#refreshIndicator').show();
// Refresh current view
switch(currentView) {
case 'calendar':
calendar.refetchEvents();
break;
case 'grid':
loadGridView();
break;
case 'list':
loadListView();
break;
}
// Refresh stats
refreshStats();
setTimeout(() => {
$('#refreshIndicator').hide();
}, 1000);
}
function refreshStats() {
$.ajax({
url: '/api/slots/stats/',
data: getFilterData(),
success: function(data) {
$('#availableCount').text(data.available);
$('#bookedCount').text(data.booked);
$('#blockedCount').text(data.blocked);
$('#utilizationRate').text(data.utilization + '%');
$('#noShowRate').text(data.no_show_rate + '%');
$('#avgDuration').text(data.avg_duration + ' min');
}
});
}
function startAutoRefresh() {
stopAutoRefresh();
autoRefreshInterval = setInterval(refreshData, 30000); // 30 seconds
}
function stopAutoRefresh() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
}
function selectProvider(providerId) {
selectedProvider = providerId;
// Update visual selection
$('.provider-item').removeClass('selected');
$(`.provider-item[data-provider-id="${providerId}"]`).addClass('selected');
// Update filter
$('#providerFilter').val(providerId).trigger('change');
}
function previousDay() {
const currentDate = new Date($('#gridDate').text());
currentDate.setDate(currentDate.getDate() - 1);
$('#gridDate').text(currentDate.toLocaleDateString());
loadGridView();
}
function nextDay() {
const currentDate = new Date($('#gridDate').text());
currentDate.setDate(currentDate.getDate() + 1);
$('#gridDate').text(currentDate.toLocaleDateString());
loadGridView();
}
function viewSlot(slotId) {
window.location.href = `/appointments/slots/${slotId}/`;
}
function bulkCreateSlots() {
$('#bulkCreateModal').modal('show');
}
function createBulkSlots() {
const formData = {
provider: $('#bulkProvider').val(),
start_date: $('#bulkStartDate').val(),
end_date: $('#bulkEndDate').val(),
start_time: $('#bulkStartTime').val(),
end_time: $('#bulkEndTime').val(),
duration: $('#bulkDuration').val(),
days: []
};
// Get selected days
$('input[type="checkbox"]:checked').each(function() {
formData.days.push($(this).val());
});
if (!formData.provider || !formData.start_date || !formData.end_date) {
alert('Please fill in all required fields');
return;
}
$.ajax({
url: '/api/slots/bulk-create/',
method: 'POST',
data: formData,
headers: {
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
},
success: function(response) {
$('#bulkCreateModal').modal('hide');
showAlert(`Created ${response.created_count} slots successfully!`, 'success');
refreshData();
},
error: function() {
showAlert('Error creating slots', 'error');
}
});
}
function exportAvailability() {
const filterData = getFilterData();
const params = new URLSearchParams(filterData);
window.open(`/api/slots/export/?${params}`, '_blank');
}
function manageBlocks() {
window.location.href = '/appointments/blocks/';
}
function showAlert(message, type) {
const alertClass = type === 'success' ? 'alert-success' :
type === 'warning' ? 'alert-warning' :
type === 'info' ? 'alert-info' : 'alert-danger';
const alertHtml = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
$('.container-fluid').prepend(alertHtml);
setTimeout(() => $('.alert').fadeOut(), 5000);
}
// Update current time every minute
setInterval(function() {
const now = new Date();
$('#currentTime').text(now.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}));
}, 60000);
</script>
{% endblock %}