<?php
require DENTAL_ROOT . '/app/bootstrap.php';
require_login();
require_permission('patients.view',['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/clinical_safety.php';

$pdo = db();
$action = $_GET['action'] ?? 'list';
$advanced = documentation_schema_ready();
$taskRole = active_role();
$patientAccess = dental_patient_access_capabilities();
$canViewPatientIdentity = !empty($patientAccess['identity']);
$canViewPatientContact = !empty($patientAccess['contact']);
$canViewSafetyAlert = !empty($patientAccess['safety_alert']);
$canViewMedicalHistory = !empty($patientAccess['medical']);
$canRecordMedicalIntake = !empty($patientAccess['medical_intake']);
$canVerifyMedical = !empty($patientAccess['medical_verify']) && active_role_is('dentist');
$canViewFinancialRecord = !empty($patientAccess['financial']);
$canManagePortal = !empty($patientAccess['portal']);
$branchId = signed_user_branch_id();
if ($branchId < 1) {
    $activeBranchIds = $pdo->query('SELECT id FROM `' . table('branches') . '` WHERE is_active=1 ORDER BY id LIMIT 2')->fetchAll(PDO::FETCH_COLUMN);
    if (count($activeBranchIds) === 1) $branchId = (int)$activeBranchIds[0];
}
if (!$canViewPatientIdentity) access_denied('The active task workspace does not permit patient-directory access.');

$canManagePatient = task_context_action_allowed('patients.manage')
    && can('patients.manage',['super_admin','clinic_admin','receptionist','staff']);
$canMedicalHistory = task_context_action_allowed('patients.medical')
    && ($canViewMedicalHistory || $canRecordMedicalIntake);
$canVerifyGuardian = has_role(['super_admin','clinic_admin'])
    && in_array($taskRole,['super_admin','clinic_admin'],true);
$canPortalInvite = task_context_action_allowed('patients.portal') && $canManagePortal;
$canClinicalWrite = active_role_is('dentist');
$patientScopeAllowed = static function(int $patientId): bool { return task_patient_scope_allowed($patientId); };
if($_SERVER['REQUEST_METHOD']==='POST'){
 verify_csrf();$postAction=$_POST['action']??'save_patient';
 if($postAction==='save_patient'){
  require_task_action('patients.manage');require_permission('patients.manage',['super_admin','clinic_admin','receptionist','staff']);$id=(int)($_POST['id']??0);$wasExisting=$id>0;if($id>0)require_patient($id);elseif($branchId<1)throw new RuntimeException('Assign an active clinic branch before registering a patient.');$first=trim((string)$_POST['first_name']);$last=trim((string)$_POST['last_name']);if($first===''||$last==='')throw new RuntimeException('First and last name are required.');
  if(!$id){
$patientsTable=table('patients');$deletedClause=schema_column_exists($patientsTable,'deleted_at')?'deleted_at IS NULL AND ':'';
   $duplicate=$pdo->prepare('SELECT id,patient_no,first_name,last_name,birth_date,mobile FROM `' . $patientsTable . '` WHERE '.$deletedClause.'branch_id=? AND first_name=? AND last_name=? AND ((birth_date IS NOT NULL AND birth_date=?) OR (mobile IS NOT NULL AND mobile<>"" AND mobile=?)) LIMIT 1');
   $duplicate->execute([$branchId,$first,$last,$_POST['birth_date']?:null,trim((string)$_POST['mobile'])]);
   if($match=$duplicate->fetch())throw new RuntimeException('Possible duplicate patient: '.$match['patient_no'].' · '.$match['first_name'].' '.$match['last_name'].'. Open the existing record before creating another.');
  }
  $patientsTable=table('patients');
  $numbering=function_exists('dental_patient_numbering_settings')?dental_patient_numbering_settings():['mode'=>'auto'];
  $postedPatientNo=trim((string)($_POST['patient_no']??''));
  $currentPatientNo='';
  if($id>0){$noStmt=$pdo->prepare('SELECT patient_no FROM `' . $patientsTable . '` WHERE id=? LIMIT 1');$noStmt->execute([$id]);$currentPatientNo=(string)($noStmt->fetchColumn()?:'');}
  $patientNoForSave=$currentPatientNo;
  if($id>0){
   if(has_role(['super_admin','clinic_admin'])&&$postedPatientNo!==''&&$postedPatientNo!==$currentPatientNo){dental_patient_number_validate_unique($pdo,$postedPatientNo,$id);$patientNoForSave=$postedPatientNo;}
  }else{
   $mode=(string)($numbering['mode']??'auto');
   if($mode==='manual'){if($postedPatientNo==='')throw new RuntimeException('Enter the patient number or change Patient Numbering to automatic.');dental_patient_number_validate_unique($pdo,$postedPatientNo,0);$patientNoForSave=$postedPatientNo;}
   elseif($mode==='auto_allow_override'&&$postedPatientNo!==''){dental_patient_number_validate_unique($pdo,$postedPatientNo,0);$patientNoForSave=$postedPatientNo;}
   else{$patientNoForSave=dental_next_patient_number($pdo,$branchId,$numbering);}
  }
  $protectedMedicalAlerts=null;if($id>0){$alertStmt=$pdo->prepare('SELECT medical_alerts FROM `' . table('patients') . '` WHERE id=? LIMIT 1');$alertStmt->execute([$id]);$protectedMedicalAlerts=$alertStmt->fetchColumn();}
  if($advanced){$data=[$first,trim((string)$_POST['middle_name']),$last,trim((string)$_POST['preferred_name']),$_POST['birth_date']?:null,$_POST['sex'],trim((string)$_POST['civil_status']),trim((string)$_POST['nationality']),trim((string)$_POST['occupation']),trim((string)$_POST['employer']),trim((string)$_POST['preferred_language']),trim((string)$_POST['communication_preference']),trim((string)$_POST['mobile']),trim((string)$_POST['email']),trim((string)$_POST['address']),trim((string)$_POST['emergency_contact']),$protectedMedicalAlerts];if($id){$stmt=$pdo->prepare('UPDATE `' . table('patients') . '` SET first_name=?,middle_name=?,last_name=?,preferred_name=?,birth_date=?,sex=?,civil_status=?,nationality=?,occupation=?,employer=?,preferred_language=?,communication_preference=?,mobile=?,email=?,address=?,emergency_contact=?,medical_alerts=?,updated_at=NOW() WHERE id=?');$stmt->execute([...$data,$id]);}else{$no=$patientNoForSave;$stmt=$pdo->prepare('INSERT INTO `' . table('patients') . '` (branch_id,patient_no,first_name,middle_name,last_name,preferred_name,birth_date,sex,civil_status,nationality,occupation,employer,preferred_language,communication_preference,mobile,email,address,emergency_contact,medical_alerts,created_by,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())');$stmt->execute([$branchId,$no,...$data,(int)current_user()['id']]);$id=(int)$pdo->lastInsertId();}}
  else{$data=[$first,trim((string)$_POST['middle_name']),$last,$_POST['birth_date']?:null,$_POST['sex'],trim((string)$_POST['mobile']),trim((string)$_POST['email']),trim((string)$_POST['address']),trim((string)$_POST['emergency_contact']),$protectedMedicalAlerts];if($id){$stmt=$pdo->prepare('UPDATE `' . table('patients') . '` SET first_name=?,middle_name=?,last_name=?,birth_date=?,sex=?,mobile=?,email=?,address=?,emergency_contact=?,medical_alerts=? WHERE id=?');$stmt->execute([...$data,$id]);}else{$no=$patientNoForSave;$stmt=$pdo->prepare('INSERT INTO `' . table('patients') . '` (branch_id,patient_no,first_name,middle_name,last_name,birth_date,sex,mobile,email,address,emergency_contact,medical_alerts,created_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)');$stmt->execute([$branchId,$no,...$data,(int)current_user()['id']]);$id=(int)$pdo->lastInsertId();}}
  if($wasExisting&&$patientNoForSave!==''&&$patientNoForSave!==$currentPatientNo){$pdo->prepare('UPDATE `' . table('patients') . '` SET patient_no=?,updated_at=NOW() WHERE id=?')->execute([$patientNoForSave,$id]);}
  audit($wasExisting?'patient_saved':'patient_created','patient',$id);module_return('patients.php?action=view&id='.$id,'success','Patient record saved.');
 }
 require_documentation_schema();$patientId=(int)$_POST['patient_id'];require_patient($patientId);if(!$patientScopeAllowed($patientId)){http_response_code(403);exit('This patient is outside the active task workspace.');}
 if ($postAction === 'save_medical') {
  if (!$canMedicalHistory || (!$canVerifyMedical && !$canRecordMedicalIntake)) {
   dental_access_record_decision('denied','patients.medical.intake','patient',$patientId,'The active task workspace cannot update dental-safety history.');
   access_denied('This task view cannot update dental-safety history.');
  }
  $values = [
   'allergies'=>trim((string)$_POST['allergies']),
   'medications'=>trim((string)$_POST['medications']),
   'conditions'=>trim((string)$_POST['conditions']),
   'dental_history'=>trim((string)$_POST['dental_history']),
  ];
  if ($canVerifyMedical) {
   $verifiedBy=(int)current_user()['id'];
   $startedMedicalTransaction=!$pdo->inTransaction();
   if($startedMedicalTransaction)$pdo->beginTransaction();
   try{
    $medicalHistoryId=integrity_insert_verified_medical_history($pdo,$patientId,$values,$verifiedBy,trim((string)($_POST['verification_note']??'')));
    $structuredSafetySynced=clinical_safety_schema_ready();
    if($structuredSafetySynced)clinical_safety_sync_verified_history($pdo,$patientId,$medicalHistoryId,$values,$verifiedBy);
    $pdo->prepare('UPDATE `' . table('patients') . '` SET medical_alerts=?,updated_at=NOW() WHERE id=?')->execute([integrity_medical_alert_summary($values),$patientId]);
    audit_required('medical_history_verified','patient',$patientId,['verified_by'=>$verifiedBy,'medical_history_id'=>$medicalHistoryId,'structured_safety_sync'=>$structuredSafetySynced]);
    if($startedMedicalTransaction)$pdo->commit();
   }catch(Throwable $exception){if($startedMedicalTransaction&&$pdo->inTransaction())$pdo->rollBack();throw $exception;}
   $message=$structuredSafetySynced?'Medical and dental history verified and saved with structured clinical-safety records.':'Medical and dental history verified. Apply v0.9.0.7 to enable structured safety records.';
  } else {
   integrity_insert_reported_medical_history($pdo,$patientId,$values,'staff_recorded',(int)current_user()['id'],'Recorded by authorized intake staff. Pending dentist review.');
   audit('medical_history_reported','patient',$patientId,['recorded_by'=>(int)current_user()['id']]);
   $message='Patient-reported medical information saved for dentist review. It was not activated as a clinical alert.';
  }
  module_return('patients.php?action=view&id='.$patientId.'&tab=history&section=medical','success',$message);
 }
 if($postAction==='add_guardian'){if(!$canManagePatient){http_response_code(403);exit('This task view cannot manage guardians.');}$guardianId=integrity_insert_pending_guardian($pdo,$patientId,['name'=>$_POST['name']??'','relationship'=>$_POST['relationship']??'','mobile'=>$_POST['mobile']??'','email'=>$_POST['email']??'','address'=>$_POST['address']??'']);audit('patient_guardian_added','patient_guardian',$guardianId,['patient_id'=>$patientId,'verification_status'=>'pending']);module_return('patients.php?action=view&id='.$patientId,'success','Guardian added as pending verification. No consent or record-release authority was granted.');}
 if($postAction==='verify_guardian'){if(!$canVerifyGuardian){http_response_code(403);exit('Only an Administrator can verify guardian authority.');}try{$pdo->beginTransaction();$guardianId=(int)($_POST['guardian_id']??0);integrity_verify_guardian($pdo,$guardianId,$patientId,isset($_POST['authorized_to_consent']),isset($_POST['authorized_to_receive_records']),trim((string)($_POST['verification_method']??'')),trim((string)($_POST['verification_note']??'')));audit('patient_guardian_authority_updated','patient_guardian',$guardianId,['patient_id'=>$patientId,'authorized_to_consent'=>isset($_POST['authorized_to_consent']),'authorized_to_receive_records'=>isset($_POST['authorized_to_receive_records'])]);$pdo->commit();module_return('patients.php?action=view&id='.$patientId,'success','Guardian identity and authority were saved. Portal permissions update immediately.');}catch(Throwable $exception){if($pdo->inTransaction())$pdo->rollBack();throw $exception;}}
 if($postAction==='revoke_guardian'){if(!$canVerifyGuardian){http_response_code(403);exit('Only an Administrator can revoke guardian authority.');}try{$pdo->beginTransaction();$guardianId=(int)($_POST['guardian_id']??0);$reason=trim((string)($_POST['revocation_reason']??''));integrity_revoke_guardian_authority($pdo,$guardianId,$patientId,$reason);audit('patient_guardian_authority_revoked','patient_guardian',$guardianId,['patient_id'=>$patientId,'reason'=>$reason]);$pdo->commit();module_return('patients.php?action=view&id='.$patientId,'success','Guardian authority and dependent portal access were revoked.');}catch(Throwable $exception){if($pdo->inTransaction())$pdo->rollBack();throw $exception;}}
 if($postAction==='add_alert'){
  if(!$canVerifyMedical){http_response_code(403);exit('Only the signed-in dentist can activate a verified dental treatment safety alert.');}
  $alertType=trim((string)$_POST['alert_type']);$alertDescription=trim((string)$_POST['description']);$alertSeverity=(string)($_POST['severity']??'caution');
  $alertNormalize=static fn(string $value): string => function_exists('clinical_safety_normalize')?clinical_safety_normalize($value):strtolower(trim((string)(preg_replace('/\s+/u',' ',$value)?:$value)));
  $existingAlertStmt=$pdo->prepare('SELECT id,alert_type,description FROM `' . table('medical_alerts') . '` WHERE patient_id=? AND is_active=1 ORDER BY id DESC');$existingAlertStmt->execute([$patientId]);$existingAlertId=0;
  foreach($existingAlertStmt->fetchAll() as $existingAlert){if($alertNormalize((string)$existingAlert['alert_type'])===$alertNormalize($alertType)&&$alertNormalize((string)$existingAlert['description'])===$alertNormalize($alertDescription)){$existingAlertId=(int)$existingAlert['id'];break;}}
  if($existingAlertId>0){module_return('patients.php?action=view&id='.$patientId.'&tab=history&section=allergies','information','This verified alert is already active. No duplicate record was created.');}
  $pdo->prepare('INSERT INTO `' . table('medical_alerts') . '` (patient_id,alert_type,severity,description,is_active,created_by,created_at) VALUES (?,?,?,?,1,?,NOW())')->execute([$patientId,$alertType,$alertSeverity,$alertDescription,(int)current_user()['id']]);
  audit('medical_alert_added','patient',$patientId);module_return('patients.php?action=view&id='.$patientId.'&tab=history&section=allergies','success','Dental treatment safety alert added.');
 }
 if($postAction==='invite_portal'){
  if(!$canPortalInvite){http_response_code(403);exit('This task view cannot invite patient portal accounts.');}
  $patient=require_patient($patientId);
  $guardianId=(int)($_POST['guardian_id']??0);
  $recipientName=trim((string)($patient['preferred_name']?:$patient['first_name']));
  if($guardianId>0){
   if(!schema_column_exists(table('patient_portal_invites'),'guardian_id'))throw new RuntimeException('Apply DentalCare update 0.5.9.8.4 before inviting a guardian.');
   $guardianStmt=$pdo->prepare('SELECT * FROM `' . table('patient_guardians') . '` WHERE id=? AND patient_id=? LIMIT 1');
   $guardianStmt->execute([$guardianId,$patientId]);$guardian=$guardianStmt->fetch();
   if(!$guardian)throw new RuntimeException('Guardian record not found for this patient.');
   if((string)($guardian['verification_status']??'pending')!=='verified')throw new RuntimeException('Verify the guardian identity before granting portal access.');
   if(empty($guardian['authorized_to_receive_records']))throw new RuntimeException('This guardian is not authorized to receive the patient record.');
   $email=trim((string)($guardian['email']??''));$recipientName=trim((string)($guardian['name']??'Guardian'));
   if(!filter_var($email,FILTER_VALIDATE_EMAIL))throw new RuntimeException('The verified guardian must have a valid email address.');
  }else{
   $email=trim((string)($patient['email']??''));
   $submittedEmail=trim((string)($_POST['email']??$email));
   if(!filter_var($email,FILTER_VALIDATE_EMAIL))throw new RuntimeException('Save a valid patient email before sending a patient portal invitation.');
   if(strcasecmp($submittedEmail,$email)!==0)throw new RuntimeException('Patient portal invitations may only be sent to the email saved in the patient record.');
  }
  $token=bin2hex(random_bytes(32));
  $updated=schema_column_exists(table('patient_portal_invites'),'updated_at')?',updated_at=NOW()':'';
  if(schema_column_exists(table('patient_portal_invites'),'guardian_id')){
   $expireSql='UPDATE `' . table('patient_portal_invites') . '` SET expires_at=NOW()'.$updated.' WHERE patient_id=? AND '.($guardianId>0?'guardian_id=?':'guardian_id IS NULL').' AND used_at IS NULL AND expires_at>NOW()';
   $expireArgs=$guardianId>0?[$patientId,$guardianId]:[$patientId];
   $pdo->prepare($expireSql)->execute($expireArgs);
   $pdo->prepare('INSERT INTO `' . table('patient_portal_invites') . '` (patient_id,guardian_id,email,token_hash,expires_at,created_by,created_at) VALUES (?,?,?,?,DATE_ADD(NOW(),INTERVAL 72 HOUR),?,NOW())')->execute([$patientId,$guardianId?:null,$email,hash('sha256',$token),(int)current_user()['id']]);
  }else{
   $expireSql='UPDATE `' . table('patient_portal_invites') . '` SET expires_at=NOW()'.$updated.' WHERE patient_id=? AND used_at IS NULL AND expires_at>NOW()';
   $pdo->prepare($expireSql)->execute([$patientId]);
   $pdo->prepare('INSERT INTO `' . table('patient_portal_invites') . '` (patient_id,email,token_hash,expires_at,created_by,created_at) VALUES (?,?,?,DATE_ADD(NOW(),INTERVAL 72 HOUR),?,NOW())')->execute([$patientId,$email,hash('sha256',$token),(int)current_user()['id']]);
  }
  $inviteId=(int)$pdo->lastInsertId();$url=app_absolute_url('patient-activate.php?token='.urlencode($token));
  try{$delivery=queue_template_email('patient_portal_invite',$email,['recipient_name'=>$recipientName,'activation_url'=>$url,'expires_hours'=>'72'],['type'=>'patient_portal_invite','patient_id'=>$patientId,'guardian_id'=>$guardianId?:null,'portal_invite_id'=>$inviteId],true);}
  catch(Throwable $mailError){$delivery=['id'=>0,'status'=>'failed','error'=>$mailError->getMessage(),'attempts'=>0];}
  $feedback=email_delivery_feedback($delivery,$guardianId>0?'Guardian portal invitation':'Patient portal invitation');
  audit('patient_portal_invited','patient',$patientId,['email'=>$email,'guardian_id'=>$guardianId?:null,'delivery_status'=>$delivery['status']??'pending','outbox_id'=>$delivery['id']??null,'invite_id'=>$inviteId]);
  module_return('patients.php?action=view&id='.$patientId.'&tab=overview',$feedback['type'],$feedback['message']);
 }
}

if($action==='view'&&isset($_GET['id'])){
 $id=(int)$_GET['id'];$patient=require_patient($id);if(!$patientScopeAllowed($id)){http_response_code(403);exit('This patient is outside the active task workspace.');}
 require DENTAL_ROOT . '/modules/patient-care/patient_workspace.php';
 exit;
}

if(in_array($action,['new','edit'],true)){
 if(!$canManagePatient){http_response_code(403);exit('This task view cannot create or edit patient records.');}
 $p=['id'=>'','patient_no'=>'','first_name'=>'','middle_name'=>'','last_name'=>'','preferred_name'=>'','birth_date'=>'','sex'=>'','civil_status'=>'','nationality'=>'','occupation'=>'','employer'=>'','preferred_language'=>'','communication_preference'=>'','mobile'=>'','email'=>'','address'=>'','emergency_contact'=>'','medical_alerts'=>''];if($action==='edit')$p=require_patient((int)$_GET['id']);$numbering=function_exists('dental_patient_numbering_settings')?dental_patient_numbering_settings():['mode'=>'auto'];$patientNoPreview=function_exists('dental_patient_number_preview')?dental_patient_number_preview($numbering):('P'.date('ymd').'0001');$patientNoMode=(string)($numbering['mode']??'auto');render_header($action==='new'?'Register Patient':'Edit Patient');?><form method="post" class="card form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="save_patient"><input type="hidden" name="id" value="<?=e($p['id'])?>"><?php if($action==='new'):?><label>Patient number<input name="patient_no" value="<?=e($patientNoMode==='manual'?'':($patientNoMode==='auto'?$patientNoPreview:''))?>" placeholder="<?=e($patientNoPreview)?>" <?=$patientNoMode==='auto'?'readonly':''?> <?=$patientNoMode==='manual'?'required':''?>><small><?= $patientNoMode==='auto'?'Generated automatically. Open Patient Numbering to change the format.':($patientNoMode==='manual'?'Required by your clinic numbering policy.':'Optional override; leave blank for automatic.') ?></small></label><?php else:?><label>Patient number<input name="patient_no" value="<?=e((string)($p['patient_no']??''))?>" <?=has_role(['super_admin','clinic_admin'])?'':'readonly'?>><small><?=has_role(['super_admin','clinic_admin'])?'Administrators may correct a patient number.':'Only Admin/Superuser can change patient numbers.'?></small></label><?php endif;?><label>First name<input name="first_name" value="<?=e($p['first_name'])?>" required></label><label>Middle name<input name="middle_name" value="<?=e($p['middle_name'])?>"></label><label>Last name<input name="last_name" value="<?=e($p['last_name'])?>" required></label><?php if($advanced):?><label>Preferred name<input name="preferred_name" value="<?=e($p['preferred_name'])?>"></label><?php endif;?><label>Birth date<input type="date" name="birth_date" value="<?=e($p['birth_date'])?>"></label><label>Sex<select name="sex"><option value="">Select</option><?php foreach(['Female','Male','Other'] as $sex):?><option <?=$p['sex']===$sex?'selected':''?>><?=e($sex)?></option><?php endforeach;?></select></label><?php if($advanced):?><label>Civil status<input name="civil_status" value="<?=e($p['civil_status'])?>"></label><label>Nationality<input name="nationality" value="<?=e($p['nationality'])?>"></label><label>Occupation<input name="occupation" value="<?=e($p['occupation'])?>"></label><label>Employer<input name="employer" value="<?=e($p['employer'])?>"></label><label>Preferred language<input name="preferred_language" value="<?=e($p['preferred_language'])?>"></label><label>Preferred contact<select name="communication_preference"><option value="">Not set</option><?php foreach(['Email','SMS','Phone','Patient Portal'] as $pref):?><option <?=$p['communication_preference']===$pref?'selected':''?>><?=e($pref)?></option><?php endforeach;?></select></label><?php endif;?><label>Mobile<input name="mobile" value="<?=e($p['mobile'])?>"></label><label>Email<input type="email" name="email" value="<?=e($p['email'])?>"></label><label class="wide">Address<textarea name="address"><?=e($p['address'])?></textarea></label><label class="wide">Emergency contact<input name="emergency_contact" value="<?=e($p['emergency_contact'])?>"></label><label class="wide">Clinical alerts<textarea readonly><?=e($p['medical_alerts'])?></textarea><small>Clinical alerts are verified and maintained by the dentist from Dental Safety History.</small></label><button class="btn primary">Save patient</button></form><?php render_footer();exit;
}
$q=trim((string)($_GET['q']??''));
$page=module_page_number('page');
$perPage=module_page_size('per_page',25);
$patientsTable=table('patients');
$patientColumns=['id','patient_no','first_name','middle_name','last_name','birth_date','sex','status','created_at','deleted_at'];
if($canViewPatientContact){$patientColumns[]='mobile';$patientColumns[]='email';}
if($canViewSafetyAlert)$patientColumns[]='medical_alerts';
$availableColumns=array_values(array_filter(array_unique($patientColumns),static fn(string $column):bool=>schema_column_exists($patientsTable,$column)));
$requiredColumns=['id','patient_no','first_name','last_name'];
$missingRequired=array_values(array_diff($requiredColumns,$availableColumns));
if($missingRequired){
 render_header('Patients');
 echo '<div class="alert error"><strong>Patient table repair required.</strong><p>The patient table is missing: '.e(implode(', ',$missingRequired)).'. Open Administration → System Setup → Updates & Database and apply all pending updates.</p></div>';
 render_footer();
 exit;
}
$selectColumns=implode(',',array_map(static fn(string $column):string=>'p.`'.$column.'`',$availableColumns));
$where=' WHERE 1=1';$args=[];
if(in_array('deleted_at',$availableColumns,true))$where.=' AND p.deleted_at IS NULL';
$searchable=[];
if($q!==''){
 $searchFields=['patient_no','first_name','last_name'];
 if($canViewPatientContact){$searchFields[]='mobile';$searchFields[]='email';}
 $searchable=array_values(array_filter($searchFields,static fn(string $column):bool=>in_array($column,$availableColumns,true)));
 if($searchable){
  $where.=' AND ('.implode(' OR ',array_map(static fn(string $column):string=>'p.`'.$column.'` LIKE ?',$searchable)).')';
  $like='%'.$q.'%';$args=array_merge($args,array_fill(0,count($searchable),$like));
 }
}
[$scopeSql,$scopeParams]=task_patient_scope_condition('p.id');
$where.=$scopeSql;$args=array_merge($args,$scopeParams);
$order=' ORDER BY '.(in_array('created_at',$availableColumns,true)?'p.created_at DESC':'p.id DESC');
$compatibilityMode=false;$patientRows=[];$patientTotal=0;
try{
 $countStmt=$pdo->prepare('SELECT COUNT(*) FROM `'.$patientsTable.'` p'.$where);$countStmt->execute($args);$patientTotal=(int)$countStmt->fetchColumn();
 $pages=max(1,(int)ceil($patientTotal/$perPage));$page=min($page,$pages);$offset=($page-1)*$perPage;
 $stmt=$pdo->prepare('SELECT '.$selectColumns.' FROM `'.$patientsTable.'` p'.$where.$order.' LIMIT '.$perPage.' OFFSET '.$offset);$stmt->execute($args);$patientRows=$stmt->fetchAll();
} catch (Throwable $exception) {
 error_log('DentalCare patients list authorization query failed closed: ' . $exception->getMessage());
 dental_access_record_decision('denied','patients.identity.view','patient',null,'Patient directory scope query failed; unrestricted fallback was blocked.');
 $compatibilityMode=true;
 $patientRows=[];
 $patientTotal=0;
}
render_header('Patients');?>
<?php if($compatibilityMode):?><div class="alert warning"><strong>Patient directory temporarily restricted.</strong><p>The role-scoped query could not be verified, so DentalCare blocked the unrestricted fallback. Apply the pending update or repair the reported schema issue before searching again.</p></div><?php endif;?>
<div class="toolbar"><form method="get" action="patients.php" class="auto-search-form" data-auto-submit><label class="search-field"><span class="sr-only">Search patients</span><input type="search" name="q" value="<?=e($q)?>" placeholder="<?=e($canViewPatientContact?'Search patient, number, mobile, or email':'Search patient name or number')?>" autocomplete="off" data-auto-search data-live-patient-filter data-min-length="1"></label><?php if($q!==''):?><a class="btn link" href="patients.php">Clear</a><?php endif;?></form><?php if($canManagePatient):?><a class="btn primary" href="patients.php?action=new">Register patient</a><?php endif;?></div>
<div class="card server-list-card"><?php module_render_server_pagination($patientTotal,$page,$perPage,'page','per_page','patients'); ?>
<div class="table-wrap" data-no-pagination><table data-no-pagination="true"><thead><tr><th>Patient No.</th><th>Name</th><th>Birth date</th><?php if($canViewPatientContact):?><th>Mobile</th><?php endif;?><th>Status</th><th></th></tr></thead><tbody>
<?php foreach($patientRows as $p):
 $patientSearchParts=[(string)($p['patient_no']??''),(string)($p['first_name']??''),(string)($p['middle_name']??''),(string)($p['last_name']??'')];
 if($canViewPatientContact){$patientSearchParts[]=(string)($p['mobile']??'');$patientSearchParts[]=(string)($p['email']??'');}
 $patientSearchText=mb_strtolower(trim(implode(' ',$patientSearchParts)),'UTF-8'); ?>
<tr data-patient-row data-search-text="<?=e($patientSearchText)?>"><td><?=e((string)($p['patient_no']??''))?></td><td><strong><?=e((string)($p['last_name']??'').', '.(string)($p['first_name']??''))?></strong><?php if($canViewSafetyAlert&&!empty($p['medical_alerts'])):?><small class="danger-text">Dental safety alert on file</small><?php endif;?></td><td><?=e((string)($p['birth_date']??''))?></td><?php if($canViewPatientContact):?><td><?=e((string)($p['mobile']??''))?></td><?php endif;?><td><span class="badge"><?=e((string)($p['status']??'active'))?></span></td><td><a href="patients.php?action=view&amp;id=<?=(int)$p['id']?>">Open</a></td></tr>
<?php endforeach;?>
<?php $patientColumnCount=$canViewPatientContact?6:5; if(!$patientRows):?><tr data-patient-empty><td colspan="<?=$patientColumnCount?>" class="muted">No patients matched this view.</td></tr><?php else:?><tr data-patient-live-empty hidden><td colspan="<?=$patientColumnCount?>" class="muted">No patient on this page matches what you typed. The full authorized search will update automatically.</td></tr><?php endif;?></tbody></table></div>
<?php module_render_server_pagination($patientTotal,$page,$perPage,'page','per_page','patients'); ?></div>
<?php render_footer();
