This commit is contained in:
2026-11-02 08:06:45 +02:00
parent e4f520092e
commit 2a4169162b
108 changed files with 3793 additions and 88 deletions
+12 -2
View File
@@ -19,14 +19,24 @@ body{font:17px system-ui;color:#172f29;background:#f4f6f5;margin:0}main{max-widt
<?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 id="reversals"><h2>5. Reversão integral e nova afectação</h2><p>Propor não altera saldos. Confirmar com outra identidade compensa integralmente a afectação original e volta a disponibilizar esse montante do pagamento. O pagamento continua confirmado: não há cancelamento bancário. Depois, use «Propor afectações» com uma nova referência para corrigir o valor.</p>
<?php if (! ($data['reversals_available'] ?? false)): ?><p>Actualização de reversões ainda não aplicada neste ensaio. Os saldos e o percurso anterior permanecem disponíveis.</p><?php else: ?>
<?php foreach ($data['allocations'] as $a): $reversal = null; $proposal = null; foreach ($data['reversals'] as $r) if ((int) $r->allocation_id === (int) $a->id) $reversal = $r; foreach ($data['reversal_proposals'] as $r) if ((int) $r->allocation_id === (int) $a->id) $proposal = $r; ?>
<article><h3>Afectação #<?= $e($a->id) ?> · Pagamento #<?= $e($a->payment_id) ?> · Obrigação #<?= $e($a->obligation_id) ?></h3><p>Original preservado: <?= $money($a->amount_minor) ?> · Versão <?= $e($a->source_revision) ?></p>
<?php if ($reversal): ?><p>Revertida integralmente por movimento #<?= $e($reversal->id) ?> · <?= $money($reversal->amount_minor) ?> · actor #<?= $e($reversal->actor_id) ?> · <?= $e($reversal->reason) ?></p>
<?php elseif ($proposal): ?><p>Proposta de reversão #<?= $e($proposal->id) ?> · Preparador #<?= $e($proposal->prepared_by) ?> · <?= $e($proposal->reason) ?> · Sem efeito no saldo</p>
<?php if (($data['permissions']['reversal_confirm'] ?? false) && (int) $proposal->prepared_by !== $data['current_actor']): ?><form data-action="reversal-confirm"><input type="hidden" name="proposal_id" value="<?= $e($proposal->id) ?>"><label>Motivo da confirmação independente<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Confirmar reversão integral de <?= $money($a->amount_minor) ?></button></form><?php endif ?>
<?php elseif ($data['permissions']['reversal_prepare'] ?? false): ?><form data-action="reversal-prepare"><input type="hidden" name="allocation_id" value="<?= $e($a->id) ?>"><input type="hidden" name="source_revision" value="<?= $e($a->source_revision) ?>"><label>Motivo obrigatório da reversão integral<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Propor reversão integral sem alterar saldo</button></form><?php endif ?>
<?php if ($proposal): ?><details><summary>Proposta original de reversão preservada</summary><pre><?= $e(json_encode($proposal, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></pre></details><?php endif ?></article><?php endforeach ?>
<?php endif ?></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>
<p>Reversões parciais, cancelamento bancário, reconciliação e novos pagamentos sobre documentos com histórico importado ficam fora desta entrega. 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);}};
form.onsubmit=async e=>{e.preventDefault();let body=Object.fromEntries(new FormData(form));for(const key of ['obligation_id','payment_id','proposal_id','allocation_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.startsWith('reversal-')?'reversals':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>