<?php

declare(strict_types=1);

require DENTAL_ROOT . '/app/bootstrap.php';
require_documentation_schema();
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/patient_visit_ledger.php';
require_once DENTAL_ROOT . '/app/record_release.php';

$portal = patient_portal_user();
if (!$portal) {
    header('Location: ' . dental_clean_url('patient-login.php?portal_reauth=1&reason=session'), true, 303);
    exit;
}

$pdo = db();
$portalAccountId = (int) ($portal['id'] ?? 0);
$portalPatients = array_values(array_filter(
    patient_portal_account_patients($portalAccountId),
    static fn(array $row): bool => !empty($row['can_view_records'])
));
if (!$portalPatients) {
    unset($_SESSION['patient_user'], $_SESSION['portal_patient_id']);
    http_response_code(403);
    exit('No active patient or dependent is authorized for this portal account. Contact the clinic to verify access.');
}
$explicitPatientId = (int) ($_GET['patient_id'] ?? $_POST['patient_id'] ?? 0);
$requestedPatientId = $explicitPatientId > 0
    ? $explicitPatientId
    : (int) ($_SESSION['portal_patient_id'] ?? $portal['patient_id'] ?? 0);
$portalAccess = patient_portal_access_for_patient($portalAccountId, $requestedPatientId);
if ($explicitPatientId > 0 && (!$portalAccess || empty($portalAccess['can_view_records']) || (int)($portalAccess['patient_id'] ?? 0) !== $explicitPatientId)) {
    audit('patient_portal_scope_denied', 'patient', $explicitPatientId, [
        'portal_account_id'=>$portalAccountId,
        'requested_patient_id'=>$explicitPatientId,
    ]);
    http_response_code(403);
    exit('This patient record is not authorized for your portal account.');
}
if (!$portalAccess || empty($portalAccess['can_view_records'])) {
    $portalAccess = patient_portal_access_for_patient($portalAccountId, (int)($portal['patient_id'] ?? 0));
}
if (!$portalAccess || empty($portalAccess['can_view_records'])) $portalAccess = $portalPatients[0];
$patientId = (int) $portalAccess['patient_id'];
$_SESSION['portal_patient_id'] = $patientId;
$_SESSION['patient_user']['patient_id'] = $patientId;
$canManageAppointments = !empty($portalAccess['can_manage_appointments']);
$canSubmitHealth = !empty($portalAccess['can_submit_health']);
$patient = require_patient($patientId);
$portalActionsReady = patient_portal_request_schema_ready();
$error = '';

function portal_appointment(PDO $pdo, int $patientId, int $appointmentId, bool $lock = false): array
{
    $stmt = $pdo->prepare('SELECT * FROM `' . table('appointments') . '` WHERE id=? AND patient_id=? LIMIT 1' . ($lock ? ' FOR UPDATE' : ''));
    $stmt->execute([$appointmentId, $patientId]);
    $row = $stmt->fetch();
    if (!$row) throw new RuntimeException('Appointment not found.');
    return $row;
}

function portal_followup(PDO $pdo, int $patientId, int $followUpId, bool $lock = false): array
{
    $stmt = $pdo->prepare('SELECT * FROM `' . table('follow_ups') . '` WHERE id=? AND patient_id=? LIMIT 1' . ($lock ? ' FOR UPDATE' : ''));
    $stmt->execute([$followUpId, $patientId]);
    $row = $stmt->fetch();
    if (!$row) throw new RuntimeException('Follow-up record not found.');
    return $row;
}

function portal_insert_request(PDO $pdo, array $data): int
{
    $stmt = $pdo->prepare(
        'INSERT INTO `' . table('patient_portal_requests') . '` '
        . '(patient_id,appointment_id,follow_up_id,request_type,status,requested_start_at,requested_end_at,preferred_date,preferred_time_from,preferred_time_to,reason,payload_json,staff_note,resolved_by,resolved_at,created_at,updated_at) '
        . 'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(),NOW())'
    );
    $stmt->execute([
        (int) $data['patient_id'],
        !empty($data['appointment_id']) ? (int) $data['appointment_id'] : null,
        !empty($data['follow_up_id']) ? (int) $data['follow_up_id'] : null,
        (string) $data['request_type'],
        (string) ($data['status'] ?? 'pending'),
        $data['requested_start_at'] ?? null,
        $data['requested_end_at'] ?? null,
        $data['preferred_date'] ?? null,
        $data['preferred_time_from'] ?? null,
        $data['preferred_time_to'] ?? null,
        $data['reason'] ?? null,
        isset($data['payload']) ? json_encode($data['payload'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
        $data['staff_note'] ?? null,
        $data['resolved_by'] ?? null,
        $data['resolved_at'] ?? null,
    ]);
    return (int) $pdo->lastInsertId();
}

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

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    try {
        verify_csrf();
        if (!$portalActionsReady) throw new RuntimeException('Patient self-service is temporarily unavailable while the clinic applies the latest system update.');
        $action = (string) ($_POST['action'] ?? '');
        if (in_array($action,['confirm_appointment','cancel_appointment','request_reschedule','book_followup'],true) && !$canManageAppointments) {
            throw new RuntimeException('This portal account has view-only access and cannot manage appointments for this patient.');
        }
        if ($action === 'medical_history_update' && !$canSubmitHealth) {
            throw new RuntimeException('This portal account is not authorized to submit dental-safety information changes for this patient.');
        }


        if ($action === 'portal_profile_update') {
            if (!patient_record_release_schema_ready()) throw new RuntimeException('Profile preferences are temporarily unavailable while the clinic applies the latest update.');
            $contactNumber = mb_substr(trim((string)($_POST['contact_number'] ?? '')),0,50);
            $address = mb_substr(trim((string)($_POST['address'] ?? '')),0,1000);
            $emergencyName = mb_substr(trim((string)($_POST['emergency_contact_name'] ?? '')),0,150);
            $emergencyPhone = mb_substr(trim((string)($_POST['emergency_contact_phone'] ?? '')),0,50);
            $channel = (string)($_POST['preferred_channel'] ?? 'email');
            if (!in_array($channel,['email','sms','portal'],true)) $channel='email';
            $appointmentNotifications = !empty($_POST['appointment_notifications']) ? 1 : 0;
            $recordNotifications = !empty($_POST['record_notifications']) ? 1 : 0;
            $pdo->beginTransaction();
            $pdo->prepare('UPDATE `' . table('patient_portal_accounts') . '` SET contact_number=?,address=?,emergency_contact_name=?,emergency_contact_phone=?,preferred_channel=?,appointment_notifications=?,record_notifications=?,profile_completed_at=COALESCE(profile_completed_at,NOW()),updated_at=NOW() WHERE id=?')
                ->execute([$contactNumber,$address,$emergencyName,$emergencyPhone,$channel,$appointmentNotifications,$recordNotifications,$portalAccountId]);
            if (!empty($portalAccess['is_self'])) {
                $emergency = trim($emergencyName . ($emergencyPhone!=='' ? ' · '.$emergencyPhone : ''));
                $pdo->prepare('UPDATE `' . table('patients') . '` SET mobile=?,address=?,emergency_contact=?,communication_preference=?,updated_at=NOW() WHERE id=?')
                    ->execute([$contactNumber,$address,$emergency,$channel,$patientId]);
            }
            audit('patient_portal_profile_updated','patient_portal_account',$portalAccountId,['patient_id'=>$patientId,'is_self'=>(int)($portalAccess['is_self']??0)]);
            $pdo->commit();
            flash('success','Your portal profile and communication preferences were updated.');
            header('Location: ' . dental_clean_url('patient-portal.php?patient_id='.$patientId.'&view=profile'), true, 303);
            exit;
        }

        if ($action === 'record_copy_request') {
            if (!patient_record_release_schema_ready()) throw new RuntimeException('Record-copy requests are temporarily unavailable while the clinic applies the latest update.');
            if (empty($portalAccess['can_view_records'])) throw new RuntimeException('This portal account is not authorized to request records for this patient.');
            $scope = (string)($_POST['record_scope'] ?? 'summary');
            if (!array_key_exists($scope,patient_record_scope_options())) throw new RuntimeException('Choose a valid dental-record copy type.');
            $dateFrom = trim((string)($_POST['date_from'] ?? ''));
            $dateTo = trim((string)($_POST['date_to'] ?? ''));
            if ($scope === 'date_range') {
                if ($dateFrom === '' || $dateTo === '') throw new RuntimeException('Choose both the beginning and ending dates.');
                if (strtotime($dateFrom) > strtotime($dateTo)) throw new RuntimeException('The beginning date cannot be after the ending date.');
            } else { $dateFrom=''; $dateTo=''; }
            $requestReason = mb_substr(trim((string)($_POST['reason'] ?? '')),0,500);
            if ($requestReason === '') throw new RuntimeException('Tell the clinic why you need this record copy.');
            $pdo->beginTransaction();
            $pending=$pdo->prepare('SELECT id FROM `' . table('patient_portal_requests') . '` WHERE patient_id=? AND request_type="record_copy" AND status IN ("pending_identity","pending_clinical","approved") ORDER BY id DESC LIMIT 1 FOR UPDATE');
            $pending->execute([$patientId]);
            if ($pending->fetchColumn() !== false) throw new RuntimeException('A record-copy request is already in progress for this patient.');
            $requestId=portal_insert_request($pdo,[
                'patient_id'=>$patientId,'request_type'=>'record_copy','status'=>'pending_identity','reason'=>$requestReason,
                'payload'=>['scope'=>$scope,'date_from'=>$dateFrom?:null,'date_to'=>$dateTo?:null,'portal_account_id'=>$portalAccountId],
            ]);
            $request=['id'=>$requestId,'patient_id'=>$patientId,'patient_branch_id'=>(int)($patient['branch_id']??0),'payload_json'=>json_encode(['scope'=>$scope,'date_from'=>$dateFrom?:null,'date_to'=>$dateTo?:null])];
            patient_record_release_create_for_request($pdo,$request);
            audit('patient_record_copy_requested','patient_portal_request',$requestId,['patient_id'=>$patientId,'scope'=>$scope,'date_from'=>$dateFrom?:null,'date_to'=>$dateTo?:null]);
            $pdo->commit();
            flash('success','Your dental-record copy request was submitted for identity review and approval.');
            header('Location: ' . dental_clean_url('patient-portal.php?patient_id='.$patientId.'&view=records'), true, 303);
            exit;
        }

        if ($action === 'confirm_appointment') {
            $pdo->beginTransaction();
            $appointment = portal_appointment($pdo, $patientId, (int) ($_POST['appointment_id'] ?? 0), true);
            if (strtotime((string) $appointment['start_at']) < time()) throw new RuntimeException('Past appointments cannot be confirmed.');
            if (integrity_active_queue_for_appointment($pdo,(int)$appointment['id'],true)) {
                throw new RuntimeException('This appointment already has an active clinic queue visit and can no longer be confirmed through the portal.');
            }
            if ((string) $appointment['status'] !== 'confirmed' || (string) $appointment['confirmation_status'] !== 'confirmed') {
                integrity_assert_appointment_transition((string)$appointment['status'],'confirmed',(string)$appointment['start_at']);
                $update=$pdo->prepare('UPDATE `' . table('appointments') . '` SET status="confirmed",confirmation_status="confirmed",updated_at=NOW() WHERE id=? AND patient_id=? AND status=?');
                $update->execute([(int) $appointment['id'],$patientId,(string)$appointment['status']]);
                if($update->rowCount()!==1)throw new RuntimeException('The appointment changed while confirmation was being completed. Please reopen the appointment.');
                $pdo->prepare('INSERT INTO `' . table('appointment_status_history') . '` (appointment_id,previous_status,new_status,reason,changed_by,created_at) VALUES (?,?,"confirmed","Confirmed through patient portal",NULL,NOW())')->execute([(int) $appointment['id'],(string) $appointment['status']]);
                portal_insert_request($pdo,[
                    'patient_id'=>$patientId,'appointment_id'=>(int)$appointment['id'],'request_type'=>'appointment_confirm','status'=>'completed',
                    'reason'=>'Patient confirmed through the secure portal.','payload'=>['portal_account_id'=>$portalAccountId],
                    'staff_note'=>'Completed automatically by the patient portal.','resolved_at'=>date('Y-m-d H:i:s'),
                ]);
            }
            $pdo->commit();
            workflow_event($patientId,'appointment_confirmed','Appointment confirmed',['appointment_id'=>(int)$appointment['id']],'Confirmed through the patient portal.');
            audit('patient_portal_appointment_confirmed','appointment',(int)$appointment['id'],['patient_id'=>$patientId,'portal_account_id'=>$portalAccountId,'previous_status'=>(string)$appointment['status']]);
            flash('success','Your appointment is confirmed.');
        } elseif ($action === 'cancel_appointment') {
            $reason = trim((string) ($_POST['reason'] ?? ''));
            if ($reason === '') throw new RuntimeException('Tell the clinic why you need to cancel.');
            $pdo->beginTransaction();
            $appointment = portal_appointment($pdo, $patientId, (int) ($_POST['appointment_id'] ?? 0), true);
            if (strtotime((string) $appointment['start_at']) < time()) throw new RuntimeException('Past appointments cannot be cancelled through the portal.');
            integrity_assert_appointment_transition((string)$appointment['status'],'cancelled',(string)$appointment['start_at']);
            if (integrity_active_queue_for_appointment($pdo,(int)$appointment['id'],true)) {
                throw new RuntimeException('This appointment is already active in the clinic queue. Contact the clinic so staff can safely close the visit.');
            }
            $update=$pdo->prepare('UPDATE `' . table('appointments') . '` SET status="cancelled",confirmation_status="cancelled",updated_at=NOW() WHERE id=? AND patient_id=? AND status=?');
            $update->execute([(int)$appointment['id'],$patientId,(string)$appointment['status']]);
            if($update->rowCount()!==1)throw new RuntimeException('The appointment changed while cancellation was being completed. Please reopen the appointment.');
            $pdo->prepare('INSERT INTO `' . table('appointment_status_history') . '` (appointment_id,previous_status,new_status,reason,changed_by,created_at) VALUES (?,? ,"cancelled",?,NULL,NOW())')->execute([(int)$appointment['id'],(string)$appointment['status'],'Patient portal: '.$reason]);
            portal_insert_request($pdo,[
                'patient_id'=>$patientId,'appointment_id'=>(int)$appointment['id'],'request_type'=>'appointment_cancel','status'=>'completed',
                'reason'=>$reason,'payload'=>['portal_account_id'=>$portalAccountId],
                'staff_note'=>'Cancellation completed automatically by the patient portal.','resolved_at'=>date('Y-m-d H:i:s'),
            ]);
            $pdo->commit();
            workflow_event($patientId,'appointment_cancelled','Appointment cancelled',['appointment_id'=>(int)$appointment['id']],$reason.' · Patient portal');
            audit('patient_portal_appointment_cancelled','appointment',(int)$appointment['id'],['patient_id'=>$patientId,'reason'=>$reason,'portal_account_id'=>$portalAccountId],['status'=>$appointment['status']]);
            flash('success','Your appointment was cancelled. The released time is now available to the clinic.');
        } elseif ($action === 'request_reschedule') {
            $date = trim((string) ($_POST['preferred_date'] ?? ''));
            $from = substr(trim((string) ($_POST['preferred_time_from'] ?? '')),0,5);
            $to = substr(trim((string) ($_POST['preferred_time_to'] ?? '')),0,5);
            $reason = trim((string) ($_POST['reason'] ?? ''));
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date) || strtotime($date) < strtotime('today')) throw new RuntimeException('Choose a valid preferred date.');
            if ($from !== '' && !preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/',$from)) throw new RuntimeException('Choose a valid preferred starting time.');
            if ($to !== '' && !preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/',$to)) throw new RuntimeException('Choose a valid preferred ending time.');
            if ($from !== '' && $to !== '' && $to <= $from) throw new RuntimeException('The preferred ending time must be later than the starting time.');
            if ($reason === '') throw new RuntimeException('Add a short reason or scheduling note.');
            $pdo->beginTransaction();
            $appointment = portal_appointment($pdo, $patientId, (int) ($_POST['appointment_id'] ?? 0), true);
            if (strtotime((string) $appointment['start_at']) < time() || !in_array((string)$appointment['status'],['requested','pending_confirmation','confirmed'],true)) {
                throw new RuntimeException('This appointment can no longer be rescheduled through the portal.');
            }
            if (integrity_active_queue_for_appointment($pdo,(int)$appointment['id'],true)) throw new RuntimeException('This appointment is already active in the clinic queue and cannot be rescheduled through the portal.');
            $pending=$pdo->prepare('SELECT id FROM `' . table('patient_portal_requests') . '` WHERE patient_id=? AND appointment_id=? AND request_type="appointment_reschedule" AND status="pending" FOR UPDATE');
            $pending->execute([$patientId,(int)$appointment['id']]);
            if($pending->fetchColumn())throw new RuntimeException('A reschedule request is already waiting for clinic review.');
            $requestId = portal_insert_request($pdo,[
                'patient_id'=>$patientId,'appointment_id'=>(int)$appointment['id'],'request_type'=>'appointment_reschedule','status'=>'pending',
                'preferred_date'=>$date,'preferred_time_from'=>$from!==''?$from.':00':null,'preferred_time_to'=>$to!==''?$to.':00':null,
                'reason'=>$reason,'payload'=>['current_start_at'=>$appointment['start_at'],'service_name'=>$appointment['service_name'],'portal_account_id'=>$portalAccountId],
            ]);
            $pdo->commit();
            workflow_event($patientId,'portal_reschedule_requested','Patient requested reschedule',['appointment_id'=>(int)$appointment['id']],$date.($from!==''?' · '.$from:'').' · '.$reason);
            audit('patient_portal_reschedule_requested','patient_portal_request',$requestId,['patient_id'=>$patientId,'appointment_id'=>(int)$appointment['id'],'preferred_date'=>$date,'preferred_time_from'=>$from,'preferred_time_to'=>$to]);
            flash('success','Your reschedule request was sent to the clinic. Your current appointment remains reserved until staff confirms a new time.');
        } elseif ($action === 'medical_history_update') {
            $payload = [
                'allergies'=>trim((string)($_POST['allergies']??'')),
                'medications'=>trim((string)($_POST['medications']??'')),
                'conditions'=>trim((string)($_POST['conditions']??'')),
                'dental_history'=>trim((string)($_POST['dental_history']??'')),
                'portal_account_id'=>$portalAccountId,
            ];
            $reason = trim((string) ($_POST['reason'] ?? ''));
            if ($reason === '') throw new RuntimeException('Describe what changed or what the clinic should verify.');
            $pdo->beginTransaction();
            $patientLock=$pdo->prepare('SELECT id FROM `' . table('patients') . '` WHERE id=? FOR UPDATE');
            $patientLock->execute([$patientId]);
            if(!$patientLock->fetchColumn())throw new RuntimeException('Patient record not found.');
            $pending=$pdo->prepare('SELECT id FROM `' . table('patient_portal_requests') . '` WHERE patient_id=? AND request_type="medical_history_update" AND status="pending" FOR UPDATE');
            $pending->execute([$patientId]);
            if($pending->fetchColumn())throw new RuntimeException('A dental-safety information update is already waiting for clinic review.');
            $requestId = portal_insert_request($pdo,[
                'patient_id'=>$patientId,'request_type'=>'medical_history_update','status'=>'pending','reason'=>$reason,'payload'=>$payload,
            ]);
            $pdo->commit();
            workflow_event($patientId,'medical_update_requested','Dental-safety information update requested',[], $reason);
            audit('patient_portal_medical_update_requested','patient_portal_request',$requestId,['patient_id'=>$patientId,'fields'=>array_keys(array_filter($payload,static fn($value)=>$value!==''))]);
            flash('success','Your dental-safety information update was submitted for dentist verification. The existing verified record remains unchanged until a dentist approves it.');
        } elseif ($action === 'book_followup') {
            $followUp = portal_followup($pdo,$patientId,(int)($_POST['follow_up_id']??0));
            if (!in_array((string)$followUp['status'],['pending','deferred'],true) || !empty($followUp['appointment_id'])) throw new RuntimeException('This follow-up is no longer available for booking.');
            $branchId = (int)($followUp['branch_id']??0);
            if ($branchId < 1) $branchId = (int)(public_request_branch_id()??0);
            if ($branchId < 1) throw new RuntimeException('The clinic branch could not be determined. Contact the clinic.');
            $serviceId = (int)($_POST['service_id']??$followUp['service_id']??0);
            if ((int)($followUp['service_id']??0)>0 && $serviceId !== (int)$followUp['service_id']) throw new RuntimeException('Choose the service assigned to this follow-up.');
            $service = service_definition($serviceId,$branchId);
            if (!$service || empty($service['is_active'])) throw new RuntimeException('The selected follow-up service is unavailable.');
            $date = trim((string)($_POST['date']??''));
            $time = substr(trim((string)($_POST['time']??'')),0,5);
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date) || !preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/',$time)) throw new RuntimeException('Choose an available date and time.');
            $providerType = service_provider_type($service);
            $doctorId = (int)($_POST['dentist_user_id']??0);
            $doctor = null;
            $inhouseProviderId = 0;
            $providerName = 'In-house clinic team';
            $chairId = 0;
            $chairName = '';
            if ($providerType === 'dentist') {
                $continuity = patient_primary_dentist($patientId);
                $requiredDoctorId = (int)($followUp['dentist_user_id']??0) ?: (int)($continuity['id']??0);
                if ($requiredDoctorId > 0) $doctorId = $requiredDoctorId;
                $doctor = portal_find_doctor(active_dentists($branchId),$doctorId);
                if (!$doctor) throw new RuntimeException('Choose an available dentist for this follow-up.');
                if (!validate_service_slot($branchId,$service,$date,$time,$doctorId)) throw new RuntimeException('That dentist-and-chair time is no longer available. Choose another listed time.');
                $binding = doctor_chair_binding($doctorId,$branchId);
                if (empty($binding['valid'])) throw new RuntimeException((string)($binding['issue']??'The dentist has no usable chair assignment.'));
                $providerName = (string)($doctor['name']??$doctor['display_name']??'Assigned dentist');
                $chairId = (int)($binding['chair_id']??0);
                $chairName = (string)($binding['chair_name']??'');
            } else {
                if (!validate_service_slot($branchId,$service,$date,$time,0)) throw new RuntimeException('That in-house time is no longer available. Choose another listed time.');
                $assignment = inhouse_slot_assignment($branchId,$date,$time,$service);
                if (empty($assignment['valid'])) throw new RuntimeException((string)($assignment['issue']??'No in-house resource is available.'));
                $inhouseProviderId = (int)$assignment['provider_id'];
                $providerName = (string)$assignment['provider_name'];
                $chairId = (int)$assignment['chair_id'];
                $chairName = (string)$assignment['chair_name'];
            }
            $start = $date.' '.$time.':00';
            $end = date('Y-m-d H:i:s',strtotime($start.' +'.max(5,(int)$service['duration_minutes']).' minutes'));
            $conflicts = appointment_resource_conflicts($branchId,$patientId,$doctorId,$chairName,$start,$end,0,(int)$service['preparation_minutes'],(int)$service['cleanup_minutes'],$inhouseProviderId,$chairId);
            if (!empty($conflicts['has_conflict'])) throw new RuntimeException(appointment_conflict_message($conflicts));

            $pdo->beginTransaction();
            $followUp = portal_followup($pdo,$patientId,(int)($_POST['follow_up_id']??0),true);
            if (!in_array((string)$followUp['status'],['pending','deferred'],true) || !empty($followUp['appointment_id'])) {
                throw new RuntimeException('This follow-up was already scheduled or changed by another request.');
            }
            integrity_lock_appointment_resources($pdo,$branchId,$patientId,$doctorId,$chairId,$inhouseProviderId,$chairName,$start,$end,0);
            $conflicts = appointment_resource_conflicts($branchId,$patientId,$doctorId,$chairName,$start,$end,0,(int)$service['preparation_minutes'],(int)$service['cleanup_minutes'],$inhouseProviderId,$chairId);
            if (!empty($conflicts['has_conflict'])) throw new RuntimeException(appointment_conflict_message($conflicts));
            $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,follow_up_id,reason_for_visit,confirmation_status,notes,created_by,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,"confirmed","patient_portal_followup",?,?,"confirmed",?,NULL,NOW())');
            $stmt->execute([$branchId,$patientId,$serviceId,$providerType==='dentist'?$doctorId:null,$providerType==='dentist'?$providerName:null,$inhouseProviderId?:null,$providerType==='inhouse'?$providerName:null,$service['name'],$providerType,$start,$end,$chairId?:null,$chairName?:null,(int)$followUp['id'],(string)$followUp['purpose'],'Booked by patient through the secure portal.']);
            $appointmentId = (int)$pdo->lastInsertId();
            $pdo->prepare('INSERT INTO `' . table('appointment_status_history') . '` (appointment_id,previous_status,new_status,reason,changed_by,created_at) VALUES (?,NULL,"confirmed","Booked from patient portal follow-up",NULL,NOW())')->execute([$appointmentId]);
            $followUpdate=$pdo->prepare('UPDATE `' . table('follow_ups') . '` SET status="scheduled",appointment_id=?,scheduled_at=NOW(),scheduled_by=NULL WHERE id=? AND patient_id=? AND status IN ("pending","deferred") AND appointment_id IS NULL');
            $followUpdate->execute([$appointmentId,(int)$followUp['id'],$patientId]);
            if($followUpdate->rowCount()!==1)throw new RuntimeException('This follow-up was scheduled by another request. No duplicate appointment was saved.');
            portal_insert_request($pdo,[
                'patient_id'=>$patientId,'appointment_id'=>$appointmentId,'follow_up_id'=>(int)$followUp['id'],'request_type'=>'followup_booking','status'=>'completed',
                'requested_start_at'=>$start,'requested_end_at'=>$end,'reason'=>'Patient booked the recommended follow-up through the secure portal.',
                'payload'=>['service_id'=>$serviceId,'provider_type'=>$providerType,'provider_name'=>$providerName,'chair_name'=>$chairName,'portal_account_id'=>$portalAccountId],
                'staff_note'=>'Completed automatically after live availability and resource validation.','resolved_at'=>date('Y-m-d H:i:s'),
            ]);
            $pdo->commit();

            $delivery = null;
            $confirmationEmail=(string)($portal['email']??$patient['email']??'');
            if (filter_var($confirmationEmail,FILTER_VALIDATE_EMAIL)) {
                try {
                    $delivery = queue_template_email('appointment_confirmed',$confirmationEmail,[
                        'recipient_name'=>(string)$patient['first_name'],
                        'appointment_datetime'=>date('F j, Y g:i A',strtotime($start)),
                        'service_name'=>(string)$service['name'],
                        'provider_name'=>$providerName,
                        'branch_name'=>(string)(public_clinic_profile()['name']??'Dental clinic'),
                        'calendar_url'=>app_absolute_url(appointment_calendar_relative_url(['id'=>$appointmentId,'patient_id'=>$patientId,'start_at'=>$start],true)),
                    ],['type'=>'appointment_confirmed','appointment_id'=>$appointmentId],true);
                } catch (Throwable $mailError) {
                    error_log('Portal follow-up confirmation could not be queued: '.$mailError->getMessage());
                }
            }
            workflow_event($patientId,'followup_scheduled','Follow-up scheduled',['appointment_id'=>$appointmentId,'follow_up_id'=>(int)$followUp['id']],date('M j, Y g:i A',strtotime($start)).' · '.$providerName.' · Patient portal');
            audit('patient_portal_followup_booked','appointment',$appointmentId,['patient_id'=>$patientId,'follow_up_id'=>(int)$followUp['id'],'start_at'=>$start,'provider'=>$providerName,'delivery_status'=>$delivery['status']??'not_available']);
            flash('success','Your follow-up is booked for '.date('F j, Y g:i A',strtotime($start)).'.');
        } else {
            throw new RuntimeException('Unsupported patient portal action.');
        }

        header('Location: ' . dental_clean_url('patient-portal.php?patient_id=' . $patientId . '&view=' . rawurlencode((string)($_POST['return_view']??'dashboard'))), true, 303);
        exit;
    } catch (Throwable $exception) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        $error = $exception->getMessage();
    }
}

$view = (string)($_GET['view']??'dashboard');
if ($view === 'overview') $view='dashboard';
if ($view === 'visits') $view='records';
$allowedViews = ['dashboard','profile','records','appointments','plans','prescriptions','followups','health','documents','billing','requests'];
if (!in_array($view,$allowedViews,true)) $view='dashboard';

$appointmentPerPage = 10;
$appointmentPage = portal_positive_page($_GET['appointment_page'] ?? 1);
$appointmentTotalStmt = $pdo->prepare('SELECT COUNT(*) FROM `' . table('appointments') . '` WHERE patient_id=?');
$appointmentTotalStmt->execute([$patientId]);
$appointmentTotal = (int)$appointmentTotalStmt->fetchColumn();
$appointmentPage = min($appointmentPage, portal_page_count($appointmentTotal,$appointmentPerPage));
$appointmentOffset = portal_page_offset($appointmentPage,$appointmentPerPage);
$appointmentsStmt = $pdo->prepare('SELECT * FROM `' . table('appointments') . '` WHERE patient_id=? ORDER BY start_at DESC,id DESC LIMIT ' . $appointmentPerPage . ' OFFSET ' . $appointmentOffset);
$appointmentsStmt->execute([$patientId]);
$appointments = $appointmentsStmt->fetchAll();
$upcomingStmt = $pdo->prepare('SELECT * FROM `' . table('appointments') . '` WHERE patient_id=? AND start_at>=NOW() AND status NOT IN ("cancelled","no_show","completed") ORDER BY start_at,id LIMIT 20');
$upcomingStmt->execute([$patientId]);
$upcomingAppointments = $upcomingStmt->fetchAll();
$appointmentServicesByAppointment = portal_appointment_services($pdo,array_merge(array_column($appointments,'id'),array_column($upcomingAppointments,'id')));

$plansStmt = $pdo->prepare('SELECT * FROM `' . table('treatment_plans') . '` WHERE patient_id=? AND status IN ("presented","partially_accepted","accepted","scheduled","in_progress","completed") ORDER BY created_at DESC');
$plansStmt->execute([$patientId]);
$plans = $plansStmt->fetchAll();
$planItemsByPlan=[];
if($plans){
    $planIds=array_map(static fn(array $row):int=>(int)$row['id'],$plans);
    $placeholders=implode(',',array_fill(0,count($planIds),'?'));
    $extendedPlanItems=schema_column_exists(table('treatment_plan_items'),'consent_status')
        && schema_table_exists(table('odontogram_entries'))
        && schema_table_exists(table('dental_condition_catalog'));
    $itemFields=$extendedPlanItems?'i.*,o.condition_code,c.name finding_name':'i.*,NULL odontogram_entry_id,NULL consent_status,NULL prognosis,NULL clinical_rationale,NULL condition_code,NULL finding_name';
    $planItemStmt=$pdo->prepare('SELECT '.$itemFields.' FROM `' . table('treatment_plan_items') . '` i '.($extendedPlanItems?'LEFT JOIN `' . table('odontogram_entries') . '` o ON o.id=i.odontogram_entry_id LEFT JOIN `' . table('dental_condition_catalog') . '` c ON c.code=o.condition_code ':'').'WHERE i.treatment_plan_id IN ('.$placeholders.') ORDER BY i.treatment_plan_id,i.sequence_no,i.id');
    $planItemStmt->execute($planIds);
    foreach($planItemStmt as $item)$planItemsByPlan[(int)$item['treatment_plan_id']][]=$item;
}

$invoicePerPage = 10;
$invoicePage = portal_positive_page($_GET['invoice_page'] ?? 1);
$invoiceCountStmt = $pdo->prepare('SELECT COUNT(*) FROM `' . table('invoices') . '` WHERE patient_id=?');
$invoiceCountStmt->execute([$patientId]);
$invoiceTotal = (int)$invoiceCountStmt->fetchColumn();
$invoicePage = min($invoicePage, portal_page_count($invoiceTotal,$invoicePerPage));
$invoiceOffset = portal_page_offset($invoicePage,$invoicePerPage);
$invoiceStmt = $pdo->prepare('SELECT * FROM `' . table('invoices') . '` WHERE patient_id=? ORDER BY created_at DESC,id DESC LIMIT ' . $invoicePerPage . ' OFFSET ' . $invoiceOffset);
$invoiceStmt->execute([$patientId]);
$invoices = $invoiceStmt->fetchAll();
$statementTotals = ['charges'=>0.0,'paid'=>0.0,'returned'=>0.0,'balance'=>0.0];
$totalsStmt=$pdo->prepare('SELECT COALESCE(SUM(amount),0) charges,COALESCE(SUM(paid_amount),0) paid,COALESCE(SUM(GREATEST(amount-paid_amount,0)),0) balance FROM `' . table('invoices') . '` WHERE patient_id=? AND status<>"voided"');
$totalsStmt->execute([$patientId]);
$totalsRow=$totalsStmt->fetch()?:[];
$statementTotals['charges']=(float)($totalsRow['charges']??0);
$statementTotals['paid']=(float)($totalsRow['paid']??0);
$statementTotals['balance']=(float)($totalsRow['balance']??0);
if(schema_table_exists(table('payment_adjustments'))){
    $returnsTotalStmt=$pdo->prepare('SELECT COALESCE(SUM(pa.amount),0) FROM `' . table('payment_adjustments') . '` pa JOIN `' . table('invoices') . '` i ON i.id=pa.invoice_id WHERE i.patient_id=? AND pa.status="processed"');
    $returnsTotalStmt->execute([$patientId]);
    $statementTotals['returned']=(float)$returnsTotalStmt->fetchColumn();
}
$balance=$statementTotals['balance'];
$invoiceDetailMaps=portal_invoice_detail_maps($pdo,array_column($invoices,'id'));
$portalPaymentsByInvoice=$invoiceDetailMaps['payments'];
$portalReturnsByInvoice=$invoiceDetailMaps['returns'];
$portalInvoiceItemsByInvoice=$invoiceDetailMaps['items'];
$portalPaymentAllocations=$invoiceDetailMaps['allocations'];

$rxStmt = $pdo->prepare('SELECT r.*,i.medication_name,i.strength,i.dosage,i.frequency,i.instructions FROM `' . table('prescriptions') . '` r LEFT JOIN `' . table('prescription_items') . '` i ON i.prescription_id=r.id WHERE r.patient_id=? AND r.status="issued" ORDER BY r.issued_at DESC');
$rxStmt->execute([$patientId]); $prescriptions=$rxStmt->fetchAll();
$docsStmt = $pdo->prepare('SELECT * FROM `' . table('documents') . '` WHERE patient_id=? AND access_level="patient" ORDER BY created_at DESC');
$docsStmt->execute([$patientId]); $documents=$docsStmt->fetchAll();
$followStmt = $pdo->prepare('SELECT f.*,s.name service_name,s.provider_type,u.name dentist_name FROM `' . table('follow_ups') . '` f LEFT JOIN `' . table('service_catalog') . '` s ON s.id=f.service_id LEFT JOIN `' . table('users') . '` u ON u.id=f.dentist_user_id WHERE f.patient_id=? AND f.status IN ("pending","deferred","scheduled") ORDER BY f.recommended_date,f.id');
$followStmt->execute([$patientId]); $followUps=$followStmt->fetchAll();
$medicalStmt = $pdo->prepare('SELECT * FROM `' . table('medical_histories') . '` WHERE patient_id=?' . (integrity_medical_columns_ready()?' AND verification_status="verified"':'') . ' ORDER BY created_at DESC LIMIT 1');
$medicalStmt->execute([$patientId]); $latestMedical=$medicalStmt->fetch()?:[];
$portalRequests=[];
if($portalActionsReady){$requestStmt=$pdo->prepare('SELECT * FROM `' . table('patient_portal_requests') . '` WHERE patient_id=? ORDER BY created_at DESC LIMIT 50');$requestStmt->execute([$patientId]);$portalRequests=$requestStmt->fetchAll();}
$portalAccountStmt=$pdo->prepare('SELECT * FROM `' . table('patient_portal_accounts') . '` WHERE id=? LIMIT 1');
$portalAccountStmt->execute([$portalAccountId]);
$portalAccount=$portalAccountStmt->fetch()?:[];
$recordReleases=patient_record_release_schema_ready()?patient_record_release_patient_rows($pdo,$patientId):[];
$servicesByBranch=[];
foreach(branch_options() as $branch){$servicesByBranch[(int)$branch['id']]=public_services_catalog((int)$branch['id']);}

$visitLedger=['total'=>0,'page'=>1,'per_page'=>10,'rows'=>[],'details'=>[]];
if($view==='records'){
    try{$visitLedger=portal_visit_ledger($pdo,$patientId,portal_positive_page($_GET['visit_page']??1),10);}catch(Throwable $ledgerError){
        error_log('Patient portal visit ledger fallback: '.$ledgerError->getMessage());
        $error=$error!==''?$error:'The lifetime visit ledger is temporarily unavailable. Appointments and statements remain accessible.';
    }
}

render_header('Patient Portal',true);
render_public_nav();
?>
<main class="portal-main portal-self-service">
<header class="portal-header"><div><h1>Hello, <?=e($patient['preferred_name']?:$patient['first_name'])?></h1><p><?=e($patient['patient_no'])?> · Secure dental records, appointments, documents, requests, and account information</p></div><a class="btn ghost" href="patient-logout">Sign out</a></header>
<?php if(count($portalPatients)>1):?><form method="get" id="dependent-switcher" class="card portal-dependent-switcher"><input type="hidden" name="view" value="<?=e($view)?>"><label>Viewing patient or dependent<select name="patient_id" onchange="this.form.submit()"><?php foreach($portalPatients as $portalPatient):?><option value="<?=(int)$portalPatient['patient_id']?>" <?=(int)$portalPatient['patient_id']===$patientId?'selected':''?>><?=e(trim((string)$portalPatient['first_name'].' '.(string)$portalPatient['last_name']).' · '.(string)$portalPatient['patient_no'])?></option><?php endforeach;?></select></label><noscript><button class="btn ghost">Open record</button></noscript></form><?php endif;?>
<?php if($error!==''):?><div class="alert error"><?=e($error)?></div><?php endif;?>
<?php if(!$portalActionsReady):?><div class="alert warning"><strong>Self-service maintenance:</strong> You can review existing information, but appointment actions and dental-safety updates will return after the clinic applies Version 0.5.8.6.</div><?php endif;?>
<?php if(!$canManageAppointments&&!$canSubmitHealth):?><div class="alert info"><strong>View-only dependent access:</strong> Appointment changes and health updates require verified consent authority.</div><?php endif;?>
<div class="patient-portal-shell">
<aside class="patient-portal-sidebar" aria-label="Patient portal sections">
<div class="portal-sidebar-patient"><strong><?=e(trim((string)$patient['first_name'].' '.(string)$patient['last_name']))?></strong><small><?=e((string)$patient['patient_no'])?></small></div>
<?php $portalNav=['dashboard'=>'Dashboard','profile'=>'My Profile','records'=>'Dental Records','plans'=>'Treatment Plans','prescriptions'=>'Prescriptions','documents'=>'Documents','billing'=>'Billing & Receipts','requests'=>'Requests & Messages'];foreach($portalNav as $key=>$label):?><a href="patient-portal?patient_id=<?=$patientId?>&amp;view=<?=e($key)?>" class="<?=$view===$key?'active':''?>"><span><?=e($label)?></span><?php if($key==='dashboard'&&count($upcomingAppointments)>0):?><b><?=count($upcomingAppointments)?></b><?php endif;?></a><?php endforeach;?>
<?php if(count($portalPatients)>1):?><a href="#dependent-switcher"><span>Dependents</span><b><?=count($portalPatients)?></b></a><?php endif;?>
<a href="patient-logout"><span>Sign Out</span></a>
</aside><section class="patient-portal-content">
<?php if($view==='dashboard'):?><section class="portal-summary"><?php stat_card('Upcoming',count($upcomingAppointments),'Appointments');stat_card('Treatment plans',count($plans),'Shared by clinic');stat_card('Balance','₱'.number_format($balance,2),'Outstanding');stat_card('Follow-ups',count(array_filter($followUps,static fn($r)=>in_array($r['status'],['pending','deferred'],true))),'Available to book');?></section><?php endif;?>

<?php if($view==='dashboard'):?>
<div class="record-grid portal-record-grid">
<section class="card"><div class="card-head"><div><h2>Next appointments</h2><p>Confirm, cancel, or request a different schedule.</p></div><a href="patient-portal?patient_id=<?=$patientId?>&amp;view=appointments">View all</a></div>
<?php foreach(array_slice($upcomingAppointments,0,4) as $row):?><article class="timeline-item"><h4><?=e($row['service_name'])?></h4><p><?=e(date('M j, Y g:i A',strtotime((string)$row['start_at'])))?> · <?=e(appointment_provider_display($row))?></p><span class="badge"><?=e(module_status((string)$row['status']))?></span><a class="portal-calendar-link" href="<?=e(appointment_calendar_relative_url($row))?>">Add to calendar</a></article><?php endforeach;?><?php if(!$upcomingAppointments):?><p class="muted">No upcoming appointment is scheduled.</p><?php endif;?></section>
<section class="card"><div class="card-head"><div><h2>Follow-up care</h2><p>Book a due follow-up using live clinic availability.</p></div><a href="patient-portal?patient_id=<?=$patientId?>&amp;view=followups">Open follow-ups</a></div><?php foreach(array_slice($followUps,0,4) as $row):?><article class="timeline-item"><h4><?=e((string)$row['purpose'])?></h4><p>Recommended <?=e(date('M j, Y',strtotime((string)$row['recommended_date'])))?><?=!empty($row['dentist_name'])?' · '.e((string)$row['dentist_name']):''?></p><span class="badge"><?=e(module_status((string)$row['status']))?></span></article><?php endforeach;?><?php if(!$followUps):?><p class="muted">No follow-up is pending.</p><?php endif;?></section>
<section class="card"><div class="card-head"><h2>Prescriptions</h2></div><?php foreach(array_slice($prescriptions,0,5) as $row):?><article class="timeline-item"><h4><?=e(trim((string)$row['medication_name'].' '.(string)$row['strength']))?></h4><p><?=e(trim((string)$row['dosage'].' · '.(string)$row['frequency']))?></p><small><?=e((string)$row['instructions'])?></small></article><?php endforeach;?><?php if(!$prescriptions):?><p class="muted">No active prescription is shared.</p><?php endif;?></section>
<section class="card"><div class="card-head"><h2>Recent treatment plans</h2><a href="patient-portal?patient_id=<?=$patientId?>&amp;view=plans">View details</a></div><?php foreach(array_slice($plans,0,5) as $row):?><article class="timeline-item"><h4><?=e((string)$row['title'])?></h4><p><?=e((string)$row['phase'])?> · Estimate ₱<?=number_format((float)$row['estimate_total'],2)?></p><span class="badge"><?=e(module_status((string)$row['status']))?></span></article><?php endforeach;?><?php if(!$plans):?><p class="muted">No treatment plan is currently shared.</p><?php endif;?></section>
</div>

<?php elseif($view==='profile'):?>
<section class="card"><div class="card-head"><div><h2>My Profile</h2><p>Your official patient identity is read-only. Update contact details and communication preferences here.</p></div></div>
<form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="patient_id" value="<?=$patientId?>"><input type="hidden" name="action" value="portal_profile_update"><input type="hidden" name="return_view" value="profile">
<label>Official patient name<input value="<?=e(trim((string)$patient['first_name'].' '.(string)$patient['middle_name'].' '.(string)$patient['last_name']))?>" readonly></label><label>Login email<input value="<?=e((string)($portalAccount['email']??''))?>" readonly></label>
<label>Contact number<input name="contact_number" value="<?=e((string)($portalAccount['contact_number']??$patient['mobile']??''))?>"></label><label>Preferred communication<select name="preferred_channel"><?php foreach(['email'=>'Email','sms'=>'SMS','portal'=>'Portal only'] as $value=>$label):?><option value="<?=e($value)?>" <?=($portalAccount['preferred_channel']??'email')===$value?'selected':''?>><?=e($label)?></option><?php endforeach;?></select></label>
<label class="wide">Address<textarea name="address"><?=e((string)($portalAccount['address']??$patient['address']??''))?></textarea></label><label>Emergency contact name<input name="emergency_contact_name" value="<?=e((string)($portalAccount['emergency_contact_name']??''))?>"></label><label>Emergency contact phone<input name="emergency_contact_phone" value="<?=e((string)($portalAccount['emergency_contact_phone']??''))?>"></label>
<label class="checkbox-row"><input type="checkbox" name="appointment_notifications" value="1" <?=!isset($portalAccount['appointment_notifications'])||!empty($portalAccount['appointment_notifications'])?'checked':''?>> Appointment reminders</label><label class="checkbox-row"><input type="checkbox" name="record_notifications" value="1" <?=!isset($portalAccount['record_notifications'])||!empty($portalAccount['record_notifications'])?'checked':''?>> Record-copy notifications</label><button class="btn primary wide">Save profile</button></form>
<div class="alert info">To correct your legal name, birth date, or another identity field, submit a request under Requests & Messages or contact the clinic with identity evidence.</div></section>

<?php elseif($view==='records'):?>
<section class="record-grid portal-record-grid">
<section class="card"><div class="card-head"><div><h2>Dental Records</h2><p>Finalized dental visits, treatment information, prescriptions, and shared documents.</p></div></div><div class="portal-safety-summary"><strong>Dental treatment safety summary</strong><p>Allergies: <?=e((string)($latestMedical['allergies']?:'None recorded'))?></p><p>Medicines relevant to dental care: <?=e((string)($latestMedical['medications']?:'None recorded'))?></p><p>Last verified: <?=!empty($latestMedical['verified_at'])?e(date('M j, Y',strtotime((string)$latestMedical['verified_at']))):'Not verified'?></p><?php if($canSubmitHealth):?><a href="patient-portal?patient_id=<?=$patientId?>&amp;view=health">Report a change</a><?php endif;?></div></section>
<section class="card"><div class="card-head"><div><h2>Request a record copy</h2><p>The clinic verifies identity and approves clinical content before release.</p></div></div><?php if(!patient_record_release_schema_ready()):?><div class="alert warning">Record requests will become available after the clinic applies v0.9.1.2.</div><?php elseif(patient_portal_request_pending($patientId,'record_copy')):?><div class="alert info">A dental-record copy request is already in progress.</div><?php else:?><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="patient_id" value="<?=$patientId?>"><input type="hidden" name="action" value="record_copy_request"><input type="hidden" name="return_view" value="records"><label class="wide">Copy type<select name="record_scope" data-record-scope><?php foreach(patient_record_scope_options() as $value=>$label):?><option value="<?=e($value)?>"><?=e($label)?></option><?php endforeach;?></select></label><label>Date from<input type="date" name="date_from"></label><label>Date to<input type="date" name="date_to"></label><label class="wide">Reason for request<textarea name="reason" required placeholder="Example: Personal copy, referral, insurance, or transfer to another dentist"></textarea></label><button class="btn primary wide">Submit record request</button></form><?php endif;?></section></section>
<section class="card"><div class="card-head"><div><h2>Record copies</h2><p>Approved copies are available for 30 days and exclude internal queue and audit details.</p></div></div><div class="module-card-grid"><?php foreach($recordReleases as $release):?><article class="module-card"><span class="badge"><?=e(patient_record_release_status_label((string)$release['status']))?></span><h3><?=e(patient_record_scope_label((string)$release['release_scope']))?></h3><p>Requested <?=e(date('M j, Y',strtotime((string)$release['requested_at'])))?></p><?php if((string)$release['status']==='released'):?><a class="btn primary compact" target="_blank" href="patient-record?release_id=<?=(int)$release['id']?>&amp;patient_id=<?=$patientId?>">Open approved copy</a><small>Available until <?=e(date('M j, Y',strtotime((string)$release['expires_at'])))?></small><?php elseif(!empty($release['staff_note'])):?><small><?=e((string)$release['staff_note'])?></small><?php endif;?></article><?php endforeach;?><?php if(!$recordReleases):?><p class="muted">No record copy has been requested or released.</p><?php endif;?></div></section>
<section class="portal-stack portal-visit-ledger">
<div class="card portal-ledger-intro"><div class="card-head"><div><h2>Care and transaction history</h2><p>Each card groups the appointment or walk-in, finalized treatment, invoice lines, payments, returns, and follow-up linked to one visit.</p></div><button type="button" class="btn ghost portal-print-button" onclick="window.print()">Print / Save history</button></div><div class="alert info"><strong>Lifetime record:</strong> Older appointments that predate the unified visit system remain included as historical appointment or encounter cards.</div></div>
<?php foreach(($visitLedger['rows']??[]) as $visit):
$appointmentId=(int)($visit['appointment_id']??0);$encounterId=(int)($visit['encounter_id']??0);$invoiceId=(int)($visit['invoice_id']??0);
$segments=$visitLedger['details']['appointment_services'][$appointmentId]??[];
$procedures=$visitLedger['details']['procedures'][$encounterId]??[];
$visitPrescriptions=$visitLedger['details']['prescriptions'][$encounterId]??[];
$visitFollowups=$visitLedger['details']['followups'][$encounterId]??[];
$invoiceItems=$visitLedger['details']['invoice']['items'][$invoiceId]??[];
$visitPayments=$visitLedger['details']['invoice']['payments'][$invoiceId]??[];
$visitReturns=$visitLedger['details']['invoice']['returns'][$invoiceId]??[];
$allocations=$visitLedger['details']['invoice']['allocations']??[];
$clinicalVisible=$encounterId>0&&in_array((string)($visit['encounter_status']??''),['finalized','addendum'],true);
$visitDate=(string)($visit['event_at']??'');$invoiceBalance=max(0,(float)($visit['invoice_amount']??0)-(float)($visit['paid_amount']??0));?>
<article class="card portal-visit-card">
<div class="card-head portal-visit-head"><div><span class="eyebrow"><?=e(($visit['source_type']??'appointment')==='walk_in'?'Walk-in visit':'Scheduled visit')?></span><h2><?=e((string)($visit['service_name']?:'Dental visit'))?></h2><p><?= $visitDate!==''?e(date('F j, Y · g:i A',strtotime($visitDate))):'Date unavailable' ?> · <?=e((string)($visit['visit_code']?:$visit['event_key']))?></p></div><div><span class="badge"><?=e(module_status((string)($visit['state_code']?:$visit['appointment_status']?:$visit['encounter_status']?:'recorded')))?></span><?php if(($visit['integrity_status']??'ok')==='review'):?><small class="muted">Historical record</small><?php endif;?></div></div>
<div class="portal-visit-sections">
<section><h3>Appointment / arrival</h3><dl><dt>Provider</dt><dd><?=e((string)($visit['provider_name']?:'Clinic care team'))?></dd><?php if(!empty($visit['assistant_name'])):?><dt>Dental Assistant</dt><dd><?=e((string)$visit['assistant_name'])?></dd><?php endif;?><?php if(!empty($visit['queue_no'])):?><dt>Queue ticket</dt><dd><?=e((string)$visit['queue_no'])?></dd><?php endif;?><dt>Status</dt><dd><?=e(module_status((string)($visit['appointment_status']?:$visit['state_code'])))?></dd></dl><?php if($segments):?><div class="portal-service-segments"><strong>Appointment services</strong><?php foreach($segments as $segment):?><span><?=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'])))?> · <?=e(module_status((string)$segment['status']))?></span><?php endforeach;?></div><?php endif;?></section>
<?php if($clinicalVisible):?><section><h3>Completed clinical care</h3><dl><dt>Dentist</dt><dd><?=e((string)($visit['provider_name']?:'Dentist'))?></dd><dt>Finalized</dt><dd><?=!empty($visit['finalized_at'])?e(date('F j, Y g:i A',strtotime((string)$visit['finalized_at']))):'Recorded'?></dd><?php if(!empty($visit['clinical_disposition'])):?><dt>Outcome</dt><dd><?=e(module_status((string)$visit['clinical_disposition']))?></dd><?php endif;?></dl><?php if(!empty($visit['diagnosis'])):?><p><strong>Diagnosis:</strong> <?=nl2br(e((string)$visit['diagnosis']))?></p><?php endif;?><?php if($procedures):?><div class="portal-ledger-list"><strong>Procedures completed</strong><?php foreach($procedures as $procedure):?><span><?=e((string)($procedure['service_name_snapshot']?:$procedure['procedure_name']))?><?=!empty($procedure['tooth_region'])?' · '.e((string)$procedure['tooth_region']):''?><?=!empty($procedure['outcome'])?' · '.e((string)$procedure['outcome']):''?></span><?php endforeach;?></div><?php endif;?><?php if(!empty($visit['patient_advice'])):?><p><strong>Patient instructions:</strong> <?=nl2br(e((string)$visit['patient_advice']))?></p><?php endif;?><?php if($visitPrescriptions):?><details><summary>Prescriptions from this visit</summary><div class="portal-ledger-list"><?php foreach($visitPrescriptions as $rx):?><span><?=e(trim((string)($rx['medication_name']??'Prescription').' '.(string)($rx['strength']??'')))?><?=!empty($rx['dosage'])?' · '.e((string)$rx['dosage']):''?><?=!empty($rx['frequency'])?' · '.e((string)$rx['frequency']):''?></span><?php endforeach;?></div></details><?php endif;?></section><?php elseif($encounterId>0):?><section><h3>Clinical record</h3><p class="muted">The clinic has not finalized a patient-visible clinical summary for this visit.</p></section><?php endif;?>
<?php if($invoiceId>0):?><section class="portal-visit-finance"><h3>Invoice and payments</h3><div class="portal-invoice-summary"><span><strong><?=e((string)$visit['invoice_no'])?></strong><small><?=e(module_status((string)$visit['invoice_status']))?></small></span><span>Total <strong>₱<?=number_format((float)$visit['invoice_amount'],2)?></strong></span><span>Paid <strong>₱<?=number_format((float)$visit['paid_amount'],2)?></strong></span><span>Balance <strong>₱<?=number_format($invoiceBalance,2)?></strong></span></div><?php if($invoiceItems):?><div class="table-wrap"><table><thead><tr><th>Item</th><th>Qty</th><th>Unit</th><th>Discount</th><th>Net</th></tr></thead><tbody><?php foreach($invoiceItems as $item):?><tr><td><?=e((string)$item['description'])?></td><td><?=number_format((float)$item['quantity'],2)?></td><td>₱<?=number_format((float)$item['unit_price'],2)?></td><td>₱<?=number_format((float)$item['discount_amount'],2)?></td><td><strong>₱<?=number_format((float)$item['net_amount'],2)?></strong></td></tr><?php endforeach;?></tbody></table></div><?php endif;?><?php if($visitPayments||$visitReturns):?><details class="portal-payment-details"><summary>Payment and return history</summary><?php foreach($visitPayments as $payment):?><div><strong><?=e(date('M j, Y g:i A',strtotime((string)$payment['created_at'])))?></strong><span><?=e(module_status((string)$payment['method']))?> · ₱<?=number_format((float)$payment['amount'],2)?></span><small>Reference: <?=e((string)($payment['reference_no']?:'—'))?></small><?php $paymentAllocationRows=$allocations[(int)$payment['id']]??[];if($paymentAllocationRows):?><ul><?php foreach($paymentAllocationRows as $allocation):?><li><?=e((string)$allocation['description_snapshot'])?> — ₱<?=number_format((float)$allocation['amount_applied'],2)?> applied</li><?php endforeach;?></ul><?php endif;?></div><?php endforeach;?><?php foreach($visitReturns as $return):?><div class="portal-return-row"><strong><?=e(date('M j, Y g:i A',strtotime((string)($return['processed_at']??$return['approved_at']??$return['requested_at']))))?></strong><span><?=e(module_status((string)$return['adjustment_type']))?> · -₱<?=number_format((float)$return['amount'],2)?></span><small>Reference: <?=e((string)($return['reference_no']?:'—'))?></small></div><?php endforeach;?></details><?php endif;?></section><?php endif;?>
<?php if($visitFollowups):?><section><h3>Follow-up</h3><?php foreach($visitFollowups as $followup):?><p><strong><?=e((string)$followup['purpose'])?></strong><br>Recommended <?=e(date('F j, Y',strtotime((string)$followup['recommended_date'])))?> · <?=e(module_status((string)$followup['status']))?><?php if(!empty($followup['instructions'])):?><br><?=nl2br(e((string)$followup['instructions']))?><?php endif;?></p><?php endforeach;?></section><?php endif;?>
</div></article>
<?php endforeach;?><?php if(empty($visitLedger['rows'])):?><div class="card"><p class="muted">No care or transaction history is available yet.</p></div><?php endif;?>
<?php portal_render_pagination($patientId,'visits','visit_page',(int)($visitLedger['page']??1),(int)($visitLedger['total']??0),(int)($visitLedger['per_page']??10));?>
</section>

<?php elseif($view==='plans'):?>
<section class="portal-stack">
<?php foreach($plans as $row):$sharedItems=$planItemsByPlan[(int)$row['id']]??[];?>
<article class="card portal-treatment-plan"><div class="card-head"><div><h2><?=e((string)$row['title'])?></h2><p><?=e((string)($row['phase']?:'Treatment plan'))?> · Version <?=e((string)$row['version_no'])?></p></div><div><strong>₱<?=number_format((float)$row['estimate_total'],2)?></strong> <span class="badge"><?=e(module_status((string)$row['status']))?></span></div></div><?php if(!empty($row['notes'])):?><p><?=nl2br(e((string)$row['notes']))?></p><?php endif;?>
<div class="portal-plan-items"><?php foreach($sharedItems as $item):?><section class="timeline-item"><div class="card-head"><h3><?=e((string)$item['procedure_name'])?></h3><span class="badge"><?=e(module_status((string)$item['status']))?></span></div><p><?=e((string)($item['tooth_region']?:'General care'))?><?=!empty($item['diagnosis'])?' · '.e((string)$item['diagnosis']):''?></p><dl><dt>Priority</dt><dd><?=e(module_status((string)$item['priority']))?></dd><dt>Sessions</dt><dd><?=e((string)$item['sessions'])?></dd><dt>Estimated fee</dt><dd>₱<?=number_format((float)$item['estimated_fee'],2)?></dd><?php if(!empty($item['consent_status'])):?><dt>Consent</dt><dd><?=e(module_status((string)$item['consent_status']))?></dd><?php endif;?><?php if(!empty($item['prognosis'])):?><dt>Prognosis</dt><dd><?=e((string)$item['prognosis'])?></dd><?php endif;?></dl><?php if(!empty($item['clinical_rationale'])):?><p><strong>Why this is recommended:</strong> <?=nl2br(e((string)$item['clinical_rationale']))?></p><?php endif;?></section><?php endforeach;?><?php if(!$sharedItems):?><p class="muted">No itemized procedures have been shared for this plan.</p><?php endif;?></div></article>
<?php endforeach;?><?php if(!$plans):?><div class="card"><p class="muted">No treatment plan is currently shared.</p></div><?php endif;?>
</section>

<?php elseif($view==='appointments'):?>
<section class="portal-stack">
<?php foreach($appointments as $row):$future=strtotime((string)$row['start_at'])>=time();$actionable=$future&&!in_array((string)$row['status'],['cancelled','completed','no_show','checked_in','in_treatment','awaiting_billing'],true);$pendingReschedule=$portalActionsReady&&patient_portal_request_pending($patientId,'appointment_reschedule',(int)$row['id']);?>
<article class="card portal-appointment-card"><div class="portal-appointment-main"><span class="badge"><?=e(module_status((string)$row['status']))?></span><h2><?=e((string)$row['service_name'])?></h2><p class="portal-appointment-time"><?=e(date('l, F j, Y · g:i A',strtotime((string)$row['start_at'])))?></p><dl><dt><?=($row['provider_type']??'dentist')==='inhouse'?'Care provider':'Dentist'?></dt><dd><?=e(appointment_provider_display($row))?></dd><dt>Chair</dt><dd><?=e((string)($row['chair_name']?:'Not assigned'))?></dd></dl><?php $appointmentSegments=$appointmentServicesByAppointment[(int)$row['id']]??[];if($appointmentSegments):?><div class="portal-service-segments"><strong>Services in this appointment</strong><?php foreach($appointmentSegments as $segment):?><span><?=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'])))?> · <?=e(module_status((string)$segment['status']))?></span><?php endforeach;?></div><?php endif;?><?php if($future&&!in_array((string)$row['status'],['cancelled','no_show'],true)):?><a href="<?=e(appointment_calendar_relative_url($row))?>">Add to calendar</a><?php endif;?></div>
<?php if($actionable&&$portalActionsReady&&$canManageAppointments):?><div class="portal-appointment-actions">
<?php if((string)$row['status']!=='confirmed'||(string)$row['confirmation_status']!=='confirmed'):?><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="patient_id" value="<?=$patientId?>"><input type="hidden" name="action" value="confirm_appointment"><input type="hidden" name="appointment_id" value="<?=(int)$row['id']?>"><input type="hidden" name="return_view" value="appointments"><button class="btn primary">Confirm appointment</button></form><?php endif;?>
<details <?=$pendingReschedule?'open':''?>><summary><?=$pendingReschedule?'Reschedule request pending':'Request another schedule'?></summary><?php if($pendingReschedule):?><div class="alert info">The clinic is reviewing your request. Your existing time remains reserved.</div><?php else:?><form method="post" class="form-grid compact-portal-form"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="patient_id" value="<?=$patientId?>"><input type="hidden" name="action" value="request_reschedule"><input type="hidden" name="appointment_id" value="<?=(int)$row['id']?>"><input type="hidden" name="return_view" value="appointments"><label>Preferred date<input type="date" name="preferred_date" min="<?=date('Y-m-d')?>" required></label><label>Preferred from<input type="time" name="preferred_time_from"></label><label>Preferred until<input type="time" name="preferred_time_to"></label><label class="wide">Reason or scheduling note<textarea name="reason" required></textarea></label><button class="btn ghost wide">Send reschedule request</button></form><?php endif;?></details>
<details><summary>Cancel appointment</summary><form method="post" class="compact-portal-form"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="patient_id" value="<?=$patientId?>"><input type="hidden" name="action" value="cancel_appointment"><input type="hidden" name="appointment_id" value="<?=(int)$row['id']?>"><input type="hidden" name="return_view" value="appointments"><label>Cancellation reason<textarea name="reason" required></textarea></label><button class="btn danger" data-confirm="Cancel this appointment and release the reserved time?">Cancel appointment</button></form></details>
</div><?php endif;?></article>
<?php endforeach;?><?php if(!$appointments):?><div class="card"><p class="muted">No appointment history is available.</p></div><?php endif;?>
<?php portal_render_pagination($patientId,'appointments','appointment_page',$appointmentPage,$appointmentTotal,$appointmentPerPage);?>
</section>

<?php elseif($view==='prescriptions'):?>
<section class="card"><div class="card-head"><div><h2>Dental Prescriptions</h2><p>Issued prescriptions shared by your Dentist.</p></div></div><div class="module-card-grid"><?php foreach($prescriptions as $row):?><article class="module-card"><span class="badge"><?=e(date('M j, Y',strtotime((string)$row['issued_at'])))?></span><h3><?=e(trim((string)$row['medication_name'].' '.(string)$row['strength']))?></h3><p><?=e(trim((string)$row['dosage'].' · '.(string)$row['frequency']))?></p><small><?=e((string)$row['instructions'])?></small></article><?php endforeach;?><?php if(!$prescriptions):?><p class="muted">No issued dental prescription is shared.</p><?php endif;?></div></section>

<?php elseif($view==='followups'):?>
<section class="portal-stack">
<?php foreach($followUps as $row):$branch=(int)($row['branch_id']??0)?:((int)(public_request_branch_id()??0));$serviceOptions=$servicesByBranch[$branch]??public_services_catalog($branch?:null);$fixedService=(int)($row['service_id']??0);$continuity=patient_primary_dentist($patientId);$fixedDoctor=(int)($row['dentist_user_id']??0)?:((int)($continuity['id']??0));?>
<article class="card portal-followup-card"><div class="card-head"><div><span class="badge"><?=e(module_status((string)$row['status']))?></span><h2><?=e((string)$row['purpose'])?></h2><p>Recommended <?=e(date('F j, Y',strtotime((string)$row['recommended_date'])))?><?=!empty($row['maximum_date'])?' · Complete by '.e(date('F j, Y',strtotime((string)$row['maximum_date']))):''?></p></div></div><?php if(!empty($row['instructions'])):?><div class="continuity-care-note"><?=nl2br(e((string)$row['instructions']))?></div><?php endif;?>
<?php if(in_array((string)$row['status'],['pending','deferred'],true)&&empty($row['appointment_id'])&&$portalActionsReady&&$canManageAppointments):?>
<form method="post" class="form-grid portal-followup-form" data-slot-picker data-schedule-first data-branch-id="<?=$branch?>" data-fixed-doctor="<?=$fixedDoctor?>"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="patient_id" value="<?=$patientId?>"><input type="hidden" name="action" value="book_followup"><input type="hidden" name="follow_up_id" value="<?=(int)$row['id']?>"><input type="hidden" name="return_view" value="followups">
<label>Service<select name="service_id" data-slot-service required><option value="">Choose service</option><?php foreach($serviceOptions as $service):if($fixedService>0&&(int)$service['id']!==$fixedService)continue;?><option value="<?=(int)$service['id']?>" data-provider-type="<?=e(service_provider_type($service))?>" data-requires-chair="<?=(int)($service['requires_chair']??1)?>" <?=($fixedService===(int)$service['id'])?'selected':''?>><?=e((string)$service['name'].' · '.(int)$service['duration_minutes'].' min')?></option><?php endforeach;?></select></label>
<label>Dentist<select name="dentist_user_id" data-slot-doctor required><option value="">Choose a time first</option></select></label>
<label>Date<input type="date" name="date" data-slot-date min="<?=date('Y-m-d')?>" value="<?=e(max(date('Y-m-d'),(string)$row['recommended_date']))?>" required></label><div class="available-dates-panel wide" data-available-dates>Choose the follow-up service to see available dates.</div>
<label>Time<select name="time" data-slot-time required><option value="">Choose an available time</option></select></label>
<label>Assigned resource<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 after selecting a time" readonly></label>
<div class="slot-status wide" data-slot-status>Live availability will verify the provider and chair before booking.</div><button class="btn primary wide">Book this follow-up</button></form>
<?php elseif((string)$row['status']==='scheduled'):?><div class="alert success">This follow-up is already scheduled. Open Appointments for the confirmed date and provider.</div><?php endif;?></article>
<?php endforeach;?><?php if(!$followUps):?><div class="card"><p class="muted">No follow-up is due or scheduled.</p></div><?php endif;?>
</section>

<?php elseif($view==='health'):?>
<section class="card portal-health-card"><div class="card-head"><div><h2>Report a change that may affect dental care</h2><p>Your existing dental record is not overwritten. A dentist must verify and approve the new version.</p></div></div>
<?php if(!$canSubmitHealth):?><div class="alert info">This account can view shared records but is not authorized to submit dental-safety information changes for this patient.</div><?php elseif($portalActionsReady&&patient_portal_request_pending($patientId,'medical_history_update')):?><div class="alert info">A dental-safety information update is already waiting for clinic review.</div><?php elseif($portalActionsReady):?><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="patient_id" value="<?=$patientId?>"><input type="hidden" name="action" value="medical_history_update"><input type="hidden" name="return_view" value="health"><label class="wide">Allergies<textarea name="allergies" placeholder="Medicines, latex, foods, or other allergies"><?=e((string)($latestMedical['allergies']??''))?></textarea></label><label class="wide">Current medicines<textarea name="medications" placeholder="Name, dose, and frequency"><?=e((string)($latestMedical['medications']??''))?></textarea></label><label class="wide">Health information relevant to dental care<textarea name="conditions" placeholder="Pregnancy, bleeding risk, heart condition, diabetes, recent hospitalization, anesthesia reaction, or another condition that may affect dental care"><?=e((string)($latestMedical['conditions']??''))?></textarea></label><label class="wide">Dental history<textarea name="dental_history"><?=e((string)($latestMedical['dental_history']??''))?></textarea></label><label class="wide">What changed or needs verification?<textarea name="reason" required></textarea></label><button class="btn primary wide">Submit for clinic review</button></form><?php endif;?></section>

<?php elseif($view==='documents'):?>
<section class="card"><h2>Shared documents</h2><div class="module-card-grid"><?php foreach($documents as $row):?><article class="module-card"><span class="badge"><?=e((string)$row['category'])?></span><h3><?=e((string)($row['description']?:$row['file_name']))?></h3><p><?=e((string)$row['file_name'])?></p><a href="download-document?id=<?=(int)$row['id']?>">Open document</a></article><?php endforeach;?><?php if(!$documents):?><p class="muted">No patient-accessible document has been shared.</p><?php endif;?></div></section>

<?php elseif($view==='billing'):?>
<section class="card table-wrap portal-statement-card" id="portal-statement-print-area"><div class="card-head"><div><h2>Statements</h2><p>Lifetime account balances with itemized charges, payment allocations, approved returns, and receipt references.</p></div><button type="button" class="btn ghost portal-print-button" onclick="window.print()">Print / Save statement</button></div>
<div class="portal-statement-summary"><div><span>Total charges</span><strong>₱<?=number_format($statementTotals['charges'],2)?></strong></div><div><span>Paid to date</span><strong>₱<?=number_format($statementTotals['paid'],2)?></strong></div><div><span>Processed returns</span><strong>₱<?=number_format($statementTotals['returned'],2)?></strong></div><div><span>Outstanding balance</span><strong>₱<?=number_format($statementTotals['balance'],2)?></strong></div></div>
<div class="portal-invoice-list"><?php foreach($invoices as $row):$invoiceId=(int)$row['id'];$invoiceBalance=max(0,(float)$row['amount']-(float)$row['paid_amount']);$payments=$portalPaymentsByInvoice[$invoiceId]??[];$returns=$portalReturnsByInvoice[$invoiceId]??[];$items=$portalInvoiceItemsByInvoice[$invoiceId]??[];?><article class="portal-invoice-card"><div class="card-head"><div><span class="eyebrow">Invoice</span><h3><?=e((string)$row['invoice_no'])?></h3><p><?=!empty($row['created_at'])?e(date('F j, Y',strtotime((string)$row['created_at']))):''?> · <?=e((string)$row['description'])?></p></div><span class="badge"><?=e(module_status((string)$row['status']))?></span></div><div class="portal-invoice-summary"><span>Total <strong>₱<?=number_format((float)$row['amount'],2)?></strong></span><span>Paid <strong>₱<?=number_format((float)$row['paid_amount'],2)?></strong></span><span>Balance <strong>₱<?=number_format($invoiceBalance,2)?></strong></span></div><?php if($items):?><details open><summary>Itemized charges</summary><div class="table-wrap"><table><thead><tr><th>Item</th><th>Qty</th><th>Unit price</th><th>Discount</th><th>Net</th></tr></thead><tbody><?php foreach($items as $item):?><tr><td><?=e((string)$item['description'])?><?php if(!empty($item['item_code'])):?><small><?=e((string)$item['item_code'])?></small><?php endif;?></td><td><?=number_format((float)$item['quantity'],2)?></td><td>₱<?=number_format((float)$item['unit_price'],2)?></td><td>₱<?=number_format((float)$item['discount_amount'],2)?></td><td><strong>₱<?=number_format((float)$item['net_amount'],2)?></strong></td></tr><?php endforeach;?></tbody></table></div></details><?php else:?><p class="muted">This historical invoice does not yet have an itemized line breakdown.</p><?php endif;?><?php if($payments||$returns):?><details class="portal-payment-details"><summary>Payments, allocations, and returns</summary><?php foreach($payments as $payment):?><div><strong><?=e(date('M j, Y g:i A',strtotime((string)$payment['created_at'])))?></strong><span><?=e(module_status((string)$payment['method']))?> · ₱<?=number_format((float)$payment['amount'],2)?></span><small>Reference: <?=e((string)($payment['reference_no']?:'—'))?> · <?=e(module_status((string)($payment['status']??'posted')))?></small><?php $allocationRows=$portalPaymentAllocations[(int)$payment['id']]??[];if($allocationRows):?><ul><?php foreach($allocationRows as $allocation):?><li><?=e((string)$allocation['description_snapshot'])?> — ₱<?=number_format((float)$allocation['amount_applied'],2)?> applied · ₱<?=number_format((float)$allocation['remaining_after_snapshot'],2)?> remaining after payment</li><?php endforeach;?></ul><?php endif;?></div><?php endforeach;?><?php foreach($returns as $return):?><div class="portal-return-row"><strong><?=e(date('M j, Y g:i A',strtotime((string)($return['processed_at']??$return['approved_at']??$return['requested_at']))))?></strong><span><?=e(module_status((string)$return['adjustment_type']))?> · -₱<?=number_format((float)$return['amount'],2)?></span><small>Reference: <?=e((string)($return['reference_no']?:'—'))?> · <?=e(module_status((string)$return['status']))?></small></div><?php endforeach;?></details><?php else:?><p class="muted">No payment has been posted.</p><?php endif;?></article><?php endforeach;?><?php if(!$invoices):?><p class="muted">No statement is available.</p><?php endif;?></div>
<?php portal_render_pagination($patientId,'billing','invoice_page',$invoicePage,$invoiceTotal,$invoicePerPage);?>
<p class="muted portal-statement-note">This portal statement is for patient review. Official receipt validation remains with the clinic cashier records.</p></section>
<?php else:?>
<section class="card table-wrap"><div class="card-head"><div><h2>Portal activity and requests</h2><p>Completed self-service actions and items waiting for clinic review.</p></div></div><table><thead><tr><th>Date</th><th>Request</th><th>Preferred schedule / reason</th><th>Status</th><th>Clinic note</th></tr></thead><tbody><?php foreach($portalRequests as $row):?><tr><td><?=e(date('M j, Y g:i A',strtotime((string)$row['created_at'])))?></td><td><strong><?=e(patient_portal_request_type_label((string)$row['request_type']))?></strong></td><td><?php if($row['preferred_date']):?><?=e(date('M j, Y',strtotime((string)$row['preferred_date'])))?><?=!empty($row['preferred_time_from'])?' · '.e(date('g:i A',strtotime((string)$row['preferred_time_from']))):''?><?php endif;?><?php if($row['reason']):?><small><?=e((string)$row['reason'])?></small><?php endif;?></td><td><span class="badge"><?=e(module_status((string)$row['status']))?></span></td><td><?=e((string)($row['staff_note']??''))?></td></tr><?php endforeach;?><?php if(!$portalRequests):?><tr><td colspan="5" class="muted">No portal request has been recorded.</td></tr><?php endif;?></tbody></table></section>
<?php endif;?>
</section></div>
</main>
<?php render_public_footer();render_footer(true); ?>
