28 lines
11 KiB
PHP
28 lines
11 KiB
PHP
<?php
|
|
$e=static fn($v)=>htmlspecialchars((string)$v,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');$json=static fn($v)=>json_encode($v,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR);
|
|
$money=static fn($v)=>($v<0?'-':'').intdiv(abs($v),100).','.str_pad((string)(abs($v)%100),2,'0',STR_PAD_LEFT);
|
|
$can=fn($a)=>in_array($a,$settings['can'],true);$actor=$settings['actor_id'];$open=($data['cycle']['periods'][0]->status??null)==='open';
|
|
$form=function($action,$body,$label)use($e,$json){echo '<form class="command" data-action="'.$e($action).'" data-body="'.$e($json($body)).'"><label>Fundamento individual<textarea name="reason" required minlength="10" maxlength="4000"></textarea></label><button>'.$e($label).'</button></form>';};
|
|
?><!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Stock e valorização QA</title><style>body{font:17px system-ui;background:#f3f6f5;color:#18372f;margin:0}main{max-width:1150px;margin:auto;padding:24px}section,header{background:white;border:1px solid #cedbd5;border-radius:10px;padding:22px;margin:16px 0}table{border-collapse:collapse;width:100%}th,td{padding:10px;border-bottom:1px solid #ddd;text-align:left;vertical-align:top}.scroll{overflow:auto}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-width:75ch}input,textarea,select,button{font:inherit;padding:8px}textarea{display:block;width:min(90%,40rem)}form{margin:12px 0}.notice{background:#fff0c3;padding:14px}a{color:#155644}#message{white-space:pre-wrap}</style></head><body><main>
|
|
<header><a href="<?= $e($settings['return_url']) ?>">Voltar aos percursos QA</a><h1>Stock físico → valorização → diário</h1><p class="notice">Exclusivamente sintético. Taxa de ensaio, contas artificiais e MZN. Conferir o impacto não move quantidades nem cria obrigação ou pagamento.</p><p>Livro<?= $e($data['stock']['book']['id']) ?> · artigo<?= $e($data['stock']['book']['stock_item_id']) ?> · armazém<?= $e($data['stock']['book']['warehouse_id']) ?> · unidade<?= $e($data['stock']['book']['unit_code']) ?> · período<?= $e($data['stock']['book']['period_key']) ?> · <?= $open?'aberto':'fechado' ?></p><p><a href="/stock-valuation/reports/trial-balance?from_on=2026-10-01&to_on=2026-10-31">Balancete</a> · <a href="/stock-valuation/reports/account-ledger?from_on=2026-10-01&to_on=2026-10-31&account_id=<?= $e($settings['stock_account_id']) ?>">Razão de stock</a></p></header>
|
|
<p id="message" role="status"></p>
|
|
<section><h2>Taxa por UN, com revisão distinta</h2><p>A última versão tem de estar aprovada. Uma taxa ausente ou uma sucessora pendente deixa a valorização incompleta. Correcção após lançamento não faz parte deste ensaio.</p>
|
|
<?php if($open&&$can('prepare')&&$data['ledger']['pagination']['total']===0): ?><form id="rate"><label>Valor por UN em MZN<input name="amount" value="50,00" required inputmode="decimal"></label><label>Fundamento<textarea name="reason" minlength="10" maxlength="4000" required></textarea></label><button>Propor taxa</button></form><?php endif; ?>
|
|
<?php foreach($data['stock']['rates']as$r): ?><article><p>Versão<?= $e($r->version) ?> · <?= $e($money($r->unit_minor)) ?> MZN/UN · autor<?= $e($r->prepared_by) ?> · <?= $e($r->review->decision??'pendente de revisão') ?> · <?= $e($r->reason) ?></p><?php if($open&&!$r->review&&$can('validate')&&$r->prepared_by!==$actor){foreach(['approved'=>'Aprovar taxa','rejected'=>'Recusar taxa']as$decision=>$label)$form('rate-validate',['rate_id'=>$r->id,'version'=>$r->version,'decision'=>$decision],$label);} ?></article><?php endforeach; ?></section>
|
|
<section><h2>Movimentos efectivos e dependências</h2><p>Saldo físico:<?= $e($data['stock']['physical_balance_milli']) ?> milésimos de UN. Cada movimento é conferido individualmente; não há valorização em lote.</p><div class="scroll"><table><thead><tr><th>Origem</th><th>Quantidade em milésimos</th><th>Valor MZN</th><th>Conferência</th></tr></thead><tbody>
|
|
<?php foreach($data['stock']['rows']as$row):$m=$row['movement'];$source=$row['source']; ?><tr><td>#<?= $e($m['id']) ?> <?= $e($m['movement_type']) ?><br><?= $e($m['moved_at']) ?></td><td><?= $e($m['quantity_milli']) ?></td><td><?= $source?$e($money($source['principal_minor'])):'Incompleto' ?></td><td><?php if($source): ?><details><summary>Versões e dependências actuais</summary><pre><?= $e($json($source)) ?></pre></details><?php if($open&&$can('prepare'))$form('effect-prepare',['effect_type'=>$source['effect_type'],'source_id'=>$m['id'],'journal_on'=>$source['stock_movement_on'],'period_key'=>$source['permitted_period_key'],'rule_key'=>$source['effect_type']],'Preparar conferência deste movimento'); ?><?php else: ?><?= $e($row['pending']) ?><?php endif; ?></td></tr><?php endforeach; ?></tbody></table></div>
|
|
<nav><?php $p=$data['stock']['pagination'];if($p['page']>1): ?><a href="?book_id=<?= $e($data['stock']['book']['id']) ?>&page=<?= $e($p['page']-1) ?>&per_page=<?= $e($p['per_page']) ?>">Anterior</a><?php endif;if($p['page']*$p['per_page']<$p['total']): ?><a href="?book_id=<?= $e($data['stock']['book']['id']) ?>&page=<?= $e($p['page']+1) ?>&per_page=<?= $e($p['per_page']) ?>">Seguinte</a><?php endif; ?> Página<?= $e($p['page']) ?> · total<?= $e($p['total']) ?></nav></section>
|
|
<section><h2>Propostas e impacto contabilístico</h2><p>Conferir débito/crédito, origem, taxa, dependências e versão antes de decidir. A aplicação volta a validar o estado actual.</p>
|
|
<?php foreach($data['ledger']['proposals']as$p):$snapshot=json_decode($p->snapshot,true);$review=collect($data['ledger']['reviews'])->firstWhere('proposal_id',$p->id); ?><article><h3>Proposta<?= $e($p->id) ?> · captura<?= $e($p->capture_version) ?></h3><p>Origem<?= $e($snapshot['source']['stock_movement_id']??'') ?> · autor<?= $e($p->prepared_by) ?> · <?= $e($review->decision??'sem revisão') ?><?= $p->journal_id?' · lançamento #'.$e($p->journal_id):'' ?></p><ul><?php foreach($snapshot['lines']as$l): ?><li><?= $e($l['account_code']) ?>: débito<?= $e($money($l['debit_minor'])) ?> / crédito<?= $e($money($l['credit_minor'])) ?> MZN</li><?php endforeach; ?></ul><details><summary>Fotografia completa da conferência</summary><pre><?= $e($json($snapshot)) ?></pre></details>
|
|
<?php if($open&&!$p->journal_id&&!$review&&$can('validate')&&$p->prepared_by!==$actor){foreach(['approved'=>'Aprovar esta versão','rejected'=>'Recusar esta versão']as$decision=>$label)$form('validate',['proposal_id'=>$p->id,'revision'=>$p->revision,'decision'=>$decision],$label);}if($open&&!$p->journal_id&&($review->decision??null)==='approved'&&$can('post')&&$p->prepared_by===$actor)$form('post',['proposal_id'=>$p->id,'revision'=>$p->revision],'Aplicar exactamente este impacto'); ?></article><?php endforeach; ?></section>
|
|
<section><h2>Conferência e fecho do período QA</h2><?php if($open&&$can('period_prepare'))$form('period-prepare',['period_key'=>$data['stock']['book']['period_key']],'Preparar conferência do período'); ?>
|
|
<?php foreach($data['cycle']['period_checks']as$c):$r=collect($data['cycle']['period_reviews'])->firstWhere('check_id',$c->id); ?><article><p>Conferência<?= $e($c->id) ?> · autor<?= $e($c->prepared_by) ?> · <?= $e($r->decision??'pendente') ?></p><details><summary>Factos e impacto do fecho</summary><pre><?= $e($c->snapshot) ?></pre></details><?php if($open&&!$r&&$can('period_validate')&&$c->prepared_by!==$actor)$form('period-validate',['check_id'=>$c->id,'revision'=>$c->revision,'decision'=>'approved'],'Aprovar conferência exacta do período');if($open&&($r->decision??null)==='approved'&&$can('period_close')&&$c->prepared_by===$actor)$form('period-close',['check_id'=>$c->id,'revision'=>$c->revision],'Confirmar fecho QA'); ?></article><?php endforeach; ?></section>
|
|
<section><h2>Recuperar comprovativo sem executar</h2><p>Uma resposta perdida não autoriza repetição automática. Consulte a chave do pedido no domínio certo; ausência de comprovativo não prova falha.</p><form id="receipt"><label>Domínio<select name="domain"><option value="ledger">Diário/fecho</option><option value="rate">Taxa</option></select></label><label>Chave<input name="key" required pattern="[a-fA-F0-9-]{36}" maxlength="36"></label><button>Consultar</button></form><pre id="receipt-result"></pre></section>
|
|
<script id="stock-contract" type="application/json"><?= json_encode($settings+['book_id'=>$data['stock']['book']['id'],'rate_version'=>$data['stock']['rates'][0]->version??0],JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_THROW_ON_ERROR) ?></script>
|
|
<script>(()=>{const contract=JSON.parse(document.getElementById('stock-contract').textContent),message=document.getElementById('message');let busy=false;
|
|
const command=async(action,body)=>{if(busy)return;if(!confirm('Confirma esta decisão individual depois de conferir o impacto apresentado?'))return;busy=true;const key=crypto.randomUUID();message.textContent='Chave do pedido: '+key;try{const r=await fetch('/api/stock-valuation/'+action,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':contract.csrf,'X-Stock-Context':contract.context,'Idempotency-Key':key},body:JSON.stringify(body)});const data=await r.json();if(!r.ok)throw Error(data.message||'Operação recusada.');message.textContent='Decisão registada. Chave: '+key;location.reload();}catch(error){message.textContent=error.message+' Chave: '+key+'. Confira o estado/comprovativo antes de repetir; não existe repetição automática.';}finally{busy=false;}};
|
|
document.querySelectorAll('form.command').forEach(form=>form.onsubmit=event=>{event.preventDefault();command(form.dataset.action,{...JSON.parse(form.dataset.body),reason:new FormData(form).get('reason')});});
|
|
const rate=document.getElementById('rate');if(rate)rate.onsubmit=event=>{event.preventDefault();const f=new FormData(rate),v=String(f.get('amount')).replace(',','.');if(!/^\d{1,6}(\.\d{1,2})?$/.test(v)){message.textContent='Valor em MZN inválido.';return;}const [whole,part='']=v.split('.'),minor=Number(whole)*100+Number(part.padEnd(2,'0'));command('rate-prepare',{book_id:contract.book_id,version:contract.rate_version,unit_minor:minor,reason:f.get('reason')});};
|
|
const form=document.getElementById('receipt'),result=document.getElementById('receipt-result');let generation=0,active=null;const invalidate=()=>{generation++;active?.abort();active=null;result.textContent='';};form.addEventListener('input',invalidate);form.addEventListener('change',invalidate);form.onsubmit=async event=>{event.preventDefault();invalidate();const serial=generation,key=form.elements.key.value.trim(),domain=form.elements.domain.value,controller=new AbortController();active=controller;const current=()=>serial===generation&&form.elements.key.value.trim()===key&&form.elements.domain.value===domain;try{const r=await fetch('/api/stock-valuation/receipts/'+encodeURIComponent(domain)+'/'+encodeURIComponent(key),{credentials:'same-origin',signal:controller.signal,headers:{Accept:'application/json','X-Stock-Context':contract.context}});const data=await r.json();if(!current())return;if(!r.ok)throw Error(data.message||'Consulta recusada.');result.textContent=JSON.stringify(data,null,2);}catch(error){if(current()&&error.name!=='AbortError')result.textContent=error.message;}finally{if(serial===generation)active=null;}};
|
|
})();</script></main></body></html>
|