106 lines
22 KiB
TypeScript
106 lines
22 KiB
TypeScript
import { useState } from 'react';
|
||
import { fundraisingActivities } from '@/lib/iva-fundraising';
|
||
import { pharmaAnnexEntries, pharmaAnnexEntry, pharmaAnnexReference } from '@/lib/iva-pharma-annex';
|
||
import { annexIEntries, annexIEntry, annexIReference } from '@/lib/iva-annex-i';
|
||
import { temporaryBenefit, temporaryBenefitNotice } from '@/lib/iva-temporary-benefits';
|
||
import { isExportService } from '@/lib/iva-export-services';
|
||
import { isSpecialRegime, specialRegimesFor, specialRegimeIssue, type SpecialRegime } from '@/lib/iva-special-regimes';
|
||
import { isOnboardSupply, supplyCategories, onboardSupplyIssue, type SupplyCategory } from '@/lib/iva-onboard-supplies';
|
||
import { emptyProductionTaxOption, type ProductionTaxOption } from '@/lib/iva-production-option';
|
||
import { referenceExportOperation, continueTemporaryOperation, continueAnnexIOperation, 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);
|
||
const [annexSearch,setAnnexSearch]=useState<Record<string,string>>({});
|
||
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)}));
|
||
const continueTemporary=(id:string,target:string)=>run(()=>save({...book,rows:book.rows.map(row=>row.id===id?continueTemporaryOperation(row,target):row)}));
|
||
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>Campo06 — limitação residualP001</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),annexI=annexIEntry(row.annexIItem);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=>temporaryBenefit(row.treatment)&&!temporaryBenefit(e.target.value)?continueTemporary(row.id,e.target.value):change(row.id,{treatment:e.target.value,confirmed:[]})}>{operationTreatments.map(t=><option value={t.id} key={t.id}>{t.label}</option>)}</select></label>
|
||
</div>
|
||
{isSpecialRegime(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Regime aduaneiro ou fiscal da operação</legend>
|
||
<label>Regime do artigo14(1)<select value={specialRegimesFor(row.treatment).some(([value])=>value===row.specialRegime)?row.specialRegime:''} onChange={e=>change(row.id,{specialRegime:e.target.value as SpecialRegime,confirmed:[]})}><option value="">Por identificar</option>{specialRegimesFor(row.treatment).map(([value,label])=><option value={value} key={value}>{label}</option>)}</select></label>
|
||
<p>Identifique no fundamento os bens, a operação e o documento ou pressuposto expresso do regime. A escolha conserva preço, moeda e período e pede a revisão das condições; não comprova o enquadramento por estar numa zona. Bens e serviço de transporte ou serviço conexo têm preços próprios, sem duplicar o valor.</p>
|
||
{specialRegimeIssue(row.treatment,row.specialRegime)&&<p role="status">{specialRegimeIssue(row.treatment,row.specialRegime)}</p>}
|
||
</fieldset>}
|
||
{isOnboardSupply(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Natureza do abastecimento a bordo</legend>
|
||
<label>Categoria do artigo13(3)<select value={row.supplyCategory??''} onChange={e=>change(row.id,{supplyCategory:e.target.value as SupplyCategory,confirmed:[]})}><option value="">Por identificar</option>{supplyCategories.map(([value,label])=><option key={value} value={value}>{label}</option>)}</select></label>
|
||
<p>A escolha conserva o preço, a moeda e o período; reveja as condições. Para fornecimento misto, discrimine as parcelas efectivas sem repetir o total em cada linha. Desconhecido não equivale a «sem bebidas».</p>
|
||
{onboardSupplyIssue(row.treatment,row.supplyCategory)&&<p role="status">{onboardSupplyIssue(row.treatment,row.supplyCategory)}</p>}
|
||
</fieldset>}
|
||
{isExportService(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Referência à operação relacionada</legend>
|
||
<p>Se a operação já consta desta oportunidade,pode referenciá-la sem preencher de novo a descrição. A escolha conserva o preço,moeda,período e fundamento deste serviço;limpa as confirmações para revisão. Não copia o valor dos bens para o preço do serviço nem comprova automaticamente a isenção. Para operação documentada fora deste livro,use o fundamento abaixo.</p>
|
||
<label>Operação já preenchida<select value={row.relatedExport?.id??''} onChange={e=>run(()=>{if(!e.target.value){save({...book,rows:book.rows.map(item=>item.id===row.id?{...item,relatedExport:undefined,confirmed:[]}:item)});return;}const related=book.rows.find(item=>item.id===e.target.value);if(!related)throw new Error('Operação relacionada já não disponível; dados conservados.');save({...book,rows:book.rows.map(item=>item.id===row.id?referenceExportOperation(item,related):item)});})}><option value="">Sem referência interna — identificar no fundamento</option>{book.rows.filter(item=>item.id!==row.id).map(item=><option key={item.id} value={item.id}>{item.description||item.id} — {item.period}</option>)}</select></label>
|
||
{row.relatedExport&&<><p>Referência guardada: {row.relatedExport.description}; ID {row.relatedExport.id}; {row.relatedExport.treatment}.</p>{book.rows.some(item=>item.id===row.relatedExport!.id)&&<button type="button" onClick={()=>run(()=>{const related=book.rows.find(item=>item.id===row.relatedExport!.id)!;save({...book,rows:book.rows.map(item=>item.id===row.id?referenceExportOperation(item,related):item)});})}>Actualizar referência escolhida e rever condições</button>}</>}
|
||
</fieldset>}
|
||
{temporaryBenefit(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Benefício temporário — prazo e continuidade</legend>
|
||
<p>{temporaryBenefitNotice(row.treatment,row.period)}</p>
|
||
<p>{temporaryBenefit(row.treatment)!.historicalScope}</p>
|
||
<p>Não precisa de preencher documentos de uma isenção já terminada para prosseguir em2026. Antes de escolher outro tratamento, confirme a natureza da operação: a escolha conserva valor,moeda,período,estabelecimento eorigem; limpa as confirmações para revisão e mantém o resultado anterior no histórico.</p>
|
||
<button type="button" onClick={()=>continueTemporary(row.id,'general')}>Escolher operação interna geral e rever condições</button>
|
||
{row.treatment==='temporary_sugar_cane'&&<button type="button" onClick={()=>continueTemporary(row.id,'production_agriculture')}>Analisar produção agrícola própria — artigo9(7)(a)</button>}
|
||
<p>Para outro enquadramento disponível,use a escolha «Tratamento da operação». Nenhum resultado é recalculado automaticamente.</p>
|
||
</fieldset>}
|
||
{temporaryBenefit(row.temporaryBenefitOrigin)&&!temporaryBenefit(row.treatment)&&<p>Origem conservada: {temporaryBenefit(row.temporaryBenefitOrigin)!.label},benefício com termo31/12/2025. Enquadramento actual escolhido expressamente;confirme as respectivas condições antes de calcular.</p>}
|
||
{row.treatment==='annexI_goods'&&<fieldset className="space-y-2 rounded border p-2"><legend>Bem do Anexo I — edição Lei22/2022</legend>
|
||
<p>Escolha código e designação e confira apenas os qualificadores dessa linha. A pesquisa conserva a escolha actual; mudar de posição exige rever as condições específicas. O anexo não dispensa o enquadramento de outras isenções aplicáveis ao mesmo bem.</p>
|
||
<label className="block">Pesquisar código ou designação no Anexo I<input value={annexSearch[`I:${row.id}`]??''} onChange={e=>setAnnexSearch({...annexSearch,[`I:${row.id}`]:e.target.value})}/></label>
|
||
<label className="block">Posição do Anexo I<select className="block max-w-full rounded border p-2" value={row.annexIItem??''} onChange={e=>change(row.id,{annexIItem:e.target.value,annexIOnly:undefined,confirmed:row.confirmed.filter(n=>n===0)})}><option value="">Ainda por determinar</option>{annexIEntries.filter(entry=>entry.id===row.annexIItem||`${entry.code} ${entry.context??''} ${entry.label}`.toLocaleLowerCase('pt').includes((annexSearch[`I:${row.id}`]??'').trim().toLocaleLowerCase('pt'))).map(entry=><option key={entry.id} value={entry.id}>{entry.code} — {entry.context?entry.context+' — ':''}{entry.label} (PDF{entry.pdfPage})</option>)}</select></label>
|
||
{row.annexIItem&&<p>{annexI?annexIReference(annexI):`Posição guardada não encontrada nesta edição: ${row.annexIItem}. Original conservado; escolha explicitamente a correspondência.`}</p>}
|
||
{!!annexI?.relatedTreatments?.length&&<div><p>Confira a natureza e o destino: há um tratamento existente que pode abranger este bem. Se for aplicável, use-o para conservar também o direito à dedução nas condições legais. Ao confirmar a passagem, conserva valores e origem e terá de rever as condições do tratamento escolhido; nenhum resultado anterior é substituído automaticamente.</p>
|
||
{annexI.relatedTreatments.map(id=>{const target=operationTreatments.find(t=>t.id===id);return target&&<button className="block underline" type="button" key={id} onClick={()=>run(()=>save({...book,rows:book.rows.map(r=>r.id===row.id?continueAnnexIOperation(r,id):r)}))}>Usar {target.label} e rever condições</button>;})}
|
||
{!annexI.relatedRequired&&<label className="block"><input type="checkbox" checked={row.annexIOnly===true} onChange={e=>change(row.id,{annexIOnly:e.target.checked})}/>Conferi a designação e o destino: esta operação não corresponde ao tratamento indicado acima; o fundamento explica a aplicação apenas do artigo9(12)(h).</label>}
|
||
</div>}
|
||
</fieldset>}
|
||
{row.treatment==='pharma_manufacturing_inputs'&&<fieldset className="space-y-2 rounded border p-2"><legend>Bem do Anexo III — edição Lei22/2022</legend>
|
||
<p>Escolha a linha pelo código e pela designação. O estabelecimento que transmite deve estar integrado no SNS; o destino hospitalar não substitui essa condição. A selecção não classifica automaticamente uma mercadoria nem confirma a sua utilização.</p>
|
||
<label className="block">Pesquisar código ou designação<input value={annexSearch[row.id]??''} onChange={e=>setAnnexSearch({...annexSearch,[row.id]:e.target.value})}/></label>
|
||
<label className="block">Linha do Anexo III<select className="block max-w-full rounded border p-2" value={row.pharmaAnnexItem??''} onChange={e=>change(row.id,{pharmaAnnexItem:e.target.value})}><option value="">Ainda por determinar</option>{pharmaAnnexEntries.filter(entry=>entry.id===row.pharmaAnnexItem||`${entry.code} ${entry.label}`.toLocaleLowerCase('pt').includes((annexSearch[row.id]??'').trim().toLocaleLowerCase('pt'))).map(entry=><option key={entry.id} value={entry.id}>{entry.code} — {entry.label} (PDF{entry.pdfPage})</option>)}</select></label>
|
||
{row.pharmaAnnexItem&&<p>{pharmaAnnexEntry(row.pharmaAnnexItem)?pharmaAnnexReference(pharmaAnnexEntry(row.pharmaAnnexItem)!):`Escolha guardada não encontrada nesta edição: ${row.pharmaAnnexItem}. Original conservado; seleccione explicitamente a correspondência.`}</p>}
|
||
<p>O exemplar repete2912.19.00 em duas linhas com designações diferentes e imprime2935. sem completar a posição. As linhas mantêm identidades próprias; não se inventam dígitos nem se estende a isenção apenas pelo código.</p>
|
||
</fieldset>}
|
||
{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>P001mantém-se residual,sem atribuição automática06;retoma apenas com novo esclarecimento concreto. Os tratamentos independentes mantêm-se disponíveis. 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>;
|
||
}
|