313 lines
12 KiB
PHP
313 lines
12 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../db/config.php';
|
|
require_once __DIR__ . '/../helpers.php';
|
|
|
|
header('Content-Type: application/json');
|
|
|
|
$db = db();
|
|
$lang = $_SESSION['lang'] ?? 'en';
|
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
|
|
|
if ($method === 'GET') {
|
|
$id = $_GET['id'] ?? null;
|
|
if ($id) {
|
|
// Fetch single appointment
|
|
$stmt = $db->prepare("SELECT * FROM appointments WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$appointment = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
echo json_encode($appointment);
|
|
exit;
|
|
}
|
|
|
|
$startStr = $_GET['start'] ?? null;
|
|
$endStr = $_GET['end'] ?? null;
|
|
$doctor_id = $_GET['doctor_id'] ?? null;
|
|
|
|
$events = [];
|
|
$businessHours = [];
|
|
|
|
// Fetch Appointments
|
|
$query = "
|
|
SELECT
|
|
a.id, a.start_time as start, a.end_time as end, a.reason, a.status,
|
|
a.patient_id, a.doctor_id,
|
|
p.name as patient_name,
|
|
d.name_$lang as doctor_name
|
|
FROM appointments a
|
|
JOIN patients p ON a.patient_id = p.id
|
|
JOIN doctors d ON a.doctor_id = d.id
|
|
WHERE 1=1";
|
|
|
|
$params = [];
|
|
if ($startStr) { $query .= " AND a.start_time >= ?"; $params[] = $startStr; }
|
|
if ($endStr) { $query .= " AND a.start_time <= ?"; $params[] = $endStr; }
|
|
if ($doctor_id) { $query .= " AND a.doctor_id = ?"; $params[] = $doctor_id; }
|
|
|
|
$stmt = $db->prepare($query);
|
|
$stmt->execute($params);
|
|
$appointments = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($appointments as $a) {
|
|
$color = '#0d6efd'; // blue
|
|
if ($a['status'] === 'Completed') $color = '#198754'; // green
|
|
if ($a['status'] === 'Cancelled') $color = '#dc3545'; // red
|
|
|
|
$events[] = [
|
|
'id' => $a['id'],
|
|
'title' => $a['patient_name'] . ' (' . $a['doctor_name'] . ')',
|
|
'start' => $a['start'],
|
|
'end' => $a['end'],
|
|
'color' => $color,
|
|
'extendedProps' => [
|
|
'type' => 'appointment',
|
|
'patient_id' => $a['patient_id'],
|
|
'doctor_id' => $a['doctor_id'],
|
|
'patient_name' => $a['patient_name'],
|
|
'doctor_name' => $a['doctor_name'],
|
|
'status' => $a['status'],
|
|
'reason' => $a['reason']
|
|
]
|
|
];
|
|
}
|
|
|
|
// Fetch Public Holidays
|
|
$holidayQuery = "SELECT holiday_date as start, name_$lang as title FROM holidays WHERE 1=1";
|
|
$holidayParams = [];
|
|
if ($startStr) { $holidayQuery .= " AND holiday_date >= ?"; $holidayParams[] = date('Y-m-d', strtotime($startStr)); }
|
|
if ($endStr) { $holidayQuery .= " AND holiday_date <= ?"; $holidayParams[] = date('Y-m-d', strtotime($endStr)); }
|
|
|
|
$stmt = $db->prepare($holidayQuery);
|
|
$stmt->execute($holidayParams);
|
|
$holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($holidays as $h) {
|
|
// Render a visible block event in the time grid
|
|
$events[] = [
|
|
'id' => 'hol_block_' . $h['start'],
|
|
'title' => __('holiday') . ': ' . $h['title'],
|
|
'start' => $h['start'] . 'T' . $global_start . ':00',
|
|
'end' => $h['start'] . 'T' . $global_end . ':00',
|
|
'allDay' => false,
|
|
'color' => '#dc3545',
|
|
'textColor' => '#fff',
|
|
'className' => 'public-holiday-event',
|
|
'extendedProps' => ['type' => 'public_holiday_block', 'blocks_selection' => true]
|
|
];
|
|
|
|
// Render daily blocks for time grid
|
|
$events[] = [
|
|
'id' => 'hol_bg_' . $h['start'],
|
|
'start' => $h['start'] . 'T00:00:00',
|
|
'end' => $h['start'] . 'T23:59:59',
|
|
'allDay' => false,
|
|
'display' => 'background',
|
|
'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']
|
|
];
|
|
}
|
|
|
|
// Fetch Doctor Holidays
|
|
$docHolidayQuery = "SELECT dh.*, d.name_$lang as doctor_name FROM doctor_holidays dh JOIN doctors d ON dh.doctor_id = d.id WHERE 1=1";
|
|
$docHolidayParams = [];
|
|
|
|
// Date filtering for doctor holidays (ranges)
|
|
if ($startStr && $endStr) {
|
|
$docHolidayQuery .= " AND dh.start_date <= ? AND dh.end_date >= ?";
|
|
$docHolidayParams[] = date('Y-m-d', strtotime($endStr));
|
|
$docHolidayParams[] = date('Y-m-d', strtotime($startStr));
|
|
}
|
|
|
|
if ($doctor_id) {
|
|
$docHolidayQuery .= " AND dh.doctor_id = ?";
|
|
$docHolidayParams[] = $doctor_id;
|
|
}
|
|
|
|
$stmt = $db->prepare($docHolidayQuery);
|
|
$stmt->execute($docHolidayParams);
|
|
$docHolidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($docHolidays as $dh) {
|
|
$title = $dh['doctor_name'] . ' - ' . ($dh['note'] ?: 'Holiday');
|
|
$endDayStr = date('Y-m-d', strtotime($dh['end_date'] . ' +1 day'));
|
|
|
|
$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']);
|
|
|
|
// Render a visible block event in the time grid so it's super obvious
|
|
$s = get_system_settings();
|
|
$global_start = $s['working_hours_start'] ?? '08:00';
|
|
$global_end = $s['working_hours_end'] ?? '17:00';
|
|
|
|
$events[] = [
|
|
'id' => 'doc_hol_block_' . $dh['id'] . '_' . $dateStr,
|
|
'title' => 'Holiday: ' . $dh['doctor_name'],
|
|
'start' => $dateStr . 'T' . $global_start . ':00',
|
|
'end' => $dateStr . 'T' . $global_end . ':00',
|
|
'allDay' => false,
|
|
'color' => '#ffc107',
|
|
'textColor' => '#000',
|
|
'className' => 'doctor-holiday-event',
|
|
'extendedProps' => ['type' => 'doctor_holiday_block', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor]
|
|
];
|
|
|
|
// Keep the background shading
|
|
$events[] = [
|
|
'id' => 'doc_hol_bg_' . $dh['id'] . '_' . $dateStr,
|
|
'start' => $dateStr . 'T00:00:00',
|
|
'end' => $dateStr . 'T23:59:59',
|
|
'display' => 'background',
|
|
'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, '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'], 'blocks_selection' => $isFilteredDoctor]
|
|
];
|
|
}
|
|
|
|
// Fetch Doctor Business Hours
|
|
if ($doctor_id) {
|
|
$scheduleStmt = $db->prepare("SELECT day_of_week as day, start_time as start, end_time as end FROM doctor_schedules WHERE doctor_id = ?");
|
|
$scheduleStmt->execute([$doctor_id]);
|
|
$schedules = $scheduleStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$bhMap = [];
|
|
foreach ($schedules as $s) {
|
|
$key = $s['start'] . '-' . $s['end'];
|
|
if (!isset($bhMap[$key])) {
|
|
$bhMap[$key] = [
|
|
'daysOfWeek' => [],
|
|
'startTime' => $s['start'],
|
|
'endTime' => $s['end']
|
|
];
|
|
}
|
|
$bhMap[$key]['daysOfWeek'][] = (int)$s['day'];
|
|
}
|
|
$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' => $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,
|
|
'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'] ?? '';
|
|
|
|
if ($action === 'create') {
|
|
$patient_id = $input['patient_id'] ?? '';
|
|
$doctor_id = $input['doctor_id'] ?? '';
|
|
$start_time = $input['start_time'] ?? '';
|
|
$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()]);
|
|
} else {
|
|
echo json_encode(['success' => false, 'error' => 'Missing fields']);
|
|
}
|
|
} elseif ($action === 'update') {
|
|
$id = $input['id'] ?? '';
|
|
$patient_id = $input['patient_id'] ?? '';
|
|
$doctor_id = $input['doctor_id'] ?? '';
|
|
$start_time = $input['start_time'] ?? '';
|
|
$status = $input['status'] ?? 'Scheduled';
|
|
$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]);
|
|
} else {
|
|
echo json_encode(['success' => false, 'error' => 'Missing fields']);
|
|
}
|
|
} elseif ($action === 'delete') {
|
|
$id = $input['id'] ?? '';
|
|
if ($id) {
|
|
$stmt = $db->prepare("DELETE FROM appointments WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
echo json_encode(['success' => true]);
|
|
} else {
|
|
echo json_encode(['success' => false, 'error' => 'Missing ID']);
|
|
}
|
|
}
|
|
exit;
|
|
} |