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

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 %}