commit melhorias1
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-10 12:58:19 +02:00
parent 63d20b10fc
commit 976271f948
17 changed files with 640 additions and 0 deletions
+7
View File
@@ -50,6 +50,7 @@ export default function Invoices({
orders,
canManage,
payablesAvailable = false,
sgfDispatchAvailable = false,
stockReturns = [],
stockReversals = [],
focusInvoiceId = null,
@@ -61,6 +62,7 @@ export default function Invoices({
focusInvoiceId?: number | null;
focusOrderId?: number | null;
payablesAvailable?: boolean;
sgfDispatchAvailable?: boolean;
stockReturns?: {
reference: string;
quantity_milli: number;
@@ -116,6 +118,11 @@ export default function Invoices({
title="Facturas e conferência"
description="Conferência da ordem, recepção aceite e factura. Validação por outra pessoa de Finanças."
/>
{sgfDispatchAvailable && (
<a href="/facturas/envio-sgf" className="underline">
Rascunhos para conferência e envio ao SGF
</a>
)}
<p className="rounded-lg border border-amber-300 p-4 text-sm">
Desenvolvimento. Não autoriza pagamentos. A validação exige
compromisso orçamental aprovado. Diferenças, notas de
@@ -0,0 +1,16 @@
@php($doc=old('document',$draft['document']??[]))
<label>Referência de origem<input name="reference" value="{{ old('reference',$draft['reference']??'') }}" required maxlength="120" @readonly(isset($draft))></label>
<input type="hidden" name="connection_id" value="{{ $selection['connection'] }}"><input type="hidden" name="mapping_id" value="{{ $selection['id'] }}">
<p>Mapeamento escolhido: <strong>{{ $selection['label'] }}</strong>; ordem SGF #{{ $selection['mapping']['order_id'] }}; versão {{ $selection['mapping']['version'] }}.</p>
<label>Número do documento recebido<input name="document[number]" value="{{ $doc['number']??'' }}" required maxlength="100"></label>
<label>Data do documento<input type="date" name="document[issued_on]" value="{{ $doc['issued_on']??'' }}" required></label>
@foreach(['subtotal'=>'Base MZN','tax'=>'Imposto documental MZN','total'=>'Total MZN'] as $key=>$label)<label>{{ $label }}<input name="document[{{ $key }}]" value="{{ $doc[$key]??'' }}" required inputmode="decimal" pattern="[0-9]+\.[0-9]{2}" placeholder="0.00"></label>@endforeach
<fieldset><legend>Linhas da fonte — referências explicitamente mapeadas</legend><p>Introduzir as linhas recebidas. Não se convertem unidades ou preços automaticamente.</p>
@php($items=$doc['items']??[['source_line_ref'=>'','quantity'=>'','unit'=>'','unit_price'=>'','tax_rate'=>'']])
<div id="source-lines">@foreach($items as $i=>$item)<fieldset class="source-line"><legend>Linha {{ $loop->iteration }}</legend>
<label>Referência da linha<select name="document[items][{{ $i }}][source_line_ref]" required><option value="">Escolher</option>@foreach($selection['mapping']['lines'] as $ref=>$target)<option value="{{ $ref }}" @selected(($item['source_line_ref']??'')===$ref)>{{ $ref }} → linha SGF #{{ $target }}</option>@endforeach</select></label>
@foreach(['quantity'=>'Quantidade','unit'=>'Unidade','unit_price'=>'Preço unitário','tax_rate'=>'Taxa documental %'] as $key=>$label)<label>{{ $label }}<input name="document[items][{{ $i }}][{{ $key }}]" value="{{ $item[$key]??'' }}" required maxlength="30"></label>@endforeach
<button type="button" onclick="if(this.parentElement.parentElement.children.length>1)this.parentElement.remove()">Retirar linha</button></fieldset>@endforeach</div>
<button type="button" id="add-source-line">Acrescentar linha</button></fieldset>
<label>Fundamento<textarea name="reason" required minlength="10" maxlength="2000">{{ old('reason',$draft['reason']??'') }}</textarea></label>
<script>document.getElementById('add-source-line').addEventListener('click',()=>{const root=document.getElementById('source-lines');if(root.children.length>=50)return;let index=0;root.querySelectorAll('[name]').forEach(e=>{const m=e.name.match(/\[items\]\[(\d+)\]/);if(m)index=Math.max(index,Number(m[1])+1)});const next=root.firstElementChild.cloneNode(true);next.querySelector('legend').textContent='Nova linha';next.querySelectorAll('input,select').forEach(e=>{e.name=e.name.replace(/\[items\]\[\d+\]/,'[items]['+index+']');e.value=''});root.appendChild(next)});</script>
@@ -0,0 +1,2 @@
@if(is_array($value))<dl>@foreach($value as $key=>$item)<dt>{{ ['operation'=>'Operação','invoice'=>'Factura','number'=>'Número','items'=>'Linhas','quantity_milli'=>'Quantidade (milésimos)','subtotal_minor'=>'Base (centésimos)','tax_minor'=>'Imposto (centésimos)','total_minor'=>'Total (centésimos)','budget_consumption_minor'=>'Consumo orçamental (centésimos)','payable_created'=>'Obrigação criada','payment_created'=>'Pagamento criado','ledger_posted'=>'Lançamento criado','reason'=>'Fundamento','actor_id'=>'Autor','phase'=>'Estado','version'=>'Versão','document'=>'Documento','mapping_snapshot'=>'Mapeamento','receipt'=>'Recibo','before_state'=>'Antes','after_state'=>'Depois'][$key]??$key }}</dt><dd>@include('sgf-invoices.evidence',['value'=>$item])</dd>@endforeach</dl>
@elseif(is_bool($value)){{ $value?'Sim':'Não' }}@elseif($value===null)Não indicado@else{{ $value }}@endif
@@ -0,0 +1,9 @@
@extends('sgf-invoices.layout')
@section('content')
<section><h2>Escolher ligação e mapeamento aprovados</h2><form method="get"><label>Mapeamento<select id="mapping-choice" required><option value="">Escolher explicitamente</option>@foreach($options as $m)<option data-connection="{{ $m['connection'] }}" data-mapping="{{ $m['id'] }}" @selected($selection&&$selection['connection']===$m['connection']&&$selection['id']===$m['id'])>{{ $m['label'] }} · ordem #{{ $m['mapping']['order_id'] }}</option>@endforeach</select></label><input id="connection" type="hidden" name="connection"><input id="mapping" type="hidden" name="mapping"><button>Usar este mapeamento</button></form>
@if(!$options)<p>Não existem mapeamentos aprovados para esta empresa. Nenhum é escolhido automaticamente.</p>@endif</section>
@if($selection)<section><h2>Guardar novo rascunho</h2><form method="post" action="/facturas/envio-sgf">@csrf @include('sgf-invoices.document-form')<button>Guardar rascunho sem enviar</button></form></section>@endif
<section><h2>Rascunhos e resultados</h2><form method="get"><label>Estado<select name="phase"><option value="">Todos</option>@foreach(['draft'=>'Rascunho','previewed'=>'Conferido','sending'=>'Confirmação em curso','uncertain'=>'Resultado incerto','received_pending'=>'Recebido pendente','blocked'=>'Acesso bloqueado','review_required'=>'Nova conferência','not_sent'=>'Envio não confirmado','previewing'=>'Prévia em curso'] as $key=>$label)<option value="{{ $key }}" @selected(request('phase')===$key)>{{ $label }}</option>@endforeach</select></label><button>Filtrar</button></form>
<div class="scroll"><table><thead><tr><th>Referência</th><th>Documento</th><th>Estado</th><th>Versão</th></tr></thead><tbody>@forelse($rows as $r)<tr><td><a href="/facturas/envio-sgf/{{ $r->id }}">{{ $r->reference }}</a></td><td>{{ $r->number }}</td><td>{{ $r->phase }}</td><td>{{ $r->version }}</td></tr>@empty<tr><td colspan="4">Sem rascunhos neste filtro.</td></tr>@endforelse</tbody></table></div>{{ $rows->links() }}</section>
<script>const select=document.getElementById('mapping-choice');const fill=()=>{const o=select.selectedOptions[0];document.getElementById('connection').value=o?.dataset.connection??'';document.getElementById('mapping').value=o?.dataset.mapping??''};select.addEventListener('change',fill);fill();</script>
@endsection
@@ -0,0 +1,7 @@
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SGC · Envio de facturas ao SGF</title>
<style>body{font:16px/1.5 system-ui,sans-serif;color:#163934;background:#f3f6f5;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,fieldset{background:white;border:1px solid #c5d5d0;border-radius:8px;padding:20px;margin:16px 0}h1,h2,h3{line-height:1.25}label{display:block;margin:12px 0}input,textarea,select,button{font:inherit;padding:8px;max-width:100%;box-sizing:border-box}input,textarea,select{display:block;border:1px solid #69837a;border-radius:4px}textarea{width:100%;min-height:80px}button{background:#165c4c;color:white;border:0;border-radius:4px;cursor:pointer}button:disabled{opacity:.5;cursor:wait}a{color:#165c4c}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:8px;border-bottom:1px solid #d3dfdb;vertical-align:top}dl{margin:8px 0}dt{font-weight:650}dd{margin:0 0 8px 16px;overflow-wrap:anywhere}.notice{padding:12px;background:#fff4d8}.error{color:#9d2323}.scroll{overflow:auto}small{color:#49665d}:focus-visible{outline:3px solid #bf6f13;outline-offset:3px}</style></head><body><main>
<nav><a href="/facturas">Facturas SGC</a> · <a href="/facturas/envio-sgf">Envio ao SGF</a> · <a href="/dashboard">Painel inicial</a></nav>
<h1>Rascunhos de factura para o SGF</h1><p class="notice">Só documentos ainda não registados no SGC. O envio cria uma factura pendente no SGF; não é validação, obrigação ou pagamento.</p>
@if($errors->any())<section class="error" role="alert"><ul>@foreach($errors->all() as $error)<li>{{ $error }}</li>@endforeach</ul></section>@endif
@yield('content')
</main><script>let dirty=false,pending=false;document.addEventListener('input',e=>{if(e.isTrusted&&e.target.closest('form[method="post"]'))dirty=true});document.addEventListener('submit',e=>{if(e.target.method.toLowerCase()!=='post')return;pending=true;e.target.querySelectorAll('button').forEach(b=>b.disabled=true)});document.addEventListener('click',e=>{const a=e.target.closest('a');if(!a)return;if(pending){e.preventDefault();return}if(dirty&&!window.confirm('Sair e perder os campos ainda não guardados?'))e.preventDefault()});window.addEventListener('beforeunload',e=>{if(dirty&&!pending){e.preventDefault();e.returnValue=''}});</script></body></html>
@@ -0,0 +1,23 @@
@extends('sgf-invoices.layout')
@section('content')
<section><h2>{{ $row['reference'] }} · {{ $row['number'] }}</h2><p>Estado: <strong>{{ $row['phase'] }}</strong> · versão {{ $row['version'] }} · fonte {{ $row['source_version'] }}</p>
<p>Fundamento: {{ $row['reason'] }}</p>@include('sgf-invoices.evidence',['value'=>$row['document']])
@if($row['receipt'])<h3>Recibo conservado</h3>@include('sgf-invoices.evidence',['value'=>$row['receipt']])<p>A factura está pendente no SGF. A revisão financeira é uma decisão distinta.</p>
@php($continue='http://127.0.0.1:8085/compras?'.http_build_query(['return_to'=>'/purchasing/invoices?'.http_build_query(['order'=>$row['receipt']['order_id'],'invoice'=>$row['receipt']['invoice_id']])]))
<a href="{{ $continue }}">Conferir a factura no SGF após escolher a função autorizada</a>@endif
@if($row['lease_until'])<p role="status">Operação iniciada. Actualizar esta página para consultar o resultado; não criar outro rascunho.</p>@endif
</section>
@if(!$row['ever_sending'])
@php($selection=collect($options)->first(fn($m)=>$m['connection']===$row['connection_id']&&$m['id']===$row['mapping_id']))
@if($selection)<section><details><summary>Rever campos antes de enviar</summary><form method="post" action="/facturas/envio-sgf/{{ $row['id'] }}/rever">@csrf<input type="hidden" name="version" value="{{ $row['version'] }}">@include('sgf-invoices.document-form',['draft'=>$row])<button>Guardar nova versão e invalidar prévia</button></form></details></section>@endif
@endif
@if(!$row['receipt'])<section><h2>Conferência individual</h2>
@if(!$transportEnabled)<p class="notice">Transporte desligado. O rascunho está conservado; não há envio automático.</p>@else
<form method="post" action="/facturas/envio-sgf/{{ $row['id'] }}/operar">@csrf<input type="hidden" name="version" value="{{ $row['version'] }}"><input type="hidden" name="action" value="preview"><label>Fundamento da conferência<textarea name="reason" required minlength="10" maxlength="2000"></textarea></label><button>Obter prévia actual sem confirmar</button></form>
@endif
@if($row['impact'])<h3>Impacto devolvido pelo SGF</h3>@include('sgf-invoices.evidence',['value'=>$row['impact']])@endif
@if($transportEnabled&&$row['phase']==='previewed')<form method="post" action="/facturas/envio-sgf/{{ $row['id'] }}/operar">@csrf<input type="hidden" name="version" value="{{ $row['version'] }}"><input type="hidden" name="action" value="confirm"><input type="hidden" name="approval_hash" value="{{ $row['approval_hash'] }}"><label>Fundamento da decisão<textarea name="reason" required minlength="10" maxlength="2000"></textarea></label><p>Confirmar conserva a decisão e envia esta intenção. Não aprova financeiramente a factura.</p><button>Confirmar individualmente este impacto</button></form>@endif
@if($transportEnabled&&$row['ever_sending']&&in_array($row['phase'],['sending','uncertain','blocked','review_required','not_sent']))<form method="post" action="/facturas/envio-sgf/{{ $row['id'] }}/operar">@csrf<input type="hidden" name="version" value="{{ $row['version'] }}"><input type="hidden" name="action" value="recover"><label>Fundamento da recuperação<textarea name="reason" required minlength="10" maxlength="2000"></textarea></label><button>Recuperar a mesma intenção e chave</button></form>@endif
</section>@endif
<section><h2>Histórico auditado</h2>@foreach($events as $e)<details><summary>{{ $e->created_at }} UTC · {{ $e->action }} · autor #{{ $e->actor_id }}</summary><p>{{ $e->reason }}</p><h3>Antes</h3>@include('sgf-invoices.evidence',['value'=>$e->before_state?json_decode($e->before_state,true):null])<h3>Depois</h3>@include('sgf-invoices.evidence',['value'=>json_decode($e->after_state,true)])</details>@endforeach{{ $events->links() }}</section>
@endsection