Files
2026-11-04 09:37:20 +02:00

30 lines
14 KiB
PHP

<?php $e=fn($v)=>htmlspecialchars((string)$v,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8'); $money=fn($v,$currency='MZN')=>number_format(intdiv((int)$v,100),0,'.',' ').'.'.str_pad((string)abs((int)$v%100),2,'0',STR_PAD_LEFT).' '.$e($currency); ?>
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Obrigações a fornecedores — ensaio</title>
<style>body{font:16px system-ui;background:#f3f6f5;color:#142b28;margin:0}main{max-width:1100px;margin:auto;padding:24px}header,section{background:white;padding:24px;margin:18px 0;border:1px solid #d5e0dc;border-radius:12px}h1{font-size:28px}h2{font-size:20px}.notice{padding:16px;background:#fff2cc}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px}label{display:block;margin:12px 0}select,textarea,button{font:inherit;padding:10px;max-width:100%}textarea{display:block;width:90%}button{background:#125a4b;color:white;border:0;border-radius:6px;cursor:pointer}button:disabled{opacity:.5}table{border-collapse:collapse;width:100%}td,th{text-align:left;padding:12px;border-bottom:1px solid #ddd}.scroll{overflow:auto}.amount{font-size:26px;font-weight:bold}pre{white-space:pre-wrap;overflow-wrap:anywhere}a{color:#125a4b}</style></head><body><main>
<header><?php if(session()->get('payables_demo_entry')): ?><p><a href="/demo">Trocar função ou sair da demonstração</a></p><?php endif ?><a href="<?= $e($settings['return_url']??'/') ?>">← <?= $e($settings['return_label']??'Voltar à aplicação') ?></a><p><?= $e($settings['host']) ?> · <?= $e($settings['actor']) ?></p><h1>Obrigações a fornecedores</h1><p><?= $e($settings['organisation']) ?></p><p class="notice">ENSAIO SINTÉTICO — SEM UTILIZAÇÃO REAL. Documento, dívida, pagamento e contabilidade são factos distintos. Este percurso reconhece o principal e, quando activado, preserva o histórico existente.</p><p>Fornecedor, montantes e conferência são reutilizados da factura. Vencimento desconhecido não é substituído por uma data ou zero dias de atraso.</p></header>
<?php if(isset($data['history_receipt_notice'])): ?><p class="notice"><?= $e($data['history_receipt_notice']) ?></p><?php endif ?>
<?php if(isset($settings['report_url'])): ?><p><a href="<?= $e($settings['report_url'].'?'.http_build_query(array_intersect_key($data['listing_query'],array_flip(['q','source'])),'','&',PHP_QUERY_RFC3986)) ?>">Relatório das obrigações reconhecidas abrangidas pelo filtro</a></p><?php endif ?>
<?php if(isset($data['listing_query']['source'])): ?><p>Conferência da origem #<?= $e($data['listing_query']['source']) ?>. <a href="<?= $e($settings['page_url']??'/supplier-payables') ?>">Consultar todas as origens</a></p><?php endif ?>
<?php $pageNav=function(string $key)use($data,$settings,$e){if(!isset($data['pagination'][$key]))return;$p=$data['pagination'][$key];$param=['documents'=>'page','proposals'=>'proposal_page','events'=>'event_page'][$key];$label=['documents'=>'documentos','proposals'=>'propostas','events'=>'histórico'][$key];echo '<nav aria-label="Paginação '.$e($label).'"><p>Página '.$e($p['page']).' de '.$e($p['last_page']).' · '.$e($p['total']).' registos</p>';foreach([-1=>'Anterior',1=>'Seguinte'] as $delta=>$label){$target=$p['page']+$delta;if($target<1||$target>$p['last_page'])continue;$query=array_replace($data['listing_query'],[$param=>$target]);if($key==='documents'){$query['proposal_page']=1;$query['event_page']=1;}$url=($settings['page_url']??'/supplier-payables').'?'.http_build_query($query,'','&',PHP_QUERY_RFC3986);echo '<a href="'.$e($url).'">'.$label.'</a> ';}echo '</nav>';}; ?>
<?php if(isset($data['pagination'])): ?><section><h2>Filtrar documentos</h2><form method="get" action="<?= $e($settings['page_url']??'/supplier-payables') ?>"><?php foreach(['invoice','source'] as $focus): if(isset($data['listing_query'][$focus])): ?><input type="hidden" name="<?= $focus ?>" value="<?= $e($data['listing_query'][$focus]) ?>"><?php endif; endforeach ?><label>Número ou fornecedor<input name="q" maxlength="100" value="<?= $e($data['listing_query']['q']) ?>"></label><label>Reconhecimento<select name="recognition"><?php foreach(['all'=>'Todos','recognised'=>'Reconhecidos','unrecognised'=>'Por reconhecer'] as $v=>$label): ?><option value="<?= $e($v) ?>" <?= $data['listing_query']['recognition']===$v?'selected':'' ?>><?= $e($label) ?></option><?php endforeach ?></select></label><label>Documentos por página<select name="per_page"><?php foreach([25,100] as $size): ?><option value="<?= $size ?>" <?= $data['listing_query']['per_page']===$size?'selected':'' ?>><?= $size ?></option><?php endforeach ?></select></label><button>Aplicar filtros</button></form><p>Obrigações, propostas e eventos abaixo pertencem aos documentos desta página. A pesquisa e o estado são aplicados antes da paginação.</p></section><?php endif ?>
<?php if($data['payments_available']??false): ?><p><a href="<?= $e('/supplier-payments'.(isset($data['listing_query']['source'])?'?'.http_build_query(['source'=>$data['listing_query']['source']],'','&',PHP_QUERY_RFC3986):'')) ?>">Pagamentos e afectações sintéticas — sem execução bancária</a></p><?php endif ?><p id="status" role="status"></p><p id="error" role="alert"></p><button id="reload">Actualizar consulta</button>
<section><h2>Facturas de origem</h2><div class="scroll"><table><thead><tr><th>Documento</th><th>Fornecedor</th><th>Base</th><th>Imposto documental</th><th>Total</th><th>Conferência / revisão</th></tr></thead><tbody>
<?php foreach($data['documents'] as $d): ?><tr><td><?= $e($d['number']??'Indeterminado') ?><?php if(($settings['document_filter']??null)==='source'): ?><p><a href="<?= $e(($settings['page_url']??'/supplier-payables').'?'.http_build_query(['source'=>$d['local_id']],'','&',PHP_QUERY_RFC3986)) ?>">Conferir evidências desta origem</a></p><?php endif ?></td><td><?= $e($d['supplier_name']??'Indeterminado') ?></td><td><?= isset($d['subtotal_minor'])?$money($d['subtotal_minor']):'Indeterminado' ?></td><td><?= isset($d['tax_minor'])?$money($d['tax_minor']):'Indeterminado' ?></td><td><?= isset($d['total_minor'])?$money($d['total_minor']):'Indeterminado' ?></td><td><?= $e($d['status']) ?><details><summary>Origem e pendências</summary><pre><?= $e(json_encode($d,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)) ?></pre></details></td></tr><?php endforeach ?>
</tbody></table></div><?php $pageNav('documents'); ?></section>
<?php if($data['permissions']['prepare']): ?><section><h2>Preparar a obrigação</h2><form data-action="prepare"><label>Factura existente<select name="id"><?php foreach($data['documents'] as $d): ?><option value="<?= $e($d['local_id']) ?>"><?= $e($d['number']) ?> · <?= $money($d['total_minor']) ?></option><?php endforeach ?></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000">Proposta sintética com base na factura conferida.</textarea></label><p>Sem edição de fornecedor, valor ou imposto. Vencimento mantém-se desconhecido neste recorte.</p><button>Preparar da origem</button></form></section><?php endif ?>
<?php $historyDocuments=array_filter($data['documents'],fn($d)=>($d['payment_count']??0)>0); if(($data['history_import_available']??false)&&$data['permissions']['prepare']&&$historyDocuments): ?><section><h2>Incorporar factura com pagamentos anteriores</h2><p>Preserva o principal integral e liga os pagamentos originais uma única vez. Só os confirmados reduzem o saldo; pendentes e rejeitados mantêm o seu estado. Não executa nem confirma pagamentos.</p><form data-action="prepare_history"><label>Factura com histórico<select name="id"><?php foreach($historyDocuments as $d): ?><option value="<?= $e($d['local_id']) ?>"><?= $e($d['number']) ?> · <?= $money($d['total_minor']) ?></option><?php endforeach ?></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000">Passagem de responsabilidade com preservação dos pagamentos históricos.</textarea></label><button>Preparar passagem com histórico</button></form></section><?php endif ?>
<section><h2>Propostas e decisões</h2><?php if(!$data['proposals']->count()): ?><p>Ainda não há propostas.</p><?php endif ?>
<?php foreach($data['proposals'] as $p): ?><article><h3>Proposta #<?= $e($p->id) ?> · captura <?= $e($p->capture_version) ?></h3><p>Preparador #<?= $e($p->prepared_by) ?> · <?= $e($p->created_at) ?></p><p><?= $e($p->reason) ?></p><details><summary>Fotografia e pendências</summary><pre><?= $e($p->snapshot) ?></pre><pre><?= $e($p->missing) ?></pre></details>
<?php $history=json_decode($p->snapshot,true)['_history_import']??null; if($history): ?><p>Histórico proposto: confirmados <?= $money($history['confirmed_minor']) ?> · pendentes <?= $money($history['pending_minor']) ?> · rejeitados <?= $money($history['rejected_minor']) ?>. Os pendentes e rejeitados não reduzem o saldo.</p><?php endif ?>
<?php if($data['permissions']['validate']&&(!$history||($data['history_import_available']??false))): ?><form data-action="<?= $history?'validate_history':'validate' ?>"><input type="hidden" name="id" value="<?= $e($p->id) ?>"><label>Fundamento da decisão<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button><?= $history?'Validar passagem e preservar histórico':'Validar revisão exacta e reconhecer' ?></button></form><?php endif ?></article><?php endforeach ?><?php $pageNav('proposals'); ?></section>
<section><h2>Obrigações reconhecidas</h2><?php if(!$data['obligations']): ?><p>Nenhuma obrigação reconhecida. Proposta ou factura conferida não é decisão de reconhecimento.</p><?php endif ?><div class="grid">
<?php foreach($data['obligations'] as $o): ?><article><h3>Obrigação #<?= $e($o['id']) ?></h3><p class="amount"><?= $money($o['principal_minor']) ?></p><p>Principal reconhecido — vencimento pendente</p><?php if($o['history_imported']??false): ?><p>Pagamentos históricos confirmados: <?= $money($o['confirmed_payments_minor']) ?></p><p>Pendentes: <?= $money($o['pending_payments_minor']) ?> · Rejeitados: <?= $money($o['rejected_payments_minor']) ?> — sem abatimento.</p><p><strong>Saldo por liquidar: <?= $o['balance_minor']===null?'Pendente de revisão':$money($o['balance_minor']) ?></strong></p><?php if($data['history_decisions_available']??false): ?><p>Importação original preservada. A conferência independente dos pendentes actualiza este saldo uma única vez.</p><p><a href="/pagamentos">Conferir pagamentos pendentes →</a></p><?php else: ?><p>Importação original preservada. Conferência de pendentes não activada nesta instalação.</p><?php endif ?><?php endif ?><?php if(!($o['history_imported']??false)): ?><p>Afectações confirmadas neste percurso: <?= $money($o['allocated_payments_minor']??0) ?></p><p><strong>Saldo por liquidar: <?= $o['balance_minor']===null?'Pendente de revisão':$money($o['balance_minor']) ?></strong></p><?php endif ?><?php if($o['source_conflict']??false): ?><p role="alert">Origem ou histórico divergente; saldo não presumido. O principal original permanece preservado.</p><?php endif ?><p>Atraso indeterminado · Dias de atraso: sem valor</p><p>Validador #<?= $e($o['recognised_by']) ?> · <?= $e($o['created_at']) ?></p><details><summary>Origem, versão e decisão</summary><pre><?= $e(json_encode($o,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)) ?></pre></details></article><?php endforeach ?></div></section>
<section><h2>Histórico durável</h2><?php foreach($data['events'] as $event): ?><details><summary>#<?= $e($event->id) ?> · <?= $e($event->kind) ?> · actor #<?= $e($event->actor_id) ?> · <?= $e($event->created_at) ?></summary><pre><?= $e($event->payload) ?></pre></details><?php endforeach ?><?php $pageNav('events'); ?></section>
<p>Sem execução de novos pagamentos, cobrança, efeitos de stock, novo consumo orçamental ou lançamento automático. A passagem de histórico não confirma pagamentos pendentes.</p>
</main><script>
const settings=<?= json_encode($settings,JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_THROW_ON_ERROR) ?>;
if(settings.ledger_flow_url){const button=document.createElement('button');button.textContent='Conferir percurso até ao lançamento';document.querySelector('#status').before(button);button.onclick=async()=>{button.disabled=true;try{const r=await fetch('/api/context',{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':settings.csrf},body:JSON.stringify({...settings.ledger_flow_scope,module_id:'accounting'})});const data=await r.json();if(!r.ok)throw Error(data.message||'Módulo indisponível.');location.assign(settings.ledger_flow_url);}catch(e){document.querySelector('#error').textContent=e.message;button.disabled=false;}};}
document.querySelector('#reload').onclick=()=>location.reload();
for(const form of document.querySelectorAll('form[data-action]'))form.onsubmit=async event=>{event.preventDefault();const body=Object.fromEntries(new FormData(form));body.id=Number(body.id);const signature=JSON.stringify(body);if(form.dataset.signature!==signature){form.dataset.signature=signature;form.dataset.key=crypto.randomUUID();}const button=form.querySelector('button');button.disabled=true;document.querySelector('#error').textContent='';try{const r=await fetch(settings.api+'/'+form.dataset.action,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':settings.csrf,'X-SGF-Context':settings.context??'','Idempotency-Key':form.dataset.key},body:JSON.stringify(body)});const d=await r.json();if(!r.ok)throw new Error(d.message??Object.values(d.errors??{}).flat().join(' '));if(d.state==='conflict'){document.querySelector('#error').textContent=d.reason+' Consulte o histórico.';return;}location.reload();}catch(e){document.querySelector('#error').textContent=e.message;}finally{button.disabled=false;}};
</script></body></html>