<?php

declare(strict_types=1);

require DENTAL_ROOT . '/app/bootstrap.php';
require_login();
require_permission('appointments.manage', ['super_admin','clinic_admin','receptionist','staff','dentist','dental_assistant']);
require DENTAL_ROOT . '/app/layout.php';
require DENTAL_ROOT . '/app/module_helpers.php';
require_once DENTAL_ROOT . '/app/transaction_integrity.php';
require_once DENTAL_ROOT . '/app/queue_flow.php';
require_once DENTAL_ROOT . '/app/appointment_planning.php';
require_once DENTAL_ROOT . '/app/appointment_schedule_guard.php';

$pdo = db();
$user = current_user() ?? [];
$actualRole = actual_role();
$taskRole = active_role();
$branchId = (int)($user['branch_id'] ?? public_request_branch_id() ?? 0);
if ($branchId < 1) $branchId = (int)($pdo->query('SELECT id FROM `' . table('branches') . '` WHERE is_active=1 ORDER BY id LIMIT 1')->fetchColumn() ?: 0);

$dentists = active_dentists($branchId ?: null);
$services = appointment_booking_services_catalog($branchId ?: null);
$appointmentIntegrityReady = appointment_confirmation_schema_ready();
$error = '';
$selectedDentistId = 0;
if ($taskRole === 'dentist') {
    if (active_role_can_operate('dentist')) $selectedDentistId = (int)($user['id'] ?? 0);
    else {
        $requested = (int)($_GET['dentist_id'] ?? $_SESSION['task_dentist_id'] ?? 0);
        $ids = array_map(static fn(array $row): int => (int)$row['id'], $dentists);
        if (!in_array($requested,$ids,true)) $requested=(int)($ids[0]??0);
        $selectedDentistId=$requested;
        if($selectedDentistId>0) $_SESSION['task_dentist_id']=$selectedDentistId;
    }
}
$assistantDentistIds = ($taskRole === 'dental_assistant' && active_role_can_operate('dental_assistant')) ? task_dentist_scope_ids() : [];
$canManage = task_context_action_allowed('appointments.manage') && has_role(['super_admin','clinic_admin','receptionist','staff']);
$canProcessRequests = task_context_action_allowed('appointment_requests.manage') && has_role(['super_admin','clinic_admin','receptionist','staff']);
$canCheckIn = task_context_action_allowed('queue.intake') && has_role(['super_admin','clinic_admin','receptionist','staff']);
$canBypassSchedule = schedule_bypass_allowed() && can('appointments.schedule_bypass',['super_admin','clinic_admin']);
$canTransferDentist = patient_dentist_transfer_allowed();

function appointment_record(PDO $pdo,int $id,int $branchId): array
{
    $stmt=$pdo->prepare('SELECT a.*,TRIM(CONCAT_WS(" ",p.first_name,NULLIF(p.middle_name,""),p.last_name)) patient,p.patient_no FROM `' . table('appointments') . '` a JOIN `' . table('patients') . '` p ON p.id=a.patient_id WHERE a.id=? AND a.branch_id=? LIMIT 1');
    $stmt->execute([$id,$branchId]);
    $row=$stmt->fetch();
    if(!$row) throw new RuntimeException('Appointment not found.');
    return $row;
}

function appointment_history(PDO $pdo,int $id,?string $old,string $new,string $reason): void
{
    $pdo->prepare('INSERT INTO `' . table('appointment_status_history') . '` (appointment_id,previous_status,new_status,reason,changed_by,created_at) VALUES (?,?,?,?,?,NOW())')->execute([$id,$old,$new,$reason,current_user()['id']??null]);
}

function appointment_doctor(array $dentists,int $id): ?array
{
    foreach($dentists as $doctor) if((int)$doctor['id']===$id) return $doctor;
    return null;
}

function appointment_service(array $services,int $id): ?array
{
    foreach($services as $service) if((int)$service['id']===$id) return $service;
    return null;
}

function appointment_send_calendar_notice(PDO $pdo,int $appointmentId,int $patientId,string $start,string $service,string $provider,bool $rescheduled=false): array
{
    $stmt=$pdo->prepare('SELECT first_name,email FROM `' . table('patients') . '` WHERE id=? LIMIT 1');
    $stmt->execute([$patientId]);
    $patient=$stmt->fetch();
    if(!$patient || !filter_var((string)($patient['email']??''),FILTER_VALIDATE_EMAIL)) return ['id'=>0,'status'=>'not_available','error'=>'Patient has no valid email address.','attempts'=>0];
    $calendarUrl=app_absolute_url(appointment_calendar_relative_url(['id'=>$appointmentId,'patient_id'=>$patientId,'start_at'=>$start],true));
    try{
        return queue_template_email($rescheduled?'appointment_rescheduled':'appointment_confirmed',(string)$patient['email'],[
            'recipient_name'=>(string)$patient['first_name'],
            'appointment_datetime'=>date('F j, Y g:i A',strtotime($start)),
            'service_name'=>$service,
            'provider_name'=>$provider,
            'branch_name'=>(string)(public_clinic_profile()['name']??'Dental clinic'),
            'calendar_url'=>$calendarUrl,
        ],['type'=>$rescheduled?'appointment_rescheduled':'appointment_confirmed','appointment_id'=>$appointmentId],true);
    }catch(Throwable $mailError){
        error_log('Appointment email could not be queued: '.$mailError->getMessage());
        return ['id'=>0,'status'=>'failed','error'=>$mailError->getMessage(),'attempts'=>0];
    }
}

function appointment_send_cancellation_notice(PDO $pdo,array $appointment): array
{
    $stmt=$pdo->prepare('SELECT first_name,email FROM `' . table('patients') . '` WHERE id=? LIMIT 1');
    $stmt->execute([(int)$appointment['patient_id']]);
    $patient=$stmt->fetch();
    if(!$patient || !filter_var((string)($patient['email']??''),FILTER_VALIDATE_EMAIL)) return ['id'=>0,'status'=>'not_available','error'=>'Patient has no valid email address.','attempts'=>0];
    try{
        return queue_template_email('appointment_cancelled',(string)$patient['email'],[
            'recipient_name'=>(string)$patient['first_name'],
            'appointment_datetime'=>date('F j, Y g:i A',strtotime((string)$appointment['start_at'])),
            'service_name'=>(string)($appointment['service_name']??'Dental appointment'),
            'provider_name'=>trim((string)($appointment['dentist_name']??'')) !== '' ? (string)$appointment['dentist_name'] : (trim((string)($appointment['inhouse_provider_name']??'')) !== '' ? (string)$appointment['inhouse_provider_name'] : 'Clinic team'),
            'booking_url'=>app_absolute_url('appointment-returning.php'),
        ],['type'=>'appointment_cancelled','appointment_id'=>(int)$appointment['id']],true);
    }catch(Throwable $mailError){
        error_log('Appointment cancellation email could not be queued: '.$mailError->getMessage());
        return ['id'=>0,'status'=>'failed','error'=>$mailError->getMessage(),'attempts'=>0];
    }
}


$operationRequest = null;
if($_SERVER['REQUEST_METHOD']==='POST'){
    try{
        verify_csrf();
        require_task_action('appointments.manage');
        require_roles(['super_admin','clinic_admin','receptionist','staff']);
        $action=(string)($_POST['action']??'create');
        $operationRequest = operation_integrity_begin(
            $pdo,
            'appointments',
            $action,
            $_POST,
            'appointment',
            (int)($_POST['appointment_id'] ?? 0) ?: null
        );

        if($action==='create_plan'){
            if(!appointment_planning_schema_ready())throw new RuntimeException('Apply Version 0.5.9.8.14 before booking a future visit plan.');
            $patientId=(int)($_POST['patient_id']??0);
            $defaultDoctorId=(int)($_POST['dentist_user_id']??0);
            $seriesName=trim((string)($_POST['series_name']??''));
            if(!$appointmentIntegrityReady)throw new RuntimeException('Apply Version 0.5.9.8.32 before booking appointments.');
            $status=(string)($_POST['status']??'pending_confirmation');
            if(!in_array($status,['confirmed','pending_confirmation','requested'],true))$status='pending_confirmation';
            $confirmation=appointment_confirmation_capture($_POST,$status,'staff');
            $reason=trim((string)($_POST['reason_for_visit']??''));
            $multiProviderReason=trim((string)($_POST['multi_provider_reason']??''));
            $serviceInputs=(array)($_POST['plan_service_id']??[]);
            $doctorInputs=(array)($_POST['plan_dentist_user_id']??[]);
            $dateInputs=(array)($_POST['plan_date']??[]);
            $timeInputs=(array)($_POST['plan_time']??[]);
            $labelInputs=(array)($_POST['plan_label']??[]);
            $visitPlan=[];
            $count=max(count($serviceInputs),count($dateInputs),count($timeInputs),count($doctorInputs));
            for($index=0;$index<$count;$index++){
                $sid=(int)($serviceInputs[$index]??0);
                $visitDate=trim((string)($dateInputs[$index]??''));
                $visitTime=substr(trim((string)($timeInputs[$index]??'')),0,5);
                $visitDoctorId=(int)($doctorInputs[$index]??0);
                if($sid<1&&$visitDate===''&&$visitTime===''&&$visitDoctorId<1)continue;
                if($sid<1||!preg_match('/^\d{4}-\d{2}-\d{2}$/',$visitDate)||!preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/',$visitTime))throw new RuntimeException('Complete the service, date, and time for future visit '.($index+1).'.');
                if($visitDate<date('Y-m-d'))throw new RuntimeException('Future visit '.($index+1).' cannot be scheduled in the past.');
                $visitPlan[]=['service_id'=>$sid,'date'=>$visitDate,'time'=>$visitTime,'label'=>trim((string)($labelInputs[$index]??'')),'dentist_user_id'=>$visitDoctorId];
            }
            if($patientId<1)throw new RuntimeException('Select the patient for the visit plan.');
            if(count($visitPlan)<2)throw new RuntimeException('A future visit plan must contain at least two appointments. For one visit, use Book appointment.');
            if(count($visitPlan)>12)throw new RuntimeException('A visit plan can contain up to 12 future appointments at a time.');
            if($seriesName==='')$seriesName='Dental treatment visit plan';

            $uniqueDentistIds=[];
            foreach($visitPlan as $index=>$visit){
                $rows=appointment_service_definitions($branchId,[(int)$visit['service_id']]);
                if(!$rows)throw new RuntimeException('The service selected for future visit '.($index+1).' is no longer available.');
                $serviceRow=$rows[0];
                $visitDentistRequired=service_provider_type($serviceRow)==='dentist';
                $visitDoctor=null;
                if($visitDentistRequired){
                    $visitDoctorId=(int)$visit['dentist_user_id'] ?: $defaultDoctorId;
                    $visitDoctor=appointment_doctor($dentists,$visitDoctorId);
                    if(!$visitDoctor)throw new RuntimeException('Select a Dentist for Dentist-required future visit '.($index+1).'.');
                    $uniqueDentistIds[$visitDoctorId]=$visitDoctorId;
                    $visitPlan[$index]['dentist_user_id']=$visitDoctorId;
                }else{
                    $visitPlan[$index]['dentist_user_id']=0;
                }
                $visitPlan[$index]['service']=$serviceRow;
                $visitPlan[$index]['doctor']=$visitDoctor;
            }
            $uniqueDentistIds=array_values($uniqueDentistIds);
            $primaryDentist=patient_primary_dentist($patientId);
            $multiProvider=count($uniqueDentistIds)>1;
            if($multiProvider){
                if(mb_strlen($multiProviderReason)<10)throw new RuntimeException('Enter why this treatment plan uses different Dentists. The patient’s primary continuity Dentist will not be replaced.');
                if($primaryDentist&&!in_array((int)$primaryDentist['id'],$uniqueDentistIds,true))throw new RuntimeException('This multidisciplinary plan must include the patient’s continuity Dentist, '.(string)$primaryDentist['name'].', in at least one Dentist-required visit. Use an authorized continuity transfer first when their primary Dentist must change.');
            }elseif($uniqueDentistIds){
                validate_patient_dentist_continuity($patientId,(int)$uniqueDentistIds[0],false,'');
            }

            $pdo->beginTransaction();
            $patientLock=$pdo->prepare('SELECT id,branch_id FROM `' . table('patients') . '` WHERE id=? AND deleted_at IS NULL FOR UPDATE');
            $patientLock->execute([$patientId]);
            $lockedPatient=$patientLock->fetch();
            if(!$lockedPatient)throw new RuntimeException('The selected patient record is not available.');
            if((int)($lockedPatient['branch_id']??0)>0&&(int)$lockedPatient['branch_id']!==$branchId)throw new RuntimeException('The selected patient belongs to another clinic branch.');
            $seriesId=appointment_create_series($pdo,$branchId,$patientId,$seriesName,(int)$user['id']);
            $createdAppointments=[];
            foreach($visitPlan as $index=>$visit){
                $serviceRow=$visit['service'];
                $visitDoctor=$visit['doctor'];
                $visitDoctorId=(int)$visit['dentist_user_id'];
                $visitDentistRequired=service_provider_type($serviceRow)==='dentist';
                $segments=appointment_build_segments($branchId,$patientId,[$serviceRow],(string)$visit['date'],(string)$visit['time'],$visitDentistRequired?$visitDoctorId:0,0,true,0,0);
                foreach($segments as $segment)integrity_lock_appointment_resources($pdo,$branchId,$patientId,(int)($segment['dentist_user_id']??0),(int)($segment['chair_id']??0),(int)($segment['inhouse_provider_user_id']??0),(string)($segment['chair_name']??''),(string)$segment['start_at'],(string)$segment['end_at'],0);
                $first=$segments[0];
                $start=(string)$first['start_at'];
                $end=(string)$segments[count($segments)-1]['end_at'];
                $serviceSummary=(string)$serviceRow['name'];
                $dentistDbId=$visitDentistRequired?$visitDoctorId:null;
                $dentistDbName=$visitDentistRequired?(string)$visitDoctor['name']:null;
                $inhouseId=(int)($first['inhouse_provider_user_id']??0);
                $inhouseName=(string)($first['inhouse_provider_name']??'');
                $chairId=(int)($first['chair_id']??0);
                $chairName=(string)($first['chair_name']??'');
                $visitLabel=(string)($visit['label']?:('Visit '.($index+1).' of '.count($visitPlan)));
                $notes='Treatment plan: '.$seriesName.($multiProvider?' · Multidisciplinary provider plan: '.$multiProviderReason:'');
                $stmt=$pdo->prepare('INSERT INTO `' . table('appointments') . '` (branch_id,patient_id,service_id,dentist_user_id,dentist_name,inhouse_provider_user_id,inhouse_provider_name,service_name,provider_type,start_at,end_at,chair_id,chair_name,status,appointment_origin,booking_source,reason_for_visit,confirmation_status,confirmation_source,confirmation_note,patient_agreed_at,notes,created_by,created_at,series_id,series_sequence,planned_visit_label) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,? ,"manual",?,?,?,?,?,?,?,?,NOW(),?,?,?)');
                $stmt->execute([$branchId,$patientId,(int)$serviceRow['id'],$dentistDbId,$dentistDbName,$inhouseId?:null,$inhouseName?:null,$serviceSummary,(string)$first['provider_type'],$start,$end,$chairId?:null,$chairName?:null,$status,$confirmation['booking_source'],$reason?:null,$status,$confirmation['confirmation_source'],$confirmation['confirmation_note'],$confirmation['patient_agreed_at'],$notes,(int)$user['id'],$seriesId,$index+1,mb_substr($visitLabel,0,190)]);
                $appointmentId=(int)$pdo->lastInsertId();
                appointment_store_segments($pdo,$appointmentId,$segments);
                appointment_history($pdo,$appointmentId,null,$status,'Created from future visit plan: '.$seriesName);
                workflow_event($patientId,'appointment_scheduled','Future treatment visit booked',['appointment_id'=>$appointmentId,'series_id'=>$seriesId],date('M j, Y g:i A',strtotime($start)).' · '.$serviceSummary.' · '.$visitLabel);
                audit('appointment_plan_visit_created','appointment',$appointmentId,['series_id'=>$seriesId,'series_sequence'=>$index+1,'service_id'=>(int)$serviceRow['id'],'start_at'=>$start,'dentist_id'=>$dentistDbId,'multi_provider'=>$multiProvider,'multi_provider_reason'=>$multiProviderReason?:null]);
                $createdAppointments[]=['id'=>$appointmentId,'start_at'=>$start,'service_name'=>$serviceSummary,'provider_name'=>$visitDentistRequired?(string)$visitDoctor['name']:($inhouseName?:'Clinic care team'),'status'=>$status];
            }
            if(!$primaryDentist&&$uniqueDentistIds){
                $continuityDoctorId=$defaultDoctorId&&in_array($defaultDoctorId,$uniqueDentistIds,true)?$defaultDoctorId:(int)$uniqueDentistIds[0];
                commit_patient_dentist_continuity($patientId,$continuityDoctorId,false,'');
            }elseif(!$multiProvider&&$uniqueDentistIds){
                commit_patient_dentist_continuity($patientId,(int)$uniqueDentistIds[0],false,'');
            }
            $pdo->commit();
            $emailFailures=0;
            foreach($createdAppointments as $saved){
                if($saved['status']!=='confirmed')continue;
                $delivery=appointment_send_calendar_notice($pdo,(int)$saved['id'],$patientId,(string)$saved['start_at'],(string)$saved['service_name'],(string)$saved['provider_name'],false);
                if(!in_array((string)($delivery['status']??''),['sent','pending'],true))$emailFailures++;
            }
            flash($emailFailures?'warning':'success',count($createdAppointments).' future appointments were booked under “'.$seriesName.'”. Each visit keeps its own provider, service, schedule, check-in, queue visit, and checkout.'.($emailFailures?' One or more confirmation emails require review.':''));
        } elseif($action==='create' || $action==='reschedule'){
            if(!schema_column_exists(table('appointments'),'chair_id'))throw new RuntimeException('Apply the v0.5.8.4 database update from System Setup before creating or rescheduling appointments.');
            if(!appointment_planning_schema_ready())throw new RuntimeException('Apply Version 0.5.9.8.14 from Superuser Settings before booking multi-service or grouped future appointments.');
            $appointmentId=(int)($_POST['appointment_id']??0);
            $portalRequestId=(int)($_POST['portal_request_id']??0);
            $portalRequest=null;
            $patientId=(int)($_POST['patient_id']??0);
            $doctorId=(int)($_POST['dentist_user_id']??0);
            $serviceIds=appointment_requested_service_ids($_POST);
            $serviceRows=appointment_service_definitions($branchId,$serviceIds);
            $service=$serviceRows[0]??null;
            $serviceId=(int)($service['id']??0);
            $waitlistId=(int)($_POST['waitlist_id']??0);
            $date=trim((string)($_POST['date']??''));
            $time=substr(trim((string)($_POST['time']??'')),0,5);
            if($patientId<1||!$service||$date===''||$time==='') throw new RuntimeException('Select a patient, at least one service, a date, and an available time.');
            $dentistRequired=false;foreach($serviceRows as $serviceRow)if(service_provider_type($serviceRow)==='dentist'){$dentistRequired=true;break;}
            $doctor=$dentistRequired?appointment_doctor($dentists,$doctorId):null;
            if($dentistRequired&&!$doctor)throw new RuntimeException('Select an available Dentist because one or more services require a Dentist.');
            if(!$dentistRequired)$doctorId=0;
            $transferRequested=$dentistRequired&&$canTransferDentist&&isset($_POST['change_continuity_dentist']);
            $transferReason=trim((string)($_POST['continuity_change_reason']??''));
            if($dentistRequired)validate_patient_dentist_continuity($patientId,$doctorId,$transferRequested,$transferReason);
            $pdo->beginTransaction();
            $existing=$action==='reschedule'?integrity_lock_appointment($pdo,$appointmentId,$branchId):null;
            if($existing){
                if((int)$existing['patient_id']!==$patientId)throw new RuntimeException('Rescheduling cannot change the patient. Create a separate appointment for another patient.');
                if(!in_array((string)$existing['status'],['requested','pending_confirmation','confirmed'],true))throw new RuntimeException('Only requested, pending-confirmation, or confirmed appointments can be rescheduled.');
                if(integrity_active_queue_for_appointment($pdo,$appointmentId,true))throw new RuntimeException('This appointment already has an active queue visit and must be managed from the Queue.');
            }
            $rescheduleReason=trim((string)($_POST['reschedule_reason']??''));
            if($action==='reschedule'&&$rescheduleReason==='')throw new RuntimeException('Enter the reschedule/contact note before saving the new schedule.');
            $bypassRequested=$dentistRequired&&$canBypassSchedule&&isset($_POST['schedule_bypass']);
            $bypassReason=trim((string)($_POST['schedule_bypass_reason']??''));
            if($bypassRequested&&$bypassReason==='')throw new RuntimeException('Enter the reason for the schedule bypass.');
            if($bypassRequested){$manualTime=substr(trim((string)($_POST['bypass_time']??'')),0,5);if(!preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/',$manualTime))throw new RuntimeException('Enter the manual time for the schedule bypass.');$time=$manualTime;}
            if($action==='reschedule'&&$portalRequestId>0){
                if(!patient_portal_request_schema_ready())throw new RuntimeException('Apply Version 0.5.8.6 before resolving a patient portal request.');
                $portalStmt=$pdo->prepare('SELECT * FROM `' . table('patient_portal_requests') . '` WHERE id=? AND appointment_id=? AND patient_id=? AND request_type="appointment_reschedule" AND status="pending" LIMIT 1 FOR UPDATE');
                $portalStmt->execute([$portalRequestId,$appointmentId,$patientId]);
                $portalRequest=$portalStmt->fetch();
                if(!$portalRequest)throw new RuntimeException('The patient reschedule request is no longer pending or does not match this appointment.');
            }
            if(!$appointmentIntegrityReady)throw new RuntimeException('Apply Version 0.5.9.8.32 before booking or rescheduling appointments.');
            $status=(string)($_POST['status']??'pending_confirmation');
            if(!in_array($status,['confirmed','requested','pending_confirmation'],true))$status='pending_confirmation';
            $confirmation=appointment_confirmation_capture($_POST,$status,'staff');
            if($existing)integrity_assert_appointment_transition((string)$existing['status'],$status,(string)$existing['start_at']);
            $reason=trim((string)($_POST['reason_for_visit']??''));
            $notes=trim((string)($_POST['notes']??''))?:null;
            $repeatCount=$action==='create'?max(1,min(12,(int)($_POST['repeat_count']??1))):1;
            $intervalWeeks=max(1,min(52,(int)($_POST['repeat_interval_weeks']??1)));
            if($waitlistId>0&&$repeatCount>1)throw new RuntimeException('A waitlist offer can create only one appointment. Save it first, then use Book another visit for the future series.');
            $seriesName=trim((string)($_POST['series_name']??''));
            $visitDates=appointment_repeat_dates($date,$repeatCount,$intervalWeeks);
            $serviceSummary=appointment_service_bundle_summary($serviceRows);
            $createdAppointments=[];
            $savedBundleDuration=0;

            $patientLock=$pdo->prepare('SELECT id,branch_id FROM `' . table('patients') . '` WHERE id=? AND deleted_at IS NULL FOR UPDATE');
            $patientLock->execute([$patientId]);
            $lockedPatient=$patientLock->fetch();
            if(!$lockedPatient)throw new RuntimeException('The selected patient record is not available.');
            if((int)($lockedPatient['branch_id']??0)>0&&(int)$lockedPatient['branch_id']!==$branchId)throw new RuntimeException('The selected patient belongs to another clinic branch.');
            $waitlistRecord=null;
            if($action==='create'&&$waitlistId>0&&schema_table_exists(table('appointment_waitlist'))){$waitStmt=$pdo->prepare('SELECT * FROM `' . table('appointment_waitlist') . '` WHERE id=? AND branch_id=? AND status IN ("waiting","offered") LIMIT 1 FOR UPDATE');$waitStmt->execute([$waitlistId,$branchId]);$waitlistRecord=$waitStmt->fetch();if(!$waitlistRecord||(int)$waitlistRecord['patient_id']!==$patientId||(int)$waitlistRecord['service_id']!==$serviceId)throw new RuntimeException('The selected waitlist request no longer matches this patient and primary service.');}
            $seriesId=$repeatCount>1?appointment_create_series($pdo,$branchId,$patientId,$seriesName!==''?$seriesName:$serviceSummary.' series',(int)$user['id']):0;

            foreach($visitDates as $visitIndex=>$visitDate){
                $excludeId=$existing?$appointmentId:0;
                $segments=appointment_build_segments($branchId,$patientId,$serviceRows,$visitDate,$time,$doctorId,$excludeId,!$bypassRequested,(int)($_POST['inhouse_provider_user_id']??0),(int)($_POST['chair_id']??0));
                $start=(string)$segments[0]['start_at'];
                $end=(string)$segments[count($segments)-1]['end_at'];
                $savedBundleDuration=appointment_bundle_duration_minutes($segments);
                foreach($segments as $segment){
                    appointment_guard_assert_schedule_available($pdo,$branchId,$patientId,(int)($segment['dentist_user_id']??0),(int)($segment['chair_id']??0),(int)($segment['inhouse_provider_user_id']??0),(string)($segment['chair_name']??''),(string)$segment['start_at'],(string)$segment['end_at'],$excludeId,$bypassRequested,$bypassReason);
                    integrity_lock_appointment_resources($pdo,$branchId,$patientId,(int)($segment['dentist_user_id']??0),(int)($segment['chair_id']??0),(int)($segment['inhouse_provider_user_id']??0),(string)($segment['chair_name']??''),(string)$segment['start_at'],(string)$segment['end_at'],$excludeId);
                }
                $firstSegment=$segments[0];
                $firstInhouse=null;foreach($segments as $candidate)if($candidate['provider_type']==='inhouse'){$firstInhouse=$candidate;break;}
                $providerModel=appointment_bundle_provider_model($serviceRows);
                $masterProviderType=$firstSegment['provider_type'];
                $dentistDbId=$dentistRequired?$doctorId:null;
                $dentistDbName=$dentistRequired?(string)$doctor['name']:null;
                $inhouseProviderId=(int)($firstInhouse['inhouse_provider_user_id']??0);
                $inhouseName=(string)($firstInhouse['inhouse_provider_name']??'');
                $chairId=(int)($firstSegment['chair_id']??0);
                $chair=(string)($firstSegment['chair_name']??'');
                $providerName=$providerModel==='mixed'?((string)$doctor['name'].' + clinic care team'):($dentistRequired?(string)$doctor['name']:(string)($firstInhouse['inhouse_provider_name']??'In-house clinic team'));
                $visitLabel=$repeatCount>1?'Visit '.($visitIndex+1).' of '.$repeatCount:null;

                if($action==='create'){
                    $stmt=$pdo->prepare('INSERT INTO `' . table('appointments') . '` (branch_id,patient_id,service_id,dentist_user_id,dentist_name,inhouse_provider_user_id,inhouse_provider_name,service_name,provider_type,start_at,end_at,chair_id,chair_name,status,appointment_origin,booking_source,reason_for_visit,confirmation_status,confirmation_source,confirmation_note,patient_agreed_at,schedule_bypass,schedule_bypass_reason,schedule_bypass_by,schedule_bypass_at,notes,created_by,created_at,series_id,series_sequence,planned_visit_label) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,? ,"manual",?,?,?,?,?,?,?,?,?,?,?,?,NOW(),?,?,?)');
                    $stmt->execute([$branchId,$patientId,$serviceId,$dentistDbId,$dentistDbName,$inhouseProviderId?:null,$inhouseName?:null,$serviceSummary,$masterProviderType,$start,$end,$chairId?:null,$chair?:null,$status,$confirmation['booking_source'],$reason?:null,$status,$confirmation['confirmation_source'],$confirmation['confirmation_note'],$confirmation['patient_agreed_at'],$bypassRequested?1:0,$bypassReason?:null,$bypassRequested?(int)$user['id']:null,$bypassRequested?date('Y-m-d H:i:s'):null,$notes,(int)$user['id'],$seriesId?:null,$seriesId?($visitIndex+1):null,$visitLabel]);
                    $id=(int)$pdo->lastInsertId();
                    appointment_store_segments($pdo,$id,$segments);
                    appointment_guard_mark_checked($pdo,$id,$bypassRequested?'bypass_approved':'passed',$bypassRequested?'Schedule bypass approved: '.$bypassReason:'Schedule guard passed.');
                    appointment_guard_sync_assignment($pdo,['id'=>$id,'branch_id'=>$branchId,'patient_id'=>$patientId,'provider_type'=>$masterProviderType,'dentist_user_id'=>$dentistDbId,'start_at'=>$start],null,$dentistDbId?'Provider assigned.':'Dentist assignment required.');
                    appointment_history($pdo,$id,null,$status,$bypassRequested?'Appointment created using Schedule Bypass: '.$bypassReason:'Appointment created by staff');
                    workflow_event($patientId,'appointment_scheduled','Appointment created',['appointment_id'=>$id,'series_id'=>$seriesId?:null],date('M j, Y g:i A',strtotime($start)).' · '.$serviceSummary.' · '.$providerName);
                    audit('appointment_created','appointment',$id,['provider_model'=>$providerModel,'doctor_id'=>$dentistDbId,'service_ids'=>$serviceIds,'series_id'=>$seriesId?:null,'series_sequence'=>$seriesId?($visitIndex+1):null,'schedule_bypass'=>$bypassRequested,'bypass_reason'=>$bypassReason,'continuity_transfer'=>$transferRequested,'continuity_reason'=>$transferReason]);
                    $createdAppointments[]=['id'=>$id,'start_at'=>$start,'service_name'=>$serviceSummary,'provider_name'=>$providerName,'status'=>$status];
                }else{
                    $oldStatus=(string)$existing['status'];
                    $rescheduleUpdate=$pdo->prepare('UPDATE `' . table('appointments') . '` SET patient_id=?,service_id=?,dentist_user_id=?,dentist_name=?,inhouse_provider_user_id=?,inhouse_provider_name=?,service_name=?,provider_type=?,start_at=?,end_at=?,chair_id=?,chair_name=?,status=?,booking_source=?,reason_for_visit=?,confirmation_status=?,confirmation_source=?,confirmation_note=?,patient_agreed_at=?,schedule_bypass=?,schedule_bypass_reason=?,schedule_bypass_by=?,schedule_bypass_at=?,notes=?,updated_at=NOW() WHERE id=? AND status=?');
                    $rescheduleUpdate->execute([$patientId,$serviceId,$dentistDbId,$dentistDbName,$inhouseProviderId?:null,$inhouseName?:null,$serviceSummary,$masterProviderType,$start,$end,$chairId?:null,$chair?:null,$status,$confirmation['booking_source'],$reason?:null,$status,$confirmation['confirmation_source'],$confirmation['confirmation_note'],$confirmation['patient_agreed_at'],$bypassRequested?1:0,$bypassReason?:null,$bypassRequested?(int)$user['id']:null,$bypassRequested?date('Y-m-d H:i:s'):null,$notes,$appointmentId,$oldStatus]);
                    if($rescheduleUpdate->rowCount()!==1)throw new RuntimeException('The appointment changed while rescheduling was being completed.');
                    appointment_store_segments($pdo,$appointmentId,$segments);
                    appointment_guard_mark_checked($pdo,$appointmentId,$bypassRequested?'bypass_approved':'passed',$bypassRequested?'Schedule bypass approved: '.$bypassReason:'Schedule guard passed after reschedule.');
                    appointment_guard_sync_assignment($pdo,['id'=>$appointmentId,'branch_id'=>$branchId,'patient_id'=>$patientId,'provider_type'=>$masterProviderType,'dentist_user_id'=>$dentistDbId,'start_at'=>$start],null,$dentistDbId?'Provider assigned.':'Dentist assignment required after reschedule.');
                    appointment_history($pdo,$appointmentId,$oldStatus,$status,($bypassRequested?'Schedule Bypass: '.$bypassReason.'. ':'').'Appointment rescheduled from '.date('M j, Y g:i A',strtotime((string)$existing['start_at'])));
                    workflow_event($patientId,'appointment_rescheduled','Appointment rescheduled',['appointment_id'=>$appointmentId],date('M j, Y g:i A',strtotime($start)).' · '.$serviceSummary.' · '.$providerName);
                    audit('appointment_rescheduled','appointment',$appointmentId,['start_at'=>$start,'provider_model'=>$providerModel,'dentist_id'=>$dentistDbId,'service_ids'=>$serviceIds,'schedule_bypass'=>$bypassRequested,'bypass_reason'=>$bypassReason,'continuity_transfer'=>$transferRequested,'continuity_reason'=>$transferReason],['start_at'=>$existing['start_at'],'dentist_id'=>$existing['dentist_user_id']]);
                    if($portalRequest){
                        $staffNote='Rescheduled by clinic to '.date('M j, Y g:i A',strtotime($start)).'. '.$rescheduleReason;
                        $portalUpdate=$pdo->prepare('UPDATE `' . table('patient_portal_requests') . '` SET status="completed",requested_start_at=?,requested_end_at=?,staff_note=?,resolved_by=?,resolved_at=NOW(),updated_at=NOW() WHERE id=? AND status="pending"');
                        $portalUpdate->execute([$start,$end,$staffNote,(int)$user['id'],$portalRequestId]);
                        if($portalUpdate->rowCount()!==1)throw new RuntimeException('The patient reschedule request was already resolved by another user.');
                        workflow_event($patientId,'portal_reschedule_completed','Portal reschedule request completed',['appointment_id'=>$appointmentId,'portal_request_id'=>$portalRequestId],$staffNote);
                        audit('patient_portal_reschedule_completed','patient_portal_request',$portalRequestId,['appointment_id'=>$appointmentId,'start_at'=>$start]);
                    }
                    $createdAppointments[]=['id'=>$appointmentId,'start_at'=>$start,'service_name'=>$serviceSummary,'provider_name'=>$providerName,'status'=>$status];
                }
            }
            if($dentistRequired)commit_patient_dentist_continuity($patientId,$doctorId,$transferRequested,$transferReason);
            if($waitlistRecord){$firstId=(int)$createdAppointments[0]['id'];$pdo->prepare('UPDATE `' . table('appointment_waitlist') . '` SET status="booked",appointment_id=?,updated_at=NOW() WHERE id=?')->execute([$firstId,$waitlistId]);audit('appointment_waitlist_booked','appointment_waitlist',$waitlistId,['appointment_id'=>$firstId]);}
            $pdo->commit();

            $emailFailures=0;
            foreach($createdAppointments as $savedAppointment){
                if($savedAppointment['status']!=='confirmed')continue;
                $delivery=appointment_send_calendar_notice($pdo,(int)$savedAppointment['id'],$patientId,(string)$savedAppointment['start_at'],(string)$savedAppointment['service_name'],(string)$savedAppointment['provider_name'],$action==='reschedule');
                if(!in_array((string)($delivery['status']??''),['sent','pending'],true))$emailFailures++;
            }
            if($action==='reschedule')flash($emailFailures?'warning':'success','Appointment rescheduled with '.count($serviceRows).' planned service'.(count($serviceRows)===1?'':'s').'.'.($emailFailures?' The patient email requires review.':''));
            elseif($repeatCount>1)flash($emailFailures?'warning':'success',$repeatCount.' future appointments were booked as one series. Each visit remains independently reschedulable and receives its own check-in, queue visit, and checkout. '.($emailFailures?'One or more confirmation emails require review.':''));
            else flash($emailFailures?'warning':'success','Appointment saved with '.count($serviceRows).' coordinated service'.(count($serviceRows)===1?'':'s').' and an estimated duration of '.$savedBundleDuration.' minutes.'.($emailFailures?' The confirmation email requires review.':''));
        } elseif($action==='resend_email'){
            $id=(int)($_POST['appointment_id']??0);$row=appointment_record($pdo,$id,$branchId);
            if((string)$row['status']==='cancelled')$delivery=appointment_send_cancellation_notice($pdo,$row);
            elseif((string)$row['status']==='confirmed'){
                $provider=($row['provider_type']??'dentist')==='inhouse'?(string)($row['inhouse_provider_name']?:'Clinic care team'):(string)($row['dentist_name']?:'Clinic dentist');
                $delivery=appointment_send_calendar_notice($pdo,$id,(int)$row['patient_id'],(string)$row['start_at'],(string)$row['service_name'],$provider,false);
            }else throw new RuntimeException('Only confirmed or cancelled appointments have a patient email that can be resent.');
            $feedback=email_delivery_feedback($delivery,(string)$row['status']==='cancelled'?'Cancellation notice':'Appointment confirmation');
            audit('appointment_email_resent','appointment',$id,['delivery_status'=>$delivery['status']??'unknown','outbox_id'=>$delivery['id']??null]);
            flash($feedback['type'],$feedback['message']);
        } elseif($action==='status'){
            $id=(int)($_POST['appointment_id']??0);
            $pdo->beginTransaction();
            $row=integrity_lock_appointment($pdo,$id,$branchId);
            $new=(string)($_POST['status']??'');
            if(!in_array($new,['confirmed','pending_confirmation','cancelled','no_show'],true))throw new RuntimeException('Invalid appointment status.');
            integrity_assert_appointment_transition((string)$row['status'],$new,(string)$row['start_at']);
            $activeQueue=integrity_active_queue_for_appointment($pdo,$id,true);
            if($activeQueue)throw new RuntimeException('This appointment has an active queue visit. Continue cancellation or other visit changes from the Queue.');
            $reason=trim((string)($_POST['reason']??''));
            appointment_guard_assert_status_change($pdo,$row,$new,$reason);
            if(in_array($new,['cancelled','no_show'],true)&&$reason==='')throw new RuntimeException('Enter a reason for the status change.');
            $confirmation=$new==='confirmed'
                ? appointment_confirmation_capture($_POST,$new,(string)($row['booking_source']??'staff'))
                : [
                    'confirmation_source'=>$row['confirmation_source']??null,
                    'confirmation_note'=>$row['confirmation_note']??null,
                    'patient_agreed_at'=>$row['patient_agreed_at']??null,
                ];
            $statusSql='UPDATE `' . table('appointments') . '` SET status=?,confirmation_status=?,confirmation_source=?,confirmation_note=?,patient_agreed_at=?,updated_at=NOW()';
            if($new==='no_show'&&schema_column_exists(table('appointments'),'no_show_marked_at'))$statusSql.=',no_show_marked_at=NOW()';
            $statusSql.=' WHERE id=? AND status=?';
            $statusUpdate=$pdo->prepare($statusSql);
            $statusUpdate->execute([$new,$new,$confirmation['confirmation_source'],$confirmation['confirmation_note'],$confirmation['patient_agreed_at'],$id,(string)$row['status']]);
            if($statusUpdate->rowCount()!==1)throw new RuntimeException('The appointment status changed while this update was being completed.');
            if (in_array($new, ['cancelled','no_show'], true)) {
                if (function_exists('queue_sync_terminal_appointment_segments')) {
                    queue_sync_terminal_appointment_segments($pdo, $id, $new);
                } elseif (schema_table_exists(table('appointment_services')) && schema_column_exists(table('appointment_services'), 'status')) {
                    $segmentStatus = $new === 'no_show' ? 'no_show' : 'cancelled';
                    $pdo->prepare('UPDATE `' . table('appointment_services') . '` SET status=?,updated_at=NOW() WHERE appointment_id=? AND COALESCE(status,"") NOT IN ("completed","cancelled","no_show")')->execute([$segmentStatus,$id]);
                }
            }
            appointment_history($pdo,$id,(string)$row['status'],$new,$reason?:'Appointment status updated');
            workflow_event((int)$row['patient_id'],'appointment_'.$new,'Appointment '.str_replace('_',' ',$new),['appointment_id'=>$id],$reason?:null);
            if($new==='cancelled'&&$activeQueue)integrity_cancel_appointment_queue($pdo,$id,$reason);
            audit('appointment_status_changed','appointment',$id,['status'=>$new,'reason'=>$reason,'queue_entries_cancelled'=>$new==='cancelled'?count($activeQueue):0],['status'=>$row['status']]);
            $pdo->commit();
            $delivery=null;
            if($new==='cancelled')$delivery=appointment_send_cancellation_notice($pdo,$row);
            elseif($new==='confirmed'){
                $provider=($row['provider_type']??'dentist')==='inhouse'?(string)($row['inhouse_provider_name']?:'Clinic care team'):(string)($row['dentist_name']?:'Clinic dentist');
                $delivery=appointment_send_calendar_notice($pdo,$id,(int)$row['patient_id'],(string)$row['start_at'],(string)$row['service_name'],$provider,false);
            }
            if($delivery){$feedback=email_delivery_feedback($delivery,$new==='cancelled'?'Cancellation notice':'Appointment confirmation');flash($feedback['type'],'Appointment status updated. '.$feedback['message']);}
            else flash('success','Appointment status updated.');
        }else throw new RuntimeException('Unsupported appointment action.');
        operation_integrity_complete($pdo, $operationRequest, ['redirect'=>'appointments.php']);
        header('Location: appointments.php');exit;
    }catch(Throwable $exception){if($pdo->inTransaction())$pdo->rollBack();operation_integrity_fail($pdo,$operationRequest,$exception);$error=$exception->getMessage();}
}

$assignmentPending = function_exists('appointment_guard_pending_assignment_count') ? appointment_guard_pending_assignment_count($pdo, $branchId) : 0;
$assignmentQueuePreview = $assignmentPending > 0 && function_exists('appointment_guard_pending_assignments') ? appointment_guard_pending_assignments($pdo,$branchId,5) : [];
$patientContinuitySelect = patient_continuity_schema_ready() ? ',p.primary_dentist_user_id,u.name primary_dentist_name' : ',NULL primary_dentist_user_id,NULL primary_dentist_name';
// Patient choices are searched on demand through api/patient_search.php. This
// prevents appointment forms from loading the entire patient registry.
$onlinePending = 0;
if ($canProcessRequests && public_intake_schema_ready()) {
    try { $onlinePending = (int) $pdo->query('SELECT COUNT(*) FROM `' . table('appointment_requests') . '` WHERE status IN ("submitted","needs_verification","reviewing")')->fetchColumn(); }
    catch (Throwable) {}
}
$waitlistPrefill=null;$waitlistPrefillId=(int)($_GET['waitlist_id']??0);
if($canManage&&$waitlistPrefillId>0&&schema_table_exists(table('appointment_waitlist'))){$prefillStmt=$pdo->prepare('SELECT w.*,p.patient_no,p.first_name,p.last_name,p.mobile,p.primary_dentist_user_id,u.name primary_dentist_name FROM `' . table('appointment_waitlist') . '` w JOIN `' . table('patients') . '` p ON p.id=w.patient_id LEFT JOIN `' . table('users') . '` u ON u.id=p.primary_dentist_user_id WHERE w.id=? AND w.branch_id=? AND w.status IN ("waiting","offered") LIMIT 1');$prefillStmt->execute([$waitlistPrefillId,$branchId]);$waitlistPrefill=$prefillStmt->fetch()?:null;}
$patientPrefill=$waitlistPrefill;
$patientPrefillId=(int)($_GET['patient_id']??0);
if($canManage&&!$patientPrefill&&$patientPrefillId>0){
    $prefillStmt=$pdo->prepare('SELECT p.id patient_id,p.patient_no,p.first_name,p.last_name,p.mobile,p.primary_dentist_user_id,u.name primary_dentist_name FROM `' . table('patients') . '` p LEFT JOIN `' . table('users') . '` u ON u.id=p.primary_dentist_user_id WHERE p.id=? AND p.branch_id=? AND p.deleted_at IS NULL LIMIT 1');
    $prefillStmt->execute([$patientPrefillId,$branchId]);
    $patientPrefill=$prefillStmt->fetch()?:null;
}

$allowedViews = ['month','week','today','upcoming','history'];
$view = (string) ($_GET['view'] ?? 'week');
if (!in_array($view,$allowedViews,true)) $view = 'week';
$q = trim((string) ($_GET['q'] ?? ''));
$statusFilter = trim((string) ($_GET['status'] ?? ''));
$allowedStatusFilters = ['requested','pending_confirmation','confirmed','checked_in','in_treatment','awaiting_billing','completed','cancelled','left_before_treatment','no_show'];
if (!in_array($statusFilter,$allowedStatusFilters,true)) $statusFilter = '';
$terminalScheduleStatuses = ['cancelled','left_before_treatment','no_show'];
$showScheduleHistory = (string)($_GET['show_schedule_history'] ?? '') === '1';
$autoHideTerminalSchedule = !$showScheduleHistory && $statusFilter === '' && in_array($view, ['month','week','today','upcoming'], true);
$providerFilter = (string)($_GET['provider'] ?? '');
if(!in_array($providerFilter,['dentist','inhouse'],true))$providerFilter='';
$doctorFilterId = (int) ($_GET['doctor_filter'] ?? 0);
$dentistIds = array_map(static fn(array $row): int => (int) $row['id'], $dentists);
if (!in_array($doctorFilterId,$dentistIds,true)) $doctorFilterId = 0;

$weekAnchor = trim((string) ($_GET['week'] ?? date('Y-m-d')));
$anchorTimestamp = strtotime($weekAnchor) ?: time();
$weekStartTimestamp = strtotime('-' . ((int) date('N',$anchorTimestamp) - 1) . ' days', $anchorTimestamp);
$weekStart = date('Y-m-d',$weekStartTimestamp);
$weekEnd = date('Y-m-d',strtotime($weekStart . ' +6 days'));
$weekNextBoundary = date('Y-m-d',strtotime($weekStart . ' +7 days'));
$monthAnchor = trim((string) ($_GET['month'] ?? date('Y-m',$anchorTimestamp)));
if (!preg_match('/^\d{4}-\d{2}$/',$monthAnchor)) $monthAnchor = date('Y-m');
$monthStart = $monthAnchor . '-01';
$monthEnd = date('Y-m-t',strtotime($monthStart));
$monthGridStart = date('Y-m-d',strtotime('-' . ((int)date('N',strtotime($monthStart)) - 1) . ' days',strtotime($monthStart)));
$monthGridEnd = date('Y-m-d',strtotime('+' . (7 - (int)date('N',strtotime($monthEnd))) . ' days',strtotime($monthEnd)));
$monthGridNextBoundary = date('Y-m-d',strtotime($monthGridEnd . ' +1 day'));
$focusDate = trim((string)($_GET['focus_date'] ?? ''));
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/',$focusDate)) $focusDate = '';

$page=module_page_number('page');
$perPage=module_page_size('per_page',50);
$appointmentEmailReady=schema_column_exists(table('appointments'),'last_email_outbox_id');
$emailJoin=$appointmentEmailReady?' LEFT JOIN `' . table('email_outbox') . '` o ON o.id=a.last_email_outbox_id ':'';
$emailSelect=$appointmentEmailReady?',o.status email_status,o.attempts email_attempts,o.last_error email_error,o.sent_at email_sent_at,o.available_at email_available_at':',NULL email_status,0 email_attempts,NULL email_error,NULL email_sent_at,NULL email_available_at';
$liveQueueJoin=' LEFT JOIN `' . table('queue_entries') . '` aq ON aq.id=(SELECT aq2.id FROM `' . table('queue_entries') . '` aq2 WHERE aq2.appointment_id=a.id ORDER BY aq2.id DESC LIMIT 1)';
$liveQueueSelect=',aq.status live_queue_status,aq.queue_no live_queue_no,aq.chair_name live_queue_chair_name';
$fromSql = ' FROM `' . table('appointments') . '` a JOIN `' . table('patients') . '` p ON p.id=a.patient_id'.$emailJoin.$liveQueueJoin.' WHERE a.branch_id=?';
$whereSql='';
$args = [$branchId];
if ($taskRole === 'dentist' && $selectedDentistId > 0) {
    $whereSql .= ' AND a.dentist_user_id=?'; $args[] = $selectedDentistId;
} elseif ($taskRole === 'dental_assistant') {
    if ($assistantDentistIds) {
        $marks = implode(',',array_fill(0,count($assistantDentistIds),'?'));
        $whereSql .= ' AND a.dentist_user_id IN (' . $marks . ')';
        $args = array_merge($args,$assistantDentistIds);
    } else $whereSql .= ' AND 1=0';
} elseif ($doctorFilterId > 0) {
    $whereSql .= ' AND a.dentist_user_id=?'; $args[] = $doctorFilterId;
}
if ($providerFilter !== '') { $whereSql .= ' AND a.provider_type=?'; $args[] = $providerFilter; }
if ($statusFilter !== '') { $whereSql .= ' AND a.status=?'; $args[] = $statusFilter; }
elseif ($autoHideTerminalSchedule) {
    $marks = implode(',', array_fill(0, count($terminalScheduleStatuses), '?'));
    $whereSql .= ' AND a.status NOT IN (' . $marks . ')';
    array_push($args, ...$terminalScheduleStatuses);
}
if ($q !== '') {
    $like = '%' . $q . '%';
    $whereSql .= ' AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.patient_no LIKE ? OR p.mobile LIKE ? OR a.service_name LIKE ? OR a.dentist_name LIKE ? OR a.chair_name LIKE ? OR a.reason_for_visit LIKE ? OR a.notes LIKE ?)';
    array_push($args,$like,$like,$like,$like,$like,$like,$like,$like,$like);
}
if ($view === 'today') {
    $whereSql .= ' AND DATE(a.start_at)=CURDATE()';
} elseif ($view === 'month') {
    $whereSql .= ' AND a.start_at>=? AND a.start_at<?';
    $args[] = $monthGridStart . ' 00:00:00';
    $args[] = $monthGridNextBoundary . ' 00:00:00';
} elseif ($view === 'week') {
    $whereSql .= ' AND a.start_at>=? AND a.start_at<?';
    $args[] = $weekStart . ' 00:00:00';
    $args[] = $weekNextBoundary . ' 00:00:00';
} elseif ($view === 'history') {
    $whereSql .= ' AND a.start_at<NOW()';
} else {
    $whereSql .= ' AND (a.start_at>=DATE_SUB(NOW(),INTERVAL 2 HOUR) OR a.status IN ("checked_in","in_treatment","awaiting_billing"))';
}
$countStmt=$pdo->prepare('SELECT COUNT(*)'.$fromSql.$whereSql);
$countStmt->execute($args);
$appointmentTotal=(int)$countStmt->fetchColumn();
$orderSql=' ORDER BY a.start_at ' . ($view === 'history' ? 'DESC' : 'ASC');
$limitSql='';
$monthAppointmentCounts=[];
if($view==='month'){
    $monthCountStmt=$pdo->prepare('SELECT DATE(a.start_at) appointment_date,COUNT(*) total'.$fromSql.$whereSql.' GROUP BY DATE(a.start_at) ORDER BY DATE(a.start_at)');
    $monthCountStmt->execute($args);
    foreach($monthCountStmt->fetchAll() as $monthCountRow)$monthAppointmentCounts[(string)$monthCountRow['appointment_date']]=(int)$monthCountRow['total'];
    $appointments=[];$page=1;$perPage=max(1,$appointmentTotal);
}elseif($view!=='week'){
    $pages=max(1,(int)ceil($appointmentTotal/$perPage));
    $page=min($page,$pages);
    $offset=($page-1)*$perPage;
    $limitSql=' LIMIT '.$perPage.' OFFSET '.$offset;
    $sql='SELECT a.*,TRIM(CONCAT_WS(" ",p.first_name,NULLIF(p.middle_name,""),p.last_name)) patient,p.patient_no'.$emailSelect.$liveQueueSelect.$fromSql.$whereSql.$orderSql.$limitSql;
    $stmt=$pdo->prepare($sql);$stmt->execute($args);$appointments=$stmt->fetchAll();
}else{
    $page=1;$perPage=max(1,$appointmentTotal);
    $sql='SELECT a.*,TRIM(CONCAT_WS(" ",p.first_name,NULLIF(p.middle_name,""),p.last_name)) patient,p.patient_no'.$emailSelect.$liveQueueSelect.$fromSql.$whereSql.$orderSql;
    $stmt=$pdo->prepare($sql);$stmt->execute($args);$appointments=$stmt->fetchAll();
}
$appointmentServicesById=$appointments?appointment_services_for_ids($pdo,array_map(static fn(array $row): int => (int)$row['id'],$appointments)):[];

$resourceConflicts = [];
$conflictScopeDoctor = $selectedDentistId ?: $doctorFilterId;
if ($taskRole !== 'dental_assistant' && appointment_planning_schema_ready()) {
    $conflictSql = 'SELECT a1.id appointment_a,a2.id appointment_b,a1.patient_id patient_id_a,a2.patient_id patient_id_b,'
        . 's1.dentist_user_id dentist_id_a,s2.dentist_user_id dentist_id_b,s1.inhouse_provider_user_id inhouse_id_a,s2.inhouse_provider_user_id inhouse_id_b,'
        . 's1.start_at,s1.end_at,s1.service_name service_a,s2.service_name service_b,'
        . 'COALESCE(s1.dentist_name,s1.inhouse_provider_name,"Clinic team") provider_a,COALESCE(s2.dentist_name,s2.inhouse_provider_name,"Clinic team") provider_b,'
        . 's1.chair_id chair_id_a,s2.chair_id chair_id_b,s1.chair_name chair_a,s2.chair_name chair_b,'
        . 'CONCAT(p1.first_name," ",p1.last_name) patient_a,CONCAT(p2.first_name," ",p2.last_name) patient_b '
        . 'FROM `' . table('appointment_services') . '` s1 JOIN `' . table('appointment_services') . '` s2 '
        . 'ON s1.id<s2.id AND s1.start_at<s2.end_at AND s1.end_at>s2.start_at '
        . 'JOIN `' . table('appointments') . '` a1 ON a1.id=s1.appointment_id '
        . 'JOIN `' . table('appointments') . '` a2 ON a2.id=s2.appointment_id AND a1.branch_id=a2.branch_id '
        . 'JOIN `' . table('patients') . '` p1 ON p1.id=a1.patient_id JOIN `' . table('patients') . '` p2 ON p2.id=a2.patient_id '
        . 'WHERE a1.branch_id=? AND s1.start_at>=CURDATE() AND s1.status<>"cancelled" AND s2.status<>"cancelled" '
        . 'AND a1.status NOT IN ("cancelled","no_show","left_before_treatment") AND a2.status NOT IN ("cancelled","no_show","left_before_treatment") '
        . 'AND (a1.patient_id=a2.patient_id '
        . 'OR (s1.dentist_user_id IS NOT NULL AND s1.dentist_user_id=s2.dentist_user_id) '
        . 'OR (s1.inhouse_provider_user_id IS NOT NULL AND s1.inhouse_provider_user_id=s2.inhouse_provider_user_id) '
        . 'OR (s1.chair_id IS NOT NULL AND s1.chair_id=s2.chair_id) '
        . 'OR (TRIM(COALESCE(s1.chair_name,""))<>"" AND LOWER(TRIM(s1.chair_name))=LOWER(TRIM(s2.chair_name))))';
    $conflictArgs = [$branchId];
    if ($conflictScopeDoctor > 0) { $conflictSql .= ' AND (s1.dentist_user_id=? OR s2.dentist_user_id=?)'; $conflictArgs[]=$conflictScopeDoctor; $conflictArgs[]=$conflictScopeDoctor; }
    $conflictSql .= ' ORDER BY s1.start_at LIMIT 30';
    $conflictStmt=$pdo->prepare($conflictSql);$conflictStmt->execute($conflictArgs);$resourceConflicts=$conflictStmt->fetchAll();
}

$appointmentConflictWarnings = [];
foreach ($resourceConflicts as $conflict) {
    $types = [];
    if ((int)($conflict['patient_id_a'] ?? 0) > 0 && (int)($conflict['patient_id_a'] ?? 0) === (int)($conflict['patient_id_b'] ?? 0)) $types[] = 'Patient conflict';
    if ((int)($conflict['dentist_id_a'] ?? 0) > 0 && (int)($conflict['dentist_id_a'] ?? 0) === (int)($conflict['dentist_id_b'] ?? 0)) $types[] = 'Dentist conflict';
    if ((int)($conflict['inhouse_id_a'] ?? 0) > 0 && (int)($conflict['inhouse_id_a'] ?? 0) === (int)($conflict['inhouse_id_b'] ?? 0)) $types[] = 'In-house provider conflict';
    $sameChair=((int)($conflict['chair_id_a']??0)>0&&(int)$conflict['chair_id_a']===(int)($conflict['chair_id_b']??0));
    $chairA = strtolower(trim((string)($conflict['chair_a'] ?? '')));
    $chairB = strtolower(trim((string)($conflict['chair_b'] ?? '')));
    if ($sameChair || ($chairA !== '' && $chairA === $chairB)) $types[] = 'Chair conflict';
    foreach ([(int)($conflict['appointment_a'] ?? 0),(int)($conflict['appointment_b'] ?? 0)] as $conflictAppointmentId) {
        if ($conflictAppointmentId < 1) continue;
        $appointmentConflictWarnings[$conflictAppointmentId] = array_values(array_unique(array_merge($appointmentConflictWarnings[$conflictAppointmentId] ?? [],$types)));
    }
}

$appointmentsByDay = [];
foreach ($appointments as $appointment) $appointmentsByDay[date('Y-m-d',strtotime((string) $appointment['start_at']))][] = $appointment;
$weekDays = [];
for ($offset=0; $offset<7; $offset++) {
    $day = date('Y-m-d',strtotime($weekStart . ' +' . $offset . ' days'));
    $weekDays[] = ['date'=>$day,'appointments'=>$appointmentsByDay[$day] ?? []];
}
$monthDays=[];$monthVisibleTotal=0;
for($dayTimestamp=strtotime($monthGridStart),$monthGridLast=strtotime($monthGridEnd);$dayTimestamp<=$monthGridLast;$dayTimestamp=strtotime('+1 day',$dayTimestamp)){
    $dateKey=date('Y-m-d',$dayTimestamp);
    $inMonth=date('Y-m',$dayTimestamp)===$monthAnchor;$dayCount=(int)($monthAppointmentCounts[$dateKey]??0);if($inMonth)$monthVisibleTotal+=$dayCount;
    $monthDays[]=['date'=>$dateKey,'count'=>$dayCount,'in_month'=>$inMonth];
}

function appointment_schedule_state(array $appointment): array
{
    $queueStatus=trim((string)($appointment['live_queue_status']??''));
    $appointmentStatus=trim((string)($appointment['status']??'confirmed'));
    $state=$queueStatus!==''?$queueStatus:$appointmentStatus;
    return match($state){
        'completed'=>['key'=>'completed','label'=>'Completed','icon'=>'check'],
        'cancelled'=>['key'=>$appointmentStatus==='left_before_treatment'?'left':'cancelled','label'=>$appointmentStatus==='left_before_treatment'?'Left before treatment':'Cancelled','icon'=>'arrow-left'],
        'left_before_treatment'=>['key'=>'left','label'=>'Left before treatment','icon'=>'arrow-left'],
        'seated'=>['key'=>'in-chair','label'=>'In chair','icon'=>'patient'],
        'in_treatment'=>['key'=>'in-treatment','label'=>'In treatment','icon'=>'clinical'],
        'waiting_billing'=>['key'=>'for-payment','label'=>'Waiting for cashier','icon'=>'receipt'],
        'cashier_called'=>['key'=>'at-cashier','label'=>'At cashier','icon'=>'finance'],
        'called'=>['key'=>'called','label'=>'Called','icon'=>'queue'],
        'waiting','checked_in'=>['key'=>'waiting','label'=>'Checked in / waiting','icon'=>'clock'],
        'awaiting_billing'=>['key'=>'for-payment','label'=>'For payment','icon'=>'receipt'],
        'no_show'=>['key'=>'no-show','label'=>'No-show','icon'=>'patient'],
        'pending_confirmation'=>['key'=>'pending','label'=>'Pending confirmation','icon'=>'clock'],
        'requested'=>['key'=>'requested','label'=>'Requested','icon'=>'clipboard'],
        default=>['key'=>'scheduled','label'=>'Scheduled','icon'=>'calendar'],
    };
}

function appointment_page_url(array $changes = []): string
{
    $params = $_GET;
    unset($params['edit_id'],$params['new']);
    foreach ($changes as $key => $value) {
        if ($value === null || $value === '') unset($params[$key]);
        else $params[$key] = $value;
    }
    return 'appointments.php' . ($params ? '?' . http_build_query($params) : '');
}

$editId = (int) ($_GET['edit_id'] ?? 0);
$portalRequestId = (int) ($_GET['portal_request_id'] ?? 0);
$editAppointment = null;
$portalRequestPrefill = null;
if ($editId && $canManage) {
    try {
        $editAppointment = appointment_record($pdo,$editId,$branchId);
        if($portalRequestId>0&&patient_portal_request_schema_ready()){
            $portalStmt=$pdo->prepare('SELECT * FROM `' . table('patient_portal_requests') . '` WHERE id=? AND appointment_id=? AND patient_id=? AND request_type="appointment_reschedule" AND status="pending" LIMIT 1');
            $portalStmt->execute([$portalRequestId,$editId,(int)$editAppointment['patient_id']]);
            $portalRequestPrefill=$portalStmt->fetch()?:null;
            if(!$portalRequestPrefill)$error='The selected patient portal reschedule request is no longer pending.';
        }
    } catch (Throwable $exception) { $error = $exception->getMessage(); }
}
$editServiceRows=$editAppointment?($appointmentServicesById[(int)$editAppointment['id']]??(appointment_services_for_ids($pdo,[(int)$editAppointment['id']])[(int)$editAppointment['id']]??[])):[];
$editServiceIds=array_map(static fn(array $row): int => (int)$row['service_id'],$editServiceRows);
if($editAppointment&&!$editServiceIds)$editServiceIds=[(int)$editAppointment['service_id']];
$editPrimaryServiceId=(int)($editAppointment['service_id']??0);

$title = ($taskRole === 'dentist' && active_role_can_operate('dentist')) ? 'My Schedule' : (($taskRole === 'dentist') ? 'Dentist Schedule' : ($taskRole === 'dental_assistant' ? 'Assigned Schedule' : 'Clinic Schedule'));
render_header($title);
?>
<?php if ($error !== ''): ?><div class="flash error"><?=e($error)?></div><?php endif; ?>
<?php if ($assignmentPending > 0): ?><div class="alert warning"><strong><?= e((string)$assignmentPending) ?> provider assignment<?= $assignmentPending===1?'':'s' ?> need review.</strong> Assign an available Dentist before confirmation/check-in.<?php if($assignmentQueuePreview): ?><ul><?php foreach($assignmentQueuePreview as $assignment): ?><li><?= e((string)($assignment['patient_name'] ?: 'Patient')) ?> · <?= e((string)($assignment['service_name'] ?: 'Dental appointment')) ?><?= !empty($assignment['start_at'])?' · '.e(date('M j, g:i A',strtotime((string)$assignment['start_at']))):'' ?></li><?php endforeach; ?></ul><?php endif; ?></div><?php endif; ?>

<?php if($resourceConflicts): ?><section class="card resource-conflict-panel"><div class="card-head"><div><h2>Service-segment conflicts · <?=count($resourceConflicts)?></h2><p>Only the actual service segments overlap. Unused portions of a mixed visit no longer block a Dentist, in-house provider, or chair.</p></div></div><table><thead><tr><th>Segment time</th><th>First booking</th><th>Second booking</th><th>Action</th></tr></thead><tbody><?php foreach(array_slice($resourceConflicts,0,10) as $conflict):?><tr><td><?=e(date('M j, Y g:i A',strtotime((string)$conflict['start_at'])))?>–<?=e(date('g:i A',strtotime((string)$conflict['end_at'])))?></td><td><strong><?=e((string)$conflict['patient_a'])?></strong><small><?=e((string)$conflict['service_a'])?> · <?=e((string)$conflict['provider_a'])?> · <?=e((string)($conflict['chair_a']?:'No chair'))?></small></td><td><strong><?=e((string)$conflict['patient_b'])?></strong><small><?=e((string)$conflict['service_b'])?> · <?=e((string)$conflict['provider_b'])?> · <?=e((string)($conflict['chair_b']?:'No chair'))?></small></td><td><?php if($canManage):?><a href="appointments.php?edit_id=<?=(int)$conflict['appointment_b']?>">Reschedule second</a><?php else:?><span class="muted">Ask reception or administration to reschedule.</span><?php endif;?></td></tr><?php endforeach;?></tbody></table></section><?php endif; ?>

<?php if ($taskRole === 'dentist' && !active_role_can_operate('dentist') && $dentists): ?>
<section class="card task-target-selector">
    <form method="get" action="appointments.php" class="toolbar" data-auto-submit>
        <input type="hidden" name="view" value="<?=e($view)?>"><?php if ($view === 'week'): ?><input type="hidden" name="week" value="<?=e($weekStart)?>"><?php elseif($view === 'month'): ?><input type="hidden" name="month" value="<?=e($monthAnchor)?>"><?php endif; ?>
        <div><strong>Dentist schedule</strong><span class="muted">Choose the Dentist schedule you need to review.</span></div>
        <label>Search dentist<input type="search" data-select-search="task-dentist-schedule" placeholder="Type dentist name" autocomplete="off"></label>
        <label>Dentist<select id="task-dentist-schedule" name="dentist_id"><?php foreach ($dentists as $doctor): ?><option value="<?=(int) $doctor['id']?>" <?=$selectedDentistId === (int) $doctor['id'] ? 'selected' : ''?>><?=e((string) $doctor['name'])?></option><?php endforeach; ?></select></label>
    </form>
</section>
<?php endif; ?>

<div class="toolbar schedule-toolbar">
    <div class="view-switch">
        <a href="<?=e(appointment_page_url(['view'=>'month','month'=>$monthAnchor,'week'=>null,'focus_date'=>null]))?>" class="<?=$view === 'month' ? 'active' : ''?>">Month</a>
        <a href="<?=e(appointment_page_url(['view'=>'week','week'=>$weekStart,'month'=>null]))?>" class="<?=$view === 'week' ? 'active' : ''?>">Week</a>
        <a href="<?=e(appointment_page_url(['view'=>'today','week'=>null,'month'=>null,'focus_date'=>null]))?>" class="<?=$view === 'today' ? 'active' : ''?>">Today</a>
        <a href="<?=e(appointment_page_url(['view'=>'upcoming','week'=>null,'month'=>null,'focus_date'=>null]))?>" class="<?=$view === 'upcoming' ? 'active' : ''?>">Upcoming</a>
        <a href="<?=e(appointment_page_url(['view'=>'history','week'=>null,'month'=>null,'focus_date'=>null]))?>" class="<?=$view === 'history' ? 'active' : ''?>">History</a>
    </div>
    <div class="toolbar-actions">
        <?php if ($canProcessRequests): ?><a class="btn ghost" href="appointment_requests.php">Online requests<?=$onlinePending ? ' · ' . e((string) $onlinePending) : ''?></a><?php endif; ?>
        <?php if (task_context_page_allowed('visit_workflow')): ?><a class="btn ghost" href="visit_workflow.php">Visit workflow</a><?php endif; ?>
        <?php if ($canManage): ?><button class="btn ghost" data-open="appointment-plan-form">Book visit plan</button><button class="btn primary" data-open="appointment-form">Book appointment</button><?php endif; ?>
    </div>
</div>

<form method="get" action="appointments.php" class="card compact-filter appointment-auto-filter" data-auto-submit data-scroll-target="#appointment-schedule">
    <input type="hidden" name="view" value="<?=e($view)?>"><?php if ($view === 'week'): ?><input type="hidden" name="week" value="<?=e($weekStart)?>"><?php elseif($view === 'month'): ?><input type="hidden" name="month" value="<?=e($monthAnchor)?>"><?php endif; ?><?php if ($taskRole === 'dentist' && !active_role_can_operate('dentist')): ?><input type="hidden" name="dentist_id" value="<?=$selectedDentistId?>"><?php endif; ?>
    <label>Search<input type="search" name="q" value="<?=e($q)?>" placeholder="Patient, number, service, dentist, chair, or reason" autocomplete="off" data-auto-search data-min-length="2"></label>
    <label>Status<select name="status"><option value="">All statuses</option><?php foreach ($allowedStatusFilters as $status): ?><option value="<?=e($status)?>" <?=$statusFilter === $status ? 'selected' : ''?>><?=e(ucwords(str_replace('_',' ',$status)))?></option><?php endforeach; ?></select></label>
    <label>Provider<select name="provider"><option value="">All providers</option><option value="dentist" <?=$providerFilter==='dentist'?'selected':''?>>Dentist required</option><option value="inhouse" <?=$providerFilter==='inhouse'?'selected':''?>>In-house team</option></select></label>
    <?php if (!in_array($taskRole,['dentist','dental_assistant'],true)): ?>
        <label>Search dentist<input type="search" data-select-search="appointment-doctor-filter" placeholder="Type dentist name" autocomplete="off"></label>
        <label>Dentist<select id="appointment-doctor-filter" name="doctor_filter"><option value="">All dentists</option><?php foreach ($dentists as $doctor): ?><option value="<?=(int) $doctor['id']?>" <?=$doctorFilterId === (int) $doctor['id'] ? 'selected' : ''?>><?=e((string) $doctor['name'])?></option><?php endforeach; ?></select></label>
    <?php endif; ?>
    <?php if (in_array($view,['month','week','today','upcoming'],true)): ?><label class="check-option appointment-history-toggle"><input type="checkbox" name="show_schedule_history" value="1" <?= $showScheduleHistory ? 'checked' : '' ?> onchange="this.form.submit()"><span>Show cancelled / no-show / left</span></label><?php endif; ?>
    <?php if ($q !== '' || $statusFilter !== '' || $providerFilter !== '' || $doctorFilterId > 0 || $showScheduleHistory): ?><a class="btn link" href="<?=e(appointment_page_url(['q'=>null,'status'=>null,'provider'=>null,'doctor_filter'=>null,'show_schedule_history'=>null]))?>" data-scroll-target="#appointment-schedule">Clear</a><?php endif; ?>
</form>

<?php if ($editAppointment): ?>
<section class="card">
    <div class="card-head"><h2>Reschedule <?=e((string) $editAppointment['patient'])?></h2><a href="<?=e(appointment_page_url())?>">Cancel</a></div>
    <form method="post" class="form-grid" data-slot-picker data-confirmation-form data-branch-id="<?=$branchId?>" data-keyboard-entry data-keyboard-save data-protect-unsaved>
        <input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="reschedule"><input type="hidden" name="appointment_id" value="<?=(int) $editAppointment['id']?>"><?php if($portalRequestPrefill):?><input type="hidden" name="portal_request_id" value="<?=(int)$portalRequestPrefill['id']?>"><?php endif;?>
        <label class="wide">Patient<input type="hidden" name="patient_id" value="<?=(int)$editAppointment['patient_id']?>"><input value="<?=e((string)$editAppointment['patient'].' · '.(string)$editAppointment['patient_no'])?>" readonly></label>
        <label>Primary service<select name="service_id" data-slot-service required><option value="">Choose service</option><?php foreach ($services as $service): ?><option value="<?=(int)$service['id']?>" data-provider-type="<?=e(service_provider_type($service))?>" data-requires-chair="<?=(int)($service['requires_chair']??1)?>" data-duration="<?=(int)($service['duration_minutes']??30)?>" <?=$editPrimaryServiceId===(int)$service['id']?'selected':''?>><?=e($service['name'].' · '.$service['duration_minutes'].' min')?></option><?php endforeach;?></select></label><label class="wide">Additional services for the same visit<select name="additional_service_ids[]" data-slot-additional-services multiple size="<?=min(6,max(3,count($services)))?>"><?php foreach($services as $service): if((int)$service['id']===$editPrimaryServiceId)continue;?><option value="<?=(int)$service['id']?>" data-provider-type="<?=e(service_provider_type($service))?>" data-requires-chair="<?=(int)($service['requires_chair']??1)?>" data-duration="<?=(int)($service['duration_minutes']??30)?>" <?=in_array((int)$service['id'],$editServiceIds,true)?'selected':''?>><?=e($service['name'].' · '.$service['duration_minutes'].' min')?></option><?php endforeach;?></select><small>Use Add service and Remove controls. All selected services share one appointment and one queue number.</small></label><div class="wide alert info" data-service-bundle-summary>Select services to calculate the coordinated visit duration.</div>
        <label>Dentist<select name="dentist_user_id" data-slot-doctor data-continuity-doctor><?php foreach ($dentists as $doctor): ?><option value="<?=(int) $doctor['id']?>" <?=(int) $editAppointment['dentist_user_id'] === (int) $doctor['id'] ? 'selected' : ''?>><?=e((string) $doctor['name'])?></option><?php endforeach; ?></select></label>
        <?php if($portalRequestPrefill):?><div class="alert info wide"><strong>Patient preference:</strong> <?=e(date('M j, Y',strtotime((string)$portalRequestPrefill['preferred_date'])))?><?php if($portalRequestPrefill['preferred_time_from']):?> · <?=e(date('g:i A',strtotime((string)$portalRequestPrefill['preferred_time_from'])))?><?php endif;?><?php if($portalRequestPrefill['preferred_time_to']):?>–<?=e(date('g:i A',strtotime((string)$portalRequestPrefill['preferred_time_to'])))?><?php endif;?> · <?=e((string)$portalRequestPrefill['reason'])?></div><?php endif;?>
        <?php $prefillDate=$portalRequestPrefill&&$portalRequestPrefill['preferred_date']?(string)$portalRequestPrefill['preferred_date']:date('Y-m-d',strtotime((string)$editAppointment['start_at']));$prefillTime=$portalRequestPrefill&&$portalRequestPrefill['preferred_time_from']?substr((string)$portalRequestPrefill['preferred_time_from'],0,5):date('H:i',strtotime((string)$editAppointment['start_at']));?>
        <label>Date<input type="date" name="date" data-slot-date min="<?=date('Y-m-d')?>" value="<?=e($prefillDate)?>" required></label><div class="available-dates-panel" data-available-dates>Choose a service and dentist to see available dates.</div>
        <label>Time<select name="time" data-slot-time required><option value="<?=e($prefillTime)?>"><?=e(date('g:i A',strtotime($prefillTime)))?> <?=$portalRequestPrefill?'patient preference':'current'?></option></select></label>
        <label>Assigned chair<input type="hidden" name="inhouse_provider_user_id" data-inhouse-provider value="<?=e((string)($editAppointment['inhouse_provider_user_id']??''))?>"><input type="hidden" name="chair_id" data-slot-chair-id value="<?=e((string)($editAppointment['chair_id']??''))?>"><input type="hidden" name="chair_name" data-slot-chair value="<?=e((string) $editAppointment['chair_name'])?>"><input data-slot-chair-display value="<?=e((string) ($editAppointment['chair_name'] ?: 'Assigned from dentist'))?>" readonly></label>
        <details class="wide booking-advanced-options" <?=((string)$editAppointment['status']==='confirmed'||$portalRequestPrefill)?'open':''?>> <summary><span>Advanced booking options</span><small>Confirmation evidence, booking source</small></summary><div class="form-grid booking-advanced-grid"><label>Status<select name="status" data-confirmation-status><option value="pending_confirmation" <?=((string)$editAppointment['status']!=='confirmed'?'selected':'')?>>Pending confirmation</option><option value="confirmed" <?=((string)$editAppointment['status']==='confirmed'?'selected':'')?>>Confirmed</option></select></label><label>Booking source<select name="booking_source"><option value="phone">Phone</option><option value="in_person">In person</option><option value="email">Email</option><option value="patient_portal" <?=!empty($portalRequestPrefill)?'selected':''?>>Patient portal</option><option value="staff">Internal staff booking</option></select></label><div class="wide form-grid" data-confirmation-fields><label>Agreement source<select name="confirmation_source"><option value="">Choose agreement evidence</option><option value="patient_phone">Patient by phone</option><option value="patient_in_person">Patient in person</option><option value="patient_email">Patient by email</option><option value="patient_portal">Patient portal</option><option value="guardian_phone">Guardian by phone</option><option value="guardian_in_person">Guardian in person</option></select></label><label class="wide">Confirmation note<textarea name="confirmation_note" maxlength="500" placeholder="Who agreed, how, and the outcome"></textarea></label></div></div></details>
        <label class="wide">Reason for visit<textarea name="reason_for_visit"><?=e((string) ($editAppointment['reason_for_visit'] ?: $editAppointment['notes']))?></textarea></label>
        <label class="wide">Reschedule/contact note<textarea name="reschedule_reason" required placeholder="Record the patient agreement, mail-server result, call outcome, or reason for changing the appointment."></textarea></label>
        <label class="wide">Internal notes<textarea name="notes"><?=e((string) $editAppointment['notes'])?></textarea></label>
        <details class="wide booking-advanced-options booking-exception-options"><summary><span>Continuity or schedule exception</span><small>Open only for an approved dentist transfer or schedule bypass</small></summary><div class="form-grid booking-advanced-grid"><div class="wide continuity-care-note" data-continuity-note>Selecting the patient automatically uses the established continuity dentist.</div>
        <?php if ($canTransferDentist): ?><label class="wide check-label"><input type="checkbox" name="change_continuity_dentist" value="1" data-continuity-transfer><span>Transfer this patient’s future care to the selected dentist</span></label><label class="wide" data-continuity-reason-wrap hidden>Care-transfer reason<textarea name="continuity_change_reason" data-continuity-reason placeholder="Patient request, specialty referral, dentist departure, or approved reassignment"></textarea></label><?php endif; ?>
        <?php if ($canBypassSchedule): ?><label class="wide check-label"><input type="checkbox" name="schedule_bypass" value="1" data-bypass-toggle="#reschedule-bypass-reason"><span>Use Schedule Bypass only for an authorized time outside published availability. Patient, dentist, and chair conflicts remain blocked.</span></label><div class="wide form-grid" id="reschedule-bypass-reason" hidden><label>Manual time<input type="time" name="bypass_time"></label><label class="wide">Bypass reason<textarea name="schedule_bypass_reason" placeholder="Emergency, doctor-approved overbook, after-hours arrangement, or other authorized reason"></textarea></label></div><?php endif; ?>
        </div></details><button class="btn primary wide" data-status-submit data-confirmed-label="Save confirmed schedule" data-pending-label="Save pending schedule">Save pending schedule</button>
    </form>
</section>
<?php endif; ?>

<div id="appointment-schedule" data-live-queue-region="appointment-status-list" data-scroll-context>
<?php if ($view === 'month'): ?>
<section class="card appointment-month-card">
    <div class="month-calendar-head">
        <a class="icon-button" href="<?=e(appointment_page_url(['view'=>'month','month'=>date('Y-m',strtotime($monthStart . ' -1 month')),'week'=>null,'focus_date'=>null]))?>" aria-label="Previous month">‹</a>
        <div><span class="eyebrow">Monthly schedule</span><h2><?=e(date('F Y',strtotime($monthStart)))?></h2><p><?=e((string)$monthVisibleTotal)?> appointment<?=$monthVisibleTotal===1?'':'s'?> · select a date to open its week</p></div>
        <div class="month-calendar-actions"><a class="btn link" href="<?=e(appointment_page_url(['view'=>'month','month'=>date('Y-m'),'week'=>null,'focus_date'=>null]))?>">This month</a><a class="icon-button" href="<?=e(appointment_page_url(['view'=>'month','month'=>date('Y-m',strtotime($monthStart . ' +1 month')),'week'=>null,'focus_date'=>null]))?>" aria-label="Next month">›</a></div>
    </div>
    <div class="appointment-month-weekdays" aria-hidden="true"><?php foreach(['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] as $weekday):?><span><?=e($weekday)?></span><?php endforeach;?></div>
    <div class="appointment-month-grid" role="grid" aria-label="<?=e(date('F Y',strtotime($monthStart)))?> appointment counts">
        <?php foreach($monthDays as $monthDay): $monthDate=(string)$monthDay['date'];$monthCount=(int)$monthDay['count'];$monthToday=$monthDate===date('Y-m-d');?>
        <a class="appointment-month-day <?=!$monthDay['in_month']?'outside-month ':''?><?=$monthToday?'today ':''?>" role="gridcell" href="<?=e(appointment_page_url(['view'=>'week','week'=>$monthDate,'focus_date'=>$monthDate,'month'=>null]))?>" aria-label="<?=e(date('l, F j, Y',strtotime($monthDate)).': '.$monthCount.' appointment'.($monthCount===1?'':'s'))?>">
            <span class="month-day-number"><?=e(date('j',strtotime($monthDate)))?></span>
            <strong class="month-appointment-count"><?=e((string)$monthCount)?></strong>
            <small>appointment<?=$monthCount===1?'':'s'?></small>
        </a>
        <?php endforeach;?>
    </div>
</section>
<?php elseif ($view === 'week'): ?>
<section class="card appointment-week-card appointment-week-simple">
    <div class="week-calendar-head">
        <a class="icon-button" href="<?=e(appointment_page_url(['view'=>'week','week'=>date('Y-m-d',strtotime($weekStart . ' -7 days')),'focus_date'=>null]))?>" aria-label="Previous week">‹</a>
        <div><span class="eyebrow">Weekly schedule</span><h2><?=e(date('F j',strtotime($weekStart)) . '–' . date('F j, Y',strtotime($weekEnd)))?></h2><p><?=count($appointments)?> appointment<?=count($appointments) === 1 ? '' : 's'?> · <?= $showScheduleHistory || $statusFilter!=='' ? 'history/terminal statuses visible as non-blocking tiles' : 'cancelled/no-show/left visits hidden from the active schedule' ?></p></div>
        <div class="week-calendar-actions"><a class="btn link" href="<?=e(appointment_page_url(['view'=>'week','week'=>date('Y-m-d'),'focus_date'=>null]))?>">This week</a><a class="icon-button" href="<?=e(appointment_page_url(['view'=>'week','week'=>date('Y-m-d',strtotime($weekStart . ' +7 days')),'focus_date'=>null]))?>" aria-label="Next week">›</a></div>
    </div>
    <div class="appointment-state-legend" aria-label="Appointment status legend">
        <?php foreach([
            ['scheduled','Scheduled','calendar'],['waiting','Waiting','clock'],['called','Called','queue'],['in-chair','In chair','patient'],
            ['in-treatment','Treatment','clinical'],['for-payment','Cashier','receipt'],['completed','Completed','check'],['left','Left','arrow-left']
        ] as $legend):?><span class="appointment-state-key state-<?=e($legend[0])?>" title="<?=e($legend[1])?>"><?=dental_icon($legend[2],'appointment-state-svg')?><em><?=e($legend[1])?></em></span><?php endforeach;?>
    </div>
    <div class="appointment-week-carousel" data-week-carousel>
        <div class="appointment-week-scroll-controls" aria-label="Calendar day navigation">
            <button type="button" class="btn ghost compact" data-week-scroll="-1" aria-label="Show previous days">‹ Previous days</button>
            <span data-week-position aria-live="polite">Days 1–7 of 7</span>
            <button type="button" class="btn ghost compact" data-week-scroll="1" aria-label="Show next days">Next days ›</button>
        </div>
        <div class="appointment-week-viewport" data-week-viewport tabindex="0" aria-label="Seven-day appointment calendar. Use the left and right arrow keys or horizontal scrolling to move between days.">
            <div class="appointment-week-grid" data-week-grid>
                <?php foreach ($weekDays as $day): $isToday = $day['date'] === date('Y-m-d'); $isSelected=$focusDate!==''&&$day['date']===$focusDate; ?>
                <section class="appointment-day-column <?=$isToday ? 'today' : ''?> <?=$isSelected ? 'selected-date' : ''?>" data-week-day data-date="<?=e($day['date'])?>">
                    <header><span><?=e(date('D',strtotime($day['date'])))?></span><strong><?=e(date('j',strtotime($day['date'])))?></strong><small><?=e(date('M',strtotime($day['date'])))?> · <?=count($day['appointments'])?></small></header>
                    <div class="appointment-day-list" data-page-label="appointments">
                        <?php foreach ($day['appointments'] as $row): $tileConflicts = $appointmentConflictWarnings[(int)$row['id']] ?? []; $rowServices=$appointmentServicesById[(int)$row['id']]??[]; $scheduleState=appointment_schedule_state($row); $providerName=(($row['provider_type']??'dentist')==='inhouse'?(string)($row['inhouse_provider_name']?:'Clinic care team'):(string)$row['dentist_name']); $displayChair=(string)($row['live_queue_chair_name']?:$row['chair_name']?:((($row['provider_type']??'dentist')==='inhouse')?'No chair':'Unassigned')); ?>
                        <article class="appointment-calendar-tile compact-state-tile state-<?=e($scheduleState['key'])?> status-<?=e((string)$row['status'])?> <?=$tileConflicts ? 'has-conflict' : ''?>">
                            <div class="appointment-tile-head">
                                <strong><?=e(date('g:i A',strtotime((string)$row['start_at'])))?></strong>
                                <span class="appointment-state-icon state-<?=e($scheduleState['key'])?>" title="<?=e($scheduleState['label'])?>" aria-label="<?=e($scheduleState['label'])?>"><?=dental_icon($scheduleState['icon'],'appointment-state-svg')?><span class="sr-only"><?=e($scheduleState['label'])?></span></span>
                            </div>
                            <?php if ($tileConflicts): ?><div class="appointment-conflict-tags"><?php foreach ($tileConflicts as $conflictLabel): ?><span><?=e($conflictLabel)?></span><?php endforeach; ?></div><?php endif; ?>
                            <h3 title="<?=e((string)$row['patient'])?>"><?=e((string)$row['patient'])?></h3>
                            <p class="appointment-tile-service" title="<?=e((string)$row['service_name'])?>"><?=e((string)$row['service_name'])?><?php if(count($rowServices)>1):?> <b>+<?=count($rowServices)-1?></b><?php endif;?></p>
                            <p class="appointment-tile-compact-meta" title="<?=e($providerName.' · '.$displayChair)?>"><?=e($providerName)?> · <?=e($displayChair)?></p>
                            <?php if(!empty($row['series_id'])):?><span class="appointment-series-badge"><?=e((string)($row['planned_visit_label']?:'Treatment series'))?></span><?php endif;?>
                            <div class="appointment-tile-actions">
                                <?php if (task_context_page_allowed('patients')): ?><a class="tile-action-primary" href="patients.php?action=view&amp;id=<?=(int)$row['patient_id']?>">Open</a><?php endif; ?>
                                <?php if ($canCheckIn && $day['date'] === date('Y-m-d') && (string)$row['status']==='confirmed'): ?><a class="tile-action-primary" href="queue.php?appointment_id=<?=(int)$row['id']?>">Check in</a><?php endif; ?>
                                <details class="tile-more-actions"><summary aria-label="More actions and details for <?=e((string)$row['patient'])?>">⋯<span class="sr-only">More actions</span></summary><div class="appointment-menu-panel">
                                    <strong><?=e($scheduleState['label'])?></strong><?php if(in_array((string)$row['status'],$terminalScheduleStatuses,true)):?><span class="badge neutral">Non-blocking history</span><?php endif;?>
                                    <small><?=e(date('g:i A',strtotime((string)$row['start_at'])))?>–<?=e(date('g:i A',strtotime((string)$row['end_at'])))?> · <?=e($providerName)?> · <?=e($displayChair)?></small>
                                    <?php if(count($rowServices)>1):?><div class="appointment-menu-services"><?php foreach($rowServices as $segment):?><small><?=e((string)$segment['service_name'])?> · <?=e(date('g:i A',strtotime((string)$segment['start_at'])))?>–<?=e(date('g:i A',strtotime((string)$segment['end_at'])))?></small><?php endforeach;?></div><?php endif;?>
                                    <?php if($row['reason_for_visit']):?><small>Reason: <?=e((string)$row['reason_for_visit'])?></small><?php endif;?>
                                    <?php if(!empty($row['live_queue_no'])):?><small>Queue <?=e((string)$row['live_queue_no'])?></small><?php endif;?>
                                    <a href="<?=e(appointment_calendar_relative_url($row))?>">Add to calendar</a>
                                    <?php if ($canManage && in_array((string)$row['status'],['requested','pending_confirmation','confirmed'],true)): ?>
                                        <a href="<?=e(appointment_page_url(['edit_id'=>(int)$row['id']]))?>" data-scroll-target="#appointment-schedule">Reschedule</a>
                                        <?php if((string)$row['status']==='confirmed'):?><form method="post" class="appointment-menu-action"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="resend_email"><input type="hidden" name="appointment_id" value="<?=(int)$row['id']?>"><button class="link-btn" type="submit"><?=in_array((string)($row['email_status']??''),['failed','retry'],true)?'Retry email':'Resend confirmation'?></button></form><?php endif;?>
                                        <?php if((string)$row['status']==='confirmed'):?><form method="post" class="appointment-menu-action"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="status"><input type="hidden" name="appointment_id" value="<?=(int)$row['id']?>"><input type="hidden" name="status" value="pending_confirmation"><button class="link-btn" type="submit">Mark pending</button></form><?php else:?><a href="<?=e(appointment_page_url(['edit_id'=>(int)$row['id']]))?>" data-scroll-target="#appointment-schedule">Review &amp; confirm</a><?php endif;?>
                                        <form method="post" class="appointment-menu-status"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="status"><input type="hidden" name="appointment_id" value="<?=(int)$row['id']?>"><label>Status<select name="status"><option value="cancelled">Cancelled</option><?php if(strtotime((string)$row['start_at'])<=time()&&(string)$row['status']==='confirmed'):?><option value="no_show">No-show</option><?php endif;?></select></label><label>Reason<input name="reason" required placeholder="Required reason"></label><button class="btn ghost compact" type="submit">Apply status</button></form>
                                    <?php elseif($canManage && in_array((string)$row['status'],$terminalScheduleStatuses,true)): ?>
                                        <?php if((string)$row['status']==='cancelled'):?><form method="post" class="appointment-menu-action"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="resend_email"><input type="hidden" name="appointment_id" value="<?=(int)$row['id']?>"><button class="link-btn" type="submit"><?=in_array((string)($row['email_status']??''),['failed','retry'],true)?'Retry cancellation email':'Resend cancellation'?></button></form><?php endif;?>
                                        <small class="muted">This tile is historical only. It does not block the dentist, chair, or patient schedule.</small>
                                    <?php endif; ?>
                                </div></details>
                            </div>
                        </article>
                        <?php endforeach; ?>
                        <?php if (!$day['appointments']): ?><div class="empty-day-tile">No appointments</div><?php endif; ?>
                    </div>
                </section>
                <?php endforeach; ?>
            </div>
        </div>
    </div>
</section>
<?php else: ?>
<div class="card server-list-card"><?php module_render_server_pagination($appointmentTotal,$page,$perPage,'page','per_page','appointments'); ?><div class="table-wrap" data-no-pagination><table data-no-pagination="true"><thead><tr><th>Date & time</th><th>Patient</th><th>Provider</th><th>Service / reason</th><th>Chair</th><th>Status</th><th></th></tr></thead><tbody>
<?php foreach ($appointments as $row): $rowServices=$appointmentServicesById[(int)$row['id']]??[]; ?><tr><td><?=e(date('M j, Y g:i A',strtotime((string) $row['start_at'])))?><small><?=e(ucwords(str_replace('_',' ',(string) ($row['appointment_origin'] ?? 'manual'))))?></small></td><td><strong><?=e((string) $row['patient'])?></strong><small><?=e((string) $row['patient_no'])?></small></td><td><?=e(($row['provider_type']??'dentist')==='inhouse'?(string)($row['inhouse_provider_name']?:'Unassigned in-house provider'):(string)$row['dentist_name'])?></td><td><?=e((string)$row['service_name'])?><?php if(!empty($row['series_id'])):?><span class="appointment-series-badge"><?=e((string)($row['planned_visit_label']?:'Treatment series'))?></span><?php endif;?><?php if(count($rowServices)>1):?><div class="appointment-service-segments"><?php foreach($rowServices as $segment):?><small><span><?=e((string)$segment['service_name'])?></span><span><?=e(date('g:i A',strtotime((string)$segment['start_at'])))?>–<?=e(date('g:i A',strtotime((string)$segment['end_at'])))?></span></small><?php endforeach;?></div><?php endif;?><small><?=e((string)($row['reason_for_visit']?:$row['notes']))?></small></td><td><?=e((string) ($row['chair_name'] ?: '—'))?></td><td><span class="badge"><?=e(ucwords(str_replace('_',' ',(string) $row['status'])))?></span><?php if (!empty($row['schedule_bypass'])): ?><small class="warning-text">Schedule bypass</small><?php endif; ?><?php if(!empty($row['email_status'])):?><small><span class="badge email-<?=e((string)$row['email_status'])?>"><?=e(email_delivery_label((string)$row['email_status']))?></span></small><?php if(!empty($row['email_error'])):?><small class="warning-text" title="<?=e((string)$row['email_error'])?>">Delivery error</small><?php endif;?><?php endif;?></td><td class="appointment-action-cell"><div class="appointment-list-actions">
    <?php if ($canCheckIn && date('Y-m-d',strtotime((string) $row['start_at'])) === date('Y-m-d') && (string)$row['status']==='confirmed'): ?><a class="list-action-primary" href="queue.php?appointment_id=<?=(int) $row['id']?>">Check in</a><?php endif; ?>
    <details class="appointment-row-more"><summary>Actions</summary><div class="appointment-menu-panel appointment-list-menu">
        <a href="<?=e(appointment_calendar_relative_url($row))?>">Add to calendar</a>
        <?php if($canManage&&in_array((string)$row['status'],['confirmed','cancelled'],true)):?><form method="post" class="appointment-menu-action"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="resend_email"><input type="hidden" name="appointment_id" value="<?=(int)$row['id']?>"><button class="link-btn" type="submit"><?=in_array((string)($row['email_status']??''),['failed','retry'],true)?'Retry email':((string)$row['status']==='cancelled'?'Resend cancellation':'Resend confirmation')?></button></form><?php endif;?>
        <?php if($canManage):?><a href="appointments.php?new=1&patient_id=<?=(int)$row['patient_id']?>">Book another visit</a><?php endif;?>
        <?php if ($canManage && in_array((string)$row['status'],['requested','pending_confirmation','confirmed'],true)): ?>
            <a href="<?=e(appointment_page_url(['edit_id'=>(int) $row['id']]))?>">Reschedule</a>
            <?php if((string)$row['status']==='confirmed'):?><form method="post" class="appointment-menu-action"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="status"><input type="hidden" name="appointment_id" value="<?=(int)$row['id']?>"><input type="hidden" name="status" value="pending_confirmation"><button class="link-btn" type="submit">Mark pending</button></form><?php else:?><a href="<?=e(appointment_page_url(['edit_id'=>(int)$row['id']]))?>">Review &amp; confirm</a><?php endif;?>
            <details class="inline-details appointment-menu-cancel"><summary>Cancel / no-show</summary><form method="post" class="compact-form"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="status"><input type="hidden" name="appointment_id" value="<?=(int) $row['id']?>"><select name="status"><option value="cancelled">Cancelled</option><?php if(strtotime((string)$row['start_at'])<=time()&&(string)$row['status']==='confirmed'):?><option value="no_show">No-show</option><?php endif;?></select><input name="reason" required placeholder="Reason"><button class="btn ghost compact" type="submit">Apply</button></form></details>
        <?php endif; ?>
    </div></details>
</div></td></tr><?php endforeach; ?>
<?php if (!$appointments): ?><tr><td colspan="7" class="muted">No appointments match this automatic filter.</td></tr><?php endif; ?>
</tbody></table></div><?php module_render_server_pagination($appointmentTotal,$page,$perPage,'page','per_page','appointments'); ?></div>
<?php endif; ?>
</div>

<?php if ($canManage): ?>
<dialog id="appointment-plan-form"><form method="post" class="form-grid" data-appointment-plan data-confirmation-form data-branch-id="<?=$branchId?>" data-keyboard-entry data-keyboard-save data-protect-unsaved><button type="button" class="dialog-close" data-close>×</button><h2 class="wide">Book future visit plan</h2><p class="wide muted">Use this for different future appointments such as examination → treatment → review. The complete plan is validated and saved together.</p><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="create_plan">
<label class="wide">Patient<div class="remote-patient-picker" data-patient-picker data-endpoint="api/patient_search.php"><input type="search" data-patient-query placeholder="Search patient number, name, mobile, or email" autocomplete="off" value="<?=e($patientPrefill ? ((string)$patientPrefill['patient_no'].' · '.(string)$patientPrefill['last_name'].', '.(string)$patientPrefill['first_name']) : '')?>" required><input type="hidden" name="patient_id" data-patient-id value="<?=e((string)($patientPrefill['patient_id']??''))?>" data-primary-dentist-id="<?=e((string)($patientPrefill['primary_dentist_user_id']??''))?>" data-primary-dentist-name="<?=e((string)($patientPrefill['primary_dentist_name']??''))?>"><div class="remote-patient-selection" data-patient-selection <?=$patientPrefill?'':'hidden'?>><?=$patientPrefill?'Selected patient':''?></div><div class="remote-patient-results" data-patient-results hidden></div></div></label>
<div class="wide patient-upcoming-panel" data-patient-upcoming data-endpoint="api/patient_upcoming_appointments.php"><strong>Future visits for this patient</strong><p class="muted">Select a patient to review existing future appointments.</p></div>
<label>Plan name<input name="series_name" maxlength="190" required placeholder="Example: Extraction and review plan"></label><label>Default Dentist<select name="dentist_user_id" data-continuity-doctor><option value="">Choose default Dentist</option><?php foreach($dentists as $doctor):?><option value="<?=(int)$doctor['id']?>"><?=e((string)$doctor['name'])?></option><?php endforeach;?></select><small>Used only when a Dentist-required row has no visit-specific provider.</small></label><label>Status<select name="status" data-confirmation-status><option value="pending_confirmation">Pending confirmation</option><option value="confirmed">Confirmed</option><option value="requested">Requested</option></select></label><label>Booking source<select name="booking_source"><option value="phone">Phone</option><option value="in_person">In person</option><option value="email">Email</option><option value="patient_portal">Patient portal</option><option value="staff">Internal staff booking</option></select></label><div class="wide form-grid" data-confirmation-fields hidden><label>Agreement source<select name="confirmation_source"><option value="">Choose agreement evidence</option><option value="patient_phone">Patient by phone</option><option value="patient_in_person">Patient in person</option><option value="patient_email">Patient by email</option><option value="patient_portal">Patient portal</option><option value="guardian_phone">Guardian by phone</option><option value="guardian_in_person">Guardian in person</option></select></label><label class="wide">Confirmation note<textarea name="confirmation_note" maxlength="500" placeholder="Who agreed, how, and the outcome"></textarea></label></div><label class="wide">Overall reason / treatment objective<textarea name="reason_for_visit" required></textarea></label>
<div class="wide appointment-plan-builder"><div class="card-head"><div><h3>Future visits</h3><span class="muted">Each row becomes a separate appointment. Its queue visit is created only when the patient checks in.</span></div><button type="button" class="btn ghost compact" data-plan-add>Add future visit</button></div><div data-plan-rows>
<?php for($planIndex=0;$planIndex<2;$planIndex++):?><div class="appointment-plan-row" data-plan-row><span class="plan-row-number">Visit <?=$planIndex+1?></span><label>Visit label<input name="plan_label[]" placeholder="<?= $planIndex===0?'Examination':'Treatment / review' ?>"></label><label>Service<select name="plan_service_id[]" data-plan-service required><option value="">Choose service</option><?php foreach($services as $service):?><option value="<?=(int)$service['id']?>"><?=e($service['name'].' · '.$service['duration_minutes'].' min')?></option><?php endforeach;?></select></label><label>Visit Dentist<select name="plan_dentist_user_id[]" data-plan-doctor><option value="">Use default / clinic team</option><?php foreach($dentists as $doctor):?><option value="<?=(int)$doctor['id']?>"><?=e((string)$doctor['name'])?></option><?php endforeach;?></select></label><label>Date<input type="date" name="plan_date[]" data-plan-date min="<?=date('Y-m-d')?>" required></label><label>Available time<select name="plan_time[]" data-plan-time required><option value="">Choose service, Dentist, and date</option></select></label><button type="button" class="icon-button" data-plan-remove aria-label="Remove visit">×</button></div><?php endfor;?>
</div></div>
<template data-plan-template><div class="appointment-plan-row" data-plan-row><span class="plan-row-number">Visit</span><label>Visit label<input name="plan_label[]" placeholder="Treatment / review"></label><label>Service<select name="plan_service_id[]" data-plan-service required><option value="">Choose service</option><?php foreach($services as $service):?><option value="<?=(int)$service['id']?>"><?=e($service['name'].' · '.$service['duration_minutes'].' min')?></option><?php endforeach;?></select></label><label>Visit Dentist<select name="plan_dentist_user_id[]" data-plan-doctor><option value="">Use default / clinic team</option><?php foreach($dentists as $doctor):?><option value="<?=(int)$doctor['id']?>"><?=e((string)$doctor['name'])?></option><?php endforeach;?></select></label><label>Date<input type="date" name="plan_date[]" data-plan-date min="<?=date('Y-m-d')?>" required></label><label>Available time<select name="plan_time[]" data-plan-time required><option value="">Choose service, Dentist, and date</option></select></label><button type="button" class="icon-button" data-plan-remove aria-label="Remove visit">×</button></div></template>
<label class="wide">Reason for using different Dentists<textarea name="multi_provider_reason" placeholder="Specialist visit, surgery, coverage, or coordinated multidisciplinary care. Required only when the plan uses more than one Dentist."></textarea></label><div class="alert info wide">Each row may use a different qualified Dentist. Multidisciplinary plans keep the patient’s existing continuity Dentist and require a reason. The system validates every service segment, provider privilege, chair, and schedule before saving anything.</div><button class="btn primary wide" data-status-submit data-confirmed-label="Save confirmed visit plan" data-pending-label="Save pending visit plan">Save pending visit plan</button></form></dialog>

<dialog id="appointment-form"><form method="post" class="form-grid" data-slot-picker data-confirmation-form data-branch-id="<?=$branchId?>" data-keyboard-entry data-keyboard-save data-protect-unsaved><button type="button" class="dialog-close" data-close>×</button><h2 class="wide">Book appointment</h2><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="create">
<input type="hidden" name="waitlist_id" value="<?=e((string)($waitlistPrefill['id']??''))?>"><label class="wide">Patient<div class="remote-patient-picker" data-patient-picker data-endpoint="api/patient_search.php"><input type="search" data-patient-query placeholder="Search patient number, name, mobile, or email" autocomplete="off" value="<?=e($patientPrefill ? ((string)$patientPrefill['patient_no'].' · '.(string)$patientPrefill['last_name'].', '.(string)$patientPrefill['first_name']) : '')?>" required><input type="hidden" name="patient_id" data-patient-id value="<?=e((string)($patientPrefill['patient_id']??''))?>" data-primary-dentist-id="<?=e((string)($patientPrefill['primary_dentist_user_id']??''))?>" data-primary-dentist-name="<?=e((string)($patientPrefill['primary_dentist_name']??''))?>"><div class="remote-patient-selection" data-patient-selection <?=$patientPrefill?'':'hidden'?>><?=$waitlistPrefill?'Selected from appointment waitlist':($patientPrefill?'Selected patient':'')?></div><div class="remote-patient-results" data-patient-results hidden></div></div></label>
<div class="wide patient-upcoming-panel" data-patient-upcoming data-endpoint="api/patient_upcoming_appointments.php"><strong>Future visits for this patient</strong><p class="muted">Select a patient to review existing future appointments before adding another.</p></div>
<label>Primary service<select name="service_id" data-slot-service required><option value="">Choose service</option><?php foreach($services as $service):?><option value="<?=(int)$service['id']?>" data-provider-type="<?=e(service_provider_type($service))?>" data-requires-chair="<?=(int)($service['requires_chair']??1)?>" data-duration="<?=(int)($service['duration_minutes']??30)?>" <?=(int)($waitlistPrefill['service_id']??0)===(int)$service['id']?'selected':''?>><?=e($service['name'].' · '.$service['duration_minutes'].' min')?></option><?php endforeach;?></select></label>
<label class="wide">Additional services for this same visit<select name="additional_service_ids[]" data-slot-additional-services multiple size="<?=min(6,max(3,count($services)))?>"><?php foreach($services as $service):?><option value="<?=(int)$service['id']?>" data-provider-type="<?=e(service_provider_type($service))?>" data-requires-chair="<?=(int)($service['requires_chair']??1)?>" data-duration="<?=(int)($service['duration_minutes']??30)?>"><?=e($service['name'].' · '.$service['duration_minutes'].' min')?></option><?php endforeach;?></select><small>Add or remove services before saving. They run sequentially under one appointment and one queue number.</small></label><div class="wide alert info" data-service-bundle-summary>Select services to calculate the coordinated visit duration.</div>
<label>Dentist<select name="dentist_user_id" data-slot-doctor data-continuity-doctor><option value="">Choose dentist</option><?php foreach ($dentists as $doctor): ?><option value="<?=(int) $doctor['id']?>" <?=(int)($waitlistPrefill['dentist_user_id']??$waitlistPrefill['primary_dentist_user_id']??0)===(int)$doctor['id']?'selected':''?>><?=e((string) $doctor['name'])?></option><?php endforeach; ?></select></label>
<label>Date<input type="date" name="date" data-slot-date min="<?=date('Y-m-d')?>" value="<?=e((string)($waitlistPrefill['earliest_date']??''))?>" required></label><div class="available-dates-panel" data-available-dates>Choose a service and dentist to see available dates.</div><label>Available time<select name="time" data-slot-time required><option value="">Choose date, service, and dentist</option></select></label><label>Assigned chair<input type="hidden" name="inhouse_provider_user_id" data-inhouse-provider><input type="hidden" name="chair_id" data-slot-chair-id><input type="hidden" name="chair_name" data-slot-chair><input data-slot-chair-display value="Assigned from dentist" readonly></label><details class="wide booking-advanced-options"><summary><span>Advanced booking options</span><small>Confirmation evidence, booking source, and repeating visit series</small></summary><div class="form-grid booking-advanced-grid"><label>Status<select name="status" data-confirmation-status><option value="pending_confirmation">Pending confirmation</option><option value="confirmed">Confirmed</option><option value="requested">Requested</option></select></label><label>Booking source<select name="booking_source"><option value="phone">Phone</option><option value="in_person">In person</option><option value="email">Email</option><option value="patient_portal">Patient portal</option><option value="staff">Internal staff booking</option></select></label><div class="wide form-grid" data-confirmation-fields hidden><label>Agreement source<select name="confirmation_source"><option value="">Choose agreement evidence</option><option value="patient_phone">Patient by phone</option><option value="patient_in_person">Patient in person</option><option value="patient_email">Patient by email</option><option value="patient_portal">Patient portal</option><option value="guardian_phone">Guardian by phone</option><option value="guardian_in_person">Guardian in person</option></select></label><label class="wide">Confirmation note<textarea name="confirmation_note" maxlength="500" placeholder="Who agreed, how, and the outcome"></textarea></label></div><fieldset class="wide appointment-series-fields"><legend>Future appointment series</legend><div class="form-grid"><label>Number of visits<input type="number" name="repeat_count" min="1" max="12" value="1" data-series-count></label><label>Repeat every<input type="number" name="repeat_interval_weeks" min="1" max="52" value="1"><span>week(s)</span></label><label class="wide">Series / treatment plan name<input name="series_name" maxlength="190" placeholder="Example: Root canal treatment plan"></label></div><small>Use more than one visit only when the same service bundle and start time repeat. Every visit remains a separate appointment and receives its own queue visit and checkout when attended.</small></fieldset></div></details><label class="wide">Reason for visit<textarea name="reason_for_visit" required></textarea></label><label class="wide">Internal notes<textarea name="notes"></textarea></label>
<details class="wide booking-advanced-options booking-exception-options"><summary><span>Continuity or schedule exception</span><small>Open only for an approved dentist transfer or schedule bypass</small></summary><div class="form-grid booking-advanced-grid"><div class="wide continuity-care-note" data-continuity-note>Select a patient to use their established continuity dentist. A patient without one will be assigned to the dentist on this first confirmed booking.</div>
<?php if ($canTransferDentist): ?><label class="wide check-label"><input type="checkbox" name="change_continuity_dentist" value="1" data-continuity-transfer><span>Transfer this patient’s future care to the selected dentist</span></label><label class="wide" data-continuity-reason-wrap hidden>Care-transfer reason<textarea name="continuity_change_reason" data-continuity-reason placeholder="Patient request, specialty referral, dentist departure, or approved reassignment"></textarea></label><?php endif; ?>
<?php if ($canBypassSchedule): ?><label class="wide check-label"><input type="checkbox" name="schedule_bypass" value="1" data-bypass-toggle="#create-bypass-reason"><span>Use Schedule Bypass only for an authorized time outside published availability. It cannot double-book a patient, dentist, or chair.</span></label><div class="wide form-grid" id="create-bypass-reason" hidden><label>Manual time<input type="time" name="bypass_time"></label><label class="wide">Bypass reason<textarea name="schedule_bypass_reason" placeholder="Emergency, doctor-approved overbook, after-hours arrangement, or other authorized reason"></textarea></label></div><?php endif; ?>
</div></details><button class="btn primary wide" data-status-submit data-confirmed-label="Create confirmed appointment" data-pending-label="Create pending appointment">Create pending appointment</button></form></dialog>
<?php endif; ?>
<?php if ($canManage && (isset($_GET['new']) || $waitlistPrefill || $patientPrefill)): ?><span hidden data-auto-open-dialog="appointment-form"></span><?php endif; ?>
<?php render_footer(); ?>
