<?php
require __DIR__ . '/app/bootstrap.php';
require_once __DIR__ . '/app/layout.php';
require_auth();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') redirect('dashboard.php');
verify_csrf();
$action = request('action');
$user = current_user();
track_admin_presence($user);
$returnPage = preg_replace('/[^a-z_]/','',request('return_page','home'));
// Administradores começam sem acesso aos módulos sensíveis; o Administrador Sênior libera individualmente.
if((string)($user['role']??'')==='sub_admin'){
    $actionModules=[
        'wallet'=>['request_wallet_withdrawal','review_wallet_withdrawal'],
        'fees'=>['save_fee_rule','delete_fee_rule','save_financial_fee_settings'],
        'backups'=>['create_server_backup','delete_server_backup','prepare_database_restore'],
        'settings'=>['save_settings'],
        'banks'=>['save_bank','delete_bank'],
        'charges'=>['save_charge','update_charge','delete_charge','send_charge_proof'],
        'subscriptions'=>['save_subscription','delete_subscription','release_subscription_access'],
        'plans'=>['save_plan','review_plan_request','submit_plan_request'],
        'segments'=>['save_segment','delete_segment'],
    ];
    foreach($actionModules as $module=>$names){if(in_array($action,$names,true)&&!subadmin_has_module_access($user,$module))throw new RuntimeException('Este módulo não foi liberado para seu perfil pelo Administrador Sênior.');}
}
if (user_needs_onboarding($user) && !is_admin()) { flash('warning','Conclua seus dados antes de usar o painel.'); redirect('complete_registration.php'); }
if (admin_profile_completion_required($user) && !is_admin() && !in_array($action,['save_profile','request_account_deletion','cancel_account_deletion_request'],true)) { flash('warning','Complete nome, CPF/CNPJ e endereço para liberar os demais módulos.'); redirect('dashboard.php?page=profile'); }
if (account_restricted($user) && !is_admin() && !in_array($action,['send_charge_proof','submit_plan_request','request_account_deletion','cancel_account_deletion_request'],true)) { flash('error','Regularize sua mensalidade para liberar os demais módulos.'); redirect('dashboard.php?page=subscriptions'); }
if (!account_restricted($user) && documents_access_restricted($user) && !is_admin() && !in_array($action,['upload_required_document','request_account_deletion','cancel_account_deletion_request'],true)) { flash('error','Envie os documentos obrigatórios para liberar os demais módulos.'); redirect('dashboard.php?page=documents'); }

function back_to($page, $message, $type='success') {
    flash($type, $message);
    if ($page === 'marketplace') redirect('marketplace.php');
    $anchor=preg_replace('/[^a-zA-Z0-9_-]/','',(string)request('return_anchor',''));
    redirect('dashboard.php?page=' . $page . ($anchor!==''?'#'.$anchor:''));
}
function must_admin() { if (!is_admin()) throw new RuntimeException('Ação permitida somente ao Adminitrador Sênior.'); }
function must_super_admin() { if (!is_super_admin()) throw new RuntimeException('Ação permitida somente ao Administrador Sênior.'); }
function must_role($roles) { if (!is_role($roles) && !is_admin()) throw new RuntimeException('Você não tem permissão para está ação.'); }
function must_company_type($type) { $u=current_user(); if(!is_admin() && (($u['company_type']??'')!==$type)) throw new RuntimeException('Este módulo não está habilitado para o segmento da sua empresa.'); }

function can_manage_clinic_company(int $companyId): bool {
    if($companyId<1||!company_is_clinic($companyId))return false;
    $u=current_user();if(!$u)return false;
    if(is_super_admin())return false; // denúncia libera somente visualização; não edição
    if(is_role('sub_admin'))return false;
    $linked=resolve_company_id_for_user($u);
    return (string)($u['role']??'')==='company'&&$linked===$companyId&&clinic_company_owner_user_id($companyId)===(int)$u['id'];
}
function must_manage_clinic(int $companyId): void { if(!can_manage_clinic_company($companyId))throw new RuntimeException('Sem permissão para administrar esta clínica.'); }
function can_manage_pet_company(int $companyId): bool { if($companyId<1||!company_is_petcare($companyId))return false;if(is_super_admin())return true;$u=current_user();$linked=resolve_company_id_for_user($u);return in_array((string)($u['role']??''),['company','employee'],true)&&$linked===$companyId; }
function must_manage_pet_company(int $companyId): void { if(!can_manage_pet_company($companyId))throw new RuntimeException('Sem permissão para administrar esta Veterinária / Pet Shop.'); }
function must_pet_belong_to_company(int $petId,int $companyId): array { $q=db()->prepare("SELECT p.*,pc.platform_user_id tutor_user_id FROM pets p JOIN pet_clients pc ON pc.id=p.client_id WHERE p.id=? AND p.company_id=? AND p.status='active' AND pc.status='active' LIMIT 1");$q->execute([$petId,$companyId]);$p=$q->fetch();if(!$p)throw new RuntimeException('Pet não encontrado nesta empresa.');return $p; }
function clinic_platform_user_by_token(string $token): ?array {
    $token=trim($token);if($token==='')return null;
    $q=db()->prepare("SELECT * FROM users WHERE upper(public_id)=upper(?) OR lower(email)=lower(?) OR id=? LIMIT 1");$q->execute([$token,$token,(int)$token]);$row=$q->fetch();return $row?:null;
}
function clinic_create_platform_user(string $role,string $name,string $email,string $phone,string $document,string $password,int $companyId=0): array {
    $email=strtolower(trim($email));if($name===''||!filter_var($email,FILTER_VALIDATE_EMAIL))throw new RuntimeException('Nome e e-mail válidos são obrigatórios para criar a conta na plataforma.');
    $q=db()->prepare("SELECT * FROM users WHERE lower(email)=lower(?) OR (COALESCE(document,'')<>'' AND document=?) LIMIT 1");$q->execute([$email,$document]);$exists=$q->fetch();
    if($exists)throw new RuntimeException('Já existe uma conta na plataforma com estes dados. Use o ID '.public_user_id($exists).' para vincular.');
    $allowed=['client','clinic_professional'];if(!in_array($role,$allowed,true))throw new RuntimeException('Perfil clínico inválido.');
    $userCompanyId=$role==='clinic_professional'?($companyId?:null):null;$segmentSlug=$role==='clinic_professional'?'clinic':'';db()->prepare('INSERT INTO users(company_id,role,name,email,phone,document,password_hash,status,work_status,profile_completed,segment_slug,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)')
      ->execute([$userCompanyId,$role,$name,$email,only_digits($phone),$document,password_hash($password?:'123456',PASSWORD_DEFAULT),'active','offline',1,$segmentSlug,now(),now()]);
    $id=(int)db()->lastInsertId();ensure_public_user_id($id);$q=db()->prepare('SELECT * FROM users WHERE id=?');$q->execute([$id]);return $q->fetch();
}

function linkexpress_backup_dir(): string {
    $dir=dirname(config('db_path')).'/backups';
    if(!is_dir($dir) && !mkdir($dir,0755,true) && !is_dir($dir)) throw new RuntimeException('Não foi possível criar a pasta de backups.');
    return $dir;
}
function linkexpress_restore_dir(): string {
    $dir=dirname(config('db_path')).'/restore';
    if(!is_dir($dir) && !mkdir($dir,0755,true) && !is_dir($dir)) throw new RuntimeException('Não foi possível criar a pasta temporária de restauração.');
    return $dir;
}
function create_database_snapshot(string $prefix='manual'): string {
    $source=config('db_path');
    if(!is_file($source) || !is_readable($source)) throw new RuntimeException('Banco SQLite não encontrado ou sem permissão de leitura.');
    $name=preg_replace('/[^a-z0-9_-]/i','_',$prefix).'-'.date('Y-m-d-His').'-'.bin2hex(random_bytes(3)).'.sqlite';
    $target=linkexpress_backup_dir().'/'.$name;
    @unlink($target);

    // Preferência: snapshot nativo consistente, inclusive quando o banco usa WAL.
    try {
        db()->exec('VACUUM INTO '.db()->quote($target));
    } catch(Throwable $vacuumError) {
        // Fallback para hospedagens com SQLite antigo.
        try { db()->exec('PRAGMA wal_checkpoint(FULL)'); } catch(Throwable $ignored) {}
        clearstatcache(true,$source);
        if(!copy($source,$target)) throw new RuntimeException('Não foi possível criar o backup no servidor.');
    }

    clearstatcache(true,$target);
    if(!is_file($target) || filesize($target)<16){@unlink($target);throw new RuntimeException('O backup gerado ficou vazio ou inválido.');}
    $fh=@fopen($target,'rb');$signature=$fh?fread($fh,16):'';if(is_resource($fh))fclose($fh);
    if($signature!=="SQLite format 3\000"){@unlink($target);throw new RuntimeException('Falha ao validar o backup SQLite gerado.');}
    @chmod($target,0640);
    return $target;
}
function validate_sqlite_backup_file(string $path): array {
    if(!is_file($path) || filesize($path)<100) throw new RuntimeException('O arquivo de backup está vazio ou inválido.');
    $handle=fopen($path,'rb');$header=$handle?fread($handle,16):'';if(is_resource($handle))fclose($handle);
    if($header!=="SQLite format 3\000") throw new RuntimeException('O arquivo enviado não é um banco SQLite válido.');
    $test=new PDO('sqlite:'.$path);
    $test->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
    $integrity=(string)$test->query('PRAGMA integrity_check')->fetchColumn();
    if(strtolower(trim($integrity))!=='ok') throw new RuntimeException('O backup falhou na verificação de integridade: '.$integrity);
    $tables=$test->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(PDO::FETCH_COLUMN);
    foreach(['users','companies','settings'] as $required){
        if(!in_array($required,$tables,true)) throw new RuntimeException('Backup incompatível: tabela obrigatória ausente ('.$required.').');
    }
    $admins=(int)$test->query("SELECT COUNT(*) FROM users WHERE role='super_admin'")->fetchColumn();
    if($admins<1) throw new RuntimeException('O backup não possui uma conta Administrador Sênior e não pode ser restaurado.');
    $test=null;
    return ['integrity'=>$integrity,'admins'=>$admins,'tables'=>count($tables)];
}

try {
    switch ($action) {
        case 'update_work_status':
            $allowed=['available','busy','offline','on_route'];
            $status=request('work_status');
            if(!in_array($status,$allowed,true)) throw new RuntimeException('Status inválido.');
            db()->prepare('UPDATE users SET work_status=?,updated_at=? WHERE id=?')->execute([$status,now(),$user['id']]);
            $returnPage=(string)request('return_page','home');
            if(!in_array($returnPage,['home','gas_courier','deliveries'],true))$returnPage='home';
            back_to($returnPage,'Status atualizado.');
            break;


        case 'request_delivery_mode':
            must_role('mototaxi');
            if(user_can_work_deliveries($user))throw new RuntimeException('Seu acesso à modalidade Entregas já está liberado.');
            $pending=db()->prepare("SELECT id FROM delivery_mode_requests WHERE user_id=? AND status='pending' LIMIT 1");$pending->execute([$user['id']]);
            if($pending->fetchColumn())throw new RuntimeException('Sua solicitação de Entregas já está aguardando análise.');
            db()->prepare('INSERT INTO delivery_mode_requests(user_id,status,notes,created_at,updated_at) VALUES(?,"pending",?,?,?)')->execute([$user['id'],trim((string)request('notes')),now(),now()]);
            $requestId=(int)db()->lastInsertId();
            notify_super_admins('Mototaxista solicitou acesso a Entregas',$user['name'].' pediu liberação para também receber trabalhos de entrega.','warning',0,'dashboard.php?page=users','delivery_mode_request',$requestId);
            log_activity('request_delivery_mode','delivery_mode_requests',$requestId,$user['name']);
            back_to('deliveries','Solicitação enviada ao administrador.');
            break;

        case 'review_delivery_mode_request':
            must_admin();$id=(int)request('id',0);$decision=request('decision');if(!in_array($decision,['approved','rejected'],true))throw new RuntimeException('Decisão inválida.');
            $q=db()->prepare("SELECT r.*,u.name,u.role FROM delivery_mode_requests r JOIN users u ON u.id=r.user_id WHERE r.id=? AND r.status='pending'");$q->execute([$id]);$req=$q->fetch();if(!$req)throw new RuntimeException('Solicitação não encontrada ou já analisada.');
            if($req['role']!=='mototaxi')throw new RuntimeException('A solicitação não pertence a um mototaxista.');
            db()->beginTransaction();
            try{
                db()->prepare('UPDATE delivery_mode_requests SET status=?,admin_notes=?,reviewed_by=?,reviewed_at=?,updated_at=? WHERE id=?')->execute([$decision,trim((string)request('admin_notes')),$user['id'],now(),now(),$id]);
                db()->prepare('UPDATE users SET delivery_mode_enabled=?,updated_at=? WHERE id=?')->execute([$decision==='approved'?1:0,now(),$req['user_id']]);
                db()->commit();
            }catch(Throwable $e){if(db()->inTransaction())db()->rollBack();throw $e;}
            create_notification((int)$req['user_id'],$decision==='approved'?'Entregas liberadas':'Solicitação de Entregas recusada',$decision==='approved'?'Você já pode ser contratado por empresas para realizar entregas.':'O administrador não liberou a modalidade Entregas neste momento.',$decision==='approved'?'success':'warning','dashboard.php?page=deliveries','delivery_mode_request',$id);
            log_activity('review_delivery_mode_request','delivery_mode_requests',$id,$decision);
            back_to('users',$decision==='approved'?'Mototaxista liberado para Entregas.':'Solicitação recusada.');
            break;

        case 'save_platform_term':
            must_admin();
            $id=(int)request('id',0);$title=trim((string)request('title'));$content=trim((string)request('content'));
            if($title===''||$content==='')throw new RuntimeException('Informe o título e o conteúdo do documento.');
            $required=(int)request('required',1)===1?1:0;$status=request('status','active');if(!in_array($status,['active','inactive'],true))$status='active';$sort=(int)request('sort_order',0);
            if($id){
                $q=db()->prepare('SELECT * FROM platform_terms WHERE id=?');$q->execute([$id]);$old=$q->fetch();if(!$old)throw new RuntimeException('Documento não encontrado.');
                $version=(int)$old['version'];if(trim((string)$old['title'])!==$title||trim((string)$old['content'])!==$content)$version++;
                db()->prepare('UPDATE platform_terms SET title=?,content=?,version=?,required=?,status=?,sort_order=?,updated_at=? WHERE id=?')->execute([$title,$content,$version,$required,$status,$sort,now(),$id]);
            }else{
                db()->prepare('INSERT INTO platform_terms(title,content,version,required,status,sort_order,created_at,updated_at) VALUES(?,?,1,?,?,?,?,?)')->execute([$title,$content,$required,$status,$sort,now(),now()]);$id=(int)db()->lastInsertId();
            }
            log_activity('save_platform_term','platform_terms',$id,$title);back_to('terms_policies','Termo/política salvo.');
            break;

        case 'delete_platform_term':
            must_admin();$id=(int)request('id',0);$q=db()->prepare('SELECT title FROM platform_terms WHERE id=?');$q->execute([$id]);$title=$q->fetchColumn();if(!$title)throw new RuntimeException('Documento não encontrado.');
            $a=db()->prepare('SELECT COUNT(*) FROM term_acceptances WHERE term_id=?');$a->execute([$id]);
            if((int)$a->fetchColumn()>0)db()->prepare("UPDATE platform_terms SET status='inactive',updated_at=? WHERE id=?")->execute([now(),$id]);
            else db()->prepare('DELETE FROM platform_terms WHERE id=?')->execute([$id]);
            log_activity('delete_platform_term','platform_terms',$id,(string)$title);back_to('terms_policies','Documento removido da área pública.');
            break;

        case 'save_settings':
            must_admin();
            $keys=['company_name','support_email','pix_key','whatsapp','platform_city','platform_state','primary_color','secondary_color','allow_public_registration','maintenance_mode','ride_auto_fare_enabled','ride_price_km','ride_base_fare','ride_min_fare','moving_price_km','moving_floor_fee','moving_helper_fee','moving_assembly_fee','moving_packaging_fee','subscription_payment_link','platform_service_payment_link','service_provider_radius_km','wallet_min_withdrawal','android_apk_label'];
            foreach($keys as $key){
                $value=request($key,setting($key,''));
                if($key==='platform_state')$value=normalize_state($value);
                if(in_array($key,['subscription_payment_link','platform_service_payment_link'],true)&&trim((string)$value)!==''&&!filter_var(trim((string)$value),FILTER_VALIDATE_URL))throw new RuntimeException('Informe uma URL válida para o link de pagamento.');
                save_setting($key,(string)$value);
            }
            $oldLogo=(string)setting('brand_logo','');
            if(request('remove_brand_logo')){delete_local_upload($oldLogo);save_setting('brand_logo','');}
            else{
                $newLogo=upload_image('brand_logo_file','marca',$oldLogo,6291456);
                if($newLogo!==$oldLogo)save_setting('brand_logo',$newLogo);
            }
            $oldApk=(string)setting('android_apk_path','');
            if(request('remove_android_apk')){
                delete_local_upload($oldApk);save_setting('android_apk_path','');save_setting('android_apk_updated_at','');
            }else{
                $newApk=upload_android_apk('android_apk_file',$oldApk);
                if($newApk!==$oldApk){save_setting('android_apk_path',$newApk);save_setting('android_apk_updated_at',now());}
            }
            log_activity('save_settings','settings',null,'Identidade e configurações atualizadas');
            back_to('settings','Configurações e identidade visual atualizadas.');
            break;

        case 'save_home_content':
            must_super_admin();
            $keys=['home_badge','home_title_white','home_title_accent','home_description','home_primary_cta_label','home_primary_cta_link','home_secondary_cta_label','home_secondary_cta_link','home_process_heading','home_step_1_title','home_step_1_text','home_step_2_title','home_step_2_text','home_step_3_title','home_step_3_text','home_step_4_title','home_step_4_text'];
            foreach($keys as $key){$value=trim((string)request($key));if(str_ends_with($key,'_link')&&$value!==''&&!filter_var($value,FILTER_VALIDATE_URL)&&!preg_match('~^(?:[a-z0-9_-]+\.php(?:[?#].*)?|#[a-z0-9_-]+)$~i',$value))throw new RuntimeException('Link inválido em '.str_replace('_',' ',$key).'.');save_setting($key,$value);}
            log_activity('save_home_content','settings',null,'Conteúdo público atualizado');back_to('home_content','Tela inicial atualizada.');
            break;

        case 'save_publication_limits':
            must_admin();
            save_setting('max_products_per_company',(string)max(0,(int)request('max_products_per_company',0)));
            save_setting('max_publications_per_user',(string)max(0,(int)request('max_publications_per_user',0)));
            log_activity('save_publication_limits','settings',null,'Limites de produtos e publicações atualizados');
            back_to('settings','Limites de publicações atualizados.');
            break;

        case 'save_document_requirement':
            must_admin();
            $id=(int)request('id',0);
            $name=trim(request('name'));
            $targetRole=trim(request('target_role','all'));
            $targetUserId=(int)request('target_user_id',0);
            $allowedRoles=['all','company','client','courier','motofreight','mototaxi','taxi','private_driver','service_provider','clinic_professional','property_owner','rental_company','employee','moving_provider','shuttle_driver'];
            if(!in_array($targetRole,$allowedRoles,true)) throw new RuntimeException('Perfil inválido para a exigência.');
            if($targetUserId>0){$uq=db()->prepare("SELECT id,role FROM users WHERE id=? AND role NOT IN ('super_admin','sub_admin') LIMIT 1");$uq->execute([$targetUserId]);$targetAccount=$uq->fetch();if(!$targetAccount)throw new RuntimeException('Usuário específico não encontrado.');$targetRole='all';}
            if($name==='') throw new RuntimeException('Informe o nome do documento exigido.');
            $description=trim(request('description'));
            $blocks=(int)request('blocks_access',0)===1?1:0;
            $sort=(int)request('sort_order',0);
            $status=request('status','active');
            if(!in_array($status,['active','inactive'],true))$status='active';
            if($id){
                db()->prepare('UPDATE document_requirements SET target_role=?,target_user_id=?,name=?,description=?,blocks_access=?,sort_order=?,status=?,updated_at=? WHERE id=?')
                    ->execute([$targetRole,$targetUserId?:null,$name,$description,$blocks,$sort,$status,now(),$id]);
            }else{
                db()->prepare('INSERT INTO document_requirements(target_role,target_user_id,name,description,blocks_access,sort_order,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)')
                    ->execute([$targetRole,$targetUserId?:null,$name,$description,$blocks,$sort,$status,now(),now()]);
                $id=(int)db()->lastInsertId();
            }
            if($targetUserId>0&&$status==='active'&&$blocks===1){
                db()->prepare('UPDATE users SET admin_require_documents=1,updated_at=? WHERE id=?')->execute([now(),$targetUserId]);
                create_notification($targetUserId,'Documentos obrigatórios','A administração solicitou o envio de “'.$name.'”. Envie o arquivo para liberar o acesso.','warning','dashboard.php?page=documents','document_requirement',$id);
            }
            log_activity('save_document_requirement','document_requirements',$id,$name);
            back_to('documents','Exigência de documento salva.');
            break;

        case 'delete_document_requirement':
            must_admin();
            $id=(int)request('id',0);
            $q=db()->prepare('SELECT name,target_user_id FROM document_requirements WHERE id=?');$q->execute([$id]);$requirementRow=$q->fetch();$name=$requirementRow['name']??'';$targetUserId=(int)($requirementRow['target_user_id']??0);
            if(!$name)throw new RuntimeException('Exigência não encontrada.');
            $files=db()->prepare('SELECT file_path FROM user_documents WHERE requirement_id=?');$files->execute([$id]);
            foreach($files->fetchAll() as $fileRow){delete_local_upload((string)($fileRow['file_path']??''));}
            db()->prepare('DELETE FROM document_requirements WHERE id=?')->execute([$id]);
            if($targetUserId>0){$uq=db()->prepare('SELECT * FROM users WHERE id=?');$uq->execute([$targetUserId]);$targetUser=$uq->fetch();if($targetUser&&pending_required_documents_count($targetUser,false)===0)db()->prepare('UPDATE users SET admin_require_documents=0,updated_at=? WHERE id=?')->execute([now(),$targetUserId]);}
            log_activity('delete_document_requirement','document_requirements',$id,$name);
            back_to('documents','Exigência removida.');
            break;

        case 'upload_required_document':
            $requirementId=(int)request('requirement_id',0);
            $q=db()->prepare("SELECT * FROM document_requirements WHERE id=? AND status='active' AND (target_user_id=? OR (COALESCE(target_user_id,0)=0 AND (target_role='all' OR target_role=?)))");
            $q->execute([$requirementId,(int)$user['id'],$user['role']]);
            $req=$q->fetch();
            if(!$req)throw new RuntimeException('Este documento não é exigido para o seu perfil.');
            $existingQ=db()->prepare('SELECT * FROM user_documents WHERE user_id=? AND requirement_id=? ORDER BY id DESC LIMIT 1');
            $existingQ->execute([$user['id'],$requirementId]);$existing=$existingQ->fetch();
            $path=upload_document('document_file','documentos/usuarios/'.(int)$user['id'],$existing['file_path']??'');
            if($path==='')throw new RuntimeException('Selecione um arquivo PDF ou imagem.');
            if($existing){
                db()->prepare("UPDATE user_documents SET file_path=?,status='pending',notes=NULL,reviewer_id=NULL,reviewed_at=NULL,updated_at=? WHERE id=?")
                    ->execute([$path,now(),$existing['id']]);
                $docId=(int)$existing['id'];
            }else{
                db()->prepare("INSERT INTO user_documents(requirement_id,user_id,file_path,status,created_at,updated_at) VALUES(?,?,?,'pending',?,?)")
                    ->execute([$requirementId,$user['id'],$path,now(),now()]);
                $docId=(int)db()->lastInsertId();
            }
            notify_super_admins('Documento enviado',$user['name'].' enviou o documento obrigatório: '.$req['name'].'.','info',0,'dashboard.php?page=documents','user_document',$docId);
            create_notification((int)$user['id'],'Documento enviado','Seu documento “'.$req['name'].'” foi enviado para análise.','info','dashboard.php?page=documents','user_document',$docId);
            log_activity('upload_required_document','user_documents',$docId,$req['name']);
            back_to('documents','Documento enviado para análise.');
            break;

        case 'review_user_document':
            must_admin();
            $id=(int)request('id',0);
            $status=request('status');
            if(!in_array($status,['approved','rejected'],true))throw new RuntimeException('Situação inválida.');
            $notes=trim(request('notes'));
            $q=db()->prepare('SELECT ud.*,dr.name,u.name user_name FROM user_documents ud JOIN document_requirements dr ON dr.id=ud.requirement_id JOIN users u ON u.id=ud.user_id WHERE ud.id=?');
            $q->execute([$id]);$doc=$q->fetch();
            if(!$doc)throw new RuntimeException('Documento não encontrado.');
            db()->prepare('UPDATE user_documents SET status=?,notes=?,reviewer_id=?,reviewed_at=?,updated_at=? WHERE id=?')
                ->execute([$status,$notes,$user['id'],now(),now(),$id]);
            if($status==='approved'){
                $uq=db()->prepare('SELECT * FROM users WHERE id=?');$uq->execute([(int)$doc['user_id']]);$docUser=$uq->fetch();
                if($docUser&&!empty($docUser['admin_require_documents'])&&pending_required_documents_count($docUser,false)===0){db()->prepare('UPDATE users SET admin_require_documents=0,updated_at=? WHERE id=?')->execute([now(),(int)$doc['user_id']]);}
            }
            $msg=$status==='approved'?'Documento aprovado: ':'Documento recusado: ';
            create_notification((int)$doc['user_id'],$msg.$doc['name'],$notes?:($status==='approved'?'Documento validado pelo administrador.':'Envie um novo arquivo para análise.'),$status==='approved'?'success':'warning','dashboard.php?page=documents','user_document',$id);
            log_activity('review_user_document','user_documents',$id,$status);
            back_to('documents',$status==='approved'?'Documento aprovado.':'Documento recusado.');
            break;

        case 'save_user':
            must_admin();
            $id=(int)request('id',0);$wasNew=$id<1;$role=request('role');$name=request('name');$email=strtolower(request('email'));$phone=only_digits(request('phone'));$document=request('document');$status=request('status','pending');$companyId=(int)request('company_id',0);$expires=request('subscription_expires_at');$city=request('city');$state=normalize_state(request('state'));$instagram=normalize_social_handle(request('instagram'));$telegram=normalize_social_handle(request('telegram'));
            $allowedRoles=['super_admin','sub_admin','company','client','courier','motofreight','mototaxi','taxi','private_driver','service_provider','clinic_professional','property_owner','rental_company','moving_provider','shuttle_driver','employee'];
            if(!in_array($role,$allowedRoles,true))throw new RuntimeException('Perfil inválido.');
            $existingTarget=null;if($id){$tq=db()->prepare('SELECT id,role,status,admin_require_documents,admin_require_profile FROM users WHERE id=?');$tq->execute([$id]);$existingTarget=$tq->fetch();if(!$existingTarget)throw new RuntimeException('Usuário não encontrado.');}
            if(is_role('sub_admin')){if(in_array($role,['super_admin','sub_admin'],true)||($existingTarget&&in_array($existingTarget['role'],['super_admin','sub_admin'],true)))throw new RuntimeException('Administrador não pode criar ou alterar contas administrativas.');}
            $document=validate_document_or_throw($document,$id);
            if(!$name||!filter_var($email,FILTER_VALIDATE_EMAIL))throw new RuntimeException('Nome e e-mail válido são obrigatórios.');
            $walletEnabled=(int)request('wallet_enabled',0)===1?1:0;$pixBillingEnabled=(int)request('pix_billing_enabled',0)===1?1:0;$clientBillingEnabled=(int)request('client_billing_enabled',0)===1?1:0;$providerClientsEnabled=(int)request('provider_clients_enabled',0)===1?1:0;$providerClientLimit=max(0,(int)request('provider_client_limit',0));$deliveryModeEnabled=(int)request('delivery_mode_enabled',0)===1?1:0;
            $identityUnlocked=(int)request('identity_edit_unlocked',0)===1?1:0;$newAreaEnabled=(int)request('new_area_enabled',0)===1?1:0;$platformLinksEnabled=(int)request('platform_links_enabled',0)===1?1:0;$ownReceivingEnabled=(int)request('own_receiving_enabled',0)===1?1:0;
            $requireDocuments=(int)request('admin_require_documents',0)===1?1:0;$requireProfile=(int)request('admin_require_profile',0)===1?1:0;
            $billingMode=(string)request('platform_billing_mode','transaction');if(!in_array($billingMode,['monthly','transaction'],true))$billingMode='transaction';
            $feePayer=(string)request('fee_payer_preference','client');if(!in_array($feePayer,['client','provider'],true))$feePayer='client';
            if($role!=='service_provider'){$providerClientsEnabled=0;$providerClientLimit=0;}if($role!=='mototaxi')$deliveryModeEnabled=0;
            if($id){
                $stmt=db()->prepare('UPDATE users SET company_id=?,role=?,name=?,email=?,phone=?,document=?,status=?,subscription_expires_at=?,city=?,state=?,instagram=?,telegram=?,wallet_enabled=?,pix_billing_enabled=?,client_billing_enabled=?,provider_clients_enabled=?,provider_client_limit=?,delivery_mode_enabled=?,identity_edit_unlocked=?,new_area_enabled=?,platform_links_enabled=?,own_receiving_enabled=?,platform_billing_mode=?,fee_payer_preference=?,admin_require_documents=?,admin_require_profile=?,profile_completed=1,updated_at=? WHERE id=?');
                $stmt->execute([$companyId?:null,$role,$name,$email,$phone,$document,$status,$expires?:null,$city,$state,$instagram,$telegram,$walletEnabled,$pixBillingEnabled,$clientBillingEnabled,$providerClientsEnabled,$providerClientLimit,$deliveryModeEnabled,$identityUnlocked,$newAreaEnabled,$platformLinksEnabled,$ownReceivingEnabled,$billingMode,$feePayer,$requireDocuments,$requireProfile,now(),$id]);
                if(request('password'))db()->prepare('UPDATE users SET password_hash=? WHERE id=?')->execute([password_hash(request('password'),PASSWORD_DEFAULT),$id]);
            }else{
                $password=request('password')?:'123456';
                db()->prepare('INSERT INTO users(company_id,role,name,email,phone,document,password_hash,status,work_status,subscription_expires_at,city,state,instagram,telegram,wallet_enabled,pix_billing_enabled,client_billing_enabled,provider_clients_enabled,provider_client_limit,delivery_mode_enabled,identity_edit_unlocked,new_area_enabled,platform_links_enabled,own_receiving_enabled,platform_billing_mode,fee_payer_preference,admin_require_documents,admin_require_profile,profile_completed,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
                  ->execute([$companyId?:null,$role,$name,$email,$phone,$document,password_hash($password,PASSWORD_DEFAULT),$status,'offline',$expires?:null,$city,$state,$instagram,$telegram,$walletEnabled,$pixBillingEnabled,$clientBillingEnabled,$providerClientsEnabled,$providerClientLimit,$deliveryModeEnabled,$identityUnlocked,$newAreaEnabled,$platformLinksEnabled,$ownReceivingEnabled,$billingMode,$feePayer,$requireDocuments,$requireProfile,1,now()]);
                $id=(int)db()->lastInsertId();
            }
            // Rastreia qual administrador cadastrou/ativou a conta e guarda liberações especiais do Sub Admin.
            if($wasNew){db()->prepare('UPDATE users SET created_by_admin_id=?,activated_by_admin_id=CASE WHEN status=\"active\" THEN ? ELSE activated_by_admin_id END,updated_at=? WHERE id=?')->execute([(int)$user['id'],(int)$user['id'],now(),$id]);}
            elseif(($existingTarget['status']??'')!=='active'&&$status==='active'){db()->prepare('UPDATE users SET activated_by_admin_id=?,updated_at=? WHERE id=?')->execute([(int)$user['id'],now(),$id]);}
            if(is_super_admin()){
                $allowedSubModules=subadmin_sensitive_modules();$requestedModules=(array)request('subadmin_modules',[]);$granted=[];
                foreach($requestedModules as $module){$module=preg_replace('/[^a-z_]/','',(string)$module);if(in_array($module,$allowedSubModules,true))$granted[]=$module;}
                $permissions=$role==='sub_admin'?json_encode(array_values(array_unique($granted)),JSON_UNESCAPED_UNICODE):'';
                db()->prepare('UPDATE users SET admin_module_permissions=?,updated_at=? WHERE id=?')->execute([$permissions,now(),$id]);
            }
            ensure_public_user_id($id);
            // Exigências individuais: se a administração marcou documentação obrigatória e
            // não existe regra aplicável, cria uma solicitação genérica somente para este usuário.
            if($requireDocuments){
                $rq=db()->prepare("SELECT COUNT(*) FROM document_requirements WHERE status='active' AND (target_user_id=? OR (COALESCE(target_user_id,0)=0 AND (target_role='all' OR target_role=?)))");$rq->execute([$id,$role]);
                if((int)$rq->fetchColumn()===0){
                    db()->prepare("INSERT INTO document_requirements(target_role,target_user_id,name,description,blocks_access,sort_order,status,created_at,updated_at) VALUES('all',?,'Documentação solicitada pelo administrador','Envie um arquivo PDF ou imagem com a documentação solicitada pela administração. Se forem necessários documentos separados, o administrador poderá criar novas exigências específicas.',1,0,'active',?,?)")->execute([$id,now(),now()]);
                }
            }else{
                db()->prepare('UPDATE document_requirements SET blocks_access=0,updated_at=? WHERE target_user_id=?')->execute([now(),$id]);
            }
            $freshTargetQ=db()->prepare('SELECT * FROM users WHERE id=?');$freshTargetQ->execute([$id]);$freshTarget=$freshTargetQ->fetch();
            if($freshTarget&&!empty($freshTarget['admin_require_profile'])&&count(user_profile_required_fields_missing($freshTarget))===0){db()->prepare('UPDATE users SET admin_require_profile=0,updated_at=? WHERE id=?')->execute([now(),$id]);$requireProfile=0;}
            if($id>0&&$requireDocuments&&!($existingTarget['admin_require_documents']??0))create_notification($id,'Envio de documentos obrigatório','A administração solicitou documentos para manter o acesso à plataforma. Entre em Documentos e envie os arquivos solicitados.','warning','dashboard.php?page=documents','user',$id);
            if($id>0&&$requireProfile&&!($existingTarget['admin_require_profile']??0))create_notification($id,'Complete seu cadastro','A administração solicitou que você complete nome, CPF/CNPJ e endereço antes de continuar usando os demais módulos.','warning','dashboard.php?page=profile','user',$id);
            if($role==='shuttle_driver'){$approval=$status==='active'?'approved':($status==='blocked'?'blocked':'pending');db()->prepare("INSERT INTO shuttle_driver_profiles(user_id,approval_status,approved_by,approved_at,created_at,updated_at) VALUES(?,?,?,?,?,?) ON CONFLICT(user_id) DO UPDATE SET approval_status=excluded.approval_status,approved_by=excluded.approved_by,approved_at=excluded.approved_at,updated_at=excluded.updated_at")->execute([$id,$approval,$approval==='approved'?(int)$user['id']:null,$approval==='approved'?now():null,now(),now()]);}
            if($companyId>0&&in_array($role,['company','rental_company'],true)){$q=db()->prepare('SELECT owner_user_id FROM companies WHERE id=?');$q->execute([$companyId]);$existingOwner=(int)$q->fetchColumn();if($existingOwner<=0||$existingOwner===$id)db()->prepare('UPDATE companies SET owner_user_id=?,updated_at=? WHERE id=?')->execute([$id,now(),$companyId]);}
            log_activity($wasNew?'create_user':'save_user','users',$id,$name);
            back_to('users','Usuário salvo com sucesso.');
            break;

        case 'toggle_user':
            must_admin();
            $id=(int)request('id');
            if($id===$user['id']) throw new RuntimeException('Você não pode bloquear sua própria conta.');
            $current=db()->prepare('SELECT status,role FROM users WHERE id=?');$current->execute([$id]);$currentRow=$current->fetch();if(!$currentRow)throw new RuntimeException('Usuário não encontrado.');
            if(is_role('sub_admin')&&in_array($currentRow['role'],['super_admin','sub_admin'],true))throw new RuntimeException('Administrador não pode alterar contas administrativas.');
            if($currentRow['role']==='super_admin')throw new RuntimeException('A conta Administrador Sênior principal é protegida.');
            $status=$currentRow['status']??'pending';$newStatus=$status==='active'?'blocked':'active';
            db()->prepare('UPDATE users SET status=?,activated_by_admin_id=CASE WHEN ?="active" THEN ? ELSE activated_by_admin_id END,updated_at=? WHERE id=?')->execute([$newStatus,$newStatus,(int)$user['id'],now(),$id]);
            if(($currentRow['role']??'')==='shuttle_driver'){
                $approval=$newStatus==='active'?'approved':'blocked';
                db()->prepare("INSERT INTO shuttle_driver_profiles(user_id,approval_status,approved_by,approved_at,created_at,updated_at) VALUES(?,?,?,?,?,?) ON CONFLICT(user_id) DO UPDATE SET approval_status=excluded.approval_status,approved_by=excluded.approved_by,approved_at=excluded.approved_at,updated_at=excluded.updated_at")
                  ->execute([$id,$approval,$approval==='approved'?(int)$user['id']:null,$approval==='approved'?now():null,now(),now()]);
            }
            log_activity('toggle_user','users',$id,$status);
            back_to('users','Status do usuário atualizado.');
            break;

        case 'delete_user':
            must_admin();if(strtoupper(trim((string)request('confirmed_delete_phrase')))!=='EXCLUIR')throw new RuntimeException('Confirmação de exclusão inválida. Digite EXCLUIR para apagar a conta.');$id=(int)request('id');
            if($id===(int)$user['id'])throw new RuntimeException('Você não pode apagar sua própria conta.');
            $q=db()->prepare('SELECT role,name,email FROM users WHERE id=?');$q->execute([$id]);$target=$q->fetch();
            if(!$target)throw new RuntimeException('Usuário não encontrado.');
            if($target['role']==='super_admin')throw new RuntimeException('A conta Administrador Sênior principal é protegida e não pode ser apagada.');
            if(is_role('sub_admin')&&$target['role']==='sub_admin')throw new RuntimeException('Administrador não pode apagar outro administrador.');
            db()->beginTransaction();
            foreach(['notifications','tickets','favorite_addresses','charges','user_ui_state','wallet_transactions','withdrawal_requests'] as $table){db()->prepare('DELETE FROM '.$table.' WHERE user_id=?')->execute([$id]);}db()->prepare('DELETE FROM wallet_accounts WHERE user_id=?')->execute([$id]);db()->prepare('DELETE FROM provider_clients WHERE provider_user_id=? OR client_user_id=?')->execute([$id,$id]);db()->prepare("DELETE FROM platform_fee_rules WHERE scope_type='user' AND scope_value=?")->execute([(string)$id]);db()->prepare('DELETE FROM favorite_drivers WHERE user_id=? OR driver_id=?')->execute([$id,$id]);
            db()->prepare('UPDATE account_deletion_requests SET status="resolved",updated_at=?,resolved_at=?,resolved_by=? WHERE user_id=? AND request_type="user" AND status="pending"')->execute([now(),now(),(int)$user['id'],$id]);
            db()->prepare('DELETE FROM users WHERE id=?')->execute([$id]);db()->commit();
            log_activity('delete_user','users',$id,$target['name']);back_to('users','Usuário apagado com sucesso.');
            break;

        case 'save_company':
            if(!is_admin() && !is_role(['company','rental_company'])) throw new RuntimeException('Sem permissão.');
            $id=(int)request('id',0);
            if(!is_admin()){
                $id=(int)($user['company_id']??0);
                if($id<=0){
                    $ownerCompany=db()->prepare('SELECT id FROM companies WHERE owner_user_id=? ORDER BY id DESC LIMIT 1');
                    $ownerCompany->execute([(int)$user['id']]);
                    $id=(int)$ownerCompany->fetchColumn();
                    if($id>0){
                        db()->prepare('UPDATE users SET company_id=?,updated_at=? WHERE id=?')->execute([$id,now(),(int)$user['id']]);
                        $user['company_id']=$id;
                    }
                }
                if($id<=0) throw new RuntimeException('Nenhuma empresa está vinculada a esta conta.');
            }
            $existingCompany=null;if($id){$cx=db()->prepare('SELECT plan_id,status,logo,banner,verified,premium,response_minutes,completed_orders,allow_self_edit,client_limit,employee_limit,courier_limit,new_area_enabled,identity_edit_unlocked,name,document FROM companies WHERE id=?');$cx->execute([$id]);$existingCompany=$cx->fetch();}
            if(!is_admin() && $existingCompany && (int)($existingCompany['allow_self_edit']??1)!==1) throw new RuntimeException('Edição das informações da empresa está bloqueada pelo administrador.');
            $oldLogo=$existingCompany?(string)($existingCompany['logo']??''):'';
            $oldBanner=$existingCompany?(string)($existingCompany['banner']??''):'';
            if(is_admin()&&request('remove_logo')){delete_local_upload($oldLogo);$oldLogo='';}
            if(is_admin()&&request('remove_banner')){delete_local_upload($oldBanner);$oldBanner='';}
            $logo=upload_image('logo_file','empresas/logos',$oldLogo);
            $banner=upload_image('banner_file','empresas/banners',$oldBanner);
            $companyIdentityUnlocked=is_admin()||!$existingCompany||(int)($existingCompany['identity_edit_unlocked']??0)===1;
            $companyName=$companyIdentityUnlocked?request('name'):(string)($existingCompany['name']??'');$companyDocument=$companyIdentityUnlocked?request('document'):(string)($existingCompany['document']??'');
            $data=[is_admin()?(request('plan_id')?:null):($existingCompany?$existingCompany['plan_id']:null),request('type','delivery'),$companyName,request('slug'),$companyDocument,only_digits(request('phone')),request('email'),request('address'),request('city'),normalize_state(request('state')),$logo,$banner,request('description'),request('open_status','open'),is_admin()?request('status','pending'):($existingCompany?$existingCompany['status']:'pending'),is_admin()?(int)request('verified',0):(int)($existingCompany['verified']??0),is_admin()?(int)request('premium',0):(int)($existingCompany['premium']??0),is_admin()?(int)request('response_minutes',60):(int)($existingCompany['response_minutes']??60),is_admin()?(int)request('completed_orders',0):(int)($existingCompany['completed_orders']??0),(float)request('latitude')?:null,(float)request('longitude')?:null,request('sector')?:request('type','delivery')];
            if(!$data[2]) throw new RuntimeException('Informe o nome da empresa.');
            if($id){
                $stmt=db()->prepare('UPDATE companies SET plan_id=?,type=?,name=?,slug=?,document=?,phone=?,email=?,address=?,city=?,state=?,logo=?,banner=?,description=?,open_status=?,status=?,verified=?,premium=?,response_minutes=?,completed_orders=?,latitude=?,longitude=?,sector=?,updated_at=? WHERE id=?');
                $stmt->execute(array_merge($data,[now(),$id]));
                if(is_admin()&&($existingCompany['status']??'')!=='active'&&$data[14]==='active')db()->prepare('UPDATE companies SET activated_by_admin_id=?,updated_at=? WHERE id=?')->execute([(int)$user['id'],now(),$id]);
            }else{
                must_admin();
                $stmt=db()->prepare('INSERT INTO companies(plan_id,type,name,slug,document,phone,email,address,city,state,logo,banner,description,open_status,status,verified,premium,response_minutes,completed_orders,latitude,longitude,sector,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)');
                $stmt->execute(array_merge($data,[now(),now()]));$id=(int)db()->lastInsertId();
                db()->prepare('UPDATE companies SET created_by_admin_id=?,activated_by_admin_id=CASE WHEN status="active" THEN ? ELSE activated_by_admin_id END,updated_at=? WHERE id=?')->execute([(int)$user['id'],(int)$user['id'],now(),$id]);
            }
            db()->prepare('UPDATE companies SET instagram=?,telegram=?,address_number=?,allow_self_edit=?,client_limit=?,employee_limit=?,courier_limit=?,new_area_enabled=?,identity_edit_unlocked=?,updated_at=? WHERE id=?')->execute([normalize_social_handle(request('instagram')),normalize_social_handle(request('telegram')),request('address_number'),is_admin()?(int)request('allow_self_edit',1):(int)($existingCompany['allow_self_edit']??1),is_admin()?max(0,(int)request('client_limit',0)):max(0,(int)($existingCompany['client_limit']??0)),is_admin()?max(0,(int)request('employee_limit',0)):max(0,(int)($existingCompany['employee_limit']??0)),is_admin()?max(0,(int)request('courier_limit',0)):max(0,(int)($existingCompany['courier_limit']??0)),is_admin()?((int)request('new_area_enabled',0)===1?1:0):(int)($existingCompany['new_area_enabled']??0),is_admin()?((int)request('identity_edit_unlocked',0)===1?1:0):0,now(),$id]);
            $pickupEnabled=(int)request('pickup_scheduling_enabled',0)===1?1:0;
            $pickupDays=request('pickup_weekdays',[]);if(!is_array($pickupDays))$pickupDays=[];$pickupDays=array_values(array_unique(array_filter(array_map('intval',$pickupDays),static fn($d)=>$d>=0&&$d<=6)));sort($pickupDays);if(!$pickupDays)$pickupDays=[1,2,3,4,5,6];
            $pickupStart=trim((string)request('pickup_start_time','08:00'))?:'08:00';$pickupEnd=trim((string)request('pickup_end_time','18:00'))?:'18:00';
            $pickupInterval=(int)request('pickup_interval_minutes',30);if(!in_array($pickupInterval,[15,20,30,45,60,90,120],true))$pickupInterval=30;
            if($pickupEnabled&&(strtotime('2000-01-01 '.$pickupStart)===false||strtotime('2000-01-01 '.$pickupEnd)===false||strtotime('2000-01-01 '.$pickupStart)>=strtotime('2000-01-01 '.$pickupEnd)))throw new RuntimeException('Informe um horário válido para início e encerramento das retiradas.');
            db()->prepare('UPDATE companies SET pickup_scheduling_enabled=?,pickup_weekdays=?,pickup_start_time=?,pickup_end_time=?,pickup_interval_minutes=?,updated_at=? WHERE id=?')->execute([$pickupEnabled,implode(',',$pickupDays),$pickupStart,$pickupEnd,$pickupInterval,now(),$id]);
            if(is_admin()){
                $ownerUserId=(int)request('owner_user_id',0);
                db()->prepare('UPDATE companies SET owner_user_id=?,updated_at=? WHERE id=?')->execute([$ownerUserId?:null,now(),$id]);
                if($ownerUserId>0){
                    db()->prepare('UPDATE users SET company_id=?,updated_at=? WHERE id=?')->execute([$id,now(),$ownerUserId]);
                }
            }
            // Ao salvar/cadastrar um Lava-Jato, os 16 tipos padrão ficam disponíveis imediatamente para precificar/ativar.
            if(company_is_car_wash($id)) ensure_car_wash_default_services($id);
            log_activity('save_company','companies',$id,$data[2]);
            back_to('companies','Empresa salva com sucesso.');
            break;

        case 'save_company_pickup_schedule':
            if(!is_admin() && !is_role(['company','rental_company'])) throw new RuntimeException('Sem permissão.');
            $companyId=is_admin()?(int)request('company_id',0):resolve_company_id_for_user($user);
            if($companyId<=0) throw new RuntimeException('Empresa não encontrada.');
            if(!is_admin()){
                $cq=db()->prepare('SELECT id FROM companies WHERE id=? AND (owner_user_id=? OR id=?) LIMIT 1');
                $cq->execute([$companyId,(int)$user['id'],resolve_company_id_for_user($user)]);
                if(!(int)$cq->fetchColumn()) throw new RuntimeException('Esta empresa não pertence à sua conta.');
            }
            $pickupEnabled=(int)request('pickup_scheduling_enabled',0)===1?1:0;
            $pickupDays=request('pickup_weekdays',[]);if(!is_array($pickupDays))$pickupDays=[];
            $pickupDays=array_values(array_unique(array_filter(array_map('intval',$pickupDays),static fn($d)=>$d>=0&&$d<=6)));sort($pickupDays);
            if($pickupEnabled&&!$pickupDays) throw new RuntimeException('Selecione pelo menos um dia disponível para retirada.');
            if(!$pickupDays)$pickupDays=[1,2,3,4,5,6];
            $pickupStart=substr(trim((string)request('pickup_start_time','08:00')),0,5)?:'08:00';
            $pickupEnd=substr(trim((string)request('pickup_end_time','18:00')),0,5)?:'18:00';
            $pickupInterval=(int)request('pickup_interval_minutes',30);if(!in_array($pickupInterval,[15,20,30,45,60,90,120],true))$pickupInterval=30;
            $startTs=strtotime('2000-01-01 '.$pickupStart);$endTs=strtotime('2000-01-01 '.$pickupEnd);
            if($pickupEnabled&&(!$startTs||!$endTs||$startTs>=$endTs)) throw new RuntimeException('O horário de encerramento precisa ser depois do horário de início.');
            db()->prepare('UPDATE companies SET pickup_scheduling_enabled=?,pickup_weekdays=?,pickup_start_time=?,pickup_end_time=?,pickup_interval_minutes=?,updated_at=? WHERE id=?')->execute([$pickupEnabled,implode(',',$pickupDays),$pickupStart,$pickupEnd,$pickupInterval,now(),$companyId]);
            log_activity('save_company_pickup_schedule','companies',$companyId,$pickupEnabled?'Agenda de retirada ativada':'Agenda de retirada desativada');
            back_to('companies',$pickupEnabled?'Agenda de encomendas e retiradas salva.':'Agenda de retiradas desativada.');
            break;

        case 'toggle_company':
            must_admin();$id=(int)request('id');
            $q=db()->prepare('SELECT status FROM companies WHERE id=?');$q->execute([$id]);$status=$q->fetchColumn();
            $newCompanyStatus=$status==='active'?'blocked':'active';db()->prepare('UPDATE companies SET status=?,activated_by_admin_id=CASE WHEN ?="active" THEN ? ELSE activated_by_admin_id END,updated_at=? WHERE id=?')->execute([$newCompanyStatus,$newCompanyStatus,(int)$user['id'],now(),$id]);
            back_to('companies','Status da empresa atualizado.');
            break;

        case 'renew_user':
            must_admin();$id=(int)request('id',0);$days=max(1,min(3650,(int)request('days',30)));$return=request('return_page','users');
            $q=db()->prepare("SELECT name,role FROM users WHERE id=?");$q->execute([$id]);$target=$q->fetch();if(!$target||$target['role']==='super_admin'||(is_role('sub_admin')&&$target['role']==='sub_admin'))throw new RuntimeException('Usuário não encontrado ou conta administrativa protegida.');$name=$target['name'];
            db()->prepare("UPDATE users SET subscription_expires_at=CASE WHEN subscription_expires_at IS NULL OR subscription_expires_at<? THEN date(?, '+'||?||' days') ELSE date(subscription_expires_at, '+'||?||' days') END,status='active',activated_by_admin_id=?,blacklisted=0,blacklist_reason=NULL,blacklisted_at=NULL,provisional_access_until=NULL,updated_at=? WHERE id=?")->execute([today(),today(),$days,$days,(int)$user['id'],now(),$id]);
            $uq=db()->prepare('SELECT subscription_expires_at FROM users WHERE id=?');$uq->execute([$id]);$newEnd=(string)$uq->fetchColumn();
            db()->prepare("UPDATE subscriptions SET end_date=?,status='active',updated_at=? WHERE id=(SELECT id FROM subscriptions WHERE user_id=? ORDER BY id DESC LIMIT 1)")->execute([$newEnd,now(),$id]);
            db()->prepare("UPDATE charges SET status='cancelled',blocks_access=0,admin_notes=trim(COALESCE(admin_notes,'') || ' Acesso renovado manualmente pelo administrador.'),updated_at=? WHERE user_id=? AND blocks_access=1 AND status IN ('pending','overdue','rejected','proof_sent') AND (subscription_id IS NOT NULL OR lower(description) LIKE '%mensalidade%' OR lower(description) LIKE '%renova%')")->execute([now(),$id]);
            create_notification($id,'Acesso renovado','Seu acesso foi renovado por '.$days.' dia(s).','success','dashboard.php?page=subscriptions','subscription',0);log_activity('renew_user','users',$id,$days.' dias');back_to($return==='subscriptions'?'subscriptions':'users','Usuário renovado por '.$days.' dias.');
            break;

        case 'release_subscription_access':
            must_admin();$id=(int)request('user_id',0);$days=max(1,min(30,(int)request('days',7)));
            $q=db()->prepare("SELECT name,role FROM users WHERE id=?");$q->execute([$id]);$target=$q->fetch();if(!$target||$target['role']==='super_admin'||(is_role('sub_admin')&&$target['role']==='sub_admin'))throw new RuntimeException('Usuário não encontrado ou conta administrativa protegida.');$name=$target['name'];
            db()->prepare("UPDATE users SET provisional_access_until=datetime('now','+'||?||' days'),status='active',activated_by_admin_id=?,updated_at=? WHERE id=? AND blacklisted=0")->execute([$days,(int)$user['id'],now(),$id]);
            create_notification($id,'Acesso temporário liberado','O administrador liberou seu acesso por '.$days.' dia(s) enquanto a mensalidade é regularizada.','success','dashboard.php?page=subscriptions','subscription',0);
            log_activity('release_subscription_access','users',$id,$days.' dias');back_to('subscriptions','Acesso temporário liberado.');
            break;

        case 'toggle_user_blacklist':
            must_admin();$id=(int)request('id',0);$q=db()->prepare('SELECT blacklisted,name,role FROM users WHERE id=?');$q->execute([$id]);$row=$q->fetch();if(!$row||$row['role']==='super_admin'||(is_role('sub_admin')&&$row['role']==='sub_admin'))throw new RuntimeException('Usuário não encontrado ou conta administrativa protegida.');
            $new=(int)$row['blacklisted']?0:1;$reason=$new?(request('reason')?:'Bloqueado pelo administrador'):null;
            db()->prepare("UPDATE users SET blacklisted=?,blacklist_reason=?,blacklisted_at=?,status=CASE WHEN ?=1 THEN 'blocked' ELSE 'active' END,activated_by_admin_id=CASE WHEN ?=0 THEN ? ELSE activated_by_admin_id END,updated_at=? WHERE id=?")->execute([$new,$reason,$new?now():null,$new,$new,(int)$user['id'],now(),$id]);
            log_activity('toggle_user_blacklist','users',$id,$new?'Lista negra':'Removido da lista negra');back_to('users',$new?'Usuário incluído na lista negra.':'Usuário removido da lista negra.');
            break;

        case 'save_plan':
            must_admin();$id=(int)request('id',0);$features=array_values(array_filter(array_map('trim',explode("\n",request('features')))));
            $existingImage='';if($id){$iq=db()->prepare('SELECT image FROM plans WHERE id=?');$iq->execute([$id]);$existingImage=(string)$iq->fetchColumn();}
            if(request('remove_image')){delete_local_upload($existingImage);$existingImage='';}
            $image=upload_image('image_file','planos',$existingImage,8388608);
            $data=[request('name'),(float)str_replace(',','.',request('price',0)),(int)request('duration_days',30),json_encode($features,JSON_UNESCAPED_UNICODE),request('status','active'),$image];
            if(!$data[0]) throw new RuntimeException('Informe o nome do plano.');
            if($id) db()->prepare('UPDATE plans SET name=?,price=?,duration_days=?,features_json=?,status=?,image=? WHERE id=?')->execute(array_merge($data,[$id]));
            else {db()->prepare('INSERT INTO plans(name,price,duration_days,features_json,status,image,created_at) VALUES(?,?,?,?,?,?,?)')->execute(array_merge($data,[now()]));$id=(int)db()->lastInsertId();}
            log_activity('save_plan','plans',$id,$data[0]);back_to('plans','Plano e imagem salvos.');
            break;

        case 'submit_plan_request':
            if(is_admin()) throw new RuntimeException('Use a área administrativa de Mensalidades para cadastrar planos diretamente.');
            $planId=(int)request('plan_id',0);$pq=db()->prepare("SELECT * FROM plans WHERE id=? AND status='active' LIMIT 1");$pq->execute([$planId]);$plan=$pq->fetch();if(!$plan)throw new RuntimeException('Plano indisponível.');
            $dup=db()->prepare("SELECT COUNT(*) FROM plan_requests WHERE user_id=? AND target_plan_id=? AND status='pending'");$dup->execute([(int)$user['id'],$planId]);if((int)$dup->fetchColumn()>0)throw new RuntimeException('Você já possui uma solicitação pendente para este plano.');
            $current=db()->prepare("SELECT COUNT(*) FROM subscriptions WHERE user_id=? AND status IN ('active','pending','overdue','proof_sent')");$current->execute([(int)$user['id']]);$type=(int)$current->fetchColumn()>0?'change':'add';
            db()->prepare('INSERT INTO plan_requests(user_id,company_id,target_plan_id,request_type,status,user_notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)')->execute([(int)$user['id'],(int)($user['company_id']??0)?:null,$planId,$type,'pending',request('notes'),now(),now()]);$requestId=(int)db()->lastInsertId();
            notify_super_admins('Solicitação de plano',$user['name'].' solicitou '.($type==='change'?'alteração para o plano ':'o plano ').$plan['name'].'.','warning',(int)$user['id'],'dashboard.php?page=subscriptions','plan_request',$requestId);
            log_activity('submit_plan_request','plan_requests',$requestId,$plan['name']);back_to('subscriptions','Solicitação de plano enviada ao administrador.');break;

        case 'review_plan_request':
            must_admin();$id=(int)request('id');$decision=request('decision');if(!in_array($decision,['approved','rejected'],true))throw new RuntimeException('Decisão inválida.');
            $q=db()->prepare("SELECT pr.*,p.name plan_name,p.price,p.duration_days,u.name user_name,u.role user_role,u.status user_status,u.subscription_expires_at FROM plan_requests pr JOIN plans p ON p.id=pr.target_plan_id JOIN users u ON u.id=pr.user_id WHERE pr.id=? LIMIT 1");$q->execute([$id]);$pr=$q->fetch();if(!$pr)throw new RuntimeException('Solicitação não encontrada.');if(is_role('sub_admin')&&in_array(($pr['user_role']??''),['super_admin','sub_admin'],true))throw new RuntimeException('Administrador não pode alterar contas administrativas.');if($pr['status']!=='pending')throw new RuntimeException('Esta solicitação já foi analisada.');
            $adminNotes=request('admin_notes');$subscriptionId=null;
            if($decision==='approved'){
                $start=($pr['request_type']==='change'&&!empty($pr['subscription_expires_at'])&&$pr['subscription_expires_at']>today())?(string)$pr['subscription_expires_at']:today();$end=date('Y-m-d',strtotime($start.' +'.(int)$pr['duration_days'].' days'));$companyId=(int)($pr['company_id']??0);
                if($pr['request_type']==='change'){
                    $old=db()->prepare("SELECT id FROM subscriptions WHERE user_id=? AND status IN ('pending','overdue','proof_sent')");$old->execute([(int)$pr['user_id']]);$oldIds=$old->fetchAll(PDO::FETCH_COLUMN);
                    if($oldIds){$marks=implode(',',array_fill(0,count($oldIds),'?'));db()->prepare("UPDATE subscriptions SET status='cancelled',updated_at=? WHERE id IN ($marks)")->execute(array_merge([now()],$oldIds));db()->prepare("UPDATE charges SET status='cancelled',blocks_access=0,updated_at=? WHERE subscription_id IN ($marks)")->execute(array_merge([now()],$oldIds));}
                }
                db()->prepare('INSERT INTO subscriptions(user_id,company_id,plan_id,start_date,end_date,amount,payment_method,status,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)')->execute([(int)$pr['user_id'],$companyId?:null,(int)$pr['target_plan_id'],$start,$end,(float)$pr['price'],'pix','pending','Criada a partir da solicitação de plano #'.$id,now(),now()]);$subscriptionId=(int)db()->lastInsertId();
                $preferred=is_role('sub_admin')?null:preferred_plan_payment_link((int)$pr['target_plan_id']);$link=is_role('sub_admin')?'':(string)($preferred['url']??setting('subscription_payment_link',''));$pix=(string)($preferred['pix_key']??setting('pix_key',''));db()->prepare('INSERT INTO charges(issuer_user_id,user_id,amount,description,due_date,payment_link,pix_key_snapshot,payment_method,status,blocks_access,subscription_id,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([(int)$user['id'],(int)$pr['user_id'],(float)$pr['price'],'Mensalidade '.$pr['plan_name'],$start,$link,$pix,'pix','pending',1,$subscriptionId,now(),now()]);
                if($companyId)db()->prepare('UPDATE companies SET plan_id=?,updated_at=? WHERE id=?')->execute([(int)$pr['target_plan_id'],now(),$companyId]);
                create_notification((int)$pr['user_id'],'Plano aprovado','Sua solicitação do plano '.$pr['plan_name'].' foi aprovada. Abra Mensalidades para realizar o pagamento.','success','dashboard.php?page=subscriptions','plan_request',$id);
            }else{create_notification((int)$pr['user_id'],'Solicitação de plano recusada','A solicitação do plano '.$pr['plan_name'].' foi recusada pelo administrador.'.($adminNotes?' Motivo: '.$adminNotes:''),'warning','dashboard.php?page=subscriptions','plan_request',$id);}
            db()->prepare('UPDATE plan_requests SET status=?,admin_notes=?,reviewed_by=?,reviewed_at=?,subscription_id=?,updated_at=? WHERE id=?')->execute([$decision,$adminNotes,(int)$user['id'],now(),$subscriptionId,now(),$id]);
            log_activity('review_plan_request','plan_requests',$id,$decision.' • '.$pr['plan_name']);back_to('subscriptions',$decision==='approved'?'Plano aprovado e mensalidade criada.':'Solicitação de plano recusada.');break;

        case 'save_subscription':
            must_admin();
            $subscriptionId=(int)request('id',0);
            $wasEditing=$subscriptionId>0;
            $userId=(int)request('user_id',0);
            $companyId=(int)request('company_id',0);
            if($companyId>0){
                $billingUser=company_billing_user($companyId);
                if(!$billingUser) throw new RuntimeException('A empresa selecionada ainda não possui um usuário responsável/vinculado para receber a cobrança. Vincule um responsável na área Empresas.');
                $userId=(int)$billingUser['id'];
            }
            if($userId<1 && $companyId<1) throw new RuntimeException('Selecione um usuário ou uma empresa para registrar a mensalidade.');
            if(is_role('sub_admin')&&$userId>0){$guardQ=db()->prepare('SELECT role FROM users WHERE id=?');$guardQ->execute([$userId]);if(in_array((string)$guardQ->fetchColumn(),['super_admin','sub_admin'],true))throw new RuntimeException('Administrador não pode alterar mensalidades de contas administrativas.');}

            $planId=(int)request('plan_id');
            $start=trim((string)request('start_date',today()));
            $plan=db()->prepare('SELECT * FROM plans WHERE id=?');$plan->execute([$planId]);$p=$plan->fetch();
            if(!$p)throw new RuntimeException('Plano não encontrado.');
            if($start===''||strtotime($start)===false)throw new RuntimeException('Informe uma data de início válida.');
            $end=trim((string)request('end_date'))?:date('Y-m-d',strtotime($start.' +'.(int)$p['duration_days'].' days'));
            $amount=localized_decimal(request('amount',$p['price']));
            $method=request('payment_method','pix');
            $subStatus=request('status','active');
            $allowedSubStatus=['pending','overdue','active','cancelled','proof_sent'];
            if(!in_array($subStatus,$allowedSubStatus,true)) $subStatus='pending';
            $notes=request('notes');

            // V2.8.45 — primeiro vencimento, recorrência a cada 30 dias e link rápido.
            $paymentDate=trim((string)request('payment_date',$start));
            if($paymentDate===''||strtotime($paymentDate)===false)throw new RuntimeException('Informe uma data de pagamento válida.');
            $recurringEnabled=(int)request('recurring_enabled',0)===1?1:0;
            $paymentLinkId=is_role('sub_admin')?0:(int)request('payment_link_id',0);
            $planPreferred=preferred_plan_payment_link($planId);
            $link=is_role('sub_admin')?'':(string)($planPreferred['url']??setting('subscription_payment_link',''));
            $pix=(string)($planPreferred['pix_key']??setting('pix_key',''));
            if($paymentLinkId>0){
                $lq=db()->prepare("SELECT pl.*,ou.role owner_role FROM payment_links pl JOIN users ou ON ou.id=pl.owner_user_id WHERE pl.id=? AND pl.plan_id=? AND pl.status='active' AND ou.role='super_admin' LIMIT 1");
                $lq->execute([$paymentLinkId,$planId]);$selectedLink=$lq->fetch();
                if(!$selectedLink)throw new RuntimeException('O link selecionado não pertence a este plano ou não está mais disponível.');
                if(empty($selectedLink['url']))throw new RuntimeException('O atalho escolhido não possui URL de pagamento.');
                $link=(string)$selectedLink['url'];
                if(!empty($selectedLink['pix_key']))$pix=(string)$selectedLink['pix_key'];
            }
            $needsCharge=$userId>0&&in_array($subStatus,['pending','overdue','proof_sent'],true);
            $nextBillingDate=$recurringEnabled?date('Y-m-d',strtotime($paymentDate.($needsCharge?' +30 days':''))):null;
            $lastBillingDate=($recurringEnabled&&$needsCharge)?$paymentDate:null;

            if($subscriptionId>0){
                $oldQ=db()->prepare('SELECT * FROM subscriptions WHERE id=?');$oldQ->execute([$subscriptionId]);$oldSub=$oldQ->fetch();
                if(!$oldSub) throw new RuntimeException('Mensalidade não encontrada.');
                db()->prepare('UPDATE subscriptions SET user_id=?,company_id=?,plan_id=?,start_date=?,end_date=?,amount=?,payment_method=?,status=?,notes=?,recurring_enabled=?,billing_interval_days=30,billing_anchor_date=?,next_billing_date=?,last_billing_date=?,payment_link_id=?,payment_link=?,billing_issuer_user_id=?,updated_at=? WHERE id=?')
                    ->execute([$userId?:null,$companyId?:null,$planId,$start,$end,$amount,$method,$subStatus,$notes,$recurringEnabled,$paymentDate,$nextBillingDate,$lastBillingDate,$paymentLinkId?:null,$link,(int)$user['id'],now(),$subscriptionId]);
            }else{
                db()->prepare('INSERT INTO subscriptions(user_id,company_id,plan_id,start_date,end_date,amount,payment_method,status,notes,recurring_enabled,billing_interval_days,billing_anchor_date,next_billing_date,last_billing_date,payment_link_id,payment_link,billing_issuer_user_id,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
                    ->execute([$userId?:null,$companyId?:null,$planId,$start,$end,$amount,$method,$subStatus,$notes,$recurringEnabled,30,$paymentDate,$nextBillingDate,$lastBillingDate,$paymentLinkId?:null,$link,(int)$user['id'],now(),now()]);
                $subscriptionId=(int)db()->lastInsertId();
            }

            if($userId && $subStatus==='active'){
                db()->prepare("UPDATE users SET subscription_expires_at=?,status='active',activated_by_admin_id=?,provisional_access_until=NULL,updated_at=? WHERE id=?")->execute([$end,(int)$user['id'],now(),$userId]);
            }
            if($companyId){
                db()->prepare("UPDATE companies SET plan_id=?,status=CASE WHEN ?='active' THEN 'active' ELSE status END,activated_by_admin_id=CASE WHEN ?='active' THEN ? ELSE activated_by_admin_id END,updated_at=? WHERE id=?")->execute([$planId,$subStatus,$subStatus,(int)$user['id'],now(),$companyId]);
            }

            $cq=db()->prepare("SELECT * FROM charges WHERE subscription_id=? AND status IN ('pending','overdue','proof_sent','rejected') ORDER BY id DESC LIMIT 1");$cq->execute([$subscriptionId]);$charge=$cq->fetch();
            $chargeStatus=$subStatus==='overdue'?'overdue':($subStatus==='pending'?'pending':($subStatus==='proof_sent'?'proof_sent':'cancelled'));
            $blocks=in_array($subStatus,['pending','overdue','proof_sent'],true)?1:0;
            $recurrenceKey=$recurringEnabled?'subscription:'.$subscriptionId.':'.$paymentDate:null;

            if($needsCharge){
                if($charge){
                    db()->prepare('UPDATE charges SET user_id=?,amount=?,description=?,due_date=?,payment_link=?,payment_link_id=?,pix_key_snapshot=?,payment_method=?,status=?,blocks_access=?,recurrence_key=CASE WHEN recurrence_key IS NULL OR recurrence_key="" THEN ? ELSE recurrence_key END,updated_at=? WHERE id=?')
                        ->execute([$userId,$amount,'Mensalidade '.$p['name'],$paymentDate,$link,$paymentLinkId?:null,$pix,$method,$chargeStatus,$blocks,$recurrenceKey,now(),(int)$charge['id']]);
                }else{
                    db()->prepare('INSERT INTO charges(issuer_user_id,user_id,amount,description,due_date,payment_link,payment_link_id,pix_key_snapshot,payment_method,status,blocks_access,subscription_id,recurrence_key,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
                        ->execute([(int)$user['id'],$userId,$amount,'Mensalidade '.$p['name'],$paymentDate,$link,$paymentLinkId?:null,$pix,$method,$chargeStatus,1,$subscriptionId,$recurrenceKey,now(),now()]);
                }
                $message='Sua mensalidade de '.money($amount).' vence em '.date('d/m/Y',strtotime($paymentDate)).'.';
                if($link!=='')$message.=' O link de pagamento já está disponível.';
                if($recurringEnabled)$message.=' A cobrança será renovada automaticamente a cada 30 dias.';
                create_notification($userId,$wasEditing?'Mensalidade atualizada':'Mensalidade disponível',$message,'warning','dashboard.php?page=subscriptions','subscription',$subscriptionId);
            }elseif($charge){
                db()->prepare("UPDATE charges SET status='cancelled',blocks_access=0,admin_notes=trim(COALESCE(admin_notes,'') || ' Mensalidade alterada para situação sem bloqueio pelo administrador.'),updated_at=? WHERE id=?")
                    ->execute([now(),(int)$charge['id']]);
            }

            log_activity($wasEditing?'edit_subscription':'save_subscription','subscriptions',$subscriptionId,($companyId?'Empresa #'.$companyId.' • ':'').'Usuário #'.$userId.' • pagamento '.$paymentDate.($recurringEnabled?' • recorrente 30d':''));
            back_to('subscriptions',$wasEditing?'Mensalidade atualizada.':'Mensalidade registrada.');
            break;

        case 'delete_subscription':
            must_admin();
            $subscriptionId=(int)request('id',0);
            $q=db()->prepare('SELECT s.*,p.name plan_name,u.name user_name,c.name company_name FROM subscriptions s LEFT JOIN plans p ON p.id=s.plan_id LEFT JOIN users u ON u.id=s.user_id LEFT JOIN companies c ON c.id=s.company_id WHERE s.id=?');
            $q->execute([$subscriptionId]);$sub=$q->fetch();
            if(!$sub) throw new RuntimeException('Mensalidade não encontrada.');
            // Remove a cobrança vinculada à mensalidade, mas preserva lançamentos do Financeiro e arquivos para auditoria.
            db()->prepare('DELETE FROM charges WHERE subscription_id=?')->execute([$subscriptionId]);
            db()->prepare('DELETE FROM subscriptions WHERE id=?')->execute([$subscriptionId]);
            log_activity('delete_subscription','subscriptions',$subscriptionId,($sub['company_name']?:$sub['user_name']?:'Mensalidade').' • '.($sub['plan_name']??''));
            back_to('subscriptions','Mensalidade apagada. Os lançamentos já contabilizados no Financeiro foram preservados.');
            break;

        case 'save_category':
            must_role(['company','employee']);
            $id=(int)request('id',0);
            $companyId=is_admin()?(int)request('company_id',0):(int)$user['company_id'];
            $module=request('module','products');
            $allowedModules=['products','services','companies','vehicles'];
            if(!in_array($module,$allowedModules,true)) throw new RuntimeException('Módulo de categoria inválido.');
            $name=trim(request('name'));
            if($name==='') throw new RuntimeException('Informe o nome da categoria.');
            $icon=trim(request('icon'));
            $status=request('status','active');
            if(!in_array($status,['active','inactive'],true)) $status='active';
            $sortOrder=(int)request('sort_order',0);
            if($id){
                $q=db()->prepare('SELECT * FROM categories WHERE id=?');$q->execute([$id]);$existing=$q->fetch();
                if(!$existing) throw new RuntimeException('Categoria não encontrada.');
                if(!is_admin() && (int)$existing['company_id']!==(int)$user['company_id']) throw new RuntimeException('Você não pode editar esta categoria.');
                if(!is_admin()) $companyId=(int)$existing['company_id'];
                db()->prepare('UPDATE categories SET company_id=?,module=?,name=?,icon=?,status=?,sort_order=? WHERE id=?')
                    ->execute([$companyId?:null,$module,$name,$icon,$status,$sortOrder,$id]);
                log_activity('update_category','categories',$id,$name);
                back_to($returnPage,'Categoria atualizada.');
            }
            db()->prepare('INSERT INTO categories(company_id,module,name,icon,status,sort_order,created_at) VALUES(?,?,?,?,?,?,?)')
                ->execute([$companyId?:null,$module,$name,$icon,$status,$sortOrder,now()]);
            $id=(int)db()->lastInsertId();
            log_activity('create_category','categories',$id,$name);
            back_to($returnPage,'Categoria cadastrada.');
            break;

        case 'delete_category':
            must_role(['company','employee']);
            $id=(int)request('id',0);
            $q=db()->prepare('SELECT * FROM categories WHERE id=?');$q->execute([$id]);$category=$q->fetch();
            if(!$category) throw new RuntimeException('Categoria não encontrada.');
            if(!is_admin() && (int)$category['company_id']!==(int)$user['company_id']) throw new RuntimeException('Você não pode apagar esta categoria.');
            db()->beginTransaction();
            // Preserva produtos e serviços existentes: apenas remove o vínculo com a categoria.
            db()->prepare('UPDATE products SET category_id=NULL,updated_at=? WHERE category_id=?')->execute([now(),$id]);
            db()->prepare('UPDATE services SET category_id=NULL WHERE category_id=?')->execute([$id]);
            db()->prepare('DELETE FROM categories WHERE id=?')->execute([$id]);
            db()->commit();
            log_activity('delete_category','categories',$id,$category['name']);
            back_to($returnPage,'Categoria apagada. Produtos e serviços existentes foram mantidos sem categoria.');
            break;

        case 'save_segment':
            must_admin();
            $id=(int)request('id',0);
            $name=trim(request('name'));
            $slug=strtolower(trim(request('slug')));
            $slug=preg_replace('/[^a-z0-9_]+/','_',$slug);
            $slug=trim($slug,'_');
            $icon=trim(request('icon'));
            $status=request('status','active');
            if(!in_array($status,['active','inactive'],true)) $status='active';
            $sortOrder=(int)request('sort_order',0);
            $allowedRoles=request('allowed_roles',[]);if(!is_array($allowedRoles))$allowedRoles=[];$validSegmentRoles=segment_registration_roles();$allowedRoles=array_values(array_intersect($validSegmentRoles,array_map('strval',$allowedRoles)));if(!$allowedRoles)$allowedRoles=['company'];$allowedRolesCsv=implode(',',$allowedRoles);
            if($name===''||$slug==='') throw new RuntimeException('Informe o nome e o identificador do segmento.');
            if($id){
                $q=db()->prepare('SELECT * FROM segments WHERE id=?');$q->execute([$id]);$existing=$q->fetch();
                if(!$existing) throw new RuntimeException('Segmento não encontrado.');
                $reserved=['delivery','market','pharmacy','vehicle_store','rental','moving','taxi','mototaxi','services','shooting_club','other'];
                $oldSlug=(string)$existing['slug'];
                if(in_array($oldSlug,$reserved,true)) $slug=$oldSlug;
                $dup=db()->prepare('SELECT id FROM segments WHERE slug=? AND id<>? LIMIT 1');$dup->execute([$slug,$id]);
                if($dup->fetchColumn()) throw new RuntimeException('Já existe outro segmento com este identificador.');
                db()->beginTransaction();
                db()->prepare('UPDATE segments SET slug=?,name=?,icon=?,status=?,sort_order=?,allowed_roles=?,updated_at=? WHERE id=?')
                    ->execute([$slug,$name,$icon,$status,$sortOrder,$allowedRolesCsv,now(),$id]);
                if($slug!==$oldSlug){
                    db()->prepare('UPDATE companies SET type=?,sector=CASE WHEN sector=? THEN ? ELSE sector END,updated_at=? WHERE type=?')
                        ->execute([$slug,$oldSlug,$slug,now(),$oldSlug]);
                }
                db()->commit();
                log_activity('update_segment','segments',$id,$name.' ['.$slug.']');
                back_to('segments','Segmento atualizado.');
            }
            $dup=db()->prepare('SELECT id FROM segments WHERE slug=? LIMIT 1');$dup->execute([$slug]);
            if($dup->fetchColumn()) throw new RuntimeException('Já existe um segmento com este identificador.');
            db()->prepare('INSERT INTO segments(slug,name,icon,status,sort_order,allowed_roles,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)')
                ->execute([$slug,$name,$icon,$status,$sortOrder,$allowedRolesCsv,now(),now()]);
            $id=(int)db()->lastInsertId();
            log_activity('create_segment','segments',$id,$name.' ['.$slug.']');
            back_to('segments','Segmento cadastrado.');
            break;

        case 'delete_segment':
            must_admin();
            $id=(int)request('id',0);
            $q=db()->prepare('SELECT * FROM segments WHERE id=?');$q->execute([$id]);$segment=$q->fetch();
            if(!$segment) throw new RuntimeException('Segmento não encontrado.');
            $used=db()->prepare('SELECT COUNT(*) FROM companies WHERE type=?');$used->execute([$segment['slug']]);$companyCount=(int)$used->fetchColumn();
            if($companyCount>0) throw new RuntimeException('Este segmento está sendo usado por '.$companyCount.' empresa(s). Altere o segmento dessas empresas antes de apagar.');
            db()->prepare('DELETE FROM segments WHERE id=?')->execute([$id]);
            log_activity('delete_segment','segments',$id,$segment['name'].' ['.$segment['slug'].']');
            back_to('segments','Segmento apagado.');
            break;

        case 'save_product':
            must_role(['company','employee']);
            $id=(int)request('id',0);$companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            if(!$companyId)throw new RuntimeException('Empresa não definida.');
            if(!$id&&!is_admin()){$limitInfo=product_limit_status($companyId);if($limitInfo['reached'])throw new RuntimeException('Limite de produtos atingido para esta empresa ('.$limitInfo['used'].'/'.$limitInfo['limit'].').');}
            $existingImage='';if($id){$imgq=db()->prepare('SELECT image FROM products WHERE id=?');$imgq->execute([$id]);$existingImage=(string)$imgq->fetchColumn();}
            $removeImage=request('remove_image')?1:0;
            $hasNewImage=isset($_FILES['image_file']) && is_array($_FILES['image_file']) && (int)($_FILES['image_file']['error']??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_NO_FILE;
            if($removeImage && !$hasNewImage){delete_local_upload($existingImage);$image='';}
            else{$image=upload_image('image_file','produtos',$existingImage);}
            $data=[$companyId,(int)request('category_id')?:null,request('name'),request('description'),(float)str_replace(',','.',request('price',0)),(float)str_replace(',','.',request('stock',0)),request('unit','un'),$image,request('status','active'),request('featured')?1:0,max(0,(int)request('low_stock_threshold',5)),request('is_gas_cylinder')?1:0,max(0,(float)str_replace(',','.',request('gas_weight_kg',13))),max(0,(float)str_replace(',','.',request('cost_price',0))),request('petshop_only')?1:0];
            if(!$data[2])throw new RuntimeException('Informe o nome do produto.');
            if($id){$sql='UPDATE products SET company_id=?,category_id=?,name=?,description=?,price=?,stock=?,unit=?,image=?,status=?,featured=?,low_stock_threshold=?,is_gas_cylinder=?,gas_weight_kg=?,cost_price=?,petshop_only=?,low_stock_notified=CASE WHEN stock>? THEN 0 ELSE low_stock_notified END,updated_at=? WHERE id=?';db()->prepare($sql)->execute(array_merge($data,[$data[10],now(),$id]));}
            else{db()->prepare('INSERT INTO products(company_id,category_id,name,description,price,stock,unit,image,status,featured,low_stock_threshold,is_gas_cylinder,gas_weight_kg,cost_price,petshop_only,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute(array_merge($data,[now()]));$id=(int)db()->lastInsertId();}
            log_activity('save_product','products',$id,$data[2]);back_to('catalog','Produto salvo.');
            break;

        case 'delete_product':
            must_role(['company','employee']);
            $id=(int)request('id',0);
            $q=db()->prepare('SELECT * FROM products WHERE id=? AND deleted_at IS NULL');$q->execute([$id]);$product=$q->fetch();
            if(!$product)throw new RuntimeException('Produto não encontrado.');
            $companyId=resolve_company_id_for_user($user);
            if(!is_admin()&&(int)$product['company_id']!==$companyId)throw new RuntimeException('Você não pode apagar produto de outra empresa.');
            db()->beginTransaction();
            try{
                // Retira o produto de carrinhos atuais e da vitrine, mas mantém seus dados
                // para que pedidos, histórico do gás e movimentações antigas continuem íntegros.
                db()->prepare('DELETE FROM cart_items WHERE product_id=?')->execute([$id]);
                db()->prepare("UPDATE products SET status='inactive',deleted_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$id]);
                db()->commit();
            }catch(Throwable $e){if(db()->inTransaction())db()->rollBack();throw $e;}
            log_activity('delete_product','products',$id,$product['name']);
            back_to('catalog','Produto apagado da vitrine. O histórico de pedidos foi preservado.');
            break;

        case 'create_order':
            $productId=(int)request('product_id');$qty=max(1,(float)str_replace(',','.',request('qty',1)));$p=db()->prepare('SELECT p.*,c.status AS company_status,c.pickup_scheduling_enabled,c.pickup_weekdays,c.pickup_start_time,c.pickup_end_time,c.pickup_interval_minutes FROM products p JOIN companies c ON c.id=p.company_id WHERE p.id=? AND p.status="active" AND p.deleted_at IS NULL');$p->execute([$productId]);$product=$p->fetch();if(!$product)throw new RuntimeException('Produto indisponível.');
            $clientId=(int)$user['id'];if((float)$product['stock']<$qty)throw new RuntimeException('Estoque insuficiente para '.$product['name'].'.');$fee=(float)str_replace(',','.',request('delivery_fee',0));$subtotal=$product['price']*$qty;$number='LX'.date('ymdHis').random_int(10,99);
            db()->beginTransaction();
            $fulfillment=request('fulfillment_type','delivery');if(!in_array($fulfillment,['delivery','pickup'],true))$fulfillment='delivery';
            $address=$fulfillment==='delivery'?(request('address')?:($user['home_address']??'')):'Retirada no estabelecimento';
            $pickupScheduledAt=null;if($fulfillment==='pickup'&&!empty($product['pickup_scheduling_enabled'])){$pickupDate=trim((string)request('pickup_date'));$pickupTime=trim((string)request('pickup_time'));if(!company_pickup_schedule_allows($product,$pickupDate,$pickupTime))throw new RuntimeException('Escolha uma data e horário disponíveis para retirar a encomenda.');$pickupScheduledAt=$pickupDate.' '.$pickupTime.':00';}
            db()->prepare('INSERT INTO orders(company_id,client_id,order_number,status,subtotal,delivery_fee,total,payment_method,payment_status,address,notes,fulfillment_type,client_address_snapshot,stock_committed,pickup_scheduled_at,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$product['company_id'],$clientId?:null,$number,'received',$subtotal,$fulfillment==='delivery'?$fee:0,$subtotal+($fulfillment==='delivery'?$fee:0),request('payment_method','pix'),'pending',$address,request('notes'),$fulfillment,$address,0,$pickupScheduledAt,now(),now()]);
            $orderId=(int)db()->lastInsertId();db()->prepare('INSERT INTO order_items(order_id,product_id,name,qty,price,total) VALUES(?,?,?,?,?,?)')->execute([$orderId,$productId,$product['name'],$qty,$product['price'],$subtotal]);
            // O estoque é apenas conferido neste momento. A baixa real acontece quando
            // o cliente envia o comprovante ou quando a empresa confirma o pagamento.
            db()->commit();
            $orderMessage='Pedido '.$number.' recebido.'.($pickupScheduledAt?' Retirada agendada para '.date('d/m/Y H:i',strtotime($pickupScheduledAt)).'.':'');notify_company_operational_users((int)$product['company_id'],'Novo pedido',$orderMessage,'success','dashboard.php?page=orders','order',$orderId);
            log_activity('create_order','orders',$orderId,$number);back_to('orders','Pedido '.$number.' criado com sucesso.');
            break;

        case 'upload_order_payment_proof':
            $id=(int)request('id',0);
            $q=db()->prepare('SELECT o.*,c.owner_user_id,c.name company_name FROM orders o JOIN companies c ON c.id=o.company_id WHERE o.id=? AND o.client_id=?');
            $q->execute([$id,$user['id']]);$order=$q->fetch();
            if(!$order)throw new RuntimeException('Pedido não encontrado.');
            if($order['status']==='cancelled')throw new RuntimeException('Não é possível enviar comprovante para um pedido cancelado.');
            if($order['payment_status']==='paid')throw new RuntimeException('Este pedido já está pago.');
            if(strtolower((string)$order['payment_method'])!=='pix')throw new RuntimeException('O envio de comprovante está disponível para pedidos pagos via Pix.');
            $proof=upload_document('payment_proof_file','comprovantes-pedidos/'.(int)$order['company_id'].'/'.$id,$order['payment_proof']??'');
            if($proof==='')throw new RuntimeException('Selecione uma imagem ou PDF do comprovante Pix.');
            commit_order_stock($id);
            db()->prepare("UPDATE orders SET payment_proof=?,payment_proof_uploaded_at=?,payment_status='proof_sent',updated_at=? WHERE id=?")
                ->execute([$proof,now(),now(),$id]);
            notify_company_operational_users((int)$order['company_id'],'Comprovante Pix recebido','O cliente enviou o comprovante do pedido '.$order['order_number'].'.','success','dashboard.php?page=orders','order',$id);
            log_activity('upload_order_payment_proof','orders',$id,$order['order_number']);
            back_to('orders','Comprovante enviado. Aguarde a confirmação da empresa.');
            break;

        case 'confirm_order_payment':
            must_role(['company','employee','courier','motofreight','mototaxi']);
            if(is_role('mototaxi')&&!user_can_work_deliveries($user))throw new RuntimeException('A modalidade Entregas ainda não foi liberada pelo administrador.');
            $id=(int)request('id',0);
            $q=db()->prepare('SELECT o.*,c.owner_user_id,c.name company_name FROM orders o JOIN companies c ON c.id=o.company_id WHERE o.id=?');
            $q->execute([$id]);$order=$q->fetch();
            if(!$order)throw new RuntimeException('Pedido não encontrado.');
            if($order['status']==='cancelled')throw new RuntimeException('Pedido cancelado não pode ser marcado como pago.');
            if($order['payment_status']==='paid')back_to('orders','Este pedido já está confirmado como pago.');
            $source='admin';
            if(!is_admin()){
                if(is_role(['company','employee'])){
                    $companyId=resolve_company_id_for_user($user);
                    if($companyId<1 || (int)$order['company_id']!==$companyId)throw new RuntimeException('Pedido de outra empresa.');
                    $source='company';
                }else{
                    if((int)($order['delivery_user_id']??0)!==(int)$user['id'])throw new RuntimeException('Somente o entregador responsável pode confirmar o pagamento na entrega.');
                    if(($order['fulfillment_type']??'delivery')==='pickup')throw new RuntimeException('Este pedido é para retirada no estabelecimento.');
                    $source='courier';
                }
            }
            commit_order_stock($id);
            db()->prepare("UPDATE orders SET payment_status='paid',payment_confirmed_by=?,payment_confirmed_at=?,payment_confirmation_source=?,updated_at=? WHERE id=?")
                ->execute([$user['id'],now(),$source,now(),$id]);
            ensure_order_delivery_job($id);
            if(!empty($order['client_id'])){
                $who=$source==='courier'?'O entregador confirmou o recebimento do pagamento.':'A empresa confirmou seu pagamento.';
                create_notification((int)$order['client_id'],'Pagamento confirmado',$who.' Pedido '.$order['order_number'].' marcado como pago.','success','dashboard.php?page=orders','order',$id);
            }
            if($source==='courier')notify_company_operational_users((int)$order['company_id'],'Pagamento confirmado pelo entregador','O entregador confirmou que o cliente pagou o pedido '.$order['order_number'].'.','success','dashboard.php?page=orders','order',$id);
            if(!empty($order['client_id'])&&!empty($order['owner_user_id'])){$feeBreakdown=calculate_platform_fee((int)$order['owner_user_id'],(float)$order['total'],'order',$id);$receipt=generate_transaction_receipt_png('order',$id,(int)$order['client_id'],(int)$order['owner_user_id'],(float)$feeBreakdown['total'],'Pedido '.$order['order_number'].($feeBreakdown['fee']>0?' • taxa '.money($feeBreakdown['fee']).' paga por '.(($feeBreakdown['fee_payer']??'client')==='provider'?'prestador/empresa':'cliente'):''),(string)$order['payment_method'],(string)($order['payment_proof']??''),(int)$order['company_id']);post_finance_pair('order',$id,(int)$order['client_id'],(int)$order['owner_user_id'],(float)$order['total'],(string)$order['payment_method'],(string)($order['payment_proof']??''),$receipt,'Pedido '.$order['order_number'],(int)$order['company_id']);}
            log_activity('confirm_order_payment','orders',$id,$source);
            back_to('orders','Pagamento confirmado com sucesso.');
            break;

        case 'reject_order_payment_proof':
            must_role(['company','employee']);
            $id=(int)request('id',0);
            $q=db()->prepare('SELECT o.* FROM orders o WHERE o.id=?');$q->execute([$id]);$order=$q->fetch();
            if(!$order)throw new RuntimeException('Pedido não encontrado.');
            if(!is_admin()){
                $companyId=resolve_company_id_for_user($user);
                if($companyId<1 || (int)$order['company_id']!==$companyId)throw new RuntimeException('Pedido de outra empresa.');
            }
            if(empty($order['payment_proof']))throw new RuntimeException('Este pedido não possui comprovante enviado.');
            if($order['payment_status']==='paid')throw new RuntimeException('Este pedido já está pago.');
            db()->prepare("UPDATE orders SET payment_status='proof_rejected',updated_at=? WHERE id=?")->execute([now(),$id]);
            if(!empty($order['client_id']))create_notification((int)$order['client_id'],'Comprovante não confirmado','A empresa não confirmou o comprovante do pedido '.$order['order_number'].'. Envie um novo comprovante ou entre em contato.','warning','dashboard.php?page=orders','order',$id);
            log_activity('reject_order_payment_proof','orders',$id,$order['order_number']);
            back_to('orders','Comprovante recusado. O cliente foi notificado.','warning');
            break;

        case 'update_order_status':
            must_role(['company','employee','courier','motofreight','mototaxi']);if(is_role('mototaxi')&&!user_can_work_deliveries($user))throw new RuntimeException('A modalidade Entregas ainda não foi liberada pelo administrador.');$id=(int)request('id');$status=request('status');$allowed=['received','preparing','ready','on_route','delivered','cancelled'];if(!in_array($status,$allowed,true))throw new RuntimeException('Status inválido.');
            $stmt=db()->prepare('SELECT * FROM orders WHERE id=?');$stmt->execute([$id]);$order=$stmt->fetch();if(!$order)throw new RuntimeException('Pedido não encontrado.');
            if(in_array($order['status'],['delivered','cancelled'],true))throw new RuntimeException('Este pedido já foi encerrado.');
            if(is_role(['company','employee'])){$companyId=resolve_company_id_for_user($user);if($companyId<1||(int)$order['company_id']!==$companyId)throw new RuntimeException('Pedido de outra empresa.');}
            if(user_can_work_deliveries($user)&&!is_role(['company','employee'])){if($order['delivery_user_id']&&(int)$order['delivery_user_id']!==(int)$user['id'])throw new RuntimeException('Entrega atribuída a outro profissional.');db()->prepare('UPDATE orders SET delivery_user_id=? WHERE id=?')->execute([$user['id'],$id]);}
            if($status==='cancelled'){
                release_order_stock($id);
                db()->prepare("UPDATE delivery_jobs SET status='cancelled',terminal_at=?,updated_at=? WHERE order_id=? AND status NOT IN ('delivered','cancelled','rejected')")->execute([now(),now(),$id]);
            }
            db()->prepare('UPDATE orders SET status=?,updated_at=? WHERE id=?')->execute([$status,now(),$id]);
            if($order['client_id'])create_notification((int)$order['client_id'],'Atualização do pedido','Pedido '.$order['order_number'].': '.status_label($status),'info');
            log_activity('update_order_status','orders',$id,$status);back_to('orders','Status do pedido atualizado.');
            break;

        case 'assign_order_courier':
            must_role(['company','employee']);$id=(int)request('id');$courierId=(int)request('courier_id');
            $q=db()->prepare('SELECT * FROM orders WHERE id=?');$q->execute([$id]);$order=$q->fetch();if(!$order)throw new RuntimeException('Pedido não encontrado.');
            $companyId=is_admin()?(int)$order['company_id']:resolve_company_id_for_user($user);if(!is_admin()&&((int)$order['company_id']!==$companyId))throw new RuntimeException('Pedido de outra empresa.');
            if(($order['fulfillment_type']??'delivery')==='pickup')throw new RuntimeException('Este pedido é para retirada no estabelecimento.');
            if(($order['payment_status']??'pending')!=='paid')throw new RuntimeException('Confirme a venda/pagamento antes de selecionar o entregador.');
            $check=db()->prepare("SELECT COUNT(*) FROM company_couriers WHERE company_id=? AND user_id=? AND status='active'");$check->execute([(int)$order['company_id'],$courierId]);if(!(int)$check->fetchColumn())throw new RuntimeException('Entregador não pertence à equipe ativa.');
            $jobId=ensure_order_delivery_job($id);if($jobId<1)throw new RuntimeException('Não foi possível gerar a ordem de entrega deste pedido.');
            db()->prepare("UPDATE delivery_jobs SET assigned_courier_id=?,status='assigned',updated_at=? WHERE id=?")->execute([$courierId,now(),$jobId]);
            db()->prepare('UPDATE orders SET delivery_user_id=?,status=CASE WHEN status="received" THEN "ready" ELSE status END,updated_at=? WHERE id=?')->execute([$courierId,now(),$id]);
            if(company_is_gas((int)$order['company_id']))create_notification($courierId,'Nova entrega de gás','Você recebeu a ordem do pedido '.$order['order_number'].'. Abra sua Área de Trabalho — Gás para ver cliente, endereço e produtos.','success','dashboard.php?page=gas_courier','delivery_job',$jobId);
            else create_notification($courierId,'Nova entrega atribuída','Você recebeu a ordem do pedido '.$order['order_number'].'. Abra Entregadores e Entregas para ver cliente, endereço e produtos.','success','dashboard.php?page=deliveries','delivery_job',$jobId);
            if(!empty($order['client_id']))create_notification((int)$order['client_id'],'Entregador selecionado','A empresa selecionou o entregador do pedido '.$order['order_number'].'.','info','dashboard.php?page=orders','order',$id);
            log_activity('assign_order_courier','orders',$id,'courier='.$courierId);back_to('orders','Entregador selecionado e ordem de entrega enviada.');
            break;

        case 'save_service':
            must_role('service_provider');
            $id=(int)request('id',0);$providerId=is_admin()?(int)request('provider_id'):(int)$user['id'];
            if($providerId<1)throw new RuntimeException('Prestador não definido.');
            if($id){
                $own=db()->prepare('SELECT * FROM services WHERE id=? AND deleted_at IS NULL');$own->execute([$id]);$existingService=$own->fetch();if(!$existingService)throw new RuntimeException('Serviço não encontrado.');
                if(!is_admin()&&(int)$existingService['provider_id']!==(int)$user['id'])throw new RuntimeException('Você não pode editar o serviço de outro prestador.');
            }elseif(!is_admin()){
                $limitInfo=publication_limit_status($providerId);if($limitInfo['reached'])throw new RuntimeException('Limite de publicações atingido ('.$limitInfo['used'].'/'.$limitInfo['limit'].').');
            }
            $name=trim((string)request('name'));if($name==='')throw new RuntimeException('Informe o serviço.');
            $categoryId=(int)request('category_id')?:null;$description=trim((string)request('description'));$price=(float)str_replace(',','.',request('price',0));$priceType=request('price_type','fixed');if(!in_array($priceType,['fixed','from','quote'],true))$priceType='fixed';
            $status=request('status','active');if(!in_array($status,['active','inactive'],true))$status='active';$bookingEnabled=(int)request('booking_enabled',0)===1?1:0;$bookingMode=request('booking_mode','time')==='arrival'?'arrival':'time';$bookingStartDate=trim((string)request('booking_start_date'));$bookingEndDate=trim((string)request('booking_end_date'));$bookingInterval=(int)request('booking_interval_minutes',60);if(!in_array($bookingInterval,[15,20,30,45,60,90,120],true))$bookingInterval=60;$fixedLocationEnabled=(int)request('fixed_location_enabled',0)===1?1:0;$fixedAddress=trim((string)request('fixed_address'));$fixedLat=request('fixed_lat')!==''?(float)request('fixed_lat'):null;$fixedLng=request('fixed_lng')!==''?(float)request('fixed_lng'):null;if($fixedLocationEnabled&&$fixedAddress==='')throw new RuntimeException('Informe o endereço fixo onde o cliente será atendido.');if(!$fixedLocationEnabled){$fixedAddress='';$fixedLat=null;$fixedLng=null;}
            $days=request('availability_days',[]);if(!is_array($days))$days=[];$days=array_values(array_unique(array_filter(array_map('intval',$days),static function($d){return $d>=0&&$d<=6;})));
            $startTime=trim((string)request('available_start'));$endTime=trim((string)request('available_end'));
            if($bookingEnabled&&!$days)throw new RuntimeException('Selecione pelo menos um dia disponível para agendamento.');
            if($bookingEnabled&&($startTime===''||$endTime===''))throw new RuntimeException('Informe o horário inicial e final para criar as vagas de agendamento.');
            if($bookingEnabled&&$startTime>=$endTime)throw new RuntimeException('O horário final deve ser maior que o horário inicial.');
            db()->beginTransaction();
            try{
                if($id)db()->prepare('UPDATE services SET provider_id=?,category_id=?,name=?,description=?,price=?,price_type=?,status=?,booking_enabled=?,booking_mode=?,booking_start_date=?,booking_end_date=?,booking_interval_minutes=?,fixed_location_enabled=?,fixed_address=?,fixed_lat=?,fixed_lng=? WHERE id=?')->execute([$providerId,$categoryId,$name,$description,$price,$priceType,$status,$bookingEnabled,$bookingMode,$bookingStartDate?:null,$bookingEndDate?:null,$bookingInterval,$fixedLocationEnabled,$fixedAddress?:null,$fixedLat,$fixedLng,$id]);
                else{db()->prepare('INSERT INTO services(provider_id,category_id,name,description,price,price_type,status,booking_enabled,booking_mode,booking_start_date,booking_end_date,booking_interval_minutes,fixed_location_enabled,fixed_address,fixed_lat,fixed_lng,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$providerId,$categoryId,$name,$description,$price,$priceType,$status,$bookingEnabled,$bookingMode,$bookingStartDate?:null,$bookingEndDate?:null,$bookingInterval,$fixedLocationEnabled,$fixedAddress?:null,$fixedLat,$fixedLng,now()]);$id=(int)db()->lastInsertId();}
                db()->prepare('DELETE FROM service_availability WHERE service_id=?')->execute([$id]);
                if($bookingEnabled){$ins=db()->prepare('INSERT INTO service_availability(service_id,weekday,start_time,end_time,status,created_at,updated_at) VALUES(?,?,?,?,"active",?,?)');foreach($days as $day)$ins->execute([$id,$day,$startTime?:null,$endTime?:null,now(),now()]);}
                db()->commit();
            }catch(Throwable $e){if(db()->inTransaction())db()->rollBack();throw $e;}
            log_activity('save_service','services',$id,$name);back_to('services','Serviço salvo.');
            break;

        case 'save_service_bulk_availability':
            must_role('service_provider');
            $serviceId=(int)request('service_id',0);if($serviceId<1)throw new RuntimeException('Selecione o serviço.');
            $q=db()->prepare('SELECT * FROM services WHERE id=? AND deleted_at IS NULL LIMIT 1');$q->execute([$serviceId]);$service=$q->fetch();if(!$service)throw new RuntimeException('Serviço não encontrado.');
            if(!is_admin()&&(int)$service['provider_id']!==(int)$user['id'])throw new RuntimeException('Você não pode liberar horários para outro prestador.');
            $dateStart=trim((string)request('date_start'));$dateEnd=trim((string)request('date_end'));
            if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$dateStart)||!preg_match('/^\d{4}-\d{2}-\d{2}$/',$dateEnd)||strtotime($dateStart)===false||strtotime($dateEnd)===false)throw new RuntimeException('Informe o período da agenda.');
            if($dateStart<date('Y-m-d'))$dateStart=date('Y-m-d');
            if($dateEnd<$dateStart)throw new RuntimeException('A data final deve ser igual ou posterior à data inicial.');
            $startTs=strtotime($dateStart);$endTs=strtotime($dateEnd);if((int)(($endTs-$startTs)/86400)>180)throw new RuntimeException('Libere no máximo 180 dias por vez.');
            $days=request('bulk_weekdays',[]);if(!is_array($days))$days=[];$days=array_values(array_unique(array_filter(array_map('intval',$days),static fn($d)=>$d>=0&&$d<=6)));if(!$days)throw new RuntimeException('Selecione pelo menos um dia da semana.');
            $startTime=trim((string)request('bulk_start_time'));$endTime=trim((string)request('bulk_end_time'));if(!preg_match('/^\d{2}:\d{2}$/',$startTime)||!preg_match('/^\d{2}:\d{2}$/',$endTime))throw new RuntimeException('Informe os horários de início e encerramento.');if($startTime>=$endTime)throw new RuntimeException('O horário de encerramento deve ser maior que o de início.');
            $interval=(int)request('bulk_interval_minutes',60);if(!in_array($interval,[15,20,30,45,60,90,120],true))$interval=60;
            db()->beginTransaction();
            try{
                db()->prepare('UPDATE services SET booking_enabled=1,booking_interval_minutes=? WHERE id=?')->execute([$interval,$serviceId]);
                $up=db()->prepare('INSERT INTO service_availability_dates(service_id,availability_date,start_time,end_time,interval_minutes,status,created_at,updated_at) VALUES(?,?,?,?,?,"active",?,?) ON CONFLICT(service_id,availability_date) DO UPDATE SET start_time=excluded.start_time,end_time=excluded.end_time,interval_minutes=excluded.interval_minutes,status="active",updated_at=excluded.updated_at');
                $count=0;for($ts=$startTs;$ts<=$endTs;$ts+=86400){$date=date('Y-m-d',$ts);$weekday=(int)date('w',$ts);if(!in_array($weekday,$days,true))continue;$up->execute([$serviceId,$date,$startTime,$endTime,$interval,now(),now()]);$count++;}
                if($count<1)throw new RuntimeException('Nenhuma data do período corresponde aos dias selecionados.');
                db()->commit();
            }catch(Throwable $e){if(db()->inTransaction())db()->rollBack();throw $e;}
            log_activity('save_service_bulk_availability','services',$serviceId,$dateStart.' a '.$dateEnd.' • '.$startTime.'-'.$endTime);back_to('services','Horários liberados em massa para '.$count.' dia(s).');
            break;

        case 'delete_service':
            must_role('service_provider');$id=(int)request('id',0);$q=db()->prepare('SELECT * FROM services WHERE id=? AND deleted_at IS NULL');$q->execute([$id]);$service=$q->fetch();if(!$service)throw new RuntimeException('Serviço não encontrado.');
            if(!is_admin()&&(int)$service['provider_id']!==(int)$user['id'])throw new RuntimeException('Você não pode apagar o serviço de outro prestador.');
            db()->prepare("UPDATE services SET status='inactive',deleted_at=? WHERE id=?")->execute([now(),$id]);
            log_activity('delete_service','services',$id,$service['name']);back_to('services','Serviço apagado da vitrine. O histórico de contratações foi preservado.');
            break;

        case 'create_service_request':
            if(is_admin())throw new RuntimeException('Este perfil não pode contratar um serviço nesta área.');
            $serviceId=(int)request('service_id');
            $q=db()->prepare('SELECT * FROM services WHERE id=? AND status="active" AND deleted_at IS NULL');$q->execute([$serviceId]);$service=$q->fetch();
            if(!$service)throw new RuntimeException('Serviço indisponível.');
            if((int)$service['provider_id']===(int)$user['id'])throw new RuntimeException('Você não pode contratar o próprio serviço.');
            $address=trim((string)request('address'));$lat=request('latitude')!==''?(float)request('latitude'):null;$lng=request('longitude')!==''?(float)request('longitude'):null;if(!empty($service['fixed_location_enabled'])){$address=trim((string)($service['fixed_address']??''));$lat=($service['fixed_lat']??'')!==''?(float)$service['fixed_lat']:null;$lng=($service['fixed_lng']??'')!==''?(float)$service['fixed_lng']:null;}if($address==='')throw new RuntimeException('Informe o endereço do atendimento.');
            $scheduledAt=trim((string)request('scheduled_at'));
            if(!empty($service['booking_enabled'])){
                if($scheduledAt==='')throw new RuntimeException('Escolha a data e o horário do agendamento.');
                if(strtotime($scheduledAt)!==false&&strtotime($scheduledAt)<time())throw new RuntimeException('Escolha um horário futuro.');
                if(!service_schedule_allows($serviceId,$scheduledAt))throw new RuntimeException('O prestador não está disponível nesse dia ou horário.');
                $busy=db()->prepare("SELECT COUNT(*) FROM service_requests WHERE provider_id=? AND scheduled_at=? AND status IN ('requested','accepted','in_progress')");$busy->execute([$service['provider_id'],$scheduledAt]);
                if((int)$busy->fetchColumn()>0)throw new RuntimeException('Esse horário acabou de ser reservado. Escolha outro horário disponível.');
            }
            db()->prepare('INSERT INTO service_requests(client_id,provider_id,service_id,status,address,latitude,longitude,scheduled_at,amount,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)')
                ->execute([$user['id'],$service['provider_id'],$serviceId,'requested',$address,$lat,$lng,$scheduledAt?:null,$service['price'],request('notes'),now(),now()]);
            $requestId=(int)db()->lastInsertId();register_provider_client((int)$service['provider_id'],(int)$user['id']);
            create_notification((int)$service['provider_id'],'Nova solicitação',$user['name'].' solicitou o serviço '.$service['name'].'.','info','dashboard.php?page=services','service_request',$requestId);
            back_to('services',!empty($service['booking_enabled'])?'Agendamento enviado ao prestador.':'Solicitação enviada ao prestador.');
            break;

        case 'update_service_request':
            must_role('service_provider');
            $id=(int)request('id');$status=request('status');$allowed=['accepted','in_progress','completed','cancelled'];
            if(!in_array($status,$allowed,true))throw new RuntimeException('Status inválido.');
            $q=db()->prepare('SELECT * FROM service_requests WHERE id=?');$q->execute([$id]);$req=$q->fetch();
            if(!$req)throw new RuntimeException('Solicitação não encontrada.');
            if(!is_admin()&&(int)$req['provider_id']!==(int)$user['id'])throw new RuntimeException('Solicitação de outro prestador.');
            if(in_array($req['status'],['completed','cancelled'],true))throw new RuntimeException('Este serviço já foi encerrado.');
            // V2.8.48: a forma de pagamento do serviço só é liberada depois que o cliente confirma que o trabalho foi realizado.
            $pixRelease=(int)($req['pix_released']??0);
            db()->prepare('UPDATE service_requests SET status=?,amount=?,pix_released=?,updated_at=? WHERE id=?')
                ->execute([$status,(float)str_replace(',','.',request('amount',$req['amount'])),$pixRelease,now(),$id]);
            create_notification((int)$req['client_id'],'Serviço atualizado','Status da solicitação: '.status_label($status),'info','dashboard.php?page=services','service_request',$id);
            if($status==='completed')create_notification((int)$req['client_id'],'Confirme o trabalho realizado','O prestador marcou o serviço como concluído. Confirme que o trabalho foi feito para o sistema liberar a forma de pagamento.','warning','dashboard.php?page=services','service_request',$id);
            if($status==='accepted') create_notification((int)$req['provider_id'],'Rota do atendimento liberada','Use Google Maps ou Waze na área Serviços para chegar ao cliente.','success','dashboard.php?page=services','service_request',$id);
            back_to('services','Solicitação atualizada.');
            break;


        case 'save_bus_vehicle':
            must_role(['company','employee']);
            $companyId=bus_company_id_for_user($user); if($companyId<1) throw new RuntimeException('Empresa não encontrada.');
            $id=(int)request('id',0); $ident=trim((string)request('identification')); if($ident==='') throw new RuntimeException('Informe a identificação do ônibus.');
            $plate=strtoupper(trim((string)request('plate'))); $model=trim((string)request('model')); $seats=max(1,(int)request('seats_count',0));
            $layoutJson=(string)request('layout_json','[]'); json_decode($layoutJson,true); if(json_last_error()!==JSON_ERROR_NONE)$layoutJson='[]';
            if($id){$q=db()->prepare('SELECT id FROM bus_vehicles WHERE id=? AND company_id=?');$q->execute([$id,$companyId]);if(!$q->fetchColumn())throw new RuntimeException('Ônibus não encontrado.');db()->prepare('UPDATE bus_vehicles SET identification=?,plate=?,model=?,seats_count=?,layout_json=?,status=?,updated_at=? WHERE id=?')->execute([$ident,$plate?:null,$model?:null,$seats,$layoutJson,request('status','active')==='inactive'?'inactive':'active',now(),$id]);}
            else{$q=db()->prepare('INSERT INTO bus_vehicles(company_id,identification,plate,model,seats_count,layout_json,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)');$q->execute([$companyId,$ident,$plate?:null,$model?:null,$seats,$layoutJson,'active',now(),now()]);$id=(int)db()->lastInsertId();}
            $layout=json_decode($layoutJson,true)?:[]; db()->prepare('DELETE FROM bus_seats WHERE bus_id=?')->execute([$id]); $ins=db()->prepare('INSERT INTO bus_seats(bus_id,seat_number,row_number,column_number,seat_type,position,active) VALUES(?,?,?,?,?,?,TRUE)');
            if($layout){foreach($layout as $seat){$num=trim((string)($seat['seat_number']??$seat['number']??''));if($num==='')continue;$ins->execute([$id,$num,(int)($seat['row']??0)?:null,(int)($seat['column']??0)?:null,(string)($seat['type']??'normal'),(string)($seat['position']??'')]);}}
            else {for($i=1;$i<=$seats;$i++)$ins->execute([$id,(string)$i,null,null,'normal','']);}
            back_to('bus_trips','Ônibus salvo.'); break;

        case 'save_bus_route':
            must_role(['company','employee']); $companyId=bus_company_id_for_user($user); if($companyId<1)throw new RuntimeException('Empresa não encontrada.');
            $id=(int)request('id',0);$name=trim((string)request('name'));$origin=trim((string)request('origin_city'));$dest=trim((string)request('final_destination_city'));if(!$name||!$origin||!$dest)throw new RuntimeException('Nome, origem e destino final são obrigatórios.');
            if($id)db()->prepare('UPDATE bus_routes SET name=?,origin_city=?,final_destination_city=?,updated_at=? WHERE id=? AND company_id=?')->execute([$name,$origin,$dest,now(),$id]);else{db()->prepare('INSERT INTO bus_routes(company_id,name,origin_city,final_destination_city,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?)')->execute([$companyId,$name,$origin,$dest,'active',now(),now()]);$id=(int)db()->lastInsertId();}
            $cities=request('stop_city',[]);$boards=request('stop_boarding',[]);$drops=request('stop_dropoff',[]);$times=request('stop_time',[]);$allowB=request('stop_boarding_allowed',[]);$allowD=request('stop_dropoff_allowed',[]);
            if(!is_array($cities))$cities=[];db()->prepare('DELETE FROM bus_route_stops WHERE route_id=?')->execute([$id]);$ins=db()->prepare('INSERT INTO bus_route_stops(route_id,city,boarding_location,dropoff_location,estimated_time,allow_boarding,allow_dropoff,stop_order) VALUES(?,?,?,?,?,?,?,?)');$n=0;
            foreach($cities as $i=>$city){$city=trim((string)$city);if($city==='')continue;$ins->execute([$id,$city,trim((string)($boards[$i]??''))?:null,trim((string)($drops[$i]??''))?:null,trim((string)($times[$i]??''))?:null,!empty($allowB[$i]),!empty($allowD[$i]),++$n]);}
            if($n<2)throw new RuntimeException('Cadastre pelo menos duas paradas na rota.'); back_to('bus_trips','Rota salva.'); break;

        case 'save_bus_trip':
            must_role(['company','employee']);$companyId=bus_company_id_for_user($user);if($companyId<1)throw new RuntimeException('Empresa não encontrada.');$id=(int)request('id',0);$routeId=(int)request('route_id');$busId=(int)request('bus_id');$date=trim((string)request('trip_date'));$time=substr(trim((string)request('departure_time')),0,5);$price=(float)str_replace(',','.',request('base_price',0));
            if($routeId<1||$busId<1||!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)||!preg_match('/^\d{2}:\d{2}$/',$time))throw new RuntimeException('Preencha rota, ônibus, data e horário.');
            $q=db()->prepare('SELECT id FROM bus_routes WHERE id=? AND company_id=? AND status=\'active\'');$q->execute([$routeId,$companyId]);if(!$q->fetchColumn())throw new RuntimeException('Rota inválida.');$q=db()->prepare('SELECT id FROM bus_vehicles WHERE id=? AND company_id=? AND status=\'active\'');$q->execute([$busId,$companyId]);if(!$q->fetchColumn())throw new RuntimeException('Ônibus inválido.');
            if($id){db()->prepare('UPDATE bus_trips SET route_id=?,bus_id=?,trip_date=?,departure_time=?,base_price=?,status=?,updated_at=? WHERE id=? AND company_id=?')->execute([$routeId,$busId,$date,$time,$price,'draft',now(),$id]);}
            else{db()->prepare('INSERT INTO bus_trips(company_id,route_id,bus_id,trip_date,departure_time,base_price,status,created_at,updated_at) VALUES(?,?,?,?,?,?,\'draft\',?,?)')->execute([$companyId,$routeId,$busId,$date,$time,$price,now(),now()]);$id=(int)db()->lastInsertId();}
            db()->prepare('DELETE FROM bus_trip_stops WHERE trip_id=?')->execute([$id]);$q=db()->prepare('SELECT * FROM bus_route_stops WHERE route_id=? ORDER BY stop_order');$q->execute([$routeId]);$ins=db()->prepare('INSERT INTO bus_trip_stops(trip_id,route_stop_id,city,boarding_location,dropoff_location,estimated_time,allow_boarding,allow_dropoff,stop_order) VALUES(?,?,?,?,?,?,?,?,?)');foreach($q->fetchAll() as $st)$ins->execute([$id,$st['id'],$st['city'],$st['boarding_location'],$st['dropoff_location'],$st['estimated_time'],$st['allow_boarding'],$st['allow_dropoff'],$st['stop_order']]);
            back_to('bus_trips','Viagem salva como rascunho.');break;

        case 'publish_bus_trip':
            must_role(['company','employee']);$companyId=bus_company_id_for_user($user);$id=(int)request('id');$q=db()->prepare('SELECT t.*,r.name route_name,b.identification FROM bus_trips t JOIN bus_routes r ON r.id=t.route_id JOIN bus_vehicles b ON b.id=t.bus_id WHERE t.id=? AND t.company_id=?');$q->execute([$id,$companyId]);$trip=$q->fetch();if(!$trip)throw new RuntimeException('Viagem não encontrada.');$checks=[];$checks[]=(int)$trip['bus_id']>0;$checks[]=(int)$trip['route_id']>0;$q=db()->prepare('SELECT COUNT(*) FROM bus_seats WHERE bus_id=? AND active=1');$q->execute([$trip['bus_id']]);$checks[]=(int)$q->fetchColumn()>0;$q=db()->prepare('SELECT COUNT(*) FROM bus_trip_stops WHERE trip_id=?');$q->execute([$id]);$checks[]=(int)$q->fetchColumn()>=2;if(in_array(false,$checks,true))throw new RuntimeException('Ônibus, assentos e rota completa são obrigatórios para publicar.');db()->prepare("UPDATE bus_trips SET status='published',published_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$id]);back_to('bus_trips','Viagem publicada para os clientes.');break;

        case 'bus_book_trip':
            if(is_admin())throw new RuntimeException('Conta administrativa não pode comprar passagem.');$tripId=(int)request('trip_id');$passengerId=(int)request('passenger_id');$origin=(int)request('origin_stop_id');$dest=(int)request('destination_stop_id');$seatId=(int)request('seat_id');$method=(string)request('payment_method','pix');
            $q=db()->prepare("SELECT t.*,c.name company_name FROM bus_trips t JOIN companies c ON c.id=t.company_id WHERE t.id=? AND t.status='published' AND t.trip_date>=CURRENT_DATE");$q->execute([$tripId]);$trip=$q->fetch();if(!$trip)throw new RuntimeException('Viagem indisponível.');$q=db()->prepare('SELECT * FROM bus_trip_stops WHERE id IN (?,?) AND trip_id=? ORDER BY stop_order');$q->execute([$origin,$dest,$tripId]);$stops=$q->fetchAll();if(count($stops)!==2||$stops[0]['id']===$stops[1]['id']||$stops[0]['stop_order']>=$stops[1]['stop_order'])throw new RuntimeException('Origem e destino inválidos.');$q=db()->prepare('SELECT * FROM bus_passengers WHERE id=? AND (owner_user_id=? OR platform_user_id=?)');$q->execute([$passengerId,$user['id'],$user['id']]);$passenger=$q->fetch();if(!$passenger)throw new RuntimeException('Passageiro não encontrado.');if(bus_segment_occupied($tripId,$seatId,$origin,$dest))throw new RuntimeException('Este assento já está ocupado neste trecho. Escolha outro.');$q=db()->prepare('SELECT id FROM bus_seats WHERE id=? AND bus_id=? AND active=1');$q->execute([$seatId,$trip['bus_id']]);if(!$q->fetchColumn())throw new RuntimeException('Assento inválido.');$amount=(float)$trip['base_price'];$code=bus_generate_booking_code();$qr=bin2hex(random_bytes(16));$expires=date('Y-m-d H:i:s',time()+900);db()->prepare('INSERT INTO bus_bookings(booking_code,trip_id,passenger_id,seat_id,origin_stop_id,destination_stop_id,buyer_user_id,amount,payment_method,payment_status,status,temporary_expires_at,qr_token,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$code,$tripId,$passengerId,$seatId,$origin,$dest,$user['id'],$amount,$method,'pending','reserved',$expires,$qr,now(),now()]);$id=(int)db()->lastInsertId();back_to('bus_trips','Reserva criada. Conclua o pagamento para confirmar a passagem.');break;

        case 'bus_confirm_payment':
            if(is_admin())must_admin();$id=(int)request('id');$q=db()->prepare('SELECT b.*,t.company_id,t.trip_date,t.departure_time,p.full_name FROM bus_bookings b JOIN bus_trips t ON t.id=b.trip_id JOIN bus_passengers p ON p.id=b.passenger_id WHERE b.id=?');$q->execute([$id]);$b=$q->fetch();if(!$b)throw new RuntimeException('Reserva não encontrada.');if(!is_admin()&&(int)$b['buyer_user_id']!==(int)$user['id']&&(int)$b['company_id']!==(int)bus_company_id_for_user($user))throw new RuntimeException('Sem permissão.');db()->prepare("UPDATE bus_bookings SET payment_status='paid',status='confirmed',temporary_expires_at=NULL,updated_at=? WHERE id=?")->execute([now(),$id]);back_to('bus_trips','Pagamento confirmado e passagem liberada.');break;

        case 'bus_add_passenger':
            if(is_admin())throw new RuntimeException('Perfil inválido.');$name=trim((string)request('full_name'));if($name==='')throw new RuntimeException('Informe o nome do passageiro.');$cpf=trim((string)request('document'));$birth=trim((string)request('birth_date'));$phone=trim((string)request('phone'));$uid=(int)$user['id'];$q=db()->prepare('INSERT INTO bus_passengers(platform_user_id,owner_user_id,full_name,document,birth_date,phone,platform_public_id,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)');$q->execute([$uid,$uid,$name,$cpf?:null,$birth?:null,$phone?:null,public_user_id($uid),now(),now()]);back_to('bus_trips','Passageiro cadastrado.');break;

        case 'bus_boarding_event':
            must_role(['company','employee','shuttle_driver']);$bookingId=(int)request('booking_id');$type=request('event_type')==='dropoff'?'dropoff':'boarding';$stopId=(int)request('trip_stop_id');$q=db()->prepare('SELECT b.*,t.company_id FROM bus_bookings b JOIN bus_trips t ON t.id=b.trip_id WHERE b.id=?');$q->execute([$bookingId]);$b=$q->fetch();if(!$b)throw new RuntimeException('Passagem não encontrada.');if(!is_admin()&&(int)$b['company_id']!==bus_company_id_for_user($user) && (string)$user['role']!=='shuttle_driver')throw new RuntimeException('Sem permissão.');db()->prepare('INSERT INTO bus_boarding_events(booking_id,trip_stop_id,driver_user_id,event_type,status,created_at) VALUES(?,?,?,?,?,?)')->execute([$bookingId,$stopId,$user['id'],$type,'confirmed',now()]);db()->prepare('UPDATE bus_bookings SET status=?,updated_at=? WHERE id=?')->execute([$type==='boarding'?'boarded':'completed',now(),$bookingId]);back_to('bus_trips','Embarque/desembarque confirmado.');break;

        case 'create_ride':
            $type=request('ride_type');$allowed=['mototaxi','taxi','private_driver'];if(!in_array($type,$allowed,true))throw new RuntimeException('Tipo de corrida inválido.');
            $pickup=request('pickup');$destination=request('destination');if(!$pickup||!$destination)throw new RuntimeException('Informe origem e destino.');
            $distance=max(0,(float)str_replace(',','.',request('distance_km',0)));$duration=max(0,(int)request('duration_minutes',0));
            if($distance<=0&&request('pickup_lat')&&request('destination_lat')){$fallback=distance_km(request('pickup_lat'),request('pickup_lng'),request('destination_lat'),request('destination_lng'));if($fallback!==null)$distance=$fallback*1.25;}
            if($duration<=0&&$distance>0)$duration=(int)round($distance/30*60);
            $fare=ride_fare_settings($type);$priceKm=$fare['price_km'];$base=$fare['base'];$min=$fare['min'];$adminMinimum=max($min,$base+$distance*$priceKm);$autoFare=$fare['auto'];$estimated=$autoFare?$adminMinimum:0;
            $companyId=(int)request('ride_company_id',0);
            $driverId=0;
            $driverPublicId=strtoupper(trim((string)request('driver_public_id')));
            if($driverPublicId!==''){
                $dq=db()->prepare("SELECT id,name,role,status FROM users WHERE upper(public_id)=upper(?) LIMIT 1");$dq->execute([$driverPublicId]);$driver=$dq->fetch();
                if(!$driver||$driver['status']!=='active')throw new RuntimeException('Motorista não encontrado.');
                $compatible=($type==='mototaxi'&&$driver['role']==='mototaxi')||($type==='taxi'&&in_array($driver['role'],['taxi','private_driver'],true))||($type==='private_driver'&&in_array($driver['role'],['private_driver','taxi'],true));
                if(!$compatible)throw new RuntimeException('O ID informado não pertence a um profissional compatível com este tipo de corrida.');
                $driverId=(int)$driver['id'];
                $companyId=0;
            }
            $dispatch=$driverId>0?'direct':($companyId>0?'company':'open');
            if($companyId){
                $company=transport_company_info($companyId);
                if(!$company || ($company['status']??'')!=='active' || ($company['transport_kind']??null)!==$type)throw new RuntimeException('Empresa de transporte indisponível ou incompatível com o tipo de corrida.');
            }
            db()->prepare('INSERT INTO rides(client_id,company_id,driver_id,dispatch_mode,ride_type,pickup,destination,status,estimated_value,distance_km,payment_method,scheduled_at,pickup_lat,pickup_lng,destination_lat,destination_lng,duration_minutes,fare_per_km,admin_minimum_value,fare_review_status,pix_released,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
                ->execute([$user['id'],$companyId?:null,$driverId?:null,$dispatch,$type,$pickup,$destination,'requested',$estimated,$distance,request('payment_method','pix'),null,request('pickup_lat')?:null,request('pickup_lng')?:null,request('destination_lat')?:null,request('destination_lng')?:null,$duration,$priceKm,$adminMinimum,'not_required',0,now(),now()]);
            $rideId=(int)db()->lastInsertId();
            if(request('favorite_label'))db()->prepare('INSERT OR IGNORE INTO favorite_addresses(user_id,label,address,latitude,longitude,created_at) VALUES(?,?,?,?,?,?)')->execute([$user['id'],request('favorite_label'),$destination,request('destination_lat')?:null,request('destination_lng')?:null,now()]);
            if($driverId){
                create_notification($driverId,'Corrida direcionada para você',$pickup.' → '.$destination.' • '.number_format($distance,1,',','.').' km','success','dashboard.php?page=rides','ride',$rideId);
            }elseif($companyId){
                $ids=[];
                $uq=db()->prepare("SELECT id FROM users WHERE company_id=? AND role IN ('company','employee') AND status='active'");$uq->execute([$companyId]);foreach($uq->fetchAll() as $row)$ids[(int)$row['id']]=1;
                $fq=db()->prepare("SELECT user_id AS id FROM fleet_drivers WHERE company_id=? AND status='active'");$fq->execute([$companyId]);foreach($fq->fetchAll() as $row)$ids[(int)$row['id']]=1;
                foreach(array_keys($ids) as $notifyId)create_notification($notifyId,'Nova corrida da empresa',$pickup.' → '.$destination.' • '.($autoFare?money($estimated):'valor a informar pelo motorista'),'info','dashboard.php?page=rides','ride',$rideId);
            }else{
                $drivers=db()->prepare("SELECT id FROM users WHERE role=? AND status='active' AND work_status<>'offline' AND (city='' OR city IS NULL OR city=?)");$drivers->execute([$type,$user['city']??'']);
                foreach($drivers->fetchAll() as $d)create_notification((int)$d['id'],'Nova corrida disponível',$pickup.' → '.$destination.' • '.number_format($distance,1,',','.').' km • '.($autoFare?money($estimated):'valor a informar'),'info','dashboard.php?page=rides','ride',$rideId);
            }
            back_to('rides','Corrida solicitada com sucesso.');
            break;

        case 'update_ride':
            $fleetMemberQ=db()->prepare("SELECT COUNT(*) FROM fleet_drivers WHERE user_id=? AND status='active'");$fleetMemberQ->execute([$user['id']]);$isFleetMember=(int)$fleetMemberQ->fetchColumn()>0;if(!is_admin()&&!in_array($user['role'],['mototaxi','taxi','private_driver','company','employee'],true)&&!$isFleetMember)throw new RuntimeException('Sem permissão para atualizar corridas.');$id=(int)request('id');$status=request('status');
            $allowed=['accepted','on_route','in_progress','completed','cancelled'];if(!in_array($status,$allowed,true))throw new RuntimeException('Status inválido.');
            $q=db()->prepare('SELECT * FROM rides WHERE id=?');$q->execute([$id]);$ride=$q->fetch();if(!$ride)throw new RuntimeException('Corrida não encontrada.');
            if(in_array($ride['status'],['completed','cancelled'],true))throw new RuntimeException('Esta corrida já foi encerrada e não pode mais ser alterada.');if(in_array((string)$ride['status'],['fare_proposed','fare_review'],true)&&$status!=='cancelled')throw new RuntimeException('Aguarde o aceite do valor pelo cliente antes de iniciar a corrida.');
            $driverRoles=['mototaxi','taxi','private_driver'];$isDriver=in_array($user['role'],$driverRoles,true)||$isFleetMember;
            $driverId=(int)request('driver_id',0);
            if($isDriver)$driverId=(int)$user['id'];
            if(in_array($user['role'],['company','employee'],true)){
                if((int)$ride['company_id']!==(int)$user['company_id'])throw new RuntimeException('Corrida de outra empresa.');
                if(!$driverId)throw new RuntimeException('Selecione um motorista da frota.');
                $fc=db()->prepare("SELECT COUNT(*) FROM fleet_drivers WHERE company_id=? AND user_id=? AND status='active'");$fc->execute([$user['company_id'],$driverId]);if(!(int)$fc->fetchColumn())throw new RuntimeException('Motorista fora da frota ativa.');
            } elseif(is_admin() && $driverId){
                $fleetCompanyId=(int)($ride['company_id']?:request('company_id',0));
                if($fleetCompanyId<1)throw new RuntimeException('A corrida precisa estar vinculada a uma empresa antes do despacho.');
                $fc=db()->prepare("SELECT COUNT(*) FROM fleet_drivers WHERE company_id=? AND user_id=? AND status='active'");$fc->execute([$fleetCompanyId,$driverId]);if(!(int)$fc->fetchColumn())throw new RuntimeException('Motorista fora da frota ativa dessa empresa.');
            }
            if($isDriver){
                if(!$ride['driver_id'] && in_array($user['role'],$driverRoles,true) && $ride['ride_type']!==$user['role'])throw new RuntimeException('Tipo de corrida incompatível.');
                if(!$ride['driver_id'] && $isFleetMember && !in_array($user['role'],$driverRoles,true))throw new RuntimeException('Esta corrida precisa ser atribuída a você pela empresa antes de aceitar.');
                if($ride['driver_id'] && (int)$ride['driver_id']!==$driverId)throw new RuntimeException('Corrida aceita por outro motorista.');
                $blocked=db()->prepare('SELECT COUNT(*) FROM ride_driver_blocks WHERE ride_id=? AND driver_id=?');$blocked->execute([$id,$driverId]);if((int)$blocked->fetchColumn())throw new RuntimeException('Você cancelou esta corrida e ela foi liberada para outro motorista.');
            }
            if($status==='cancelled' && $isDriver){
                db()->beginTransaction();
                db()->prepare('INSERT OR IGNORE INTO ride_driver_blocks(ride_id,driver_id,reason,created_at) VALUES(?,?,?,?)')->execute([$id,$driverId,'Cancelada pelo motorista',now()]);
                db()->prepare('UPDATE rides SET driver_id=NULL,status="requested",driver_proposed_value=0,fare_review_status="not_required",client_fare_status="not_required",pix_released=0,driver_cancel_count=driver_cancel_count+1,updated_at=? WHERE id=?')->execute([now(),$id]);
                db()->commit();
                create_notification((int)$ride['client_id'],'Motorista cancelou a corrida','A corrida voltou a ficar disponível para outro motorista.','warning','dashboard.php?page=rides','ride',$id);
                back_to('rides','Corrida liberada novamente para outro motorista.');
            }
            $minimum=(float)($ride['admin_minimum_value']?:$ride['estimated_value']);$autoFare=ride_fare_settings((string)$ride['ride_type'])['auto'];
            $proposed=(float)str_replace(',','.',request('estimated_value',$ride['estimated_value']));if($autoFare&&$proposed<=0)$proposed=(float)$ride['estimated_value'];if(!$autoFare&&$status==='accepted'&&$proposed<=0)throw new RuntimeException('Informe o valor da corrida antes de aceitar.');if(!$autoFare&&$status!=='accepted'&&$proposed<=0)$proposed=(float)($ride['driver_proposed_value']?:$ride['estimated_value']);
            if($status==='accepted'&&!$autoFare&&$proposed+0.001<$minimum){
                db()->prepare('UPDATE rides SET driver_id=?,driver_proposed_value=?,status="fare_review",fare_review_status="pending",client_fare_status="waiting_admin",pix_released=0,updated_at=? WHERE id=?')->execute([$driverId,$proposed,now(),$id]);
                notify_super_admins('Tarifa de corrida para aprovação',$user['name'].' propôs '.money($proposed).' para a corrida #'.$id.'.','warning');
                create_notification((int)$ride['client_id'],'Corrida aguardando aprovação','Valor proposto: '.money($proposed).'.','warning');back_to('rides','Valor enviado para aprovação.');
            }
            if($status==='accepted'&&!$autoFare){
                db()->prepare('UPDATE rides SET driver_id=?,driver_proposed_value=?,status="fare_proposed",fare_review_status="approved",client_fare_status="pending",pix_released=0,updated_at=? WHERE id=?')->execute([$driverId,$proposed,now(),$id]);
                create_notification((int)$ride['client_id'],'Aceite o valor da corrida',$user['name'].' propôs '.money($proposed).'. Abra a corrida para aceitar ou recusar o valor.','warning','dashboard.php?page=rides','ride',$id);
                create_notification($driverId,'Proposta enviada ao cliente','Aguarde o cliente aceitar o valor de '.money($proposed).'.','info','dashboard.php?page=rides','ride',$id);
                back_to('rides','Valor enviado ao cliente para aceite.');
            }
            $finalValue=$autoFare?(float)$ride['estimated_value']:$proposed;$pixRelease=(($ride['payment_method']??'pix')==='pix'&&in_array($status,['accepted','on_route','in_progress','completed'],true))?1:(int)$ride['pix_released'];
            $terminal=$status==='completed'?now():null;
            db()->prepare('UPDATE rides SET driver_id=?,status=?,estimated_value=?,driver_proposed_value=?,fare_review_status="approved",pix_released=?,completed_at=CASE WHEN ? IS NOT NULL THEN ? ELSE completed_at END,terminal_at=CASE WHEN ? IS NOT NULL THEN ? ELSE terminal_at END,updated_at=? WHERE id=?')
                ->execute([$driverId,$status,$finalValue,$proposed,$pixRelease,$terminal,$terminal,$terminal,$terminal,now(),$id]);
            create_notification((int)$ride['client_id'],'Corrida atualizada','Status: '.status_label($status).' • Valor: '.money($finalValue),'info','dashboard.php?page=rides','ride',$id);
            if($status==='accepted'&&$driverId){release_pix_notification((int)$ride['client_id'],$driverId,'corrida contratada');create_notification($driverId,'Localização do passageiro liberada','Use Google Maps ou Waze na tela da corrida.','success','dashboard.php?page=rides','ride',$id);}
            back_to('rides','Corrida atualizada.');
            break;

        case 'upload_service_payment_proof':
            $id=(int)request('id',0);
            $q=db()->prepare('SELECT sr.*,s.name service_name FROM service_requests sr JOIN services s ON s.id=sr.service_id WHERE sr.id=? AND sr.client_id=?');$q->execute([$id,$user['id']]);$req=$q->fetch();
            if(!$req)throw new RuntimeException('Serviço contratado não encontrado.');
            if($req['status']!=='completed'||empty($req['client_completion_confirmed']))throw new RuntimeException('Confirme primeiro que o trabalho foi realizado. Depois a forma de pagamento será liberada.');
            $pq=db()->prepare('SELECT * FROM users WHERE id=?');$pq->execute([(int)$req['provider_id']]);$provider=$pq->fetch()?:[];$destination=service_payment_destination($provider);
            if(($destination['channel']??'none')==='none')throw new RuntimeException('Nenhuma forma de pagamento está disponível para este prestador. Fale com o administrador.');
            $proof=upload_document('proof_file','comprovantes-servicos',$req['payment_proof']??'');if($proof==='')throw new RuntimeException('Selecione uma imagem ou PDF.');
            db()->prepare("UPDATE service_requests SET payment_proof=?,payment_channel=?,payment_status='proof_sent',updated_at=? WHERE id=?")->execute([$proof,(string)$destination['channel'],now(),$id]);
            create_notification((int)$req['provider_id'],'Comprovante de serviço enviado','O cliente enviou o comprovante do serviço '.$req['service_name'].'.','success','dashboard.php?page=services','service_request',$id);
            back_to('services','Comprovante enviado ao profissional.');
            break;

        case 'confirm_service_payment':
            $id=(int)request('id',0);$q=db()->prepare('SELECT sr.*,s.name service_name FROM service_requests sr JOIN services s ON s.id=sr.service_id WHERE sr.id=?');$q->execute([$id]);$req=$q->fetch();
            if(!$req)throw new RuntimeException('Serviço não encontrado.');if(!is_admin()&&(int)$req['provider_id']!==(int)$user['id'])throw new RuntimeException('Sem permissão.');if((float)$req['amount']<=0)throw new RuntimeException('Defina o valor final do serviço antes de confirmar o pagamento.');if(empty($req['client_completion_confirmed']))throw new RuntimeException('O cliente ainda não confirmou que o trabalho foi realizado.');if(empty($req['payment_proof']))throw new RuntimeException('O cliente ainda não enviou o comprovante do pagamento.');
            db()->prepare('UPDATE service_requests SET provider_payment_confirmed=1,payment_confirmed_by=?,paid_at=COALESCE(paid_at,?),updated_at=? WHERE id=?')->execute([(int)$user['id'],now(),now(),$id]);
            $settled=service_settlement_if_ready($id);
            create_notification((int)$req['client_id'],'Profissional confirmou o pagamento',$settled?'O valor foi liquidado porque você também já confirmou a conclusão.':'Agora confirme que o serviço foi realizado para liberar o valor na carteira do profissional.','info','dashboard.php?page=services','service_request',$id);
            back_to('services',$settled?'Pagamento liquidado e valor liberado na carteira.':'Pagamento confirmado pelo prestador. Falta a confirmação do cliente.');
            break;

        case 'confirm_service_completion':
            $id=(int)request('id',0);$q=db()->prepare('SELECT sr.*,s.name service_name FROM service_requests sr JOIN services s ON s.id=sr.service_id WHERE sr.id=? AND sr.client_id=?');$q->execute([$id,(int)$user['id']]);$req=$q->fetch();
            if(!$req)throw new RuntimeException('Serviço contratado não encontrado.');if($req['status']!=='completed')throw new RuntimeException('O profissional ainda não marcou o serviço como concluído.');
            db()->prepare('UPDATE service_requests SET client_completion_confirmed=1,pix_released=1,updated_at=? WHERE id=?')->execute([now(),$id]);
            $pq=db()->prepare('SELECT * FROM users WHERE id=?');$pq->execute([(int)$req['provider_id']]);$provider=$pq->fetch()?:[];$destination=service_payment_destination($provider);
            $hasPayment=($destination['channel']??'none')!=='none';
            create_notification((int)$req['provider_id'],'Cliente confirmou o serviço',$hasPayment?'O cliente confirmou a conclusão. A forma de pagamento foi liberada para ele; aguarde o comprovante para confirmar o recebimento.':'O cliente confirmou a conclusão, mas não há forma de pagamento configurada. Verifique com o administrador.',$hasPayment?'success':'warning','dashboard.php?page=services','service_request',$id);
            back_to('services',$hasPayment?'Trabalho confirmado. A forma de pagamento foi liberada abaixo; toque em “Pagar serviço”.':'Trabalho confirmado, mas ainda não existe forma de pagamento liberada para este prestador.');
            break;

        case 'client_review_ride_fare':
            $id=(int)request('id');$decision=(string)request('decision');if(!in_array($decision,['accept','reject'],true))throw new RuntimeException('Resposta inválida.');$q=db()->prepare("SELECT * FROM rides WHERE id=? AND client_id=? AND status='fare_proposed' AND client_fare_status='pending'");$q->execute([$id,(int)$user['id']]);$ride=$q->fetch();if(!$ride)throw new RuntimeException('Esta proposta não está mais aguardando seu aceite.');$driverId=(int)$ride['driver_id'];$value=(float)$ride['driver_proposed_value'];if($decision==='accept'){$pixRelease=($ride['payment_method']??'pix')==='pix'?1:0;db()->prepare("UPDATE rides SET status='accepted',estimated_value=?,client_fare_status='accepted',client_fare_responded_at=?,pix_released=?,updated_at=? WHERE id=?")->execute([$value,now(),$pixRelease,now(),$id]);if($driverId){create_notification($driverId,'Valor aceito pelo cliente',$user['name'].' aceitou a corrida por '.money($value).'. A localização do passageiro está liberada.','success','dashboard.php?page=rides','ride',$id);if($pixRelease)release_pix_notification((int)$user['id'],$driverId,'corrida contratada');}back_to('rides','Valor aceito. Corrida confirmada.');}
            db()->prepare("UPDATE rides SET driver_id=NULL,status='requested',driver_proposed_value=0,fare_review_status='not_required',client_fare_status='rejected',client_fare_responded_at=?,pix_released=0,updated_at=? WHERE id=?")->execute([now(),now(),$id]);if($driverId)create_notification($driverId,'Valor recusado pelo cliente',$user['name'].' não aceitou a proposta de '.money($value).'. A corrida voltou a ficar disponível.','warning','dashboard.php?page=rides','ride',$id);back_to('rides','Valor recusado. A corrida foi liberada para outra proposta.');break;

        case 'upload_ride_payment_proof':
            $id=(int)request('id',0);$q=db()->prepare('SELECT * FROM rides WHERE id=? AND client_id=?');$q->execute([$id,$user['id']]);$ride=$q->fetch();
            if(!$ride||!(int)$ride['driver_id'])throw new RuntimeException('Corrida não encontrada ou ainda sem motorista.');
            if(!in_array($ride['status'],['accepted','on_route','in_progress','completed'],true))throw new RuntimeException('A corrida ainda não aceita pagamento.');
            if(($ride['payment_method']??'pix')==='cash')throw new RuntimeException('Pagamento em dinheiro deve ser confirmado diretamente pelo motorista.');
            $proof=upload_document('proof_file','comprovantes-corridas',$ride['payment_proof']??'');if($proof==='')throw new RuntimeException('Selecione uma imagem ou PDF.');
            db()->prepare("UPDATE rides SET payment_proof=?,payment_status='proof_sent',updated_at=? WHERE id=?")->execute([$proof,now(),$id]);
            create_notification((int)$ride['driver_id'],'Comprovante de corrida enviado','O passageiro enviou um comprovante de pagamento.','success','dashboard.php?page=rides','ride',$id);
            back_to('rides','Comprovante enviado ao motorista.');
            break;

        case 'confirm_ride_payment':
            $id=(int)request('id',0);$q=db()->prepare('SELECT * FROM rides WHERE id=?');$q->execute([$id]);$ride=$q->fetch();
            if(!$ride||!(int)$ride['driver_id'])throw new RuntimeException('Corrida não encontrada.');
            if(!is_admin()&&(int)$ride['driver_id']!==(int)$user['id'])throw new RuntimeException('Sem permissão.');
            $amount=(float)($ride['estimated_value']?:$ride['driver_proposed_value']);if($amount<=0)throw new RuntimeException('A corrida está sem valor.');
            $feeBreakdown=calculate_platform_fee((int)$ride['driver_id'],$amount,'ride',$id);$receipt=generate_transaction_receipt_png('ride',$id,(int)$ride['client_id'],(int)$ride['driver_id'],(float)$feeBreakdown['total'],'Corrida '.$ride['pickup'].' → '.$ride['destination'].($feeBreakdown['fee']>0?' • taxa '.money($feeBreakdown['fee']).' paga por '.(($feeBreakdown['fee_payer']??'client')==='provider'?'prestador/empresa':'cliente'):''),(string)($ride['payment_method']?:'pix'),(string)($ride['payment_proof']??''),(int)($ride['company_id']??0));
            post_finance_pair('ride',$id,(int)$ride['client_id'],(int)$ride['driver_id'],$amount,(string)($ride['payment_method']?:'pix'),(string)($ride['payment_proof']??''),$receipt,'Corrida '.$ride['pickup'].' → '.$ride['destination'],(int)($ride['company_id']??0));
            db()->prepare("UPDATE rides SET payment_status='paid',receipt_path=?,paid_at=?,payment_confirmed_by=?,updated_at=? WHERE id=?")->execute([$receipt,now(),(int)$user['id'],now(),$id]);
            create_notification((int)$ride['client_id'],'Pagamento da corrida confirmado','O motorista confirmou o recebimento.','success','dashboard.php?page=rides','ride',$id);
            back_to('rides','Pagamento confirmado e comprovante gerado.');
            break;

        case 'upload_vehicle_reservation_payment_proof':
            $id=(int)request('id',0);$q=db()->prepare('SELECT vr.*,v.brand,v.model,v.owner_user_id,v.company_id FROM vehicle_reservations vr JOIN vehicles v ON v.id=vr.vehicle_id WHERE vr.id=? AND vr.client_id=?');$q->execute([$id,$user['id']]);$res=$q->fetch();
            if(!$res)throw new RuntimeException('Reserva não encontrada.');
            $proof=upload_document('proof_file','comprovantes-locacoes',$res['payment_proof']??'');if($proof==='')throw new RuntimeException('Selecione uma imagem ou PDF.');
            db()->prepare("UPDATE vehicle_reservations SET payment_proof=?,payment_status='proof_sent' WHERE id=?")->execute([$proof,$id]);
            $recipient=(int)$res['owner_user_id'];if((int)$res['company_id']>0){$cq=db()->prepare('SELECT owner_user_id FROM companies WHERE id=?');$cq->execute([(int)$res['company_id']]);$recipient=(int)($cq->fetchColumn()?:$recipient);}if($recipient)create_notification($recipient,'Comprovante de locação enviado','O cliente enviou comprovante da reserva de '.$res['brand'].' '.$res['model'].'.','success','dashboard.php?page=vehicles','vehicle_reservation',$id);
            back_to('vehicles','Comprovante enviado à locadora.');
            break;

        case 'save_property':
            must_role('property_owner');
            $id=(int)request('id',0);$ownerId=is_admin()?(int)request('owner_id'):(int)$user['id'];if(!$id&&!is_admin()){$limitInfo=publication_limit_status((int)$ownerId);if($limitInfo['reached'])throw new RuntimeException('Limite de publicações atingido ('.$limitInfo['used'].'/'.$limitInfo['limit'].').');}$existingImage='';
            if($id){$imgq=db()->prepare('SELECT image,owner_id FROM properties WHERE id=?');$imgq->execute([$id]);$existingRow=$imgq->fetch();if(!$existingRow)throw new RuntimeException('Cadastro não encontrado.');if(!is_admin()&&(int)$existingRow['owner_id']!==(int)$user['id'])throw new RuntimeException('Sem permissão.');$existingImage=(string)$existingRow['image'];}
            $hasNewImage=isset($_FILES['image_file'])&&is_array($_FILES['image_file'])&&(int)($_FILES['image_file']['error']??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_NO_FILE;
            if(request('remove_image')&&!$hasNewImage){delete_local_upload($existingImage);$image='';}else{$image=upload_image('image_file','imoveis',$existingImage);}
            $propertyKind=request('property_kind','property');if(!in_array($propertyKind,['property','pool'],true))$propertyKind='property';
            $type=$propertyKind==='pool'?'pool':request('type','house');
            $weekdays=request('available_weekdays',[]);if(!is_array($weekdays))$weekdays=[];$weekdays=implode(',',array_values(array_intersect(['0','1','2','3','4','5','6'],$weekdays)));
            $paymentLink=trim((string)request('payment_link'));$pixKey=trim((string)request('pix_key'));
            if($pixKey===''){$oq=db()->prepare('SELECT pix_key FROM users WHERE id=?');$oq->execute([$ownerId]);$pixKey=(string)$oq->fetchColumn();}
            $data=[$ownerId,request('title'),$type,request('purpose','rent'),request('description'),request('address'),request('city'),normalize_state(request('state')),(float)str_replace(',','.',request('price',0)),(int)request('bedrooms',0),(int)request('bathrooms',0),(int)request('garage',0),(float)str_replace(',','.',request('area',0)),$image,request('status','available'),request('latitude')?:null,request('longitude')?:null,$propertyKind,$weekdays,$paymentLink,$pixKey];
            if(!$data[1])throw new RuntimeException('Informe o título.');
            if($id){db()->prepare('UPDATE properties SET owner_id=?,title=?,type=?,purpose=?,description=?,address=?,city=?,state=?,price=?,bedrooms=?,bathrooms=?,garage=?,area=?,image=?,status=?,latitude=?,longitude=?,property_kind=?,available_weekdays=?,payment_link=?,pix_key=?,updated_at=? WHERE id=?')->execute(array_merge($data,[now(),$id]));}
            else{db()->prepare('INSERT INTO properties(owner_id,title,type,purpose,description,address,city,state,price,bedrooms,bathrooms,garage,area,image,status,latitude,longitude,property_kind,available_weekdays,payment_link,pix_key,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute(array_merge($data,[now(),now()]));$id=(int)db()->lastInsertId();}
            back_to('properties',$propertyKind==='pool'?'Piscina salva com agenda de disponibilidade.':'Imóvel salvo com localização.');
            break;

        case 'delete_property_image':
            must_role('property_owner');
            $id=(int)request('id',0);$q=db()->prepare('SELECT owner_id,image FROM properties WHERE id=?');$q->execute([$id]);$prop=$q->fetch();
            if(!$prop)throw new RuntimeException('Anúncio não encontrado.');
            if(!is_admin()&&(int)$prop['owner_id']!==(int)$user['id'])throw new RuntimeException('Sem permissão.');
            delete_local_upload((string)$prop['image']);db()->prepare("UPDATE properties SET image='',updated_at=? WHERE id=?")->execute([now(),$id]);
            back_to('properties','Imagem do anúncio removida.');
            break;

        case 'delete_property':
            must_role('property_owner');
            $id=(int)request('id',0);$q=db()->prepare('SELECT owner_id,image,title FROM properties WHERE id=?');$q->execute([$id]);$prop=$q->fetch();
            if(!$prop)throw new RuntimeException('Anúncio não encontrado.');
            if(!is_admin()&&(int)$prop['owner_id']!==(int)$user['id'])throw new RuntimeException('Sem permissão.');
            $files=db()->prepare('SELECT proof,receipt_path FROM property_bookings WHERE property_id=?');$files->execute([$id]);foreach($files->fetchAll() as $f){delete_local_upload((string)($f['proof']??''));delete_local_upload((string)($f['receipt_path']??''));}
            db()->prepare('DELETE FROM property_bookings WHERE property_id=?')->execute([$id]);db()->prepare('DELETE FROM properties WHERE id=?')->execute([$id]);delete_local_upload((string)$prop['image']);
            log_activity('delete_property','properties',$id,(string)$prop['title']);back_to('properties','Anúncio apagado.');
            break;

        case 'upload_property_booking_proof':
            $id=(int)request('id',0);$q=db()->prepare('SELECT pb.*,p.owner_id,p.title FROM property_bookings pb JOIN properties p ON p.id=pb.property_id WHERE pb.id=? AND pb.client_id=?');$q->execute([$id,$user['id']]);$book=$q->fetch();
            if(!$book)throw new RuntimeException('Agendamento não encontrado.');
            if(in_array($book['status'],['rejected','cancelled'],true))throw new RuntimeException('Este agendamento não aceita mais comprovante.');
            $proof=upload_document('proof_file','comprovantes-imoveis/'.$book['property_id'],$book['proof']??'');if($proof==='')throw new RuntimeException('Selecione uma imagem ou PDF.');
            db()->prepare("UPDATE property_bookings SET proof=?,payment_status='proof_sent',updated_at=? WHERE id=?")->execute([$proof,now(),$id]);
            create_notification((int)$book['owner_id'],'Comprovante recebido',$user['name'].' enviou comprovante para “'.$book['title'].'”.','success','dashboard.php?page=property_agenda&property_id='.(int)$book['property_id'],'property_booking',$id);
            notify_super_admins('Comprovante de imóvel/piscina',$user['name'].' enviou comprovante para “'.$book['title'].'”.','success',(int)$book['owner_id'],'dashboard.php?page=property_agenda&property_id='.(int)$book['property_id'],'property_booking',$id);
            flash('success','Comprovante enviado para o proprietário.');if($returnPage==='properties')redirect('dashboard.php?page=properties');redirect('dashboard.php?page=property_agenda&property_id='.(int)$book['property_id']);
            break;

        case 'confirm_property_booking_payment':
            $id=(int)request('id',0);$q=db()->prepare('SELECT pb.*,p.owner_id,p.title,p.property_kind FROM property_bookings pb JOIN properties p ON p.id=pb.property_id WHERE pb.id=?');$q->execute([$id]);$book=$q->fetch();
            if(!$book)throw new RuntimeException('Agendamento não encontrado.');
            if(!is_admin()&&(int)$book['owner_id']!==(int)$user['id'])throw new RuntimeException('Somente o proprietário ou administrador pode confirmar.');
            $amount=(float)$book['amount'];$description=(($book['property_kind']??'property')==='pool'?'Aluguel de piscina: ':'Imóvel: ').$book['title'];
            $feeBreakdown=calculate_platform_fee((int)$book['owner_id'],$amount,'property_booking',$id);
            $receipt=generate_transaction_receipt_png('property_booking',$id,(int)$book['client_id'],(int)$book['owner_id'],(float)$feeBreakdown['total'],$description.($feeBreakdown['fee']>0?' • taxa '.money($feeBreakdown['fee']).' paga por '.(($feeBreakdown['fee_payer']??'client')==='provider'?'prestador/empresa':'cliente'):''),'pix',(string)($book['proof']??''),0);
            db()->prepare("UPDATE property_bookings SET payment_status='paid',paid_at=?,payment_confirmed_by=?,receipt_path=?,updated_at=? WHERE id=?")->execute([now(),$user['id'],$receipt,now(),$id]);
            post_finance_pair('property_booking',$id,(int)$book['client_id'],(int)$book['owner_id'],$amount,'pix',(string)($book['proof']??''),$receipt,$description,0);
            create_notification((int)$book['client_id'],'Pagamento confirmado','Pagamento de '.money($amount).' confirmado. O comprovante já está disponível.','success','dashboard.php?page=property_agenda&property_id='.(int)$book['property_id'],'property_booking',$id);
            flash('success','Pagamento confirmado e comprovante gerado.');if($returnPage==='properties')redirect('dashboard.php?page=properties');redirect('dashboard.php?page=property_agenda&property_id='.(int)$book['property_id']);
            break;

        case 'request_property':
            $propertyId=(int)request('property_id');
            $q=db()->prepare('SELECT * FROM properties WHERE id=? AND status="available"');$q->execute([$propertyId]);$prop=$q->fetch();
            if(!$prop)throw new RuntimeException('Imóvel indisponível.');
            $bookingType=request('booking_type','visit');
            $preferredDate=request('start_date')?:null;
            if(($prop['property_kind']??'property')==='pool'){
                $bookingType='pool';
                if(!$preferredDate)throw new RuntimeException('Escolha a data para alugar a piscina.');
                $allowedDays=array_filter(explode(',',(string)($prop['available_weekdays']??'')));
                $weekday=(string)date('w',strtotime($preferredDate));
                if($allowedDays&&!in_array($weekday,$allowedDays,true))throw new RuntimeException('A piscina não está disponível nesse dia da semana.');
                $busy=db()->prepare("SELECT COUNT(*) FROM property_bookings WHERE property_id=? AND start_date=? AND status IN ('requested','approved','scheduled')");
                $busy->execute([$propertyId,$preferredDate]);
                if((int)$busy->fetchColumn()>0)throw new RuntimeException('Essa data já possui reserva ou solicitação em andamento.');
            }
            db()->prepare('INSERT INTO property_bookings(property_id,client_id,booking_type,status,start_date,end_date,amount,notes,created_at) VALUES(?,?,?,?,?,?,?,?,?)')->execute([$propertyId,$user['id'],$bookingType,'requested',$preferredDate,request('end_date')?:null,$prop['price'],request('notes'),now()]);
            $typeLabel=['visit'=>'visita','rent'=>'aluguel','purchase'=>'compra','pool'=>'reserva de piscina'];
            $label=isset($typeLabel[$bookingType])?$typeLabel[$bookingType]:'interesse';
            $message=$user['name'].' enviou uma solicitação de '.$label.' para o imóvel “'.$prop['title'].'”'.($preferredDate?' em '.date('d/m/Y',strtotime($preferredDate)):'').'.';
            create_notification((int)$prop['owner_id'],$prop['property_kind']==='pool'?'Nova reserva de piscina':'Novo interesse em imóvel',$message,'success','dashboard.php?page=properties','property_booking',(int)db()->lastInsertId());
            notify_super_admins('Novo interesse em imóvel',$message,'success',(int)$prop['owner_id'],'dashboard.php?page=properties','property_booking',(int)db()->lastInsertId());
            log_activity('request_property','properties',$propertyId,$message);
            flash('success','Solicitação enviada. O proprietário e a administração foram notificados.');
            if($returnPage==='property_agenda')redirect('dashboard.php?page=property_agenda&property_id='.$propertyId);
            redirect('dashboard.php?page=properties');
            break;

        case 'update_property_booking':
            must_role('property_owner');
            $id=(int)request('id');$status=request('status');
            $q=db()->prepare('SELECT pb.*,p.owner_id,p.title FROM property_bookings pb JOIN properties p ON p.id=pb.property_id WHERE pb.id=?');$q->execute([$id]);$book=$q->fetch();
            if(!$book)throw new RuntimeException('Solicitação não encontrada.');
            if(!is_admin()&&(int)$book['owner_id']!==(int)$user['id'])throw new RuntimeException('Sem permissão.');
            db()->prepare('UPDATE property_bookings SET status=? WHERE id=?')->execute([$status,$id]);
            $statusLabels=['approved'=>'aprovada','scheduled'=>'visita agendada','completed'=>'concluída','rejected'=>'recusada'];
            $statusLabel=isset($statusLabels[$status])?$statusLabels[$status]:$status;
            create_notification((int)$book['client_id'],'Solicitação atualizada','Sua solicitação para “'.$book['title'].'” foi '.$statusLabel.'.','info','dashboard.php?page=properties','property_booking',$id);
            flash('success','Solicitação atualizada e cliente notificado.');
            if($returnPage==='property_agenda')redirect('dashboard.php?page=property_agenda&property_id='.(int)$book['property_id']);
            redirect('dashboard.php?page=properties');
            break;

        case 'save_vehicle':
            must_role(['rental_company','company','employee']);
            $id=(int)request('id',0);
            $companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            $ownerId=is_admin()?(int)request('owner_user_id'):(int)$user['id'];
            if(!$id&&!is_admin()){$limitInfo=publication_limit_status((int)$ownerId);if($limitInfo['reached'])throw new RuntimeException('Limite de publicações atingido ('.$limitInfo['used'].'/'.$limitInfo['limit'].').');}
            $existingImage='';
            if($id){
                $imgq=db()->prepare('SELECT image,owner_user_id,company_id FROM vehicles WHERE id=?');
                $imgq->execute([$id]);
                $existing=$imgq->fetch();
                if(!$existing)throw new RuntimeException('Veículo não encontrado.');
                if(!is_admin() && (int)$existing['owner_user_id']!==(int)$user['id'] && (int)$existing['company_id']!==(int)$user['company_id'])throw new RuntimeException('Sem permissão.');
                $existingImage=(string)$existing['image'];
            }
            $image=upload_image('image_file','veiculos',$existingImage);
            $listingMode=request('listing_mode','rental');
            if(!in_array($listingMode,['rental','sale'],true))$listingMode='rental';
            if(!is_admin()){
                $companyType=(string)($user['company_type']??'');
                if($listingMode==='sale' && $companyType!=='vehicle_store')throw new RuntimeException('Somente empresas cadastradas como Loja de Veículos podem anunciar para venda.');
                if($listingMode==='rental' && !is_role('rental_company') && $companyType!=='rental')throw new RuntimeException('Somente locadoras cadastradas podem anunciar veículos para aluguel.');
            }
            $status=request('status','available');
            $data=[
                $companyId?:null,$ownerId,request('type','car'),request('brand'),request('model'),(int)request('year'),
                request('plate'),request('color'),(float)str_replace(',','.',request('daily_rate',0)),
                (float)str_replace(',','.',request('mileage',0)),$image,$status,$listingMode,
                request('transmission'),request('fuel'),request('chassis'),
                (float)str_replace(',','.',request('sale_price',0)),request('video_url'),request('options_text')
            ];
            if(!$data[3]||!$data[4])throw new RuntimeException('Marca e modelo são obrigatórios.');
            if($listingMode==='sale' && $data[16]<=0)throw new RuntimeException('Informe o preço de venda.');
            if($id){
                db()->prepare('UPDATE vehicles SET company_id=?,owner_user_id=?,type=?,brand=?,model=?,year=?,plate=?,color=?,daily_rate=?,mileage=?,image=?,status=?,listing_mode=?,transmission=?,fuel=?,chassis=?,sale_price=?,video_url=?,options_text=?,sold_at=CASE WHEN ?="sold" THEN COALESCE(sold_at,?) ELSE sold_at END,updated_at=? WHERE id=?')
                    ->execute(array_merge($data,[$status,$status==='sold'?now():null,now(),$id]));
            }else{
                db()->prepare('INSERT INTO vehicles(company_id,owner_user_id,type,brand,model,year,plate,color,daily_rate,mileage,image,status,listing_mode,transmission,fuel,chassis,sale_price,video_url,options_text,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
                    ->execute(array_merge($data,[now(),now()]));
                $id=(int)db()->lastInsertId();
            }
            $gallery=upload_image_gallery('gallery_files','veiculos/galeria');
            if($gallery){
                $sort=(int)db()->query('SELECT COALESCE(MAX(sort_order),0) FROM vehicle_media WHERE vehicle_id='.(int)$id)->fetchColumn();
                $stmt=db()->prepare('INSERT INTO vehicle_media(vehicle_id,image,sort_order,created_at) VALUES(?,?,?,?)');
                foreach($gallery as $path){$sort++;$stmt->execute([$id,$path,$sort,now()]);}
            }
            log_activity('save_vehicle','vehicles',$id,$data[3].' '.$data[4]);
            back_to($listingMode==='sale'?'vehicle_store':'vehicles','Veículo salvo com sucesso.');
            break;

        case 'delete_vehicle':
            must_role(['rental_company','company','employee']);
            $id=(int)request('id');
            $q=db()->prepare('SELECT id,company_id,owner_user_id,listing_mode FROM vehicles WHERE id=?');$q->execute([$id]);$v=$q->fetch();
            if(!$v)throw new RuntimeException('Veículo não encontrado.');
            if(!is_admin()&&(int)$v['company_id']!==(int)$user['company_id']&&(int)$v['owner_user_id']!==(int)$user['id'])throw new RuntimeException('Sem permissão.');
            db()->prepare('DELETE FROM vehicle_media WHERE vehicle_id=?')->execute([$id]);
            db()->prepare('DELETE FROM vehicles WHERE id=?')->execute([$id]);
            back_to($v['listing_mode']==='sale'?'vehicle_store':'vehicles','Anúncio do veículo apagado.');
            break;

        case 'reserve_vehicle':
            $vehicleId=(int)request('vehicle_id');$start=request('start_date');$end=request('end_date');if(!$start||!$end||$end<$start)throw new RuntimeException('Informe um período válido.');$q=db()->prepare('SELECT v.*,c.owner_user_id AS store_owner FROM vehicles v LEFT JOIN companies c ON c.id=v.company_id WHERE v.id=? AND v.status="available" AND v.listing_mode="rental"');$q->execute([$vehicleId]);$vehicle=$q->fetch();if(!$vehicle)throw new RuntimeException('Veículo indisponível.');$days=max(1,(int)((strtotime($end)-strtotime($start))/86400)+1);$total=$days*(float)$vehicle['daily_rate'];
            $residence=upload_document('residence_proof','locacoes/residencia');$license=upload_document('driver_license','locacoes/cnh');if(!$residence||!$license)throw new RuntimeException('Envie o comprovante de residência e a CNH.');
            db()->prepare('INSERT INTO vehicle_reservations(vehicle_id,client_id,status,start_date,end_date,total,payment_status,notes,residence_proof,driver_license,client_phone,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$vehicleId,$user['id'],'requested',$start,$end,$total,'pending',request('notes'),$residence,$license,only_digits($user['phone']),now()]);
            $notifyId=$vehicle['owner_user_id']?:$vehicle['store_owner'];if($notifyId)create_notification((int)$notifyId,'Nova reserva','Reserva solicitada para '.$vehicle['brand'].' '.$vehicle['model'].' no valor de '.money($total).'.','success','dashboard.php?page=vehicles','vehicle_reservation',(int)db()->lastInsertId());back_to('vehicles','Reserva solicitada e documentos enviados.');
            break;

        case 'update_vehicle_reservation':
            must_role(['rental_company','company','employee']);$id=(int)request('id');$status=request('status');
            if(!in_array($status,['approved','reserved','completed','rejected','cancelled'],true))throw new RuntimeException('Status inválido.');
            $q=db()->prepare('SELECT vr.*,v.owner_user_id,v.company_id,v.brand,v.model FROM vehicle_reservations vr JOIN vehicles v ON v.id=vr.vehicle_id WHERE vr.id=?');$q->execute([$id]);$res=$q->fetch();if(!$res)throw new RuntimeException('Reserva não encontrada.');
            if(!is_admin()&&(int)$res['owner_user_id']!==(int)$user['id']&&(int)$res['company_id']!==(int)$user['company_id'])throw new RuntimeException('Sem permissão.');
            $paymentStatus=request('payment_status',$res['payment_status']);
            db()->prepare('UPDATE vehicle_reservations SET status=?,payment_status=? WHERE id=?')->execute([$status,$paymentStatus,$id]);
            if($paymentStatus==='paid'&&$res['payment_status']!=='paid'){$vq=db()->prepare('SELECT v.company_id,v.owner_user_id,v.brand,v.model,c.owner_user_id company_owner FROM vehicles v LEFT JOIN companies c ON c.id=v.company_id WHERE v.id=?');$vq->execute([$res['vehicle_id']]);$veh=$vq->fetch();$recipient=(int)($veh['company_owner']?:$veh['owner_user_id']);if($recipient>0){$proof=(string)($res['payment_proof']??'');$feeBreakdown=calculate_platform_fee($recipient,(float)$res['total'],'vehicle_rental',$id);$description='Locação '.$veh['brand'].' '.$veh['model'];$receipt=generate_transaction_receipt_png('vehicle_rental',$id,(int)$res['client_id'],$recipient,(float)$feeBreakdown['total'],$description.($feeBreakdown['fee']>0?' • taxa '.money($feeBreakdown['fee']).' paga por '.(($feeBreakdown['fee_payer']??'client')==='provider'?'prestador/empresa':'cliente'):''),'pix',$proof,(int)$veh['company_id']);post_finance_pair('vehicle_rental',$id,(int)$res['client_id'],$recipient,(float)$res['total'],'pix',$proof,$receipt,$description,(int)$veh['company_id']);db()->prepare('UPDATE vehicle_reservations SET receipt_path=?,paid_at=?,payment_confirmed_by=? WHERE id=?')->execute([$receipt,now(),(int)$user['id'],$id]);}}
            create_notification((int)$res['client_id'],'Reserva de veículo atualizada','A reserva de '.$res['brand'].' '.$res['model'].' agora está com status '.status_label($status).'.','info','dashboard.php?page=vehicles','vehicle_reservation',$id);
            back_to('vehicles','Reserva atualizada e cliente notificado.');
            break;

        case 'save_payment':
            if(!is_admin() && !is_role(['company','client','rental_company','property_owner','service_provider','courier','motofreight','mototaxi','taxi','private_driver','moving_provider','employee']))throw new RuntimeException('Sem permissão.');
            $payerId=is_admin()?(int)request('payer_user_id',0):(int)$user['id'];$companyId=is_admin()?(int)request('company_id',0):(int)($user['company_id']??0);
            $proof=upload_document('proof_file','comprovantes','');$refType=request('reference_type','subscription');$entryKind=$refType==='expense'?'expense':($refType==='income'?'income':null);$payStatus=is_admin()?request('status','pending'):'pending';
            db()->prepare('INSERT INTO payments(owner_user_id,payer_user_id,company_id,reference_type,reference_id,amount,method,status,due_date,paid_at,proof,notes,created_at,entry_kind) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
                ->execute([(int)$user['id'],$payerId?:null,$companyId?:null,$refType,(int)request('reference_id',0)?:null,(float)str_replace(',','.',request('amount',0)),request('method','pix'),$payStatus,request('due_date')?:null,$payStatus==='paid'?now():null,$proof,request('notes'),now(),$entryKind]);
            back_to('finance','Movimentação registrada.');
            break;

        case 'update_payment':
            must_admin();$id=(int)request('id');$status=request('status');db()->prepare("UPDATE payments SET status=?,paid_at=CASE WHEN ?='paid' THEN ? ELSE paid_at END WHERE id=?")->execute([$status,$status,now(),$id]);back_to('finance','Pagamento atualizado.');
            break;

        case 'save_banner':
            must_super_admin();$id=(int)request('id',0);$title=trim((string)request('title'));if($title==='')throw new RuntimeException('Informe o título do banner.');$position=(string)request('position','home');if(!in_array($position,['home','dashboard','marketplace'],true))$position='home';$status=(string)request('status','active');if(!in_array($status,['active','inactive'],true))$status='active';$link=trim((string)request('link'));if($link!==''&&!filter_var($link,FILTER_VALIDATE_URL)&&!preg_match('~^(?:dashboard|index|marketplace)\.php(?:[?#].*)?$~',$link))throw new RuntimeException('Informe um link válido para o banner.');$existingImage='';if($id){$imgq=db()->prepare('SELECT image FROM banners WHERE id=?');$imgq->execute([$id]);$existingImage=(string)$imgq->fetchColumn();}if(request('remove_image')){delete_local_upload($existingImage);$existingImage='';}$image=upload_image('image_file','banners',$existingImage);$data=[$title,trim((string)request('subtitle')),$image,$link,$position,$status,(int)request('sort_order',0)];
            if($id)db()->prepare('UPDATE banners SET title=?,subtitle=?,image=?,link=?,position=?,status=?,sort_order=? WHERE id=?')->execute(array_merge($data,[$id]));else db()->prepare('INSERT INTO banners(title,subtitle,image,link,position,status,sort_order,created_at) VALUES(?,?,?,?,?,?,?,?)')->execute(array_merge($data,[now()]));back_to('banners','Banner salvo.');
            break;

        case 'toggle_banner':
            must_super_admin();$id=(int)request('id');$q=db()->prepare('SELECT status FROM banners WHERE id=?');$q->execute([$id]);$s=$q->fetchColumn();db()->prepare('UPDATE banners SET status=? WHERE id=?')->execute([$s==='active'?'inactive':'active',$id]);back_to('banners','Banner atualizado.');
            break;

        case 'delete_banner':
            must_super_admin();$id=(int)request('id');$q=db()->prepare('SELECT image FROM banners WHERE id=?');$q->execute([$id]);$image=(string)$q->fetchColumn();if($image===''){$exists=db()->prepare('SELECT COUNT(*) FROM banners WHERE id=?');$exists->execute([$id]);if(!(int)$exists->fetchColumn())throw new RuntimeException('Banner não encontrado.');}db()->prepare('DELETE FROM banners WHERE id=?')->execute([$id]);delete_local_upload($image);log_activity('delete_banner','banners',$id,'');back_to('banners','Banner apagado.');
            break;

        case 'save_manual_item':
            must_super_admin();$id=(int)request('id');$title=trim((string)request('title'));$description=trim((string)request('description'));$content=trim((string)request('content'));$video=trim((string)request('video_url'));if($title==='')throw new RuntimeException('Informe o título do manual.');if($video!==''&&!filter_var($video,FILTER_VALIDATE_URL))throw new RuntimeException('Informe um link de vídeo válido.');$status=(string)request('status','active');if(!in_array($status,['active','inactive'],true))$status='active';$sort=(int)request('sort_order');if($id){db()->prepare('UPDATE manual_items SET title=?,description=?,content=?,video_url=?,status=?,sort_order=?,updated_at=? WHERE id=?')->execute([$title,$description,$content,$video,$status,$sort,now(),$id]);}else{db()->prepare('INSERT INTO manual_items(title,description,content,video_url,status,sort_order,created_by,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)')->execute([$title,$description,$content,$video,$status,$sort,(int)$user['id'],now(),now()]);$id=(int)db()->lastInsertId();}log_activity('save_manual_item','manual_items',$id,$title);back_to('information','Manual salvo.');
            break;

        case 'delete_manual_item':
            must_super_admin();$id=(int)request('id');db()->prepare('DELETE FROM manual_items WHERE id=?')->execute([$id]);log_activity('delete_manual_item','manual_items',$id,'');back_to('information','Item do manual apagado.');
            break;

        case 'request_account_deletion':
            if(is_demo_user_account($user))throw new RuntimeException('Perfis de demonstração são protegidos e não podem solicitar exclusão.');
            if(is_admin())throw new RuntimeException('A conta Administrador Sênior não pode solicitar exclusão por este fluxo.');
            $scope=trim((string)request('scope','user'));if(!in_array($scope,['user','company'],true))throw new RuntimeException('Tipo de solicitação inválido.');
            $reason=trim((string)request('reason'));if(strlen($reason)>1000)$reason=substr($reason,0,1000);
            $subjectUserId=(int)$user['id'];$companyId=null;$companyName='';
            if($scope==='company'){
                if(!is_role(['company','rental_company']))throw new RuntimeException('Somente o responsável pela empresa pode solicitar a exclusão da empresa.');
                $candidate=(int)($user['company_id']??0);
                $cq=db()->prepare('SELECT id,name,owner_user_id FROM companies WHERE id=? LIMIT 1');$cq->execute([$candidate]);$company=$cq->fetch();
                if(!$company || (int)($company['owner_user_id']??0)!==(int)$user['id'])throw new RuntimeException('Apenas o responsável cadastrado pode solicitar a exclusão desta empresa.');
                $companyId=(int)$company['id'];$companyName=(string)$company['name'];
                $dup=db()->prepare('SELECT id FROM account_deletion_requests WHERE request_type="company" AND company_id=? AND status="pending" LIMIT 1');$dup->execute([$companyId]);
            }else{
                $dup=db()->prepare('SELECT id FROM account_deletion_requests WHERE request_type="user" AND user_id=? AND status="pending" LIMIT 1');$dup->execute([$subjectUserId]);
            }
            if($dup->fetchColumn())back_to('profile','Sua solicitação de exclusão já está aguardando análise do administrador.','warning');
            db()->prepare('INSERT INTO account_deletion_requests(request_type,user_id,company_id,requested_by,user_name_snapshot,user_email_snapshot,company_name_snapshot,reason,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,"pending",?,?)')
              ->execute([$scope,$subjectUserId,$companyId,$subjectUserId,(string)$user['name'],(string)$user['email'],$companyName?:null,$reason?:null,now(),now()]);
            $requestId=(int)db()->lastInsertId();
            if($scope==='company'){
                $message=$user['name'].' solicitou a exclusão da empresa “'.$companyName.'”.';
                if($reason!=='')$message.=' Motivo: '.$reason;
                notify_super_admins('Exclusão de empresa solicitada',$message,'warning',0,'dashboard.php?page=notifications','account_deletion_request',$requestId);
                create_notification($subjectUserId,'Solicitação de exclusão recebida','Seu pedido para excluir a empresa “'.$companyName.'” foi enviado ao administrador. A exclusão não é automática.','info','dashboard.php?page=profile','account_deletion_request',$requestId);
                log_activity('request_company_deletion','companies',$companyId,'Solicitação #'.$requestId);
            }else{
                $publicId=public_user_id($user);
                $message=$user['name'].' ('.($publicId?:$user['email']).') solicitou a exclusão da própria conta.';
                if($reason!=='')$message.=' Motivo: '.$reason;
                notify_super_admins('Exclusão de conta solicitada',$message,'warning',0,'dashboard.php?page=notifications','account_deletion_request',$requestId);
                create_notification($subjectUserId,'Solicitação de exclusão recebida','Seu pedido para excluir sua conta foi enviado ao administrador. A conta permanecerá ativa até a análise e exclusão pelo administrador.','info','dashboard.php?page=profile','account_deletion_request',$requestId);
                log_activity('request_user_deletion','users',$subjectUserId,'Solicitação #'.$requestId);
            }
            back_to('profile','Solicitação enviada. O administrador foi notificado.');
            break;

        case 'cancel_account_deletion_request':
            $id=(int)request('id',0);
            $q=db()->prepare('SELECT * FROM account_deletion_requests WHERE id=? AND requested_by=? AND status="pending" LIMIT 1');$q->execute([$id,(int)$user['id']]);$req=$q->fetch();
            if(!$req)throw new RuntimeException('Solicitação pendente não encontrada.');
            db()->prepare('UPDATE account_deletion_requests SET status="cancelled",updated_at=?,resolved_at=? WHERE id=?')->execute([now(),now(),$id]);
            $what=($req['request_type']??'user')==='company'?'da empresa '.($req['company_name_snapshot']?:''):'da própria conta';
            notify_super_admins('Solicitação de exclusão cancelada',$user['name'].' cancelou a solicitação de exclusão '.$what.'.','info',0,'dashboard.php?page=notifications','account_deletion_request',$id);
            back_to('profile','Solicitação de exclusão cancelada.');
            break;

        case 'resolve_account_deletion_request':
            must_admin();$id=(int)request('id',0);
            $q=db()->prepare('SELECT * FROM account_deletion_requests WHERE id=? AND status="pending" LIMIT 1');$q->execute([$id]);$req=$q->fetch();if(!$req)throw new RuntimeException('Solicitação pendente não encontrada.');
            db()->prepare('UPDATE account_deletion_requests SET status="dismissed",updated_at=?,resolved_at=?,resolved_by=? WHERE id=?')->execute([now(),now(),(int)$user['id'],$id]);
            $requester=(int)($req['requested_by']??0);if($requester>0){$exists=db()->prepare('SELECT id FROM users WHERE id=?');$exists->execute([$requester]);if($exists->fetchColumn())create_notification($requester,'Solicitação de exclusão analisada','O administrador encerrou sua solicitação sem excluir a conta neste momento.','info','dashboard.php?page=profile','account_deletion_request',$id);}
            log_activity('dismiss_account_deletion_request','account_deletion_requests',$id,'Arquivada pelo administrador');
            back_to('notifications','Solicitação arquivada.');
            break;

        case 'mark_notifications':
            db()->prepare('UPDATE notifications SET read_at=? WHERE (user_id=? OR user_id IS NULL) AND read_at IS NULL')->execute([now(),$user['id']]);back_to('notifications','Notificações marcadas como lidas.');
            break;


        case 'save_company_notification_permission':
            if(!is_role('company'))throw new RuntimeException('Somente a empresa pode alterar esta permissão.');
            $companyId=(int)($user['company_id']??0);if($companyId<1)throw new RuntimeException('Empresa não definida.');
            db()->prepare('UPDATE companies SET employees_can_manage_notifications=?,updated_at=? WHERE id=?')->execute([(int)request('enabled',0)===1?1:0,now(),$companyId]);
            back_to('notifications','Permissão dos funcionários atualizada.');
            break;

        case 'save_manual_notification':
            if(!can_manage_manual_notifications($user))throw new RuntimeException('Você não tem permissão para publicar notificações.');
            $broadcastId=trim((string)request('broadcast_id'));$existing=null;$existingImage='';$createdAt=now();$sourceCompanyId=is_admin()?0:(int)($user['company_id']??0);$issuerId=(int)$user['id'];
            if($broadcastId!==''){
                $q=db()->prepare('SELECT * FROM notifications WHERE broadcast_id=? ORDER BY id LIMIT 1');$q->execute([$broadcastId]);$existing=$q->fetch();
                if(!$existing||!notification_broadcast_manageable($existing,$user))throw new RuntimeException('Notificação não encontrada ou sem permissão.');
                $existingImage=(string)($existing['image']??'');$createdAt=(string)($existing['created_at']?:now());$sourceCompanyId=(int)($existing['source_company_id']??$sourceCompanyId);$issuerId=(int)($existing['issuer_user_id']??$issuerId)?:$issuerId;
            }else{$broadcastId='NTF-'.date('YmdHis').'-'.bin2hex(random_bytes(4));}
            $title=trim((string)request('title'));$message=trim((string)request('message'));if($title===''||$message==='')throw new RuntimeException('Informe título e mensagem.');
            $type=request('type','info');if(!in_array($type,['info','success','warning'],true))$type='info';
            $audienceType=trim((string)request('audience_type'));$audienceValue='';
            if($audienceType==='role')$audienceValue=trim((string)request('target_role'));
            elseif($audienceType==='company')$audienceValue=(string)(int)request('target_company_id',0);
            elseif($audienceType==='user')$audienceValue=trim((string)request('target_user'));
            $audience=manual_notification_recipient_ids($user,$audienceType,$audienceValue);if(!$audience['ids'])throw new RuntimeException('Nenhum destinatário encontrado para este público.');
            $hasNewImage=isset($_FILES['image_file'])&&is_array($_FILES['image_file'])&&(int)($_FILES['image_file']['error']??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_NO_FILE;
            if(request('remove_image')&&!$hasNewImage){delete_local_upload($existingImage);$image='';}else{$image=upload_image('image_file','notificacoes',$existingImage);}
            $imageUrl=trim((string)request('image_url'));if($imageUrl!==''&&preg_match('~^https?://~i',$imageUrl)){if($image!==''&&!preg_match('~^https?://~i',$image)&&$image!==$existingImage)delete_local_upload($image);if($existingImage!==''&&!preg_match('~^https?://~i',$existingImage))delete_local_upload($existingImage);$image=$imageUrl;}
            $expiresAt=trim((string)request('expires_at'));if($expiresAt!==''){$ts=strtotime($expiresAt);if(!$ts)throw new RuntimeException('Data final inválida.');$expiresAt=date('Y-m-d H:i:s',$ts);if($expiresAt<now())throw new RuntimeException('O tempo final deve ser futuro.');}else $expiresAt=null;
            db()->beginTransaction();
            if($existing)db()->prepare('DELETE FROM notifications WHERE broadcast_id=?')->execute([$broadcastId]);
            $ins=db()->prepare('INSERT INTO notifications(user_id,title,message,type,read_at,created_at,issuer_user_id,source_company_id,broadcast_id,audience_type,audience_value,audience_label,image,home_visible,expires_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)');
            foreach($audience['ids'] as $recipientId)$ins->execute([(int)$recipientId,$title,$message,$type,null,$createdAt,$issuerId,$sourceCompanyId?:null,$broadcastId,$audience['type'],$audience['value'],$audience['label'],$image,1,$expiresAt,now()]);
            db()->commit();log_activity($existing?'update_notification_broadcast':'create_notification_broadcast','notifications',null,$title.' • '.$audience['label']);
            back_to('notifications',$existing?'Notificação atualizada para todos os destinatários.':'Notificação publicada para '.$audience['label'].'.');
            break;

        case 'delete_manual_notification':
            if(!can_manage_manual_notifications($user)&&!is_admin())throw new RuntimeException('Sem permissão.');
            $broadcastId=trim((string)request('broadcast_id'));$q=db()->prepare('SELECT * FROM notifications WHERE broadcast_id=? ORDER BY id LIMIT 1');$q->execute([$broadcastId]);$row=$q->fetch();if(!$row||!notification_broadcast_manageable($row,$user))throw new RuntimeException('Notificação não encontrada ou sem permissão.');
            $image=(string)($row['image']??'');db()->prepare('DELETE FROM notifications WHERE broadcast_id=?')->execute([$broadcastId]);delete_local_upload($image);log_activity('delete_notification_broadcast','notifications',null,(string)$row['title']);back_to('notifications','Notificação apagada para todos os destinatários.');
            break;

        case 'save_ticket':
            db()->prepare('INSERT INTO tickets(user_id,subject,message,status,priority,created_at,updated_at) VALUES(?,?,?,?,?,?,?)')->execute([$user['id'],request('subject'),request('message'),'open',request('priority','normal'),now(),now()]);back_to('support','Chamado aberto. Nossa equipe poderá responder pelo painel.');
            break;

        case 'update_ticket':
            must_admin();$id=(int)request('id');db()->prepare('UPDATE tickets SET status=?,priority=?,admin_reply=?,updated_at=? WHERE id=?')->execute([request('status'),request('priority'),request('admin_reply'),now(),$id]);back_to('support','Chamado atualizado.');
            break;

        case 'delete_ticket':
            must_admin();$id=(int)request('id',0);$q=db()->prepare('SELECT id FROM tickets WHERE id=?');$q->execute([$id]);if(!$q->fetchColumn())throw new RuntimeException('Chamado não encontrado.');
            if(is_super_admin()){db()->prepare('DELETE FROM moderation_deletion_approvals WHERE target_type=? AND target_id=?')->execute(['ticket',$id]);db()->prepare('DELETE FROM tickets WHERE id=?')->execute([$id]);log_activity('delete_ticket','tickets',$id,'Exclusão pelo Administrador Sênior');back_to('support','Mensagem de suporte apagada.');}
            if(!is_role('sub_admin'))throw new RuntimeException('Ação administrativa necessária.');
            db()->prepare('INSERT OR IGNORE INTO moderation_deletion_approvals(target_type,target_id,admin_user_id,created_at) VALUES(?,?,?,?)')->execute(['ticket',$id,(int)$user['id'],now()]);
            $state=moderation_deletion_approval_state('ticket',$id,(int)$user['id']);
            if((int)$state['count']>=2){db()->prepare('DELETE FROM moderation_deletion_approvals WHERE target_type=? AND target_id=?')->execute(['ticket',$id]);db()->prepare('DELETE FROM tickets WHERE id=?')->execute([$id]);log_activity('delete_ticket','tickets',$id,'Exclusão confirmada por dois administradores');back_to('support','Mensagem de suporte apagada após duas confirmações.');}
            back_to('support','Confirmação registrada (1/2). Outro Administrador precisa confirmar a exclusão.');
            break;

        case 'remove_profile_avatar':
            $oldAvatar=(string)($user['avatar']??'');
            if($oldAvatar==='') throw new RuntimeException('Seu perfil não possui foto para remover.');
            delete_local_upload($oldAvatar);
            db()->prepare('UPDATE users SET avatar=NULL,updated_at=? WHERE id=?')->execute([now(),(int)$user['id']]);
            log_activity('remove_profile_avatar','users',(int)$user['id'],'Foto removida pelo próprio usuário');
            back_to('profile','Foto de perfil removida.');
            break;

        case 'admin_remove_user_avatar':
            must_admin();
            $targetId=(int)request('id',0);
            $q=db()->prepare('SELECT id,name,avatar,role FROM users WHERE id=?');$q->execute([$targetId]);$target=$q->fetch();
            if(!$target) throw new RuntimeException('Usuário não encontrado.');
            if(is_role('sub_admin')&&in_array(($target['role']??''),['super_admin','sub_admin'],true)) throw new RuntimeException('Administrador não pode alterar contas administrativas.');
            if(trim((string)($target['avatar']??''))==='') throw new RuntimeException('Este usuário não possui foto de perfil.');
            delete_local_upload((string)$target['avatar']);
            db()->prepare('UPDATE users SET avatar=NULL,updated_at=? WHERE id=?')->execute([(int)$user['id'],now(),$targetId]);
            create_notification($targetId,'Foto de perfil removida','O administrador removeu sua foto de perfil. Você pode enviar uma nova imagem em Meu Perfil.','info','dashboard.php?page=profile','user',$targetId);
            log_activity('admin_remove_user_avatar','users',$targetId,$target['name']);
            back_to('users','Foto de perfil de '.$target['name'].' removida.');
            break;

        case 'submit_user_report':
            $reportedId=(int)request('reported_user_id',0);
            if($reportedId<=0 || $reportedId===(int)$user['id']) throw new RuntimeException('Selecione outro usuário para denunciar.');
            $q=db()->prepare("SELECT id,name,status FROM users WHERE id=?");$q->execute([$reportedId]);$reported=$q->fetch();
            if(!$reported || ($reported['status']??'')==='blocked') throw new RuntimeException('O usuário informado não está disponível para denúncia.');
            $reason=trim((string)request('reason'));
            $allowedReasons=['Comportamento inadequado','Fraude ou cobrança indevida','Problema no serviço','Spam ou abuso','Perfil ou informação falsa','Outro'];
            if(!in_array($reason,$allowedReasons,true)) throw new RuntimeException('Selecione um motivo válido.');
            $description=trim((string)request('description'));if(strlen($description)<5) throw new RuntimeException('Descreva brevemente o que aconteceu.');
            $scope=clinic_report_scope_for_user($reportedId);
            $requestedCompany=(int)request('reported_company_id',0);$requestedProfessional=(int)request('reported_clinic_professional_id',0);
            if($requestedCompany>0){$q=db()->prepare('SELECT owner_user_id FROM companies WHERE id=?');$q->execute([$requestedCompany]);$owner=(int)$q->fetchColumn();if($owner===$reportedId)$scope['company_id']=$requestedCompany;}
            if($requestedProfessional>0){$q=db()->prepare('SELECT user_id,company_id FROM clinic_professionals WHERE id=?');$q->execute([$requestedProfessional]);$rp=$q->fetch();if($rp&&(int)$rp['user_id']===$reportedId){$scope['professional_id']=$requestedProfessional;$scope['company_id']=(int)$rp['company_id'];}}
            $dup=db()->prepare("SELECT COUNT(*) FROM user_reports WHERE reporter_user_id=? AND reported_user_id=? AND status IN ('pending','reviewed','actioned') AND datetime(created_at)>=datetime('now','-24 hours')");
            $dup->execute([(int)$user['id'],$reportedId]);if((int)$dup->fetchColumn()>0) throw new RuntimeException('Você já enviou uma denúncia em análise sobre este usuário nas últimas 24 horas.');
            db()->prepare("INSERT INTO user_reports(reporter_user_id,reported_user_id,reporter_name_snapshot,reported_name_snapshot,reason,description,status,reported_company_id,reported_clinic_professional_id,created_at,updated_at) VALUES(?,?,?,?,?,?,'pending',?,?,?,?)")
                ->execute([(int)$user['id'],$reportedId,$user['name'],$reported['name'],$reason,$description,$scope['company_id']?:null,$scope['professional_id']?:null,now(),now()]);
            $reportId=(int)db()->lastInsertId();
            notify_super_admins(!empty($scope['company_id'])?'Nova denúncia de clínica':'Nova denúncia de usuário',$user['name'].' denunciou '.$reported['name'].' por: '.$reason.'.','warning',0,'dashboard.php?page=reports','user_report',$reportId);
            create_notification((int)$user['id'],'Denúncia enviada','Sua denúncia foi enviada ao Administrador Sênior para análise.','info','dashboard.php?page=reports','user_report',$reportId);
            log_activity('submit_user_report','user_reports',$reportId,$reported['name'].' - '.$reason);back_to('reports','Denúncia enviada ao Administrador Sênior.');break;

        case 'review_user_report':
            if(!is_admin())throw new RuntimeException('Ação administrativa necessária.');
            $id=(int)request('id',0);$status=(string)request('status');
            if(!in_array($status,['reviewed','actioned','dismissed','finalized'],true)) throw new RuntimeException('Situação de análise inválida.');
            $q=db()->prepare('SELECT * FROM user_reports WHERE id=?');$q->execute([$id]);$report=$q->fetch();if(!$report) throw new RuntimeException('Denúncia não encontrada.');
            $isClinicReport=(int)($report['reported_company_id']??0)>0 || (int)($report['reported_clinic_professional_id']??0)>0;
            if($isClinicReport&&!is_super_admin())throw new RuntimeException('Somente o Administrador Sênior pode analisar denúncias de clínicas e profissionais de clínicas.');
            $notes=trim((string)request('admin_notes'));$block=(int)request('block_user',0)===1;
            db()->prepare('UPDATE user_reports SET status=?,admin_notes=?,reviewed_by=?,reviewed_at=?,updated_at=? WHERE id=?')->execute([$status,$notes,(int)$user['id'],now(),now(),$id]);
            if($block && (int)($report['reported_user_id']??0)>0){$targetId=(int)$report['reported_user_id'];$roleQ=db()->prepare('SELECT role FROM users WHERE id=?');$roleQ->execute([$targetId]);$role=$roleQ->fetchColumn();if($role && $role!=='super_admin')db()->prepare("UPDATE users SET status='blocked',updated_at=? WHERE id=?")->execute([now(),$targetId]);}
            if((int)($report['reporter_user_id']??0)>0){$msg='Sua denúncia sobre '.$report['reported_name_snapshot'].' foi '.strtolower(status_label($status)).'.';if($notes!=='')$msg.=' Observação: '.$notes;create_notification((int)$report['reporter_user_id'],'Denúncia analisada',$msg,'info','dashboard.php?page=reports','user_report',$id);}
            log_activity('review_user_report','user_reports',$id,status_label($status).($block?' + usuário bloqueado':''));back_to('reports',$status==='finalized'?'Denúncia finalizada. O acesso extraordinário aos dados da clínica foi encerrado.':'Denúncia atualizada.');break;

        case 'delete_user_report':
            if(!is_admin())throw new RuntimeException('Ação administrativa necessária.');$id=(int)request('id',0);$q=db()->prepare('SELECT reported_company_id,reported_clinic_professional_id FROM user_reports WHERE id=?');$q->execute([$id]);$r=$q->fetch();if(!$r)throw new RuntimeException('Denúncia não encontrada.');$clinicReport=((int)($r['reported_company_id']??0)>0||(int)($r['reported_clinic_professional_id']??0)>0);if($clinicReport&&!is_super_admin())throw new RuntimeException('Somente o Administrador Sênior pode apagar denúncia clínica.');
            if(is_super_admin()){db()->prepare('DELETE FROM moderation_deletion_approvals WHERE target_type=? AND target_id=?')->execute(['user_report',$id]);db()->prepare('DELETE FROM user_reports WHERE id=?')->execute([$id]);log_activity('delete_user_report','user_reports',$id,'Exclusão pelo Administrador Sênior');back_to('reports','Denúncia apagada.');}
            if(!is_role('sub_admin'))throw new RuntimeException('Ação administrativa necessária.');db()->prepare('INSERT OR IGNORE INTO moderation_deletion_approvals(target_type,target_id,admin_user_id,created_at) VALUES(?,?,?,?)')->execute(['user_report',$id,(int)$user['id'],now()]);$state=moderation_deletion_approval_state('user_report',$id,(int)$user['id']);if((int)$state['count']>=2){db()->prepare('DELETE FROM moderation_deletion_approvals WHERE target_type=? AND target_id=?')->execute(['user_report',$id]);db()->prepare('DELETE FROM user_reports WHERE id=?')->execute([$id]);log_activity('delete_user_report','user_reports',$id,'Exclusão confirmada por dois administradores');back_to('reports','Denúncia apagada após duas confirmações.');}back_to('reports','Confirmação registrada (1/2). Outro Administrador precisa confirmar a exclusão.');break;

        case 'save_profile':
            $freshQ=db()->prepare('SELECT * FROM users WHERE id=?');$freshQ->execute([(int)$user['id']]);$fresh=$freshQ->fetch()?:$user;
            $identityUnlocked=(int)($fresh['identity_edit_unlocked']??0)===1;
            $forceProfile=(int)($fresh['admin_require_profile']??0)===1;
            $identityEditable=$identityUnlocked||$forceProfile;
            $name=$identityEditable?trim((string)request('name')):(string)$fresh['name'];
            $email=strtolower(request('email'));if(!$name||!filter_var($email,FILTER_VALIDATE_EMAIL))throw new RuntimeException('Dados inválidos.');
            $username=validate_public_username(request('username'),(int)$user['id']);
            $document=$identityEditable?validate_document_or_throw(request('document'),(int)$user['id']):(string)($fresh['document']??'');
            if($forceProfile){
                if(strlen($name)<3)throw new RuntimeException('Informe seu nome completo.');
                if(!in_array(strlen(only_digits($document)),[11,14],true))throw new RuntimeException('Informe um CPF ou CNPJ válido.');
                if(trim((string)request('home_address'))==='')throw new RuntimeException('Informe seu endereço.');
                if(trim((string)request('city'))===''||trim((string)request('state'))==='')throw new RuntimeException('Informe cidade e estado do endereço.');
            }
            $avatar=upload_image('avatar_file','perfis',$fresh['avatar']);
            $feePayer=(string)request('fee_payer_preference',$fresh['fee_payer_preference']??'client');if(!in_array($feePayer,['client','provider'],true))$feePayer='client';if(platform_billing_mode_for_user($fresh)!=='transaction')$feePayer=(string)($fresh['fee_payer_preference']??'client');
            $receivingMethod=(string)request('receiving_method',$fresh['receiving_method']??'pix');if(!in_array($receivingMethod,['pix','link'],true))$receivingMethod='pix';
            $personalPaymentLink=trim((string)request('personal_payment_link',$fresh['personal_payment_link']??''));if($personalPaymentLink!==''&&!filter_var($personalPaymentLink,FILTER_VALIDATE_URL))throw new RuntimeException('Informe um link de pagamento válido.');
            if(empty($fresh['own_receiving_enabled'])){$receivingMethod=(string)($fresh['receiving_method']??'pix');$personalPaymentLink=(string)($fresh['personal_payment_link']??'');}
            db()->prepare('UPDATE users SET name=?,username=?,email=?,phone=?,document=?,avatar=?,city=?,state=?,work_status=?,home_address=?,home_number=?,home_lat=?,home_lng=?,pix_key=?,pix_key_type=?,instagram=?,telegram=?,fee_payer_preference=?,receiving_method=?,personal_payment_link=?,identity_edit_unlocked=0,admin_require_profile=?,profile_completed=1,updated_at=? WHERE id=?')->execute([$name,$username,$email,only_digits(request('phone')),$document,$avatar,request('city'),normalize_state(request('state')),$fresh['work_status']?request('work_status',$fresh['work_status']):'offline',request('home_address'),request('home_number'),request('home_lat')?:null,request('home_lng')?:null,request('pix_key'),request('pix_key_type'),normalize_social_handle(request('instagram')),normalize_social_handle(request('telegram')),$feePayer,$receivingMethod,$personalPaymentLink,$forceProfile?0:(int)($fresh['admin_require_profile']??0),now(),$user['id']]);
            ensure_public_user_id((int)$user['id']);
            if(request('password')){if(strlen(request('password'))<6)throw new RuntimeException('A nova senha deve ter no mínimo 6 caracteres.');db()->prepare('UPDATE users SET password_hash=? WHERE id=?')->execute([password_hash(request('password'),PASSWORD_DEFAULT),$user['id']]);}
            if($forceProfile)create_notification((int)$user['id'],'Cadastro completo','Seus dados obrigatórios foram preenchidos e os demais módulos foram liberados.','success','dashboard.php?page=home','user',(int)$user['id']);
            back_to('profile',$forceProfile?'Cadastro obrigatório concluído. Acesso aos demais módulos liberado.':($identityUnlocked?'Perfil atualizado. Nome/CPF/CNPJ foram bloqueados novamente após esta alteração.':'Perfil atualizado.'));
            break;

        case 'save_favorite_address':
            $id=(int)request('id',0);$label=trim((string)request('label'));$address=trim((string)request('address'));
            if($label===''||$address==='')throw new RuntimeException('Informe o nome e o endereço do destino.');
            $lat=request('latitude')!==''?(float)request('latitude'):null;$lng=request('longitude')!==''?(float)request('longitude'):null;
            if($id){
                $q=db()->prepare('SELECT id FROM favorite_addresses WHERE id=? AND user_id=?');$q->execute([$id,$user['id']]);
                if(!$q->fetchColumn())throw new RuntimeException('Endereço favorito não encontrado.');
                db()->prepare('UPDATE favorite_addresses SET label=?,address=?,latitude=?,longitude=?,updated_at=? WHERE id=? AND user_id=?')->execute([$label,$address,$lat,$lng,now(),$id,$user['id']]);
            }else{
                $dup=db()->prepare('SELECT id FROM favorite_addresses WHERE user_id=? AND label=? AND address=?');$dup->execute([$user['id'],$label,$address]);
                if($dup->fetchColumn())throw new RuntimeException('Este endereço já está salvo nos seus favoritos.');
                db()->prepare('INSERT INTO favorite_addresses(user_id,label,address,latitude,longitude,created_at,updated_at) VALUES(?,?,?,?,?,?,?)')->execute([$user['id'],$label,$address,$lat,$lng,now(),now()]);
            }
            back_to('favorite_addresses',$id?'Endereço favorito atualizado.':'Endereço favorito cadastrado.');
            break;

        case 'delete_favorite_address':
            $id=(int)request('id');
            db()->prepare('DELETE FROM favorite_addresses WHERE id=? AND user_id=?')->execute([$id,$user['id']]);
            back_to('favorite_addresses','Endereço favorito apagado.');
            break;

        case 'toggle_favorite':
            must_role('client');
            $targetType=request('target_type');
            $targetId=(int)request('target_id');
            if(!in_array($targetType,['company','service','property','vehicle'],true)||$targetId<1)throw new RuntimeException('Favorito inválido.');
            $q=db()->prepare('SELECT id FROM favorites WHERE user_id=? AND target_type=? AND target_id=?');
            $q->execute([$user['id'],$targetType,$targetId]);
            $favoriteId=$q->fetchColumn();
            if($favoriteId)db()->prepare('DELETE FROM favorites WHERE id=?')->execute([$favoriteId]);
            else db()->prepare('INSERT INTO favorites(user_id,target_type,target_id,created_at) VALUES(?,?,?,?)')->execute([$user['id'],$targetType,$targetId,now()]);
            back_to($returnPage?:'home',$favoriteId?'Removido dos favoritos.':'Adicionado aos favoritos.');
            break;

        case 'save_review':
            $targetType=request('target_type');$targetId=(int)request('target_id');$rating=(int)request('rating');
            $allowedTargets=['company','service_provider','ride_driver','delivery_courier','vehicle_store','moving_driver'];
            if(!in_array($targetType,$allowedTargets,true)||$targetId<1)throw new RuntimeException('Avaliação inválida.');
            if($rating<1||$rating>5)throw new RuntimeException('Escolha uma nota de 1 a 5 estrelas.');

            $orderId=(int)request('order_id',0);
            $serviceRequestId=(int)request('service_request_id',0);
            $rideId=(int)request('ride_id',0);
            $deliveryJobId=(int)request('delivery_job_id',0);
            $vehicleProposalId=(int)request('vehicle_proposal_id',0);
            $eligible=false;$sourceField='';$sourceId=0;

            if($targetType==='company'){
                if($orderId<=0){
                    $find=db()->prepare("SELECT o.id FROM orders o LEFT JOIN reviews rv ON rv.order_id=o.id AND rv.reviewer_id=? AND rv.target_type='company' WHERE o.client_id=? AND o.company_id=? AND o.status IN ('completed','delivered') AND rv.id IS NULL ORDER BY o.id DESC LIMIT 1");
                    $find->execute([$user['id'],$user['id'],$targetId]);$orderId=(int)$find->fetchColumn();
                }
                if($orderId>0){$check=db()->prepare("SELECT COUNT(*) FROM orders WHERE id=? AND client_id=? AND company_id=? AND status IN ('completed','delivered')");$check->execute([$orderId,$user['id'],$targetId]);$eligible=(int)$check->fetchColumn()>0;$sourceField='order_id';$sourceId=$orderId;}
            }elseif($targetType==='service_provider'){
                if($serviceRequestId>0){$check=db()->prepare("SELECT COUNT(*) FROM service_requests WHERE id=? AND client_id=? AND provider_id=? AND status='completed'");$check->execute([$serviceRequestId,$user['id'],$targetId]);$eligible=(int)$check->fetchColumn()>0;$sourceField='service_request_id';$sourceId=$serviceRequestId;}
            }elseif($targetType==='ride_driver'){
                if($rideId>0){$check=db()->prepare("SELECT COUNT(*) FROM rides WHERE id=? AND client_id=? AND driver_id=? AND status='completed'");$check->execute([$rideId,$user['id'],$targetId]);$eligible=(int)$check->fetchColumn()>0;$sourceField='ride_id';$sourceId=$rideId;}
            }elseif($targetType==='delivery_courier'){
                if($deliveryJobId>0){$check=db()->prepare("SELECT COUNT(*) FROM delivery_jobs WHERE id=? AND client_id=? AND assigned_courier_id=? AND status='delivered'");$check->execute([$deliveryJobId,$user['id'],$targetId]);$eligible=(int)$check->fetchColumn()>0;$sourceField='delivery_job_id';$sourceId=$deliveryJobId;}
                elseif($orderId>0){$check=db()->prepare("SELECT COUNT(*) FROM orders WHERE id=? AND client_id=? AND delivery_user_id=? AND status='delivered'");$check->execute([$orderId,$user['id'],$targetId]);$eligible=(int)$check->fetchColumn()>0;$sourceField='order_id';$sourceId=$orderId;}
            }elseif($targetType==='moving_driver'){
                $check=db()->prepare("SELECT COUNT(*) FROM moving_requests WHERE client_id=? AND driver_id=? AND status='completed'");
                $check->execute([$user['id'],$targetId]);$eligible=(int)$check->fetchColumn()>0;
            }elseif($targetType==='vehicle_store'){
                if($vehicleProposalId>0){$check=db()->prepare("SELECT COUNT(*) FROM vehicle_proposals vp JOIN vehicles v ON v.id=vp.vehicle_id WHERE vp.id=? AND vp.client_id=? AND v.company_id=? AND vp.status='accepted'");$check->execute([$vehicleProposalId,$user['id'],$targetId]);$eligible=(int)$check->fetchColumn()>0;$sourceField='vehicle_proposal_id';$sourceId=$vehicleProposalId;}
                else{$check=db()->prepare("SELECT COUNT(*) FROM vehicle_proposals vp JOIN vehicles v ON v.id=vp.vehicle_id WHERE vp.client_id=? AND v.company_id=? AND vp.status='accepted'");$check->execute([$user['id'],$targetId]);$eligible=(int)$check->fetchColumn()>0;}
            }
            if(!$eligible)throw new RuntimeException('A avaliação é liberada somente após a conclusão do atendimento, entrega, corrida ou venda.');

            $reviewId=0;
            if($sourceField!==''&&$sourceId>0){
                $exists=db()->prepare("SELECT id FROM reviews WHERE reviewer_id=? AND target_type=? AND target_id=? AND $sourceField=? LIMIT 1");
                $exists->execute([$user['id'],$targetType,$targetId,$sourceId]);$reviewId=(int)$exists->fetchColumn();
            }elseif(in_array($targetType,['moving_driver','vehicle_store'],true)){
                $exists=db()->prepare('SELECT id FROM reviews WHERE reviewer_id=? AND target_type=? AND target_id=? LIMIT 1');
                $exists->execute([$user['id'],$targetType,$targetId]);$reviewId=(int)$exists->fetchColumn();
            }

            $serviceRating=max(1,min(5,(int)request('service_rating',$rating)));
            $qualityRating=max(1,min(5,(int)request('quality_rating',$rating)));
            $deliveryRating=max(1,min(5,(int)request('delivery_rating',$rating)));
            $priceRating=max(1,min(5,(int)request('price_rating',$rating)));
            if($reviewId){
                db()->prepare('UPDATE reviews SET rating=?,service_rating=?,quality_rating=?,delivery_rating=?,price_rating=?,comment=?,status="published",updated_at=? WHERE id=?')
                    ->execute([$rating,$serviceRating,$qualityRating,$deliveryRating,$priceRating,request('comment'),now(),$reviewId]);
            }else{
                db()->prepare('INSERT INTO reviews(reviewer_id,target_type,target_id,order_id,service_request_id,ride_id,delivery_job_id,vehicle_proposal_id,rating,service_rating,quality_rating,delivery_rating,price_rating,comment,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,"published",?,?)')
                    ->execute([$user['id'],$targetType,$targetId,$orderId?:null,$serviceRequestId?:null,$rideId?:null,$deliveryJobId?:null,$vehicleProposalId?:null,$rating,$serviceRating,$qualityRating,$deliveryRating,$priceRating,request('comment'),now(),now()]);
                $reviewId=(int)db()->lastInsertId();
            }
            if(in_array($targetType,['service_provider','ride_driver','delivery_courier','moving_driver'],true))db()->prepare('UPDATE users SET marketplace_visible=1 WHERE id=?')->execute([$targetId]);
            notify_super_admins('Nova avaliação','Uma nova avaliação por estrelas foi publicada no Marketplace.','info');
            back_to($returnPage?:'home','Avaliação publicada.');
            break;

        case 'respond_review':
            must_role(['company','service_provider']);
            $id=(int)request('id');$q=db()->prepare('SELECT * FROM reviews WHERE id=?');$q->execute([$id]);$review=$q->fetch();
            if(!$review)throw new RuntimeException('Avaliação não encontrada.');
            if(!is_admin()){
                $allowed=false;
                if($review['target_type']==='company' && (int)$review['target_id']===(int)$user['company_id'])$allowed=true;
                if($review['target_type']==='service_provider' && (int)$review['target_id']===(int)$user['id'])$allowed=true;
                if(!$allowed)throw new RuntimeException('Sem permissão.');
            }
            db()->prepare('UPDATE reviews SET response=?,updated_at=? WHERE id=?')->execute([request('response'),now(),$id]);
            back_to('marketplace_admin','Resposta publicada.');
            break;

        case 'moderate_review':
            must_admin();$id=(int)request('id');$status=request('status');
            if(!in_array($status,['published','hidden'],true))throw new RuntimeException('Status inválido.');
            db()->prepare('UPDATE reviews SET status=?,updated_at=? WHERE id=?')->execute([$status,now(),$id]);
            back_to('marketplace_admin','Avaliação moderada.');
            break;

        case 'save_vehicle_proposal':$vehicleId=(int)request('vehicle_id');$amount=(float)str_replace(',','.',request('amount',0));
            $q=db()->prepare('SELECT v.*,u.id AS seller_id FROM vehicles v LEFT JOIN users u ON u.id=v.owner_user_id WHERE v.id=? AND v.listing_mode="sale" AND v.status="available"');$q->execute([$vehicleId]);$vehicle=$q->fetch();
            if(!$vehicle)throw new RuntimeException('Veículo indisponível para proposta.');
            if($amount<=0)throw new RuntimeException('Informe o valor da proposta.');
            db()->prepare('INSERT INTO vehicle_proposals(vehicle_id,client_id,amount,message,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?)')->execute([$vehicleId,$user['id'],$amount,request('message'),'pending',now(),now()]);
            create_notification((int)$vehicle['seller_id'],'Nova proposta de veículo','Você recebeu uma proposta para '.$vehicle['brand'].' '.$vehicle['model'].'.','success');
            back_to('vehicle_store','Proposta enviada.');
            break;

        case 'update_vehicle_proposal':
            must_role(['company','rental_company']);$id=(int)request('id');$status=request('status');
            if(!in_array($status,['accepted','rejected','negotiating'],true))throw new RuntimeException('Status inválido.');
            $q=db()->prepare('SELECT vp.*,v.owner_user_id,v.company_id,v.brand,v.model FROM vehicle_proposals vp JOIN vehicles v ON v.id=vp.vehicle_id WHERE vp.id=?');$q->execute([$id]);$proposal=$q->fetch();
            if(!$proposal)throw new RuntimeException('Proposta não encontrada.');
            if(!is_admin() && (int)$proposal['owner_user_id']!==(int)$user['id'] && (int)$proposal['company_id']!==(int)$user['company_id'])throw new RuntimeException('Sem permissão.');
            db()->prepare('UPDATE vehicle_proposals SET status=?,updated_at=? WHERE id=?')->execute([$status,now(),$id]);
            create_notification((int)$proposal['client_id'],'Proposta atualizada','Sua proposta para '.$proposal['brand'].' '.$proposal['model'].' foi atualizada.','info');
            back_to('vehicle_store','Proposta atualizada.');
            break;

        case 'save_vehicle_appointment':$vehicleId=(int)request('vehicle_id');$scheduled=request('scheduled_at');
            if(!$scheduled)throw new RuntimeException('Informe data e horário.');
            $q=db()->prepare('SELECT brand,model,owner_user_id FROM vehicles WHERE id=? AND listing_mode="sale"');$q->execute([$vehicleId]);$vehicle=$q->fetch();
            if(!$vehicle)throw new RuntimeException('Veículo não encontrado.');
            db()->prepare('INSERT INTO vehicle_appointments(vehicle_id,client_id,appointment_type,scheduled_at,notes,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)')->execute([$vehicleId,$user['id'],request('appointment_type','test_drive'),$scheduled,request('notes'),'requested',now(),now()]);
            create_notification((int)$vehicle['owner_user_id'],'Novo agendamento','Novo pedido de test drive/visita para '.$vehicle['brand'].' '.$vehicle['model'].'.','info');
            back_to('vehicle_store','Agendamento solicitado.');
            break;

        case 'save_financing_request':$vehicleId=(int)request('vehicle_id');
            $q=db()->prepare('SELECT brand,model,owner_user_id FROM vehicles WHERE id=? AND listing_mode="sale"');$q->execute([$vehicleId]);$vehicle=$q->fetch();
            if(!$vehicle)throw new RuntimeException('Veículo não encontrado.');
            $documentFile=upload_document('document_file','financiamentos');
            db()->prepare('INSERT INTO financing_requests(vehicle_id,client_id,entry_amount,term_months,income,document_file,status,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)')->execute([$vehicleId,$user['id'],(float)str_replace(',','.',request('entry_amount',0)),(int)request('term_months',12),(float)str_replace(',','.',request('income',0)),$documentFile,'requested',request('notes'),now(),now()]);
            create_notification((int)$vehicle['owner_user_id'],'Nova solicitação de financiamento','Financiamento solicitado para '.$vehicle['brand'].' '.$vehicle['model'].'.','info');
            back_to('vehicle_store','Solicitação de financiamento enviada.');
            break;

        case 'save_moving_request':
            $origin=request('origin');$destination=request('destination');if(!$origin||!$destination)throw new RuntimeException('Informe origem e destino.');
            $image=upload_image('cover_image_file','mudancas/itens','');$distance=(float)str_replace(',','.',request('distance_km',0));$duration=(int)request('duration_minutes',0);if($distance<=0&&request('origin_lat')&&request('destination_lat')){$fallback=distance_km(request('origin_lat'),request('origin_lng'),request('destination_lat'),request('destination_lng'));if($fallback!==null)$distance=$fallback*1.25;}if($duration<=0&&$distance>0)$duration=(int)round($distance/30*60);
            $estimated=max(0,$distance*(float)setting('moving_price_km','4.50')+(int)request('floors',0)*(float)setting('moving_floor_fee','25'));
            if(request('needs_helpers'))$estimated+=(float)setting('moving_helper_fee','120');if(request('needs_assembly'))$estimated+=(float)setting('moving_assembly_fee','150');if(request('needs_packaging'))$estimated+=(float)setting('moving_packaging_fee','100');
            db()->prepare('INSERT INTO moving_requests(client_id,service_type,origin,destination,scheduled_at,vehicle_type,item_quantity,needs_helpers,needs_assembly,needs_packaging,floors,distance_km,estimated_value,status,notes,cover_image,origin_lat,origin_lng,destination_lat,destination_lng,duration_minutes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$user['id'],request('service_type','residential'),$origin,$destination,request('scheduled_at')?:null,request('vehicle_type'),(int)request('item_quantity',0),request('needs_helpers')?1:0,request('needs_assembly')?1:0,request('needs_packaging')?1:0,(int)request('floors',0),$distance,$estimated,'requested',request('notes'),$image,request('origin_lat')?:null,request('origin_lng')?:null,request('destination_lat')?:null,request('destination_lng')?:null,$duration,now(),now()]);
            notify_super_admins('Nova solicitação de mudança',$user['name'].' solicitou mudança de '.$origin.' para '.$destination.'.','info');
            $drivers=db()->query("SELECT u.id FROM users u LEFT JOIN companies c ON c.id=u.company_id WHERE u.status='active' AND (u.role='moving_provider' OR (u.role='company' AND c.type='moving'))")->fetchAll();foreach($drivers as $driver)create_notification((int)$driver['id'],'Nova mudança disponível',$origin.' → '.$destination.' • '.money($estimated),'info');
            back_to('moving','Solicitação criada. Empresas e profissionais de Mudanças e Fretes foram notificados.');
            break;

        case 'save_moving_quote':
            must_role(['moving_provider','company']);
            if(is_role('company')&&($user['company_type']??'')!=='moving')throw new RuntimeException('Este módulo é exclusivo para empresas de mudanças.');
            $requestId=(int)request('request_id');$amount=(float)str_replace(',','.',request('amount',0));
            if($amount<=0)throw new RuntimeException('Informe o valor do orçamento.');
            $q=db()->prepare('SELECT * FROM moving_requests WHERE id=? AND status IN ("requested","quoted")');$q->execute([$requestId]);$move=$q->fetch();
            if(!$move)throw new RuntimeException('Solicitação indisponível.');
            db()->prepare('INSERT INTO moving_quotes(request_id,driver_id,amount,message,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?)')->execute([$requestId,$user['id'],$amount,request('message'),'pending',now(),now()]);
            db()->prepare('UPDATE moving_requests SET status="quoted",updated_at=? WHERE id=?')->execute([now(),$requestId]);
            create_notification((int)$move['client_id'],'Novo orçamento de mudança','Você recebeu um orçamento de '.money($amount).'.','success');
            back_to('moving','Orçamento enviado.');
            break;

        case 'update_moving_request':
            $id=(int)request('id');$status=request('status');
            $allowed=['accepted','on_route','in_progress','completed','cancelled'];
            if(!in_array($status,$allowed,true))throw new RuntimeException('Status inválido.');
            $q=db()->prepare('SELECT * FROM moving_requests WHERE id=?');$q->execute([$id]);$move=$q->fetch();
            if(!$move)throw new RuntimeException('Mudança não encontrada.');
            if((int)$move['client_id']===(int)$user['id']){
                if(!in_array($status,['accepted','cancelled'],true))throw new RuntimeException('Sem permissão.');
                if($status==='cancelled'){
                    db()->prepare('UPDATE moving_requests SET status="cancelled",updated_at=? WHERE id=?')->execute([now(),$id]);
                    if(!empty($move['driver_id']))create_notification((int)$move['driver_id'],'Mudança cancelada','O solicitante cancelou a mudança.','warning');
                }else{
                    $quoteId=(int)request('quote_id');
                    $quote=db()->prepare('SELECT * FROM moving_quotes WHERE id=? AND request_id=?');$quote->execute([$quoteId,$id]);$qv=$quote->fetch();
                    if(!$qv)throw new RuntimeException('Orçamento não encontrado.');
                    $driverId=(int)$qv['driver_id'];
                    db()->prepare('UPDATE moving_quotes SET status="accepted",updated_at=? WHERE id=?')->execute([now(),$quoteId]);
                    db()->prepare('UPDATE moving_quotes SET status="rejected",updated_at=? WHERE request_id=? AND id<>?')->execute([now(),$id,$quoteId]);
                    db()->prepare('UPDATE moving_requests SET driver_id=?,final_value=?,status="accepted",pix_released=1,updated_at=? WHERE id=?')->execute([$driverId,$qv['amount'],now(),$id]);
                    create_notification($driverId,'Orçamento aceito','O solicitante aceitou seu orçamento de mudança.','success');
                    release_pix_notification((int)$move['client_id'],$driverId,'mudança ou frete');
                }
            }else{
                if(!is_admin() && (int)$move['driver_id']!==(int)$user['id'])throw new RuntimeException('Sem permissão.');
                db()->prepare('UPDATE moving_requests SET status=?,updated_at=? WHERE id=?')->execute([$status,now(),$id]);
                create_notification((int)$move['client_id'],'Mudança atualizada','O status da sua mudança agora é '.status_label($status).'.','info');
            }
            back_to('moving','Mudança atualizada.');
            break;

        case 'save_club_member':
            must_role(['company','employee']);must_company_type('shooting_club');$companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];$id=(int)request('id',0);
            if(!$companyId)throw new RuntimeException('Vincule este usuário a uma empresa.');
            $documentDigits=only_digits(request('document'));
            if(strlen($documentDigits)!==11||!validate_cpf($documentDigits))throw new RuntimeException('CPF inválido. Verifique o número informado.');
            $document=format_cpf($documentDigits);
            $dup=db()->prepare('SELECT id FROM club_members WHERE company_id=? AND replace(replace(document,".",""),"-","")=? AND id<>? LIMIT 1');
            $dup->execute([$companyId,$documentDigits,$id]);
            if($dup->fetchColumn())throw new RuntimeException('Este CPF já está cadastrado neste clube.');
            $existingPhoto='';if($id){$q=db()->prepare('SELECT photo FROM club_members WHERE id=?');$q->execute([$id]);$existingPhoto=(string)$q->fetchColumn();}
            $photo=upload_image('photo_file','clube/associados',$existingPhoto);
            $data=[$companyId,request('name'),$document,only_digits(request('phone')),request('email'),$photo,request('plan_name'),request('status','active'),request('expires_at')?:null,request('notes')];
            if(!$data[1])throw new RuntimeException('Informe o nome do associado.');
            if($id)db()->prepare('UPDATE club_members SET company_id=?,name=?,document=?,phone=?,email=?,photo=?,plan_name=?,status=?,expires_at=?,notes=?,updated_at=? WHERE id=?')->execute(array_merge($data,[now(),$id]));
            else db()->prepare('INSERT INTO club_members(company_id,name,document,phone,email,photo,plan_name,status,expires_at,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)')->execute(array_merge($data,[now(),now()]));
            back_to('club','Associado salvo.');
            break;

        case 'save_club_instructor':
            must_role(['company','employee']);must_company_type('shooting_club');$companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            db()->prepare('INSERT INTO club_instructors(company_id,name,specialties,certifications,phone,email,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)')->execute([$companyId,request('name'),request('specialties'),request('certifications'),only_digits(request('phone')),request('email'),request('status','available'),now(),now()]);
            back_to('club','Instrutor salvo.');
            break;

        case 'save_club_appointment':
            must_role(['company','employee']);must_company_type('shooting_club');$companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            db()->prepare('INSERT INTO club_appointments(company_id,member_id,instructor_id,appointment_type,scheduled_at,status,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)')->execute([$companyId,(int)request('member_id')?:null,(int)request('instructor_id')?:null,request('appointment_type','range'),request('scheduled_at'),request('status','scheduled'),request('notes'),now(),now()]);
            back_to('club','Agendamento salvo.');
            break;

        case 'save_club_course':
            must_role(['company','employee']);must_company_type('shooting_club');$companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            db()->prepare('INSERT INTO club_courses(company_id,title,level,instructor_id,starts_at,capacity,enrolled,status,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,request('title'),request('level'),(int)request('instructor_id')?:null,request('starts_at')?:null,(int)request('capacity',0),(int)request('enrolled',0),request('status','open'),request('notes'),now(),now()]);
            back_to('club','Curso salvo.');
            break;

        case 'save_club_asset':
            must_role(['company','employee']);must_company_type('shooting_club');$companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            db()->prepare('INSERT INTO club_assets(company_id,category,asset_name,caliber,patrimony_number,status,maintenance_at,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,request('category'),request('asset_name'),request('caliber'),request('patrimony_number'),request('status','available'),request('maintenance_at')?:null,request('notes'),now(),now()]);
            back_to('club','Item patrimonial salvo.');
            break;

        case 'save_club_ammunition':
            must_role(['company','employee']);must_company_type('shooting_club');$companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            db()->prepare('INSERT INTO club_ammunition(company_id,caliber,quantity,minimum_stock,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?)')->execute([$companyId,request('caliber'),(int)request('quantity',0),(int)request('minimum_stock',0),request('notes'),now(),now()]);
            back_to('club','Estoque administrativo atualizado.');
            break;

        case 'save_bank':
            must_admin();$id=(int)request('id',0);$existingImage='';if($id){$q=db()->prepare('SELECT image FROM banks WHERE id=?');$q->execute([$id]);$existingImage=(string)$q->fetchColumn();}
            $image=upload_image('image_file','bancos',$existingImage);$imageUrl=trim((string)request('image_url'));
            if($imageUrl!==''&&preg_match('~^https?://~i',$imageUrl)){if($image!==$existingImage&&$existingImage)delete_local_upload($existingImage);$image=$imageUrl;}
            $allowedRoles=request('allowed_roles',[]);if(!is_array($allowedRoles))$allowedRoles=[];$valid=array_keys(bank_role_options());$allowedRoles=array_values(array_intersect($valid,$allowedRoles));$allowed=request('allowed_all')?'all':implode(',',$allowedRoles);
            $accountUrl=trim((string)request('account_creation_url'));if(!is_super_admin()){$accountUrl='';if($id){$aq=db()->prepare('SELECT account_creation_url FROM banks WHERE id=?');$aq->execute([$id]);$accountUrl=(string)$aq->fetchColumn();}}if($accountUrl!==''&&!preg_match('~^https?://~i',$accountUrl))throw new RuntimeException('Informe um link válido para criar conta.');$data=[trim(request('name')),request('website_url'),request('app_url'),$accountUrl,$image,in_array(request('status','active'),['active','inactive'],true)?request('status','active'):'active',(int)request('sort_order',0),$allowed];if($data[0]==='')throw new RuntimeException('Informe o nome do banco.');
            if($id){db()->prepare('UPDATE banks SET name=?,website_url=?,app_url=?,account_creation_url=?,image=?,status=?,sort_order=?,allowed_roles=?,updated_at=? WHERE id=?')->execute(array_merge($data,[now(),$id]));$msg='Banco atualizado.';}
            else{db()->prepare('INSERT INTO banks(name,website_url,app_url,account_creation_url,image,status,sort_order,allowed_roles,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)')->execute(array_merge($data,[now(),now()]));$msg='Banco cadastrado.';}
            back_to('banks',$msg);
            break;
        case 'delete_bank':
            must_admin();$id=(int)request('id');$q=db()->prepare('SELECT image FROM banks WHERE id=?');$q->execute([$id]);$image=$q->fetchColumn();db()->prepare('DELETE FROM banks WHERE id=?')->execute([$id]);delete_local_upload($image);back_to('banks','Banco excluído.');
            break;
        case 'toggle_driver_favorite':
            $driverId=(int)request('driver_id');$q=db()->prepare("SELECT id FROM users WHERE id=? AND role IN ('mototaxi','taxi','private_driver')");$q->execute([$driverId]);if(!$q->fetchColumn())throw new RuntimeException('Motorista inválido.');$q=db()->prepare('SELECT id FROM favorite_drivers WHERE user_id=? AND driver_id=?');$q->execute([$user['id'],$driverId]);$id=$q->fetchColumn();if($id)db()->prepare('DELETE FROM favorite_drivers WHERE id=?')->execute([$id]);else db()->prepare('INSERT INTO favorite_drivers(user_id,driver_id,created_at) VALUES(?,?,?)')->execute([$user['id'],$driverId,now()]);back_to('rides',$id?'Motorista removido dos favoritos.':'Motorista adicionado aos favoritos.');
            break;

        case 'save_charge':
            if(!client_billing_enabled_for_user($user))throw new RuntimeException('O sistema de cobranças para seus clientes ainda não foi liberado pelo administrador.');
            $recipientId=(int)request('user_id',0);$publicId=strtoupper(trim(request('recipient_public_id')));
            if(!$recipientId&&$publicId!==''){$q=db()->prepare("SELECT id FROM users WHERE upper(public_id)=upper(?) AND status='active'");$q->execute([$publicId]);$recipientId=(int)$q->fetchColumn();}
            if(!$recipientId)throw new RuntimeException('Informe um usuário válido.');
            if($recipientId===(int)$user['id'])throw new RuntimeException('Você não pode cobrar a si mesmo.');
            if(!can_issue_charge_to($user,$recipientId))throw new RuntimeException('Cobrança não permitida. Você só pode cobrar clientes cadastrados pela sua empresa ou usuários que já utilizaram seus serviços.');
            $amount=(float)str_replace(',','.',request('amount',0));if($amount<=0)throw new RuntimeException('Informe um valor válido.');
            $paymentMethod=request('payment_method',pix_billing_enabled_for_user($user)?'pix':'link');if(!in_array($paymentMethod,['pix','link','card','cash'],true))$paymentMethod='link';
            if($paymentMethod==='pix'&&!pix_billing_enabled_for_user($user))throw new RuntimeException('A cobrança por Pix ainda não foi liberada pelo administrador.');
            $blocks=is_admin()?(int)request('blocks_access',0):0;$linkId=(int)request('payment_link_id',0);$link=request('payment_link');$pix=pix_billing_enabled_for_user($user)?($user['pix_key']??''):'';
            if(is_role('sub_admin')){$linkId=0;$link='';if($paymentMethod==='link')$paymentMethod='card';}
            if($linkId){$lq=db()->prepare('SELECT * FROM payment_links WHERE id=? AND owner_user_id=? AND status="active"');$lq->execute([$linkId,$user['id']]);$pl=$lq->fetch();if($pl){$link=$pl['url'];if(pix_billing_enabled_for_user($user)&&!$pix)$pix=$pl['pix_key'];}}
            db()->prepare('INSERT INTO charges(issuer_user_id,user_id,amount,description,due_date,payment_link,payment_link_id,pix_key_snapshot,payment_method,status,blocks_access,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)')
                ->execute([$user['id'],$recipientId,$amount,request('description'),request('due_date')?:null,$link,$linkId?:null,$pix,$paymentMethod,'pending',$blocks,now(),now()]);
            $chargeId=(int)db()->lastInsertId();$feeBreakdown=calculate_platform_fee((int)$user['id'],$amount,'charge',$chargeId);
            create_notification($recipientId,'Nova cobrança',($user['name']??'Um usuário').' enviou uma cobrança de '.money($feeBreakdown['total']).($feeBreakdown['fee']>0?' (taxa da plataforma de '.money($feeBreakdown['fee']).' paga por '.(($feeBreakdown['fee_payer']??'client')==='provider'?'prestador/empresa':'cliente').')':'').'.','warning','dashboard.php?page=charges','charge',$chargeId);
            back_to('charges','Cobrança enviada.');
            break;

        case 'send_charge_proof':
            $id=(int)request('id');$q=db()->prepare('SELECT * FROM charges WHERE id=? AND user_id=?');$q->execute([$id,$user['id']]);$ch=$q->fetch();if(!$ch)throw new RuntimeException('Cobrança não encontrada.');
            $proof=upload_document('proof_file','cobrancas/comprovantes',$ch['proof']);if(!$proof)throw new RuntimeException('Selecione o comprovante.');
            db()->prepare('UPDATE charges SET proof=?,status="proof_sent",updated_at=? WHERE id=?')->execute([$proof,now(),$id]);
            if(!empty($ch['subscription_id'])) db()->prepare("UPDATE subscriptions SET status='proof_sent',updated_at=? WHERE id=?")->execute([now(),(int)$ch['subscription_id']]);
            if((int)$ch['blocks_access']===1){db()->prepare("UPDATE users SET provisional_access_until=datetime('now','+7 days'),status='active',updated_at=? WHERE id=? AND blacklisted=0")->execute([now(),$user['id']]);}
            if($ch['issuer_user_id'])create_notification((int)$ch['issuer_user_id'],'Comprovante recebido',$user['name'].' enviou um comprovante para a cobrança #'.$id.'.','success');
            notify_super_admins('Comprovante recebido',$user['name'].' enviou comprovante para a cobrança #'.$id.'.','success');
            back_to($returnPage==='subscriptions'?'subscriptions':'charges','Comprovante enviado. Seu acesso fica liberado enquanto o administrador analisa.');
            break;

        case 'update_charge':
            $id=(int)request('id');$status=request('status');if(!in_array($status,['paid','rejected','pending','overdue'],true))throw new RuntimeException('Status inválido.');
            $q=db()->prepare('SELECT * FROM charges WHERE id=?');$q->execute([$id]);$ch=$q->fetch();if(!$ch)throw new RuntimeException('Cobrança não encontrada.');
            if(is_role('sub_admin')){$guardQ=db()->prepare('SELECT role FROM users WHERE id IN (?,?)');$guardQ->execute([(int)$ch['user_id'],(int)$ch['issuer_user_id']]);foreach($guardQ->fetchAll(PDO::FETCH_COLUMN) as $guardRole)if(in_array((string)$guardRole,['super_admin','sub_admin'],true))throw new RuntimeException('Administrador não pode alterar cobranças de contas administrativas.');}
            if(!is_admin()&&(int)$ch['issuer_user_id']!==(int)$user['id'])throw new RuntimeException('Sem permissão para analisar esta cobrança.');
            db()->prepare('UPDATE charges SET status=?,admin_notes=?,updated_at=? WHERE id=?')->execute([$status,request('admin_notes'),now(),$id]);
            if($status==='paid'){
                $issuerId=(int)($ch['issuer_user_id']?:($user['id']??0));
                $feeBreakdown=$issuerId>0?calculate_platform_fee($issuerId,(float)$ch['amount'],'charge',$id):['base'=>(float)$ch['amount'],'fee'=>0,'total'=>(float)$ch['amount']];$receipt=$issuerId>0?generate_transaction_receipt_png('charge',$id,(int)$ch['user_id'],$issuerId,(float)$feeBreakdown['total'],'Cobrança: '.(string)$ch['description'].($feeBreakdown['fee']>0?' • taxa '.money($feeBreakdown['fee']).' paga por '.(($feeBreakdown['fee_payer']??'client')==='provider'?'prestador/empresa':'cliente'):''),(string)($ch['payment_method']?:'pix'),(string)($ch['proof']??''),0):generate_charge_receipt_png($id);if($receipt!=='')db()->prepare('UPDATE charges SET receipt_path=? WHERE id=?')->execute([$receipt,$id]);
                if($issuerId>0)post_finance_pair('charge',$id,(int)$ch['user_id'],$issuerId,(float)$ch['amount'],(string)($ch['payment_method']?:'pix'),(string)($ch['proof']??''),$receipt,'Cobrança: '.(string)$ch['description'],0);
            }
            if(is_admin()&&$status==='paid'&&(int)$ch['blocks_access']===1){
                $renewDays=30;
                if(!empty($ch['subscription_id'])){
                    $pd=db()->prepare('SELECT p.duration_days FROM subscriptions s JOIN plans p ON p.id=s.plan_id WHERE s.id=?');$pd->execute([(int)$ch['subscription_id']]);$renewDays=max(1,(int)($pd->fetchColumn()?:30));
                }
                db()->prepare("UPDATE users SET subscription_expires_at=CASE WHEN subscription_expires_at IS NULL OR subscription_expires_at<? THEN date(?, '+'||?||' days') ELSE date(subscription_expires_at, '+'||?||' days') END,status='active',activated_by_admin_id=?,provisional_access_until=NULL,blacklisted=0,blacklist_reason=NULL,blacklisted_at=NULL,updated_at=? WHERE id=?")->execute([today(),today(),$renewDays,$renewDays,(int)$user['id'],now(),$ch['user_id']]);
                $eu=db()->prepare('SELECT subscription_expires_at FROM users WHERE id=?');$eu->execute([$ch['user_id']]);$newEnd=(string)$eu->fetchColumn();
                if(!empty($ch['subscription_id'])) db()->prepare("UPDATE subscriptions SET status='active',end_date=?,receipt_path=?,updated_at=? WHERE id=?")->execute([$newEnd,$receipt,now(),(int)$ch['subscription_id']]);
            }
            if(is_admin()&&$status==='rejected'&&(int)$ch['blocks_access']===1){
                if(request('blacklist_user')) db()->prepare("UPDATE users SET blacklisted=1,blacklist_reason=?,blacklisted_at=?,status='blocked',provisional_access_until=NULL,updated_at=? WHERE id=?")->execute([request('admin_notes')?:'Irregularidade em comprovante de pagamento',now(),now(),$ch['user_id']]);
                else db()->prepare("UPDATE users SET provisional_access_until=NULL,updated_at=? WHERE id=?")->execute([now(),$ch['user_id']]);
                if(!empty($ch['subscription_id'])) db()->prepare("UPDATE subscriptions SET status='overdue',updated_at=? WHERE id=?")->execute([now(),(int)$ch['subscription_id']]);
            }
            create_notification((int)$ch['user_id'],!empty($ch['subscription_id'])?'Mensalidade atualizada':'Cobrança atualizada',$status==='paid'?'Pagamento aprovado.':'Situação: '.status_label($status),$status==='paid'?'success':'warning',!empty($ch['subscription_id'])?'dashboard.php?page=subscriptions':'dashboard.php?page=charges',!empty($ch['subscription_id'])?'subscription':'charge',!empty($ch['subscription_id'])?(int)$ch['subscription_id']:$id);
            back_to($returnPage==='subscriptions'?'subscriptions':'charges','Cobrança atualizada.');
            break;

        case 'delete_charge':
            must_admin();$id=(int)request('id');$q=db()->prepare('SELECT proof,receipt_path,user_id,issuer_user_id FROM charges WHERE id=?');$q->execute([$id]);$chargeFiles=$q->fetch();
            if(is_role('sub_admin')&&$chargeFiles){$guardQ=db()->prepare('SELECT role FROM users WHERE id IN (?,?)');$guardQ->execute([(int)$chargeFiles['user_id'],(int)$chargeFiles['issuer_user_id']]);foreach($guardQ->fetchAll(PDO::FETCH_COLUMN) as $guardRole)if(in_array((string)$guardRole,['super_admin','sub_admin'],true))throw new RuntimeException('Administrador não pode apagar cobranças de contas administrativas.');}
            db()->prepare('DELETE FROM charges WHERE id=?')->execute([$id]);
            if($chargeFiles){if(!empty($chargeFiles['proof']))delete_local_upload($chargeFiles['proof']);if(!empty($chargeFiles['receipt_path']))delete_local_upload($chargeFiles['receipt_path']);}
            back_to('charges','Cobrança apagada.');
            break;


        case 'delete_old_receipts':
            must_admin();
            $result=purge_generated_receipts_older_than(3);
            $message='Limpeza concluída: '.(int)$result['files_deleted'].' arquivo(s) de comprovante com 3 meses ou mais foram apagados.';
            if((int)$result['records_deleted']>0)$message.=' '.(int)$result['records_deleted'].' registro(s) antigo(s) removido(s) da pasta de comprovantes.';
            log_activity('delete_old_receipts','receipts',null,$message.' Limite: '.$result['cutoff']);
            back_to('charges',$message);
            break;

        case 'save_contact':
            $contactId=(int)request('contact_user_id');
            if($contactId<1||$contactId===(int)$user['id'])throw new RuntimeException('Contato inválido.');
            $q=db()->prepare("SELECT id FROM users WHERE id=? AND status='active'");$q->execute([$contactId]);if(!$q->fetchColumn())throw new RuntimeException('Usuário não encontrado.');
            db()->prepare('INSERT OR IGNORE INTO contact_book(owner_user_id,contact_user_id,label,created_at) VALUES(?,?,?,?)')->execute([$user['id'],$contactId,request('label'),now()]);
            back_to('contacts','Contato salvo na sua conta.');
            break;

        case 'delete_contact':
            $id=(int)request('id');db()->prepare('DELETE FROM contact_book WHERE id=? AND owner_user_id=?')->execute([$id,$user['id']]);back_to('contacts','Contato removido.');
            break;

        case 'link_company_courier':
            must_role(['company','employee']);$companyId=is_admin()?(int)request('company_id'):resolve_company_id_for_user($user);if($companyId<1)throw new RuntimeException('Empresa não definida.');
            $publicId=strtoupper(trim(request('public_id')));$q=db()->prepare("SELECT id,name,role,delivery_mode_enabled FROM users WHERE upper(public_id)=upper(?) AND status='active' AND (role IN ('courier','motofreight') OR (role='mototaxi' AND delivery_mode_enabled=1))");$q->execute([$publicId]);$courier=$q->fetch();
            if(!$courier)throw new RuntimeException('Entregador não encontrado com este ID ou modalidade ainda não liberada.');
            $already=db()->prepare("SELECT id FROM company_couriers WHERE company_id=? AND user_id=? AND status='active'");$already->execute([$companyId,$courier['id']]);
            if(!$already->fetchColumn())assert_company_capacity($companyId,'couriers');
            db()->prepare('INSERT INTO company_couriers(company_id,user_id,relationship,status,created_at,updated_at) VALUES(?,?,?,?,?,?) ON CONFLICT(company_id,user_id) DO UPDATE SET relationship=excluded.relationship,status="active",updated_at=excluded.updated_at')->execute([$companyId,$courier['id'],'autonomous','active',now(),now()]);
            create_notification((int)$courier['id'],'Nova empresa contratante','Uma empresa adicionou você à equipe de entregadores autônomos.','success');
            back_to('deliveries','Entregador autônomo adicionado.');
            break;

        case 'request_company_edit_permission':
            must_role(['company','employee','courier','motofreight']);$companyId=in_array((string)$user['role'],['courier','motofreight'],true)?(int)request('company_id'):resolve_company_id_for_user($user);if(in_array((string)$user['role'],['courier','motofreight'],true)&&!courier_can_register_company_client((int)$user['id'],$companyId))throw new RuntimeException('Você não está vinculado a esta empresa.');$targetId=(int)request('target_user_id');$targetType=(string)request('target_type');if(!in_array($targetType,['client','employee'],true))throw new RuntimeException('Tipo de conta inválido.');
            if(in_array((string)$user['role'],['courier','motofreight'],true)&&$targetType!=='client')throw new RuntimeException('Entregadores só podem solicitar edição de clientes cadastrados para a empresa.');
            $roleNeeded=$targetType==='employee'?'employee':'client';$q=db()->prepare('SELECT id,name FROM users WHERE id=? AND company_id=? AND role=?');$q->execute([$targetId,$companyId,$roleNeeded]);$target=$q->fetch();if(!$target)throw new RuntimeException('Conta não pertence a esta empresa.');
            $pending=db()->prepare("SELECT id FROM company_edit_permission_requests WHERE company_id=? AND target_user_id=? AND target_type=? AND status='pending' ORDER BY id DESC LIMIT 1");$pending->execute([$companyId,$targetId,$targetType]);if($pending->fetchColumn())throw new RuntimeException('Já existe uma solicitação pendente para esta conta.');
            db()->prepare("INSERT INTO company_edit_permission_requests(company_id,target_type,target_user_id,requested_by,reason,status,created_at,updated_at) VALUES(?,?,?,?,?,'pending',?,?)")->execute([$companyId,$targetType,$targetId,(int)$user['id'],trim((string)request('reason')),now(),now()]);$rid=(int)db()->lastInsertId();
            foreach(db()->query("SELECT id FROM users WHERE role IN ('super_admin','sub_admin') AND status='active'")->fetchAll(PDO::FETCH_COLUMN) as $adminId)create_notification((int)$adminId,'Solicitação para editar conta',($user['name']??'Empresa').' solicitou permissão para editar '.$target['name'].'.','warning','dashboard.php?page=company_clients#edit-permissions','company_edit_permission',$rid);
            back_to($returnPage?:($targetType==='employee'?'company_team':'company_clients'),'Solicitação enviada à administração.');break;

        case 'review_company_edit_permission':
            must_admin();$id=(int)request('id');$decision=(string)request('decision');if(!in_array($decision,['approved','rejected'],true))throw new RuntimeException('Decisão inválida.');$q=db()->prepare("SELECT r.*,t.name target_name,c.owner_user_id FROM company_edit_permission_requests r JOIN users t ON t.id=r.target_user_id JOIN companies c ON c.id=r.company_id WHERE r.id=? AND r.status='pending'");$q->execute([$id]);$r=$q->fetch();if(!$r)throw new RuntimeException('Solicitação não encontrada ou já analisada.');$expires=$decision==='approved'?date('Y-m-d H:i:s',strtotime('+7 days')):null;db()->prepare('UPDATE company_edit_permission_requests SET status=?,reviewed_by=?,reviewed_at=?,expires_at=?,updated_at=? WHERE id=?')->execute([$decision,(int)$user['id'],now(),$expires,now(),$id]);if(!empty($r['owner_user_id']))create_notification((int)$r['owner_user_id'],'Permissão de edição '.($decision==='approved'?'aprovada':'recusada'),'A solicitação para editar '.$r['target_name'].' foi '.($decision==='approved'?'aprovada por 7 dias e vale para uma alteração.':'recusada.'),$decision==='approved'?'success':'danger','dashboard.php?page=company_clients#edit-permissions','company_edit_permission',$id);back_to($returnPage?:'company_clients',$decision==='approved'?'Permissão aprovada.':'Solicitação recusada.');break;

        case 'unlink_company_courier':
            must_role(['company','employee']);$companyId=resolve_company_id_for_user($user);$courierId=(int)request('courier_id');$q=db()->prepare("SELECT cc.relationship,u.company_id FROM company_couriers cc JOIN users u ON u.id=cc.user_id WHERE cc.company_id=? AND cc.user_id=? AND cc.status='active'");$q->execute([$companyId,$courierId]);$link=$q->fetch();if(!$link)throw new RuntimeException('Vínculo do entregador não encontrado.');db()->prepare("UPDATE company_couriers SET status='inactive',updated_at=? WHERE company_id=? AND user_id=?")->execute([now(),$companyId,$courierId]);if(($link['relationship']??'')==='internal'&&(int)($link['company_id']??0)===$companyId)db()->prepare('UPDATE users SET company_id=NULL,updated_at=? WHERE id=?')->execute([now(),$courierId]);create_notification($courierId,'Vínculo com empresa encerrado','A empresa encerrou seu vínculo de entregas. Sua conta LinkExpress continua ativa.','info','dashboard.php?page=deliveries','company_courier',$companyId);back_to('deliveries','Vínculo encerrado sem apagar a conta do entregador.');break;

        case 'create_company_courier':
            must_role(['company','employee']);$companyId=is_admin()?(int)request('company_id'):resolve_company_id_for_user($user);if($companyId<1)throw new RuntimeException('Empresa não definida.');
            $name=trim(request('name'));$email=strtolower(trim(request('email')));if($name===''||!filter_var($email,FILTER_VALIDATE_EMAIL))throw new RuntimeException('Nome e e-mail válidos são obrigatórios.');
            assert_company_capacity($companyId,'couriers');
            $exists=db()->prepare('SELECT id FROM users WHERE lower(email)=lower(?)');$exists->execute([$email]);if($exists->fetchColumn())throw new RuntimeException('Já existe usuário com este e-mail.');
            db()->prepare('INSERT INTO users(company_id,role,name,email,phone,password_hash,status,work_status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,'courier',$name,$email,only_digits(request('phone')),password_hash(request('password')?:'123456',PASSWORD_DEFAULT),'active','offline',now(),now()]);
            $courierId=(int)db()->lastInsertId();ensure_public_user_id($courierId);
            db()->prepare('INSERT INTO company_couriers(company_id,user_id,relationship,status,created_at,updated_at) VALUES(?,?,?,?,?,?)')->execute([$companyId,$courierId,'internal','active',now(),now()]);
            back_to('deliveries','Entregador próprio cadastrado. ID: '.public_user_id($courierId));
            break;

        case 'create_company_client':
        case 'save_company_client':
            must_role(['company','employee','courier','motofreight']);
            if((string)($user['role']??'')==='employee'&&!company_member_can_manage_clients($user))throw new RuntimeException('Seu nível na empresa não permite cadastrar ou editar clientes.');
            $courierManaging=in_array((string)($user['role']??''),['courier','motofreight'],true);
            $companyId=(is_admin()||$courierManaging)?(int)request('company_id'):resolve_company_id_for_user($user);
            if($courierManaging&&!courier_can_register_company_client((int)$user['id'],$companyId))throw new RuntimeException('Você não está vinculado a esta empresa.');
            if($companyId<1)throw new RuntimeException('Selecione uma empresa.');
            $id=(int)request('id',0);$name=trim(request('name'));$email=strtolower(trim(request('email')));
            if($name===''||!filter_var($email,FILTER_VALIDATE_EMAIL))throw new RuntimeException('Nome e e-mail válidos são obrigatórios.');
            $duplicate=db()->prepare('SELECT id FROM users WHERE lower(email)=lower(?) AND id<>?');$duplicate->execute([$email,$id]);if($duplicate->fetchColumn())throw new RuntimeException('Já existe outro usuário com este e-mail.');
            $phone=only_digits(request('phone'));$home=request('home_address');$homeNumber=request('home_number');$city=request('city');$state=normalize_state(request('state'));
            if($id){
                $own=db()->prepare("SELECT id FROM users WHERE id=? AND company_id=? AND role='client'");$own->execute([$id,$companyId]);if(!$own->fetchColumn())throw new RuntimeException('Cliente não pertence a esta empresa.');
                if(!company_can_edit_linked_user($companyId,$id,'client'))throw new RuntimeException('A edição deste cliente precisa de autorização da administração. Solicite a permissão na área Clientes da Empresa.');
                db()->prepare('UPDATE users SET name=?,email=?,phone=?,home_address=?,home_number=?,home_lat=?,home_lng=?,city=?,state=?,updated_at=? WHERE id=?')
                    ->execute([$name,$email,$phone,$home,$homeNumber,request('home_lat')?:null,request('home_lng')?:null,$city,$state,now(),$id]);
                if(request('password'))db()->prepare('UPDATE users SET password_hash=? WHERE id=?')->execute([password_hash(request('password'),PASSWORD_DEFAULT),$id]);
                consume_company_edit_permission($companyId,$id,'client');$clientId=$id;$msg='Cliente atualizado com autorização administrativa.';
            }else{
                assert_company_capacity($companyId,'clients');
                db()->prepare('INSERT INTO users(company_id,role,name,email,phone,password_hash,status,work_status,home_address,home_number,home_lat,home_lng,city,state,subscription_expires_at,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
                    ->execute([$companyId,'client',$name,$email,$phone,password_hash(request('password')?:'123456',PASSWORD_DEFAULT),'active','offline',$home,$homeNumber,request('home_lat')?:null,request('home_lng')?:null,$city,$state,date('Y-m-d',strtotime('+30 days')),now(),now()]);
                $clientId=(int)db()->lastInsertId();ensure_public_user_id($clientId);
                create_notification($clientId,'Conta criada por empresa','Uma empresa cadastrou sua conta na LinkExpress. Altere a senha após o primeiro acesso.','success');
                $msg='Cliente cadastrado. ID: '.public_user_id($clientId);
            }
            // Cliente cadastrado diretamente pela empresa não precisa aprovar o endereço a cada entrega.
            db()->prepare('INSERT INTO delivery_address_permissions(company_id,user_id,always_visible,status,created_at,updated_at) VALUES(?,?,1,"active",?,?) ON CONFLICT(company_id,user_id) DO UPDATE SET always_visible=1,status="active",updated_at=excluded.updated_at')
                ->execute([$companyId,$clientId,now(),now()]);
            back_to($returnPage?:'company_clients',$msg);
            break;

        case 'save_company_employee':
            must_role(['company','employee']);
            if((string)($user['role']??'')==='employee'&&!company_member_can_manage_team($user))throw new RuntimeException('Somente o responsável ou um administrador da empresa pode gerenciar a equipe.');
            $companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];if($companyId<1)throw new RuntimeException('Selecione uma empresa.');
            $id=(int)request('id',0);$name=trim(request('name'));$email=strtolower(trim(request('email')));$status=request('status','active');$position=trim((string)request('company_position'));$access=(string)request('company_access_level','staff');if(!in_array($access,['admin','manager','staff'],true))$access='staff';
            if($name===''||!filter_var($email,FILTER_VALIDATE_EMAIL))throw new RuntimeException('Nome e e-mail válidos são obrigatórios.');
            $dup=db()->prepare('SELECT id FROM users WHERE lower(email)=lower(?) AND id<>?');$dup->execute([$email,$id]);if($dup->fetchColumn())throw new RuntimeException('Já existe outro usuário com este e-mail.');
            if($id){
                $own=db()->prepare("SELECT id FROM users WHERE id=? AND company_id=? AND role='employee'");$own->execute([$id,$companyId]);if(!$own->fetchColumn())throw new RuntimeException('Funcionário não pertence a esta empresa.');
                if(!company_can_edit_linked_user($companyId,$id,'employee'))throw new RuntimeException('A edição deste funcionário precisa de autorização da administração. Solicite a permissão na área Clientes da Empresa.');
                db()->prepare('UPDATE users SET name=?,email=?,phone=?,status=?,company_position=?,company_access_level=?,updated_at=? WHERE id=?')->execute([$name,$email,only_digits(request('phone')),$status,$position,$access,now(),$id]);
                if(request('password'))db()->prepare('UPDATE users SET password_hash=? WHERE id=?')->execute([password_hash(request('password'),PASSWORD_DEFAULT),$id]);consume_company_edit_permission($companyId,$id,'employee');
            }else{
                assert_company_capacity($companyId,'employees');
                db()->prepare('INSERT INTO users(company_id,role,name,email,phone,password_hash,status,work_status,subscription_expires_at,company_position,company_access_level,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)')
                    ->execute([$companyId,'employee',$name,$email,only_digits(request('phone')),password_hash(request('password')?:'123456',PASSWORD_DEFAULT),'active','offline',date('Y-m-d',strtotime('+30 days')),$position,$access,now(),now()]);
                $id=(int)db()->lastInsertId();ensure_public_user_id($id);
            }
            back_to('company_team','Funcionário salvo com sucesso.');
            break;

        case 'delete_company_employee':
        case 'unlink_company_employee':
            must_role(['company','employee']);if((string)($user['role']??'')==='employee'&&!company_member_can_manage_team($user))throw new RuntimeException('Seu nível na empresa não permite encerrar vínculos da equipe.');$companyId=is_admin()?(int)request('company_id'):resolve_company_id_for_user($user);$id=(int)request('id');
            if($id===(int)$user['id'])throw new RuntimeException('Você não pode encerrar seu próprio vínculo por esta tela.');
            $q=db()->prepare("SELECT id FROM users WHERE id=? AND company_id=? AND role='employee'");$q->execute([$id,$companyId]);if(!$q->fetchColumn())throw new RuntimeException('Funcionário não encontrado.');
            db()->prepare("UPDATE users SET company_id=NULL,role='client',updated_at=? WHERE id=?")->execute([now(),$id]);create_notification($id,'Vínculo de funcionário encerrado','A empresa encerrou seu vínculo de funcionário. Sua conta pessoal na LinkExpress foi mantida.','info');back_to('company_team','Vínculo encerrado sem apagar a conta do usuário.');
            break;

        case 'create_delivery_job':
            must_role(['company','employee']);$companyId=is_admin()?(int)request('company_id'):resolve_company_id_for_user($user);
            $publicId=strtoupper(trim(request('client_public_id')));$client=null;
            if($publicId!==''){$q=db()->prepare("SELECT u.*,c.address company_address,c.address_number company_address_number,c.latitude company_lat,c.longitude company_lng FROM users u LEFT JOIN companies c ON c.id=u.company_id WHERE upper(u.public_id)=upper(?) AND u.status='active'");$q->execute([$publicId]);$client=$q->fetch();if(!$client)throw new RuntimeException('Cliente não encontrado pelo ID informado.');}
            $clientId=$client?(int)$client['id']:null;$name=$client?$client['name']:request('client_name');$phone=$client?$client['phone']:only_digits(request('client_phone'));
            $address=$client?(full_user_address($client)) : request('address');$lat=$client?$client['home_lat']:(request('latitude')?:null);$lng=$client?$client['home_lng']:(request('longitude')?:null);
            if($client&&$client['role']==='company'&&$client['company_address']){$address=$client['company_address'].($client['company_address_number']?', '.$client['company_address_number']:'');$lat=$client['company_lat'];$lng=$client['company_lng'];}
            if(!$name)throw new RuntimeException('Informe o cliente pelo ID ou pelo nome.');if(!$address)throw new RuntimeException('Informe ou cadastre o endereço da entrega.');
            $permission=false;if($client){$pq=db()->prepare("SELECT COUNT(*) FROM delivery_address_permissions WHERE company_id=? AND user_id=? AND always_visible=1 AND status='active'");$pq->execute([$companyId,$clientId]);$permission=(int)$pq->fetchColumn()>0||$client['role']==='company'||(int)($client['company_id']??0)===$companyId;}
            $auth=$client?($permission?'approved':'pending'):'approved';$status=$auth==='approved'?'ready':'waiting_authorization';
            $distance=max(0,(float)str_replace(',','.',request('distance_km',0)));
            if($companyId>0 && $lat!==null && $lng!==null){
                $cq=db()->prepare('SELECT latitude,longitude FROM companies WHERE id=? LIMIT 1');$cq->execute([$companyId]);$origin=$cq->fetch();
                if($origin && $origin['latitude']!==null && $origin['longitude']!==null){$straight=distance_km($origin['latitude'],$origin['longitude'],$lat,$lng);if($straight!==null)$distance=round($straight*1.25,2);}
            }
            db()->prepare('INSERT INTO delivery_jobs(company_id,client_id,client_public_id,client_name,client_phone,address,latitude,longitude,status,authorization_status,notes,package_description,sack_qty,box_qty,scheduled_at,distance_km,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
                ->execute([$companyId,$clientId,$client?$client['public_id']:$publicId,$name,$phone,$address,$lat,$lng,$status,$auth,request('notes'),request('package_description'),max(0,(int)request('sack_qty')),max(0,(int)request('box_qty')),request('scheduled_at')?:null,$distance,now(),now()]);
            $jobId=(int)db()->lastInsertId();if($client&&$auth==='pending')create_notification($clientId,'Autorizar entrega','Uma empresa solicitou autorização para usar seu endereço na entrega #'.$jobId.'.','warning');
            elseif($client)create_notification($clientId,'Nova entrega agendada','A empresa agendou a entrega #'.$jobId.'.','info');
            back_to('deliveries',$auth==='pending'?'Entrega criada e autorização enviada.':'Entrega criada.');
            break;

        case 'respond_delivery_authorization':
            $id=(int)request('id');$decision=request('decision');if(!in_array($decision,['approved','rejected'],true))throw new RuntimeException('Decisão inválida.');
            $q=db()->prepare('SELECT * FROM delivery_jobs WHERE id=? AND client_id=?');$q->execute([$id,$user['id']]);$job=$q->fetch();if(!$job)throw new RuntimeException('Solicitação não encontrada.');
            db()->prepare('UPDATE delivery_jobs SET authorization_status=?,status=?,updated_at=? WHERE id=?')->execute([$decision,$decision==='approved'?'ready':'rejected',now(),$id]);
            if($decision==='approved'&&request('always_visible')){
                db()->prepare('INSERT INTO delivery_address_permissions(company_id,user_id,always_visible,status,created_at,updated_at) VALUES(?,?,1,"active",?,?) ON CONFLICT(company_id,user_id) DO UPDATE SET always_visible=1,status="active",updated_at=excluded.updated_at')->execute([$job['company_id'],$user['id'],now(),now()]);
            }
            notify_company_operational_users((int)$job['company_id'],'Autorização de entrega atualizada',$user['name'].' '.($decision==='approved'?'autorizou':'recusou').' o uso do endereço.','info','dashboard.php?page=deliveries','delivery_job',$id);
            back_to('deliveries',$decision==='approved'?'Endereço autorizado.':'Entrega recusada.');
            break;

        case 'assign_delivery_job':
            must_role(['company','employee']);$id=(int)request('id');$courierId=(int)request('courier_id');
            $q=db()->prepare('SELECT * FROM delivery_jobs WHERE id=?');$q->execute([$id]);$job=$q->fetch();if(!$job)throw new RuntimeException('Entrega não encontrada.');
            if(!is_admin()){$companyId=resolve_company_id_for_user($user);if($companyId<1||(int)$job['company_id']!==$companyId)throw new RuntimeException('Entrega de outra empresa.');}
            if($job['authorization_status']!=='approved')throw new RuntimeException('O cliente ainda não autorizou o endereço.');
            $check=db()->prepare("SELECT COUNT(*) FROM company_couriers WHERE company_id=? AND user_id=? AND status='active'");$check->execute([$job['company_id'],$courierId]);if(!(int)$check->fetchColumn())throw new RuntimeException('Entregador não pertence à equipe ativa.');
            db()->prepare('UPDATE delivery_jobs SET assigned_courier_id=?,status="assigned",updated_at=? WHERE id=?')->execute([$courierId,now(),$id]);
            if(!empty($job['order_id']))db()->prepare('UPDATE orders SET delivery_user_id=?,status=CASE WHEN status="received" THEN "ready" ELSE status END,updated_at=? WHERE id=?')->execute([$courierId,now(),(int)$job['order_id']]);
            if(company_is_gas((int)$job['company_id']))create_notification($courierId,'Nova entrega de gás','Uma nova ordem foi atribuída a você. Abra sua Área de Trabalho — Gás para ver cliente, endereço e produtos.','success','dashboard.php?page=gas_courier','delivery_job',$id);
            else create_notification($courierId,'Nova entrega atribuída','O endereço foi liberado. Abra Entregadores e Entregas para navegar pelo Waze ou Google Maps.','success','dashboard.php?page=deliveries','delivery_job',$id);
            if($job['client_id'])create_notification((int)$job['client_id'],'Entregador atribuído','Sua entrega já possui entregador responsável.','info');
            back_to('deliveries','Entrega atribuída e endereço liberado.');
            break;

        case 'update_delivery_job':
            must_role(['courier','motofreight','mototaxi']);if(!user_can_work_deliveries($user))throw new RuntimeException('A modalidade Entregas ainda não foi liberada pelo administrador.');$id=(int)request('id');$status=request('status');if(!in_array($status,['accepted','on_route','delivered','cancelled'],true))throw new RuntimeException('Status inválido.');
            $q=db()->prepare('SELECT * FROM delivery_jobs WHERE id=? AND assigned_courier_id=?');$q->execute([$id,$user['id']]);$job=$q->fetch();if(!$job)throw new RuntimeException('Entrega não encontrada.');
            if(in_array($job['status'],['delivered','cancelled','rejected'],true))throw new RuntimeException('Esta entrega já foi encerrada e não pode mais ser alterada.');
            $terminal=in_array($status,['delivered','cancelled'],true)?now():null;
            db()->prepare('UPDATE delivery_jobs SET status=?,delivered_at=CASE WHEN ?="delivered" THEN ? ELSE delivered_at END,terminal_at=CASE WHEN ? IS NOT NULL THEN ? ELSE terminal_at END,updated_at=? WHERE id=?')->execute([$status,$status,$terminal,$terminal,$terminal,now(),$id]);
            if(!empty($job['order_id'])&&in_array($status,['accepted','on_route','delivered'],true)){$orderStatus=$status==='accepted'?'ready':$status;db()->prepare('UPDATE orders SET delivery_user_id=?,status=?,updated_at=? WHERE id=?')->execute([(int)$user['id'],$orderStatus,now(),(int)$job['order_id']]);}
            if(!empty($job['order_id'])&&$status==='cancelled')db()->prepare("UPDATE orders SET delivery_user_id=NULL,status=CASE WHEN status NOT IN ('delivered','cancelled') THEN 'ready' ELSE status END,updated_at=? WHERE id=?")->execute([now(),(int)$job['order_id']]);
            if($job['client_id'])create_notification((int)$job['client_id'],'Entrega atualizada','Status: '.status_label($status).'.','success','dashboard.php?page=deliveries','delivery_job',$id);
            notify_company_operational_users((int)$job['company_id'],'Entrega atualizada','A entrega #'.$id.' foi marcada como '.status_label($status).'.','success','dashboard.php?page=deliveries','delivery_job',$id,(int)$user['id']);
            $returnPage=(string)request('return_page','deliveries');
            if(!in_array($returnPage,['deliveries','gas_courier'],true))$returnPage='deliveries';
            back_to($returnPage,'Entrega atualizada.');
            break;

        case 'save_public_org':
            must_admin();$id=(int)request('id',0);$name=trim(request('name'));$city=trim(request('city'));$address=trim(request('address'));if(!$name||!$city||!$address)throw new RuntimeException('Nome, cidade e endereço são obrigatórios.');
            $data=[$name,request('category'),$city,normalize_state(request('state')),$address,request('latitude')?:null,request('longitude')?:null,request('opens_at')?:null,request('closes_at')?:null,only_digits(request('phone')),request('website'),in_array(request('status','active'),['active','inactive'],true)?request('status','active'):'active'];
            if($id){db()->prepare('UPDATE public_orgs SET name=?,category=?,city=?,state=?,address=?,latitude=?,longitude=?,opens_at=?,closes_at=?,phone=?,website=?,status=?,updated_at=? WHERE id=?')->execute(array_merge($data,[now(),$id]));$msg='Órgão público atualizado.';}
            else{db()->prepare('INSERT INTO public_orgs(name,category,city,state,address,latitude,longitude,opens_at,closes_at,phone,website,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute(array_merge($data,[now(),now()]));$msg='Órgão público cadastrado.';}
            back_to('public_orgs',$msg);
            break;

        case 'delete_public_org':
            must_admin();db()->prepare('DELETE FROM public_orgs WHERE id=?')->execute([(int)request('id')]);back_to('public_orgs','Órgão removido.');
            break;

        case 'approve_ride_fare':
            must_admin();$id=(int)request('id');$decision=request('decision');$q=db()->prepare('SELECT * FROM rides WHERE id=? AND fare_review_status="pending"');$q->execute([$id]);$ride=$q->fetch();if(!$ride)throw new RuntimeException('Tarifa não está pendente.');
            if($decision==='approve'){
                $value=(float)$ride['driver_proposed_value'];db()->prepare('UPDATE rides SET estimated_value=?,status="fare_proposed",fare_review_status="approved",client_fare_status="pending",pix_released=0,updated_at=? WHERE id=?')->execute([$value,now(),$id]);
                create_notification((int)$ride['client_id'],'Valor liberado para seu aceite','A administração aprovou a proposta de '.money($value).'. Agora aceite ou recuse o valor da corrida.','warning','dashboard.php?page=rides','ride',$id);create_notification((int)$ride['driver_id'],'Tarifa aprovada pela administração','A proposta foi liberada e agora aguarda o aceite do cliente.','success','dashboard.php?page=rides','ride',$id);
            }else{
                db()->prepare('UPDATE rides SET driver_id=NULL,status="requested",fare_review_status="rejected",client_fare_status="not_required",driver_proposed_value=0,pix_released=0,updated_at=? WHERE id=?')->execute([now(),$id]);
                create_notification((int)$ride['driver_id'],'Tarifa recusada','A proposta abaixo do mínimo foi recusada.','warning');create_notification((int)$ride['client_id'],'Corrida novamente disponível','A proposta do motorista foi recusada e a corrida voltou para a fila.','info');
            }
            back_to('rides','Análise da tarifa concluída.');
            break;

        case 'link_club_member_by_id':
            must_role(['company','employee']);must_company_type('shooting_club');$companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];$publicId=strtoupper(trim(request('public_id')));
            $q=db()->prepare("SELECT * FROM users WHERE upper(public_id)=upper(?) AND status='active'");$q->execute([$publicId]);$member=$q->fetch();if(!$member)throw new RuntimeException('Usuário não encontrado.');
            $exists=db()->prepare('SELECT id FROM club_members WHERE company_id=? AND user_id=?');$exists->execute([$companyId,$member['id']]);if($exists->fetchColumn())throw new RuntimeException('Este usuário já é associado.');
            db()->prepare('INSERT INTO club_members(company_id,user_id,name,document,phone,email,photo,plan_name,status,expires_at,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$member['id'],$member['name'],$member['document'],$member['phone'],$member['email'],$member['avatar'],request('plan_name'),'active',request('expires_at')?:null,request('notes'),now(),now()]);
            create_notification((int)$member['id'],'Vínculo com clube','Seu ID foi associado a um clube na LinkExpress.','success');back_to('club','Usuário associado ao clube.');
            break;


        case 'delete_company':
            must_admin();if(strtoupper(trim((string)request('confirmed_delete_phrase')))!=='EXCLUIR')throw new RuntimeException('Confirmação de exclusão inválida. Digite EXCLUIR para apagar a empresa.');$id=(int)request('id');$q=db()->prepare('SELECT c.*,u.email owner_email FROM companies c LEFT JOIN users u ON u.id=c.owner_user_id WHERE c.id=?');$q->execute([$id]);$company=$q->fetch();if(!$company)throw new RuntimeException('Empresa não encontrada.');
            db()->beginTransaction();
            $vehicleIds=db()->prepare('SELECT id FROM vehicles WHERE company_id=?');$vehicleIds->execute([$id]);$vids=array_column($vehicleIds->fetchAll(),'id');
            foreach($vids as $vid){db()->prepare('DELETE FROM vehicle_media WHERE vehicle_id=?')->execute([$vid]);db()->prepare('DELETE FROM vehicle_reservations WHERE vehicle_id=?')->execute([$vid]);}
            foreach(['products','categories','delivery_jobs','company_couriers','fleet_drivers','orders','vehicles'] as $table)db()->prepare('DELETE FROM '.$table.' WHERE company_id=?')->execute([$id]);
            db()->prepare('UPDATE users SET company_id=NULL,role=CASE WHEN role="employee" THEN "client" ELSE role END,updated_at=? WHERE company_id=?')->execute([now(),$id]);
            db()->prepare('UPDATE account_deletion_requests SET status="resolved",updated_at=?,resolved_at=?,resolved_by=? WHERE company_id=? AND request_type="company" AND status="pending"')->execute([now(),now(),(int)$user['id'],$id]);
            db()->prepare('DELETE FROM companies WHERE id=?')->execute([$id]);db()->commit();
            delete_local_upload($company['logo']);delete_local_upload($company['banner']);log_activity('delete_company','companies',$id,$company['name']);back_to('companies','Empresa apagada. As contas vinculadas foram preservadas e desvinculadas.');

        case 'add_cart':
            $productId=(int)request('product_id');$qty=max(1,(float)str_replace(',','.',request('qty',1)));
            $q=db()->prepare("SELECT * FROM products WHERE id=? AND status='active' AND stock>0");$q->execute([$productId]);$product=$q->fetch();if(!$product)throw new RuntimeException('Produto indisponível.');
            $existingCompany=db()->prepare('SELECT company_id FROM cart_items WHERE user_id=? LIMIT 1');$existingCompany->execute([$user['id']]);$cartCompany=(int)$existingCompany->fetchColumn();
            if($cartCompany&&$cartCompany!==(int)$product['company_id'])throw new RuntimeException('Finalize ou esvazie o carrinho da empresa atual antes de adicionar itens de outra empresa.');
            $qty=min($qty,(float)$product['stock']);
            db()->prepare('INSERT INTO cart_items(user_id,company_id,product_id,qty,created_at,updated_at) VALUES(?,?,?,?,?,?) ON CONFLICT(user_id,product_id) DO UPDATE SET qty=MIN(excluded.qty+cart_items.qty,(SELECT stock FROM products WHERE id=excluded.product_id)),updated_at=excluded.updated_at')
                ->execute([$user['id'],$product['company_id'],$productId,$qty,now(),now()]);
            back_to('catalog','Produto adicionado ao carrinho.');

        case 'update_cart_item':
            $id=(int)request('id');$qty=max(0,(float)str_replace(',','.',request('qty',1)));
            if($qty<=0)db()->prepare('DELETE FROM cart_items WHERE id=? AND user_id=?')->execute([$id,$user['id']]);
            else{
                $cq=db()->prepare('SELECT ci.id,p.stock,p.name FROM cart_items ci JOIN products p ON p.id=ci.product_id WHERE ci.id=? AND ci.user_id=?');$cq->execute([$id,$user['id']]);$cartRow=$cq->fetch();if(!$cartRow)throw new RuntimeException('Item do carrinho não encontrado.');
                if($qty>(float)$cartRow['stock'])throw new RuntimeException('Quantidade indisponível. Estoque atual de '.$cartRow['name'].': '.number_format((float)$cartRow['stock'],0,',','.').'.');
                db()->prepare('UPDATE cart_items SET qty=?,updated_at=? WHERE id=? AND user_id=?')->execute([$qty,now(),$id,$user['id']]);
            }
            back_to('cart','Carrinho atualizado.');

        case 'remove_cart_item':
            db()->prepare('DELETE FROM cart_items WHERE id=? AND user_id=?')->execute([(int)request('id'),$user['id']]);back_to('cart','Item removido.');

        case 'checkout_cart':
            $q=db()->prepare('SELECT ci.*,p.name,p.price,p.stock,p.status,c.owner_user_id,c.name company_name FROM cart_items ci JOIN products p ON p.id=ci.product_id JOIN companies c ON c.id=ci.company_id WHERE ci.user_id=? ORDER BY ci.id');$q->execute([$user['id']]);$items=$q->fetchAll();if(!$items)throw new RuntimeException('Seu carrinho está vazio.');
            $companyId=(int)$items[0]['company_id'];foreach($items as $it)if((int)$it['company_id']!==$companyId)throw new RuntimeException('O carrinho deve conter produtos de uma única empresa.');$cq=db()->prepare('SELECT pickup_scheduling_enabled,pickup_weekdays,pickup_start_time,pickup_end_time,pickup_interval_minutes FROM companies WHERE id=? LIMIT 1');$cq->execute([$companyId]);$pickupCompany=$cq->fetch()?:[];
            $fulfillment=request('fulfillment_type','delivery');if(!in_array($fulfillment,['delivery','pickup'],true))$fulfillment='delivery';
            $address=$fulfillment==='delivery'?(request('address')?:full_user_address($user)):'Retirada no estabelecimento';if($fulfillment==='delivery'&&trim($address)==='')throw new RuntimeException('Informe o endereço de entrega.');$pickupScheduledAt=null;if($fulfillment==='pickup'&&!empty($pickupCompany['pickup_scheduling_enabled'])){$pickupDate=trim((string)request('pickup_date'));$pickupTime=trim((string)request('pickup_time'));if(!company_pickup_schedule_allows($pickupCompany,$pickupDate,$pickupTime))throw new RuntimeException('Escolha uma data e horário disponíveis para retirar a encomenda.');$pickupScheduledAt=$pickupDate.' '.$pickupTime.':00';}
            $subtotal=0;foreach($items as $it){if($it['status']!=='active'||(float)$it['stock']<(float)$it['qty'])throw new RuntimeException('Estoque insuficiente para '.$it['name'].'.');$subtotal+=(float)$it['price']*(float)$it['qty'];}
            $fee=$fulfillment==='delivery'?(float)str_replace(',','.',request('delivery_fee',0)):0;$number='LX'.date('ymdHis').random_int(10,99);
            db()->beginTransaction();
            db()->prepare('INSERT INTO orders(company_id,client_id,order_number,status,subtotal,delivery_fee,total,payment_method,payment_status,address,notes,fulfillment_type,client_address_snapshot,stock_committed,pickup_scheduled_at,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
                ->execute([$companyId,$user['id'],$number,'received',$subtotal,$fee,$subtotal+$fee,request('payment_method','pix'),'pending',$address,request('notes'),$fulfillment,$address,0,$pickupScheduledAt,now(),now()]);
            $orderId=(int)db()->lastInsertId();$ins=db()->prepare('INSERT INTO order_items(order_id,product_id,name,qty,price,total) VALUES(?,?,?,?,?,?)');
            foreach($items as $it){$line=(float)$it['price']*(float)$it['qty'];$ins->execute([$orderId,$it['product_id'],$it['name'],$it['qty'],$it['price'],$line]);}
            db()->prepare('DELETE FROM cart_items WHERE user_id=?')->execute([$user['id']]);db()->commit();
            $cartOrderMessage='Pedido '.$number.' com '.count($items).' item(ns).'.($pickupScheduledAt?' Retirada agendada para '.date('d/m/Y H:i',strtotime($pickupScheduledAt)).'.':'');create_notification((int)$items[0]['owner_user_id'],'Novo pedido',$cartOrderMessage,'success');back_to('orders','Pedido '.$number.' criado com sucesso.');

        case 'delete_payment':
            $id=(int)request('id');$q=db()->prepare('SELECT * FROM payments WHERE id=?');$q->execute([$id]);$pay=$q->fetch();if(!$pay)throw new RuntimeException('Movimentação não encontrada.');
            if(!is_admin()&&(int)($pay['owner_user_id']?:$pay['payer_user_id'])!==(int)$user['id'])throw new RuntimeException('Sem permissão.');
            db()->prepare('DELETE FROM payments WHERE id=?')->execute([$id]);delete_local_upload($pay['proof']);back_to('finance','Movimentação apagada.');

        case 'save_payment_link':
            if(is_role('sub_admin'))throw new RuntimeException('Administrador não possui permissão para criar ou enviar links de pagamento.');
            $rp=request('return_page','charges');
            if($rp==='subscriptions'&&!is_super_admin())throw new RuntimeException('Somente o Administrador Sênior pode configurar links das mensalidades.');
            if($rp!=='subscriptions'&&!client_billing_enabled_for_user($user))throw new RuntimeException('O sistema de cobranças para seus clientes ainda não foi liberado pelo administrador.');
            $title=trim(request('title'));$urlValue=trim(request('url'));$pix=trim(request('pix_key'));$planId=(int)request('plan_id',0);$kind=$rp==='subscriptions'?'subscription_plan':'generic';
            if($pix!==''&&!pix_billing_enabled_for_user($user))throw new RuntimeException('O uso de Pix ainda não foi liberado pelo administrador.');
            if($title===''||($urlValue===''&&$pix===''))throw new RuntimeException('Informe o título e um link ou chave Pix.');
            if($kind==='subscription_plan'){$pq=db()->prepare('SELECT id FROM plans WHERE id=?');$pq->execute([$planId]);if($planId<1||!$pq->fetchColumn())throw new RuntimeException('Selecione o plano deste link.');}
            db()->prepare('INSERT INTO payment_links(owner_user_id,title,url,pix_key,plan_id,link_kind,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)')->execute([$user['id'],$title,$urlValue,$pix,$planId?:null,$kind,'active',now(),now()]);
            back_to($rp==='subscriptions'?'subscriptions':'charges','Link de cobrança salvo.');

        case 'delete_payment_link':
            $linkId=(int)request('id');$rp=request('return_page','charges');
            if(is_role('sub_admin'))throw new RuntimeException('Administrador não possui permissão para apagar links de pagamento.');
            if(is_super_admin())db()->prepare('DELETE FROM payment_links WHERE id=?')->execute([$linkId]);else db()->prepare('DELETE FROM payment_links WHERE id=? AND owner_user_id=?')->execute([$linkId,$user['id']]);
            back_to($rp==='subscriptions'?'subscriptions':'charges','Link removido.');

        case 'delete_ride':
            must_admin();db()->prepare('DELETE FROM ride_driver_blocks WHERE ride_id=?')->execute([(int)request('id')]);db()->prepare('DELETE FROM rides WHERE id=?')->execute([(int)request('id')]);back_to('rides','Corrida apagada.');

        case 'delete_rides_bulk':
            must_admin();$ids=request('ids',[]);if(!is_array($ids)||!$ids)throw new RuntimeException('Selecione ao menos uma corrida.');
            $ids=array_values(array_filter(array_map('intval',$ids)));$ph=implode(',',array_fill(0,count($ids),'?'));db()->prepare('DELETE FROM ride_driver_blocks WHERE ride_id IN ('.$ph.')')->execute($ids);db()->prepare('DELETE FROM rides WHERE id IN ('.$ph.')')->execute($ids);back_to('rides',count($ids).' corrida(s) apagada(s).');

        case 'dispatch_fleet_ride':
            must_role(['company','employee']);
            $rideId=(int)request('id');$driverId=(int)request('driver_id');
            $companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            if($rideId<1||$driverId<1||$companyId<1)throw new RuntimeException('Selecione a corrida e o motorista.');
            $rq=db()->prepare("SELECT * FROM rides WHERE id=? AND company_id=? AND status NOT IN ('completed','cancelled')");$rq->execute([$rideId,$companyId]);$ride=$rq->fetch();
            if(!$ride)throw new RuntimeException('Corrida não encontrada para esta empresa.');
            if($ride['driver_id'])throw new RuntimeException('Esta corrida já foi atribuída a um motorista.');
            $fq=db()->prepare("SELECT u.name FROM fleet_drivers fd JOIN users u ON u.id=fd.user_id WHERE fd.company_id=? AND fd.user_id=? AND fd.status='active' AND u.status='active'");$fq->execute([$companyId,$driverId]);$driverName=$fq->fetchColumn();
            if(!$driverName)throw new RuntimeException('Motorista fora da frota ativa.');
            db()->prepare('UPDATE rides SET driver_id=?,updated_at=? WHERE id=?')->execute([$driverId,now(),$rideId]);
            create_notification($driverId,'Nova corrida da frota','A empresa atribuiu uma corrida para você. Abra Corridas para aceitar e confirmar o valor.','success');
            create_notification((int)$ride['client_id'],'Motorista selecionado',$driverName.' foi selecionado pela empresa para sua corrida.','info');
            back_to('fleet','Corrida enviada ao motorista. Ele deverá aceitar a solicitação.');
            break;

        case 'link_fleet_driver':
            must_role(['company','employee']);
            $companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            if($companyId<1)throw new RuntimeException('Selecione a empresa da frota.');
            $fleetCompany=transport_company_info($companyId);
            if(!$fleetCompany||($fleetCompany['status']??'')!=='active'||!in_array($fleetCompany['transport_kind']??null,['taxi','mototaxi'],true))throw new RuntimeException('A empresa selecionada precisa ser do segmento Táxi ou Mototáxi.');
            $publicId=strtoupper(trim(request('public_id')));
            $allowedRoles=($fleetCompany['transport_kind']??'taxi')==='mototaxi'?['mototaxi']:['taxi','private_driver'];
            $placeholders=implode(',',array_fill(0,count($allowedRoles),'?'));
            $params=array_merge([$publicId],$allowedRoles);
            $q=db()->prepare("SELECT id,name,role FROM users WHERE upper(public_id)=upper(?) AND role IN ($placeholders) AND status='active'");$q->execute($params);$driver=$q->fetch();
            if(!$driver)throw new RuntimeException('Motorista não encontrado. Para Táxi use perfil Taxista ou Motorista Particular; para Mototáxi use perfil Mototaxista.');
            db()->prepare('INSERT INTO fleet_drivers(company_id,user_id,vehicle_label,status,created_at,updated_at) VALUES(?,?,?,"active",?,?) ON CONFLICT(company_id,user_id) DO UPDATE SET vehicle_label=excluded.vehicle_label,status="active",updated_at=excluded.updated_at')
                ->execute([$companyId,$driver['id'],request('vehicle_label'),now(),now()]);
            create_notification((int)$driver['id'],'Vínculo com frota','Você foi adicionado à frota de uma empresa.','success');
            back_to('fleet','Motorista adicionado à frota.');
            break;

        case 'create_fleet_driver':
            must_role(['company','employee']);
            $companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];if($companyId<1)throw new RuntimeException('Selecione a empresa da frota.');
            $fleetCompany=transport_company_info($companyId);
            if(!$fleetCompany||($fleetCompany['status']??'')!=='active'||!in_array($fleetCompany['transport_kind']??null,['taxi','mototaxi'],true))throw new RuntimeException('Empresa incompatível com frota.');
            $role=request('role');$allowedRoles=$fleetCompany['type']==='mototaxi'?['mototaxi']:['taxi','private_driver'];if(!in_array($role,$allowedRoles,true))$role=$allowedRoles[0];
            $name=trim(request('name'));$email=strtolower(trim(request('email')));if($name===''||!filter_var($email,FILTER_VALIDATE_EMAIL))throw new RuntimeException('Nome e e-mail válidos são obrigatórios.');
            $exists=db()->prepare('SELECT id FROM users WHERE lower(email)=lower(?)');$exists->execute([$email]);if($exists->fetchColumn())throw new RuntimeException('Já existe usuário com este e-mail.');
            db()->prepare('INSERT INTO users(company_id,role,name,email,phone,password_hash,status,work_status,subscription_expires_at,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)')
                ->execute([$companyId,$role,$name,$email,only_digits(request('phone')),password_hash(request('password')?:'123456',PASSWORD_DEFAULT),'active','available',date('Y-m-d',strtotime('+30 days')),now(),now()]);
            $driverId=(int)db()->lastInsertId();ensure_public_user_id($driverId);
            db()->prepare('INSERT INTO fleet_drivers(company_id,user_id,vehicle_label,status,created_at,updated_at) VALUES(?,?,?,"active",?,?)')
                ->execute([$companyId,$driverId,request('vehicle_label'),now(),now()]);
            back_to('fleet','Motorista cadastrado e adicionado à frota. ID: '.public_user_id($driverId));
            break;

        case 'remove_fleet_driver':
            must_role(['company','employee']);
            $companyId=is_admin()?(int)request('company_id'):(int)$user['company_id'];
            db()->prepare('DELETE FROM fleet_drivers WHERE id=? AND company_id=?')->execute([(int)request('id'),$companyId]);
            back_to('fleet','Motorista removido da frota.');
            break;
        case 'save_quick_actions':
            $mode=(string)request('mode',is_admin()?'admin':'self');
            $items=request('quick_actions',[]);if(!is_array($items))$items=[];
            $catalog=navigation_catalog();$clean=[];

            if($mode==='self'){
                $allowed=navigation_items((string)$user['role']);
                if(request('reset')){
                    save_setting('quick_actions_user_'.(int)$user['id'],'');
                    log_activity('save_quick_actions','settings',(int)$user['id'],'Ações rápidas pessoais restauradas.');
                    flash('success','Ações rápidas restauradas para o padrão do perfil.');
                    redirect('dashboard.php?page=quick_actions&self=1');
                }
                foreach($items as $key){
                    $key=preg_replace('/[^a-z_]/','',(string)$key);
                    if($key!==''&&$key!=='home'&&$key!=='quick_actions'&&isset($catalog[$key])&&isset($allowed[$key])&&!in_array($key,$clean,true))$clean[]=$key;
                    if(count($clean)>=5)break;
                }
                save_setting('quick_actions_user_'.(int)$user['id'],json_encode($clean,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES));
                log_activity('save_quick_actions','settings',(int)$user['id'],'Ações rápidas pessoais: '.implode(', ',$clean));
                flash('success','Suas ações rápidas foram atualizadas.');
                redirect('dashboard.php?page=quick_actions&self=1');
            }

            must_admin();
            $scope=preg_replace('/[^a-z_]/','',(string)request('role'));
            $scopes=['super_admin','sub_admin','company','clinic','employee','client','courier','motofreight','mototaxi','taxi','private_driver','service_provider','clinic_professional','property_owner','rental_company','moving_provider','shuttle_driver'];
            if(!in_array($scope,$scopes,true))throw new RuntimeException('Perfil inválido para ações rápidas.');
            if(is_role('sub_admin')&&in_array($scope,['super_admin','sub_admin'],true))throw new RuntimeException('Administrador não pode alterar atalhos de administradores.');
            foreach($items as $key){$key=preg_replace('/[^a-z_]/','',(string)$key);if($key!==''&&$key!=='home'&&$key!=='quick_actions'&&isset($catalog[$key])&&!in_array($key,$clean,true))$clean[]=$key;if(count($clean)>=5)break;}
            save_setting('quick_actions_'.$scope,json_encode($clean,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES));
            $scopeLabel=$scope==='clinic'?'Clínicas':role_label($scope);
            log_activity('save_quick_actions','settings',null,$scopeLabel.': '.implode(', ',$clean));
            flash('success','Ações rápidas de '.$scopeLabel.' atualizadas.');
            redirect('dashboard.php?page=quick_actions&role='.$scope);
            break;

        case 'clear_login_history':
            must_admin();
            if(is_role('sub_admin')){
                $count=(int)db()->query("SELECT COUNT(*) FROM activity_logs l LEFT JOIN users u ON u.id=l.user_id WHERE l.action='login' AND (u.role IS NULL OR u.role NOT IN ('super_admin','sub_admin'))")->fetchColumn();
                db()->exec("DELETE FROM activity_logs WHERE action='login' AND user_id NOT IN (SELECT id FROM users WHERE role IN ('super_admin','sub_admin'))");
            }else{
                $count=(int)db()->query("SELECT COUNT(*) FROM activity_logs WHERE action='login'")->fetchColumn();
                db()->exec("DELETE FROM activity_logs WHERE action='login'");
            }
            log_activity('clear_login_history','activity_logs',null,$count.' registro(s) de login removido(s)');
            back_to('login_history',$count.' registro(s) de login apagado(s).');
            break;

        case 'save_clinic_settings':
            $companyId=(int)request('company_id',0);if(!is_admin())$companyId=resolve_company_id_for_user($user);must_manage_clinic($companyId);
            $old=clinic_settings($companyId);$opening=trim((string)request('opening_hours',$old['opening_hours']));
            $allowSelf=(int)request('allow_self_booking',0)===1?1:0;$allowRes=(int)request('allow_reschedule',0)===1?1:0;$online=(int)request('online_payment_enabled',0)===1?1:0;
            $method=(string)request('payment_method','pix');if(!in_array($method,['pix','link','presential'],true))$method='pix';
            $pix=trim((string)request('pix_key'));$link=trim((string)request('payment_link'));if($link!==''&&!filter_var($link,FILTER_VALIDATE_URL))throw new RuntimeException('Informe um link de pagamento válido.');
            $profLimit=is_admin()?max(0,(int)request('professional_limit',0)):(int)$old['professional_limit'];
            $patientLimit=is_admin()?max(0,(int)request('patient_limit',0)):(int)$old['patient_limit'];
            $attendantLimit=is_admin()?max(0,(int)request('attendant_limit',0)):(int)$old['attendant_limit'];
            $dailyConsult=max(0,(int)request('daily_consultation_limit',$old['daily_consultation_limit']??0));
            $dailyExam=max(0,(int)request('daily_exam_limit',$old['daily_exam_limit']??0));
            $checkinRadius=max(50,min(5000,(int)request('checkin_radius_meters',$old['checkin_radius_meters']??250)));
            db()->prepare("INSERT INTO clinic_settings(company_id,opening_hours,allow_self_booking,allow_reschedule,online_payment_enabled,payment_method,pix_key,payment_link,professional_limit,patient_limit,attendant_limit,daily_consultation_limit,daily_exam_limit,checkin_radius_meters,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(company_id) DO UPDATE SET opening_hours=excluded.opening_hours,allow_self_booking=excluded.allow_self_booking,allow_reschedule=excluded.allow_reschedule,online_payment_enabled=excluded.online_payment_enabled,payment_method=excluded.payment_method,pix_key=excluded.pix_key,payment_link=excluded.payment_link,professional_limit=excluded.professional_limit,patient_limit=excluded.patient_limit,attendant_limit=excluded.attendant_limit,daily_consultation_limit=excluded.daily_consultation_limit,daily_exam_limit=excluded.daily_exam_limit,checkin_radius_meters=excluded.checkin_radius_meters,updated_at=excluded.updated_at")
              ->execute([$companyId,$opening,$allowSelf,$allowRes,$online,$method,$pix,$link,$profLimit,$patientLimit,$attendantLimit,$dailyConsult,$dailyExam,$checkinRadius,now(),now()]);
            log_activity('save_clinic_settings','companies',$companyId,'Configurações da clínica');back_to('clinic','Configurações da clínica salvas.');break;

        case 'save_clinic_specialty':
            $companyId=(int)request('company_id',0);if(!is_admin())$companyId=resolve_company_id_for_user($user);must_manage_clinic($companyId);
            $id=(int)request('id',0);$name=trim((string)request('name'));if($name==='')throw new RuntimeException('Informe a especialidade/procedimento.');
            $description=trim((string)request('description'));$duration=max(15,min(480,(int)request('duration_minutes',30)));$price=max(0,localized_decimal(request('price',0)));$status=in_array(request('status'),['active','inactive'],true)?request('status'):'active';
            $serviceKind=in_array(request('service_kind','consultation'),['consultation','exam','procedure'],true)?request('service_kind'):'consultation';$prep=trim((string)request('preparation_instructions'));$days=trim((string)request('available_days'));$hours=trim((string)request('available_hours'));$allowPatient=(int)request('allow_patient_booking',0)===1?1:0;
            if($id){db()->prepare('UPDATE clinic_specialties SET name=?,description=?,duration_minutes=?,price=?,status=?,service_kind=?,preparation_instructions=?,available_days=?,available_hours=?,allow_patient_booking=?,updated_at=? WHERE id=? AND company_id=?')->execute([$name,$description,$duration,$price,$status,$serviceKind,$prep,$days,$hours,$allowPatient,now(),$id,$companyId]);}
            else{db()->prepare('INSERT INTO clinic_specialties(company_id,name,description,duration_minutes,price,status,service_kind,preparation_instructions,available_days,available_hours,allow_patient_booking,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$name,$description,$duration,$price,$status,$serviceKind,$prep,$days,$hours,$allowPatient,now(),now()]);$id=(int)db()->lastInsertId();}
            if($serviceKind==='exam'){$examMode=request('exam_booking_mode','time')==='arrival'?'arrival':'time';$examDays=request('exam_weekdays',[]);if(!is_array($examDays))$examDays=[];$examDays=array_values(array_unique(array_filter(array_map('intval',$examDays),fn($d)=>$d>=0&&$d<=6)));sort($examDays);$examStart=substr(trim((string)request('exam_start_time')),0,5);$examEnd=substr(trim((string)request('exam_end_time')),0,5);if($examDays&&($examStart===''||$examEnd===''||car_wash_minutes($examStart)===null||car_wash_minutes($examEnd)===null||car_wash_minutes($examEnd)<=car_wash_minutes($examStart)))throw new RuntimeException('Informe um horário inicial e final válido para o exame.');if($examDays){$daysText=implode(', ',array_map('clinic_weekday_label',$examDays));$hoursText=$examStart.'–'.$examEnd;db()->prepare('UPDATE clinic_specialties SET exam_booking_mode=?,exam_weekdays=?,exam_start_time=?,exam_end_time=?,available_days=?,available_hours=?,updated_at=? WHERE id=? AND company_id=?')->execute([$examMode,implode(',',$examDays),$examStart,$examEnd,$daysText,$hoursText,now(),$id,$companyId]);}}
            log_activity('save_clinic_specialty','clinic_specialties',$id,$name);back_to('clinic','Especialidade/procedimento salvo.');break;

        case 'save_clinic_exam_schedule':
            $id=(int)request('id');$q=db()->prepare("SELECT * FROM clinic_specialties WHERE id=? AND service_kind='exam'");$q->execute([$id]);$sp=$q->fetch();if(!$sp)throw new RuntimeException('Exame não encontrado.');$companyId=(int)$sp['company_id'];must_manage_clinic($companyId);
            $mode=request('exam_booking_mode','time')==='arrival'?'arrival':'time';$weekdays=request('exam_weekdays',[]);if(!is_array($weekdays))$weekdays=[];$weekdays=array_values(array_unique(array_filter(array_map('intval',$weekdays),fn($d)=>$d>=0&&$d<=6)));sort($weekdays);if(!$weekdays)throw new RuntimeException('Selecione pelo menos um dia de realização do exame.');
            $start=substr(trim((string)request('exam_start_time')),0,5);$end=substr(trim((string)request('exam_end_time')),0,5);if(car_wash_minutes($start)===null||car_wash_minutes($end)===null||car_wash_minutes($end)<=car_wash_minutes($start))throw new RuntimeException('Informe o horário de início e término dos exames.');
            $daysText=implode(', ',array_map('clinic_weekday_label',$weekdays));$hoursText=$start.'–'.$end;db()->prepare('UPDATE clinic_specialties SET exam_booking_mode=?,exam_weekdays=?,exam_start_time=?,exam_end_time=?,available_days=?,available_hours=?,updated_at=? WHERE id=?')->execute([$mode,implode(',',$weekdays),$start,$end,$daysText,$hoursText,now(),$id]);
            back_to('clinic',$mode==='arrival'?'Agenda do exame salva como ordem de chegada.':'Agenda e horários do exame atualizados.');break;

        case 'delete_clinic_specialty':
            $id=(int)request('id');$q=db()->prepare('SELECT company_id FROM clinic_specialties WHERE id=?');$q->execute([$id]);$companyId=(int)$q->fetchColumn();if($companyId<1)throw new RuntimeException('Especialidade não encontrada.');must_manage_clinic($companyId);
            db()->prepare('UPDATE clinic_professionals SET specialty_id=NULL WHERE specialty_id=?')->execute([$id]);db()->prepare('DELETE FROM clinic_specialties WHERE id=?')->execute([$id]);back_to('clinic','Especialidade apagada.');break;

        case 'save_clinic_professional':
            $companyId=(int)request('company_id',0);if(!is_admin())$companyId=resolve_company_id_for_user($user);must_manage_clinic($companyId);
            $id=(int)request('id',0);$platformToken=trim((string)request('platform_user_id'));$linkedUser=null;
            if($platformToken!==''){$linkedUser=clinic_platform_user_by_token($platformToken);if(!$linkedUser)throw new RuntimeException('Usuário não encontrado pelo ID informado.');if(in_array((string)$linkedUser['role'],['super_admin','sub_admin'],true))throw new RuntimeException('Conta administrativa não pode ser vinculada como profissional.');}
            $name=trim((string)request('name'));$email=strtolower(trim((string)request('email')));$phone=only_digits(request('phone'));$document=trim((string)request('document'));
            if($linkedUser){$name=(string)$linkedUser['name'];$email=(string)$linkedUser['email'];$phone=(string)$linkedUser['phone'];$document=(string)$linkedUser['document'];}if($name==='')throw new RuntimeException('Informe o nome do profissional.');
            $specialtyId=(int)request('specialty_id',0);if($specialtyId){$q=db()->prepare('SELECT COUNT(*) FROM clinic_specialties WHERE id=? AND company_id=?');$q->execute([$specialtyId,$companyId]);if(!(int)$q->fetchColumn())throw new RuntimeException('Especialidade inválida.');}
            $existing=null;if($id){$q=db()->prepare('SELECT * FROM clinic_professionals WHERE id=? AND company_id=?');$q->execute([$id,$companyId]);$existing=$q->fetch();if(!$existing)throw new RuntimeException('Profissional não encontrado.');}
            if(!$id)assert_clinic_capacity($companyId,'professionals');
            if(!$linkedUser&&!$id){$linkedUser=clinic_create_platform_user('clinic_professional',$name,$email,$phone,$document,(string)request('password','123456'),$companyId);}elseif(!$linkedUser&&$existing&&!empty($existing['user_id'])){$q=db()->prepare('SELECT * FROM users WHERE id=?');$q->execute([(int)$existing['user_id']]);$linkedUser=$q->fetch()?:null;}
            $userId=$linkedUser?(int)$linkedUser['id']:(int)($existing['user_id']??0);if($userId>0){$dup=db()->prepare('SELECT id FROM clinic_professionals WHERE company_id=? AND user_id=? AND id<>?');$dup->execute([$companyId,$userId,$id]);if($dup->fetchColumn())throw new RuntimeException('Este usuário já está cadastrado como profissional desta clínica.');}
            $share=(int)request('share_whatsapp',0)===1?1:0;$professionalType=trim((string)request('professional_type'));$registry=trim((string)request('registry'));
            if($id){db()->prepare('UPDATE clinic_professionals SET user_id=?,specialty_id=?,name=?,professional_type=?,registry=?,document=?,phone=?,email=?,share_whatsapp=?,updated_at=? WHERE id=? AND company_id=?')->execute([$userId?:null,$specialtyId?:null,$name,$professionalType,$registry,$document,$phone,$email,$share,now(),$id,$companyId]);$msg='Dados do profissional atualizados.';}
            else{$linkState=$userId>0?'pending_professional':'active';$status=$userId>0?'inactive':'active';db()->prepare('INSERT INTO clinic_professionals(company_id,user_id,specialty_id,name,professional_type,registry,document,phone,email,share_whatsapp,status,link_state,requested_by_user_id,admin_approval_status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$userId?:null,$specialtyId?:null,$name,$professionalType,$registry,$document,$phone,$email,$share,$status,$linkState,(int)$user['id'],'not_required',now(),now()]);$id=(int)db()->lastInsertId();$msg=$userId>0?'Solicitação enviada. O profissional precisa aceitar o vínculo.':'Profissional salvo.';}
            $dailyConsult=max(0,(int)request('daily_consultation_limit',$existing['daily_consultation_limit']??0));$dailyExam=max(0,(int)request('daily_exam_limit',$existing['daily_exam_limit']??0));
            db()->prepare('UPDATE clinic_professionals SET daily_consultation_limit=?,daily_exam_limit=?,updated_at=? WHERE id=? AND company_id=?')->execute([$dailyConsult,$dailyExam,now(),$id,$companyId]);
            if($userId>0){db()->prepare("UPDATE users SET segment_slug=CASE WHEN COALESCE(segment_slug,'')='' THEN 'clinic' ELSE segment_slug END,updated_at=? WHERE id=?")->execute([now(),$userId]);if(!$existing)create_notification($userId,'Solicitação de vínculo profissional','A clínica solicitou vincular seu perfil profissional. Abra “Painel do Profissional” para aceitar ou recusar.','warning','dashboard.php?page=clinic_professional','clinic_professional',$id);}
            log_activity('save_clinic_professional','clinic_professionals',$id,$name);back_to('clinic',$msg.' Conta: '.($userId?public_user_id($userId):'sem vínculo'));break;

        case 'delete_clinic_professional':
            $id=(int)request('id');$q=db()->prepare('SELECT company_id,user_id FROM clinic_professionals WHERE id=?');$q->execute([$id]);$row=$q->fetch();if(!$row)throw new RuntimeException('Profissional não encontrado.');must_manage_clinic((int)$row['company_id']);
            $has=db()->prepare("SELECT COUNT(*) FROM clinic_appointments WHERE professional_id=? AND status NOT IN ('cancelled','completed')");$has->execute([$id]);if((int)$has->fetchColumn()>0)throw new RuntimeException('Este profissional possui consultas em aberto. Cancele ou conclua antes de remover.');
            db()->prepare('DELETE FROM clinic_professional_availability WHERE professional_id=?')->execute([$id]);db()->prepare('DELETE FROM clinic_professional_blocks WHERE professional_id=?')->execute([$id]);db()->prepare('DELETE FROM clinic_professionals WHERE id=?')->execute([$id]);back_to('clinic','Profissional removido da clínica. A conta da plataforma foi preservada.');break;

        case 'save_clinic_professional_limits':
            $id=(int)request('id');$q=db()->prepare('SELECT company_id FROM clinic_professionals WHERE id=?');$q->execute([$id]);$companyId=(int)$q->fetchColumn();if($companyId<1)throw new RuntimeException('Profissional não encontrado.');must_manage_clinic($companyId);$consult=max(0,(int)request('daily_consultation_limit',0));$exam=max(0,(int)request('daily_exam_limit',0));db()->prepare('UPDATE clinic_professionals SET daily_consultation_limit=?,daily_exam_limit=?,updated_at=? WHERE id=?')->execute([$consult,$exam,now(),$id]);back_to('clinic','Limites diários do profissional atualizados.');break;

        case 'save_clinic_professional_availability':
            $professionalId=(int)request('professional_id',0);$q=db()->prepare('SELECT company_id,user_id FROM clinic_professionals WHERE id=?');$q->execute([$professionalId]);$pr=$q->fetch();if(!$pr)throw new RuntimeException('Profissional não encontrado.');
            $ownProfessional=(int)($pr['user_id']??0)===(int)$user['id'];if(!$ownProfessional)must_manage_clinic((int)$pr['company_id']);
            $weekday=max(0,min(6,(int)request('weekday')));$start=substr(trim((string)request('start_time')),0,5);$end=substr(trim((string)request('end_time')),0,5);if(car_wash_minutes($start)===null||car_wash_minutes($end)===null||car_wash_minutes($end)<=car_wash_minutes($start))throw new RuntimeException('Informe um horário inicial e final válidos.');
            db()->prepare("INSERT INTO clinic_professional_availability(professional_id,weekday,start_time,end_time,status,created_at,updated_at) VALUES(?,?,?,?,'active',?,?) ON CONFLICT(professional_id,weekday) DO UPDATE SET start_time=excluded.start_time,end_time=excluded.end_time,status='active',updated_at=excluded.updated_at")->execute([$professionalId,$weekday,$start,$end,now(),now()]);
            back_to($ownProfessional?'clinic_professional':'clinic','Disponibilidade salva.');break;

        case 'delete_clinic_professional_availability':
            $id=(int)request('id');$q=db()->prepare('SELECT a.*,p.company_id,p.user_id FROM clinic_professional_availability a JOIN clinic_professionals p ON p.id=a.professional_id WHERE a.id=?');$q->execute([$id]);$av=$q->fetch();if(!$av)throw new RuntimeException('Disponibilidade não encontrada.');$own=(int)$av['user_id']===(int)$user['id'];if(!$own)must_manage_clinic((int)$av['company_id']);db()->prepare('DELETE FROM clinic_professional_availability WHERE id=?')->execute([$id]);back_to($own?'clinic_professional':'clinic','Disponibilidade removida.');break;

        case 'save_clinic_professional_block':
            $professionalId=(int)request('professional_id');$q=db()->prepare('SELECT company_id,user_id FROM clinic_professionals WHERE id=?');$q->execute([$professionalId]);$pr=$q->fetch();if(!$pr)throw new RuntimeException('Profissional não encontrado.');$own=(int)$pr['user_id']===(int)$user['id'];if(!$own)must_manage_clinic((int)$pr['company_id']);
            $date=trim((string)request('block_date'));if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date))throw new RuntimeException('Data inválida.');$start=substr(trim((string)request('start_time')),0,5);$end=substr(trim((string)request('end_time')),0,5);if(($start!==''||$end!=='')&&(car_wash_minutes($start)===null||car_wash_minutes($end)===null||car_wash_minutes($end)<=car_wash_minutes($start)))throw new RuntimeException('Informe início e fim válidos ou deixe ambos vazios para bloquear o dia todo.');
            db()->prepare('INSERT INTO clinic_professional_blocks(professional_id,block_date,start_time,end_time,reason,created_at) VALUES(?,?,?,?,?,?)')->execute([$professionalId,$date,$start?:null,$end?:null,trim((string)request('reason')),now()]);back_to($own?'clinic_professional':'clinic','Bloqueio de agenda salvo.');break;

        case 'delete_clinic_professional_block':
            $id=(int)request('id');$q=db()->prepare('SELECT b.*,p.company_id,p.user_id FROM clinic_professional_blocks b JOIN clinic_professionals p ON p.id=b.professional_id WHERE b.id=?');$q->execute([$id]);$b=$q->fetch();if(!$b)throw new RuntimeException('Bloqueio não encontrado.');$own=(int)$b['user_id']===(int)$user['id'];if(!$own)must_manage_clinic((int)$b['company_id']);db()->prepare('DELETE FROM clinic_professional_blocks WHERE id=?')->execute([$id]);back_to($own?'clinic_professional':'clinic','Bloqueio removido.');break;

        case 'toggle_clinic_professional_whatsapp':
            $professionalId=(int)request('professional_id',0);$q=db()->prepare("SELECT * FROM clinic_professionals WHERE id=? AND user_id=? AND status='active' AND COALESCE(link_state,'active')='active'");$q->execute([$professionalId,(int)$user['id']]);$pr=$q->fetch();if(!$pr)throw new RuntimeException('Perfil profissional não encontrado.');$new=(int)$pr['share_whatsapp']?0:1;db()->prepare('UPDATE clinic_professionals SET share_whatsapp=?,updated_at=? WHERE id=?')->execute([$new,now(),$professionalId]);back_to('clinic_professional',$new?'WhatsApp liberado para pacientes.':'WhatsApp ocultado dos pacientes.');break;

        case 'save_clinic_patient':
            $companyId=(int)request('company_id',0);if(!is_admin())$companyId=resolve_company_id_for_user($user);must_manage_clinic($companyId);
            $id=(int)request('id',0);$platformToken=trim((string)request('platform_user_id'));$linkedUser=null;
            if($platformToken!==''){$linkedUser=clinic_platform_user_by_token($platformToken);if(!$linkedUser)throw new RuntimeException('Paciente não encontrado pelo ID da plataforma.');if(in_array((string)$linkedUser['role'],['super_admin','sub_admin'],true))throw new RuntimeException('Conta administrativa não pode ser cadastrada como paciente.');}
            $name=trim((string)request('name'));$document=trim((string)request('document'));$phone=only_digits(request('phone'));$email=strtolower(trim((string)request('email')));
            if($linkedUser){$name=(string)$linkedUser['name'];$document=(string)$linkedUser['document'];$phone=(string)$linkedUser['phone'];$email=(string)$linkedUser['email'];}
            if($name==='')throw new RuntimeException('Informe o nome do paciente.');$existing=null;if($id){$q=db()->prepare('SELECT * FROM clinic_patients WHERE id=? AND company_id=?');$q->execute([$id,$companyId]);$existing=$q->fetch();if(!$existing)throw new RuntimeException('Paciente não encontrado.');}
            if(!$id)assert_clinic_capacity($companyId,'patients');
            if(!$linkedUser&&!$id){$linkedUser=clinic_create_platform_user('client',$name,$email,$phone,$document,(string)request('password','123456'),0);}
            elseif(!$linkedUser&&$existing&&!empty($existing['platform_user_id'])){$q=db()->prepare('SELECT * FROM users WHERE id=?');$q->execute([(int)$existing['platform_user_id']]);$linkedUser=$q->fetch()?:null;}
            $platformUserId=$linkedUser?(int)$linkedUser['id']:(int)($existing['platform_user_id']??0);if($platformUserId>0){$dup=db()->prepare('SELECT id FROM clinic_patients WHERE company_id=? AND platform_user_id=? AND id<>?');$dup->execute([$companyId,$platformUserId,$id]);if($dup->fetchColumn())throw new RuntimeException('Este usuário já é paciente desta clínica.');}
            $data=[$platformUserId?:null,$name,$document,$phone,$email,trim((string)request('birth_date')),trim((string)request('notes')),in_array(request('status'),['active','inactive'],true)?request('status'):'active'];
            if($id){db()->prepare('UPDATE clinic_patients SET platform_user_id=?,name=?,document=?,phone=?,email=?,birth_date=?,notes=?,status=?,updated_at=? WHERE id=? AND company_id=?')->execute(array_merge($data,[now(),$id,$companyId]));}
            else{db()->prepare('INSERT INTO clinic_patients(company_id,platform_user_id,name,document,phone,email,birth_date,notes,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)')->execute(array_merge([$companyId],$data,[now(),now()]));$id=(int)db()->lastInsertId();}
            if($platformUserId>0)create_notification($platformUserId,'Cadastro de paciente','Você foi cadastrado como paciente da clínica. Abra “Minha Saúde” para consultar seus agendamentos.','success','dashboard.php?page=clinic_patient','clinic_patient',$id);
            log_activity('save_clinic_patient','clinic_patients',$id,$name);back_to('clinic','Paciente salvo. Conta: '.($platformUserId?public_user_id($platformUserId):'sem vínculo'));break;

        case 'clinic_patient_join':
            if(is_admin())throw new RuntimeException('Use o acesso a clínica para cadastrar pacientes.');$companyId=(int)request('company_id');if($companyId<1||!company_is_clinic($companyId))throw new RuntimeException('Clínica inválida.');
            $dup=db()->prepare('SELECT id FROM clinic_patients WHERE company_id=? AND platform_user_id=?');$dup->execute([$companyId,$user['id']]);if($dup->fetchColumn())back_to('clinic_patient','Você já está cadastrado nesta clínica.');assert_clinic_capacity($companyId,'patients');
            db()->prepare('INSERT INTO clinic_patients(company_id,platform_user_id,name,document,phone,email,status,created_at,updated_at) VALUES(?,?,?,?,?,?,"active",?,?)')->execute([$companyId,$user['id'],$user['name'],$user['document'],$user['phone'],$user['email'],now(),now()]);
            $id=(int)db()->lastInsertId();$ownerQ=db()->prepare('SELECT owner_user_id,name FROM companies WHERE id=?');$ownerQ->execute([$companyId]);$c=$ownerQ->fetch();if(!empty($c['owner_user_id']))create_notification((int)$c['owner_user_id'],'Novo paciente',$user['name'].' entrou como paciente da clínica.','info','dashboard.php?page=clinic','clinic_patient',$id);back_to('clinic_patient','Cadastro de paciente concluído.');break;

        case 'delete_clinic_patient':
            $id=(int)request('id');$q=db()->prepare('SELECT company_id FROM clinic_patients WHERE id=?');$q->execute([$id]);$companyId=(int)$q->fetchColumn();if($companyId<1)throw new RuntimeException('Paciente não encontrado.');must_manage_clinic($companyId);$has=db()->prepare("SELECT COUNT(*) FROM clinic_appointments WHERE patient_id=? AND status NOT IN ('cancelled','completed')");$has->execute([$id]);if((int)$has->fetchColumn()>0)throw new RuntimeException('Este paciente possui agendamento em aberto.');db()->prepare('DELETE FROM clinic_patients WHERE id=?')->execute([$id]);back_to('clinic','Paciente removido da clínica. A conta da plataforma foi preservada.');break;

        case 'save_clinic_appointment':
            $companyId=(int)request('company_id',0);if(!is_admin())$companyId=resolve_company_id_for_user($user);must_manage_clinic($companyId);
            $patientId=(int)request('patient_id');$professionalId=(int)request('professional_id');$specialtyId=(int)request('specialty_id');$bookingMode=request('booking_mode','time')==='arrival'?'arrival':'time';$date=trim((string)request('appointment_date'));$time=substr(trim((string)request('appointment_time')),0,5);if($bookingMode==='arrival'&&$time==='')$time='00:00';if($patientId<1||!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)||($bookingMode==='time'&&car_wash_minutes($time)===null))throw new RuntimeException('Paciente, data e horário são obrigatórios.');if($bookingMode==='arrival')$time='00:00';
            $q=db()->prepare('SELECT platform_user_id FROM clinic_patients WHERE id=? AND company_id=?');$q->execute([$patientId,$companyId]);$patientRow=$q->fetch();if(!$patientRow)throw new RuntimeException('Paciente inválido.');$patientUserId=(int)($patientRow['platform_user_id']??0);
            $conflicts=$bookingMode==='time'?clinic_patient_external_conflicts($patientUserId,$date,$time,$companyId):[];if($conflicts && !(int)request('confirm_conflict',0)){throw new RuntimeException('ATENÇÃO: este paciente já possui agendamento em outra instituição próximo deste horário. Marque “Confirmar mesmo com conflito” se deseja continuar.');}
            $duration=30;$defaultPrice=0;if($specialtyId){$q=db()->prepare('SELECT price,duration_minutes,service_kind FROM clinic_specialties WHERE id=? AND company_id=?');$q->execute([$specialtyId,$companyId]);$sp=$q->fetch();if(!$sp)throw new RuntimeException('Especialidade inválida.');if(($sp['service_kind']??'consultation')==='exam')throw new RuntimeException('Exames devem ser agendados na área de Exames, separada das consultas.');$defaultPrice=(float)$sp['price'];$duration=(int)$sp['duration_minutes'];}
            if($professionalId){$q=db()->prepare("SELECT user_id FROM clinic_professionals WHERE id=? AND company_id=? AND status='active' AND COALESCE(link_state,'active')='active'");$q->execute([$professionalId,$companyId]);$professionalRow=$q->fetch();if(!$professionalRow)throw new RuntimeException('Profissional inválido.');$professionalUserId=(int)($professionalRow['user_id']??0);clinic_assert_daily_capacity($professionalId,$date,'consultation');if($bookingMode==='time'&&!clinic_slot_is_available($professionalId,$date,$time,$duration))throw new RuntimeException('Este horário não está disponível para o profissional.');}else{$professionalUserId=0;}
            $amount=localized_decimal(request('amount',$defaultPrice));$method=trim((string)request('payment_method',''));$notes=trim((string)request('notes'));$first=clinic_first_visit($companyId,$patientId,$professionalId)?1:0;$visitType=$first?'first':'return';
            db()->prepare('INSERT INTO clinic_appointments(company_id,patient_id,professional_id,specialty_id,appointment_date,appointment_time,amount,payment_method,payment_status,status,notes,requested_by_user_id,scheduled_by_user_id,visit_type,first_visit,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$patientId,$professionalId?:null,$specialtyId?:null,$date,$time,$amount,$method,'pending','scheduled',$notes,$patientUserId?:null,(int)$user['id'],$visitType,$first,now(),now()]);$id=(int)db()->lastInsertId();$arrivalOrder=$bookingMode==='arrival'&&$professionalId?clinic_next_arrival_order($professionalId,$date):null;db()->prepare('UPDATE clinic_appointments SET booking_mode=?,arrival_order=? WHERE id=?')->execute([$bookingMode,$arrivalOrder,$id]);
            if($professionalUserId)clinic_sync_professional_contact($professionalUserId,$companyId,$patientId);if($patientUserId)create_notification($patientUserId,'Consulta agendada','Sua consulta foi marcada para '.date('d/m/Y',strtotime($date)).' às '.$time.'.','success','dashboard.php?page=clinic_patient','clinic_appointment',$id);if($professionalUserId)create_notification($professionalUserId,'Novo atendimento','Uma consulta foi marcada para '.date('d/m/Y',strtotime($date)).' às '.$time.'.','info','dashboard.php?page=clinic_professional&clinic_company='.$companyId,'clinic_appointment',$id);
            log_activity('save_clinic_appointment','clinic_appointments',$id,$date.' '.$time);back_to('clinic','Consulta/agendamento cadastrado.');break;

        case 'clinic_patient_book':
            if(is_admin())throw new RuntimeException('Agendamento do paciente deve ser feito por uma conta de usuário.');$companyId=(int)request('company_id');$patient=clinic_patient_for_user((int)$user['id'],$companyId);if(!$patient)throw new RuntimeException('Cadastre-se como paciente desta clínica primeiro.');$settings=clinic_settings($companyId);if(!(int)$settings['allow_self_booking'])throw new RuntimeException('Esta clínica não liberou agendamento direto pelo paciente.');
            $specialtyId=(int)request('specialty_id');$bookingMode=request('booking_mode','time')==='arrival'?'arrival':'time';$professionalId=(int)request('professional_id');$date=trim((string)request('appointment_date'));$time=substr(trim((string)request('appointment_time')),0,5);$slotChoice=trim((string)request('slot_choice'));if($bookingMode==='time'&&$slotChoice!==''&&substr_count($slotChoice,'|')>=2){[$professionalId,$date,$time]=explode('|',$slotChoice,3);$professionalId=(int)$professionalId;$time=substr($time,0,5);}if($bookingMode==='arrival')$time='00:00';if($professionalId<1||!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)||($bookingMode==='time'&&car_wash_minutes($time)===null))throw new RuntimeException('Escolha o profissional e a data; para horário marcado, selecione também um horário livre.');
            $q=db()->prepare('SELECT price,duration_minutes,service_kind FROM clinic_specialties WHERE id=? AND company_id=? AND status="active"');$q->execute([$specialtyId,$companyId]);$sp=$q->fetch();if(!$sp)throw new RuntimeException('Especialidade inválida.');if(($sp['service_kind']??'consultation')==='exam')throw new RuntimeException('Exames são agendados exclusivamente na área de Exames.');$q=db()->prepare("SELECT user_id FROM clinic_professionals WHERE id=? AND company_id=? AND status='active' AND COALESCE(link_state,'active')='active' AND (specialty_id=? OR specialty_id IS NULL)");$q->execute([$professionalId,$companyId,$specialtyId]);$professionalRow=$q->fetch();if(!$professionalRow)throw new RuntimeException('Profissional inválido.');$professionalUserId=(int)($professionalRow['user_id']??0);clinic_assert_daily_capacity($professionalId,$date,'consultation');if($bookingMode==='time'&&!clinic_slot_is_available($professionalId,$date,$time,(int)$sp['duration_minutes']))throw new RuntimeException('Horário indisponível. Atualize a página e escolha outro.');
            $conflicts=$bookingMode==='time'?clinic_patient_external_conflicts((int)$user['id'],$date,$time,$companyId):[];if($conflicts)throw new RuntimeException('Você já possui consulta em outra instituição próxima deste horário. Escolha outro horário.');$first=clinic_first_visit($companyId,(int)$patient['id'],$professionalId)?1:0;
            db()->prepare('INSERT INTO clinic_appointments(company_id,patient_id,professional_id,specialty_id,appointment_date,appointment_time,amount,payment_method,payment_status,status,requested_by_user_id,visit_type,first_visit,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$patient['id'],$professionalId,$specialtyId,$date,$time,(float)$sp['price'],$settings['payment_method'],'pending','scheduled',$user['id'],$first?'first':'return',$first,now(),now()]);$id=(int)db()->lastInsertId();$arrivalOrder=$bookingMode==='arrival'?clinic_next_arrival_order($professionalId,$date):null;db()->prepare('UPDATE clinic_appointments SET booking_mode=?,arrival_order=? WHERE id=?')->execute([$bookingMode,$arrivalOrder,$id]);
            if($professionalUserId)clinic_sync_professional_contact($professionalUserId,$companyId,(int)$patient['id']);create_notification((int)$user['id'],'Consulta solicitada','Agendamento para '.date('d/m/Y',strtotime($date)).($bookingMode==='arrival'?' por ordem de chegada #'.(int)$arrivalOrder:' às '.$time).' registrado.','success','dashboard.php?page=clinic_patient','clinic_appointment',$id);if($professionalUserId)create_notification($professionalUserId,'Novo agendamento de paciente',$user['name'].' agendou atendimento para '.date('d/m/Y',strtotime($date)).' às '.$time.'.','info','dashboard.php?page=clinic_professional&clinic_company='.$companyId,'clinic_appointment',$id);$cq=db()->prepare('SELECT owner_user_id FROM companies WHERE id=?');$cq->execute([$companyId]);$owner=(int)$cq->fetchColumn();if($owner)create_notification($owner,'Novo agendamento',$user['name'].' marcou uma consulta.','info','dashboard.php?page=clinic','clinic_appointment',$id);back_to('clinic_patient','Consulta agendada com sucesso.');break;

        case 'clinic_patient_reschedule':
            $id=(int)request('id');$q=db()->prepare("SELECT a.*,pa.platform_user_id,s.duration_minutes FROM clinic_appointments a JOIN clinic_patients pa ON pa.id=a.patient_id LEFT JOIN clinic_specialties s ON s.id=a.specialty_id WHERE a.id=?");$q->execute([$id]);$a=$q->fetch();if(!$a||(int)$a['platform_user_id']!==(int)$user['id'])throw new RuntimeException('Consulta não encontrada.');$settings=clinic_settings((int)$a['company_id']);if(!(int)$settings['allow_reschedule'])throw new RuntimeException('A clínica não liberou reagendamento pelo paciente.');$history=db()->prepare('SELECT COUNT(*) FROM clinic_appointments WHERE patient_id=?');$history->execute([$a['patient_id']]);if((int)$history->fetchColumn()<1)throw new RuntimeException('Reagendamento ainda não liberado.');
            $date=trim((string)request('appointment_date'));$time=substr(trim((string)request('appointment_time')),0,5);$slotChoice=trim((string)request('slot_choice'));if($slotChoice!==''&&str_contains($slotChoice,'|')){[$date,$time]=explode('|',$slotChoice,2);$time=substr($time,0,5);}if(!clinic_slot_is_available((int)$a['professional_id'],$date,$time,(int)($a['duration_minutes']?:30)))throw new RuntimeException('Novo horário indisponível.');$conflicts=clinic_patient_external_conflicts((int)$user['id'],$date,$time,(int)$a['company_id']);if($conflicts)throw new RuntimeException('Você já possui consulta em outra instituição próxima deste horário. Escolha outro horário.');db()->prepare('UPDATE clinic_appointments SET appointment_date=?,appointment_time=?,rescheduled_from_id=COALESCE(rescheduled_from_id,id),reminder_24h_sent=0,reminder_3h_sent=0,updated_at=? WHERE id=?')->execute([$date,$time,now(),$id]);back_to('clinic_patient','Consulta reagendada.');break;

        case 'clinic_patient_cancel':
            $id=(int)request('id');$q=db()->prepare('SELECT a.id FROM clinic_appointments a JOIN clinic_patients p ON p.id=a.patient_id WHERE a.id=? AND p.platform_user_id=?');$q->execute([$id,$user['id']]);if(!$q->fetchColumn())throw new RuntimeException('Consulta não encontrada.');db()->prepare("UPDATE clinic_appointments SET status='cancelled',updated_at=? WHERE id=?")->execute([now(),$id]);back_to('clinic_patient','Consulta cancelada.');break;

        case 'clinic_professional_update_appointment':
            $id=(int)request('id');$status=(string)request('status','confirmed');if(!in_array($status,['confirmed','completed','no_show','cancelled'],true))throw new RuntimeException('Status inválido.');$q=db()->prepare("SELECT a.*,p.user_id FROM clinic_appointments a JOIN clinic_professionals p ON p.id=a.professional_id WHERE a.id=? AND p.user_id=? AND p.status='active' AND COALESCE(p.link_state,'active')='active'");$q->execute([$id,(int)$user['id']]);$a=$q->fetch();if(!$a)throw new RuntimeException('Atendimento não pertence a este profissional.');db()->prepare('UPDATE clinic_appointments SET status=?,professional_notes=?,updated_at=? WHERE id=?')->execute([$status,trim((string)request('professional_notes')),now(),$id]);$pq=db()->prepare('SELECT platform_user_id FROM clinic_patients WHERE id=?');$pq->execute([(int)$a['patient_id']]);$patientUid=(int)$pq->fetchColumn();if($patientUid)create_notification($patientUid,'Consulta atualizada','Situação do seu atendimento: '.status_label($status).'.','info','dashboard.php?page=clinic_patient','clinic_appointment',$id);back_to('clinic_professional','Atendimento atualizado.');break;

        case 'update_clinic_appointment':
            $id=(int)request('id');$status=request('status');if(!in_array($status,['scheduled','confirmed','completed','cancelled','no_show'],true))throw new RuntimeException('Status inválido.');$q=db()->prepare('SELECT company_id,patient_id,professional_id,appointment_date,appointment_time FROM clinic_appointments WHERE id=?');$q->execute([$id]);$a=$q->fetch();if(!$a)throw new RuntimeException('Agendamento não encontrado.');must_manage_clinic((int)$a['company_id']);db()->prepare('UPDATE clinic_appointments SET status=?,updated_at=? WHERE id=?')->execute([$status,now(),$id]);$pq=db()->prepare('SELECT platform_user_id FROM clinic_patients WHERE id=?');$pq->execute([$a['patient_id']]);$patientUid=(int)$pq->fetchColumn();if($patientUid)create_notification($patientUid,'Consulta atualizada','Situação do agendamento de '.date('d/m/Y',strtotime($a['appointment_date'])).' às '.substr($a['appointment_time'],0,5).': '.status_label($status).'.','info','dashboard.php?page=clinic_patient','clinic_appointment',$id);back_to('clinic','Agendamento atualizado.');break;

        case 'delete_clinic_appointment':
            $id=(int)request('id');$q=db()->prepare('SELECT company_id FROM clinic_appointments WHERE id=?');$q->execute([$id]);$companyId=(int)$q->fetchColumn();if($companyId<1)throw new RuntimeException('Agendamento não encontrado.');must_manage_clinic($companyId);db()->prepare('DELETE FROM clinic_appointments WHERE id=?')->execute([$id]);back_to('clinic','Agendamento apagado.');break;

        case 'save_clinic_review':
            $id=(int)request('appointment_id');$rating=max(1,min(5,(int)request('clinic_rating',5)));$prRating=max(1,min(5,(int)request('professional_rating',$rating)));$comment=trim((string)request('comment'));
            $q=db()->prepare("SELECT a.*,pa.platform_user_id FROM clinic_appointments a JOIN clinic_patients pa ON pa.id=a.patient_id WHERE a.id=? AND a.status='completed'");$q->execute([$id]);$a=$q->fetch();if(!$a||(int)$a['platform_user_id']!==(int)$user['id'])throw new RuntimeException('Avaliação disponível somente após a consulta concluída.');
            foreach([['company',(int)$a['company_id'],$rating],['clinic_professional',(int)$a['professional_id'],$prRating]] as $target){if($target[1]<1)continue;$ex=db()->prepare('SELECT id FROM reviews WHERE reviewer_id=? AND target_type=? AND target_id=? AND clinic_appointment_id=?');$ex->execute([$user['id'],$target[0],$target[1],$id]);$rid=(int)$ex->fetchColumn();if($rid)db()->prepare('UPDATE reviews SET rating=?,comment=?,status="published",updated_at=? WHERE id=?')->execute([$target[2],$comment,now(),$rid]);else db()->prepare('INSERT INTO reviews(reviewer_id,target_type,target_id,clinic_appointment_id,rating,comment,status,created_at,updated_at) VALUES(?,?,?,?,?,?,"published",?,?)')->execute([$user['id'],$target[0],$target[1],$id,$target[2],$comment,now(),now()]);}
            back_to('clinic_patient','Avaliação publicada.');break;

        case 'clinic_professional_link_response':
            $id=(int)request('id');$decision=(string)request('decision');if(!in_array($decision,['accept','reject'],true))throw new RuntimeException('Resposta inválida.');$q=db()->prepare("SELECT p.*,c.owner_user_id,c.name company_name FROM clinic_professionals p JOIN companies c ON c.id=p.company_id WHERE p.id=? AND p.user_id=? AND COALESCE(p.link_state,'pending_professional')='pending_professional'");$q->execute([$id,(int)$user['id']]);$link=$q->fetch();if(!$link)throw new RuntimeException('Solicitação de vínculo não encontrada.');
            if($decision==='reject'){db()->prepare("UPDATE clinic_professionals SET link_state='rejected',status='inactive',professional_responded_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$id]);if(!empty($link['owner_user_id']))create_notification((int)$link['owner_user_id'],'Vínculo profissional recusado',$user['name'].' recusou o vínculo com a clínica.','warning','dashboard.php?page=clinic','clinic_professional',$id);back_to('clinic_professional','Vínculo recusado.');}
            $state=clinic_professional_link_state_after_accept((int)$user['id'],(int)$link['company_id']);$active=$state['state']==='active'?'active':'inactive';db()->prepare('UPDATE clinic_professionals SET link_state=?,status=?,professional_responded_at=?,admin_approval_status=?,billing_required=?,updated_at=? WHERE id=?')->execute([$state['state'],$active,now(),$state['admin_status'],$state['billing_required'],now(),$id]);
            if(!empty($link['owner_user_id']))create_notification((int)$link['owner_user_id'],'Vínculo profissional respondido',$user['name'].' aceitou o vínculo. '.clinic_link_state_label($state['state']).'.','info','dashboard.php?page=clinic','clinic_professional',$id);if($state['state']==='pending_admin')notify_super_admins('Vínculo profissional aguardando aprovação',$user['name'].' aceitou um segundo vínculo com '.$link['company_name'].'.','warning',0,'dashboard.php?page=users','clinic_professional',$id);back_to('clinic_professional',$state['message']);break;

        case 'review_clinic_professional_link':
            if(!is_super_admin())throw new RuntimeException('Somente o Administrador Sênior pode liberar o segundo vínculo profissional.');$id=(int)request('id');$decision=(string)request('decision');if(!in_array($decision,['approved','rejected'],true))throw new RuntimeException('Decisão inválida.');$q=db()->prepare("SELECT p.*,c.name company_name,c.owner_user_id FROM clinic_professionals p JOIN companies c ON c.id=p.company_id WHERE p.id=? AND COALESCE(p.link_state,'')='pending_admin'");$q->execute([$id]);$link=$q->fetch();if(!$link)throw new RuntimeException('Solicitação não encontrada ou já analisada.');$state=$decision==='approved'?'active':'rejected';$status=$decision==='approved'?'active':'inactive';db()->prepare('UPDATE clinic_professionals SET link_state=?,status=?,admin_approval_status=?,admin_approved_by=?,admin_approved_at=?,updated_at=? WHERE id=?')->execute([$state,$status,$decision,(int)$user['id'],now(),now(),$id]);if(!empty($link['user_id']))create_notification((int)$link['user_id'],'Vínculo profissional '.($decision==='approved'?'aprovado':'recusado'),'Seu vínculo com '.$link['company_name'].' foi '.($decision==='approved'?'liberado':'recusado').' pelo Administrador Sênior.',$decision==='approved'?'success':'warning','dashboard.php?page=clinic_professional','clinic_professional',$id);if(!empty($link['owner_user_id']))create_notification((int)$link['owner_user_id'],'Vínculo profissional '.($decision==='approved'?'aprovado':'recusado'),'A solicitação de vínculo profissional foi analisada pelo Administrador Sênior.','info','dashboard.php?page=clinic','clinic_professional',$id);back_to('users','Solicitação de vínculo analisada.');break;

        case 'clinic_professional_save_patient':
            $professionalId=(int)request('professional_id');$q=db()->prepare("SELECT * FROM clinic_professionals WHERE id=? AND user_id=? AND status='active' AND COALESCE(link_state,'active')='active'");$q->execute([$professionalId,(int)$user['id']]);$pr=$q->fetch();if(!$pr)throw new RuntimeException('Vínculo profissional inválido.');$companyId=(int)$pr['company_id'];$token=trim((string)request('platform_user_id'));$linked=$token!==''?clinic_platform_user_by_token($token):null;if($token!==''&&!$linked)throw new RuntimeException('Paciente não encontrado pelo ID.');$name=trim((string)request('name'));$email=strtolower(trim((string)request('email')));$phone=only_digits(request('phone'));$document=trim((string)request('document'));if($linked){$name=$linked['name'];$email=$linked['email'];$phone=$linked['phone'];$document=$linked['document'];}if($name==='')throw new RuntimeException('Informe o paciente.');if(!$linked)$linked=clinic_create_platform_user('client',$name,$email,$phone,$document,(string)request('password','123456'),0);$uid=(int)$linked['id'];$dup=db()->prepare('SELECT id FROM clinic_patients WHERE company_id=? AND platform_user_id=?');$dup->execute([$companyId,$uid]);$patientId=(int)$dup->fetchColumn();if(!$patientId){assert_clinic_capacity($companyId,'patients');db()->prepare('INSERT INTO clinic_patients(company_id,platform_user_id,name,document,phone,email,birth_date,notes,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,"active",?,?)')->execute([$companyId,$uid,$name,$document,$phone,$email,trim((string)request('birth_date')),trim((string)request('notes')),now(),now()]);$patientId=(int)db()->lastInsertId();create_notification($uid,'Cadastro de paciente','Você foi cadastrado como paciente da clínica.','success','dashboard.php?page=clinic_patient','clinic_patient',$patientId);}clinic_sync_professional_contact((int)$user['id'],$companyId,$patientId);back_to('clinic_professional','Paciente disponível na sua instituição.');break;

        case 'clinic_professional_create_appointment':
            $professionalId=(int)request('professional_id');$q=db()->prepare("SELECT p.*,c.name company_name FROM clinic_professionals p JOIN companies c ON c.id=p.company_id WHERE p.id=? AND p.user_id=? AND p.status='active' AND COALESCE(p.link_state,'active')='active'");$q->execute([$professionalId,(int)$user['id']]);$pr=$q->fetch();if(!$pr)throw new RuntimeException('Vínculo profissional inválido.');$companyId=(int)$pr['company_id'];$patientId=(int)request('patient_id');$specialtyId=(int)request('specialty_id');$bookingMode=request('booking_mode','time')==='arrival'?'arrival':'time';$date=trim((string)request('appointment_date'));$time=substr(trim((string)request('appointment_time')),0,5);if($bookingMode==='arrival')$time='00:00';if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)||($bookingMode==='time'&&car_wash_minutes($time)===null))throw new RuntimeException('Informe data e horário válidos.');$pq=db()->prepare('SELECT * FROM clinic_patients WHERE id=? AND company_id=? AND status="active"');$pq->execute([$patientId,$companyId]);$patient=$pq->fetch();if(!$patient)throw new RuntimeException('Paciente inválido.');$spq=db()->prepare('SELECT * FROM clinic_specialties WHERE id=? AND company_id=? AND status="active"');$spq->execute([$specialtyId,$companyId]);$sp=$spq->fetch();if(!$sp)throw new RuntimeException('Especialidade inválida.');if(($sp['service_kind']??'consultation')==='exam')throw new RuntimeException('Exames devem ser registrados na área de Exames, separada das consultas.');clinic_assert_daily_capacity($professionalId,$date,'consultation');if($bookingMode==='time'&&!clinic_slot_is_available($professionalId,$date,$time,(int)$sp['duration_minutes']))throw new RuntimeException('Horário indisponível.');$conflicts=$bookingMode==='time'?clinic_patient_external_conflicts((int)($patient['platform_user_id']??0),$date,$time,$companyId):[];if($conflicts&&!(int)request('confirm_conflict',0))throw new RuntimeException('ATENÇÃO: o paciente já possui agendamento em outra instituição próximo deste horário. Marque “Confirmar mesmo com conflito” para continuar.');$first=clinic_first_visit($companyId,$patientId,$professionalId)?1:0;$amount=localized_decimal(request('amount',$sp['price']));db()->prepare('INSERT INTO clinic_appointments(company_id,patient_id,professional_id,specialty_id,appointment_date,appointment_time,amount,payment_method,payment_status,status,notes,scheduled_by_user_id,visit_type,first_visit,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$patientId,$professionalId,$specialtyId,$date,$time,$amount,request('payment_method','pix'),'pending','scheduled',trim((string)request('notes')),(int)$user['id'],$first?'first':'return',$first,now(),now()]);$id=(int)db()->lastInsertId();$arrivalOrder=$bookingMode==='arrival'?clinic_next_arrival_order($professionalId,$date):null;db()->prepare('UPDATE clinic_appointments SET booking_mode=?,arrival_order=? WHERE id=?')->execute([$bookingMode,$arrivalOrder,$id]);clinic_sync_professional_contact((int)$user['id'],$companyId,$patientId);if(!empty($patient['platform_user_id']))create_notification((int)$patient['platform_user_id'],'Consulta agendada',$user['name'].' agendou sua consulta em '.$pr['company_name'].' para '.date('d/m/Y',strtotime($date)).($bookingMode==='arrival'?' por ordem de chegada #'.(int)$arrivalOrder:' às '.$time).'.','success','dashboard.php?page=clinic_patient','clinic_appointment',$id);back_to('clinic_professional','Consulta criada.');break;

        case 'save_clinic_professional_contact':
            $id=(int)request('id',0);$companyId=(int)request('company_id');if(!clinic_professional_can_access_company((int)$user['id'],$companyId))throw new RuntimeException('Instituição inválida.');$name=trim((string)request('name'));if($name==='')throw new RuntimeException('Informe o nome do contato.');$phone=only_digits(request('phone'));$email=strtolower(trim((string)request('email')));$notes=trim((string)request('notes'));if($id){$q=db()->prepare('UPDATE clinic_professional_contacts SET name=?,phone=?,email=?,notes=?,is_automatic=0,updated_at=? WHERE id=? AND owner_user_id=? AND company_id=?');$q->execute([$name,$phone,$email,$notes,now(),$id,(int)$user['id'],$companyId]);}else{db()->prepare('INSERT INTO clinic_professional_contacts(owner_user_id,company_id,name,phone,email,notes,is_automatic,created_at,updated_at) VALUES(?,?,?,?,?,?,0,?,?)')->execute([(int)$user['id'],$companyId,$name,$phone,$email,$notes,now(),now()]);}back_to('clinic_professional','Contato salvo.');break;

        case 'delete_clinic_professional_contact':
            $id=(int)request('id');db()->prepare('DELETE FROM clinic_professional_contacts WHERE id=? AND owner_user_id=?')->execute([$id,(int)$user['id']]);back_to('clinic_professional','Contato apagado.');break;

        case 'save_clinic_patient_record':
            $appointmentId=(int)request('appointment_id');$q=db()->prepare("SELECT a.company_id,a.patient_id,a.professional_id,p.user_id FROM clinic_appointments a JOIN clinic_professionals p ON p.id=a.professional_id WHERE a.id=? AND p.user_id=? AND p.status='active' AND COALESCE(p.link_state,'active')='active'");$q->execute([$appointmentId,(int)$user['id']]);$a=$q->fetch();if(!$a)throw new RuntimeException('Atendimento inválido.');$attachment=upload_document('attachment_file','clinicas/prontuarios/'.(int)$a['company_id']);$q=db()->prepare('SELECT id,attachment_path FROM clinic_patient_records WHERE appointment_id=? AND professional_id=? LIMIT 1');$q->execute([$appointmentId,(int)$a['professional_id']]);$existing=$q->fetch();if($existing&&!$attachment)$attachment=(string)$existing['attachment_path'];$data=[trim((string)request('symptoms')),trim((string)request('assessment')),trim((string)request('diagnosis')),trim((string)request('prescriptions')),trim((string)request('exam_requests')),trim((string)request('allergies')),trim((string)request('medications')),trim((string)request('observations')),$attachment];if($existing){db()->prepare('UPDATE clinic_patient_records SET symptoms=?,assessment=?,diagnosis=?,prescriptions=?,exam_requests=?,allergies=?,medications=?,observations=?,attachment_path=?,updated_at=? WHERE id=?')->execute(array_merge($data,[now(),(int)$existing['id']]));}else{db()->prepare('INSERT INTO clinic_patient_records(company_id,patient_id,professional_id,appointment_id,symptoms,assessment,diagnosis,prescriptions,exam_requests,allergies,medications,observations,attachment_path,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute(array_merge([(int)$a['company_id'],(int)$a['patient_id'],(int)$a['professional_id'],$appointmentId],$data,[now(),now()]));}back_to('clinic_professional','Prontuário atualizado.');break;

        case 'clinic_professional_request_exam':
            $professionalId=(int)request('professional_id');$q=db()->prepare("SELECT * FROM clinic_professionals WHERE id=? AND user_id=? AND status='active' AND COALESCE(link_state,'active')='active'");$q->execute([$professionalId,(int)$user['id']]);$pr=$q->fetch();if(!$pr)throw new RuntimeException('Vínculo profissional inválido.');$companyId=(int)$pr['company_id'];$patientId=(int)request('patient_id');$specialtyId=(int)request('specialty_id');$pq=db()->prepare('SELECT platform_user_id FROM clinic_patients WHERE id=? AND company_id=?');$pq->execute([$patientId,$companyId]);$patientUid=(int)$pq->fetchColumn();if(!$patientUid)throw new RuntimeException('Paciente sem conta vinculada.');$sq=db()->prepare("SELECT * FROM clinic_specialties WHERE id=? AND company_id=? AND service_kind='exam' AND status='active'");$sq->execute([$specialtyId,$companyId]);$sp=$sq->fetch();if(!$sp)throw new RuntimeException('Exame inválido.');db()->prepare("INSERT INTO clinic_exam_requests(company_id,patient_id,professional_id,specialty_id,requested_by_user_id,status,notes,created_at,updated_at) VALUES(?,?,?,?,?,'requested',?,?,?)")->execute([$companyId,$patientId,$professionalId,$specialtyId,(int)$user['id'],trim((string)request('notes')),now(),now()]);$id=(int)db()->lastInsertId();create_notification($patientUid,'Exame solicitado',$user['name'].' solicitou o exame '.$sp['name'].'. Abra Minha Saúde para consultar/agendar.','info','dashboard.php?page=clinic_patient','clinic_exam_request',$id);back_to('clinic_professional','Exame solicitado ao paciente.');break;

        case 'clinic_patient_request_exam':
            $companyId=(int)request('company_id');$patient=clinic_patient_for_user((int)$user['id'],$companyId);if(!$patient)throw new RuntimeException('Você não é paciente desta clínica.');$specialtyId=(int)request('specialty_id');$q=db()->prepare("SELECT * FROM clinic_specialties WHERE id=? AND company_id=? AND service_kind='exam' AND status='active'");$q->execute([$specialtyId,$companyId]);$sp=$q->fetch();if(!$sp)throw new RuntimeException('Exame indisponível.');$date=null;$time=null;$status='requested';$bookingMode='time';$arrivalOrder=null;if((int)$sp['allow_patient_booking']===1){$sch=clinic_exam_schedule($sp);$bookingMode=$sch['mode'];$date=trim((string)request('scheduled_date'));if(!clinic_exam_date_allowed($sp,$date))throw new RuntimeException('Esta data não foi disponibilizada pela clínica para este exame. Escolha uma das datas exibidas.');if($bookingMode==='arrival'){$time=null;$arrivalOrder=clinic_exam_next_arrival_order($companyId,$specialtyId,$date);$status='scheduled';}else{$time=substr(trim((string)request('scheduled_time')),0,5);if($time===''||!in_array($time,clinic_exam_available_times($sp,$date),true))throw new RuntimeException('Este horário não foi disponibilizado pela clínica para este exame. Escolha um dos horários exibidos.');$status='scheduled';}}if($date){$settings=clinic_settings($companyId);$limit=max(0,(int)($settings['daily_exam_limit']??0));if($limit>0){$cq=db()->prepare("SELECT COUNT(*) FROM clinic_exam_requests WHERE company_id=? AND scheduled_date=? AND status NOT IN ('cancelled','completed')");$cq->execute([$companyId,$date]);if((int)$cq->fetchColumn()>=$limit)throw new RuntimeException('As vagas de exames desta clínica para o dia selecionado já foram preenchidas.');}}$settings=$settings??clinic_settings($companyId);$payMethod=(string)($settings['payment_method']??'pix');db()->prepare('INSERT INTO clinic_exam_requests(company_id,patient_id,specialty_id,requested_by_user_id,scheduled_date,scheduled_time,status,notes,booking_mode,arrival_order,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,(int)$patient['id'],$specialtyId,(int)$user['id'],$date,$time,$status,trim((string)request('notes')),$bookingMode,$arrivalOrder,now(),now()]);$id=(int)db()->lastInsertId();db()->prepare("UPDATE clinic_exam_requests SET payment_method=?,payment_status='pending' WHERE id=?")->execute([$payMethod,$id]);$oq=db()->prepare('SELECT owner_user_id,name FROM companies WHERE id=?');$oq->execute([$companyId]);$c=$oq->fetch();if(!empty($c['owner_user_id']))create_notification((int)$c['owner_user_id'],'Solicitação de exame',$user['name'].' solicitou '.$sp['name'].'.','info','dashboard.php?page=clinic','clinic_exam_request',$id);$msg=$status==='scheduled'?($bookingMode==='arrival'?'Exame reservado para o dia selecionado, por ordem de chegada.':'Exame solicitado e horário registrado.'):'Exame solicitado. A clínica informará o agendamento.';back_to('clinic_patient',$msg);break;

        case 'clinic_patient_cancel_exam':
            $id=(int)request('id');$q=db()->prepare('SELECT e.*,p.platform_user_id FROM clinic_exam_requests e JOIN clinic_patients p ON p.id=e.patient_id WHERE e.id=? AND p.platform_user_id=?');$q->execute([$id,(int)$user['id']]);$exam=$q->fetch();if(!$exam)throw new RuntimeException('Exame não encontrado.');if(in_array((string)$exam['status'],['completed','cancelled'],true))throw new RuntimeException('Este exame não pode mais ser cancelado.');db()->prepare("UPDATE clinic_exam_requests SET status='cancelled',updated_at=? WHERE id=?")->execute([now(),$id]);back_to('clinic_patient','Exame cancelado e vaga liberada.');break;

        case 'clinic_patient_upload_payment_proof':
            $id=(int)request('id');$kind=(string)request('kind','appointment');if(!in_array($kind,['appointment','exam'],true))throw new RuntimeException('Tipo de atendimento inválido.');if($kind==='appointment'){$q=db()->prepare('SELECT a.*,p.platform_user_id FROM clinic_appointments a JOIN clinic_patients p ON p.id=a.patient_id WHERE a.id=? AND p.platform_user_id=?');$q->execute([$id,(int)$user['id']]);$row=$q->fetch();}else{$q=db()->prepare('SELECT e.*,p.platform_user_id FROM clinic_exam_requests e JOIN clinic_patients p ON p.id=e.patient_id WHERE e.id=? AND p.platform_user_id=?');$q->execute([$id,(int)$user['id']]);$row=$q->fetch();}if(!$row)throw new RuntimeException('Atendimento não encontrado.');$settings=clinic_settings((int)$row['company_id']);$method=(string)($row['payment_method']??$settings['payment_method']??'');if($method!=='pix')throw new RuntimeException('O envio de comprovante está disponível quando a forma de pagamento é Pix.');$proof=upload_document('proof_file','clinicas/comprovantes/'.(int)$row['company_id'],(string)($row['payment_proof']??''));if($proof==='')throw new RuntimeException('Selecione uma imagem ou PDF do comprovante.');$table=$kind==='appointment'?'clinic_appointments':'clinic_exam_requests';db()->prepare("UPDATE $table SET payment_proof=?,payment_proof_uploaded_at=?,payment_status='proof_sent',updated_at=? WHERE id=?")->execute([$proof,now(),now(),$id]);$oq=db()->prepare('SELECT owner_user_id FROM companies WHERE id=?');$oq->execute([(int)$row['company_id']]);$owner=(int)$oq->fetchColumn();if($owner)create_notification($owner,'Comprovante Pix recebido',$user['name'].' enviou um comprovante de pagamento.','success','dashboard.php?page=clinic',$kind==='appointment'?'clinic_appointment':'clinic_exam_request',$id);back_to('clinic_patient','Comprovante Pix enviado para conferência.');break;

        case 'clinic_confirm_payment':
            $id=(int)request('id');$kind=(string)request('kind','appointment');if(!in_array($kind,['appointment','exam'],true))throw new RuntimeException('Tipo inválido.');$table=$kind==='appointment'?'clinic_appointments':'clinic_exam_requests';$q=db()->prepare("SELECT * FROM $table WHERE id=?");$q->execute([$id]);$row=$q->fetch();if(!$row)throw new RuntimeException('Atendimento não encontrado.');$companyId=(int)$row['company_id'];$professionalOk=false;if(!empty($row['professional_id'])){$pq=db()->prepare("SELECT COUNT(*) FROM clinic_professionals WHERE id=? AND user_id=? AND status='active' AND COALESCE(link_state,'active')='active'");$pq->execute([(int)$row['professional_id'],(int)$user['id']]);$professionalOk=(int)$pq->fetchColumn()>0;}if(!$professionalOk)must_manage_clinic($companyId);if(empty($row['payment_proof']))throw new RuntimeException('Ainda não há comprovante anexado.');db()->prepare("UPDATE $table SET payment_status='paid',updated_at=? WHERE id=?")->execute([now(),$id]);$pq=db()->prepare('SELECT platform_user_id FROM clinic_patients WHERE id=?');$pq->execute([(int)$row['patient_id']]);$uid=(int)$pq->fetchColumn();if($uid)create_notification($uid,'Pagamento confirmado','Seu comprovante Pix foi confirmado pela clínica.','success','dashboard.php?page=clinic_patient',$kind==='appointment'?'clinic_appointment':'clinic_exam_request',$id);back_to($professionalOk?'clinic_professional':'clinic','Pagamento confirmado.');break;

        case 'clinic_patient_checkin':
            $id=(int)request('id');$q=db()->prepare('SELECT a.*,p.platform_user_id,c.latitude clinic_lat,c.longitude clinic_lng FROM clinic_appointments a JOIN clinic_patients p ON p.id=a.patient_id JOIN companies c ON c.id=a.company_id WHERE a.id=? AND p.platform_user_id=?');$q->execute([$id,(int)$user['id']]);$a=$q->fetch();if(!$a)throw new RuntimeException('Consulta não encontrada.');if((string)$a['appointment_date']!==date('Y-m-d'))throw new RuntimeException('A confirmação de presença só fica disponível no dia da consulta.');if(in_array((string)$a['status'],['completed','cancelled','no_show'],true))throw new RuntimeException('Este atendimento já foi encerrado.');$latRaw=trim((string)request('latitude'));$lngRaw=trim((string)request('longitude'));if($latRaw===''||$lngRaw==='')throw new RuntimeException('Autorize a localização do aparelho para confirmar sua presença.');$clinicLat=(float)($a['clinic_lat']??0);$clinicLng=(float)($a['clinic_lng']??0);if(abs($clinicLat)<0.000001&&abs($clinicLng)<0.000001)throw new RuntimeException('A clínica ainda não cadastrou a localização necessária para confirmar presença.');$lat=(float)$latRaw;$lng=(float)$lngRaw;$distance=distance_km($lat,$lng,$clinicLat,$clinicLng)*1000;$settings=clinic_settings((int)$a['company_id']);$radius=max(50,(int)($settings['checkin_radius_meters']??250));if($distance>$radius)throw new RuntimeException('Você precisa estar no local da clínica para confirmar presença. Distância atual: '.round($distance).' m; limite: '.$radius.' m.');db()->prepare("UPDATE clinic_appointments SET checkin_at=?,checkin_lat=?,checkin_lng=?,checkin_distance_m=?,status=CASE WHEN status='scheduled' THEN 'confirmed' ELSE status END,updated_at=? WHERE id=?")->execute([now(),$lat,$lng,$distance,now(),$id]);$oq=db()->prepare('SELECT owner_user_id FROM companies WHERE id=?');$oq->execute([(int)$a['company_id']]);$owner=(int)$oq->fetchColumn();if($owner)create_notification($owner,'Paciente chegou à clínica',$user['name'].' confirmou presença pelo celular.','success','dashboard.php?page=clinic','clinic_appointment',$id);back_to('clinic_patient','Presença confirmada no local da clínica.');break;

        case 'clinic_update_exam_request':
            $id=(int)request('id');$q=db()->prepare('SELECT * FROM clinic_exam_requests WHERE id=?');$q->execute([$id]);$exam=$q->fetch();if(!$exam)throw new RuntimeException('Solicitação não encontrada.');$companyId=(int)$exam['company_id'];$professionalOwned=false;$actingProfessionalId=(int)($exam['professional_id']??0);if($actingProfessionalId>0){$pq=db()->prepare("SELECT COUNT(*) FROM clinic_professionals WHERE id=? AND user_id=? AND status='active' AND COALESCE(link_state,'active')='active'");$pq->execute([$actingProfessionalId,(int)$user['id']]);$professionalOwned=(int)$pq->fetchColumn()>0;}else{$pq=db()->prepare("SELECT id FROM clinic_professionals WHERE company_id=? AND user_id=? AND status='active' AND COALESCE(link_state,'active')='active' ORDER BY id LIMIT 1");$pq->execute([$companyId,(int)$user['id']]);$actingProfessionalId=(int)$pq->fetchColumn();if($actingProfessionalId>0){$professionalOwned=true;db()->prepare('UPDATE clinic_exam_requests SET professional_id=?,updated_at=? WHERE id=?')->execute([$actingProfessionalId,now(),$id]);}}if(!$professionalOwned)must_manage_clinic($companyId);$status=(string)request('status',$exam['status']);if(!in_array($status,['requested','scheduled','completed','cancelled'],true))throw new RuntimeException('Status inválido.');$date=trim((string)request('scheduled_date'));$time=substr(trim((string)request('scheduled_time')),0,5);if($status==='scheduled'&&$date!==''){$limit=$actingProfessionalId>0?clinic_professional_daily_limit($actingProfessionalId,'exam'):(int)(clinic_settings($companyId)['daily_exam_limit']??0);if($limit>0){if($actingProfessionalId>0){$cq=db()->prepare("SELECT COUNT(*) FROM clinic_exam_requests WHERE professional_id=? AND scheduled_date=? AND id<>? AND status NOT IN ('cancelled','completed')");$cq->execute([$actingProfessionalId,$date,$id]);}else{$cq=db()->prepare("SELECT COUNT(*) FROM clinic_exam_requests WHERE company_id=? AND scheduled_date=? AND id<>? AND status NOT IN ('cancelled','completed')");$cq->execute([$companyId,$date,$id]);}$used=(int)$cq->fetchColumn();if($used>=$limit)throw new RuntimeException('O limite diário de exames para este profissional/clínica já foi atingido nesta data.');}}$result=upload_document('result_file','clinicas/exames/'.$companyId,(string)($exam['result_path']??''));db()->prepare('UPDATE clinic_exam_requests SET scheduled_date=?,scheduled_time=?,status=?,result_path=?,result_notes=?,updated_at=? WHERE id=?')->execute([$date?:null,$time?:null,$status,$result?:null,trim((string)request('result_notes')),now(),$id]);$uq=db()->prepare('SELECT platform_user_id FROM clinic_patients WHERE id=?');$uq->execute([(int)$exam['patient_id']]);$puid=(int)$uq->fetchColumn();if($puid)create_notification($puid,'Exame atualizado','Sua solicitação de exame foi atualizada para '.status_label($status).'.','info','dashboard.php?page=clinic_patient','clinic_exam_request',$id);back_to($professionalOwned?'clinic_professional':'clinic','Exame atualizado.');break;

        case 'clinic_professional_quick_charge':
            $sourceType=(string)request('source_type');$sourceId=(int)request('source_id');$companyId=0;$patientId=0;$defaultAmount=0;$description='Atendimento clínico';if($sourceType==='appointment'){$q=db()->prepare("SELECT a.*,p.user_id professional_user_id,s.name service_name FROM clinic_appointments a JOIN clinic_professionals p ON p.id=a.professional_id LEFT JOIN clinic_specialties s ON s.id=a.specialty_id WHERE a.id=? AND p.user_id=? AND a.status='completed'");$q->execute([$sourceId,(int)$user['id']]);$src=$q->fetch();if(!$src)throw new RuntimeException('Consulta concluída não encontrada.');$companyId=(int)$src['company_id'];$patientId=(int)$src['patient_id'];$defaultAmount=(float)$src['amount'];$description='Consulta: '.($src['service_name']?:'Atendimento clínico');}elseif($sourceType==='exam'){$q=db()->prepare("SELECT e.*,p.user_id professional_user_id,s.name service_name,s.price FROM clinic_exam_requests e LEFT JOIN clinic_professionals p ON p.id=e.professional_id LEFT JOIN clinic_specialties s ON s.id=e.specialty_id WHERE e.id=? AND p.user_id=? AND e.status='completed'");$q->execute([$sourceId,(int)$user['id']]);$src=$q->fetch();if(!$src)throw new RuntimeException('Exame concluído não encontrado.');$companyId=(int)$src['company_id'];$patientId=(int)$src['patient_id'];$defaultAmount=(float)$src['price'];$description='Exame: '.($src['service_name']?:'Exame');}else throw new RuntimeException('Origem da cobrança inválida.');$pq=db()->prepare('SELECT platform_user_id FROM clinic_patients WHERE id=? AND company_id=?');$pq->execute([$patientId,$companyId]);$recipientId=(int)$pq->fetchColumn();if(!$recipientId)throw new RuntimeException('Paciente sem conta da plataforma.');$amount=localized_decimal(request('amount',$defaultAmount));if($amount<=0)throw new RuntimeException('Informe o valor.');$method=(string)request('payment_method','pix');if(!in_array($method,['pix','link'],true))$method='pix';$link=trim((string)request('payment_link'));$pix=(string)($user['pix_key']??'');db()->prepare('INSERT INTO charges(issuer_user_id,user_id,amount,description,due_date,payment_link,pix_key_snapshot,payment_method,status,blocks_access,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,"pending",0,?,?)')->execute([(int)$user['id'],$recipientId,$amount,trim((string)request('description',$description))?:$description,request('due_date')?:null,$link,$pix,$method,now(),now()]);$chargeId=(int)db()->lastInsertId();create_notification($recipientId,'Nova cobrança clínica',$user['name'].' enviou uma cobrança de '.money($amount).'.','warning','dashboard.php?page=charges','charge',$chargeId);back_to('clinic_professional','Cobrança enviada ao paciente.');break;

        case 'save_clinic_patient_details':
            $patientId=(int)request('patient_id');$q=db()->prepare('SELECT * FROM clinic_patients WHERE id=?');$q->execute([$patientId]);$patient=$q->fetch();if(!$patient)throw new RuntimeException('Paciente não encontrado.');$companyId=(int)$patient['company_id'];must_manage_clinic($companyId);
            $photo=upload_image('photo_file','clinicas/pacientes/'.$companyId,(string)($patient['photo']??''));
            $fields=['name','document','rg','sex','marital_status','occupation','sus_card','birth_date','email','cep','address','address_number','neighborhood','city','state','complement','reference_point','emergency_name','emergency_relation','emergency_notes','notes'];$vals=[];foreach($fields as $f)$vals[$f]=trim((string)request($f,$patient[$f]??''));
            $phone=only_digits(request('phone',$patient['phone']??''));$whatsapp=only_digits(request('whatsapp',$patient['whatsapp']??''));$residential=only_digits(request('residential_phone',$patient['residential_phone']??''));$emergencyPhone=only_digits(request('emergency_phone',$patient['emergency_phone']??''));$emergencyWhatsapp=only_digits(request('emergency_whatsapp',$patient['emergency_whatsapp']??''));if($vals['name']==='')throw new RuntimeException('Informe o nome do paciente.');if($vals['email']!==''&&!filter_var($vals['email'],FILTER_VALIDATE_EMAIL))throw new RuntimeException('E-mail inválido.');
            db()->prepare('UPDATE clinic_patients SET photo=?,name=?,document=?,rg=?,sex=?,marital_status=?,occupation=?,sus_card=?,birth_date=?,phone=?,whatsapp=?,residential_phone=?,email=?,cep=?,address=?,address_number=?,neighborhood=?,city=?,state=?,complement=?,reference_point=?,emergency_name=?,emergency_relation=?,emergency_phone=?,emergency_whatsapp=?,emergency_notes=?,notes=?,updated_at=? WHERE id=?')->execute([$photo,$vals['name'],$vals['document'],$vals['rg'],$vals['sex'],$vals['marital_status'],$vals['occupation'],$vals['sus_card'],$vals['birth_date'],$phone,$whatsapp,$residential,$vals['email'],$vals['cep'],$vals['address'],$vals['address_number'],$vals['neighborhood'],$vals['city'],normalize_state($vals['state']),$vals['complement'],$vals['reference_point'],$vals['emergency_name'],$vals['emergency_relation'],$emergencyPhone,$emergencyWhatsapp,$vals['emergency_notes'],$vals['notes'],now(),$patientId]);
            flash('success','Dados completos do paciente atualizados.');redirect('dashboard.php?page=clinic_record&id='.$patientId);break;

        case 'save_clinic_diagnosis':
            $patientId=(int)request('patient_id');$q=db()->prepare('SELECT company_id FROM clinic_patients WHERE id=?');$q->execute([$patientId]);$companyId=(int)$q->fetchColumn();if($companyId<1)throw new RuntimeException('Paciente não encontrado.');$professionalId=0;$pq=db()->prepare("SELECT id FROM clinic_professionals WHERE company_id=? AND user_id=? AND status='active' AND COALESCE(link_state,'active')='active' ORDER BY id LIMIT 1");$pq->execute([$companyId,(int)$user['id']]);$professionalId=(int)$pq->fetchColumn();if($professionalId<1&&!can_manage_clinic_company($companyId))throw new RuntimeException('Somente a clínica ou profissional vinculado pode registrar diagnóstico.');$diagnosis=trim((string)request('diagnosis'));if($diagnosis==='')throw new RuntimeException('Informe o diagnóstico.');$date=trim((string)request('diagnosed_at',date('Y-m-d')));db()->prepare('INSERT INTO clinic_diagnoses(company_id,patient_id,professional_id,diagnosis,cid,observations,evolution,diagnosed_at,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$patientId,$professionalId?:null,$diagnosis,trim((string)request('cid')),trim((string)request('observations')),trim((string)request('evolution')),$date?:date('Y-m-d'),now(),now()]);flash('success','Diagnóstico registrado.');redirect('dashboard.php?page=clinic_record&id='.$patientId);break;

        case 'save_clinic_prescription':
            $patientId=(int)request('patient_id');$q=db()->prepare('SELECT company_id,platform_user_id FROM clinic_patients WHERE id=?');$q->execute([$patientId]);$patient=$q->fetch();if(!$patient)throw new RuntimeException('Paciente não encontrado.');$companyId=(int)$patient['company_id'];$pq=db()->prepare("SELECT id,name FROM clinic_professionals WHERE company_id=? AND user_id=? AND status='active' AND COALESCE(link_state,'active')='active' ORDER BY id LIMIT 1");$pq->execute([$companyId,(int)$user['id']]);$pr=$pq->fetch();if(!$pr)throw new RuntimeException('Somente profissional vinculado pode emitir receita eletrônica.');$medicine=trim((string)request('medicine_name'));if($medicine==='')throw new RuntimeException('Informe o medicamento.');$release=(int)request('release_to_patient',0)===1?1:0;$db=db();$db->beginTransaction();try{$db->prepare("INSERT INTO clinic_prescriptions(company_id,patient_id,professional_id,title,instructions,release_to_patient,status,prescribed_at,created_at,updated_at) VALUES(?,?,?,?,?,?,'active',?,?,?)")->execute([$companyId,$patientId,(int)$pr['id'],'Receita eletrônica',trim((string)request('instructions')),$release,now(),now(),now()]);$rxId=(int)$db->lastInsertId();$db->prepare('INSERT INTO clinic_prescription_items(prescription_id,medicine_name,dosage,frequency,duration_text,start_date,end_date,times_csv,observations,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)')->execute([$rxId,$medicine,trim((string)request('dosage')),trim((string)request('frequency')),trim((string)request('duration_text')),request('start_date')?:null,request('end_date')?:null,trim((string)request('times_csv')),trim((string)request('instructions')),now(),now()]);$db->commit();}catch(Throwable $e){if($db->inTransaction())$db->rollBack();throw $e;}if($release&&!empty($patient['platform_user_id']))create_notification((int)$patient['platform_user_id'],'💊 Nova receita eletrônica',$user['name'].' liberou uma receita para você.','success','dashboard.php?page=clinic_record&id='.$patientId,'clinic_prescription',$rxId);flash('success','Receita eletrônica emitida.');redirect('dashboard.php?page=clinic_record&id='.$patientId);break;

        case 'upload_clinic_patient_document':
            $patientId=(int)request('patient_id');$q=db()->prepare('SELECT company_id FROM clinic_patients WHERE id=?');$q->execute([$patientId]);$companyId=(int)$q->fetchColumn();if($companyId<1)throw new RuntimeException('Paciente não encontrado.');$pq=db()->prepare("SELECT id FROM clinic_professionals WHERE company_id=? AND user_id=? AND status='active' AND COALESCE(link_state,'active')='active'");$pq->execute([$companyId,(int)$user['id']]);if(!(int)$pq->fetchColumn()&&!can_manage_clinic_company($companyId))throw new RuntimeException('Sem permissão para anexar documento.');$title=trim((string)request('title'));if($title==='')throw new RuntimeException('Informe o título.');$path=upload_document('document_file','clinicas/documentos/'.$companyId);if($path==='')throw new RuntimeException('Selecione um arquivo válido.');$type=trim((string)request('document_type','other'));db()->prepare('INSERT INTO clinic_patient_documents(company_id,patient_id,uploaded_by,document_type,title,file_path,notes,created_at) VALUES(?,?,?,?,?,?,?,?)')->execute([$companyId,$patientId,(int)$user['id'],$type,$title,$path,trim((string)request('notes')),now()]);flash('success','Documento anexado ao prontuário.');redirect('dashboard.php?page=clinic_record&id='.$patientId);break;

        case 'terminate_clinic_link':
            $kind=(string)request('link_kind');$id=(int)request('id');if($kind==='professional'){$q=db()->prepare('SELECT * FROM clinic_professionals WHERE id=?');$q->execute([$id]);$r=$q->fetch();if(!$r)throw new RuntimeException('Vínculo não encontrado.');$allowed=can_manage_clinic_company((int)$r['company_id'])||((int)($r['user_id']??0)===(int)$user['id']);if(!$allowed)throw new RuntimeException('Sem permissão.');db()->prepare("UPDATE clinic_professionals SET status='inactive',link_state='terminated',updated_at=? WHERE id=?")->execute([now(),$id]);if(!empty($r['user_id']))create_notification((int)$r['user_id'],'Vínculo profissional encerrado','O vínculo com a instituição foi encerrado. Sua conta permanece ativa.','info');}
            elseif($kind==='patient'){$q=db()->prepare('SELECT * FROM clinic_patients WHERE id=?');$q->execute([$id]);$r=$q->fetch();if(!$r)throw new RuntimeException('Vínculo não encontrado.');$allowed=can_manage_clinic_company((int)$r['company_id'])||((int)($r['platform_user_id']??0)===(int)$user['id']);if(!$allowed)throw new RuntimeException('Sem permissão.');db()->prepare("UPDATE clinic_patients SET status='inactive',updated_at=? WHERE id=?")->execute([now(),$id]);}
            else throw new RuntimeException('Tipo de vínculo inválido.');back_to('clinic_links','Vínculo encerrado. A conta do usuário foi preservada.');break;

        case 'save_career_fee_setting':
            if(!is_super_admin())throw new RuntimeException('Somente o Administrador Sênior pode definir está mensalidade.');$amount=max(0,localized_decimal(request('amount',0)));db()->prepare("INSERT INTO settings(key_name,value,updated_at) VALUES('career_monthly_fee',?,?) ON CONFLICT(key_name) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at")->execute([(string)$amount,now()]);
            if($amount<=0)db()->prepare("UPDATE career_subscriptions SET amount=0,status='active',updated_at=?")->execute([now()]);
            back_to('careers','Mensalidade da Área de Currículos atualizada.');break;

        case 'upload_career_subscription_proof':
            $fee=max(0,localized_decimal(setting('career_monthly_fee','0')));if($fee<=0)throw new RuntimeException('A Área de Currículos está gratuita no momento.');$proof=upload_document('proof','curriculos/mensalidades');if($proof==='')throw new RuntimeException('Envie o comprovante.');db()->prepare("INSERT INTO career_subscriptions(user_id,amount,due_date,status,proof_path,updated_at) VALUES(?,?,?,'proof_sent',?,?) ON CONFLICT(user_id) DO UPDATE SET amount=excluded.amount,due_date=excluded.due_date,status='proof_sent',proof_path=excluded.proof_path,updated_at=excluded.updated_at")->execute([(int)$user['id'],$fee,date('Y-m-d'),$proof,now()]);$admin=primary_super_admin_id();if($admin)create_notification($admin,'💼 Mensalidade de Currículos','Novo comprovante enviado por '.$user['name'].'.','info','dashboard.php?page=careers','career_subscription',(int)$user['id']);back_to('careers','Comprovante enviado. Aguarde a análise do Administrador Sênior.');break;

        case 'review_career_subscription':
            if(!is_super_admin())throw new RuntimeException('Somente o Administrador Sênior pode liberar está mensalidade.');$target=(int)request('user_id');$decision=(string)request('decision');if(!in_array($decision,['active','rejected'],true))throw new RuntimeException('Decisão inválida.');$q=db()->prepare('SELECT * FROM career_subscriptions WHERE user_id=?');$q->execute([$target]);$cs=$q->fetch();if(!$cs)throw new RuntimeException('Cobrança não encontrada.');$due=$decision==='active'?date('Y-m-d',strtotime('+30 days')):($cs['due_date']??date('Y-m-d'));db()->prepare('UPDATE career_subscriptions SET status=?,due_date=?,reviewed_by=?,reviewed_at=?,updated_at=? WHERE user_id=?')->execute([$decision,$due,(int)$user['id'],now(),now(),$target]);create_notification($target,$decision==='active'?'✅ Currículos liberado':'❌ Comprovante recusado',$decision==='active'?'Sua mensalidade foi aprovada e a Área de Currículos está liberada.':'O comprovante da Área de Currículos foi recusado. Envie um novo comprovante.',$decision==='active'?'success':'warning','dashboard.php?page=careers');back_to('careers',$decision==='active'?'Acesso liberado por 30 dias.':'Comprovante recusado.');break;

        case 'save_career_profile':
            if(!career_access_allowed((int)$user['id'])&&!is_admin()) throw new RuntimeException('Regularize a mensalidade da Área de Currículos.');
            $visibility=(string)request('visibility','registered'); if(!in_array($visibility,['public','registered','private'],true))$visibility='registered';
            $mode=(string)request('desired_work_mode'); if(!in_array($mode,['','onsite','hybrid','remote'],true))$mode='';
            db()->prepare('INSERT INTO career_profiles(user_id,birth_date,city,state,address,linkedin,portfolio,desired_job_type,desired_work_mode,availability,salary_expectation,visibility,allow_company_contact,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(user_id) DO UPDATE SET birth_date=excluded.birth_date,city=excluded.city,state=excluded.state,address=excluded.address,linkedin=excluded.linkedin,portfolio=excluded.portfolio,desired_job_type=excluded.desired_job_type,desired_work_mode=excluded.desired_work_mode,availability=excluded.availability,salary_expectation=excluded.salary_expectation,visibility=excluded.visibility,allow_company_contact=excluded.allow_company_contact,updated_at=excluded.updated_at')->execute([(int)$user['id'],trim((string)request('birth_date')),trim((string)request('city')),trim((string)request('state')),trim((string)request('address')),trim((string)request('linkedin')),trim((string)request('portfolio')),trim((string)request('desired_job_type')),$mode,trim((string)request('availability')),max(0,localized_decimal(request('salary_expectation',0))),$visibility,(int)request('allow_company_contact',1),now()]);
            $_POST['return_anchor']='professional-profile'; back_to('careers','Perfil profissional atualizado.'); break;

        case 'save_resume':
            if(!career_access_allowed((int)$user['id'])) throw new RuntimeException('Regularize a mensalidade da Área de Currículos.');
            $id=(int)request('id'); $title=trim((string)request('title')); if($title==='')throw new RuntimeException('Informe o título do currículo.');
            $visibility=(string)request('visibility','registered'); if(!in_array($visibility,['public','registered','private'],true))$visibility='registered';
            $vals=[trim((string)request('objective')),trim((string)request('summary')),trim((string)request('skills')),trim((string)request('languages')),trim((string)request('additional_info')),max(0,localized_decimal(request('salary_expectation',0))),trim((string)request('availability')),trim((string)request('desired_contract')),$visibility];
            if($id){
                $q=db()->prepare("SELECT id FROM resumes WHERE id=? AND user_id=? AND status='active'");$q->execute([$id,(int)$user['id']]);if(!$q->fetchColumn())throw new RuntimeException('Currículo não encontrado.');
                db()->prepare('UPDATE resumes SET title=?,objective=?,summary=?,skills=?,languages=?,additional_info=?,salary_expectation=?,availability=?,desired_contract=?,visibility=?,updated_at=? WHERE id=? AND user_id=?')->execute(array_merge([$title],$vals,[now(),$id,(int)$user['id']]));
            }else{
                db()->prepare("INSERT INTO resumes(user_id,title,objective,summary,skills,languages,additional_info,salary_expectation,availability,desired_contract,visibility,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,'active',?,?)")->execute(array_merge([(int)$user['id'],$title],$vals,[now(),now()]));
                $id=(int)db()->lastInsertId();
            }
            $_POST['return_anchor']='resume-'.$id; back_to('careers','Currículo salvo.'); break;

        case 'delete_resume':
            if(!career_access_allowed((int)$user['id'])) throw new RuntimeException('Acesso bloqueado.');
            $id=(int)request('id');$q=db()->prepare("SELECT id FROM resumes WHERE id=? AND user_id=? AND status='active'");$q->execute([$id,(int)$user['id']]);if(!$q->fetchColumn())throw new RuntimeException('Currículo não encontrado.');
            db()->prepare("UPDATE resumes SET status='deleted',updated_at=? WHERE id=? AND user_id=?")->execute([now(),$id,(int)$user['id']]);
            $_POST['return_anchor']='resumes'; back_to('careers','Currículo apagado.'); break;

        case 'save_resume_experience':
            if(!career_access_allowed((int)$user['id']))throw new RuntimeException('Acesso bloqueado.');$resumeId=(int)request('resume_id');$q=db()->prepare("SELECT id FROM resumes WHERE id=? AND user_id=? AND status='active'");$q->execute([$resumeId,(int)$user['id']]);if(!$q->fetchColumn())throw new RuntimeException('Currículo inválido.');$company=trim((string)request('company_name'));$job=trim((string)request('job_title'));if($company===''||$job==='')throw new RuntimeException('Informe empresa e cargo.');db()->prepare('INSERT INTO resume_experiences(resume_id,company_name,job_title,start_date,end_date,activities,last_salary,leaving_reason,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)')->execute([$resumeId,$company,$job,trim((string)request('start_date')),trim((string)request('end_date')),trim((string)request('activities')),max(0,localized_decimal(request('last_salary',0))),trim((string)request('leaving_reason')),now(),now()]);$_POST['return_anchor']='resume-'.$resumeId;back_to('careers','Experiência adicionada.');break;

        case 'save_resume_education':
            if(!career_access_allowed((int)$user['id']))throw new RuntimeException('Acesso bloqueado.');$resumeId=(int)request('resume_id');$q=db()->prepare("SELECT id FROM resumes WHERE id=? AND user_id=? AND status='active'");$q->execute([$resumeId,(int)$user['id']]);if(!$q->fetchColumn())throw new RuntimeException('Currículo inválido.');db()->prepare('INSERT INTO resume_education(resume_id,education_level,institution,course,completion_year,certificate_path,created_at,updated_at) VALUES(?,?,?,?,?,NULL,?,?)')->execute([$resumeId,trim((string)request('education_level')),trim((string)request('institution')),trim((string)request('course')),trim((string)request('completion_year')),now(),now()]);$_POST['return_anchor']='resume-'.$resumeId;back_to('careers','Formação adicionada.');break;

        case 'upload_resume_document':
            if(!career_access_allowed((int)$user['id']))throw new RuntimeException('Acesso bloqueado.');$resumeId=(int)request('resume_id');$q=db()->prepare("SELECT id FROM resumes WHERE id=? AND user_id=? AND status='active'");$q->execute([$resumeId,(int)$user['id']]);if(!$q->fetchColumn())throw new RuntimeException('Currículo inválido.');$file=upload_document('document','curriculos/documentos');if($file==='')throw new RuntimeException('Selecione o arquivo.');$title=trim((string)request('title'));if($title==='')$title='Documento';db()->prepare('INSERT INTO resume_documents(resume_id,document_type,title,file_path,created_at) VALUES(?,?,?,?,?)')->execute([$resumeId,trim((string)request('document_type','professional')),$title,$file,now()]);$_POST['return_anchor']='resume-'.$resumeId;back_to('careers','Documento anexado ao currículo.');break;

        case 'save_job_post':
            if(!career_access_allowed((int)$user['id']))throw new RuntimeException('Regularize a mensalidade da Área de Currículos.');
            $companyId=resolve_company_id_for_user($user);if($companyId<1||!in_array((string)$user['role'],['company','employee'],true))throw new RuntimeException('Somente empresas podem publicar vagas.');
            $id=(int)request('id');$title=trim((string)request('title'));$description=trim((string)request('description'));if($title===''||$description==='')throw new RuntimeException('Informe título e descrição da vaga.');$mode=(string)request('work_mode','onsite');if(!in_array($mode,['onsite','hybrid','remote'],true))$mode='onsite';$professionalArea=trim((string)request('professional_area'));
            $vals=[$title,$professionalArea,trim((string)request('city')),trim((string)request('state')),max(0,localized_decimal(request('salary_min',0))),max(0,localized_decimal(request('salary_max',0))),trim((string)request('contract_type')),$mode,$description,trim((string)request('requirements')),now()];
            $isNew=false;
            if($id){$q=db()->prepare('SELECT id FROM job_posts WHERE id=? AND company_id=?');$q->execute([$id,$companyId]);if(!$q->fetchColumn())throw new RuntimeException('Vaga não encontrada.');db()->prepare('UPDATE job_posts SET title=?,professional_area=?,city=?,state=?,salary_min=?,salary_max=?,contract_type=?,work_mode=?,description=?,requirements=?,updated_at=? WHERE id=? AND company_id=?')->execute(array_merge($vals,[$id,$companyId]));$jobId=$id;}
            else{$isNew=true;db()->prepare("INSERT INTO job_posts(company_id,created_by,title,professional_area,city,state,salary_min,salary_max,contract_type,work_mode,description,requirements,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,'active',?,?)")->execute([$companyId,(int)$user['id'],$title,$professionalArea,trim((string)request('city')),trim((string)request('state')),max(0,localized_decimal(request('salary_min',0))),max(0,localized_decimal(request('salary_max',0))),trim((string)request('contract_type')),$mode,$description,trim((string)request('requirements')),now(),now()]);$jobId=(int)db()->lastInsertId();}
            if($isNew){try{$pq=db()->query("SELECT cp.user_id,cp.desired_job_type FROM career_profiles cp JOIN users u ON u.id=cp.user_id WHERE u.status='active' AND COALESCE(cp.desired_job_type,'')<>''");$probe=strtolower($title.' '.$professionalArea);foreach($pq->fetchAll() as $cp){$want=strtolower(trim((string)$cp['desired_job_type']));if($want!==''&&(strpos($probe,$want)!==false||($professionalArea!==''&&strpos($want,strtolower($professionalArea))!==false)))create_notification((int)$cp['user_id'],'💼 Nova vaga compatível',$title.' pode combinar com seu objetivo profissional.','info','dashboard.php?page=careers#jobs','job_post',$jobId);}}catch(Throwable $ignored){}}
            $_POST['return_anchor']='company-jobs';back_to('careers',$isNew?'Vaga publicada. Candidatos compatíveis podem ser notificados.':'Vaga atualizada.');break;

        case 'delete_job_post':
            if(!career_access_allowed((int)$user['id']))throw new RuntimeException('Acesso bloqueado.');$companyId=resolve_company_id_for_user($user);$id=(int)request('id');$q=db()->prepare('SELECT id FROM job_posts WHERE id=? AND company_id=?');$q->execute([$id,$companyId]);if(!$q->fetchColumn())throw new RuntimeException('Vaga não encontrada.');db()->prepare("UPDATE job_posts SET status='deleted',updated_at=? WHERE id=? AND company_id=?")->execute([now(),$id,$companyId]);$_POST['return_anchor']='company-jobs';back_to('careers','Vaga apagada.');break;

        case 'apply_job':
            if(!career_access_allowed((int)$user['id']))throw new RuntimeException('Regularize sua mensalidade.');$jobId=(int)request('job_id');$resumeId=(int)request('resume_id');
            $q=db()->prepare("SELECT j.*,c.owner_user_id,c.name company_name FROM job_posts j JOIN companies c ON c.id=j.company_id WHERE j.id=? AND j.status='active'");$q->execute([$jobId]);$job=$q->fetch();if(!$job)throw new RuntimeException('Vaga indisponível.');
            $q=db()->prepare("SELECT id FROM resumes WHERE id=? AND user_id=? AND status='active'");$q->execute([$resumeId,(int)$user['id']]);if(!$q->fetchColumn())throw new RuntimeException('Currículo inválido.');
            $q=db()->prepare("SELECT a.created_at,j.title FROM job_applications a JOIN job_posts j ON j.id=a.job_id WHERE a.user_id=? AND j.company_id=? ORDER BY a.created_at DESC LIMIT 1");$q->execute([(int)$user['id'],(int)$job['company_id']]);$last=$q->fetch();if($last&&strtotime((string)$last['created_at'])>strtotime('-5 days')){$available=date('d/m/Y H:i',strtotime((string)$last['created_at'].' +5 days'));throw new RuntimeException('Aguarde 5 dias para enviar outra candidatura para esta empresa. Novo envio disponível em '.$available.'.');}
            try{db()->prepare("INSERT INTO job_applications(job_id,user_id,resume_id,status,created_at,updated_at) VALUES(?,?,?,'sent',?,?)")->execute([$jobId,(int)$user['id'],$resumeId,now(),now()]);}catch(Throwable $e){throw new RuntimeException('Você já se candidatou a esta vaga.');}
            if(!empty($job['owner_user_id']))create_notification((int)$job['owner_user_id'],'📥 Nova candidatura',$user['name'].' se candidatou à vaga '.$job['title'].'.','info','dashboard.php?page=careers#company-applications','job_application',$jobId);$_POST['return_anchor']='jobs';back_to('careers','Currículo enviado para '.$job['company_name'].'.');break;

        case 'toggle_job_favorite':
            $jobId=(int)request('job_id');$q=db()->prepare('SELECT 1 FROM job_favorites WHERE user_id=? AND job_id=?');$q->execute([(int)$user['id'],$jobId]);if($q->fetchColumn())db()->prepare('DELETE FROM job_favorites WHERE user_id=? AND job_id=?')->execute([(int)$user['id'],$jobId]);else db()->prepare('INSERT OR IGNORE INTO job_favorites(user_id,job_id,created_at) VALUES(?,?,?)')->execute([(int)$user['id'],$jobId,now()]);back_to('careers','Favoritos atualizados.');break;

        case 'toggle_candidate_favorite':
            if(!career_access_allowed((int)$user['id']))throw new RuntimeException('Acesso bloqueado.');$companyId=resolve_company_id_for_user($user);if($companyId<1||!in_array((string)$user['role'],['company','employee'],true))throw new RuntimeException('Somente empresas podem favoritar candidatos.');$resumeId=(int)request('resume_id');$q=db()->prepare("SELECT id FROM resumes WHERE id=? AND status='active' AND visibility IN ('public','registered')");$q->execute([$resumeId]);if(!$q->fetchColumn())throw new RuntimeException('Currículo indisponível.');$q=db()->prepare('SELECT 1 FROM candidate_favorites WHERE company_id=? AND resume_id=?');$q->execute([$companyId,$resumeId]);if($q->fetchColumn())db()->prepare('DELETE FROM candidate_favorites WHERE company_id=? AND resume_id=?')->execute([$companyId,$resumeId]);else db()->prepare('INSERT OR IGNORE INTO candidate_favorites(company_id,resume_id,created_by,created_at) VALUES(?,?,?,?)')->execute([$companyId,$resumeId,(int)$user['id'],now()]);back_to('careers','Candidatos favoritos atualizados.');break;

        case 'update_job_application':
            if(!career_access_allowed((int)$user['id']))throw new RuntimeException('Acesso bloqueado.');$id=(int)request('id');$companyId=resolve_company_id_for_user($user);$q=db()->prepare('SELECT a.*,j.company_id,j.title FROM job_applications a JOIN job_posts j ON j.id=a.job_id WHERE a.id=?');$q->execute([$id]);$a=$q->fetch();if(!$a||$companyId!==(int)$a['company_id']||!in_array((string)$user['role'],['company','employee'],true))throw new RuntimeException('Sem permissão.');$status=(string)request('status');if(!in_array($status,['sent','analysis','interview','approved','rejected'],true))throw new RuntimeException('Status inválido.');$interview=str_replace('T',' ',trim((string)request('interview_at')));db()->prepare('UPDATE job_applications SET status=?,company_notes=?,interview_at=?,updated_at=? WHERE id=?')->execute([$status,trim((string)request('company_notes')),$interview?:null,now(),$id]);$msg='Sua candidatura para '.$a['title'].' foi atualizada para '.status_label($status).'.';if($status==='interview'&&$interview)$msg.=' Entrevista: '.date('d/m/Y H:i',strtotime($interview)).'.';create_notification((int)$a['user_id'],'💼 Candidatura atualizada',$msg,'info','dashboard.php?page=careers','job_application',$id);back_to('careers','Candidato atualizado e notificado.');break;

        case 'save_pet_booking_setting':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);$allowed=is_super_admin()||((string)$user['role']==='company'&&$linked===$companyId)||((string)$user['role']==='employee'&&$linked===$companyId&&company_member_access_level($user)==='admin');
            if(!$allowed||!company_is_petcare($companyId))throw new RuntimeException('Sem permissão para alterar a agenda veterinária.');
            $opening=substr(trim((string)request('pet_opening_time','08:00')),0,5);$closing=substr(trim((string)request('pet_closing_time','18:00')),0,5);
            if(!preg_match('/^\d{2}:\d{2}$/',$opening)||!preg_match('/^\d{2}:\d{2}$/',$closing)||strtotime('2000-01-01 '.$opening)>=strtotime('2000-01-01 '.$closing))throw new RuntimeException('Informe um horário de abertura e encerramento válidos.');
            db()->prepare('UPDATE companies SET pet_booking_enabled=?,pet_opening_time=?,pet_closing_time=?,updated_at=? WHERE id=?')->execute([(int)request('pet_booking_enabled',0),$opening,$closing,now(),$companyId]);$_POST['return_anchor']='agenda-settings';back_to('petcare','Agenda e horário de funcionamento atualizados.');break;

        case 'save_pet_booking_slot':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);$allowed=is_super_admin()||((string)$user['role']==='company'&&$linked===$companyId)||((string)$user['role']==='employee'&&$linked===$companyId&&company_member_access_level($user)==='admin');
            if(!$allowed||!company_is_petcare($companyId))throw new RuntimeException('Sem permissão para liberar horários desta clínica.');
            $date=trim((string)request('slot_date'));$time=substr(trim((string)request('slot_time')),0,5);
            if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)||!preg_match('/^\d{2}:\d{2}$/',$time)||strtotime($date.' '.$time)<=time())throw new RuntimeException('Informe uma data e um horário futuros válidos.');
            $hq=db()->prepare('SELECT COALESCE(NULLIF(pet_opening_time,""),"08:00") opening,COALESCE(NULLIF(pet_closing_time,""),"18:00") closing FROM companies WHERE id=?');$hq->execute([$companyId]);$hours=$hq->fetch()?:['opening'=>'08:00','closing'=>'18:00'];if($time<substr((string)$hours['opening'],0,5)||$time>=substr((string)$hours['closing'],0,5))throw new RuntimeException('O horário precisa ficar dentro do funcionamento da clínica ('.substr((string)$hours['opening'],0,5).' às '.substr((string)$hours['closing'],0,5).').');
            $type=trim((string)request('service_type','consultation'));$types=['consultation','return','vaccine','bath','grooming','hygienic_grooming','boarding','exam','surgery','other'];if(!in_array($type,$types,true))$type='other';
            $service=trim((string)request('service_name'));if($service==='')$service=pet_service_label($type);$amount=max(0,localized_decimal(request('amount',0)));$capacity=max(1,min(50,(int)request('capacity',1)));
            $q=db()->prepare('SELECT id FROM pet_booking_slots WHERE company_id=? AND slot_date=? AND slot_time=? AND service_type=? LIMIT 1');$q->execute([$companyId,$date,$time,$type]);$slotId=(int)$q->fetchColumn();
            if($slotId){db()->prepare("UPDATE pet_booking_slots SET service_name=?,amount=?,capacity=?,status='active',updated_at=? WHERE id=?")->execute([$service,$amount,$capacity,now(),$slotId]);}
            else{db()->prepare("INSERT INTO pet_booking_slots(company_id,slot_date,slot_time,service_type,service_name,amount,capacity,status,created_by,created_at,updated_at) VALUES(?,?,?,?,?,?,?,'active',?,?,?)")->execute([$companyId,$date,$time,$type,$service,$amount,$capacity,(int)$user['id'],now(),now()]);}
            $_POST['return_anchor']='booking-slots';back_to('petcare',$slotId?'Horário atualizado.':'Horário liberado para os tutores.');break;

        case 'bulk_save_pet_booking_slots':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);$allowed=is_super_admin()||((string)$user['role']==='company'&&$linked===$companyId)||((string)$user['role']==='employee'&&$linked===$companyId&&company_member_access_level($user)==='admin');
            if(!$allowed||!company_is_petcare($companyId))throw new RuntimeException('Sem permissão para liberar vários horários desta clínica.');
            $startDate=trim((string)request('date_start'));$endDate=trim((string)request('date_end',$startDate));$startTime=substr(trim((string)request('start_time')),0,5);$endTime=substr(trim((string)request('end_time')),0,5);
            if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$startDate)||!preg_match('/^\d{4}-\d{2}-\d{2}$/',$endDate)||!preg_match('/^\d{2}:\d{2}$/',$startTime)||!preg_match('/^\d{2}:\d{2}$/',$endTime))throw new RuntimeException('Informe período e horários válidos.');
            $startTs=strtotime($startDate);$endTs=strtotime($endDate);if($startTs===false||$endTs===false||$endTs<$startTs||$startTs<strtotime(date('Y-m-d'))||($endTs-$startTs)>60*86400)throw new RuntimeException('O período deve ser futuro e ter no máximo 60 dias.');
            $toMin=static function(string $v): int {$p=array_map('intval',explode(':',$v));return ($p[0]??0)*60+($p[1]??0);};$fromMin=$toMin($startTime);$untilMin=$toMin($endTime);if($fromMin>=$untilMin)throw new RuntimeException('O horário final precisa ser depois do horário inicial.');
            $hq=db()->prepare('SELECT COALESCE(NULLIF(pet_opening_time,""),"08:00") opening,COALESCE(NULLIF(pet_closing_time,""),"18:00") closing FROM companies WHERE id=?');$hq->execute([$companyId]);$hours=$hq->fetch()?:['opening'=>'08:00','closing'=>'18:00'];$openMin=$toMin(substr((string)$hours['opening'],0,5));$closeMin=$toMin(substr((string)$hours['closing'],0,5));if($fromMin<$openMin||$untilMin>$closeMin)throw new RuntimeException('A liberação em lote precisa ficar dentro do funcionamento da clínica ('.substr((string)$hours['opening'],0,5).' às '.substr((string)$hours['closing'],0,5).').');
            $interval=max(10,min(240,(int)request('interval_minutes',30)));$type=trim((string)request('service_type','consultation'));$types=['consultation','return','vaccine','bath','grooming','hygienic_grooming','boarding','exam','surgery','other'];if(!in_array($type,$types,true))$type='other';
            $service=trim((string)request('service_name'));if($service==='')$service=pet_service_label($type);$amount=max(0,localized_decimal(request('amount',0)));$capacity=max(1,min(50,(int)request('capacity',1)));
            $weekdays=$_POST['weekdays']??[];if(!is_array($weekdays))$weekdays=[];$weekdays=array_values(array_unique(array_filter(array_map('intval',$weekdays),static fn($d)=>$d>=0&&$d<=6)));if(!$weekdays)$weekdays=[0,1,2,3,4,5,6];
            $upsert=db()->prepare("INSERT INTO pet_booking_slots(company_id,slot_date,slot_time,service_type,service_name,amount,capacity,status,created_by,created_at,updated_at) VALUES(?,?,?,?,?,?,?,'active',?,?,?) ON CONFLICT(company_id,slot_date,slot_time,service_type) DO UPDATE SET service_name=excluded.service_name,amount=excluded.amount,capacity=excluded.capacity,status='active',updated_at=excluded.updated_at");
            $count=0;db()->beginTransaction();try{for($day=$startTs;$day<=$endTs;$day+=86400){$date=date('Y-m-d',$day);if(!in_array((int)date('w',$day),$weekdays,true))continue;for($m=$fromMin;$m<$untilMin;$m+=$interval){$time=sprintf('%02d:%02d',intdiv($m,60),$m%60);if(strtotime($date.' '.$time)<=time())continue;$upsert->execute([$companyId,$date,$time,$type,$service,$amount,$capacity,(int)$user['id'],now(),now()]);$count++;}}db()->commit();}catch(Throwable $e){if(db()->inTransaction())db()->rollBack();throw $e;}
            $_POST['return_anchor']='booking-slots';back_to('petcare',$count>0?$count.' horário(s) liberado(s)/atualizado(s).':'Nenhum horário futuro foi gerado com os filtros informados.');break;

        case 'delete_pet_booking_slot':
            $id=(int)request('id');$q=db()->prepare('SELECT * FROM pet_booking_slots WHERE id=?');$q->execute([$id]);$slot=$q->fetch();if(!$slot)throw new RuntimeException('Horário não encontrado.');$companyId=(int)$slot['company_id'];$linked=resolve_company_id_for_user($user);$allowed=is_super_admin()||((string)$user['role']==='company'&&$linked===$companyId)||((string)$user['role']==='employee'&&$linked===$companyId&&company_member_access_level($user)==='admin');if(!$allowed)throw new RuntimeException('Sem permissão para remover este horário.');
            $q=db()->prepare("SELECT COUNT(*) FROM pet_appointments WHERE slot_id=? AND status<>'cancelled'");$q->execute([$id]);if((int)$q->fetchColumn()>0)throw new RuntimeException('Este horário já possui agendamento. Cancele ou mova o atendimento antes de removê-lo.');
            db()->prepare("UPDATE pet_booking_slots SET status='deleted',updated_at=? WHERE id=?")->execute([now(),$id]);$_POST['return_anchor']='booking-slots';back_to('petcare','Horário removido.');break;

        case 'save_pet_client':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);if($companyId<1||!company_is_petcare($companyId)||(!is_super_admin()&&($linked!==$companyId||!in_array((string)$user['role'],['company','employee'],true))))throw new RuntimeException('Sem permissão para cadastrar tutor nesta empresa.');
            $clientId=(int)request('client_id',0);$existing=null;if($clientId>0){$q=db()->prepare('SELECT * FROM pet_clients WHERE id=? AND company_id=? LIMIT 1');$q->execute([$clientId,$companyId]);$existing=$q->fetch();if(!$existing)throw new RuntimeException('Tutor não encontrado nesta clínica.');}
            $platformToken=trim((string)request('platform_public_id'));$platformUserId=$existing?(int)($existing['platform_user_id']??0):null;$platformUser=null;
            if($platformToken!==''){$q=db()->prepare("SELECT * FROM users WHERE upper(public_id)=upper(?) OR id=? LIMIT 1");$q->execute([$platformToken,(int)$platformToken]);$platformUser=$q->fetch();if(!$platformUser)throw new RuntimeException('ID da plataforma não encontrado.');$platformUserId=(int)$platformUser['id'];$dq=db()->prepare('SELECT id FROM pet_clients WHERE company_id=? AND platform_user_id=? AND id<>? LIMIT 1');$dq->execute([$companyId,$platformUserId,$clientId]);if($dq->fetchColumn())throw new RuntimeException('Este cliente já está cadastrado como tutor nesta clínica.');}
            $name=trim((string)request('name'));if($name===''&&$platformUser)$name=(string)$platformUser['name'];if($name==='')throw new RuntimeException('Informe o nome do tutor.');
            $document=trim((string)request('document'));$phone=only_digits((string)request('phone'));$whatsapp=only_digits((string)request('whatsapp'));$email=trim((string)request('email'));$address=trim((string)request('address'));$city=trim((string)request('city'));$state=strtoupper(substr(trim((string)request('state')),0,2));$notes=trim((string)request('notes'));$photo=upload_image('photo','petcare/tutores');
            if($platformUser){$document=$document?:($platformUser['document']??'');$phone=$phone?:only_digits((string)($platformUser['phone']??''));$whatsapp=$whatsapp?:$phone;$email=$email?:($platformUser['email']??'');$address=$address?:trim((string)($platformUser['home_address']??'').(trim((string)($platformUser['home_number']??''))!==''?', '.trim((string)$platformUser['home_number']):''));$city=$city?:($platformUser['city']??'');$state=$state?:strtoupper(substr((string)($platformUser['state']??''),0,2));}
            $notify=false;
            if($clientId>0){$notify=$platformUserId>0&&(int)($existing['platform_user_id']??0)!==$platformUserId;db()->prepare('UPDATE pet_clients SET platform_user_id=?,name=?,document=?,photo=CASE WHEN ?<>"" THEN ? ELSE photo END,phone=?,whatsapp=?,email=?,address=?,city=?,state=?,notes=?,status="active",updated_at=? WHERE id=? AND company_id=?')->execute([$platformUserId?:null,$name,$document,$photo,$photo,$phone,$whatsapp,$email,$address,$city,$state,$notes,now(),$clientId,$companyId]);$id=$clientId;}
            elseif($platformUserId){$q=db()->prepare('SELECT id FROM pet_clients WHERE company_id=? AND platform_user_id=? LIMIT 1');$q->execute([$companyId,$platformUserId]);$id=(int)$q->fetchColumn();if($id){db()->prepare('UPDATE pet_clients SET name=?,document=?,photo=CASE WHEN ?<>"" THEN ? ELSE photo END,phone=?,whatsapp=?,email=?,address=?,city=?,state=?,notes=?,status="active",updated_at=? WHERE id=?')->execute([$name,$document,$photo,$photo,$phone,$whatsapp,$email,$address,$city,$state,$notes,now(),$id]);}else{db()->prepare('INSERT INTO pet_clients(company_id,platform_user_id,name,document,photo,phone,whatsapp,email,address,city,state,notes,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,"active",?,?)')->execute([$companyId,$platformUserId,$name,$document,$photo,$phone,$whatsapp,$email,$address,$city,$state,$notes,now(),now()]);$id=(int)db()->lastInsertId();}$notify=true;}
            else{db()->prepare('INSERT INTO pet_clients(company_id,platform_user_id,name,document,photo,phone,whatsapp,email,address,city,state,notes,status,created_at,updated_at) VALUES(?,NULL,?,?,?,?,?,?,?,?,?,?,"active",?,?)')->execute([$companyId,$name,$document,$photo,$phone,$whatsapp,$email,$address,$city,$state,$notes,now(),now()]);$id=(int)db()->lastInsertId();}
            if($notify&&$platformUserId)create_notification($platformUserId,'🐾 Vínculo veterinário','Você foi vinculado a uma clínica veterinária.','info','dashboard.php?page=pet_tutor');
            $_POST['return_anchor']='clients';back_to('petcare',$clientId>0?'Tutor atualizado com sucesso.':'Tutor salvo com sucesso.');break;

        case 'delete_pet_client':
            $companyId=(int)request('company_id');$clientId=(int)request('client_id');$linked=resolve_company_id_for_user($user);if($companyId<1||!company_is_petcare($companyId)||(!is_super_admin()&&($linked!==$companyId||!in_array((string)$user['role'],['company','employee'],true))))throw new RuntimeException('Sem permissão para apagar tutor nesta empresa.');
            $q=db()->prepare('SELECT * FROM pet_clients WHERE id=? AND company_id=? AND status="active" LIMIT 1');$q->execute([$clientId,$companyId]);$client=$q->fetch();if(!$client)throw new RuntimeException('Tutor não encontrado.');
            db()->beginTransaction();try{db()->prepare('UPDATE pet_clients SET status="deleted",updated_at=? WHERE id=?')->execute([now(),$clientId]);db()->prepare('UPDATE pets SET status="inactive",updated_at=? WHERE client_id=? AND company_id=?')->execute([now(),$clientId,$companyId]);db()->prepare("UPDATE pet_appointments SET status='cancelled',updated_at=? WHERE company_id=? AND pet_id IN (SELECT id FROM pets WHERE client_id=?) AND status IN ('scheduled','confirmed')")->execute([now(),$companyId,$clientId]);db()->commit();}catch(Throwable $e){if(db()->inTransaction())db()->rollBack();throw $e;}
            $_POST['return_anchor']='clients';back_to('petcare','Tutor removido da clínica. O histórico anterior foi preservado.');break;

        case 'save_pet':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);if($companyId<1||!company_is_petcare($companyId)||(!is_super_admin()&&($linked!==$companyId||!in_array((string)$user['role'],['company','employee'],true))))throw new RuntimeException('Sem permissão.');$clientId=(int)request('client_id');$q=db()->prepare('SELECT id FROM pet_clients WHERE id=? AND company_id=? AND status="active"');$q->execute([$clientId,$companyId]);if(!$q->fetchColumn())throw new RuntimeException('Tutor inválido.');$name=trim((string)request('name'));if($name==='')throw new RuntimeException('Informe o nome do pet.');$photo=upload_image('photo','petcare/pets');$weight=max(0,localized_decimal(request('weight',0)));$token=create_pet_token();
            db()->prepare('INSERT INTO pets(company_id,client_id,public_token,name,photo,species,breed,sex,birth_date,weight,color,size,microchip,allergies,medications,diseases,notes,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,"active",?,?)')->execute([$companyId,$clientId,$token,$name,$photo,trim((string)request('species')),trim((string)request('breed')),trim((string)request('sex')),trim((string)request('birth_date')),$weight,trim((string)request('color')),trim((string)request('size')),trim((string)request('microchip')),trim((string)request('allergies')),trim((string)request('medications')),trim((string)request('diseases')),trim((string)request('notes')),now(),now()]);back_to('petcare','Pet cadastrado e carteira digital criada.');break;

        case 'save_pet_appointment':
            $companyId=(int)request('company_id');$petId=(int)request('pet_id');
            $q=db()->prepare('SELECT p.*,pc.platform_user_id FROM pets p JOIN pet_clients pc ON pc.id=p.client_id WHERE p.id=? AND p.company_id=? AND p.status="active" AND pc.status="active"');$q->execute([$petId,$companyId]);$pet=$q->fetch();if(!$pet)throw new RuntimeException('Pet inválido.');
            $linked=resolve_company_id_for_user($user);$manager=is_super_admin()||(in_array((string)$user['role'],['company','employee'],true)&&$linked===$companyId&&company_is_petcare($companyId));$tutor=((int)($pet['platform_user_id']??0)===(int)$user['id']);if(!$manager&&!$tutor)throw new RuntimeException('Sem permissão para agendar este pet.');
            $returnPetPage=in_array($returnPage,['petcare','pet_tutor'],true)?$returnPage:($tutor&&!$manager?'pet_tutor':'petcare');
            $slotId=null;$date='';$time='';$type='consultation';$service='';$amount=0;$method='';$professional='';
            if($tutor&&!$manager){
                $cq=db()->prepare('SELECT pet_booking_enabled FROM companies WHERE id=?');$cq->execute([$companyId]);if(!(int)$cq->fetchColumn())throw new RuntimeException('A clínica ainda não liberou o agendamento online para os pets.');
                $slotId=(int)request('slot_id');if($slotId<1)throw new RuntimeException('Escolha um dos horários liberados pela clínica.');
                $sq=db()->prepare("SELECT * FROM pet_booking_slots WHERE id=? AND company_id=? AND status='active' LIMIT 1");$sq->execute([$slotId,$companyId]);$slot=$sq->fetch();if(!$slot)throw new RuntimeException('Este horário não está mais disponível.');
                $date=(string)$slot['slot_date'];$time=substr((string)$slot['slot_time'],0,5);if(strtotime($date.' '.$time)<=time())throw new RuntimeException('Este horário já passou. Escolha outra vaga.');
                $type=(string)$slot['service_type'];$service=(string)($slot['service_name']?:pet_service_label($type));$amount=(float)$slot['amount'];
                $pdo=db();$started=false;try{if(!$pdo->inTransaction()){$pdo->beginTransaction();$started=true;}$check=$pdo->prepare("SELECT COUNT(*) FROM pet_appointments WHERE slot_id=? AND status<>'cancelled'");$check->execute([$slotId]);if((int)$check->fetchColumn()>=(int)$slot['capacity'])throw new RuntimeException('Este horário acabou de ficar sem vagas. Escolha outro.');$same=$pdo->prepare("SELECT COUNT(*) FROM pet_appointments WHERE slot_id=? AND pet_id=? AND status<>'cancelled'");$same->execute([$slotId,$petId]);if((int)$same->fetchColumn()>0)throw new RuntimeException('Este pet já está agendado neste horário.');$pdo->prepare('INSERT INTO pet_appointments(company_id,pet_id,slot_id,service_type,service_name,professional_name,appointment_date,appointment_time,amount,payment_method,payment_status,status,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,? ,"pending","scheduled",?,?,?)')->execute([$companyId,$petId,$slotId,$type,$service,$professional,$date,$time,$amount,$method,trim((string)request('notes')),now(),now()]);$id=(int)$pdo->lastInsertId();if($started)$pdo->commit();}catch(Throwable $e){if($started&&$pdo->inTransaction())$pdo->rollBack();throw $e;}
            }else{
                $date=trim((string)request('appointment_date'));$time=substr(trim((string)request('appointment_time')),0,5);if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)||!preg_match('/^\d{2}:\d{2}$/',$time))throw new RuntimeException('Informe data e horário válidos.');
                $type=trim((string)request('service_type','consultation'));$allowedTypes=['consultation','return','vaccine','bath','grooming','hygienic_grooming','boarding','exam','surgery','other'];if(!in_array($type,$allowedTypes,true))$type='other';$service=trim((string)request('service_name'));$amount=max(0,localized_decimal(request('amount',0)));$method=trim((string)request('payment_method'));$professional=trim((string)request('professional_name'));
                db()->prepare('INSERT INTO pet_appointments(company_id,pet_id,slot_id,service_type,service_name,professional_name,appointment_date,appointment_time,amount,payment_method,payment_status,status,notes,created_at,updated_at) VALUES(?,?,NULL,?,?,?,?,?,?,?,"pending","scheduled",?,?,?)')->execute([$companyId,$petId,$type,$service,$professional,$date,$time,$amount,$method,trim((string)request('notes')),now(),now()]);$id=(int)db()->lastInsertId();
            }
            if($tutor&&!$manager){$cq=db()->prepare('SELECT owner_user_id,name FROM companies WHERE id=?');$cq->execute([$companyId]);$c=$cq->fetch();if(!empty($c['owner_user_id']))create_notification((int)$c['owner_user_id'],'🐾 Novo agendamento','Foi agendado '.($service?:pet_service_label($type)).' para '.$pet['name'].' em '.date('d/m/Y',strtotime($date)).' às '.$time.'.','info','dashboard.php?page=petcare#agenda','pet_appointment',$id);}
            $_POST['return_anchor']='agenda';back_to($returnPetPage,$manager?'Agendamento salvo.':'Agendamento confirmado.');break;

        case 'update_pet_appointment':
            $id=(int)request('id');$q=db()->prepare('SELECT a.*,pc.platform_user_id,p.name pet_name FROM pet_appointments a JOIN pets p ON p.id=a.pet_id JOIN pet_clients pc ON pc.id=p.client_id WHERE a.id=?');$q->execute([$id]);$a=$q->fetch();if(!$a)throw new RuntimeException('Agendamento não encontrado.');
            $linked=resolve_company_id_for_user($user);$manager=is_super_admin()||(in_array((string)$user['role'],['company','employee'],true)&&$linked===(int)$a['company_id']&&company_is_petcare((int)$a['company_id']));$tutor=((int)($a['platform_user_id']??0)===(int)$user['id']);if(!$manager&&!$tutor)throw new RuntimeException('Sem permissão.');
            $status=trim((string)request('status'));if($tutor&&!$manager){if($status!=='cancelled'||!in_array((string)$a['status'],['scheduled','confirmed'],true))throw new RuntimeException('O tutor só pode cancelar um agendamento que ainda não iniciou.');}elseif(!in_array($status,['scheduled','confirmed','in_service','completed','cancelled'],true))throw new RuntimeException('Status inválido.');
            $checkin=$status==='in_service'?(string)($a['checkin_at']?:now()):$a['checkin_at'];$checkout=$status==='completed'?(string)($a['checkout_at']?:now()):$a['checkout_at'];db()->prepare('UPDATE pet_appointments SET status=?,checkin_at=?,checkout_at=?,updated_at=? WHERE id=?')->execute([$status,$checkin,$checkout,now(),$id]);
            if($manager&&!empty($a['platform_user_id']))create_notification((int)$a['platform_user_id'],'🐾 Agendamento atualizado',$a['pet_name'].' — '.status_label($status),'info','dashboard.php?page=pet_tutor#agenda','pet_appointment',$id);
            $returnPetPage=in_array($returnPage,['petcare','pet_tutor'],true)?$returnPage:($tutor&&!$manager?'pet_tutor':'petcare');$_POST['return_anchor']='agenda';back_to($returnPetPage,$status==='cancelled'?'Agendamento cancelado.':'Agendamento atualizado.');break;

        case 'save_pet_medical_record':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);if(!is_super_admin()&&($linked!==$companyId||!in_array((string)$user['role'],['company','employee'],true)))throw new RuntimeException('Sem permissão.');$petId=(int)request('pet_id');$q=db()->prepare('SELECT id FROM pets WHERE id=? AND company_id=?');$q->execute([$petId,$companyId]);if(!$q->fetchColumn())throw new RuntimeException('Pet inválido.');db()->prepare('INSERT INTO pet_medical_records(company_id,pet_id,professional_name,complaint,anamnesis,weight,temperature,diagnosis,treatment,medications,allergies,diseases,surgeries,observations,evolution,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$petId,trim((string)request('professional_name')),trim((string)request('complaint')),trim((string)request('anamnesis')),localized_decimal(request('weight',0)),localized_decimal(request('temperature',0)),trim((string)request('diagnosis')),trim((string)request('treatment')),trim((string)request('medications')),trim((string)request('allergies')),trim((string)request('diseases')),trim((string)request('surgeries')),trim((string)request('observations')),trim((string)request('evolution')),now(),now()]);back_to('petcare','Prontuário veterinário salvo.');break;

        case 'save_pet_vaccine':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);if(!is_super_admin()&&($linked!==$companyId||!in_array((string)$user['role'],['company','employee'],true)))throw new RuntimeException('Sem permissão.');$petId=(int)request('pet_id');must_pet_belong_to_company($petId,$companyId);$name=trim((string)request('vaccine_name'));if($name==='')throw new RuntimeException('Informe a vacina.');db()->prepare('INSERT INTO pet_vaccines(company_id,pet_id,vaccine_name,applied_at,dose,batch,professional_name,next_dose_at,expires_at,notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$petId,$name,trim((string)request('applied_at')),trim((string)request('dose')),trim((string)request('batch')),trim((string)request('professional_name')),trim((string)request('next_dose_at')),trim((string)request('expires_at')),trim((string)request('notes')),now(),now()]);back_to('petcare','Vacina registrada.');break;

        case 'save_pet_exam':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);if(!is_super_admin()&&($linked!==$companyId||!in_array((string)$user['role'],['company','employee'],true)))throw new RuntimeException('Sem permissão.');$petId=(int)request('pet_id');must_pet_belong_to_company($petId,$companyId);$type=trim((string)request('exam_type'));if($type==='')throw new RuntimeException('Informe o tipo de exame.');$file=upload_document('exam_file','petcare/exames');$result=trim((string)request('result_text'));$status=($result!==''||$file!=='')?'completed':'requested';db()->prepare('INSERT INTO pet_exams(company_id,pet_id,exam_type,requested_at,performed_at,result_text,report_text,file_path,professional_name,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$petId,$type,date('Y-m-d'),$status==='completed'?date('Y-m-d'):null,$result,trim((string)request('report_text')),$file,trim((string)request('professional_name')),$status,now(),now()]);back_to('petcare','Exame salvo.');break;

        case 'save_pet_prescription':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);if(!is_super_admin()&&($linked!==$companyId||!in_array((string)$user['role'],['company','employee'],true)))throw new RuntimeException('Sem permissão.');$petId=(int)request('pet_id');must_pet_belong_to_company($petId,$companyId);$medicine=trim((string)request('medicine_name'));if($medicine==='')throw new RuntimeException('Informe o medicamento.');db()->prepare('INSERT INTO pet_prescriptions(company_id,pet_id,professional_name,medicine_name,dosage,frequency,duration_text,orientations,prescribed_at,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$petId,trim((string)request('professional_name')),$medicine,trim((string)request('dosage')),trim((string)request('frequency')),trim((string)request('duration_text')),trim((string)request('orientations')),now(),now(),now()]);back_to('petcare','Receita veterinária emitida.');break;

        case 'save_pet_hospitalization':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);if(!is_super_admin()&&($linked!==$companyId||!in_array((string)$user['role'],['company','employee'],true)))throw new RuntimeException('Sem permissão.');$petId=(int)request('pet_id');must_pet_belong_to_company($petId,$companyId);$admitted=str_replace('T',' ',trim((string)request('admitted_at')));if($admitted==='')$admitted=now();$care=trim((string)request('care_notes'));db()->prepare('INSERT INTO pet_hospitalizations(company_id,pet_id,box_name,admitted_at,reason,professional_name,medications,feeding,vital_signs,evolution,observations,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,"admitted",?,?)')->execute([$companyId,$petId,trim((string)request('box_name')),$admitted,trim((string)request('reason')),trim((string)request('professional_name')),$care,$care,$care,'',$care,now(),now()]);back_to('petcare','Internação registrada.');break;

        case 'save_pet_product_details':
            $companyId=(int)request('company_id');$linked=resolve_company_id_for_user($user);if(!is_super_admin()&&($linked!==$companyId||!in_array((string)$user['role'],['company','employee'],true)))throw new RuntimeException('Sem permissão.');$productId=(int)request('product_id');$q=db()->prepare('SELECT id FROM products WHERE id=? AND company_id=?');$q->execute([$productId,$companyId]);if(!$q->fetchColumn())throw new RuntimeException('Produto não encontrado.');db()->prepare('UPDATE products SET brand=?,stock_min=?,batch=?,expires_at=?,updated_at=? WHERE id=? AND company_id=?')->execute([trim((string)request('brand')),max(0,localized_decimal(request('stock_min',0))),trim((string)request('batch')),trim((string)request('expires_at')),now(),$productId,$companyId]);back_to('petcare','Dados de estoque atualizados.');break;

        case 'save_pet_stock_movement':
            $companyId=(int)request('company_id');must_manage_pet_company($companyId);$productId=(int)request('product_id');$q=db()->prepare('SELECT * FROM products WHERE id=? AND company_id=?');$q->execute([$productId,$companyId]);$product=$q->fetch();if(!$product)throw new RuntimeException('Produto não encontrado.');$type=(string)request('movement_type');if(!in_array($type,['entry','exit','adjustment_add','adjustment_sub'],true))throw new RuntimeException('Movimentação inválida.');$qty=max(0,localized_decimal(request('qty',0)));if($qty<=0)throw new RuntimeException('Informe uma quantidade maior que zero.');$delta=in_array($type,['entry','adjustment_add'],true)?$qty:-$qty;$newStock=(float)$product['stock']+$delta;if($newStock<0)throw new RuntimeException('A saída é maior que o estoque disponível.');db()->beginTransaction();db()->prepare('UPDATE products SET stock=?,updated_at=? WHERE id=? AND company_id=?')->execute([$newStock,now(),$productId,$companyId]);db()->prepare('INSERT INTO pet_stock_movements(company_id,product_id,movement_type,qty,reason,created_by,created_at) VALUES(?,?,?,?,?,?,?)')->execute([$companyId,$productId,$type,$qty,trim((string)request('reason')),(int)$user['id'],now()]);db()->commit();back_to('petcare','Movimentação registrada. Estoque atual: '.$newStock.'.');break;

        case 'save_pet_supplier':
            $companyId=(int)request('company_id');must_manage_pet_company($companyId);$name=trim((string)request('name'));if($name==='')throw new RuntimeException('Informe o fornecedor.');db()->prepare("INSERT INTO pet_suppliers(company_id,name,document,phone,email,products_notes,status,created_at,updated_at) VALUES(?,?,?,?,?,?,'active',?,?)")->execute([$companyId,$name,trim((string)request('document')),only_digits((string)request('phone')),trim((string)request('email')),trim((string)request('products_notes')),now(),now()]);back_to('petcare','Fornecedor cadastrado.');break;

        case 'save_car_wash_service':
            $companyId=(int)request('company_id',0);if(!is_admin())$companyId=resolve_company_id_for_user($user);
            if($companyId<1||!company_is_car_wash($companyId))throw new RuntimeException('Lava-Jato não identificado.');
            if(!is_admin()&&resolve_company_id_for_user($user)!==$companyId)throw new RuntimeException('Sem permissão.');
            $id=(int)request('id',0);$name=trim((string)request('name'));if($name==='')throw new RuntimeException('Informe o tipo de lavagem.');
            $description=trim((string)request('description'));$price=max(0,localized_decimal(request('price',0)));$duration=max(10,min(480,(int)request('duration_minutes',60)));$status=request('status','active');if(!in_array($status,['active','inactive'],true))$status='active';$sort=max(0,(int)request('sort_order',0));
            if($id){db()->prepare('UPDATE car_wash_services SET name=?,description=?,price=?,duration_minutes=?,status=?,sort_order=?,updated_at=? WHERE id=? AND company_id=?')->execute([$name,$description,$price,$duration,$status,$sort,now(),$id,$companyId]);}
            else{db()->prepare('INSERT INTO car_wash_services(company_id,name,description,price,duration_minutes,status,sort_order,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)')->execute([$companyId,$name,$description,$price,$duration,$status,$sort,now(),now()]);$id=(int)db()->lastInsertId();}
            log_activity('save_car_wash_service','car_wash_services',$id,$name.' • '.money($price));back_to('car_wash','Tipo de lavagem e preço salvos.');break;

        case 'delete_car_wash_service':
            $id=(int)request('id');$q=db()->prepare('SELECT company_id FROM car_wash_services WHERE id=?');$q->execute([$id]);$companyId=(int)$q->fetchColumn();if($companyId<1)throw new RuntimeException('Tipo de lavagem não encontrado.');
            if(!is_admin()&&resolve_company_id_for_user($user)!==$companyId)throw new RuntimeException('Sem permissão.');
            db()->prepare('DELETE FROM car_wash_services WHERE id=?')->execute([$id]);back_to('car_wash','Tipo de lavagem apagado.');break;

        case 'save_car_wash_hours':
            $companyId=(int)request('company_id',0);
            if(!is_admin()){
                $companyId=resolve_company_id_for_user($user);
                if($companyId<1 || !company_is_car_wash($companyId)) throw new RuntimeException('Somente o Lava-Jato pode configurar o expediente.');
            }
            if($companyId<1 || !company_is_car_wash($companyId)) throw new RuntimeException('Lava-Jato não identificado.');
            $weekday=max(0,min(6,(int)request('weekday')));
            $opening=substr(trim((string)request('opening_time')),0,5);
            $closing=substr(trim((string)request('closing_time')),0,5);
            $interval=max(15,min(240,(int)request('interval_minutes',60)));
            $capacity=max(1,min(100,(int)request('capacity',1)));
            $lunchStart=substr(trim((string)request('lunch_start_time')),0,5);
            $lunchEnd=substr(trim((string)request('lunch_end_time')),0,5);
            if($opening==='' && $closing===''){
                db()->prepare('DELETE FROM car_wash_hours WHERE company_id=? AND weekday=?')->execute([$companyId,$weekday]);
                back_to('car_wash','Expediente automático removido deste dia. Os horários manuais continuam válidos.');
            }
            if(car_wash_minutes($opening)===null || car_wash_minutes($closing)===null) throw new RuntimeException('Preencha início e final do dia, ou deixe os dois em branco.');
            if(car_wash_minutes($closing)<=car_wash_minutes($opening)) throw new RuntimeException('O horário final deve ser posterior ao horário inicial.');
            if(($lunchStart===''&&$lunchEnd!=='')||($lunchStart!==''&&$lunchEnd===''))throw new RuntimeException('Informe início e fim do almoço, ou deixe os dois vazios.');
            if($lunchStart!==''&&($ls=car_wash_minutes($lunchStart))!==null&&($le=car_wash_minutes($lunchEnd))!==null){
                if($le<=$ls)throw new RuntimeException('O final do almoço deve ser posterior ao início.');
                if($ls<car_wash_minutes($opening)||$le>car_wash_minutes($closing))throw new RuntimeException('O almoço precisa ficar dentro do expediente.');
            }elseif($lunchStart!==''||$lunchEnd!=='')throw new RuntimeException('Horário de almoço inválido.');
            db()->prepare("INSERT INTO car_wash_hours(company_id,weekday,opening_time,closing_time,interval_minutes,capacity,lunch_start_time,lunch_end_time,status,updated_at) VALUES(?,?,?,?,?,?,?,?,'active',?) ON CONFLICT(company_id,weekday) DO UPDATE SET opening_time=excluded.opening_time,closing_time=excluded.closing_time,interval_minutes=excluded.interval_minutes,capacity=excluded.capacity,lunch_start_time=excluded.lunch_start_time,lunch_end_time=excluded.lunch_end_time,status='active',updated_at=excluded.updated_at")
                ->execute([$companyId,$weekday,$opening,$closing,$interval,$capacity,$lunchStart?:null,$lunchEnd?:null,now()]);
            back_to('car_wash','Expediente automático salvo. As vagas já ficam visíveis para os usuários.');
            break;

        case 'save_car_wash_slot':
            $companyId=(int)request('company_id',0);
            if(!is_admin()){
                $companyId=resolve_company_id_for_user($user);
                if($companyId<1 || !company_is_car_wash($companyId)) throw new RuntimeException('Somente o Lava-Jato pode configurar as vagas.');
            }
            if($companyId<1 || !company_is_car_wash($companyId)) throw new RuntimeException('Lava-Jato não identificado.');
            $weekday=max(0,min(6,(int)request('weekday')));$time=substr(trim((string)request('slot_time')),0,5);$capacity=max(1,(int)request('capacity',1));
            if(car_wash_minutes($time)===null) throw new RuntimeException('Horário inválido.');
            db()->prepare("INSERT INTO car_wash_slots(company_id,weekday,slot_time,capacity,status,created_at,updated_at) VALUES(?,?,?,?, 'active',?,?) ON CONFLICT(company_id,weekday,slot_time) DO UPDATE SET capacity=excluded.capacity,status='active',updated_at=excluded.updated_at")
                ->execute([$companyId,$weekday,$time,$capacity,now(),now()]);
            back_to('car_wash','Horário manual do Lava-Jato salvo.');
            break;

        case 'delete_car_wash_slot':
            $id=(int)request('id');$q=db()->prepare('SELECT company_id FROM car_wash_slots WHERE id=?');$q->execute([$id]);$companyId=(int)$q->fetchColumn();
            if(!$companyId) throw new RuntimeException('Horário não encontrado.');
            if(!is_admin() && resolve_company_id_for_user($user)!==$companyId) throw new RuntimeException('Sem permissão.');
            db()->prepare('DELETE FROM car_wash_slots WHERE id=?')->execute([$id]);
            back_to('car_wash','Horário removido.');
            break;

        case 'toggle_car_wash_block':
            $companyId=(int)request('company_id');if(!is_admin())$companyId=resolve_company_id_for_user($user);
            if($companyId<1 || !company_is_car_wash($companyId)) throw new RuntimeException('Lava-Jato não identificado.');
            $date=request('block_date');$time=request('slot_time');
            $q=db()->prepare('SELECT id FROM car_wash_blocks WHERE company_id=? AND block_date=? AND slot_time=?');$q->execute([$companyId,$date,$time]);$id=(int)$q->fetchColumn();
            if($id)db()->prepare('DELETE FROM car_wash_blocks WHERE id=?')->execute([$id]);
            else db()->prepare('INSERT INTO car_wash_blocks(company_id,block_date,slot_time,status,created_at) VALUES(?,?,?,?,?)')->execute([$companyId,$date,$time,'blocked',now()]);
            back_to('car_wash',$id?'Horário reaberto.':'Horário bloqueado para a data.');
            break;

        case 'create_car_wash_appointment':
            $companyId=(int)request('company_id');$date=request('appointment_date');$time=request('appointment_time');
            if($companyId<1||!preg_match('/^\\d{4}-\\d{2}-\\d{2}$/',$date)||!preg_match('/^\\d{2}:\\d{2}$/',$time)) throw new RuntimeException('Data ou horário inválido.');
            $cap=car_wash_slot_capacity_for($companyId,$date,$time);
            if($cap<1) throw new RuntimeException('Este horário não está disponível. Atualize a página e escolha uma vaga liberada.');
            $q=db()->prepare('SELECT COUNT(*) FROM car_wash_blocks WHERE company_id=? AND block_date=? AND slot_time=?');$q->execute([$companyId,$date,$time]);if((int)$q->fetchColumn()>0)throw new RuntimeException('Este horário foi bloqueado pelo Lava-Jato.');
            $q=db()->prepare("SELECT COUNT(*) FROM car_wash_appointments WHERE company_id=? AND appointment_date=? AND appointment_time=? AND status NOT IN ('cancelled','rejected')");$q->execute([$companyId,$date,$time]);if((int)$q->fetchColumn()>=$cap)throw new RuntimeException('Este horário já está lotado.');
            $serviceId=(int)request('service_id',0);$serviceName='';$servicePrice=0;
            if($serviceId>0){$sq=db()->prepare("SELECT name,price FROM car_wash_services WHERE id=? AND company_id=? AND status='active' LIMIT 1");$sq->execute([$serviceId,$companyId]);$service=$sq->fetch();if(!$service)throw new RuntimeException('Escolha um tipo de lavagem disponível.');$serviceName=(string)$service['name'];$servicePrice=(float)$service['price'];}
            db()->prepare('INSERT INTO car_wash_appointments(company_id,client_id,appointment_date,appointment_time,vehicle_type,plate,status,notes,service_id,service_name,service_price,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)')
                ->execute([$companyId,(int)$user['id'],$date,$time,request('vehicle_type'),strtoupper(request('plate')),'requested',request('notes'),$serviceId?:null,$serviceName,$servicePrice,now(),now()]);
            $id=(int)db()->lastInsertId();$owner=db()->prepare('SELECT owner_user_id,name FROM companies WHERE id=?');$owner->execute([$companyId]);$c=$owner->fetch();
            if($c&&$c['owner_user_id'])create_notification((int)$c['owner_user_id'],'Novo agendamento no Lava-Jato',$user['name'].' solicitou '.$date.' às '.$time.'.','info','dashboard.php?page=car_wash','car_wash_appointment',$id);
            create_notification((int)$user['id'],'Agendamento enviado','Sua solicitação para '.($c['name']??'o Lava-Jato').' foi registrada.','success','dashboard.php?page=car_wash','car_wash_appointment',$id);
            back_to('car_wash','Agendamento solicitado com sucesso.');
            break;

        case 'delete_car_wash_appointment':
            must_admin();
            $id=(int)request('id');
            $q=db()->prepare('SELECT id FROM car_wash_appointments WHERE id=?');
            $q->execute([$id]);
            if(!(int)$q->fetchColumn()) throw new RuntimeException('Agendamento não encontrado.');
            db()->prepare('DELETE FROM car_wash_appointments WHERE id=?')->execute([$id]);
            back_to('car_wash','Agendamento do Lava-Jato apagado definitivamente.');
            break;

        case 'cleanup_car_wash_appointments':
            must_admin();
            $q=db()->prepare("DELETE FROM car_wash_appointments
                WHERE (
                    datetime(appointment_date || ' ' || substr(appointment_time,1,5)) <= datetime('now','localtime','-7 days')
                    OR (
                        status IN ('cancelled','completed','rejected')
                        AND datetime(COALESCE(updated_at,created_at)) <= datetime('now','localtime','-7 days')
                    )
                )");
            $q->execute();
            $removed=$q->rowCount();
            back_to('car_wash',$removed.' agendamento(s) antigo(s) removido(s).');
            break;

        case 'update_car_wash_appointment':
            $id=(int)request('id');$status=request('status');if(!in_array($status,['confirmed','cancelled','completed'],true))throw new RuntimeException('Status inválido.');
            $q=db()->prepare('SELECT a.*,c.owner_user_id FROM car_wash_appointments a JOIN companies c ON c.id=a.company_id WHERE a.id=?');$q->execute([$id]);$a=$q->fetch();if(!$a)throw new RuntimeException('Agendamento não encontrado.');
            $manager=is_admin()||(resolve_company_id_for_user($user)===(int)$a['company_id']);$client=(int)$a['client_id']===(int)$user['id'];
            if(!$manager&&!($client&&$status==='cancelled'))throw new RuntimeException('Sem permissão.');
            db()->prepare('UPDATE car_wash_appointments SET status=?,updated_at=? WHERE id=?')->execute([$status,now(),$id]);
            create_notification((int)$a['client_id'],'Agendamento atualizado','Situação: '.status_label($status).'.','info','dashboard.php?page=car_wash','car_wash_appointment',$id);
            back_to('car_wash','Agendamento atualizado.');
            break;

        case 'update_shuttle_driver_approval':
            must_admin();$targetId=(int)request('user_id');$status=request('approval_status');
            if(!in_array($status,['approved','pending','blocked'],true))throw new RuntimeException('Situação inválida.');
            $q=db()->prepare("SELECT COUNT(*) FROM users WHERE id=? AND role='shuttle_driver'");$q->execute([$targetId]);if(!(int)$q->fetchColumn())throw new RuntimeException('Motorista de lotação não encontrado.');
            $exists=db()->prepare('SELECT COUNT(*) FROM shuttle_driver_profiles WHERE user_id=?');$exists->execute([$targetId]);
            $notes=trim(request('approval_notes'));
            if((int)$exists->fetchColumn())db()->prepare('UPDATE shuttle_driver_profiles SET approval_status=?,approval_notes=?,approved_by=?,approved_at=?,updated_at=? WHERE user_id=?')->execute([$status,$notes,$status==='approved'?(int)$user['id']:null,$status==='approved'?now():null,now(),$targetId]);
            else db()->prepare('INSERT INTO shuttle_driver_profiles(user_id,approval_status,approval_notes,approved_by,approved_at,created_at,updated_at) VALUES(?,?,?,?,?,?,?)')->execute([$targetId,$status,$notes,$status==='approved'?(int)$user['id']:null,$status==='approved'?now():null,now(),now()]);
            if($status==='approved')db()->prepare("UPDATE users SET status='active',activated_by_admin_id=?,updated_at=? WHERE id=?")->execute([(int)$user['id'],now(),$targetId]);
            create_notification($targetId,'Cadastro da lotação atualizado','Situação: '.status_label($status).($notes?' • '.$notes:''),'info','dashboard.php?page=shuttle','shuttle_driver',$targetId);
            back_to('shuttle','Situação do motorista atualizada.');break;

        case 'save_shuttle_profile':
            $targetId=is_admin()?(int)request('user_id',(int)$user['id']):(int)$user['id'];
            if(!is_admin() && !is_role('shuttle_driver')) throw new RuntimeException('Somente motoristas de lotação podem editar este perfil.');
            $q=db()->prepare('SELECT vehicle_photo,cnh_document,vehicle_document,approval_status,approval_notes FROM shuttle_driver_profiles WHERE user_id=?');$q->execute([$targetId]);$old=$q->fetch()?:[];
            $vehiclePhoto=upload_image('vehicle_photo_file','lotacao/veiculos',$old['vehicle_photo']??'');
            $cnhDoc=upload_document('cnh_document_file','lotacao/documentos',$old['cnh_document']??'');
            $vehicleDoc=upload_document('vehicle_document_file','lotacao/documentos',$old['vehicle_document']??'');
            $docsChanged=($cnhDoc!==($old['cnh_document']??''))||($vehicleDoc!==($old['vehicle_document']??''));
            $nextApproval=is_admin()?request('approval_status',($old['approval_status']??'approved')):($docsChanged?'pending':($old['approval_status']??'pending'));
            db()->prepare("INSERT INTO shuttle_driver_profiles(user_id,vehicle_type,vehicle_model,plate,capacity,vehicle_photo,cnh_number,cnh_document,vehicle_document,approval_status,approval_notes,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(user_id) DO UPDATE SET vehicle_type=excluded.vehicle_type,vehicle_model=excluded.vehicle_model,plate=excluded.plate,capacity=excluded.capacity,vehicle_photo=excluded.vehicle_photo,cnh_number=excluded.cnh_number,cnh_document=excluded.cnh_document,vehicle_document=excluded.vehicle_document,approval_status=excluded.approval_status,approval_notes=CASE WHEN excluded.approval_status='pending' THEN NULL ELSE shuttle_driver_profiles.approval_notes END,updated_at=excluded.updated_at")
                ->execute([$targetId,request('vehicle_type','van'),request('vehicle_model'),strtoupper(request('plate')),max(1,(int)request('capacity',4)),$vehiclePhoto,request('cnh_number'),$cnhDoc,$vehicleDoc,$nextApproval,$old['approval_notes']??null,now(),now()]);
            if($docsChanged&&!is_admin()) notify_super_admins('Documentos da lotação enviados',$user['name'].' enviou/atualizou CNH ou documento do veículo. Analise o cadastro da lotação.','info',0,'dashboard.php?page=shuttle','shuttle_driver',$targetId);
            back_to('shuttle','Dados do motorista/veículo salvos.');
            break;

        case 'save_shuttle_route':
            $driverId=is_role('shuttle_driver')?(int)$user['id']:(int)request('driver_id');$companyId=(int)($user['company_id']??request('company_id',0));
            if(!is_admin()&&!is_role(['company','employee','shuttle_driver']))throw new RuntimeException('Sem permissão.');
            if(is_role(['company','employee'])&&($user['company_type']??'')!=='shuttle')throw new RuntimeException('Sua empresa não pertence ao segmento Lotação.');
            if($driverId<1)throw new RuntimeException('Selecione o motorista.');
            $id=(int)request('id',0);$vals=[$companyId?:null,$driverId,request('name'),request('origin_city'),request('destination_city'),request('highway'),request('pickup_points'),request('dropoff_points'),max(0,(float)request('max_detour_km',5)),request('start_time'),request('end_time'),implode(',',array_map('intval',(array)request('weekdays',[]))),max(1,(int)request('capacity',4)),max(0,(float)str_replace(',','.',request('price_per_seat',0))),request('status','active')];
            if($id){$q=db()->prepare('SELECT driver_id,company_id FROM shuttle_routes WHERE id=?');$q->execute([$id]);$r=$q->fetch();if(!$r)throw new RuntimeException('Rota não encontrada.');if(!is_admin()&&(int)$r['driver_id']!==(int)$user['id']&&(int)$r['company_id']!==(int)($user['company_id']??0))throw new RuntimeException('Sem permissão.');db()->prepare('UPDATE shuttle_routes SET company_id=?,driver_id=?,name=?,origin_city=?,destination_city=?,highway=?,pickup_points=?,dropoff_points=?,max_detour_km=?,start_time=?,end_time=?,weekdays=?,capacity=?,price_per_seat=?,status=?,updated_at=? WHERE id=?')->execute(array_merge($vals,[now(),$id]));$routeId=$id;}
            else {db()->prepare('INSERT INTO shuttle_routes(company_id,driver_id,name,origin_city,destination_city,highway,pickup_points,dropoff_points,max_detour_km,start_time,end_time,weekdays,capacity,price_per_seat,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute(array_merge($vals,[now(),now()]));$routeId=(int)db()->lastInsertId();}
            $faresText=trim((string)request('city_fares'));
            db()->prepare('DELETE FROM shuttle_route_fares WHERE route_id=?')->execute([$routeId]);
            if($faresText!==''){
                $fareStmt=db()->prepare('INSERT OR REPLACE INTO shuttle_route_fares(route_id,city_name,normalized_city,price_per_seat,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?)');
                foreach(preg_split('/\r?\n/',$faresText) as $line){
                    $line=trim($line);if($line==='')continue;
                    if(!preg_match('/^(.+?)\s*(?:=|:|;)\s*R?\$?\s*([0-9.,]+)$/u',$line,$m))continue;
                    $city=trim($m[1]);$price=localized_decimal($m[2]);
                    if($city!==''&&$price>=0)$fareStmt->execute([$routeId,$city,shuttle_normalize_city($city),$price,'active',now(),now()]);
                }
            }
            back_to('shuttle','Rota da lotação salva.');
            break;

        case 'delete_shuttle_route':
            $id=(int)request('id');$q=db()->prepare('SELECT driver_id,company_id FROM shuttle_routes WHERE id=?');$q->execute([$id]);$r=$q->fetch();if(!$r)throw new RuntimeException('Rota não encontrada.');if(!is_admin()&&(int)$r['driver_id']!==(int)$user['id']&&(int)$r['company_id']!==(int)($user['company_id']??0))throw new RuntimeException('Sem permissão.');db()->prepare('DELETE FROM shuttle_routes WHERE id=?')->execute([$id]);back_to('shuttle','Rota apagada.');break;

        case 'save_shuttle_fares':
            $routeId=(int)request('route_id');$q=db()->prepare('SELECT driver_id,company_id FROM shuttle_routes WHERE id=?');$q->execute([$routeId]);$r=$q->fetch();if(!$r)throw new RuntimeException('Rota não encontrada.');if(!is_admin()&&(int)$r['driver_id']!==(int)$user['id']&&(int)$r['company_id']!==(int)($user['company_id']??0))throw new RuntimeException('Sem permissão.');
            $faresText=trim((string)request('city_fares'));db()->prepare('DELETE FROM shuttle_route_fares WHERE route_id=?')->execute([$routeId]);
            if($faresText!==''){$fareStmt=db()->prepare('INSERT OR REPLACE INTO shuttle_route_fares(route_id,city_name,normalized_city,price_per_seat,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?)');foreach(preg_split('/\r?\n/',$faresText) as $line){$line=trim($line);if($line===''||!preg_match('/^(.+?)\s*(?:=|:|;)\s*R?\$?\s*([0-9.,]+)$/u',$line,$m))continue;$city=trim($m[1]);$price=localized_decimal($m[2]);if($city!==''&&$price>=0)$fareStmt->execute([$routeId,$city,shuttle_normalize_city($city),$price,'active',now(),now()]);}}
            back_to('shuttle','Valores por cidade atualizados.');break;

        case 'start_shuttle_trip':
            $routeId=(int)request('route_id');$q=db()->prepare('SELECT * FROM shuttle_routes WHERE id=? AND status=\'active\'');$q->execute([$routeId]);$r=$q->fetch();if(!$r)throw new RuntimeException('Rota indisponível.');
            if(!is_admin()&&(int)$r['driver_id']!==(int)$user['id']&&(int)$r['company_id']!==(int)($user['company_id']??0))throw new RuntimeException('Sem permissão.');
            $du=db()->prepare('SELECT role FROM users WHERE id=?');$du->execute([$r['driver_id']]);$driverRole=(string)$du->fetchColumn();
            if($driverRole==='shuttle_driver'&&!is_admin()){$ap=db()->prepare('SELECT approval_status FROM shuttle_driver_profiles WHERE user_id=?');$ap->execute([$r['driver_id']]);if((string)$ap->fetchColumn()!=='approved')throw new RuntimeException('O cadastro do motorista de lotação ainda não foi aprovado pelo administrador.');}
            db()->prepare("UPDATE shuttle_trips SET status='completed',ended_at=?,updated_at=? WHERE driver_id=? AND status='active'")->execute([now(),now(),$r['driver_id']]);
            db()->prepare('INSERT INTO shuttle_trips(route_id,driver_id,company_id,status,available_seats,current_address,current_lat,current_lng,started_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)')->execute([$routeId,$r['driver_id'],$r['company_id'],'active',$r['capacity'],request('current_address'),request('current_lat')?:null,request('current_lng')?:null,now(),now()]);
            back_to('shuttle','Viagem iniciada e disponível para passageiros.');break;

        case 'update_shuttle_location':
            $tripId=(int)request('trip_id');$q=db()->prepare('SELECT driver_id,company_id FROM shuttle_trips WHERE id=?');$q->execute([$tripId]);$t=$q->fetch();if(!$t)throw new RuntimeException('Viagem não encontrada.');if(!is_admin()&&(int)$t['driver_id']!==(int)$user['id']&&(int)$t['company_id']!==(int)($user['company_id']??0))throw new RuntimeException('Sem permissão.');db()->prepare('UPDATE shuttle_trips SET current_address=?,current_lat=?,current_lng=?,updated_at=? WHERE id=?')->execute([request('current_address'),request('current_lat')?:null,request('current_lng')?:null,now(),$tripId]);back_to('shuttle','Localização da lotação atualizada.');break;

        case 'finish_shuttle_trip':
            $tripId=(int)request('trip_id');$q=db()->prepare('SELECT driver_id,company_id FROM shuttle_trips WHERE id=?');$q->execute([$tripId]);$t=$q->fetch();if(!$t)throw new RuntimeException('Viagem não encontrada.');if(!is_admin()&&(int)$t['driver_id']!==(int)$user['id']&&(int)$t['company_id']!==(int)($user['company_id']??0))throw new RuntimeException('Sem permissão.');db()->prepare("UPDATE shuttle_trips SET status='completed',ended_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$tripId]);back_to('shuttle','Viagem encerrada.');break;

        case 'request_shuttle_now':
            $pickup=trim(request('pickup'));$destination=trim(request('destination'));$seats=max(1,min(8,(int)request('seats',1)));if($pickup===''||$destination==='')throw new RuntimeException('Informe embarque e destino.');
            $open=db()->prepare("SELECT COUNT(*) FROM shuttle_requests WHERE client_id=? AND status IN ('waiting','accepted','boarded')");$open->execute([(int)$user['id']]);if((int)$open->fetchColumn()>0)throw new RuntimeException('Você já possui uma solicitação de lotação em andamento. Cancele ou conclua antes de criar outra.');
            db()->prepare('INSERT INTO shuttle_requests(client_id,pickup,destination,pickup_lat,pickup_lng,destination_lat,destination_lng,seats,payment_method,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)')->execute([(int)$user['id'],$pickup,$destination,request('pickup_lat')?:null,request('pickup_lng')?:null,request('destination_lat')?:null,request('destination_lng')?:null,$seats,request('payment_method','pix'),'waiting',now(),now()]);$requestId=(int)db()->lastInsertId();
            $drivers=db()->query("SELECT DISTINCT t.driver_id,t.route_id FROM shuttle_trips t WHERE t.status='active' AND t.available_seats>0")->fetchAll();$notified=[];foreach($drivers as $driver){if(!shuttle_route_accepts_destination((int)$driver['route_id'],$destination))continue;$did=(int)$driver['driver_id'];if(isset($notified[$did]))continue;create_notification($did,'Passageiro aguardando lotação',$user['name'].' está aguardando uma lotação para '.$destination.'.','warning','dashboard.php?page=shuttle','shuttle_request',$requestId);$notified[$did]=1;}
            log_activity('request_shuttle_now','shuttle_requests',$requestId,$pickup.' → '.$destination);back_to('shuttle','Solicitação enviada. As lotações online compatíveis verão você na fila por proximidade.');break;

        case 'accept_shuttle_request':
            $requestId=(int)request('id');$tripId=(int)request('trip_id');$q=db()->prepare("SELECT sr.*,t.driver_id,t.company_id,t.route_id,t.available_seats,r.price_per_seat,u.name client_name FROM shuttle_requests sr JOIN users u ON u.id=sr.client_id JOIN shuttle_trips t ON t.id=? JOIN shuttle_routes r ON r.id=t.route_id WHERE sr.id=? AND sr.status='waiting' AND t.status='active'");$q->execute([$tripId,$requestId]);$sr=$q->fetch();if(!$sr)throw new RuntimeException('Solicitação não está mais disponível.');
            $manager=is_admin()||(int)$sr['driver_id']===(int)$user['id']||((int)$sr['company_id']>0&&(int)$sr['company_id']===(int)($user['company_id']??0));if(!$manager)throw new RuntimeException('Esta solicitação não pertence à sua lotação.');if((int)$sr['available_seats']<(int)$sr['seats'])throw new RuntimeException('Sua lotação não possui vagas suficientes.');if(!shuttle_route_accepts_destination((int)$sr['route_id'],$sr['destination']))throw new RuntimeException('O destino do passageiro não é compatível com sua rota.');
            $amount=shuttle_price_for_destination((int)$sr['route_id'],$sr['destination'],(float)$sr['price_per_seat'])*(int)$sr['seats'];db()->beginTransaction();
            $lock=db()->prepare("UPDATE shuttle_requests SET status='accepted',accepted_trip_id=?,accepted_driver_id=?,amount=?,accepted_at=?,updated_at=? WHERE id=? AND status='waiting'");$lock->execute([$tripId,(int)$sr['driver_id'],$amount,now(),now(),$requestId]);if($lock->rowCount()!==1){db()->rollBack();throw new RuntimeException('Outra lotação já aceitou este passageiro.');}
            db()->prepare('INSERT INTO shuttle_bookings(trip_id,client_id,pickup,destination,pickup_lat,pickup_lng,destination_lat,destination_lng,seats,amount,payment_method,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$tripId,(int)$sr['client_id'],$sr['pickup'],$sr['destination'],$sr['pickup_lat'],$sr['pickup_lng'],$sr['destination_lat'],$sr['destination_lng'],(int)$sr['seats'],$amount,$sr['payment_method'],'reserved',now(),now()]);$bookingId=(int)db()->lastInsertId();db()->prepare('UPDATE shuttle_trips SET available_seats=available_seats-?,updated_at=? WHERE id=?')->execute([(int)$sr['seats'],now(),$tripId]);db()->prepare('UPDATE shuttle_requests SET booking_id=?,updated_at=? WHERE id=?')->execute([$bookingId,now(),$requestId]);db()->commit();
            create_notification((int)$sr['client_id'],'Lotação a caminho','Sua solicitação foi aceita. A lotação está a caminho do embarque.','success','dashboard.php?page=shuttle','shuttle_booking',$bookingId);create_notification((int)$sr['driver_id'],'Passageiro adicionado','O passageiro '.$sr['client_name'].' foi adicionado à sua viagem.','success','dashboard.php?page=shuttle','shuttle_booking',$bookingId);log_activity('accept_shuttle_request','shuttle_requests',$requestId,'Trip '.$tripId);back_to('shuttle','Passageiro aceito e cadeira reservada.');break;

        case 'cancel_shuttle_request':
            $id=(int)request('id');$q=db()->prepare('SELECT * FROM shuttle_requests WHERE id=?');$q->execute([$id]);$sr=$q->fetch();if(!$sr)throw new RuntimeException('Solicitação não encontrada.');if((int)$sr['client_id']!==(int)$user['id']&&!is_admin())throw new RuntimeException('Sem permissão.');if($sr['status']!=='waiting')throw new RuntimeException('Depois que uma lotação aceita, cancele pela reserva da viagem.');db()->prepare("UPDATE shuttle_requests SET status='cancelled',updated_at=? WHERE id=?")->execute([now(),$id]);log_activity('cancel_shuttle_request','shuttle_requests',$id,'Cancelada');back_to('shuttle','Solicitação de lotação cancelada.');break;

        case 'book_shuttle':
            $tripId=(int)request('trip_id');$seats=max(1,(int)request('seats',1));
            if(!request('pickup')||!request('destination'))throw new RuntimeException('Informe embarque e desembarque.');
            if(!request('pickup_lat')||!request('pickup_lng'))throw new RuntimeException('Confirme sua localização de embarque usando Localizar ou uma sugestão de endereço.');
            if(!request('destination_lat')||!request('destination_lng'))throw new RuntimeException('Selecione uma sugestão para o endereço de desembarque.');
            db()->beginTransaction();$q=db()->prepare("SELECT t.*,r.price_per_seat,r.name,u.name driver_name FROM shuttle_trips t JOIN shuttle_routes r ON r.id=t.route_id JOIN users u ON u.id=t.driver_id WHERE t.id=? AND t.status='active'");$q->execute([$tripId]);$t=$q->fetch();if(!$t||$t['available_seats']<$seats)throw new RuntimeException('Não há vagas suficientes nesta lotação.');$seatPrice=shuttle_price_for_destination((int)$t['route_id'],request('destination'),(float)$t['price_per_seat']);$amount=$seatPrice*$seats;db()->prepare('INSERT INTO shuttle_bookings(trip_id,client_id,pickup,destination,pickup_lat,pickup_lng,destination_lat,destination_lng,seats,amount,payment_method,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$tripId,$user['id'],request('pickup'),request('destination'),request('pickup_lat')?:null,request('pickup_lng')?:null,request('destination_lat')?:null,request('destination_lng')?:null,$seats,$amount,request('payment_method','pix'),'reserved',now(),now()]);$bookingId=(int)db()->lastInsertId();db()->prepare('UPDATE shuttle_trips SET available_seats=available_seats-?,updated_at=? WHERE id=?')->execute([$seats,now(),$tripId]);db()->commit();create_notification((int)$t['driver_id'],'Novo passageiro na sua rota',$user['name'].' reservou '.$seats.' vaga(s).','info','dashboard.php?page=shuttle','shuttle_booking',$bookingId);create_notification((int)$user['id'],'Vaga reservada','Sua vaga com '.$t['driver_name'].' foi reservada.','success','dashboard.php?page=shuttle','shuttle_booking',$bookingId);back_to('shuttle','Vaga reservada com sucesso.');break;

        case 'update_shuttle_booking':
            $id=(int)request('id');$status=request('status');if(!in_array($status,['boarded','completed','cancelled'],true))throw new RuntimeException('Status inválido.');$q=db()->prepare('SELECT b.*,t.driver_id,t.company_id,t.available_seats,r.capacity FROM shuttle_bookings b JOIN shuttle_trips t ON t.id=b.trip_id JOIN shuttle_routes r ON r.id=t.route_id WHERE b.id=?');$q->execute([$id]);$b=$q->fetch();if(!$b)throw new RuntimeException('Reserva não encontrada.');$manager=is_admin()||(int)$b['driver_id']===(int)$user['id']||(int)$b['company_id']===(int)($user['company_id']??0);$client=(int)$b['client_id']===(int)$user['id'];if(!$manager&&!($client&&$status==='cancelled'))throw new RuntimeException('Sem permissão.');if(in_array($status,['cancelled','completed'],true)&&!in_array($b['status'],['cancelled','completed'],true)){$newSeats=min((int)$b['capacity'],(int)$b['available_seats']+(int)$b['seats']);db()->prepare('UPDATE shuttle_trips SET available_seats=?,updated_at=? WHERE id=?')->execute([$newSeats,now(),$b['trip_id']]);}db()->prepare('UPDATE shuttle_bookings SET status=?,updated_at=? WHERE id=?')->execute([$status,now(),$id]);db()->prepare('UPDATE shuttle_requests SET status=?,updated_at=? WHERE booking_id=?')->execute([$status,now(),$id]);create_notification((int)$b['client_id'],'Lotação atualizada','Situação da sua viagem: '.status_label($status).'.','info','dashboard.php?page=shuttle','shuttle_booking',$id);back_to('shuttle','Reserva atualizada.');break;

        case 'request_wallet_withdrawal':
            if(!wallet_enabled_for_user($user))throw new RuntimeException('Sua carteira ainda não foi liberada.');
            $amount=localized_decimal(request('amount',0));$minimum=max(0,(float)setting('wallet_min_withdrawal','10'));if($amount<$minimum)throw new RuntimeException('A retirada mínima é '.money($minimum).'.');
            $available=wallet_available_balance((int)$user['id']);if($amount>$available+0.0001)throw new RuntimeException('Saldo disponível insuficiente.');
            $pix=trim((string)request('pix_key'));if($pix==='')throw new RuntimeException('Informe sua chave Pix.');
            $bd=withdrawal_fee_breakdown($amount);if($bd['payout']<=0)throw new RuntimeException('O valor líquido da retirada precisa ser maior que zero.');
            db()->prepare('INSERT INTO withdrawal_requests(user_id,amount,pix_key,status,fee_amount,payout_amount,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)')->execute([(int)$user['id'],$amount,$pix,'pending',$bd['fee'],$bd['payout'],now(),now()]);$id=(int)db()->lastInsertId();
            notify_super_admins('Nova solicitação de retirada',$user['name'].' solicitou '.money($amount).' (líquido '.money($bd['payout']).').','warning',(int)$user['id'],'dashboard.php?page=wallet','withdrawal_request',$id);
            back_to('wallet','Retirada solicitada. Taxa: '.money($bd['fee']).' • Valor líquido: '.money($bd['payout']).'.');
            break;

        case 'review_wallet_withdrawal':
            must_admin();$id=(int)request('id');$decision=request('decision');if(!in_array($decision,['approved','rejected'],true))throw new RuntimeException('Decisão inválida.');
            $q=db()->prepare("SELECT wr.*,u.name,u.role FROM withdrawal_requests wr JOIN users u ON u.id=wr.user_id WHERE wr.id=?");$q->execute([$id]);$wr=$q->fetch();if(!$wr)throw new RuntimeException('Solicitação não encontrada.');if($wr['status']!=='pending')throw new RuntimeException('Esta retirada já foi analisada.');
            if(is_role('sub_admin')&&in_array((string)$wr['role'],['super_admin','sub_admin'],true))throw new RuntimeException('Administrador não pode analisar retiradas de contas administrativas.');
            if($decision==='approved'){
                $account=wallet_account((int)$wr['user_id']);if((float)$account['balance']+0.0001<(float)$wr['amount'])throw new RuntimeException('O usuário não possui mais saldo suficiente para esta retirada.');
                db()->beginTransaction();
                db()->prepare('UPDATE wallet_accounts SET balance=balance-?,total_withdrawn=total_withdrawn+?,updated_at=? WHERE user_id=?')->execute([(float)$wr['amount'],(float)$wr['amount'],now(),(int)$wr['user_id']]);
                db()->prepare('INSERT INTO wallet_transactions(user_id,source_type,source_id,kind,amount,description,created_at) VALUES(?,?,?,?,?,?,?)')->execute([(int)$wr['user_id'],'withdrawal',$id,'withdrawal',-(float)$wr['amount'],'Retirada aprovada pelo administrador • líquido '.money((float)($wr['payout_amount']?:$wr['amount'])).' • taxa '.money((float)($wr['fee_amount']??0)),now()]);
                db()->prepare('UPDATE withdrawal_requests SET status=?,admin_notes=?,reviewed_by=?,reviewed_at=?,updated_at=? WHERE id=?')->execute(['approved',request('admin_notes'),(int)$user['id'],now(),now(),$id]);
                db()->commit();
            }else db()->prepare('UPDATE withdrawal_requests SET status=?,admin_notes=?,reviewed_by=?,reviewed_at=?,updated_at=? WHERE id=?')->execute(['rejected',request('admin_notes'),(int)$user['id'],now(),now(),$id]);
            create_notification((int)$wr['user_id'],'Retirada '.($decision==='approved'?'aprovada':'recusada'),$decision==='approved'?'Sua retirada de '.money($wr['amount']).' foi aprovada. Taxa: '.money((float)($wr['fee_amount']??0)).' • Valor líquido pago: '.money((float)($wr['payout_amount']?:$wr['amount'])).'.':'Sua solicitação de retirada foi recusada. '.trim((string)request('admin_notes')),$decision==='approved'?'success':'warning','dashboard.php?page=wallet','withdrawal_request',$id);
            back_to('wallet','Solicitação de retirada atualizada.');
            break;

        case 'save_financial_fee_settings':
            if(!is_super_admin())throw new RuntimeException('Somente o Admin pode alterar as tarifas financeiras.');
            $withdrawType=(string)request('withdrawal_fee_type','fixed');if(!in_array($withdrawType,['fixed','percent'],true))$withdrawType='fixed';
            $withdrawValue=max(0,localized_decimal(request('withdrawal_fee_value',0)));if($withdrawType==='percent'&&$withdrawValue>100)throw new RuntimeException('A tarifa percentual de saque não pode passar de 100%.');
            $platformServiceLink=trim((string)request('platform_service_payment_link'));if($platformServiceLink!==''&&!filter_var($platformServiceLink,FILTER_VALIDATE_URL))throw new RuntimeException('Informe uma URL válida para o link de pagamento da plataforma.');
            save_setting('wallet_withdrawal_fee_type',$withdrawType);save_setting('wallet_withdrawal_fee_value',(string)$withdrawValue);save_setting('platform_service_payment_link',$platformServiceLink);
            foreach(['mototaxi','taxi','private_driver'] as $rideType){$prefix='ride_'.$rideType.'_';if(!array_key_exists($prefix.'auto_fare_enabled',$_POST))continue;save_setting($prefix.'auto_fare_enabled',request($prefix.'auto_fare_enabled','0')==='1'?'1':'0');save_setting($prefix.'price_km',(string)max(0,localized_decimal(request($prefix.'price_km',0))));save_setting($prefix.'base_fare',(string)max(0,localized_decimal(request($prefix.'base_fare',0))));save_setting($prefix.'min_fare',(string)max(0,localized_decimal(request($prefix.'min_fare',0))));}
            log_activity('save_financial_fee_settings','settings',null,'Tarifa saque '.$withdrawType.' '.$withdrawValue);
            back_to('fees','Configurações financeiras atualizadas.');
            break;

        case 'save_new_area_link':
            if(!integration_can_publish($user))throw new RuntimeException('Seu perfil não pode publicar um sistema na Central de Integrações.');
            $id=(int)request('id',0);$title=trim((string)request('title'));$urlValue=trim((string)request('url'));$desc=trim((string)request('description'));$status=(string)request('status','active');$sort=(int)request('sort_order',0);$linkKind=(string)request('link_kind','panel');if(!in_array($linkKind,['panel','site'],true))$linkKind='panel';
            if($title===''||!filter_var($urlValue,FILTER_VALIDATE_URL))throw new RuntimeException('Informe um nome e uma URL válida para a Central de Integrações.');if(!in_array($status,['active','inactive'],true))$status='active';
            if(is_admin()){
                $targetType=(string)request('target_type','all');if(!in_array($targetType,['all','user','company'],true))$targetType='all';$targetId=0;
                if($targetType==='user'){$targetId=(int)request('target_user_id',0);$tq=db()->prepare("SELECT id FROM users WHERE id=? AND role NOT IN ('super_admin','sub_admin')");$tq->execute([$targetId]);if(!$tq->fetchColumn())throw new RuntimeException('Selecione um usuário válido para a Central de Integrações.');}
                elseif($targetType==='company'){$targetId=(int)request('target_company_id',0);$tq=db()->prepare('SELECT id FROM companies WHERE id=?');$tq->execute([$targetId]);if(!$tq->fetchColumn())throw new RuntimeException('Selecione uma empresa válida para a Central de Integrações.');}
            }else{
                // Sistemas publicados por empresa/prestador começam privados e são distribuídos pelo QR/link assinado.
                $targetType='user';$targetId=(int)$user['id'];$sort=0;
                if($id){$oq=db()->prepare('SELECT created_by FROM external_module_links WHERE id=?');$oq->execute([$id]);if((int)$oq->fetchColumn()!==(int)$user['id'])throw new RuntimeException('Você só pode editar integrações publicadas pela sua própria conta.');}
            }
            if($id){
                $sql=is_admin()?'UPDATE external_module_links SET title=?,url=?,description=?,status=?,sort_order=?,target_type=?,target_id=?,link_kind=?,updated_at=? WHERE id=?':'UPDATE external_module_links SET title=?,url=?,description=?,status=?,sort_order=?,target_type=?,target_id=?,link_kind=?,updated_at=? WHERE id=? AND created_by=?';
                $args=[$title,$urlValue,$desc,$status,$sort,$targetType,$targetId?:null,$linkKind,now(),$id];if(!is_admin())$args[]=(int)$user['id'];db()->prepare($sql)->execute($args);
            }else{
                db()->prepare('INSERT INTO external_module_links(title,url,description,status,sort_order,created_by,created_at,updated_at,target_type,target_id,link_kind) VALUES(?,?,?,?,?,?,?,?,?,?,?)')->execute([$title,$urlValue,$desc,$status,$sort,(int)$user['id'],now(),now(),$targetType,$targetId?:null,$linkKind]);$id=(int)db()->lastInsertId();
            }
            log_activity('save_new_area_link','external_module_links',$id,$title.' • '.$targetType.($targetId?' #'.$targetId:''));back_to('new_area','Integração salva. O link e o QR Code de liberação já estão disponíveis.');break;

        case 'delete_new_area_link':
            $id=(int)request('id');if($id<1)throw new RuntimeException('Integração inválida.');
            if(is_admin())db()->prepare('DELETE FROM external_module_links WHERE id=?')->execute([$id]);
            else{if(!integration_can_publish($user))throw new RuntimeException('Sem permissão.');$q=db()->prepare('DELETE FROM external_module_links WHERE id=? AND created_by=?');$q->execute([$id,(int)$user['id']]);if($q->rowCount()<1)throw new RuntimeException('Você só pode remover integrações publicadas pela sua conta.');}
            log_activity('delete_new_area_link','external_module_links',$id,'');back_to('new_area','Integração removida.');break;

        case 'grant_new_area_client_access':
            if(is_admin())throw new RuntimeException('Use o cadastro principal da Central de Integrações para liberar administrativamente.');$linkId=(int)request('module_link_id');$clientId=(int)request('client_user_id');if(!new_area_link_available_to_user($linkId,$user))throw new RuntimeException('Este painel não está liberado para sua conta.');if(!new_area_user_can_manage_client($user,$clientId))throw new RuntimeException('Este cliente não pertence à sua carteira.');$q=db()->prepare("SELECT name FROM users WHERE id=? AND status='active'");$q->execute([$clientId]);$clientName=(string)$q->fetchColumn();if($clientName==='')throw new RuntimeException('Cliente não encontrado.');$displayName=trim((string)request('display_name'));if($displayName==='')$displayName=$clientName;$companyId=resolve_company_id_for_user($user);db()->prepare("INSERT INTO external_module_client_access(module_link_id,granted_by_user_id,company_id,client_user_id,display_name,status,created_at,updated_at) VALUES(?,?,?,?,?,'active',?,?) ON CONFLICT(module_link_id,granted_by_user_id,client_user_id) DO UPDATE SET display_name=excluded.display_name,status='active',updated_at=excluded.updated_at")->execute([$linkId,(int)$user['id'],$companyId?:null,$clientId,$displayName,now(),now()]);$accessLookup=db()->prepare('SELECT id FROM external_module_client_access WHERE module_link_id=? AND granted_by_user_id=? AND client_user_id=?');$accessLookup->execute([$linkId,(int)$user['id'],$clientId]);$accessId=(int)$accessLookup->fetchColumn();create_notification($clientId,'Central de Integrações liberada',($user['name']??'Seu prestador').' liberou um painel adicional para você.','success','dashboard.php?page=new_area','external_module_client_access',$accessId);log_activity('grant_new_area_client_access','external_module_client_access',$accessId,$clientName.' • '.$displayName);back_to('new_area','Acesso do cliente liberado.');break;

        case 'revoke_new_area_client_access':
            if(is_admin())throw new RuntimeException('Ação destinada ao responsável que compartilhou o acesso.');$id=(int)request('id');$q=db()->prepare('SELECT client_user_id FROM external_module_client_access WHERE id=? AND granted_by_user_id=?');$q->execute([$id,(int)$user['id']]);$clientId=(int)$q->fetchColumn();if($clientId<1)throw new RuntimeException('Liberação não encontrada.');db()->prepare("UPDATE external_module_client_access SET status='inactive',updated_at=? WHERE id=? AND granted_by_user_id=?")->execute([now(),$id,(int)$user['id']]);create_notification($clientId,'Acesso à Central de Integrações encerrado','Um acesso compartilhado foi encerrado pelo responsável.','info','dashboard.php?page=new_area','external_module_client_access',$id);back_to('new_area','Acesso do cliente removido.');break;

        case 'save_fee_rule':
            must_admin();$id=(int)request('id',0);$scope=request('scope_type','all');$feeType=request('fee_type','fixed');$value=(float)str_replace(',','.',request('fee_value',0));
            if(!in_array($scope,['all','role','segment','category','user'],true))throw new RuntimeException('Escopo de taxa inválido.');
            if(!in_array($feeType,['fixed','percent'],true))throw new RuntimeException('Tipo de taxa inválido.');
            if($value<0)throw new RuntimeException('A taxa não pode ser negativa.');if($feeType==='percent'&&$value>100)throw new RuntimeException('Taxa percentual não pode ultrapassar 100%.');
            $scopeValue=trim((string)request('scope_value'));if($scope==='all')$scopeValue='';elseif($scopeValue==='')throw new RuntimeException('Informe o usuário, perfil, segmento ou categoria da regra.');
            if($scope==='user'){
                $uq=db()->prepare("SELECT id FROM users WHERE role NOT IN ('super_admin','sub_admin') AND (id=? OR upper(public_id)=upper(?) OR lower(email)=lower(?)) LIMIT 1");
                $uq->execute([(int)$scopeValue,$scopeValue,$scopeValue]);$resolvedUserId=(int)$uq->fetchColumn();
                if($resolvedUserId<1)throw new RuntimeException('Usuário da regra de taxa não encontrado. Use o ID LinkExpress, e-mail ou ID interno.');
                $scopeValue=(string)$resolvedUserId;
            }elseif($scope==='category'){
                $categoryId=(int)$scopeValue;$cq=db()->prepare("SELECT id FROM categories WHERE id=? LIMIT 1");$cq->execute([$categoryId]);
                if($categoryId<1||!$cq->fetchColumn())throw new RuntimeException('Categoria da taxa não encontrada.');
                $scopeValue=(string)$categoryId;
            }elseif($scope==='segment'){
                $scopeValue=strtolower(trim($scopeValue));$sq=db()->prepare("SELECT slug FROM segments WHERE lower(slug)=lower(?) LIMIT 1");$sq->execute([$scopeValue]);$resolved=(string)($sq->fetchColumn()?:'');
                if($resolved!=='')$scopeValue=$resolved;
            }elseif($scope==='role'){
                $allowedFeeRoles=['company','client','courier','motofreight','mototaxi','taxi','private_driver','service_provider','clinic_professional','property_owner','rental_company','moving_provider','shuttle_driver','employee'];
                if(!in_array($scopeValue,$allowedFeeRoles,true))throw new RuntimeException('Perfil da taxa inválido.');
            }
            $status=in_array(request('status','active'),['active','inactive'],true)?request('status','active'):'active';
            if($id)db()->prepare('UPDATE platform_fee_rules SET scope_type=?,scope_value=?,fee_type=?,fee_value=?,status=?,updated_at=? WHERE id=?')->execute([$scope,$scopeValue,$feeType,$value,$status,now(),$id]);
            else db()->prepare('INSERT INTO platform_fee_rules(scope_type,scope_value,fee_type,fee_value,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?)')->execute([$scope,$scopeValue,$feeType,$value,$status,now(),now()]);
            back_to('fees','Regra de taxa salva.');
            break;

        case 'delete_fee_rule':
            must_admin();db()->prepare('DELETE FROM platform_fee_rules WHERE id=?')->execute([(int)request('id')]);back_to('fees','Regra de taxa apagada.');break;

        case 'remove_company_client':
            $companyId=(int)request('company_id');$clientId=(int)request('client_user_id');if($companyId<1||$clientId<1)throw new RuntimeException('Cliente ou empresa inválidos.');
            $isCourierManager=in_array((string)($user['role']??''),['courier','motofreight'],true)&&courier_can_register_company_client((int)$user['id'],$companyId);
            $resolved=resolve_company_id_for_user($user);$canManage=is_admin()||$isCourierManager||(($user['role']??'')==='company'&&$resolved===$companyId)||(($user['role']??'')==='employee'&&$resolved===$companyId&&company_member_can_manage_clients($user));
            if(!$canManage)throw new RuntimeException('Você não pode remover clientes desta empresa.');
            if(!client_is_linked_to_company($clientId,$companyId))throw new RuntimeException('Este usuário não está vinculado como cliente desta empresa.');
            $cq=db()->prepare('SELECT name FROM companies WHERE id=?');$cq->execute([$companyId]);$companyName=(string)$cq->fetchColumn();
            db()->beginTransaction();try{
                db()->prepare('DELETE FROM delivery_address_permissions WHERE company_id=? AND user_id=?')->execute([$companyId,$clientId]);
                db()->prepare("UPDATE users SET company_id=NULL,updated_at=? WHERE id=? AND role='client' AND company_id=?")->execute([now(),$clientId,$companyId]);
                db()->prepare("UPDATE external_module_client_access SET status='inactive',updated_at=? WHERE company_id=? AND client_user_id=?")->execute([now(),$companyId,$clientId]);
                db()->commit();
            }catch(Throwable $e){if(db()->inTransaction())db()->rollBack();throw $e;}
            create_notification($clientId,'Vínculo de cliente encerrado','Seu vínculo de cliente com '.($companyName?:'a empresa').' foi removido pelo responsável.','info','dashboard.php?page=affiliations','company_client',$companyId);
            back_to('company_clients','Cliente removido da área da empresa.');break;

        case 'add_provider_client':
            if(!provider_clients_enabled_for_user($user))throw new RuntimeException('Sua área de clientes ainda não foi liberada pelo administrador.');
            $publicId=strtoupper(trim((string)request('client_public_id')));$q=db()->prepare("SELECT id,name FROM users WHERE upper(public_id)=upper(?) AND status='active' LIMIT 1");$q->execute([$publicId]);$client=$q->fetch();if(!$client)throw new RuntimeException('Usuário ativo não encontrado com este ID.');if((int)$client['id']===(int)$user['id'])throw new RuntimeException('Você não pode vincular a própria conta como cliente.');
            if(!register_provider_client((int)$user['id'],(int)$client['id']))throw new RuntimeException('Limite de clientes atingido. Solicite ao administrador o aumento do limite.');
            back_to('provider_clients','Cliente adicionado à sua carteira de atendimento.');break;

        case 'unlink_customer_company':
            $companyId=(int)request('company_id');if($companyId<1)throw new RuntimeException('Empresa inválida.');
            db()->beginTransaction();
            try{
                db()->prepare('DELETE FROM delivery_address_permissions WHERE company_id=? AND user_id=?')->execute([$companyId,(int)$user['id']]);
                if(($user['role']??'')==='client'&&(int)($user['company_id']??0)===$companyId)db()->prepare('UPDATE users SET company_id=NULL,updated_at=? WHERE id=?')->execute([now(),(int)$user['id']]);
                db()->prepare("UPDATE external_module_client_access SET status='inactive',updated_at=? WHERE company_id=? AND client_user_id=?")->execute([now(),$companyId,(int)$user['id']]);
                db()->commit();
            }catch(Throwable $e){if(db()->inTransaction())db()->rollBack();throw $e;}
            $cq=db()->prepare('SELECT owner_user_id,name FROM companies WHERE id=?');$cq->execute([$companyId]);$company=$cq->fetch();if($company&&(int)$company['owner_user_id']>0)create_notification((int)$company['owner_user_id'],'Cliente encerrou vínculo',($user['name']??'Um usuário').' retirou o vínculo de cliente com '.$company['name'].'.','info','dashboard.php?page=company_clients','company_client',(int)$user['id']);
            back_to('affiliations','Vínculo com a empresa encerrado.');break;

        case 'unlink_customer_provider':
            $providerId=(int)request('provider_id');if($providerId<1)throw new RuntimeException('Prestador inválido.');
            $deleted=db()->prepare('DELETE FROM provider_clients WHERE provider_user_id=? AND client_user_id=?');$deleted->execute([$providerId,(int)$user['id']]);
            if($deleted->rowCount()>0)create_notification($providerId,'Cliente encerrou vínculo',($user['name']??'Um usuário').' retirou o vínculo da sua carteira de clientes.','info','dashboard.php?page=provider_clients','provider_client',(int)$user['id']);
            back_to('affiliations','Vínculo com o prestador encerrado.');break;

        case 'delete_provider_client':
            if(!provider_clients_enabled_for_user($user))throw new RuntimeException('Sua área de clientes não está liberada.');
            db()->prepare('DELETE FROM provider_clients WHERE id=? AND provider_user_id=?')->execute([(int)request('id'),(int)$user['id']]);back_to('provider_clients','Cliente removido da sua área.');break;

        case 'create_server_backup':
            must_admin();
            $path=create_database_snapshot('manual');
            log_activity('database_backup','settings',null,'Backup salvo no servidor: '.basename($path));
            back_to('backups','Backup criado no servidor: '.basename($path));
            break;

        case 'delete_server_backup':
            must_admin();
            $name=basename((string)request('file'));
            if($name===''||!preg_match('/^[a-z0-9_.-]+\.sqlite$/i',$name)) throw new RuntimeException('Nome de backup inválido.');
            $path=linkexpress_backup_dir().'/'.$name;
            if(!is_file($path)) throw new RuntimeException('Backup não encontrado.');
            if(!unlink($path)) throw new RuntimeException('Não foi possível apagar o backup.');
            log_activity('delete_backup','settings',null,$name);
            back_to('backups','Backup apagado do servidor.');
            break;

        case 'prepare_database_restore':
            must_admin();
            if(!request('confirm_restore')) throw new RuntimeException('Marque a confirmação antes de restaurar o banco.');
            $source='';$displayName='';
            $serverBackup=basename((string)request('server_backup'));
            if($serverBackup!==''){
                if(!preg_match('/^[a-z0-9_.-]+\.sqlite$/i',$serverBackup)) throw new RuntimeException('Backup do servidor inválido.');
                $candidate=linkexpress_backup_dir().'/'.$serverBackup;
                if(!is_file($candidate)) throw new RuntimeException('Backup do servidor não encontrado.');
                $source=$candidate;$displayName=$serverBackup;
            }else{
                if(!isset($_FILES['backup_file'])||!is_array($_FILES['backup_file'])) throw new RuntimeException('Selecione um arquivo .sqlite ou .db.');
                $file=$_FILES['backup_file'];$error=(int)($file['error']??UPLOAD_ERR_NO_FILE);
                if($error!==UPLOAD_ERR_OK) throw new RuntimeException($error===UPLOAD_ERR_INI_SIZE||$error===UPLOAD_ERR_FORM_SIZE?'O backup excede o limite de upload do servidor.':'Não foi possível receber o arquivo de backup.');
                $tmp=(string)($file['tmp_name']??'');$size=(int)($file['size']??0);$displayName=(string)($file['name']??'backup.sqlite');
                if($tmp===''||!is_uploaded_file($tmp)) throw new RuntimeException('Upload de backup inválido.');
                if($size<100||$size>209715200) throw new RuntimeException('O backup deve ter no máximo 200 MB.');
                $ext=strtolower(pathinfo($displayName,PATHINFO_EXTENSION));
                if(!in_array($ext,['sqlite','db'],true)) throw new RuntimeException('Envie um arquivo com extensão .sqlite ou .db.');
                $source=$tmp;
            }
            $token=bin2hex(random_bytes(24));
            $pending=linkexpress_restore_dir().'/pending-'.$token.'.sqlite';
            if(!copy($source,$pending)) throw new RuntimeException('Não foi possível preparar o arquivo para restauração.');
            @chmod($pending,0600);
            try{
                $info=validate_sqlite_backup_file($pending);
                $safety=create_database_snapshot('pre-restore');
            }catch(Throwable $restoreError){
                @unlink($pending);
                throw $restoreError;
            }
            $_SESSION['restore_token']=$token;
            $_SESSION['restore_pending_path']=$pending;
            $_SESSION['restore_admin_id']=(int)$user['id'];
            $_SESSION['restore_source_name']=$displayName;
            $_SESSION['restore_safety_backup']=basename($safety);
            log_activity('prepare_database_restore','settings',null,'Backup validado: '.$displayName.'; cópia de segurança: '.basename($safety));
            redirect('restore_database.php?token='.$token);
            break;

        case 'clinic_queue_save_settings':
            $companyId=(int)request('company_id');if($companyId<1||!company_is_clinic($companyId))throw new RuntimeException('Clínica inválida.');if(!is_admin())must_manage_clinic($companyId);
            $mode=(string)request('attendance_mode','day_queue');$modes=['virtual_queue','scheduled_time','day_queue','arrival_order','presential_ticket'];if(!in_array($mode,$modes,true))$mode='day_queue';
            $start=substr((string)request('start_time','08:00'),0,5);$end=substr((string)request('end_time','18:00'),0,5);if(!preg_match('/^\d{2}:\d{2}$/',$start)||!preg_match('/^\d{2}:\d{2}$/',$end)||$start>=$end)throw new RuntimeException('Defina um horário de início e término válido.');
            $max=max(1,min(1000,(int)request('max_clients_per_day',50)));$avg=max(5,min(240,(int)request('average_minutes',15)));$agenda=request('agenda_open')?1:0;$reorder=request('allow_reorder')?1:0;
            $messages=[];foreach(['message_near'=>'Seu atendimento está próximo.','message_next'=>'Você é o próximo!','message_turn'=>'É sua vez! Dirija-se ao atendimento.','message_cancelled'=>'Seu atendimento foi cancelado.','message_changed'=>'Seu horário foi alterado.'] as $k=>$d){$v=trim((string)request($k,$d));$messages[$k]=$v!==''?mb_substr($v,0,240):$d;}
            db()->prepare('INSERT INTO clinic_queue_settings(company_id,attendance_mode,agenda_open,start_time,end_time,max_clients_per_day,average_minutes,allow_reorder,message_near,message_next,message_turn,message_cancelled,message_changed,updated_by,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(company_id) DO UPDATE SET attendance_mode=excluded.attendance_mode,agenda_open=excluded.agenda_open,start_time=excluded.start_time,end_time=excluded.end_time,max_clients_per_day=excluded.max_clients_per_day,average_minutes=excluded.average_minutes,allow_reorder=excluded.allow_reorder,message_near=excluded.message_near,message_next=excluded.message_next,message_turn=excluded.message_turn,message_cancelled=excluded.message_cancelled,message_changed=excluded.message_changed,updated_by=excluded.updated_by,updated_at=excluded.updated_at')->execute([$companyId,$mode,$agenda,$start,$end,$max,$avg,$reorder,$messages['message_near'],$messages['message_next'],$messages['message_turn'],$messages['message_cancelled'],$messages['message_changed'],(int)$user['id'],now()]);
            back_to('clinic_queue','Configurações da fila atualizadas.');break;

        case 'clinic_queue_save_day':
            $companyId=(int)request('company_id');if($companyId<1||!company_is_clinic($companyId))throw new RuntimeException('Clínica inválida.');if(!is_admin())must_manage_clinic($companyId);
            $date=trim((string)request('service_date'));if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date))throw new RuntimeException('Data inválida.');
            $professionalId=(int)request('professional_id',0);$serviceId=(int)request('service_id',0);$status=(string)request('day_status','open');if(!in_array($status,['open','closed','holiday'],true))$status='open';
            $start=substr((string)request('start_time',''),0,5);$end=substr((string)request('end_time',''),0,5);$max=(int)request('max_clients',0);$note=mb_substr(trim((string)request('note')),0,240);
            if($professionalId>0){$q=db()->prepare('SELECT COUNT(*) FROM clinic_professionals WHERE id=? AND company_id=?');$q->execute([$professionalId,$companyId]);if(!(int)$q->fetchColumn())throw new RuntimeException('Profissional inválido.');}
            if($serviceId>0){$q=db()->prepare('SELECT COUNT(*) FROM clinic_specialties WHERE id=? AND company_id=?');$q->execute([$serviceId,$companyId]);if(!(int)$q->fetchColumn())throw new RuntimeException('Serviço inválido.');}
            if(request('repeat_weekly')==='1'){$until=trim((string)request('effective_until'));if($until!==''&&(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$until)||$until<$date))throw new RuntimeException('Informe uma data final válida para a repetição.');$weekday=(int)date('w',strtotime($date));db()->prepare('INSERT INTO clinic_queue_weekly_rules(company_id,professional_id,service_id,weekday,status,start_time,end_time,max_clients,effective_from,effective_until,note,created_by,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(company_id,professional_id,service_id,weekday) DO UPDATE SET status=excluded.status,start_time=excluded.start_time,end_time=excluded.end_time,max_clients=excluded.max_clients,effective_from=excluded.effective_from,effective_until=excluded.effective_until,note=excluded.note,updated_at=excluded.updated_at')->execute([$companyId,$professionalId,$serviceId,$weekday,$status,$start?:null,$end?:null,$max>0?$max:null,$date,$until?:null,$note,(int)$user['id'],now(),now()]);back_to('clinic_queue','Regra semanal de atendimento salva.');}
            db()->prepare('INSERT INTO clinic_queue_days(company_id,professional_id,service_id,service_date,status,start_time,end_time,max_clients,note,created_by,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(company_id,professional_id,service_id,service_date) DO UPDATE SET status=excluded.status,start_time=excluded.start_time,end_time=excluded.end_time,max_clients=excluded.max_clients,note=excluded.note,updated_at=excluded.updated_at')->execute([$companyId,$professionalId,$serviceId,$date,$status,$start?:null,$end?:null,$max>0?$max:null,$note,(int)$user['id'],now(),now()]);
            back_to('clinic_queue','Dia de atendimento salvo.');break;

        case 'clinic_queue_delete_weekly':
            $id=(int)request('id');$q=db()->prepare('SELECT company_id FROM clinic_queue_weekly_rules WHERE id=?');$q->execute([$id]);$companyId=(int)$q->fetchColumn();if($companyId<1)throw new RuntimeException('Regra semanal não encontrada.');if(!is_admin())must_manage_clinic($companyId);db()->prepare('DELETE FROM clinic_queue_weekly_rules WHERE id=?')->execute([$id]);back_to('clinic_queue','Regra semanal removida.');break;

        case 'clinic_queue_delete_day':
            $id=(int)request('id');$q=db()->prepare('SELECT company_id FROM clinic_queue_days WHERE id=?');$q->execute([$id]);$companyId=(int)$q->fetchColumn();if($companyId<1)throw new RuntimeException('Dia não encontrado.');if(!is_admin())must_manage_clinic($companyId);db()->prepare('DELETE FROM clinic_queue_days WHERE id=?')->execute([$id]);back_to('clinic_queue','Regra do dia removida.');break;

        case 'clinic_queue_client_block':
            $companyId=(int)request('company_id');$platformUserId=(int)request('platform_user_id');if($companyId<1||$platformUserId<1)throw new RuntimeException('Cliente inválido.');if(!is_admin())must_manage_clinic($companyId);$blocked=request('blocked')?1:0;$reason=mb_substr(trim((string)request('reason')),0,240);
            db()->prepare('INSERT INTO clinic_queue_client_blocks(company_id,platform_user_id,blocked,reason,updated_by,updated_at) VALUES(?,?,?,?,?,?) ON CONFLICT(company_id,platform_user_id) DO UPDATE SET blocked=excluded.blocked,reason=excluded.reason,updated_by=excluded.updated_by,updated_at=excluded.updated_at')->execute([$companyId,$platformUserId,$blocked,$reason,(int)$user['id'],now()]);back_to($returnPage==='clinic_queue_clients'?'clinic_queue_clients':'clinic_queue',$blocked?'Cliente bloqueado para novas filas.':'Cliente liberado para novas filas.');break;

        case 'clinic_queue_company_status':
            must_admin();$companyId=(int)request('company_id');$status=(string)request('status');if(!in_array($status,['active','inactive'],true))throw new RuntimeException('Status inválido.');if(!company_is_clinic($companyId))throw new RuntimeException('Estabelecimento inválido.');db()->prepare('UPDATE companies SET status=?,updated_at=? WHERE id=?')->execute([$status,now(),$companyId]);back_to('clinic_queue',$status==='active'?'Estabelecimento ativado.':'Estabelecimento desativado.');break;

        case 'clinic_queue_admin_add_service':
            must_admin();$companyId=(int)request('company_id');if($companyId<1||!company_is_clinic($companyId))throw new RuntimeException('Clínica inválida.');$name=trim((string)request('name'));if($name==='')throw new RuntimeException('Informe o nome do serviço.');$duration=max(5,min(480,(int)request('duration_minutes',30)));db()->prepare("INSERT INTO clinic_specialties(company_id,name,description,duration_minutes,price,status,service_kind,allow_patient_booking,created_at,updated_at) VALUES(?,?,?, ?,0,'active','consultation',1,?,?)")->execute([$companyId,$name,'Serviço cadastrado pelo painel da Fila Virtual',$duration,now(),now()]);back_to('clinic_queue','Serviço cadastrado para a fila.');break;

        case 'clinic_queue_admin_add_professional':
            must_admin();$companyId=(int)request('company_id');if($companyId<1||!company_is_clinic($companyId))throw new RuntimeException('Clínica inválida.');$name=trim((string)request('name'));$specialtyId=(int)request('specialty_id');if($name==='')throw new RuntimeException('Informe o nome do profissional.');if($specialtyId>0){$q=db()->prepare('SELECT COUNT(*) FROM clinic_specialties WHERE id=? AND company_id=?');$q->execute([$specialtyId,$companyId]);if(!(int)$q->fetchColumn())throw new RuntimeException('Serviço inválido.');}db()->prepare("INSERT INTO clinic_professionals(company_id,user_id,specialty_id,name,professional_type,share_whatsapp,status,link_state,admin_approval_status,created_at,updated_at) VALUES(?,NULL,?,?,'Atendimento',0,'active','active','not_required',?,?)")->execute([$companyId,$specialtyId?:null,$name,now(),now()]);back_to('clinic_queue','Profissional cadastrado para atendimento.');break;

        case 'clinic_queue_create_presential':
            $companyId=(int)request('company_id');if($companyId<1||!company_is_clinic($companyId))throw new RuntimeException('Clínica inválida.');if(!is_admin())must_manage_clinic($companyId);$patientId=(int)request('patient_id');$specialtyId=(int)request('specialty_id');$professionalId=(int)request('professional_id');$date=date('Y-m-d');$pq=db()->prepare('SELECT * FROM clinic_patients WHERE id=? AND company_id=?');$pq->execute([$patientId,$companyId]);$patient=$pq->fetch();if(!$patient)throw new RuntimeException('Paciente inválido.');$prq=db()->prepare("SELECT * FROM clinic_professionals WHERE id=? AND company_id=? AND status='active'");$prq->execute([$professionalId,$companyId]);$pr=$prq->fetch();if(!$pr)throw new RuntimeException('Profissional inválido.');$nq=db()->prepare('SELECT COALESCE(MAX(queue_number),0)+1 FROM clinic_virtual_queue WHERE company_id=? AND queue_date=?');$nq->execute([$companyId,$date]);$number=max(1,(int)$nq->fetchColumn());db()->prepare("INSERT INTO clinic_virtual_queue(company_id,patient_id,platform_user_id,specialty_id,professional_id,queue_date,queue_number,status,attendance_mode,joined_at,sort_order,created_at,updated_at) VALUES(?,?,?,?,?,?,?,'waiting','presential_ticket',?,?,?,?)")->execute([$companyId,$patientId,(int)($patient['platform_user_id']??0),$specialtyId,$professionalId,$date,$number,now(),$number,now(),now()]);$id=(int)db()->lastInsertId();if(!empty($patient['platform_user_id']))create_notification((int)$patient['platform_user_id'],'Senha presencial emitida','Sua senha é Nº '.str_pad((string)$number,2,'0',STR_PAD_LEFT).'.','info','dashboard.php?page=clinic_queue','clinic_queue',$id);back_to('clinic_queue','Senha presencial Nº '.str_pad((string)$number,2,'0',STR_PAD_LEFT).' emitida.');break;

        case 'clinic_queue_create':
            $companyId=(int)request('company_id');$specialtyId=(int)request('specialty_id');$professionalId=(int)request('professional_id');$date=trim((string)request('queue_date'));$queueTime=substr(trim((string)request('queue_time')),0,5);
            if($companyId<1||!company_is_clinic($companyId))throw new RuntimeException('Clínica inválida.');
            if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)||$date<date('Y-m-d'))throw new RuntimeException('Escolha uma data válida a partir de hoje.');if(strtotime($date)>strtotime('+180 days'))throw new RuntimeException('Escolha uma data dentro dos próximos 180 dias.');
            $cq=db()->prepare("SELECT * FROM companies WHERE id=? AND status='active'");$cq->execute([$companyId]);$clinic=$cq->fetch();if(!$clinic)throw new RuntimeException('Clínica indisponível.');if(clinic_queue_client_is_blocked($companyId,(int)$user['id']))throw new RuntimeException('Seu acesso à fila deste estabelecimento está bloqueado. Entre em contato com a clínica.');
            $sq=db()->prepare("SELECT * FROM clinic_specialties WHERE id=? AND company_id=? AND status='active' AND COALESCE(service_kind,'consultation')<>'exam'");$sq->execute([$specialtyId,$companyId]);$sp=$sq->fetch();if(!$sp)throw new RuntimeException('Serviço inválido para esta clínica.');
            $pq=db()->prepare("SELECT * FROM clinic_professionals WHERE id=? AND company_id=? AND status='active' AND COALESCE(link_state,'active')='active'");$pq->execute([$professionalId,$companyId]);$pr=$pq->fetch();if(!$pr)throw new RuntimeException('Profissional indisponível.');if(!clinic_queue_professional_can_service($companyId,$professionalId,$specialtyId))throw new RuntimeException('Este profissional não está liberado para o serviço selecionado na agenda da Fila Virtual.');
            $capacity=clinic_queue_capacity_for_day($companyId,$date,$professionalId,$specialtyId);$settings=$capacity['settings'];$mode=(string)($settings['attendance_mode']??'day_queue');if(!$capacity['open'])throw new RuntimeException('Não existe expediente liberado para este profissional/serviço nesta data. Escolha uma das datas disponíveis.');if($mode==='presential_ticket')throw new RuntimeException('Este estabelecimento trabalha com senha presencial. Retire sua senha no local.');
            if($mode==='scheduled_time'){if(!preg_match('/^\d{2}:\d{2}$/',$queueTime))throw new RuntimeException('Escolha o horário do atendimento.');$validSlots=clinic_queue_time_slots_for_day($companyId,$professionalId,$specialtyId,$date,(int)($sp['duration_minutes']??0));if(!in_array($queueTime,$validSlots,true))throw new RuntimeException('Este horário não está mais disponível. Escolha outro horário livre.');}
            else $queueTime=null;
            $rule=$capacity['rule'];$countSql="SELECT COUNT(*) FROM clinic_virtual_queue WHERE company_id=? AND queue_date=? AND status NOT IN ('cancelled','completed','skipped')";$countParams=[$companyId,$date];if(!empty($rule['professional_id'])){$countSql.=' AND professional_id=?';$countParams[]=(int)$rule['professional_id'];}if(!empty($rule['service_id'])){$countSql.=' AND specialty_id=?';$countParams[]=(int)$rule['service_id'];}$countQ=db()->prepare($countSql);$countQ->execute($countParams);if((int)$countQ->fetchColumn()>=(int)$capacity['max'])throw new RuntimeException('As vagas deste dia já foram preenchidas.');
            $patient=clinic_patient_for_user((int)$user['id'],$companyId);if(!$patient)throw new RuntimeException('Você precisa estar vinculado como paciente desta clínica antes de usar a fila virtual.');$patientId=(int)$patient['id'];
            $dup=db()->prepare("SELECT id FROM clinic_virtual_queue WHERE company_id=? AND patient_id=? AND professional_id=? AND queue_date=? AND status NOT IN ('completed','cancelled','skipped') LIMIT 1");$dup->execute([$companyId,$patientId,$professionalId,$date]);if($dup->fetchColumn())throw new RuntimeException('Você já possui uma reserva ativa com este profissional nessa data.');
            $nq=db()->prepare("SELECT COALESCE(MAX(queue_number),0)+1 FROM clinic_virtual_queue WHERE company_id=? AND queue_date=?");$nq->execute([$companyId,$date]);$number=max(1,(int)$nq->fetchColumn());
            $status='scheduled';$joined=null;if($date===date('Y-m-d')&&in_array($mode,['virtual_queue','day_queue'],true)){$status='waiting';$joined=now();}
            db()->prepare('INSERT INTO clinic_virtual_queue(company_id,patient_id,platform_user_id,specialty_id,professional_id,queue_date,queue_time,queue_number,status,attendance_mode,joined_at,sort_order,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute([$companyId,$patientId,(int)$user['id'],$specialtyId,$professionalId,$date,$queueTime,$number,$status,$mode,$joined,$number,now(),now()]);$id=(int)db()->lastInsertId();
            $owner=(int)($clinic['owner_user_id']??0);$label=clinic_queue_mode_label($mode);if($owner>0)create_notification($owner,'Novo atendimento na fila',$user['name'].' reservou a senha Nº '.str_pad((string)$number,2,'0',STR_PAD_LEFT).' para '.date('d/m/Y',strtotime($date)).'.','info','dashboard.php?page=clinic_queue','clinic_queue',$id);if(!empty($pr['user_id']))create_notification((int)$pr['user_id'],'Novo atendimento',$user['name'].' • '.$label.' • '.date('d/m/Y',strtotime($date)).'.','info','dashboard.php?page=clinic_queue','clinic_queue',$id);
            back_to('clinic_queue','Atendimento reservado para '.date('d/m/Y',strtotime($date)).'. Senha Nº '.str_pad((string)$number,2,'0',STR_PAD_LEFT).'.');break;

        case 'clinic_queue_join_today':
            $id=(int)request('id');$q=db()->prepare("SELECT * FROM clinic_virtual_queue WHERE id=? AND platform_user_id=?");$q->execute([$id,(int)$user['id']]);$entry=$q->fetch();if(!$entry)throw new RuntimeException('Reserva não encontrada.');if($entry['queue_date']!==date('Y-m-d'))throw new RuntimeException('Você só pode entrar na fila no dia marcado.');if($entry['status']!=='scheduled')throw new RuntimeException('Esta reserva já foi atualizada.');if(($entry['attendance_mode']??'day_queue')==='scheduled_time')throw new RuntimeException('Seu atendimento é por hora marcada; aguarde o horário confirmado.');
            $number=(int)$entry['queue_number'];if(($entry['attendance_mode']??'')==='arrival_order'){$nq=db()->prepare("SELECT COALESCE(MAX(queue_number),0)+1 FROM clinic_virtual_queue WHERE company_id=? AND queue_date=? AND status IN ('waiting','in_service','completed')");$nq->execute([(int)$entry['company_id'],(string)$entry['queue_date']]);$number=max(1,(int)$nq->fetchColumn());}
            db()->prepare("UPDATE clinic_virtual_queue SET status='waiting',queue_number=?,sort_order=?,joined_at=?,updated_at=? WHERE id=?")->execute([$number,$number,now(),now(),$id]);back_to('clinic_queue','Entrada confirmada. Acompanhe sua posição na fila.');break;

        case 'clinic_queue_cancel':
            $id=(int)request('id');$q=db()->prepare("SELECT * FROM clinic_virtual_queue WHERE id=? AND platform_user_id=?");$q->execute([$id,(int)$user['id']]);$entry=$q->fetch();if(!$entry)throw new RuntimeException('Fila não encontrada.');if(!in_array($entry['status'],['scheduled','waiting'],true))throw new RuntimeException('Este atendimento não pode mais ser cancelado.');db()->prepare("UPDATE clinic_virtual_queue SET status='cancelled',cancelled_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$id]);back_to('clinic_queue','Reserva/fila cancelada.');break;

        case 'clinic_queue_call':
        case 'clinic_queue_finish':
        case 'clinic_queue_cancel_manage':
        case 'clinic_queue_recall':
        case 'clinic_queue_skip':
        case 'clinic_queue_no_show':
            $id=(int)request('id');$q=db()->prepare("SELECT q.*,cp.platform_user_id,s.duration_minutes FROM clinic_virtual_queue q JOIN clinic_patients cp ON cp.id=q.patient_id LEFT JOIN clinic_specialties s ON s.id=q.specialty_id WHERE q.id=?");$q->execute([$id]);$entry=$q->fetch();if(!$entry||!clinic_queue_can_manage_entry($user,$entry))throw new RuntimeException('Você não pode administrar esta fila.');$settings=clinic_queue_settings((int)$entry['company_id']);$patientUid=(int)$entry['platform_user_id'];
            if($action==='clinic_queue_recall'){if(!in_array($entry['status'],['in_service','skipped'],true))throw new RuntimeException('Só é possível rechamar quem já foi chamado ou teve a senha pulada.');if($entry['status']==='skipped'){$busy=db()->prepare("SELECT COUNT(*) FROM clinic_virtual_queue WHERE company_id=? AND professional_id=? AND queue_date=? AND status='in_service' AND id<>?");$busy->execute([(int)$entry['company_id'],(int)$entry['professional_id'],(string)$entry['queue_date'],$id]);if((int)$busy->fetchColumn()>0)throw new RuntimeException('Finalize o atendimento atual antes de rechamar esta senha.');db()->prepare("UPDATE clinic_virtual_queue SET status='in_service',called_at=?,recalled_at=?,updated_at=? WHERE id=?")->execute([now(),now(),now(),$id]);}else db()->prepare('UPDATE clinic_virtual_queue SET recalled_at=?,updated_at=? WHERE id=?')->execute([now(),now(),$id]);if($patientUid)create_notification($patientUid,'🔔 Rechamada',(string)$settings['message_turn'],'warning','dashboard.php?page=clinic_queue','clinic_queue',$id);back_to('clinic_queue','Cliente rechamado.');}
            if($action==='clinic_queue_skip'){if(!in_array($entry['status'],['waiting','scheduled'],true))throw new RuntimeException('Esta senha não pode ser pulada.');db()->prepare("UPDATE clinic_virtual_queue SET status='skipped',skipped_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$id]);if($patientUid)create_notification($patientUid,'Senha pulada','Sua senha foi pulada pela equipe. Procure o atendimento para orientação.','warning','dashboard.php?page=clinic_queue','clinic_queue',$id);back_to('clinic_queue','Senha pulada.');}
            if($action==='clinic_queue_no_show'){if(!in_array($entry['status'],['waiting','scheduled'],true))throw new RuntimeException('Não é possível registrar falta neste atendimento.');db()->prepare("UPDATE clinic_virtual_queue SET status='no_show',cancelled_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$id]);if($patientUid)create_notification($patientUid,'Falta registrada','O estabelecimento registrou ausência neste atendimento.','warning','dashboard.php?page=clinic_queue','clinic_queue',$id);back_to('clinic_queue','Falta registrada no histórico do cliente.');}
            if($action==='clinic_queue_call'){
                if(!in_array($entry['status'],['waiting','scheduled'],true))throw new RuntimeException('A senha precisa estar aguardando/confirmada para ser chamada.');if($entry['status']==='scheduled'&&($entry['attendance_mode']??'')!=='scheduled_time')throw new RuntimeException('O cliente ainda não entrou na fila.');
                $busy=db()->prepare("SELECT COUNT(*) FROM clinic_virtual_queue WHERE company_id=? AND professional_id=? AND queue_date=? AND status='in_service' AND id<>?");$busy->execute([(int)$entry['company_id'],(int)$entry['professional_id'],(string)$entry['queue_date'],$id]);if((int)$busy->fetchColumn()>0)throw new RuntimeException('Finalize o atendimento atual antes de chamar outro paciente.');
                if($entry['status']==='waiting'&&!is_admin()){$first=db()->prepare("SELECT id FROM clinic_virtual_queue WHERE company_id=? AND professional_id=? AND queue_date=? AND status='waiting' ORDER BY CASE WHEN sort_order>0 THEN sort_order ELSE queue_number END,queue_number LIMIT 1");$first->execute([(int)$entry['company_id'],(int)$entry['professional_id'],(string)$entry['queue_date']]);if((int)$first->fetchColumn()!==$id)throw new RuntimeException('Chame primeiro a próxima senha da fila deste profissional.');}
                db()->prepare("UPDATE clinic_virtual_queue SET status='in_service',called_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$id]);if($patientUid)create_notification($patientUid,'🟢 É sua vez!',(string)$settings['message_turn'],'success','dashboard.php?page=clinic_queue','clinic_queue',$id);
                $next=db()->prepare("SELECT id,platform_user_id,queue_number FROM clinic_virtual_queue WHERE company_id=? AND professional_id=? AND queue_date=? AND status='waiting' AND id<>? ORDER BY CASE WHEN sort_order>0 THEN sort_order ELSE queue_number END,queue_number LIMIT 2");$next->execute([(int)$entry['company_id'],(int)$entry['professional_id'],(string)$entry['queue_date'],$id]);$up=$next->fetchAll();if(isset($up[0])&&!empty($up[0]['platform_user_id'])){create_notification((int)$up[0]['platform_user_id'],'🔔 Você é o próximo!',(string)$settings['message_next'],'warning','dashboard.php?page=clinic_queue','clinic_queue',(int)$up[0]['id']);db()->prepare('UPDATE clinic_virtual_queue SET next_notified_at=?,updated_at=? WHERE id=?')->execute([now(),now(),(int)$up[0]['id']]);}if(isset($up[1])&&!empty($up[1]['platform_user_id']))create_notification((int)$up[1]['platform_user_id'],'⏱️ Atendimento próximo',(string)$settings['message_near'],'info','dashboard.php?page=clinic_queue','clinic_queue',(int)$up[1]['id']);back_to('clinic_queue','Paciente chamado.');
            }
            if($action==='clinic_queue_finish'){if($entry['status']!=='in_service')throw new RuntimeException('Este paciente não está em atendimento.');db()->prepare("UPDATE clinic_virtual_queue SET status='completed',completed_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$id]);if($patientUid)create_notification($patientUid,'Atendimento finalizado','Seu atendimento foi finalizado.','success','dashboard.php?page=clinic_queue','clinic_queue',$id);back_to('clinic_queue','Atendimento finalizado.');}
            if(!in_array($entry['status'],['scheduled','waiting','skipped'],true))throw new RuntimeException('Esta entrada não pode mais ser cancelada.');db()->prepare("UPDATE clinic_virtual_queue SET status='cancelled',cancelled_at=?,updated_at=? WHERE id=?")->execute([now(),now(),$id]);if($patientUid)create_notification($patientUid,'Atendimento cancelado',(string)$settings['message_cancelled'],'warning','dashboard.php?page=clinic_queue','clinic_queue',$id);back_to('clinic_queue','Entrada da fila cancelada.');break;

        case 'clinic_queue_reschedule_manage':
            $id=(int)request('id');$newDate=trim((string)request('new_date'));$newTime=substr(trim((string)request('new_time')),0,5);$q=db()->prepare('SELECT * FROM clinic_virtual_queue WHERE id=?');$q->execute([$id]);$entry=$q->fetch();if(!$entry||!clinic_queue_can_manage_entry($user,$entry))throw new RuntimeException('Sem permissão.');if(!in_array($entry['status'],['scheduled','waiting','skipped'],true))throw new RuntimeException('Este atendimento não pode ser remarcado.');if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$newDate)||$newDate<date('Y-m-d'))throw new RuntimeException('Escolha uma nova data válida.');$settings=clinic_queue_settings((int)$entry['company_id']);$mode=(string)($entry['attendance_mode']?:$settings['attendance_mode']);$capacity=clinic_queue_capacity_for_day((int)$entry['company_id'],$newDate,(int)$entry['professional_id'],(int)$entry['specialty_id']);if(!$capacity['open'])throw new RuntimeException('A agenda está fechada para a nova data.');if($mode==='scheduled_time'){if(!preg_match('/^\d{2}:\d{2}$/',$newTime))throw new RuntimeException('Informe o novo horário.');if($newTime<$capacity['start']||$newTime>$capacity['end'])throw new RuntimeException('O novo horário está fora do período de atendimento.');$slot=db()->prepare("SELECT COUNT(*) FROM clinic_virtual_queue WHERE company_id=? AND professional_id=? AND queue_date=? AND queue_time=? AND id<>? AND status NOT IN ('cancelled','completed','skipped','no_show')");$slot->execute([(int)$entry['company_id'],(int)$entry['professional_id'],$newDate,$newTime,$id]);if((int)$slot->fetchColumn()>0)throw new RuntimeException('O novo horário já está ocupado.');}else $newTime=null;
            $nq=db()->prepare('SELECT COALESCE(MAX(queue_number),0)+1 FROM clinic_virtual_queue WHERE company_id=? AND queue_date=? AND id<>?');$nq->execute([(int)$entry['company_id'],$newDate,$id]);$number=max(1,(int)$nq->fetchColumn());$newStatus='scheduled';$joined=null;if($newDate===date('Y-m-d')&&in_array($mode,['virtual_queue','day_queue'],true)){$newStatus='waiting';$joined=now();}db()->prepare('UPDATE clinic_virtual_queue SET queue_date=?,queue_time=?,queue_number=?,sort_order=?,status=?,joined_at=?,called_at=NULL,skipped_at=NULL,updated_at=? WHERE id=?')->execute([$newDate,$newTime,$number,$number,$newStatus,$joined,now(),$id]);if(!empty($entry['platform_user_id']))create_notification((int)$entry['platform_user_id'],'Atendimento alterado',(string)$settings['message_changed'].' Nova data: '.date('d/m/Y',strtotime($newDate)).($newTime?' às '.$newTime:''),'warning','dashboard.php?page=clinic_queue','clinic_queue',$id);back_to('clinic_queue','Data/horário do atendimento alterados.');break;

        case 'clinic_queue_transfer':
            $id=(int)request('id');$target=(int)request('target_professional_id');$q=db()->prepare('SELECT * FROM clinic_virtual_queue WHERE id=?');$q->execute([$id]);$entry=$q->fetch();if(!$entry||!clinic_queue_can_manage_entry($user,$entry))throw new RuntimeException('Sem permissão.');if(!in_array($entry['status'],['scheduled','waiting','skipped'],true))throw new RuntimeException('Este atendimento não pode ser transferido.');$pq=db()->prepare("SELECT * FROM clinic_professionals WHERE id=? AND company_id=? AND status='active' AND COALESCE(link_state,'active')='active'");$pq->execute([$target,(int)$entry['company_id']]);$pr=$pq->fetch();if(!$pr)throw new RuntimeException('Fila de destino inválida.');$nq=db()->prepare('SELECT COALESCE(MAX(queue_number),0)+1 FROM clinic_virtual_queue WHERE company_id=? AND professional_id=? AND queue_date=?');$nq->execute([(int)$entry['company_id'],$target,(string)$entry['queue_date']]);$num=max(1,(int)$nq->fetchColumn());db()->prepare("UPDATE clinic_virtual_queue SET professional_id=?,transferred_from_professional_id=?,queue_number=?,sort_order=?,status=CASE WHEN status='skipped' THEN 'waiting' ELSE status END,updated_at=? WHERE id=?")->execute([$target,(int)$entry['professional_id'],$num,$num,now(),$id]);$settings=clinic_queue_settings((int)$entry['company_id']);if(!empty($entry['platform_user_id']))create_notification((int)$entry['platform_user_id'],'Atendimento transferido',(string)$settings['message_changed'].' Novo profissional: '.$pr['name'].'.','info','dashboard.php?page=clinic_queue','clinic_queue',$id);back_to('clinic_queue','Cliente transferido para outra fila.');break;

        case 'clinic_queue_reorder':
            $id=(int)request('id');$direction=(string)request('direction');$q=db()->prepare('SELECT * FROM clinic_virtual_queue WHERE id=?');$q->execute([$id]);$entry=$q->fetch();if(!$entry||!clinic_queue_can_manage_entry($user,$entry))throw new RuntimeException('Sem permissão.');$settings=clinic_queue_settings((int)$entry['company_id']);if(!is_admin()&&empty($settings['allow_reorder']))throw new RuntimeException('A alteração manual da ordem está desativada.');if($entry['status']!=='waiting')throw new RuntimeException('Só é possível reordenar senhas aguardando.');$current=(int)($entry['sort_order']?:$entry['queue_number']);$op=$direction==='up'?'<':'>';$ord=$direction==='up'?'DESC':'ASC';$q=db()->prepare("SELECT id,CASE WHEN sort_order>0 THEN sort_order ELSE queue_number END pos FROM clinic_virtual_queue WHERE company_id=? AND professional_id=? AND queue_date=? AND status='waiting' AND (CASE WHEN sort_order>0 THEN sort_order ELSE queue_number END) $op ? ORDER BY pos $ord LIMIT 1");$q->execute([(int)$entry['company_id'],(int)$entry['professional_id'],(string)$entry['queue_date'],$current]);$other=$q->fetch();if($other){db()->beginTransaction();db()->prepare('UPDATE clinic_virtual_queue SET sort_order=?,updated_at=? WHERE id=?')->execute([(int)$other['pos'],now(),$id]);db()->prepare('UPDATE clinic_virtual_queue SET sort_order=?,updated_at=? WHERE id=?')->execute([$current,now(),(int)$other['id']]);db()->commit();}back_to('clinic_queue','Ordem da fila atualizada.');break;

        default: throw new RuntimeException('Ação desconhecida.');
    }
} catch (Throwable $e) {
    if (db()->inTransaction()) db()->rollBack();
    flash('error', $e->getMessage());
    if ($returnPage === 'marketplace') redirect('marketplace.php');
    redirect('dashboard.php?page=' . $returnPage);
}
