Files
Investidor-V2/resources/js/components/simulation-acquisitions.tsx
T

63 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { acquisitionBook, acquisitionCurrent, acquisitionExcludedFields, acquisitionPlanReferences, acquisitionReportRows, calculateAcquisitions, newAcquisition, reuseAcquisitions, saveAcquisitionBook, type Acquisition, type AcquisitionBook } from '@/lib/iva-acquisitions';
export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
const [error,setError]=useState('');
let book:AcquisitionBook;
try {book=acquisitionBook(draft);} catch(cause) {return <p role="alert">{String(cause)}</p>;}
const save=(next:AcquisitionBook)=>{try {onChange('modelo_a_snapshot',saveAcquisitionBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
const change=(id:string,patch:Partial<Acquisition>)=>save({...book,rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
const run=(action:()=>void)=>{try{action();setError('');}catch(cause){setError(cause instanceof Error?cause.message:String(cause));}};
let references:ReturnType<typeof acquisitionPlanReferences>=[];
try {references=acquisitionPlanReferences(draft);}catch { /* The action reports the concrete currency/source error without replacing saved values. */ }
const labels:Record<string,string>={'03':'Base à taxa reduzida','04':'IVA à taxa reduzida','05':'Isentas com direito à dedução','06':'Operações art.18(1)(b)','07':'Isentas sem direito','11':'Importações','12':'Regularizações a favor do sujeito passivo','13':'Regularizações a favor do Estado'};
const select=(row:Acquisition,key:keyof Acquisition,label:string,options:Array<[string,string]>)=><label className="block" key={key}>{label}<select className="block rounded border p-2" value={String(row[key]??'')} onChange={e=>change(row.id,{[key]:e.target.value})}><option value="">Por indicar</option>{options.map(([value,text])=><option key={value} value={value}>{text}</option>)}</select></label>;
const yesNo:Array<[string,string]>=[['yes','Sim'],['no','Não']];
return <section aria-label="Aquisições e apuramento parcial Modelo A" className="space-y-4 rounded border p-3">
<h4 className="font-semibold">Compras e apuramento parcial do Modelo A</h4>
<p>Oportunidade em tratamento: {String(draft.context_opportunity||'entrada geral')}. Moeda de origem: {book.currency||'por indicar'}. A compra registada não gera automaticamente IVA dedutível.</p>
{book.currency && draft.currency!==book.currency && <p role="alert">As compras conservam {book.currency}; o projecto está em {String(draft.currency||'moeda por indicar')}. Reveja a moeda no projecto. Os montantes e o resultado anterior não foram convertidos ou reinterpretados.</p>}
{!book.currency && !!draft.currency && <button type="button" className="underline" onClick={()=>save({...book,currency:String(draft.currency),rows:book.rows.map(row=>row.sourceCurrency?row:{...row,sourceCurrency:String(draft.currency)})})}>Confirmar explicitamente {String(draft.currency)} como moeda de origem dos valores sem moeda, conservando os montantes</button>}
<p>Primeiro âmbito: aquisições internas em MZN, afectação exclusiva a operações tributadas, sem exclusões ou dedução parcial, documento e dedução no mesmo período de 2026. Nas projecções, cada confirmação é um pressuposto, sem certificar documento ou declaração efectiva. Indique dois valores entre líquido, IVA e bruto; o terceiro é obtido por diferença ou soma, sem taxa de compra presumida.</p>
<label className="block">Adquirente sujeito passivo de IVA no regime normal (efectivo ou pressuposto da projecção)<select className="block rounded border p-2" value={book.taxpayerRegistered??''} onChange={e=>save({...book,taxpayerRegistered:e.target.value as AcquisitionBook['taxpayerRegistered']})}><option value="">Por confirmar</option><option value="yes">Sim</option><option value="no">Não / outro enquadramento — fora deste lote</option></select></label>
{error&&<p role="alert">{error}</p>}
<button type="button" className="underline" onClick={()=>run(()=>save(reuseAcquisitions(draft,book)))}>Reutilizar compras e períodos existentes</button>
<button type="button" className="ml-3 underline" disabled={!book.periods.length} onClick={()=>save({...book,rows:[...book.rows,newAcquisition(crypto.randomUUID(),book.periods[0]?.period??'',book.currency)]})}>Acrescentar aquisição em falta</button>
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Aquisição adicional'} — {row.period} — campo{row.field}</summary>
<p>Origem: {row.sourceMonth===null?'Introdução directa':`Plano, mês ${row.sourceMonth} (${row.sourcePeriod??'por indicar'}): ${row.sourceAmount??'sem informação'} ${row.sourceCurrency}`}. O mês da encomenda/orçamento não determina a data do documento ou a dedução. Alterações não substituem o resultado anterior.</p>
<div className="grid gap-3 md:grid-cols-2">
<label>Descrição<input className="block rounded border p-2" value={row.description} onChange={e=>change(row.id,{description:e.target.value})}/></label>
{row.sourceMonth===null && <label>Reutilizar valor existente, sem presumir que o orçamento é uma factura<select className="block rounded border p-2" value="" onChange={e=>run(()=>{const ref=references.find(r=>r.key===e.target.value);if(ref)change(row.id,{sourceMonth:ref.month,sourcePeriod:ref.period,sourceField:ref.kind,sourceAmount:ref.amount,sourceCurrency:ref.currency,...(ref.kind==='asset'?{}:{period:ref.period})});})}><option value="">Escolher apenas se corresponde a esta aquisição</option>{references.filter(ref=>ref.kind===(row.field==='08'?'asset':row.field==='10'?'operating':'inventory')).map(ref=><option key={ref.key} value={ref.key}>{ref.label}: {ref.amount??'sem informação'} {ref.currency}</option>)}</select></label>}
{select(row,'field','Categoria',[['08','08 — Imobilizado'],['09','09 — Existências'],['10','10 — Outros bens e serviços']])}
{select(row,'origin','Natureza dos dados',[['projected','Projectados / pressupostos'],['effective','Efectivos declarados']])}
<label>Período de dedução<input className="block rounded border p-2" type="month" value={row.period} onChange={e=>change(row.id,{period:e.target.value})}/></label>
{row.sourceMonth!==null && <label>O valor de origem corresponde a<select className="block rounded border p-2" value={row.sourceBasis} onChange={e=>{const basis=e.target.value as Acquisition['sourceBasis'];change(row.id,{sourceBasis:basis,...(basis==='net'&&!row.net?{net:row.sourceAmount??''}:basis==='gross'&&!row.gross?{gross:row.sourceAmount??''}:{})});}}><option value="">Por classificar</option><option value="net">Valor sem IVA</option><option value="gross">Valor com IVA</option></select></label>}
{(['net','vat','gross'] as const).map((field,index)=><label key={field}>{['Valor líquido','IVA indicado','Valor bruto'][index]} ({row.sourceCurrency})<input className="block rounded border p-2" inputMode="decimal" value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
{select(row,'documentType','Tipo de documento',[['invoice','Factura'],['equivalent','Documento equivalente']])}
<label>Referência do documento / previsão<input className="block rounded border p-2" value={row.documentRef} onChange={e=>change(row.id,{documentRef:e.target.value})}/></label>
<label>Linha do documento, se discriminado em várias aquisições<input className="block rounded border p-2" value={row.documentLine} onChange={e=>change(row.id,{documentLine:e.target.value})}/></label>
<label>Data do documento / prevista<input className="block rounded border p-2" type="date" value={row.documentDate} onChange={e=>change(row.id,{documentDate:e.target.value})}/></label>
<label>Fornecedor / previsto<input className="block rounded border p-2" value={row.supplier} onChange={e=>change(row.id,{supplier:e.target.value})}/></label>
{select(row,'domestic','Aquisição interna',yesNo)}
{select(row,'supplierRegistered','Fornecedor inscrito no IVA',yesNo)}
{select(row,'legalDocument','Documento emitido em forma legal',yesNo)}
{select(row,'held','Documento na posse do adquirente / previsto na projecção',yesNo)}
{select(row,'allocation','Afectação',[['taxable_only','Exclusiva a operações tributadas'],['mixed','Mista / parcial — fora deste lote'],['no_right','Operações sem direito à dedução']])}
{select(row,'exclusion','Exclusões de dedução',[['none','Sem exclusões ou casos especiais dos arts.18–20'],['excluded','Despesa excluída'],['special','Veículos, combustível, viagens, alojamento, comunicações, luxo ou outra situação especial — por enquadrar']])}
{select(row,'integrity','Operação real / pressuposto regular, sem simulação, margem de segunda mão ou impedimentos do art.18(3–5)',yesNo)}
</div>
<p>Fundamento: CIVA arts.18–21, fontes e versão no resultado. Não se calcula dedução parcial, excepção de despesa ou dedução posterior neste lote.</p>
{row.sourceMonth===null && <button type="button" className="mr-3 underline" onClick={()=>save({...book,rows:book.rows.filter(r=>r.id!==row.id)})}>Remover esta aquisição introduzida directamente</button>}
{row.sourceMonth!==null && <button type="button" className="underline" onClick={()=>run(()=>{const ref=acquisitionPlanReferences(draft).find(r=>r.month===row.sourceMonth && r.kind===(row.sourceField??'inventory'));if(!ref)throw new Error('Origem não disponível; conserve o registo e reveja o plano.');if(draft.currency!==row.sourceCurrency)throw new Error('Moeda divergente; não adoptar montantes de outra moeda.');change(row.id,{sourceAmount:ref.amount,sourcePeriod:ref.period,...(row.sourceBasis==='net'?{net:ref.amount??'',gross:''}:row.sourceBasis==='gross'?{gross:ref.amount??'',net:''}:{})});})}>Adoptar explicitamente o valor actual do plano (conserva histórico)</button>}
</details>)}
{book.periods.map(period=><details key={period.period} className="rounded border p-3"><summary>{period.period} — cobertura das parcelas — {period.complete && period.absent.length===acquisitionExcludedFields.length?'confirmada':'por completar'}</summary>
<label className="block"><input type="checkbox" checked={period.complete} onChange={e=>save({...book,periods:book.periods.map(p=>p.period===period.period?{...p,complete:e.target.checked}:p)})}/> Incluí todas as aquisições08–10 deste período, incluindo as sem direito à dedução; uma lista vazia significa que não houve essas aquisições.</label>
<p>Campos fora deste lote: assinale apenas quando não existiram as respectivas operações. Uma parcela não assinalada permanece indeterminada.</p>
{acquisitionExcludedFields.map(field=><label className="block" key={field}><input type="checkbox" checked={period.absent.includes(field)} onChange={e=>save({...book,periods:book.periods.map(p=>p.period===period.period?{...p,absent:e.target.checked?[...p.absent,field]:p.absent.filter(f=>f!==field)}:p)})}/> Sem operações no campo{field} — {labels[field]}</label>)}
</details>)}
<button type="button" disabled={!book.periods.length} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50" onClick={()=>run(()=>save({...book,result:calculateAcquisitions(draft,book)}))}>Calcular e guardar versão do apuramento parcial</button>
<p>O preenchimento e o resultado seguem a gravação privada do simulador. Os campos19–27 e a submissão à AT permanecem fora deste lote; 17/18 são diferenças antes de créditos anteriores, sem ordem de pagamento ou promessa de reembolso.</p>
{book.result&&<><p role="status">{acquisitionCurrent(draft,book)?'Resultado correspondente às entradas guardadas.':'Entradas ou origem alteradas: resultado histórico preservado; actualizar o cálculo.'}</p><dl>{acquisitionReportRows(draft).map(([,label,value,origin],i)=><div key={i}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</section>;
}