adding working time

This commit is contained in:
Flatlogic Bot 2026-03-16 10:31:04 +00:00
parent 4ea57c7524
commit 4f4d8efa33
10 changed files with 218 additions and 63 deletions

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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; }

View File

@ -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>

View File

@ -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
View 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
View 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
View 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';

View File

@ -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

View File

@ -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';
}
?>