941 lines
34 KiB
PHP
941 lines
34 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Doctors</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
<link rel="stylesheet" href="<?= base_url('css/app.css') ?>">
|
|
<link rel="stylesheet" href="<?= base_url('css/dashboard.css') ?>">
|
|
<link rel="stylesheet" href="<?= base_url('css/doctors.css') ?>">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap5.min.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
</head>
|
|
<body class="app-body overview-layout">
|
|
<aside class="ov-sidebar" id="sidebar">
|
|
<div class="ov-brand">
|
|
<h1><i class="bi bi-hospital me-1"></i> DoctGuide</h1>
|
|
<span>Control Panel</span>
|
|
</div>
|
|
<nav class="ov-nav">
|
|
<div class="ov-nav__section">Main</div>
|
|
<a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a>
|
|
<div class="ov-nav__section">Manage</div>
|
|
<div class="ov-nav__dropdown active">
|
|
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
|
|
<span><i class="bi bi-person-badge"></i> Doctors</span>
|
|
<i class="bi bi-chevron-down dropdown-icon"></i>
|
|
</a>
|
|
<div class="ov-dropdown-menu">
|
|
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__sublink">Doctor List</a>
|
|
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__sublink">Add Doctor</a>
|
|
</div>
|
|
</div>
|
|
<div class="ov-nav__dropdown">
|
|
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
|
|
<span><i class="bi bi-people"></i> Patients</span>
|
|
<i class="bi bi-chevron-down dropdown-icon"></i>
|
|
</a>
|
|
<div class="ov-dropdown-menu">
|
|
<a href="<?= base_url('admin/patients') ?>" class="ov-nav__sublink">Patient List</a>
|
|
<a href="<?= base_url('admin/patients/add') ?>" class="ov-nav__sublink">Add Patient</a>
|
|
</div>
|
|
</div>
|
|
<a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a>
|
|
<a href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link"><i class="bi bi-clipboard-data"></i> Activity Log</a>
|
|
</nav>
|
|
<div class="ov-sidebar__footer">
|
|
<a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a>
|
|
</div>
|
|
</aside>
|
|
|
|
<div class="ov-main" id="mainContent">
|
|
<header class="ov-topbar">
|
|
<div class="d-flex align-items-center">
|
|
<button class="ov-toggle-btn" onclick="toggleSidebar()" title="Toggle Sidebar"><i class="bi bi-list" id="toggleIcon"></i></button>
|
|
<p class="ov-topbar__title mb-0">Doctors</p>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="ov-content">
|
|
<?php if (session()->getFlashdata('success')): ?>
|
|
<div class="alert alert-success app-alert"><?= esc(session()->getFlashdata('success')) ?></div>
|
|
<?php endif; ?>
|
|
<?php if (session()->getFlashdata('error')): ?>
|
|
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="ov-panel">
|
|
<div class="ov-panel__header">
|
|
<h2 class="ov-panel__title">Doctor List</h2>
|
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-three-dots-vertical"></i>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><a class="dropdown-item" href="#" onclick="exportTable('csv'); return false;"><i class="bi bi-file-earmark-text me-2"></i> CSV</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="exportTable('excel'); return false;"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="exportTable('pdf'); return false;"><i class="bi bi-file-earmark-pdf me-2"></i> PDF</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-0">
|
|
<table id="doctorsTable" class="table table-hover" style="width:100%">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th width="60">User ID</th>
|
|
<th>Doctor Name</th>
|
|
<th>Email</th>
|
|
<th>Specialization</th>
|
|
<th>Experience</th>
|
|
<th>Consultation Fee</th>
|
|
<th width="120">Status</th>
|
|
<th width="130">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<!-- Add Availability Modal -->
|
|
<div class="modal fade" id="availabilityModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Add Availability</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
|
|
<form id="availabilityForm" method="POST" action="<?= base_url('admin/availability/save') ?>">
|
|
<!-- <div class="modal-body">
|
|
|
|
<input type="hidden" name="doctor_id" id="doctorId">
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Doctor Name</label>
|
|
<input type="text" id="doctorName" class="form-control" readonly>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Date</label>
|
|
<input type="date" name="date" class="form-control" required>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Time</label>
|
|
<input type="time" name="time" class="form-control" required>
|
|
</div>
|
|
|
|
</div> -->
|
|
<div class="modal-body modal-body-scrollable">
|
|
|
|
<input type="hidden" name="doctor_id" id="doctorId">
|
|
<div class="mb-2 d-flex align-items-center flex-wrap">
|
|
<label class="form-label mb-2 me-2 fw-bold">
|
|
Doctor Name :
|
|
</label>
|
|
|
|
<span id="doctorNameText" class="me-2 mb-2 fw-semibold"></span>
|
|
|
|
<!-- Keep badges here -->
|
|
<div id="selectedBadges" class="d-flex flex-wrap gap-3 mb-2"></div>
|
|
</div>
|
|
<style>
|
|
#availabilityModal .modal-dialog {
|
|
max-height: calc(100vh - 2rem);
|
|
margin: 1rem auto;
|
|
}
|
|
#availabilityModal .modal-content {
|
|
max-height: calc(100vh - 2rem);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
/* Make only modal body scrollable, not the modal itself */
|
|
.modal-body-scrollable {
|
|
max-height: calc(100vh - 180px);
|
|
flex: 1 1 auto;
|
|
overflow-y: auto;
|
|
}
|
|
.error-msg {
|
|
color: red;
|
|
font-size: 12px;
|
|
/* min-height: 14px; */
|
|
}
|
|
|
|
.is-invalid {
|
|
border: 1px solid red !important;
|
|
}
|
|
.is-invalid:focus {
|
|
border-color: red !important;
|
|
box-shadow: 0 0 0 0.2rem rgba(255, 0, 0, 0.25) !important;
|
|
}
|
|
/* Fix slot-box error alignment and input styling */
|
|
.slot-box {
|
|
/* align-items: flex-start !important;
|
|
display: inline-flex !important;
|
|
flex-wrap: nowrap !important;
|
|
width: fit-content; */
|
|
}
|
|
.slot-time-col {
|
|
width: 110px;
|
|
min-width: 110px;
|
|
flex: 0 0 110px;
|
|
}
|
|
.slot-box .form-control {
|
|
min-width: 110px;
|
|
}
|
|
.slot-box .error-msg {
|
|
/* min-height: 18px; */
|
|
font-size: 12px;
|
|
color: #e53935;
|
|
display: block;
|
|
margin-top: 2px;
|
|
/* line-height: 1.2; */
|
|
width: 110px;
|
|
max-width: 110px;
|
|
white-space: normal;
|
|
overflow-wrap: break-word;
|
|
}
|
|
.slot-box .form-control.is-invalid {
|
|
border-color: #e53935 !important;
|
|
box-shadow: 0 0 0 0.2rem rgba(229,57,53,0.08) !important;
|
|
}
|
|
.slot-remove-btn {
|
|
flex: 0 0 auto;
|
|
align-self: flex-start;
|
|
margin-left: 6px;
|
|
}
|
|
</style>
|
|
|
|
<div id="availabilityContainer"></div>
|
|
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="submit" class="btn btn-success">Save</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
</div>
|
|
</form>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap5.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.bootstrap5.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/pdfmake.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/vfs_fonts.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.print.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
<script>
|
|
let doctorsTable = null;
|
|
const expandedSpecializations = new Set();
|
|
const days = [
|
|
{ name: "Monday", value: 1 },
|
|
{ name: "Tuesday", value: 2 },
|
|
{ name: "Wednesday", value: 3 },
|
|
{ name: "Thursday", value: 4 },
|
|
{ name: "Friday", value: 5 },
|
|
{ name: "Saturday", value: 6 },
|
|
{ name: "Sunday", value: 7 }
|
|
];
|
|
|
|
function renderAvailabilityUI() {
|
|
let html = '';
|
|
|
|
days.forEach(day => {
|
|
html += `
|
|
<div class="day-row mb-3 p-2 border rounded" data-day="${day.value}">
|
|
|
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
|
|
<strong style="width:100px;">${day.name}</strong>
|
|
|
|
<button type="button" class="btn btn-sm btn-outline-primary add-slot" data-day="${day.value}">
|
|
<i class="bi bi-plus"></i> add slot
|
|
</button>
|
|
|
|
<div class="slots-container d-flex flex-wrap gap-2" id="slots-${day.value}">
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
$('#availabilityContainer').html(html);
|
|
}
|
|
function escapeHtml(value) {
|
|
return String(value ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
function validateSlots() {
|
|
let isValid = true;
|
|
|
|
$('.slot-box').each(function () {
|
|
|
|
let startInput = $(this).find('.start-time');
|
|
let endInput = $(this).find('.end-time');
|
|
|
|
let startError = $(this).find('.slot-error');
|
|
let endError = $(this).find('.slot-error');
|
|
|
|
// Clear old errors
|
|
startError.text('');
|
|
endError.text('');
|
|
startInput.removeClass('is-invalid');
|
|
endInput.removeClass('is-invalid');
|
|
|
|
let start = startInput.val();
|
|
let end = endInput.val();
|
|
|
|
if (!start || !end) return;
|
|
|
|
let startTime = new Date(`1970-01-01T${start}:00`);
|
|
let endTime = new Date(`1970-01-01T${end}:00`);
|
|
|
|
let minTime = new Date("1970-01-01T09:00:00");
|
|
let maxTime = new Date("1970-01-01T19:00:00");
|
|
|
|
// Start time validation
|
|
if (startTime < minTime || startTime > maxTime) {
|
|
startError.text("Start time must be between 9:00 AM and 7:00 PM");
|
|
startInput.addClass('is-invalid').focus();
|
|
isValid = false;
|
|
return false;
|
|
}
|
|
|
|
// End time validation
|
|
if (endTime < minTime || endTime > maxTime) {
|
|
endError.text("End time must be between 9:00 AM and 7:00 PM");
|
|
endInput.addClass('is-invalid').focus();
|
|
isValid = false;
|
|
return false;
|
|
}
|
|
let diff = (endTime - startTime) / (1000 * 60);
|
|
|
|
// Invalid order
|
|
if (startTime >= endTime) {
|
|
endError.text("End time must be greater than start time");
|
|
endInput.addClass('is-invalid').focus();
|
|
isValid = false;
|
|
return false;
|
|
}
|
|
|
|
// Less than 15 min
|
|
if (diff < 15) {
|
|
endError.text("Minimum slot is 15 minutes");
|
|
endInput.addClass('is-invalid').focus();
|
|
isValid = false;
|
|
return false;
|
|
}
|
|
|
|
// More than 3 hours
|
|
if (diff > 180) {
|
|
endError.text("Maximum slot is 3 hours");
|
|
endInput.addClass('is-invalid').focus();
|
|
isValid = false;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
return isValid;
|
|
}
|
|
$('#availabilityForm').on('submit', function (e) {
|
|
|
|
// 🔴 STEP 1: Validate time slots FIRST
|
|
if (!validateSlots()) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
let availability = {};
|
|
|
|
$('.day-row').each(function () {
|
|
const day = $(this).data('day');
|
|
const slots = [];
|
|
|
|
$(this).find('.slot-box').each(function () {
|
|
const start = $(this).find('.start-time').val();
|
|
const end = $(this).find('.end-time').val();
|
|
|
|
if (start && end) {
|
|
slots.push({ start, end });
|
|
}
|
|
});
|
|
|
|
if (slots.length > 0) {
|
|
availability[day] = slots;
|
|
}
|
|
});
|
|
|
|
if (Object.keys(availability).length === 0) {
|
|
alert('Please add at least one time slot!');
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
const doctorId = $('#doctorId').val();
|
|
if (!doctorId) {
|
|
alert('Doctor ID is missing!');
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
// Remove old hidden input
|
|
$('input[name="availability_json"]').remove();
|
|
|
|
$('<input>')
|
|
.attr('type', 'hidden')
|
|
.attr('name', 'availability_json')
|
|
.val(JSON.stringify(availability))
|
|
.appendTo(this);
|
|
|
|
return true;
|
|
});
|
|
// $('#availabilityForm').on('submit', function (e) {
|
|
// if (!validateSlots()) {
|
|
// e.preventDefault();
|
|
// }
|
|
// });
|
|
|
|
function renderSpecializations(data, row) {
|
|
if (!data) {
|
|
return '<span class="text-muted">N/A</span>';
|
|
}
|
|
|
|
const specs = String(data)
|
|
.split(',')
|
|
.map(spec => spec.trim())
|
|
.filter(Boolean);
|
|
|
|
let html = specs.slice(0, 2).map(spec =>
|
|
`<span class="badge bg-light text-dark me-1">${escapeHtml(spec)}</span>`
|
|
).join('');
|
|
|
|
if (specs.length > 2) {
|
|
const extraSpecializations = specs.slice(2);
|
|
const extraId = `extra-spec-${escapeHtml(row.user_id ?? '')}`;
|
|
const isExpanded = expandedSpecializations.has(extraId);
|
|
const hiddenClass = isExpanded ? '' : ' d-none';
|
|
const outerToggleHiddenClass = isExpanded ? ' d-none' : '';
|
|
const innerToggleHiddenClass = isExpanded ? '' : ' d-none';
|
|
const toggleLabel = isExpanded ? 'Show less' : `+${extraSpecializations.length} more`;
|
|
|
|
html += `<div id="${extraId}" class="extra-specializations mt-2${hiddenClass}">`;
|
|
html += extraSpecializations.map(spec =>
|
|
`<span class="badge bg-light text-dark me-1 mb-1">${escapeHtml(spec)}</span>`
|
|
).join('');
|
|
html += `<button type="button" class="badge bg-secondary border-0 specialization-toggle ms-1 mb-1${innerToggleHiddenClass}" data-target="${extraId}" data-show-label="+${extraSpecializations.length} more" data-hide-label="Show less">Show less</button>`;
|
|
html += `</div>`;
|
|
html += `<button type="button" class="badge bg-secondary border-0 specialization-toggle${outerToggleHiddenClass}" data-target="${extraId}" data-show-label="+${extraSpecializations.length} more" data-hide-label="Show less">${toggleLabel}</button>`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
function toggleSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const main = document.getElementById('mainContent');
|
|
const icon = document.getElementById('toggleIcon');
|
|
sidebar.classList.toggle('collapsed');
|
|
main.classList.toggle('expanded');
|
|
icon.className = sidebar.classList.contains('collapsed') ? 'bi bi-layout-sidebar' : 'bi bi-list';
|
|
}
|
|
|
|
function toggleNavDropdown(event, element) {
|
|
event.preventDefault();
|
|
element.parentElement.classList.toggle('active');
|
|
}
|
|
|
|
$(document).ready(function() {
|
|
doctorsTable = $('#doctorsTable').DataTable({
|
|
processing: true,
|
|
ajax: {
|
|
url: "<?= base_url('admin/doctors/data') ?>",
|
|
type: 'GET',
|
|
dataSrc: 'data'
|
|
},
|
|
columns: [
|
|
{
|
|
data: 'formatted_user_id',
|
|
defaultContent: 'N/A'
|
|
},
|
|
{
|
|
data: 'name',
|
|
render: function (data) {
|
|
return `<div class="fw-medium">${escapeHtml(data ?? 'Unknown Doctor')}</div>`;
|
|
}
|
|
},
|
|
{
|
|
data: 'email',
|
|
render: function (data) {
|
|
if (!data) {
|
|
return '<span class="text-muted">N/A</span>';
|
|
}
|
|
|
|
const safeEmail = escapeHtml(data);
|
|
return `<a href="mailto:${safeEmail}" class="text-decoration-none">${safeEmail}</a>`;
|
|
}
|
|
},
|
|
{
|
|
data: 'specialization',
|
|
render: function (data, type, row) {
|
|
return renderSpecializations(data, row);
|
|
}
|
|
},
|
|
{
|
|
data: 'experience',
|
|
render: function (data) {
|
|
return data ? escapeHtml(data) : '<span class="text-muted">N/A</span>';
|
|
}
|
|
},
|
|
{
|
|
data: 'fees',
|
|
render: function (data) {
|
|
return data && parseFloat(data) > 0
|
|
? parseFloat(data).toFixed(2)
|
|
: '<span class="text-muted">N/A</span>';
|
|
}
|
|
},
|
|
{
|
|
data: 'status',
|
|
render: function (data) {
|
|
const status = String(data ?? 'active').toLowerCase();
|
|
const statusClass = status === 'active' ? 'success' : 'secondary';
|
|
const label = status.charAt(0).toUpperCase() + status.slice(1);
|
|
|
|
return `
|
|
<span class="badge bg-${statusClass} rounded-pill">
|
|
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
|
|
${escapeHtml(label)}
|
|
</span>
|
|
`;
|
|
}
|
|
},
|
|
{
|
|
data: null,
|
|
orderable: false,
|
|
searchable: false,
|
|
render: function (data, type, row) {
|
|
const editUrl = "<?= base_url('admin/doctors/edit') ?>/" + encodeURIComponent(row.edit_token);
|
|
const deleteUrl = "<?= base_url('admin/deleteDoctor') ?>/" + encodeURIComponent(row.user_id);
|
|
|
|
return `
|
|
<div class="btn-group" role="group">
|
|
<a href="${editUrl}" class="btn btn-outline-primary btn-sm" title="Edit Doctor">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<a href="${deleteUrl}" class="btn btn-outline-danger btn-sm" title="Delete Doctor"
|
|
onclick="return confirm('Are you sure you want to delete this doctor? This action cannot be undone.');">
|
|
<i class="bi bi-trash"></i>
|
|
</a>
|
|
<button class="btn btn-outline-success btn-sm open-availability-modal"
|
|
title="Add Availability"
|
|
data-id="${row.doctor_id}" // ← was row.user_id
|
|
data-name="${row.name}">
|
|
<i class="bi bi-calendar-plus"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
],
|
|
pageLength: 10,
|
|
lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]],
|
|
order: [[0, 'asc']],
|
|
dom: '<"row mb-3"<"col-md-6 d-flex align-items-center"l><"col-md-6 d-flex justify-content-end"f>>rtip',
|
|
buttons: [
|
|
{
|
|
extend: 'csvHtml5',
|
|
className: 'buttons-csv',
|
|
title: 'Doctors',
|
|
filename: 'doctors_list_' + new Date().toISOString().split('T')[0],
|
|
exportOptions: {
|
|
columns: [0, 1, 2, 3, 4, 5, 6]
|
|
}
|
|
},
|
|
{
|
|
extend: 'excelHtml5',
|
|
className: 'buttons-excel',
|
|
title: 'Doctors',
|
|
filename: 'doctors_list_' + new Date().toISOString().split('T')[0],
|
|
exportOptions: {
|
|
columns: [0, 1, 2, 3, 4, 5, 6]
|
|
}
|
|
},
|
|
{
|
|
extend: 'pdfHtml5',
|
|
className: 'buttons-pdf',
|
|
title: 'Doctors List',
|
|
filename: 'doctors_list_' + new Date().toISOString().split('T')[0],
|
|
orientation: 'landscape',
|
|
pageSize: 'A4',
|
|
exportOptions: {
|
|
columns: [0, 1, 2, 3, 4, 5, 6]
|
|
}
|
|
}
|
|
],
|
|
language: {
|
|
// processing: '<span class="text-primary"><i class="bi bi-arrow-repeat me-1"></i>Loading doctors...</span>',
|
|
// loadingRecords: 'Loading doctors...',
|
|
search: 'Search doctors:',
|
|
lengthMenu: 'Show _MENU_ doctors',
|
|
info: 'Showing _START_ to _END_ of _TOTAL_ doctors',
|
|
paginate: {
|
|
first: 'First',
|
|
last: 'Last',
|
|
next: 'Next',
|
|
previous: 'Previous'
|
|
},
|
|
emptyTable: 'No doctors found',
|
|
zeroRecords: 'No matching doctors found'
|
|
}
|
|
});
|
|
|
|
setInterval(function () {
|
|
if (doctorsTable) {
|
|
doctorsTable.ajax.reload(null, false);
|
|
}
|
|
}, 15000);
|
|
});
|
|
|
|
function exportTable(format) {
|
|
if (!doctorsTable) {
|
|
return;
|
|
}
|
|
|
|
if (format === 'csv') {
|
|
doctorsTable.button('.buttons-csv').trigger();
|
|
} else if (format === 'excel') {
|
|
doctorsTable.button('.buttons-excel').trigger();
|
|
} else if (format === 'pdf') {
|
|
doctorsTable.button('.buttons-pdf').trigger();
|
|
}
|
|
}
|
|
|
|
$(document).on('click', '.specialization-toggle', function () {
|
|
const targetId = $(this).data('target');
|
|
const showLabel = $(this).data('show-label');
|
|
const hideLabel = $(this).data('hide-label');
|
|
const $target = $('#' + targetId);
|
|
const $allToggles = $('.specialization-toggle[data-target="' + targetId + '"]');
|
|
|
|
$target.toggleClass('d-none');
|
|
if ($target.hasClass('d-none')) {
|
|
expandedSpecializations.delete(targetId);
|
|
} else {
|
|
expandedSpecializations.add(targetId);
|
|
}
|
|
|
|
$allToggles.each(function () {
|
|
const isInsideExpandedArea = $(this).closest('.extra-specializations').length > 0;
|
|
$(this).toggleClass('d-none', $target.hasClass('d-none') ? isInsideExpandedArea : !isInsideExpandedArea);
|
|
});
|
|
|
|
$allToggles.text($target.hasClass('d-none') ? showLabel : hideLabel);
|
|
});
|
|
// $(document).on('click', '.add-slot', function () {
|
|
// const day = $(this).data('day');
|
|
|
|
// const slotHtml = `
|
|
// <div class="slot-box d-flex align-items-center gap-1 border rounded p-1">
|
|
// <input type="time" class="form-control form-control-sm start-time" style="width:110px;">
|
|
// <span>-</span>
|
|
// <input type="time" class="form-control form-control-sm end-time" style="width:110px;">
|
|
// <button type="button" class="btn btn-sm btn-danger remove-slot">
|
|
// <i class="bi bi-x"></i>
|
|
// </button>
|
|
// </div>
|
|
// `;
|
|
|
|
// $(`#slots-${day}`).append(slotHtml);
|
|
// updateBadges();
|
|
// });
|
|
// Add slot handler: always render error containers under each input
|
|
$(document).on('click', '.add-slot', function () {
|
|
const day = $(this).data('day');
|
|
const slotHtml = `
|
|
<div class="slot-box border rounded p-1 mb-1">
|
|
<div class="d-flex align-items-center gap-1">
|
|
<div class="d-flex flex-column align-items-start slot-time-col">
|
|
<input type="time" class="form-control form-control-sm start-time" style="width:110px;">
|
|
</div>
|
|
<span>-</span>
|
|
<div class="d-flex flex-column align-items-start slot-time-col">
|
|
<input type="time" class="form-control form-control-sm end-time" style="width:110px;">
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-danger remove-slot slot-remove-btn" tabindex="-1">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
</div>
|
|
<span class="text-danger slot-error"></span>
|
|
</div>
|
|
`;
|
|
$(`#slots-${day}`).append(slotHtml);
|
|
});
|
|
// Remove slot handler
|
|
$(document).on('click', '.remove-slot', function () {
|
|
$(this).closest('.slot-box').remove();
|
|
});
|
|
// Real-time validation for slot time inputs
|
|
$(document).on('blur change', '.start-time, .end-time', function () {
|
|
const slotBox = $(this).closest('.slot-box');
|
|
const startInput = slotBox.find('.start-time');
|
|
const endInput = slotBox.find('.end-time');
|
|
const startError = slotBox.find('.slot-error');
|
|
const endError = slotBox.find('.slot-error');
|
|
|
|
// Clear previous errors
|
|
startError.text('');
|
|
endError.text('');
|
|
startInput.removeClass('is-invalid');
|
|
endInput.removeClass('is-invalid');
|
|
|
|
const start = startInput.val();
|
|
const end = endInput.val();
|
|
|
|
if (!start) {
|
|
startError.text('Start time is required');
|
|
startInput.addClass('is-invalid');
|
|
return;
|
|
}
|
|
if (!end) {
|
|
endError.text('End time is required');
|
|
endInput.addClass('is-invalid');
|
|
return;
|
|
}
|
|
const startTime = new Date(`1970-01-01T${start}:00`);
|
|
const endTime = new Date(`1970-01-01T${end}:00`);
|
|
const minTime = new Date('1970-01-01T09:00:00');
|
|
const maxTime = new Date('1970-01-01T19:00:00');
|
|
const diff = (endTime - startTime) / (1000 * 60);
|
|
|
|
if (startTime < minTime || startTime > maxTime) {
|
|
startError.text('Start time must be between 9:00 AM and 7:00 PM');
|
|
startInput.addClass('is-invalid');
|
|
return;
|
|
}
|
|
if (endTime < minTime || endTime > maxTime) {
|
|
endError.text('End time must be between 9:00 AM and 7:00 PM');
|
|
endInput.addClass('is-invalid');
|
|
return;
|
|
}
|
|
|
|
if (startTime >= endTime) {
|
|
endError.text('End time must be greater than start time');
|
|
endInput.addClass('is-invalid');
|
|
return;
|
|
}
|
|
if (diff < 15) {
|
|
endError.text('Minimum slot is 15 minutes');
|
|
endInput.addClass('is-invalid');
|
|
return;
|
|
}
|
|
if (diff > 180) {
|
|
endError.text('Maximum slot is 3 hours');
|
|
endInput.addClass('is-invalid');
|
|
return;
|
|
}
|
|
});
|
|
$(document).on('input focus', '.start-time, .end-time', function () {
|
|
$(this).removeClass('is-invalid');
|
|
$(this).closest('.slot-box').find('.error-msg').text('');
|
|
});
|
|
$(document).on('change', '.start-time, .end-time', function () {
|
|
updateBadges();
|
|
});
|
|
|
|
|
|
$(document).on('click', '.open-availability-modal', function () {
|
|
|
|
const doctorId = $(this).data('id');
|
|
const doctorName = $(this).data('name');
|
|
|
|
// Reset the form completely
|
|
document.getElementById('availabilityForm').reset();
|
|
$('#selectedBadges').html('');
|
|
|
|
// Set doctor ID and name
|
|
$('#doctorId').val(doctorId);
|
|
$('#doctorNameText').text(doctorName);
|
|
|
|
// Build empty day rows
|
|
renderAvailabilityUI();
|
|
|
|
// Fetch ACTIVE slots only
|
|
$.get('/admin/availability/' + doctorId, function(slots){
|
|
|
|
slots.forEach(function(slot){
|
|
|
|
const slotHtml = `
|
|
<div class="slot-box border rounded p-1 mb-2">
|
|
<div class="d-flex align-items-center gap-1">
|
|
<div class="d-flex flex-column align-items-start slot-time-col">
|
|
<input type="time" class="form-control form-control-sm start-time" value="${slot.start_time}" style="width:110px;">
|
|
</div>
|
|
|
|
<span>-</span>
|
|
|
|
<div class="d-flex flex-column align-items-start slot-time-col">
|
|
<input type="time" class="form-control form-control-sm end-time" value="${slot.end_time}" style="width:110px;">
|
|
</div>
|
|
|
|
<button type="button" class="btn btn-sm btn-danger remove-slot slot-remove-btn" tabindex="-1"> <i class="bi bi-x"></i></button>
|
|
</div>
|
|
<span class="text-danger slot-error"></span>
|
|
</div>
|
|
`;
|
|
|
|
$('#slots-' + slot.day_number).append(slotHtml);
|
|
|
|
});
|
|
|
|
updateBadges();
|
|
|
|
});
|
|
|
|
const modal = new bootstrap.Modal(
|
|
document.getElementById('availabilityModal')
|
|
);
|
|
|
|
modal.show();
|
|
|
|
});
|
|
// $('#availabilityForm').on('submit', function (e) {
|
|
// let availability = {};
|
|
|
|
// $('.day-row').each(function () {
|
|
// const day = $(this).data('day');
|
|
// const slots = [];
|
|
|
|
// $(this).find('.slot-box').each(function () {
|
|
// const start = $(this).find('.start-time').val();
|
|
// const end = $(this).find('.end-time').val();
|
|
|
|
// if (start && end) {
|
|
// slots.push({
|
|
// start: start,
|
|
// end: end
|
|
// });
|
|
// }
|
|
// });
|
|
|
|
// if (slots.length > 0) {
|
|
// availability[day] = slots;
|
|
// }
|
|
// });
|
|
|
|
// // Validate availability data
|
|
// if (Object.keys(availability).length === 0) {
|
|
// alert('Please add at least one time slot!');
|
|
// e.preventDefault();
|
|
// return false;
|
|
// }
|
|
|
|
// const doctorId = $('#doctorId').val();
|
|
// if (!doctorId) {
|
|
// alert('Doctor ID is missing!');
|
|
// e.preventDefault();
|
|
// return false;
|
|
// }
|
|
|
|
// console.log('Submitting availability for doctor:', doctorId);
|
|
// console.log('Availability data:', availability);
|
|
|
|
// // Remove any existing hidden input to avoid duplicates
|
|
// $('input[name="availability_json"]').remove();
|
|
|
|
// $('<input>')
|
|
// .attr('type', 'hidden')
|
|
// .attr('name', 'availability_json')
|
|
// .val(JSON.stringify(availability))
|
|
// .appendTo(this);
|
|
|
|
// return true;
|
|
// });
|
|
function updateBadges() {
|
|
const badgeContainer = $('#selectedBadges');
|
|
badgeContainer.html('');
|
|
|
|
let slotSet = new Set(); // track duplicates
|
|
|
|
$('.day-row').each(function () {
|
|
const dayValue = $(this).data('day');
|
|
const dayObj = days.find(d => d.value == dayValue);
|
|
const dayName = dayObj ? dayObj.name : '';
|
|
|
|
$(this).find('.slot-box').each(function () {
|
|
const start = $(this).find('.start-time').val();
|
|
const end = $(this).find('.end-time').val();
|
|
|
|
if (start && end) {
|
|
|
|
const uniqueKey = `${dayValue}-${start}-${end}`;
|
|
|
|
// ❌ DUPLICATE DETECTED
|
|
if (slotSet.has(uniqueKey)) {
|
|
$(this).addClass('border-danger'); // highlight duplicate
|
|
return;
|
|
}
|
|
|
|
slotSet.add(uniqueKey);
|
|
$(this).removeClass('border-danger');
|
|
|
|
const badge = `
|
|
<span class="badge bg-primary slot-badge"
|
|
data-day="${dayValue}"
|
|
data-start="${start}"
|
|
data-end="${end}"
|
|
style="cursor:pointer;">
|
|
${dayName.substring(0,3)} ${start} - ${end}
|
|
<i class="fa-solid fa-xmark ms-1"></i>
|
|
</span>
|
|
`;
|
|
|
|
badgeContainer.append(badge);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
$(document).on('change', '.start-time, .end-time', function () {
|
|
updateBadges();
|
|
});
|
|
$(document).on('click', '.slot-badge', function () {
|
|
|
|
const day = $(this).data('day');
|
|
const start = $(this).data('start');
|
|
const end = $(this).data('end');
|
|
|
|
// find matching slot and remove
|
|
$(`#slots-${day} .slot-box`).each(function () {
|
|
const s = $(this).find('.start-time').val();
|
|
const e = $(this).find('.end-time').val();
|
|
|
|
if (s === start && e === end) {
|
|
$(this).remove();
|
|
}
|
|
});
|
|
|
|
updateBadges(); // refresh UI
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|