997 lines
32 KiB
HTML
997 lines
32 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Slot Details - {{ slot.provider.get_full_name }} - {{ slot.date|date:"M d, Y" }}{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.slot-detail-header {
|
|
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
|
border-radius: 0.5rem;
|
|
color: white;
|
|
margin-bottom: 2rem;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.slot-info-grid {
|
|
display: grid;
|
|
gap: 2rem;
|
|
grid-template-columns: 2fr 1fr;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.info-card {
|
|
background: white;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.info-card-header {
|
|
align-items: center;
|
|
border-bottom: 2px solid #f8f9fa;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
}
|
|
|
|
.info-card-title {
|
|
color: #495057;
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
}
|
|
|
|
.info-grid {
|
|
display: grid;
|
|
gap: 1.5rem;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
}
|
|
|
|
.info-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.info-label {
|
|
color: #6c757d;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.info-value {
|
|
color: #495057;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-badge {
|
|
align-items: center;
|
|
border-radius: 1rem;
|
|
display: inline-flex;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
gap: 0.25rem;
|
|
padding: 0.5rem 1rem;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-badge.available {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.status-badge.booked {
|
|
background: #cce5ff;
|
|
color: #004085;
|
|
}
|
|
|
|
.status-badge.blocked {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.status-badge.expired {
|
|
background: #e2e3e5;
|
|
color: #383d41;
|
|
}
|
|
|
|
.provider-profile {
|
|
align-items: center;
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.provider-avatar {
|
|
align-items: center;
|
|
background: #007bff;
|
|
border-radius: 50%;
|
|
color: white;
|
|
display: flex;
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
height: 80px;
|
|
justify-content: center;
|
|
width: 80px;
|
|
}
|
|
|
|
.provider-details {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.provider-name {
|
|
color: #495057;
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.provider-specialty {
|
|
color: #6c757d;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.provider-contact {
|
|
color: #6c757d;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.appointment-details {
|
|
background: #f8f9fa;
|
|
border-left: 4px solid #007bff;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 2rem;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.appointment-patient {
|
|
align-items: center;
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.patient-avatar {
|
|
align-items: center;
|
|
background: #28a745;
|
|
border-radius: 50%;
|
|
color: white;
|
|
display: flex;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
height: 50px;
|
|
justify-content: center;
|
|
width: 50px;
|
|
}
|
|
|
|
.patient-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.patient-name {
|
|
color: #495057;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.patient-mrn {
|
|
color: #6c757d;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.timeline-section {
|
|
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;
|
|
}
|
|
|
|
.timeline {
|
|
position: relative;
|
|
}
|
|
|
|
.timeline::before {
|
|
background: #dee2e6;
|
|
content: '';
|
|
height: 100%;
|
|
left: 20px;
|
|
position: absolute;
|
|
top: 0;
|
|
width: 2px;
|
|
}
|
|
|
|
.timeline-item {
|
|
margin-bottom: 2rem;
|
|
padding-left: 3rem;
|
|
position: relative;
|
|
}
|
|
|
|
.timeline-item::before {
|
|
align-items: center;
|
|
background: white;
|
|
border: 3px solid #007bff;
|
|
border-radius: 50%;
|
|
color: #007bff;
|
|
content: attr(data-icon);
|
|
display: flex;
|
|
font-family: 'Font Awesome 5 Free';
|
|
font-size: 0.8rem;
|
|
font-weight: 900;
|
|
height: 40px;
|
|
justify-content: center;
|
|
left: 0;
|
|
position: absolute;
|
|
top: 0;
|
|
width: 40px;
|
|
}
|
|
|
|
.timeline-time {
|
|
color: #6c757d;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.timeline-content {
|
|
color: #495057;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.btn-action {
|
|
align-items: center;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.5rem;
|
|
}
|
|
|
|
.recurring-info {
|
|
background: #e7f3ff;
|
|
border: 1px solid #b3d9ff;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 1.5rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.recurring-pattern {
|
|
color: #0066cc;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.recurring-details {
|
|
color: #004080;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.notes-section {
|
|
background: #fff8e1;
|
|
border: 1px solid #ffecb3;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 1.5rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.notes-title {
|
|
color: #f57f17;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.notes-content {
|
|
color: #f57f17;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.conflict-warning {
|
|
background: #ffebee;
|
|
border: 1px solid #ffcdd2;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 1.5rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.conflict-title {
|
|
color: #c62828;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.conflict-list {
|
|
color: #c62828;
|
|
margin: 0;
|
|
padding-left: 1.5rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.slot-info-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.slot-detail-header {
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.action-buttons {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.btn-action {
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.info-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.fade-in {
|
|
animation: fadeIn 0.6s ease-in;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(20px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.slot-time-display {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
color: #495057;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
padding: 0.75rem 1rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.duration-badge {
|
|
background: #e9ecef;
|
|
border-radius: 0.25rem;
|
|
color: #495057;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
padding: 0.25rem 0.75rem;
|
|
}
|
|
|
|
.availability-indicator {
|
|
align-items: center;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.availability-dot {
|
|
border-radius: 50%;
|
|
height: 12px;
|
|
width: 12px;
|
|
}
|
|
|
|
.availability-dot.available { background: #28a745; }
|
|
.availability-dot.booked { background: #007bff; }
|
|
.availability-dot.blocked { background: #dc3545; }
|
|
.availability-dot.expired { background: #6c757d; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="slot-detail-header fade-in">
|
|
<div class="row align-items-center">
|
|
<div class="col-lg-8">
|
|
<h1 class="mb-2">
|
|
<i class="fas fa-clock me-3"></i>
|
|
Appointment Slot Details
|
|
</h1>
|
|
<p class="mb-3 opacity-90">
|
|
Detailed information and management for appointment slot
|
|
</p>
|
|
<div class="d-flex flex-wrap gap-3">
|
|
<div class="availability-indicator">
|
|
<div class="availability-dot {{ slot.status|lower }}"></div>
|
|
<span class="status-badge {{ slot.status|lower }}">
|
|
<i class="fas fa-circle"></i>
|
|
{{ slot.get_status_display }}
|
|
</span>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<i class="fas fa-calendar"></i>
|
|
<span>{{ slot.date|date:"F d, Y" }}</span>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<i class="fas fa-clock"></i>
|
|
<span>{{ slot.start_time|time:"g:i A" }} - {{ slot.end_time|time:"g:i A" }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-4 text-lg-end">
|
|
<div class="d-flex flex-column gap-2">
|
|
{% if slot.status == 'AVAILABLE' %}
|
|
<button class="btn btn-light btn-lg" onclick="bookSlot()">
|
|
<i class="fas fa-calendar-plus me-2"></i>Book Appointment
|
|
</button>
|
|
{% elif slot.status == 'BOOKED' %}
|
|
<button class="btn btn-outline-light" onclick="viewAppointment()">
|
|
<i class="fas fa-eye me-2"></i>View Appointment
|
|
</button>
|
|
{% endif %}
|
|
<a href="{% url 'appointments:slot_update' slot.pk %}" class="btn btn-outline-light">
|
|
<i class="fas fa-edit me-2"></i>Edit Slot
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content Grid -->
|
|
<div class="slot-info-grid fade-in">
|
|
<!-- Left Column - Main Information -->
|
|
<div>
|
|
<!-- Slot Information -->
|
|
<div class="info-card">
|
|
<div class="info-card-header">
|
|
<h4 class="info-card-title">
|
|
<i class="fas fa-info-circle text-primary me-2"></i>
|
|
Slot Information
|
|
</h4>
|
|
<div class="dropdown">
|
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="{% url 'appointments:slot_update' slot.pk %}">
|
|
<i class="fas fa-edit me-2"></i>Edit Slot
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="duplicateSlot()">
|
|
<i class="fas fa-copy me-2"></i>Duplicate Slot
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item text-danger" href="{% url 'appointments:slot_delete' slot.pk %}">
|
|
<i class="fas fa-trash me-2"></i>Delete Slot
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-label">Date</div>
|
|
<div class="info-value">{{ slot.date|date:"F d, Y" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Time</div>
|
|
<div class="slot-time-display">
|
|
{{ slot.start_time|time:"g:i A" }} - {{ slot.end_time|time:"g:i A" }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Duration</div>
|
|
<div class="info-value">
|
|
<span class="duration-badge">{{ slot.duration_minutes }} minutes</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Appointment Type</div>
|
|
<div class="info-value">
|
|
<span class="badge bg-light text-dark">{{ slot.appointment_type|default:"General" }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Created</div>
|
|
<div class="info-value">{{ slot.created_at|date:"M d, Y g:i A" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Last Modified</div>
|
|
<div class="info-value">{{ slot.updated_at|date:"M d, Y g:i A" }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Provider Information -->
|
|
<div class="info-card">
|
|
<div class="info-card-header">
|
|
<h4 class="info-card-title">
|
|
<i class="fas fa-user-md text-success me-2"></i>
|
|
Provider Information
|
|
</h4>
|
|
<a href="{% url 'hr:employee_detail' slot.provider.pk %}" class="btn btn-sm btn-outline-primary">
|
|
<i class="fas fa-external-link-alt me-1"></i>View Profile
|
|
</a>
|
|
</div>
|
|
|
|
<div class="provider-profile">
|
|
{% if slot.provider.profile_picture %}
|
|
<img src="{{ slot.provider.profile_picture.url }}"
|
|
class="provider-avatar" alt="{{ slot.provider.get_full_name }}">
|
|
{% else %}
|
|
<div class="provider-avatar">
|
|
{{ slot.provider.first_name.0 }}{{ slot.provider.last_name.0 }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="provider-details">
|
|
<div class="provider-name">{{ slot.provider.get_full_name }}</div>
|
|
<div class="provider-specialty">{{ slot.provider.specialty|default:"General Practice" }}</div>
|
|
<div class="provider-contact">{{ slot.provider.email }}</div>
|
|
{% if slot.provider.phone %}
|
|
<div class="provider-contact">{{ slot.provider.phone }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-label">Department</div>
|
|
<div class="info-value">{{ slot.provider.department|default:"Not assigned" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">License Number</div>
|
|
<div class="info-value">{{ slot.provider.license_number|default:"N/A" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Years of Experience</div>
|
|
<div class="info-value">{{ slot.provider.years_experience|default:"N/A" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Languages</div>
|
|
<div class="info-value">{{ slot.provider.languages|default:"English" }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Appointment Details (if booked) -->
|
|
{% if slot.appointment %}
|
|
<div class="appointment-details">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-calendar-check text-primary me-2"></i>
|
|
Appointment Details
|
|
</h5>
|
|
|
|
<div class="appointment-patient">
|
|
{% if slot.appointment.patient.profile_picture %}
|
|
<img src="{{ slot.appointment.patient.profile_picture.url }}"
|
|
class="patient-avatar" alt="{{ slot.appointment.patient.get_full_name }}">
|
|
{% else %}
|
|
<div class="patient-avatar">
|
|
{{ slot.appointment.patient.first_name.0 }}{{ slot.appointment.patient.last_name.0 }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="patient-info">
|
|
<div class="patient-name">{{ slot.appointment.patient.get_full_name }}</div>
|
|
<div class="patient-mrn">MRN: {{ slot.appointment.patient.mrn }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-label">Appointment Type</div>
|
|
<div class="info-value">{{ slot.appointment.appointment_type|default:"Consultation" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Status</div>
|
|
<div class="info-value">
|
|
<span class="badge bg-primary">{{ slot.appointment.get_status_display }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Booked On</div>
|
|
<div class="info-value">{{ slot.appointment.created_at|date:"M d, Y g:i A" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Booked By</div>
|
|
<div class="info-value">{{ slot.appointment.created_by.get_full_name|default:"System" }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if slot.appointment.notes %}
|
|
<div class="mt-3">
|
|
<div class="info-label">Appointment Notes</div>
|
|
<div class="info-value">{{ slot.appointment.notes }}</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Recurring Information -->
|
|
{% if slot.is_recurring %}
|
|
<div class="recurring-info">
|
|
<div class="recurring-pattern">
|
|
<i class="fas fa-repeat me-2"></i>
|
|
Recurring Appointment Slot
|
|
</div>
|
|
<div class="recurring-details">
|
|
<strong>Pattern:</strong> {{ slot.recurrence_pattern|default:"Weekly" }}<br>
|
|
<strong>Frequency:</strong> Every {{ slot.recurrence_interval|default:1 }} {{ slot.recurrence_unit|default:"week" }}(s)<br>
|
|
{% if slot.recurrence_end_date %}
|
|
<strong>Ends:</strong> {{ slot.recurrence_end_date|date:"F d, Y" }}
|
|
{% else %}
|
|
<strong>Ends:</strong> No end date
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Notes Section -->
|
|
{% if slot.notes %}
|
|
<div class="notes-section">
|
|
<div class="notes-title">
|
|
<i class="fas fa-sticky-note me-2"></i>
|
|
Slot Notes
|
|
</div>
|
|
<div class="notes-content">{{ slot.notes }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Conflict Warnings -->
|
|
{% if slot.conflicts %}
|
|
<div class="conflict-warning">
|
|
<div class="conflict-title">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
Schedule Conflicts Detected
|
|
</div>
|
|
<ul class="conflict-list">
|
|
{% for conflict in slot.conflicts %}
|
|
<li>{{ conflict.description }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Right Column - Timeline and Actions -->
|
|
<div>
|
|
<!-- Activity Timeline -->
|
|
<div class="timeline-section">
|
|
<div class="info-card-header">
|
|
<h4 class="info-card-title">
|
|
<i class="fas fa-history text-info me-2"></i>
|
|
Activity Timeline
|
|
</h4>
|
|
</div>
|
|
|
|
<div class="timeline">
|
|
<div class="timeline-item" data-icon="">
|
|
<div class="timeline-time">{{ slot.created_at|date:"M d, Y g:i A" }}</div>
|
|
<div class="timeline-content">
|
|
<strong>Slot Created</strong><br>
|
|
Created by {{ slot.created_by.get_full_name|default:"System" }}
|
|
</div>
|
|
</div>
|
|
|
|
{% if slot.appointment %}
|
|
<div class="timeline-item" data-icon="">
|
|
<div class="timeline-time">{{ slot.appointment.created_at|date:"M d, Y g:i A" }}</div>
|
|
<div class="timeline-content">
|
|
<strong>Appointment Booked</strong><br>
|
|
Booked for {{ slot.appointment.patient.get_full_name }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if slot.updated_at != slot.created_at %}
|
|
<div class="timeline-item" data-icon="">
|
|
<div class="timeline-time">{{ slot.updated_at|date:"M d, Y g:i A" }}</div>
|
|
<div class="timeline-content">
|
|
<strong>Slot Modified</strong><br>
|
|
Last updated by {{ slot.updated_by.get_full_name|default:"System" }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% for activity in slot.activity_log %}
|
|
<div class="timeline-item" data-icon="{{ activity.icon }}">
|
|
<div class="timeline-time">{{ activity.timestamp|date:"M d, Y g:i A" }}</div>
|
|
<div class="timeline-content">
|
|
<strong>{{ activity.title }}</strong><br>
|
|
{{ activity.description }}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="info-card">
|
|
<div class="info-card-header">
|
|
<h4 class="info-card-title">
|
|
<i class="fas fa-chart-bar text-warning me-2"></i>
|
|
Quick Stats
|
|
</h4>
|
|
</div>
|
|
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-label">Provider's Slots Today</div>
|
|
<div class="info-value">{{ provider_slots_today|default:0 }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Utilization Rate</div>
|
|
<div class="info-value">{{ utilization_rate|default:0 }}%</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Average Duration</div>
|
|
<div class="info-value">{{ avg_duration|default:30 }} min</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">No-Show Rate</div>
|
|
<div class="info-value">{{ no_show_rate|default:0 }}%</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="action-buttons fade-in">
|
|
{% if slot.status == 'AVAILABLE' %}
|
|
<button class="btn btn-primary btn-action" onclick="bookSlot()">
|
|
<i class="fas fa-calendar-plus"></i>Book Appointment
|
|
</button>
|
|
<button class="btn btn-warning btn-action" onclick="blockSlot()">
|
|
<i class="fas fa-ban"></i>Block Slot
|
|
</button>
|
|
{% elif slot.status == 'BOOKED' %}
|
|
<a href="{% url 'appointments:appointment_detail' slot.appointment.pk %}" class="btn btn-primary btn-action">
|
|
<i class="fas fa-eye"></i>View Appointment
|
|
</a>
|
|
<button class="btn btn-warning btn-action" onclick="rescheduleAppointment()">
|
|
<i class="fas fa-calendar-alt"></i>Reschedule
|
|
</button>
|
|
<button class="btn btn-danger btn-action" onclick="cancelAppointment()">
|
|
<i class="fas fa-times"></i>Cancel Appointment
|
|
</button>
|
|
{% elif slot.status == 'BLOCKED' %}
|
|
<button class="btn btn-success btn-action" onclick="unblockSlot()">
|
|
<i class="fas fa-check"></i>Unblock Slot
|
|
</button>
|
|
{% endif %}
|
|
|
|
<a href="{% url 'appointments:slot_update' slot.pk %}" class="btn btn-secondary btn-action">
|
|
<i class="fas fa-edit"></i>Edit Slot
|
|
</a>
|
|
|
|
<button class="btn btn-info btn-action" onclick="duplicateSlot()">
|
|
<i class="fas fa-copy"></i>Duplicate Slot
|
|
</button>
|
|
|
|
<button class="btn btn-outline-secondary btn-action" onclick="printSlot()">
|
|
<i class="fas fa-print"></i>Print Details
|
|
</button>
|
|
|
|
<a href="{% url 'appointments:slot_list' %}" class="btn btn-outline-secondary btn-action">
|
|
<i class="fas fa-arrow-left"></i>Back to Slots
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Book Appointment Modal -->
|
|
<div class="modal fade" id="bookAppointmentModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-calendar-plus me-2"></i>Book Appointment
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="bookAppointmentForm">
|
|
<div class="mb-3">
|
|
<label class="form-label">Patient *</label>
|
|
<select class="form-select" id="patientSelect" required>
|
|
<option value="">Select Patient</option>
|
|
{% for patient in patients %}
|
|
<option value="{{ patient.id }}">{{ patient.get_full_name }} ({{ patient.mrn }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Appointment Type</label>
|
|
<select class="form-select" id="appointmentType">
|
|
<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="mb-3">
|
|
<label class="form-label">Notes</label>
|
|
<textarea class="form-control" id="appointmentNotes" rows="3"
|
|
placeholder="Additional notes for the appointment..."></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="confirmBooking()">
|
|
<i class="fas fa-check me-1"></i>Book Appointment
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
function bookSlot() {
|
|
$('#bookAppointmentModal').modal('show');
|
|
}
|
|
|
|
function confirmBooking() {
|
|
const formData = {
|
|
patient_id: $('#patientSelect').val(),
|
|
appointment_type: $('#appointmentType').val(),
|
|
notes: $('#appointmentNotes').val()
|
|
};
|
|
|
|
if (!formData.patient_id) {
|
|
alert('Please select a patient');
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: '{% url "appointments:slot_book" slot.pk %}',
|
|
method: 'POST',
|
|
data: formData,
|
|
headers: {
|
|
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
|
|
},
|
|
success: function(response) {
|
|
$('#bookAppointmentModal').modal('hide');
|
|
showAlert('Appointment booked successfully!', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
},
|
|
error: function() {
|
|
showAlert('Error booking appointment', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function blockSlot() {
|
|
if (confirm('Are you sure you want to block this slot?')) {
|
|
$.ajax({
|
|
url: '{% url "appointments:slot_block" slot.pk %}',
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
|
|
},
|
|
success: function() {
|
|
showAlert('Slot blocked successfully!', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
},
|
|
error: function() {
|
|
showAlert('Error blocking slot', 'error');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function unblockSlot() {
|
|
if (confirm('Are you sure you want to unblock this slot?')) {
|
|
$.ajax({
|
|
url: '{% url "appointments:slot_unblock" slot.pk %}',
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
|
|
},
|
|
success: function() {
|
|
showAlert('Slot unblocked successfully!', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
},
|
|
error: function() {
|
|
showAlert('Error unblocking slot', 'error');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function duplicateSlot() {
|
|
window.location.href = '{% url "appointments:slot_create" %}?duplicate={{ slot.pk }}';
|
|
}
|
|
|
|
function rescheduleAppointment() {
|
|
window.location.href = '{% url "appointments:appointment_reschedule" slot.appointment.pk %}';
|
|
}
|
|
|
|
function cancelAppointment() {
|
|
if (confirm('Are you sure you want to cancel this appointment?')) {
|
|
$.ajax({
|
|
url: '{% url "appointments:appointment_cancel" slot.appointment.pk %}',
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
|
|
},
|
|
success: function() {
|
|
showAlert('Appointment cancelled successfully!', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
},
|
|
error: function() {
|
|
showAlert('Error cancelling appointment', 'error');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function viewAppointment() {
|
|
window.location.href = '{% url "appointments:appointment_detail" slot.appointment.pk %}';
|
|
}
|
|
|
|
function printSlot() {
|
|
window.print();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Auto-complete for patient search
|
|
$('#patientSelect').select2({
|
|
ajax: {
|
|
url: '/api/patients/search/',
|
|
dataType: 'json',
|
|
delay: 250,
|
|
data: function (params) {
|
|
return {
|
|
q: params.term
|
|
};
|
|
},
|
|
processResults: function (data) {
|
|
return {
|
|
results: data.patients.map(patient => ({
|
|
id: patient.id,
|
|
text: `${patient.name} (${patient.mrn})`
|
|
}))
|
|
};
|
|
}
|
|
},
|
|
minimumInputLength: 2,
|
|
dropdownParent: $('#bookAppointmentModal')
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|