Files
SGF/packages/payables/ui/settlements.php
T
2026-10-30 23:20:27 +02:00

33 lines
13 KiB
PHP

<?php
$e = static fn ($v) => htmlspecialchars((string) $v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$money = static fn ($v) => $v === null ? 'Indeterminado' : number_format(intdiv((int) $v, 100), 0, '.', ' ').'.'.str_pad((string) abs((int) $v % 100), 2, '0', STR_PAD_LEFT).' MZN';
$eligible = array_values(array_filter($data['obligations'], static fn ($o) => ! $o['source_conflict'] && ! $o['history_imported']));
$confirmed = array_values(array_filter($data['payments'], static fn ($p) => $p['state'] === 'confirmed' && $p['unallocated_minor'] > 0));
$options = static function () use ($eligible, $e, $money): void {
foreach ($eligible as $o) echo '<option value="'.$e($o['id']).'" data-revision="'.$e($o['source_revision']).'">#'.$e($o['id']).' · '.$e($o['origin_snapshot']['supplier_name'] ?? 'Fornecedor').' · '.$e($money($o['balance_minor'])).'</option>';
};
?>
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Pagamentos e afectações a fornecedores — ensaio</title><style>
body{font:17px system-ui;color:#172f29;background:#f4f6f5;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,header{background:white;padding:24px;border:1px solid #dbe4df;border-radius:12px;margin:18px 0}h1{font-size:28px}h2{font-size:22px}p,li{line-height:1.6}label{display:block;margin:14px 0}input,textarea,select,button{font:inherit;padding:10px;max-width:100%}input,textarea,select{display:block;width:min(100%,600px);box-sizing:border-box}button{background:#125a4b;color:white;border:0;border-radius:7px;margin:6px;cursor:pointer}button:disabled{opacity:.5;cursor:wait}.notice{background:#fff3cf;padding:16px}a{color:#125a4b}.scroll{overflow:auto}table{border-collapse:collapse;width:100%}th,td{text-align:left;padding:12px;border-bottom:1px solid #ddd}pre{white-space:pre-wrap;overflow-wrap:anywhere}.line{border:1px solid #ddd;padding:16px;margin:12px 0}#error{color:#8d251e}article{border-top:1px solid #ddd;padding:16px 0}
</style></head><body><main>
<header><a href="<?= $e($settings['obligations_url']) ?>">← Obrigações a fornecedores</a> · <a href="/demo?next=payments">Trocar função ou sair</a><p><?= $e($settings['host']) ?> · <?= $e($settings['actor']) ?></p><h1>Pagamentos e afectações a fornecedores</h1><p class="notice"><strong>ENSAIO SINTÉTICO — SEM MOVIMENTAÇÃO DE DINHEIRO.</strong> Propor não confirma; confirmar um pagamento não abate automaticamente uma dívida. Só uma afectação confirmada reduz o saldo. Não há execução bancária, reconciliação, stock, orçamento adicional ou lançamento contabilístico.</p><p>As referências de pagamento e evidência são artificiais e começam por SIM-. Não introduza dados bancários ou comprovativos reais.</p></header>
<p id="status" role="status"></p><p id="error" role="alert" tabindex="-1"></p><button id="reload">Actualizar consulta</button>
<section id="obligation-balances"><h2>Obrigações e saldo por liquidar</h2><div class="scroll"><table><thead><tr><th>Origem / fornecedor</th><th>Principal</th><th>Afectado neste percurso</th><th>Saldo</th><th>Vencimento</th></tr></thead><tbody><?php foreach ($data['obligations'] as $o): ?><tr><td>#<?= $e($o['id']) ?> · <?= $e($o['origin_snapshot']['number'] ?? '') ?><br><?= $e($o['origin_snapshot']['supplier_name'] ?? '') ?><br>Versão <?= $e($o['source_revision']) ?> · Validador #<?= $e($o['recognised_by']) ?><details><summary>Versão e decisão</summary><pre><?= $e(json_encode($o, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></pre></details></td><td><?= $money($o['principal_minor']) ?></td><td><?= $money($o['allocated_payments_minor']) ?></td><td><?= $money($o['balance_minor']) ?><?= $o['source_conflict'] ? ' — origem divergente' : '' ?></td><td>Pendente<br>Atraso indeterminado<br>Dias de atraso: sem valor</td></tr><?php endforeach ?></tbody></table></div><?php if (! $data['obligations']): ?><p>Reconheça primeiro a obrigação no percurso existente. Não é criada uma dívida por registar um pagamento.</p><?php endif ?></section>
<?php if ($data['permissions']['payment_prepare'] && $eligible): ?><section><h2>1. Propor pagamento sintético</h2><form data-action="payment-prepare"><label>Obrigação de referência / fornecedor<select name="obligation_id" data-origin><?php $options(); ?></select></label><input type="hidden" name="source_revision"><label>Montante do pagamento (MZN)<input name="amount" required inputmode="decimal" pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="4000.00"></label><input type="hidden" name="currency" value="MZN"><label>Data factual do pagamento simulado<input type="date" name="paid_on" required></label><label>Referência estável do facto<input name="fact_reference" required pattern="SIM-[A-Z0-9][A-Z0-9._-]{2,95}" placeholder="SIM-PAY-001"></label><label>Evidência artificial de origem<input name="evidence_reference" required pattern="SIM-[A-Z0-9][A-Z0-9._-]{2,95}" placeholder="SIM-EVID-001"></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Propor sem afectar dívida</button></form></section><?php endif ?>
<section id="payments"><h2>2. Pagamentos propostos e decisões</h2><p>A confirmação exige outra identidade. Depois de confirmar, troque para Preparar para propor a afectação; a dívida ainda não foi reduzida.</p><?php foreach ($data['payments'] as $p): ?><article><h3>Pagamento #<?= $e($p['id']) ?> · <?= $e($p['fact_reference']) ?></h3><p><?= $money($p['amount_minor']) ?> · Data simulada <?= $e($p['paid_on']) ?> · Estado: <?= $e(['proposed'=>'Proposto','confirmed'=>'Confirmado em ensaio','rejected'=>'Rejeitado'][$p['state']]) ?></p><p>Por afectar: <?= $money($p['unallocated_minor']) ?> · Reconciliação bancária: não implementada.</p><details><summary>Preparação e evidência preservadas</summary><pre><?= $e($p['snapshot']) ?></pre><?php if ($p['decision']): ?><pre><?= $e(json_encode($p['decision'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></pre><?php endif ?></details>
<?php if ($data['permissions']['payment_confirm'] && ! $p['decision'] && (int) $p['prepared_by'] !== $data['current_actor']): ?><form data-action="payment-decide"><input type="hidden" name="payment_id" value="<?= $e($p['id']) ?>"><label>Decisão explícita<select name="decision"><option value="confirmed">Confirmar com evidência sintética</option><option value="rejected">Rejeitar</option></select></label><label>Evidência artificial da conferência<input name="evidence_reference" required pattern="SIM-[A-Z0-9][A-Z0-9._-]{2,95}" placeholder="SIM-CONF-001"></label><label>Fundamento da conferência<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Registar decisão independente</button></form><?php endif ?></article><?php endforeach ?><?php if (! $data['payments']): ?><p>Sem pagamentos propostos neste contexto.</p><?php endif ?></section>
<?php if ($data['permissions']['allocation_prepare'] && $confirmed && $eligible): ?><section><h2>3. Propor afectações</h2><form data-action="allocation-prepare"><label>Pagamento confirmado<select name="payment_id"><?php foreach ($confirmed as $p): ?><option value="<?= $e($p['id']) ?>">#<?= $e($p['id']) ?> · <?= $e($p['fact_reference']) ?> · Por afectar <?= $money($p['unallocated_minor']) ?></option><?php endforeach ?></select></label><label>Referência estável da afectação<input name="reference" required pattern="SIM-[A-Z0-9][A-Z0-9._-]{2,95}" placeholder="SIM-ALLOC-001"></label><div data-lines><div class="line" data-line><label>Obrigação<select data-obligation><?php $options(); ?></select></label><label>Valor a afectar (MZN)<input data-amount required inputmode="decimal" pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="4000.00"></label><button type="button" data-remove>Remover linha</button></div></div><button type="button" data-add>Adicionar outra obrigação</button><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><p>Mesmo fornecedor e moeda. O excedente fica por afectar. Não existe reembolso/crédito automático nem edição de uma afectação confirmada.</p><button>Submeter afectações sem abater saldo</button></form></section><?php endif ?>
<section id="allocation-proposals"><h2>4. Propostas de afectação</h2><p>A proposta não reduz a dívida. Troque para Validar para confirmar a afectação com outra identidade.</p><?php foreach ($data['allocation_proposals'] as $p): ?><article><h3>Proposta #<?= $e($p['id']) ?> · <?= $e($p['reference']) ?> · Pagamento #<?= $e($p['payment_id']) ?></h3><p>Preparador #<?= $e($p['prepared_by']) ?> · <?= $e($p['reason']) ?></p><ul><?php foreach (json_decode($p['plan'], true) as $line): ?><li>Obrigação #<?= $e($line['obligation_id']) ?>: <?= $money($line['amount_minor']) ?> · versão <?= $e($line['source_revision']) ?></li><?php endforeach ?></ul><p>Estado: <?= $p['decision'] ? $e($p['decision']->decision === 'confirmed' ? 'Afectação confirmada' : 'Rejeitada') : 'Proposta — sem efeito no saldo' ?></p>
<?php if ($data['permissions']['allocation_confirm'] && ! $p['decision'] && (int) $p['prepared_by'] !== $data['current_actor']): ?><form data-action="allocation-decide"><input type="hidden" name="proposal_id" value="<?= $e($p['id']) ?>"><label>Decisão<select name="decision"><option value="confirmed">Confirmar estas afectações</option><option value="rejected">Rejeitar</option></select></label><label>Fundamento da decisão<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Confirmar decisão e revalidar saldos</button></form><?php endif ?></article><?php endforeach ?></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 ?></section>
<p>Correcções, reversões, reconciliação e novos pagamentos sobre documentos com histórico importado ficam para âmbito próprio. Os originais e as decisões confirmadas não são editados ou apagados.</p>
</main><script>
const settings=<?= json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR) ?>;
document.querySelector('#reload').onclick=()=>location.reload();
for(const selector of document.querySelectorAll('[data-origin]')){const sync=()=>selector.form.elements.source_revision.value=selector.selectedOptions[0]?.dataset.revision??'';selector.onchange=sync;sync();}
for(const form of document.querySelectorAll('[data-action]')){
const add=form.querySelector('[data-add]');if(add){const lines=form.querySelector('[data-lines]');const template=lines.firstElementChild.cloneNode(true);add.onclick=()=>{if(lines.children.length<50)lines.append(template.cloneNode(true));};lines.onclick=e=>{if(e.target.matches('[data-remove]')&&lines.children.length>1)e.target.closest('[data-line]').remove();};}
form.onsubmit=async e=>{e.preventDefault();let body=Object.fromEntries(new FormData(form));for(const key of ['obligation_id','payment_id','proposal_id'])if(key in body)body[key]=Number(body[key]);if(form.dataset.action==='allocation-prepare')body.allocations=[...form.querySelectorAll('[data-line]')].map(line=>{const select=line.querySelector('[data-obligation]');return {obligation_id:Number(select.value),source_revision:select.selectedOptions[0]?.dataset.revision??'',amount:line.querySelector('[data-amount]').value};});const signature=JSON.stringify(body);if(form.dataset.signature!==signature){form.dataset.signature=signature;form.dataset.key=crypto.randomUUID();}const buttons=[...form.querySelectorAll('button')];buttons.forEach(b=>b.disabled=true);document.querySelector('#error').textContent='';document.querySelector('#status').textContent='A processar. Aguarde antes de repetir.';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:signature});const d=await r.json();if(!r.ok){document.querySelector('#error').textContent=Object.values(d.errors??{}).flat().join(' ')||d.message||'Não foi possível concluir; consulte o histórico.';document.querySelector('#error').focus();return;}location.hash=form.dataset.action==='allocation-decide'?'obligation-balances':form.dataset.action==='allocation-prepare'?'allocation-proposals':'payments';location.reload();}catch{document.querySelector('#error').textContent='Não foi possível confirmar o resultado. Actualize a consulta e confira o histórico antes de repetir.';document.querySelector('#error').focus();}finally{document.querySelector('#status').textContent='';buttons.forEach(b=>b.disabled=false);}};
}
</script></body></html>