57 lines
12 KiB
TypeScript
57 lines
12 KiB
TypeScript
import { useState } from 'react';
|
||
import { fundraisingActivities } from '@/lib/iva-fundraising';
|
||
import { emptyProductionTaxOption, type ProductionTaxOption } from '@/lib/iva-production-option';
|
||
import { modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
|
||
|
||
export function SimulationOperations({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
|
||
const [error,setError]=useState('');
|
||
const [reuseOption,setReuseOption]=useState<{rowId:string;sourceId:string}|null>(null);
|
||
let book:SalesBook;
|
||
try{book=salesBook(draft);}catch(cause){return <p role="alert">{String(cause)}</p>;}
|
||
const run=(action:()=>void)=>{try{action();setError('');}catch(cause){setError(cause instanceof Error?cause.message:String(cause));}};
|
||
const save=(next:SalesBook)=>onChange('modelo_a_snapshot',saveSalesBook(draft,next));
|
||
const change=(id:string,patch:Partial<SalesOperation>)=>run(()=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)}));
|
||
return <section aria-label="Operações IVA por oportunidade" className="space-y-3 rounded border p-3">
|
||
<h4 className="font-semibold">Operações de IVA — enquadramento das vendas</h4>
|
||
<p>Oportunidade {String(draft.context_opportunity||'entrada geral')}; moeda original {book.currency||'por indicar'}. Cobertura2026/MZN. Reutilize as vendas e complete apenas o enquadramento. Este bloco classifica as operações; não substitui nem soma novamente a execução mensal geral acima.</p>
|
||
<p>Uma empresa pode ter operações com tratamentos diferentes. Os valores são projecções sob pressupostos declarados, sem certificar facturas, reconhecer créditos ou submeter uma declaração à AT.</p>
|
||
<details><summary>Correspondência do campo06 e limites do formulário</summary><p>{modeloA06Correspondence}</p></details>
|
||
{error&&<p role="alert">{error}</p>}
|
||
<button type="button" className="underline" onClick={()=>run(()=>save(reuseSales(draft,book)))}>Reutilizar vendas e períodos existentes</button>
|
||
<button type="button" className="ml-3 underline" disabled={!book.periods.length} onClick={()=>run(()=>save({...book,rows:[...book.rows,newSalesOperation(crypto.randomUUID(),book.periods[0].period,book.currency)]}))}>Acrescentar operação em falta</button>
|
||
{book.rows.map(row=>{const rule=operationTreatments.find(r=>r.id===row.treatment);return <details className="rounded border p-3" key={row.id}><summary>{row.description||'Operação adicional'} — {row.period}</summary>
|
||
<p>Origem: {row.source}; {row.sourceAmount??'sem valor de origem'} {row.sourceCurrency}; {row.sourcePeriod}. O estabelecimento não é inferido da localização do projecto.</p>
|
||
<div className="grid gap-3 md:grid-cols-2">
|
||
<label>Descrição da operação<input className="block rounded border p-2" value={row.description} onChange={e=>change(row.id,{description:e.target.value})}/></label>
|
||
<label>Estabelecimento<input className="block rounded border p-2" value={row.establishment} onChange={e=>change(row.id,{establishment:e.target.value})}/></label>
|
||
<label>Período<input type="month" className="block rounded border p-2" min="2026-01" max="2026-12" value={row.period} onChange={e=>change(row.id,{period:e.target.value})}/></label>
|
||
<label>Valor declarado<input inputMode="decimal" className="block rounded border p-2" value={row.amount} onChange={e=>change(row.id,{amount:e.target.value})}/></label>
|
||
<label>Representação do preço<select className="block rounded border p-2" value={row.basis} onChange={e=>change(row.id,{basis:e.target.value as SalesOperation['basis']})}><option value="">Ainda desconhecida</option><option value="net">Sem IVA</option><option value="included">IVA incluído</option></select></label>
|
||
<label>Tratamento da operação<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,{treatment:e.target.value,confirmed:[]})}>{operationTreatments.map(t=><option value={t.id} key={t.id}>{t.label}</option>)}</select></label>
|
||
</div>
|
||
{row.treatment.startsWith('fundraising_')&&<fieldset className="space-y-2 rounded border p-2"><legend>Manifestação ocasional — limite anual da entidade</legend><p>O total é declarado para a entidade em todo o ano, incluindo manifestações fora deste dossier. Várias vendas da mesma manifestação não são novas manifestações.</p>
|
||
{([['entityReference','Referência da entidade organizadora'],['annualEvents','Total anual de manifestações da entidade, incluindo esta'],['eventNumber','Número desta manifestação no ano']] as const).map(([field,label])=><label className="block" key={field}>{label}<input value={row.fundraiser?.[field]??''} onChange={e=>change(row.id,{fundraiser:{entityReference:'',annualEvents:'',eventNumber:'',activity:'',...row.fundraiser,[field]:e.target.value}})}/></label>)}
|
||
<label className="block">Actividade habitual isenta<select value={row.fundraiser?.activity??''} onChange={e=>change(row.id,{fundraiser:{entityReference:'',annualEvents:'',eventNumber:'',...row.fundraiser,activity:e.target.value}})}><option value="">Por identificar</option>{fundraisingActivities.filter(([id])=>row.treatment==='fundraising_nonprofit'?!id.endsWith('_public'):id!=='statutory_membership').map(([id,label])=><option key={id} value={id}>{label}</option>)}</select></label>
|
||
</fieldset>}
|
||
{row.treatment.startsWith('production_')&&row.treatment.endsWith('_opted')&&<fieldset className="space-y-2 rounded border p-2"><legend>Opção de tributação da produção — artigo11</legend>
|
||
<p>Complete apenas as datas em falta. A opção não cessa automaticamente ao fim de cinco anos. O prazo termina às 24 horas do dia correspondente; se faltar esse dia, no último dia do mesmo mês. O artigo11(3) exige declaração antes de expirar o prazo e fixa os efeitos em 1 de Janeiro seguinte, respeitando a permanência mínima.</p>
|
||
{book.rows.some(source=>source.id!==row.id&&source.taxOption)&&<label className="block">Reutilizar opção já preenchida neste dossier<select value={reuseOption?.rowId===row.id?reuseOption.sourceId:''} onChange={e=>setReuseOption(e.target.value?{rowId:row.id,sourceId:e.target.value}:null)}><option value="">Escolha a origem, se aplicável ao mesmo sujeito passivo</option>{book.rows.filter(source=>source.id!==row.id&&source.taxOption).map(source=><option key={source.id} value={source.id}>{source.description} — {source.establishment}</option>)}</select></label>}
|
||
{reuseOption?.rowId===row.id&&<div><p>Ao confirmar, substituirá o enquadramento e as datas da opção desta linha pelos da origem escolhida, conservando a data desta operação. Confirme que se aplicam ao mesmo sujeito passivo; o resultado anterior permanece no histórico.</p><button type="button" onClick={()=>run(()=>{const source=book.rows.find(r=>r.id===reuseOption.sourceId);if(!source?.taxOption)throw new Error('Origem da opção já não disponível; dados conservados.');change(row.id,{taxOption:{...structuredClone(source.taxOption),operationDate:row.taxOption?.operationDate??'',origin:{rowId:source.id,description:source.description,establishment:source.establishment}}});setReuseOption(null);})}>Confirmar reutilização da opção escolhida</button><button type="button" onClick={()=>setReuseOption(null)}>Conservar os dados desta linha</button></div>}
|
||
<label className="block">Tipo de declaração de opção<select value={row.taxOption?.mode??''} onChange={e=>change(row.id,{taxOption:{...emptyProductionTaxOption(),...row.taxOption,mode:e.target.value as ProductionTaxOption['mode']}})}><option value="">Por indicar</option><option value="annual">Opção com efeitos no ano seguinte</option><option value="startup">Opção na declaração de início da actividade</option></select></label>
|
||
<label className="block">Declaração de regresso à isenção<select value={row.taxOption?.returnStatus??''} onChange={e=>change(row.id,{taxOption:{...emptyProductionTaxOption(),...row.taxOption,returnStatus:e.target.value as ProductionTaxOption['returnStatus']}})}><option value="">Ainda desconhecido</option><option value="none">Não existe declaração de regresso</option><option value="declared">Existe declaração de regresso</option></select></label>
|
||
{([['declarationDate','Data da declaração de opção'],...(row.taxOption?.mode==='startup'?[['startupDate','Data de início da actividade']]:[]),['operationDate','Data da operação nesta linha'],...(row.taxOption?.returnStatus==='declared'||row.taxOption?.returnDeclarationDate?[['returnDeclarationDate','Data da declaração de regresso à isenção']]:[])] as Array<['declarationDate'|'startupDate'|'operationDate'|'returnDeclarationDate',string]>).map(([field,label])=><label className="block" key={field}>{label}<input type="date" value={row.taxOption?.[field]??''} onChange={e=>change(row.id,{taxOption:{...emptyProductionTaxOption(),...row.taxOption,[field]:e.target.value}})}/></label>)}
|
||
</fieldset>}
|
||
<p>{rule?.article}; {rule?.source}. Aplicação nesta versão: Janeiro–Dezembro2026, sujeita às condições seguintes.</p>
|
||
{rule?.conditions.map((condition,index)=><label className="block" key={index}><input type="checkbox" checked={row.confirmed.includes(index)} onChange={e=>change(row.id,{confirmed:e.target.checked?[...row.confirmed,index]:row.confirmed.filter(i=>i!==index)})}/> {condition}</label>)}
|
||
<label className="block">Documento ou pressuposto que sustenta estas condições<textarea className="block w-full rounded border p-2" value={row.evidence} onChange={e=>change(row.id,{evidence:e.target.value})}/></label>
|
||
<p>Se precisar de repartir uma venda agregada, conserve o valor de origem e prepare linhas discriminadas. Não altere silenciosamente a sua representação monetária.</p>
|
||
<button type="button" className="mr-3 underline" onClick={()=>run(()=>save(splitSales(book,row.id,crypto.randomUUID())))}>Repartir explicitamente esta venda, conservando o total de origem</button>
|
||
<button type="button" className="underline" onClick={()=>run(()=>save({...book,rows:book.rows.filter(r=>r.id!==row.id),periods:book.periods.map(p=>p.period===row.period?{...p,complete:false}:p)}))}>Remover esta linha do preenchimento (resultado anterior conservado)</button>
|
||
</details>;})}
|
||
{book.periods.map(p=><label className="block" key={p.period}><input type="checkbox" checked={p.complete} onChange={e=>run(()=>save({...book,periods:book.periods.map(v=>v.period===p.period?{...v,complete:e.target.checked}:v)}))}/> {p.period}: incluí todas as operações deste período; lista vazia significa ausência expressamente confirmada.</label>)}
|
||
<p>Limites: outras isenções/bases especiais e correspondência do campo06 permanecem por delimitar. IVA incluído a5% conserva a fracção exacta e apresenta um decimal a6casas; a aproximação de apresentação não constitui regra declarativa. O desconhecido nunca é assumido como zero.</p>
|
||
<button type="button" disabled={!book.periods.length} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50" onClick={()=>run(()=>save(recordSales(draft,book)))}>Calcular e guardar operações IVA</button>
|
||
{book.result&&<><p role="status">{salesCurrent(draft,book)?'Resultado correspondente às entradas.':'Resultado histórico conservado; reveja as alterações antes de recalcular.'}</p><dl>{salesReportRows(draft).map(([,label,value,origin],index)=><div className="break-inside-avoid border-t py-2" key={index}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
|
||
</section>;
|
||
}
|