adding working time
This commit is contained in:
parent
4ea57c7524
commit
4f4d8efa33
@ -6,7 +6,7 @@ header('Content-Type: application/json');
|
||||
|
||||
$db = db();
|
||||
$lang = $_SESSION['lang'] ?? 'en';
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
if ($method === 'GET') {
|
||||
$id = $_GET['id'] ?? null;
|
||||
@ -81,16 +81,26 @@ if ($method === 'GET') {
|
||||
$holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($holidays as $h) {
|
||||
// Render daily blocks for time grid
|
||||
$events[] = [
|
||||
'id' => 'hol_' . $h['start'],
|
||||
'title' => __('holiday') . ': ' . $h['title'],
|
||||
'id' => 'hol_bg_' . $h['start'],
|
||||
'start' => $h['start'] . 'T00:00:00',
|
||||
'end' => $h['start'] . 'T23:59:59',
|
||||
'allDay' => false, // Set to false to cover vertical time slots
|
||||
'allDay' => false,
|
||||
'display' => 'background',
|
||||
'backgroundColor' => '#ffc107', // Explicitly set background color
|
||||
'color' => '#ffc107',
|
||||
'backgroundColor' => 'rgba(255, 193, 7, 0.5)',
|
||||
'overlap' => false,
|
||||
'extendedProps' => ['type' => 'public_holiday', 'blocks_selection' => true]
|
||||
];
|
||||
// Visible event strip across the top
|
||||
$events[] = [
|
||||
'id' => 'hol_title_' . $h['start'],
|
||||
'title' => __('holiday') . ': ' . $h['title'],
|
||||
'start' => $h['start'],
|
||||
'end' => $h['start'],
|
||||
'allDay' => true,
|
||||
'color' => '#ffc107',
|
||||
'textColor' => '#000',
|
||||
'extendedProps' => ['type' => 'public_holiday']
|
||||
];
|
||||
}
|
||||
@ -117,34 +127,46 @@ if ($method === 'GET') {
|
||||
|
||||
foreach ($docHolidays as $dh) {
|
||||
$title = $dh['doctor_name'] . ' - ' . ($dh['note'] ?: 'Holiday');
|
||||
$endDayStr = date('Y-m-d', strtotime($dh['end_date'] . ' +1 day'));
|
||||
|
||||
// If filtering by specific doctor, show as background to block the day clearly
|
||||
// If showing all doctors, show as a block event so we know WHO is on holiday
|
||||
if ($doctor_id) {
|
||||
$currentDate = strtotime($dh['start_date']);
|
||||
$endDate = strtotime($dh['end_date']);
|
||||
|
||||
// Output background events for each day individually to ensure they render in the time grid perfectly
|
||||
while ($currentDate <= $endDate) {
|
||||
$dateStr = date('Y-m-d', $currentDate);
|
||||
|
||||
// To block selection, FullCalendar relies on selectOverlap checking properties.
|
||||
// We'll set 'overlap: false' when filtered to a specific doctor.
|
||||
// Also we give it time 00:00:00 to 24:00:00 to fill the column.
|
||||
|
||||
$isFilteredDoctor = ($doctor_id && $doctor_id == $dh['doctor_id']);
|
||||
|
||||
$events[] = [
|
||||
'id' => 'doc_hol_' . $dh['id'],
|
||||
'title' => 'Holiday',
|
||||
'start' => $dh['start_date'] . 'T00:00:00',
|
||||
'end' => date('Y-m-d', strtotime($dh['end_date'] . ' +1 day')) . 'T00:00:00',
|
||||
'allDay' => false, // Set to false to cover vertical time slots
|
||||
'id' => 'doc_hol_bg_' . $dh['id'] . '_' . $dateStr,
|
||||
'start' => $dateStr . 'T00:00:00',
|
||||
'end' => $dateStr . 'T23:59:59',
|
||||
'display' => 'background',
|
||||
'backgroundColor' => '#ffc107', // Explicitly set background color
|
||||
'color' => '#ffc107',
|
||||
'overlap' => false, // Prevent booking on holiday
|
||||
'extendedProps' => ['type' => 'doctor_holiday']
|
||||
];
|
||||
} else {
|
||||
$events[] = [
|
||||
'id' => 'doc_hol_' . $dh['id'],
|
||||
'title' => $title,
|
||||
'start' => $dh['start_date'],
|
||||
'end' => date('Y-m-d', strtotime($dh['end_date'] . ' +1 day')),
|
||||
'allDay' => true,
|
||||
'color' => '#fd7e14', // Orange
|
||||
'textColor' => '#fff',
|
||||
'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id']]
|
||||
'allDay' => false,
|
||||
'backgroundColor' => $isFilteredDoctor ? 'rgba(255, 193, 7, 0.5)' : 'rgba(255, 193, 7, 0.15)',
|
||||
'overlap' => !$isFilteredDoctor,
|
||||
'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor]
|
||||
];
|
||||
|
||||
$currentDate = strtotime('+1 day', $currentDate);
|
||||
}
|
||||
|
||||
// Visible event strip across the top (allDay)
|
||||
$events[] = [
|
||||
'id' => 'doc_hol_' . $dh['id'],
|
||||
'title' => $title,
|
||||
'start' => $dh['start_date'],
|
||||
'end' => $endDayStr,
|
||||
'allDay' => true,
|
||||
'color' => '#ffc107',
|
||||
'textColor' => '#000',
|
||||
'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id']]
|
||||
];
|
||||
}
|
||||
|
||||
// Fetch Doctor Business Hours
|
||||
@ -167,22 +189,41 @@ if ($method === 'GET') {
|
||||
}
|
||||
$businessHours = array_values($bhMap);
|
||||
} else {
|
||||
$s = get_system_settings();
|
||||
$st = $s['working_hours_start'] ?? '08:00';
|
||||
$et = $s['working_hours_end'] ?? '17:00';
|
||||
$businessHours = [
|
||||
[
|
||||
'daysOfWeek' => [0, 1, 2, 3, 4, 5, 6],
|
||||
'startTime' => '08:00',
|
||||
'endTime' => '17:00'
|
||||
'startTime' => $st,
|
||||
'endTime' => $et
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$s = get_system_settings();
|
||||
$global_start = $s['working_hours_start'] ?? '08:00';
|
||||
$global_end = $s['working_hours_end'] ?? '17:00';
|
||||
|
||||
echo json_encode([
|
||||
'events' => $events,
|
||||
'businessHours' => $businessHours
|
||||
'businessHours' => $businessHours,
|
||||
'settings' => [
|
||||
'working_hours_start' => $global_start,
|
||||
'working_hours_end' => $global_end
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
function checkDoctorHoliday($db, $doctor_id, $start_time) {
|
||||
if (!$doctor_id || !$start_time) return false;
|
||||
$date = date('Y-m-d', strtotime($start_time));
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM doctor_holidays WHERE doctor_id = ? AND ? BETWEEN start_date AND end_date");
|
||||
$stmt->execute([$doctor_id, $date]);
|
||||
return $stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
|
||||
$action = $input['action'] ?? '';
|
||||
@ -194,6 +235,12 @@ if ($method === 'POST') {
|
||||
$reason = $input['reason'] ?? '';
|
||||
|
||||
if ($patient_id && $doctor_id && $start_time) {
|
||||
// Check for holiday conflict
|
||||
if (checkDoctorHoliday($db, $doctor_id, $start_time)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Doctor is on holiday on this date.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO appointments (patient_id, doctor_id, start_time, end_time, reason) VALUES (?, ?, ?, DATE_ADD(?, INTERVAL 30 MINUTE), ?)");
|
||||
$stmt->execute([$patient_id, $doctor_id, $start_time, $start_time, $reason]);
|
||||
echo json_encode(['success' => true, 'id' => $db->lastInsertId()]);
|
||||
@ -209,6 +256,12 @@ if ($method === 'POST') {
|
||||
$reason = $input['reason'] ?? '';
|
||||
|
||||
if ($id && $patient_id && $doctor_id && $start_time) {
|
||||
// Check for holiday conflict
|
||||
if (checkDoctorHoliday($db, $doctor_id, $start_time)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Doctor is on holiday on this date.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE appointments SET patient_id = ?, doctor_id = ?, start_time = ?, end_time = DATE_ADD(?, INTERVAL 30 MINUTE), status = ?, reason = ? WHERE id = ?");
|
||||
$stmt->execute([$patient_id, $doctor_id, $start_time, $start_time, $status, $reason, $id]);
|
||||
echo json_encode(['success' => true]);
|
||||
@ -226,4 +279,4 @@ if ($method === 'POST') {
|
||||
}
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
29
helpers.php
29
helpers.php
@ -1,4 +1,33 @@
|
||||
<?php
|
||||
|
||||
function get_system_settings() {
|
||||
global $db; // Assuming db() is already initialized or we can initialize it
|
||||
if (!isset($db)) {
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
$local_db = db();
|
||||
} else {
|
||||
$local_db = $db;
|
||||
}
|
||||
try {
|
||||
$stmt = $local_db->query('SELECT setting_key, setting_value FROM settings');
|
||||
$settings = [];
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$settings[$row['setting_key']] = $row['setting_value'];
|
||||
}
|
||||
return $settings;
|
||||
} catch (Exception $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function apply_timezone() {
|
||||
$s = get_system_settings();
|
||||
if (!empty($s['timezone'])) {
|
||||
date_default_timezone_set($s['timezone']);
|
||||
}
|
||||
}
|
||||
apply_timezone();
|
||||
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/lang.php';
|
||||
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
<?php
|
||||
// includes/pages/appointments.php
|
||||
?>
|
||||
|
||||
<?php $s = get_system_settings(); ?>
|
||||
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/main.min.css' rel='stylesheet' />
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script>
|
||||
|
||||
@ -237,8 +234,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||
},
|
||||
allDaySlot: true,
|
||||
slotMinTime: '07:00:00',
|
||||
slotMaxTime: '21:00:00',
|
||||
slotMinTime: '<?php echo substr(($s['working_hours_start'] ?? '07:00'), 0, 5) . ':00'; ?>',
|
||||
slotMaxTime: '<?php echo substr(($s['working_hours_end'] ?? '21:00'), 0, 5) . ':00'; ?>',
|
||||
height: 'auto',
|
||||
themeSystem: 'bootstrap5',
|
||||
businessHours: true,
|
||||
@ -249,12 +246,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (data.businessHours) {
|
||||
calendar.setOption('businessHours', data.businessHours);
|
||||
}
|
||||
console.log('Events fetched:', data.events); // Debug log
|
||||
successCallback(data.events);
|
||||
})
|
||||
.catch(error => failureCallback(error));
|
||||
},
|
||||
editable: false,
|
||||
selectable: true,
|
||||
selectOverlap: function(event) {
|
||||
if (event.extendedProps && event.extendedProps.blocks_selection) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
selectAllow: function(selectInfo) {
|
||||
// Also optionally check if we can select
|
||||
return true;
|
||||
},
|
||||
select: function(info) {
|
||||
showCreateModal(info.start);
|
||||
calendar.unselect();
|
||||
@ -292,11 +300,66 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Select2 after some delay to ensure modal is ready
|
||||
$('#appointmentDetailsModal').on('shown.bs.modal', function() {
|
||||
initAptSelect2();
|
||||
validateHolidayFrontend();
|
||||
});
|
||||
|
||||
$('#apt_doctor_id').on('change', validateHolidayFrontend);
|
||||
$('#apt_start_time').on('change', validateHolidayFrontend);
|
||||
|
||||
function validateHolidayFrontend() {
|
||||
var docId = $('#apt_doctor_id').val();
|
||||
var startTimeStr = $('#apt_start_time').val();
|
||||
var btnSave = document.getElementById('btnSaveApt');
|
||||
|
||||
if (!docId || !startTimeStr) return;
|
||||
|
||||
var datePrefix = startTimeStr.split('T')[0];
|
||||
var events = calendar.getEvents();
|
||||
var isHoliday = false;
|
||||
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var ev = events[i];
|
||||
// We check against the allDay visible event OR background events
|
||||
if (ev.extendedProps && ev.extendedProps.type === 'doctor_holiday' && ev.extendedProps.doctor_id == docId) {
|
||||
var evStartStr = ev.startStr.split('T')[0];
|
||||
var evEndStr = ev.end ? ev.endStr.split('T')[0] : evStartStr;
|
||||
|
||||
// If it's an allDay event, the end date might be exclusive (e.g. 17th means up to 16th 23:59:59)
|
||||
// If it's our new daily block background event, start and end are the same day
|
||||
if (ev.allDay) {
|
||||
if (datePrefix >= evStartStr && datePrefix < evEndStr) {
|
||||
isHoliday = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (datePrefix === evStartStr) {
|
||||
isHoliday = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isHoliday) {
|
||||
if ($('#holidayWarning').length === 0) {
|
||||
$('<div id="holidayWarning" class="alert alert-warning mt-3 mb-0 small"><i class="bi bi-exclamation-triangle"></i> Selected doctor is on holiday on this date.</div>').appendTo('#appointmentDetailsModal .modal-body');
|
||||
}
|
||||
btnSave.disabled = true;
|
||||
} else {
|
||||
$('#holidayWarning').remove();
|
||||
btnSave.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Standard event styles now */
|
||||
.doctor-holiday-bg {
|
||||
border: 1px solid #e0a800 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fc-event { cursor: pointer; border: none; padding: 2px; transition: transform 0.1s ease; }
|
||||
.fc-event:hover { transform: scale(1.02); z-index: 5; }
|
||||
.fc-event-title { font-weight: 500; font-size: 0.85rem; }
|
||||
|
||||
@ -51,6 +51,30 @@
|
||||
<input type="text" class="form-control" id="company_vat_no" name="company_vat_no" value="<?php echo htmlspecialchars($settings['company_vat_no'] ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<!-- Timezone & Working Hours -->
|
||||
<div class="col-12 mt-4"><hr></div>
|
||||
<div class="col-md-4">
|
||||
<label for="timezone" class="form-label fw-semibold text-muted small text-uppercase"><?php echo __('timezone'); ?></label>
|
||||
<select class="form-select" id="timezone" name="timezone">
|
||||
<?php
|
||||
$timezones = DateTimeZone::listIdentifiers();
|
||||
$current_tz = $settings['timezone'] ?? 'UTC';
|
||||
foreach ($timezones as $tz) {
|
||||
$selected = ($tz === $current_tz) ? 'selected' : '';
|
||||
echo "<option value=\"{$tz}\" {$selected}>{$tz}</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="working_hours_start" class="form-label fw-semibold text-muted small text-uppercase"><?php echo __('working_hours_start'); ?></label>
|
||||
<input type="time" class="form-control" id="working_hours_start" name="working_hours_start" value="<?php echo htmlspecialchars($settings['working_hours_start'] ?? '08:00'); ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="working_hours_end" class="form-label fw-semibold text-muted small text-uppercase"><?php echo __('working_hours_end'); ?></label>
|
||||
<input type="time" class="form-control" id="working_hours_end" name="working_hours_end" value="<?php echo htmlspecialchars($settings['working_hours_end'] ?? '17:00'); ?>">
|
||||
</div>
|
||||
|
||||
<!-- Branding -->
|
||||
<div class="col-md-6 mt-5">
|
||||
<label for="company_logo" class="form-label fw-semibold text-muted small text-uppercase"><?php echo __('company_logo'); ?></label>
|
||||
|
||||
6
lang.php
6
lang.php
@ -199,6 +199,9 @@ $translations = [
|
||||
'ctr_no' => 'CTR No',
|
||||
'registration_no' => 'Registration No',
|
||||
'vat_no' => 'VAT No',
|
||||
'timezone' => 'Timezone',
|
||||
'working_hours_start' => 'Working Hours Start',
|
||||
'working_hours_end' => 'Working Hours End',
|
||||
'company_logo' => 'Company Logo',
|
||||
'company_favicon' => 'Company Favicon',
|
||||
'save_changes' => 'Save Changes',
|
||||
@ -487,6 +490,9 @@ $translations = [
|
||||
'ctr_no' => 'رقم CTR',
|
||||
'registration_no' => 'رقم التسجيل',
|
||||
'vat_no' => 'الرقم الضريبي',
|
||||
'timezone' => 'المنطقة الزمنية',
|
||||
'working_hours_start' => 'بداية ساعات العمل',
|
||||
'working_hours_end' => 'نهاية ساعات العمل',
|
||||
'company_logo' => 'شعار الشركة',
|
||||
'company_favicon' => 'أيقونة الشركة',
|
||||
'save_changes' => 'حفظ التغييرات',
|
||||
|
||||
1
p.php
Normal file
1
p.php
Normal file
@ -0,0 +1 @@
|
||||
<?php $content = file_get_contents('lang.php'); $en_add = "'timezone' => 'Timezone',\n 'working_hours_start' => 'Working Hours Start',\n 'working_hours_end' => 'Working Hours End',"; $ar_add = "'timezone' => 'المنطقة الزمنية',\n 'working_hours_start' => 'بداية ساعات العمل',\n 'working_hours_end' => 'نهاية ساعات العمل',"; $content = str_replace("'company_vat_no' => 'VAT No',", "'company_vat_no' => 'VAT No',\n " . $en_add, $content); $content = str_replace("'vat_no' => 'الرقم الضريبي',", "'vat_no' => 'الرقم الضريبي',\n " . $ar_add, $content); file_put_contents('lang.php', $content); echo 'patched';
|
||||
1
p2.php
Normal file
1
p2.php
Normal file
@ -0,0 +1 @@
|
||||
<?php $c = file_get_contents('includes/pages/settings.php'); $new_fields = " <!-- Timezone & Working Hours -->\n <div class=\"col-12 mt-4\"><hr></div>\n <div class=\"col-md-4\">\n <label for=\"timezone\" class=\"form-label fw-semibold text-muted small text-uppercase\"><?php echo __('timezone'); ?></label>\n <select class=\"form-select\" id=\"timezone\" name=\"timezone\">\n <?php\n \$timezones = DateTimeZone::listIdentifiers();\n \$current_tz = \$settings['timezone'] ?? 'UTC';\n foreach (\$timezones as \$tz) {\n \$selected = (\$tz === \$current_tz) ? 'selected' : '';\n echo \"<option value=\\\"{\$tz}\\\" {\$selected}>{\$tz}</option>\";\n }\n ?>\n </select>\n </div>\n <div class=\"col-md-4\">\n <label for=\"working_hours_start\" class=\"form-label fw-semibold text-muted small text-uppercase\"><?php echo __('working_hours_start'); ?></label>\n <input type=\"time\" class=\"form-control\" id=\"working_hours_start\" name=\"working_hours_start\" value=\"<?php echo htmlspecialchars(\$settings['working_hours_start'] ?? '08:00'); ?>\">\n </div>\n <div class=\"col-md-4\">\n <label for=\"working_hours_end\" class=\"form-label fw-semibold text-muted small text-uppercase\"><?php echo __('working_hours_end'); ?></label>\n <input type=\"time\" class=\"form-control\" id=\"working_hours_end\" name=\"working_hours_end\" value=\"<?php echo htmlspecialchars(\$settings['working_hours_end'] ?? '17:00'); ?>\">\n </div>"; $c = str_replace('<!-- Branding -->', $new_fields . "\n\n <!-- Branding -->", $c); file_put_contents('includes/pages/settings.php', $c); echo 'patched settings UI';
|
||||
1
patch_settings_db.php
Normal file
1
patch_settings_db.php
Normal file
@ -0,0 +1 @@
|
||||
<?php require_once __DIR__ . '/db/config.php'; $db = db(); $s = ['timezone' => 'UTC', 'working_hours_start' => '08:00', 'working_hours_end' => '17:00']; foreach ($s as $k => $v) { try { $db->prepare('INSERT IGNORE INTO settings (setting_key, setting_value) VALUES (?, ?)')->execute([$k, $v]); } catch (Exception $e) {} } echo 'patched';
|
||||
@ -45,7 +45,7 @@ if ($doctor_id) {
|
||||
$query = "
|
||||
SELECT
|
||||
a.start_time, a.end_time, a.status, a.reason,
|
||||
p.name as patient_name, p.tel as patient_tel,
|
||||
p.name as patient_name, p.phone as patient_tel,
|
||||
d.name_$lang as doctor_name
|
||||
FROM appointments a
|
||||
JOIN patients p ON a.patient_id = p.id
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
$section = 'test_groups';
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/helpers.php';
|
||||
|
||||
$db = db();
|
||||
$lang = $_SESSION['lang'];
|
||||
|
||||
require_once __DIR__ . '/includes/actions.php';
|
||||
require_once __DIR__ . '/includes/common_data.php';
|
||||
|
||||
$is_ajax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
|
||||
|
||||
if (!$is_ajax) {
|
||||
require_once __DIR__ . '/includes/layout/header.php';
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/includes/pages/test_groups.php';
|
||||
|
||||
if (!$is_ajax) {
|
||||
require_once __DIR__ . '/includes/layout/footer.php';
|
||||
}
|
||||
?>
|
||||
Loading…
x
Reference in New Issue
Block a user