1238 lines
42 KiB
HTML
1238 lines
42 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% if object %}Edit Slot{% else %}Create Slot{% endif %} - Appointment Management{% endblock %}
|
|
|
|
{% block css %}
|
|
<link href="{% static 'plugins/select2/css/select2.min.css' %}" rel="stylesheet" />
|
|
<style>
|
|
.slot-form-header {
|
|
background: linear-gradient(135deg, #6f42c1 0%, #5a2d91 100%);
|
|
border-radius: 0.5rem;
|
|
color: white;
|
|
margin-bottom: 2rem;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.form-container {
|
|
background: white;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
|
padding: 2rem;
|
|
}
|
|
|
|
.form-section {
|
|
border-bottom: 2px solid #f8f9fa;
|
|
margin-bottom: 2rem;
|
|
padding-bottom: 2rem;
|
|
}
|
|
|
|
.form-section:last-child {
|
|
border-bottom: none;
|
|
margin-bottom: 0;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.section-title {
|
|
color: #495057;
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.section-icon {
|
|
align-items: center;
|
|
background: linear-gradient(135deg, #007bff, #0056b3);
|
|
border-radius: 50%;
|
|
color: white;
|
|
display: inline-flex;
|
|
font-size: 1rem;
|
|
height: 40px;
|
|
justify-content: center;
|
|
margin-right: 1rem;
|
|
width: 40px;
|
|
}
|
|
|
|
.form-grid {
|
|
display: grid;
|
|
gap: 1.5rem;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.form-label {
|
|
color: #495057;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.form-label.required::after {
|
|
color: #dc3545;
|
|
content: ' *';
|
|
}
|
|
|
|
.form-control,
|
|
.form-select {
|
|
border: 2px solid #e9ecef;
|
|
border-radius: 0.375rem;
|
|
padding: 0.75rem 1rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.form-control:focus,
|
|
.form-select:focus {
|
|
border-color: #007bff;
|
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
|
}
|
|
|
|
.form-control.is-valid {
|
|
border-color: #28a745;
|
|
}
|
|
|
|
.form-control.is-invalid {
|
|
border-color: #dc3545;
|
|
}
|
|
|
|
.input-group {
|
|
position: relative;
|
|
}
|
|
|
|
.input-group-text {
|
|
background: #f8f9fa;
|
|
border: 2px solid #e9ecef;
|
|
border-right: none;
|
|
color: #6c757d;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.time-picker-container {
|
|
display: grid;
|
|
gap: 1rem;
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
|
|
.duration-display {
|
|
align-items: center;
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
color: #495057;
|
|
display: flex;
|
|
font-weight: 600;
|
|
justify-content: center;
|
|
min-height: 50px;
|
|
padding: 0.75rem;
|
|
}
|
|
|
|
.recurrence-options {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
display: none;
|
|
margin-top: 1rem;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.recurrence-options.show {
|
|
display: block;
|
|
}
|
|
|
|
.checkbox-group {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
}
|
|
|
|
.form-check {
|
|
align-items: center;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.form-check-input {
|
|
margin: 0;
|
|
}
|
|
|
|
.provider-selection {
|
|
background: #e7f3ff;
|
|
border: 1px solid #b3d9ff;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.provider-card {
|
|
align-items: center;
|
|
background: white;
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
padding: 1rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.provider-card:hover {
|
|
border-color: #007bff;
|
|
box-shadow: 0 0.25rem 0.5rem rgba(0, 123, 255, 0.15);
|
|
}
|
|
|
|
.provider-card.selected {
|
|
border-color: #007bff;
|
|
box-shadow: 0 0.25rem 0.5rem rgba(0, 123, 255, 0.25);
|
|
}
|
|
|
|
.provider-avatar {
|
|
align-items: center;
|
|
background: #007bff;
|
|
border-radius: 50%;
|
|
color: white;
|
|
display: flex;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
height: 50px;
|
|
justify-content: center;
|
|
width: 50px;
|
|
}
|
|
|
|
.provider-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.provider-name {
|
|
color: #495057;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.provider-specialty {
|
|
color: #6c757d;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.availability-preview {
|
|
background: #fff3cd;
|
|
border: 1px solid #ffeaa7;
|
|
border-radius: 0.375rem;
|
|
margin-top: 1rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.preview-title {
|
|
color: #856404;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.preview-content {
|
|
color: #856404;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.conflict-warning {
|
|
background: #f8d7da;
|
|
border: 1px solid #f5c6cb;
|
|
border-radius: 0.375rem;
|
|
display: none;
|
|
margin-top: 1rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.conflict-warning.show {
|
|
display: block;
|
|
}
|
|
|
|
.conflict-title {
|
|
color: #721c24;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.conflict-list {
|
|
color: #721c24;
|
|
margin: 0;
|
|
padding-left: 1.5rem;
|
|
}
|
|
|
|
.form-actions {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: flex-end;
|
|
margin-top: 2rem;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.btn-action {
|
|
align-items: center;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.5rem;
|
|
}
|
|
|
|
.validation-feedback {
|
|
color: #dc3545;
|
|
font-size: 0.875rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.help-text {
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.quick-fill-buttons {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.quick-fill-btn {
|
|
background: #e9ecef;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
color: #495057;
|
|
cursor: pointer;
|
|
font-size: 0.8rem;
|
|
padding: 0.375rem 0.75rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.quick-fill-btn:hover {
|
|
background: #007bff;
|
|
color: white;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.slot-form-header {
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.form-container {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.form-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.time-picker-container {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.form-actions {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.btn-action {
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
.fade-in {
|
|
animation: fadeIn 0.6s ease-in;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(20px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.step-indicator {
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.step {
|
|
align-items: center;
|
|
background: #e9ecef;
|
|
border-radius: 50%;
|
|
color: #6c757d;
|
|
display: flex;
|
|
font-weight: 600;
|
|
height: 40px;
|
|
justify-content: center;
|
|
margin: 0 1rem;
|
|
position: relative;
|
|
width: 40px;
|
|
}
|
|
|
|
.step.active {
|
|
background: #007bff;
|
|
color: white;
|
|
}
|
|
|
|
.step.completed {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.step::after {
|
|
background: #e9ecef;
|
|
content: '';
|
|
height: 2px;
|
|
left: 100%;
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 2rem;
|
|
}
|
|
|
|
.step:last-child::after {
|
|
display: none;
|
|
}
|
|
|
|
.step.completed::after {
|
|
background: #28a745;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="slot-form-header fade-in">
|
|
<div class="row align-items-center">
|
|
<div class="col-lg-8">
|
|
<h1 class="mb-2">
|
|
<i class="fas fa-{% if object %}edit{% else %}plus{% endif %} me-3"></i>
|
|
{% if object %}Edit Appointment Slot{% else %}Create Appointment Slot{% endif %}
|
|
</h1>
|
|
<p class="mb-3 opacity-90">
|
|
{% if object %}
|
|
Modify the appointment slot details and availability
|
|
{% else %}
|
|
Set up a new appointment slot for provider scheduling
|
|
{% endif %}
|
|
</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>{{ 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>
|
|
{% if object %}
|
|
<div class="d-flex align-items-center gap-2">
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>Last modified {{ object.updated_at|date:"M d, Y" }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-4 text-lg-end">
|
|
<div class="d-flex flex-column gap-2">
|
|
<a href="{% url 'appointments:slot_list' %}" class="btn btn-outline-light">
|
|
<i class="fas fa-arrow-left me-2"></i>Back to Slots
|
|
</a>
|
|
{% if object %}
|
|
<a href="{% url 'appointments:slot_detail' object.pk %}" class="btn btn-outline-light">
|
|
<i class="fas fa-eye me-2"></i>View Details
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step Indicator -->
|
|
<div class="step-indicator fade-in">
|
|
<div class="step active" id="step1">1</div>
|
|
<div class="step" id="step2">2</div>
|
|
<div class="step" id="step3">3</div>
|
|
<div class="step" id="step4">4</div>
|
|
</div>
|
|
|
|
<!-- Form Container -->
|
|
<div class="form-container fade-in">
|
|
<form method="post" id="slotForm" novalidate>
|
|
{% csrf_token %}
|
|
|
|
<!-- Step 1: Basic Information -->
|
|
<div class="form-section" id="section1">
|
|
<h3 class="section-title">
|
|
<span class="section-icon">
|
|
<i class="fas fa-info-circle"></i>
|
|
</span>
|
|
Basic Information
|
|
</h3>
|
|
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label class="form-label required">Date</label>
|
|
<input type="date" class="form-control" name="date" id="slotDate"
|
|
value="{{ object.date|date:'Y-m-d'|default:'' }}" required>
|
|
<div class="help-text">Select the date for this appointment slot</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label required">Appointment Type</label>
|
|
<select class="form-select" name="appointment_type" id="appointmentType" required>
|
|
<option value="">Select Type</option>
|
|
<option value="consultation" {% if object.appointment_type == 'consultation' %}selected{% endif %}>Consultation</option>
|
|
<option value="follow_up" {% if object.appointment_type == 'follow_up' %}selected{% endif %}>Follow-up</option>
|
|
<option value="procedure" {% if object.appointment_type == 'procedure' %}selected{% endif %}>Procedure</option>
|
|
<option value="emergency" {% if object.appointment_type == 'emergency' %}selected{% endif %}>Emergency</option>
|
|
<option value="routine" {% if object.appointment_type == 'routine' %}selected{% endif %}>Routine Check-up</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Status</label>
|
|
<select class="form-select" name="status" id="slotStatus">
|
|
<option value="available" {% if object.status == 'AVAILABLE' %}selected{% endif %}>Available</option>
|
|
<option value="blocked" {% if object.status == 'BLOCKED' %}selected{% endif %}>Blocked</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Priority</label>
|
|
<select class="form-select" name="priority" id="slotPriority">
|
|
<option value="normal" {% if object.priority == 'normal' %}selected{% endif %}>Normal</option>
|
|
<option value="high" {% if object.priority == 'high' %}selected{% endif %}>High</option>
|
|
<option value="urgent" {% if object.priority == 'urgent' %}selected{% endif %}>Urgent</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Time & Duration -->
|
|
<div class="form-section" id="section2" style="display: none;">
|
|
<h3 class="section-title">
|
|
<span class="section-icon">
|
|
<i class="fas fa-clock"></i>
|
|
</span>
|
|
Time & Duration
|
|
</h3>
|
|
|
|
<!-- Quick Fill Buttons -->
|
|
<div class="quick-fill-buttons">
|
|
<button type="button" class="quick-fill-btn" onclick="quickFillTime('09:00', '17:00', 30)">
|
|
Standard Hours (9 AM - 5 PM, 30 min)
|
|
</button>
|
|
<button type="button" class="quick-fill-btn" onclick="quickFillTime('08:00', '12:00', 15)">
|
|
Morning Shift (8 AM - 12 PM, 15 min)
|
|
</button>
|
|
<button type="button" class="quick-fill-btn" onclick="quickFillTime('13:00', '17:00', 45)">
|
|
Afternoon Shift (1 PM - 5 PM, 45 min)
|
|
</button>
|
|
<button type="button" class="quick-fill-btn" onclick="quickFillTime('18:00', '22:00', 60)">
|
|
Evening Hours (6 PM - 10 PM, 60 min)
|
|
</button>
|
|
</div>
|
|
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label class="form-label required">Start Time</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">
|
|
<i class="fas fa-clock"></i>
|
|
</span>
|
|
<input type="time" class="form-control" name="start_time" id="startTime"
|
|
value="{{ object.start_time|time:'H:i'|default:'' }}" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label required">End Time</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">
|
|
<i class="fas fa-clock"></i>
|
|
</span>
|
|
<input type="time" class="form-control" name="end_time" id="endTime"
|
|
value="{{ object.end_time|time:'H:i'|default:'' }}" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Duration</label>
|
|
<div class="duration-display" id="durationDisplay">
|
|
{% if object %}{{ object.duration_minutes }} minutes{% else %}0 minutes{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Buffer Time (minutes)</label>
|
|
<select class="form-select" name="buffer_time" id="bufferTime">
|
|
<option value="0" {% if object.buffer_time == 0 %}selected{% endif %}>No Buffer</option>
|
|
<option value="5" {% if object.buffer_time == 5 %}selected{% endif %}>5 minutes</option>
|
|
<option value="10" {% if object.buffer_time == 10 %}selected{% endif %}>10 minutes</option>
|
|
<option value="15" {% if object.buffer_time == 15 %}selected{% endif %}>15 minutes</option>
|
|
<option value="30" {% if object.buffer_time == 30 %}selected{% endif %}>30 minutes</option>
|
|
</select>
|
|
<div class="help-text">Time between appointments for preparation</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Provider Selection -->
|
|
<div class="form-section" id="section3" style="display: none;">
|
|
<h3 class="section-title">
|
|
<span class="section-icon">
|
|
<i class="fas fa-user-md"></i>
|
|
</span>
|
|
Provider Selection
|
|
</h3>
|
|
|
|
<div class="provider-selection">
|
|
<div class="form-group">
|
|
<label class="form-label required">Select Provider</label>
|
|
<input type="hidden" name="provider" id="selectedProvider" value="{{ object.provider.id|default:'' }}">
|
|
|
|
<div class="mb-3">
|
|
<input type="text" class="form-control" id="providerSearch"
|
|
placeholder="Search providers by name or specialty...">
|
|
</div>
|
|
|
|
<div id="providerList">
|
|
{% for provider in providers %}
|
|
<div class="provider-card" data-provider-id="{{ provider.id }}"
|
|
{% if object.provider.id == provider.id %}data-selected="true"{% endif %}>
|
|
{% 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="form-check">
|
|
<input class="form-check-input" type="radio" name="provider_radio"
|
|
value="{{ provider.id }}" {% if object.provider.id == provider.id %}checked{% endif %}>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-user-md text-muted fa-3x mb-3"></i>
|
|
<h5 class="text-muted">No Providers Available</h5>
|
|
<p class="text-muted">Please add providers to the system first</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 4: Advanced Options -->
|
|
<div class="form-section" id="section4" style="display: none;">
|
|
<h3 class="section-title">
|
|
<span class="section-icon">
|
|
<i class="fas fa-cogs"></i>
|
|
</span>
|
|
Advanced Options
|
|
</h3>
|
|
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="is_recurring"
|
|
id="isRecurring" {% if object.is_recurring %}checked{% endif %}>
|
|
<label class="form-check-label" for="isRecurring">
|
|
<strong>Recurring Appointment Slot</strong>
|
|
</label>
|
|
</div>
|
|
<div class="help-text">Create multiple slots with the same pattern</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="allow_online_booking"
|
|
id="allowOnlineBooking" {% if object.allow_online_booking %}checked{% endif %}>
|
|
<label class="form-check-label" for="allowOnlineBooking">
|
|
<strong>Allow Online Booking</strong>
|
|
</label>
|
|
</div>
|
|
<div class="help-text">Patients can book this slot online</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="send_reminders"
|
|
id="sendReminders" {% if object.send_reminders %}checked{% endif %}>
|
|
<label class="form-check-label" for="sendReminders">
|
|
<strong>Send Appointment Reminders</strong>
|
|
</label>
|
|
</div>
|
|
<div class="help-text">Automatic reminders via email/SMS</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Maximum Bookings</label>
|
|
<input type="number" class="form-control" name="max_bookings" id="maxBookings"
|
|
value="{{ object.max_bookings|default:1 }}" min="1" max="10">
|
|
<div class="help-text">Number of patients that can book this slot</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recurrence Options -->
|
|
<div class="recurrence-options" id="recurrenceOptions">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-repeat me-2"></i>
|
|
Recurrence Pattern
|
|
</h5>
|
|
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label class="form-label">Repeat</label>
|
|
<select class="form-select" name="recurrence_pattern" id="recurrencePattern">
|
|
<option value="daily">Daily</option>
|
|
<option value="weekly" selected>Weekly</option>
|
|
<option value="monthly">Monthly</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Every</label>
|
|
<div class="input-group">
|
|
<input type="number" class="form-control" name="recurrence_interval"
|
|
id="recurrenceInterval" value="1" min="1" max="12">
|
|
<span class="input-group-text" id="intervalUnit">week(s)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">End Date</label>
|
|
<input type="date" class="form-control" name="recurrence_end_date"
|
|
id="recurrenceEndDate">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Days of Week</label>
|
|
<div class="checkbox-group">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="recurrence_days"
|
|
value="1" id="monday">
|
|
<label class="form-check-label" for="monday">Mon</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="recurrence_days"
|
|
value="2" id="tuesday">
|
|
<label class="form-check-label" for="tuesday">Tue</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="recurrence_days"
|
|
value="3" id="wednesday">
|
|
<label class="form-check-label" for="wednesday">Wed</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="recurrence_days"
|
|
value="4" id="thursday">
|
|
<label class="form-check-label" for="thursday">Thu</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="recurrence_days"
|
|
value="5" id="friday">
|
|
<label class="form-check-label" for="friday">Fri</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="recurrence_days"
|
|
value="6" id="saturday">
|
|
<label class="form-check-label" for="saturday">Sat</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="recurrence_days"
|
|
value="0" id="sunday">
|
|
<label class="form-check-label" for="sunday">Sun</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Notes</label>
|
|
<textarea class="form-control" name="notes" id="slotNotes" rows="3"
|
|
placeholder="Additional notes or instructions for this slot...">{{ object.notes|default:'' }}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Availability Preview -->
|
|
<div class="availability-preview" id="availabilityPreview" style="display: none;">
|
|
<div class="preview-title">
|
|
<i class="fas fa-eye me-2"></i>
|
|
Availability Preview
|
|
</div>
|
|
<div class="preview-content" id="previewContent">
|
|
<!-- Preview content will be populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conflict Warning -->
|
|
<div class="conflict-warning" id="conflictWarning">
|
|
<div class="conflict-title">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
Schedule Conflicts Detected
|
|
</div>
|
|
<ul class="conflict-list" id="conflictList">
|
|
<!-- Conflicts will be populated by JavaScript -->
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary btn-action" id="prevBtn" onclick="previousStep()" style="display: none;">
|
|
<i class="fas fa-arrow-left"></i>Previous
|
|
</button>
|
|
<button type="button" class="btn btn-primary btn-action" id="nextBtn" onclick="nextStep()">
|
|
<i class="fas fa-arrow-right"></i>Next
|
|
</button>
|
|
<button type="submit" class="btn btn-success btn-action" id="submitBtn" style="display: none;">
|
|
<i class="fas fa-save"></i>{% if object %}Update Slot{% else %}Create Slot{% endif %}
|
|
</button>
|
|
<a href="{% url 'appointments:slot_list' %}" class="btn btn-outline-secondary btn-action">
|
|
<i class="fas fa-times"></i>Cancel
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'plugins/select2/dist/js/select2.full.js' %}"></script>
|
|
<script>
|
|
let currentStep = 1;
|
|
const totalSteps = 4;
|
|
|
|
$(document).ready(function() {
|
|
// Initialize form
|
|
initializeForm();
|
|
|
|
// Update time every minute
|
|
updateCurrentTime();
|
|
setInterval(updateCurrentTime, 60000);
|
|
|
|
// Set up event listeners
|
|
setupEventListeners();
|
|
|
|
// Load existing data if editing
|
|
{% if object %}
|
|
loadExistingData();
|
|
{% endif %}
|
|
});
|
|
|
|
function initializeForm() {
|
|
// Initialize date picker with minimum date as today
|
|
const today = new Date().toISOString().split('T')[0];
|
|
$('#slotDate').attr('min', today);
|
|
|
|
// Initialize provider search
|
|
$('#providerSearch').on('keyup', function() {
|
|
filterProviders($(this).val());
|
|
});
|
|
|
|
// Set up provider selection
|
|
$('.provider-card').on('click', function() {
|
|
selectProvider($(this).data('provider-id'));
|
|
});
|
|
|
|
// Initialize recurrence options
|
|
toggleRecurrenceOptions();
|
|
|
|
// Set up form validation
|
|
setupFormValidation();
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Time change listeners
|
|
$('#startTime, #endTime').on('change', function() {
|
|
calculateDuration();
|
|
checkConflicts();
|
|
updatePreview();
|
|
});
|
|
|
|
// Date change listener
|
|
$('#slotDate').on('change', function() {
|
|
checkConflicts();
|
|
updatePreview();
|
|
});
|
|
|
|
// Provider change listener
|
|
$('input[name="provider_radio"]').on('change', function() {
|
|
$('#selectedProvider').val($(this).val());
|
|
checkConflicts();
|
|
updatePreview();
|
|
});
|
|
|
|
// Recurrence toggle
|
|
$('#isRecurring').on('change', function() {
|
|
toggleRecurrenceOptions();
|
|
updatePreview();
|
|
});
|
|
|
|
// Recurrence pattern change
|
|
$('#recurrencePattern').on('change', function() {
|
|
updateIntervalUnit();
|
|
updatePreview();
|
|
});
|
|
|
|
// Form submission
|
|
$('#slotForm').on('submit', function(e) {
|
|
e.preventDefault();
|
|
if (validateCurrentStep()) {
|
|
submitForm();
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateCurrentTime() {
|
|
const now = new Date();
|
|
const timeString = now.toLocaleTimeString('en-US', {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
$('#currentTime').text(timeString);
|
|
}
|
|
|
|
function nextStep() {
|
|
if (validateCurrentStep()) {
|
|
if (currentStep < totalSteps) {
|
|
currentStep++;
|
|
showStep(currentStep);
|
|
}
|
|
}
|
|
}
|
|
|
|
function previousStep() {
|
|
if (currentStep > 1) {
|
|
currentStep--;
|
|
showStep(currentStep);
|
|
}
|
|
}
|
|
|
|
function showStep(step) {
|
|
// Hide all sections
|
|
$('.form-section').hide();
|
|
|
|
// Show current section
|
|
$(`#section${step}`).show();
|
|
|
|
// Update step indicators
|
|
$('.step').removeClass('active completed');
|
|
for (let i = 1; i < step; i++) {
|
|
$(`#step${i}`).addClass('completed');
|
|
}
|
|
$(`#step${step}`).addClass('active');
|
|
|
|
// Update buttons
|
|
$('#prevBtn').toggle(step > 1);
|
|
$('#nextBtn').toggle(step < totalSteps);
|
|
$('#submitBtn').toggle(step === totalSteps);
|
|
|
|
// Update preview on final step
|
|
if (step === totalSteps) {
|
|
updatePreview();
|
|
$('#availabilityPreview').show();
|
|
} else {
|
|
$('#availabilityPreview').hide();
|
|
}
|
|
}
|
|
|
|
function validateCurrentStep() {
|
|
let isValid = true;
|
|
const currentSection = $(`#section${currentStep}`);
|
|
|
|
// Clear previous validation
|
|
currentSection.find('.form-control, .form-select').removeClass('is-invalid');
|
|
currentSection.find('.validation-feedback').remove();
|
|
|
|
// Validate required fields in current step
|
|
currentSection.find('[required]').each(function() {
|
|
if (!$(this).val()) {
|
|
$(this).addClass('is-invalid');
|
|
$(this).after('<div class="validation-feedback">This field is required.</div>');
|
|
isValid = false;
|
|
}
|
|
});
|
|
|
|
// Step-specific validation
|
|
switch (currentStep) {
|
|
case 1:
|
|
// Validate date is not in the past
|
|
const selectedDate = new Date($('#slotDate').val());
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
if (selectedDate < today) {
|
|
$('#slotDate').addClass('is-invalid');
|
|
$('#slotDate').after('<div class="validation-feedback">Date cannot be in the past.</div>');
|
|
isValid = false;
|
|
}
|
|
break;
|
|
|
|
case 2:
|
|
// Validate time range
|
|
const startTime = $('#startTime').val();
|
|
const endTime = $('#endTime').val();
|
|
|
|
if (startTime && endTime && startTime >= endTime) {
|
|
$('#endTime').addClass('is-invalid');
|
|
$('#endTime').after('<div class="validation-feedback">End time must be after start time.</div>');
|
|
isValid = false;
|
|
}
|
|
break;
|
|
|
|
case 3:
|
|
// Validate provider selection
|
|
if (!$('#selectedProvider').val()) {
|
|
showAlert('Please select a provider', 'warning');
|
|
isValid = false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
function selectProvider(providerId) {
|
|
// Update hidden field
|
|
$('#selectedProvider').val(providerId);
|
|
|
|
// Update visual selection
|
|
$('.provider-card').removeClass('selected');
|
|
$(`.provider-card[data-provider-id="${providerId}"]`).addClass('selected');
|
|
|
|
// Update radio button
|
|
$(`input[name="provider_radio"][value="${providerId}"]`).prop('checked', true);
|
|
|
|
// Check for conflicts
|
|
checkConflicts();
|
|
}
|
|
|
|
function filterProviders(searchTerm) {
|
|
$('.provider-card').each(function() {
|
|
const providerName = $(this).find('.provider-name').text().toLowerCase();
|
|
const providerSpecialty = $(this).find('.provider-specialty').text().toLowerCase();
|
|
const searchLower = searchTerm.toLowerCase();
|
|
|
|
if (providerName.includes(searchLower) || providerSpecialty.includes(searchLower)) {
|
|
$(this).show();
|
|
} else {
|
|
$(this).hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
function calculateDuration() {
|
|
const startTime = $('#startTime').val();
|
|
const endTime = $('#endTime').val();
|
|
|
|
if (startTime && endTime) {
|
|
const start = new Date(`2000-01-01T${startTime}`);
|
|
const end = new Date(`2000-01-01T${endTime}`);
|
|
const diffMs = end - start;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
|
|
if (diffMins > 0) {
|
|
$('#durationDisplay').text(`${diffMins} minutes`);
|
|
} else {
|
|
$('#durationDisplay').text('Invalid time range');
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleRecurrenceOptions() {
|
|
if ($('#isRecurring').is(':checked')) {
|
|
$('#recurrenceOptions').addClass('show');
|
|
} else {
|
|
$('#recurrenceOptions').removeClass('show');
|
|
}
|
|
}
|
|
|
|
function updateIntervalUnit() {
|
|
const pattern = $('#recurrencePattern').val();
|
|
let unit = 'week(s)';
|
|
|
|
switch (pattern) {
|
|
case 'daily':
|
|
unit = 'day(s)';
|
|
break;
|
|
case 'weekly':
|
|
unit = 'week(s)';
|
|
break;
|
|
case 'monthly':
|
|
unit = 'month(s)';
|
|
break;
|
|
}
|
|
|
|
$('#intervalUnit').text(unit);
|
|
}
|
|
|
|
function quickFillTime(startTime, endTime, duration) {
|
|
$('#startTime').val(startTime);
|
|
$('#endTime').val(endTime);
|
|
calculateDuration();
|
|
checkConflicts();
|
|
}
|
|
|
|
function checkConflicts() {
|
|
const formData = {
|
|
date: $('#slotDate').val(),
|
|
start_time: $('#startTime').val(),
|
|
end_time: $('#endTime').val(),
|
|
provider: $('#selectedProvider').val()
|
|
};
|
|
|
|
if (formData.date && formData.start_time && formData.end_time && formData.provider) {
|
|
$.ajax({
|
|
url: '/api/slots/check-conflicts/',
|
|
method: 'POST',
|
|
data: formData,
|
|
headers: {
|
|
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
|
|
},
|
|
success: function(response) {
|
|
if (response.conflicts && response.conflicts.length > 0) {
|
|
showConflicts(response.conflicts);
|
|
} else {
|
|
hideConflicts();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function showConflicts(conflicts) {
|
|
const conflictList = $('#conflictList');
|
|
conflictList.empty();
|
|
|
|
conflicts.forEach(conflict => {
|
|
conflictList.append(`<li>${conflict.description}</li>`);
|
|
});
|
|
|
|
$('#conflictWarning').addClass('show');
|
|
}
|
|
|
|
function hideConflicts() {
|
|
$('#conflictWarning').removeClass('show');
|
|
}
|
|
|
|
function updatePreview() {
|
|
const formData = {
|
|
date: $('#slotDate').val(),
|
|
start_time: $('#startTime').val(),
|
|
end_time: $('#endTime').val(),
|
|
provider: $('#selectedProvider').val(),
|
|
appointment_type: $('#appointmentType').val(),
|
|
is_recurring: $('#isRecurring').is(':checked'),
|
|
recurrence_pattern: $('#recurrencePattern').val(),
|
|
recurrence_interval: $('#recurrenceInterval').val()
|
|
};
|
|
|
|
let previewText = '';
|
|
|
|
if (formData.date && formData.start_time && formData.end_time) {
|
|
const date = new Date(formData.date);
|
|
const dateStr = date.toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
|
|
previewText = `${dateStr} from ${formData.start_time} to ${formData.end_time}`;
|
|
|
|
if (formData.appointment_type) {
|
|
previewText += ` for ${formData.appointment_type}`;
|
|
}
|
|
|
|
if (formData.is_recurring) {
|
|
previewText += ` (Recurring ${formData.recurrence_pattern})`;
|
|
}
|
|
}
|
|
|
|
$('#previewContent').text(previewText || 'Complete the form to see preview');
|
|
}
|
|
|
|
function setupFormValidation() {
|
|
// Real-time validation
|
|
$('.form-control, .form-select').on('blur', function() {
|
|
validateField($(this));
|
|
});
|
|
}
|
|
|
|
function validateField(field) {
|
|
const value = field.val();
|
|
const isRequired = field.prop('required');
|
|
|
|
field.removeClass('is-invalid is-valid');
|
|
field.siblings('.validation-feedback').remove();
|
|
|
|
if (isRequired && !value) {
|
|
field.addClass('is-invalid');
|
|
field.after('<div class="validation-feedback">This field is required.</div>');
|
|
return false;
|
|
} else if (value) {
|
|
field.addClass('is-valid');
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function submitForm() {
|
|
// Show loading state
|
|
$('#submitBtn').prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>Saving...');
|
|
|
|
// Submit the form
|
|
$('#slotForm').off('submit').submit();
|
|
}
|
|
|
|
function loadExistingData() {
|
|
// Load provider selection
|
|
const providerId = $('#selectedProvider').val();
|
|
if (providerId) {
|
|
selectProvider(providerId);
|
|
}
|
|
|
|
// Calculate duration for existing slot
|
|
calculateDuration();
|
|
|
|
// Show recurrence options if recurring
|
|
if ($('#isRecurring').is(':checked')) {
|
|
toggleRecurrenceOptions();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Initialize first step
|
|
showStep(1);
|
|
</script>
|
|
{% endblock %}
|
|
|