Files
Investidor-V2/resources/js/components/simulation-irpc-annual.tsx
T
2026-11-10 15:09:31 +02:00

611 lines
28 KiB
TypeScript

import { SimulationIrpcInvestment } from './simulation-irpc-investment';
import { SimulationIrpcEconomic } from './simulation-irpc-economic';
import { FiscalProofParcelFields } from './fiscal-proof-parcel';
import { SimulationIrpcCarry } from './simulation-irpc-carry';
import { SimulationIrpcInternational } from './simulation-irpc-international';
import { SimulationIrpcPec } from './simulation-irpc-pec';
import { useState } from 'react';
import {
annualBook,
newAnnualBook,
saveAnnualBook,
reuseAnnualAssessment,
annualAssessment,
annualWithholdingOptions,
reuseAnnualWithholding,
newAnnualPpc,
recordAnnual,
annualReportRows,
annualHtml,
annualSource,
type AnnualBook,
type AnnualPpc,
} from '@/lib/irpc-annual-settlement';
type Props = {
draft: Record<string, unknown>;
onChange: (field: string, value: string) => void;
};
export function SimulationIrpcAnnual({ draft, onChange }: Props) {
const [error, setError] = useState('');
if (draft.fiscal_component !== 'irpc') return null;
try {
const b = annualBook(draft),
save = (next: AnnualBook) => {
try {
onChange('modelo_a_snapshot', saveAnnualBook(draft, next));
setError('');
} catch (e) {
setError(String(e));
}
};
if (!b)
return (
<section
aria-label="IRPC — L8 fecho anual"
className="rounded border p-4"
>
<h4>L8 — da colecta ao saldo anual simulado</h4>
<p>
Reutiliza a colecta válida, retenções efectivamente
sofridas por conta e PPC comprovados do próprio
sujeito/exercício. PEC comprovado pode ser detalhado no
próprio exercício; créditos com regras próprias
permanecem separados.
</p>
<button
type="button"
onClick={() => {
try {
save(
reuseAnnualAssessment(
draft,
newAnnualBook(),
),
);
} catch (e) {
setError(String(e));
}
}}
>
Preparar saldo anual L8
</button>
{error && <p role="alert">{error}</p>}
</section>
);
const patch = (p: Partial<AnnualBook>) => save({ ...b, ...p }),
a = annualAssessment(draft),
options = annualWithholdingOptions(draft);
const field = (
key: 'subjectNuit' | 'year' | 'reference' | 'assessmentReference',
label: string,
) => (
<label className="block">
{label}
<input
aria-label={label}
value={b[key]}
onChange={(e) => patch({ [key]: e.target.value })}
/>
</label>
);
const group = (key: 'withholdings' | 'ppc', label: string) => (
<>
<label className="block">
Estado — {label}
<select
aria-label={'Estado L8 — ' + label}
value={b[key].state}
onChange={(e) =>
patch({
[key]: {
...b[key],
state: e.target.value as
| ''
| 'known'
| 'absent',
},
})
}
>
<option value="">Informação em falta</option>
<option value="known">
Valores conhecidos — seleccionar comprovativos
</option>
<option value="absent">
Ausência declarada com suporte
</option>
</select>
</label>
<label className="block">
Suporte — {label}
<input
aria-label={'Suporte L8 — ' + label}
value={b[key].reference}
onChange={(e) =>
patch({
[key]: { ...b[key], reference: e.target.value },
})
}
/>
</label>
</>
);
return (
<section
aria-label="IRPC — L8 fecho anual"
className="space-y-3 rounded border p-4"
>
<h4>L8 — colecta, abatimentos e saldo anual simulado</h4>
<p>
A colecta acima é anterior aos abatimentos. Este saldo tem
cálculo e histórico próprios; retenção entregue pela empresa
por pagamento a terceiros não é retenção sofrida. O
resultado continua simulado, sem submissão, pagamento ou
reembolso automático.
</p>
<p>{annualSource(b)}</p>
{error && <p role="alert">{error}</p>}
{field('subjectNuit', 'NUIT do sujeito do fecho L8')}
{field('year', 'Exercício do fecho L8')}
{field(
'reference',
'Suporte do fecho da entidade e exercício L8',
)}
<p>
Colecta disponível:
{a.colecta
? a.colecta.numerator +
'/' +
a.colecta.denominator +
' MZN'
: 'Indeterminada'}
;origem:
{a.kind === 'special' ? 'L7 qualificado' : 'L6 normal'}
;NUIT:{a.subjectNuit || 'Por identificar'};exercício:
{a.year || 'Por identificar'}.
</p>
<button
type="button"
onClick={() => {
try {
save(reuseAnnualAssessment(draft, b));
} catch (e) {
setError(String(e));
}
}}
>
Reutilizar colecta corrente L6/L7
</button>
{field(
'assessmentReference',
'Suporte da colecta reutilizada L8',
)}
<fieldset className="space-y-2 rounded border p-3">
<legend>Retenções efectivamente sofridas por conta</legend>
{group('withholdings', 'Retenções sofridas')}
{options.length === 0 ? (
<p>
Não há afectações de comprovativosL5 neste contexto.
Reutilize e reconcilie a retenção recebida emL5 ou
declare ausência com suporte.
</p>
) : (
options.map((o) => (
<div
key={o.rowId + '|' + o.paymentId}
className="rounded border p-2"
>
<p>
{o.document || 'Prova por identificar'}
;emitente:{o.issuer};data:{o.date}
;parcelaL5:{o.amount}MZN;
{o.reason ||
'Retenção recebida/efectiva por conta reconciliada emL5.'}
</p>
<button
type="button"
disabled={
!o.ready ||
b.withholdings.rows.some(
(r) =>
r.rowId === o.rowId &&
r.paymentId === o.paymentId,
)
}
onClick={() => {
try {
save(
reuseAnnualWithholding(
draft,
b,
o.rowId,
o.paymentId,
crypto.randomUUID(),
),
);
} catch (e) {
setError(String(e));
}
}}
>
Seleccionar prova L5{' '}
{o.document || o.paymentId}
</button>
</div>
))
)}
{b.withholdings.rows.map((r, index) => {
const change = (p: Partial<typeof r>) =>
patch({
withholdings: {
...b.withholdings,
rows: b.withholdings.rows.map((x) =>
x.id === r.id ? { ...x, ...p } : x,
),
},
});
return (
<fieldset key={r.id} className="rounded border p-2">
<legend>
Retenção seleccionada {index + 1}
</legend>
<p>
Rendimento:{r.rowId};prova:{r.paymentId}
;NUIT:{r.subjectNuit};exercício:{r.year}.
Fonte conservada. Se mudou, retire esta
selecção e seleccione a prova actual. O
histórico mantém-se.
</p>
<label className="block">
Parcela a abater
<input
aria-label={
'Parcela da retenção L8 ' +
(index + 1)
}
value={r.amount}
onChange={(e) =>
change({ amount: e.target.value })
}
/>
</label>
<label className="block">
O próprio sujeito é beneficiário e o
rendimento está incluído na base deste
exercício
<select
aria-label={
'Rendimento incluído L8 ' +
(index + 1)
}
value={r.incomeIncluded}
onChange={(e) =>
change({
incomeIncluded: e.target
.value as '' | 'yes' | 'no',
})
}
>
<option value="">
Informação em falta
</option>
<option value="yes">
Sim — documentado
</option>
<option value="no">
Não — crédito não aplicável neste
fecho
</option>
</select>
</label>
<label className="block">
Suporte da titularidade e inclusão
<input
aria-label={
'Suporte da retenção L8 ' +
(index + 1)
}
value={r.reference}
onChange={(e) =>
change({
reference: e.target.value,
})
}
/>
</label>
<FiscalProofParcelFields
value={r}
change={change}
label={'Retenção L8 ' + (index + 1)}
/>
<button
type="button"
onClick={() =>
patch({
withholdings: {
...b.withholdings,
rows: b.withholdings.rows.filter(
(x) => x.id !== r.id,
),
},
})
}
>
Retirar selecção L8 da retenção {index + 1}
</button>
</fieldset>
);
})}
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>PPC efectivamente pagos</legend>
{group('ppc', 'PPC efectuados')}
<p>
Regista comprovativos de pagamentos por conta; não
calcula automaticamente a obrigação PPC nem usa
pagamentos previstos no plano.
</p>
<button
type="button"
onClick={() =>
patch({
ppc: {
...b.ppc,
rows: [
...b.ppc.rows,
{
...newAnnualPpc(
crypto.randomUUID(),
),
subjectNuit: b.subjectNuit,
year: b.year,
},
],
},
})
}
>
Registar comprovativo PPC
</button>
{b.ppc.rows.map((r, index) => {
const change = (p: Partial<AnnualPpc>) =>
patch({
ppc: {
...b.ppc,
rows: b.ppc.rows.map((x) =>
x.id === r.id ? { ...x, ...p } : x,
),
},
});
return (
<fieldset key={r.id} className="rounded border p-2">
<legend>PPC {index + 1}</legend>
{(
[
['document', 'Comprovativo'],
['issuer', 'Emitente'],
['date', 'Data (AAAA-MM-DD)'],
['subjectNuit', 'NUIT beneficiário'],
['year', 'Exercício do comprovativo'],
['currency', 'Moeda do comprovativo'],
['proofAmount', 'Montante comprovado'],
['amount', 'Parcela a abater'],
[
'reference',
'Suporte do pagamento e afectação',
],
] as const
).map(([key, label]) => (
<label key={key} className="block">
{label}
<input
aria-label={
'PPC L8 ' +
(index + 1) +
' — ' +
label
}
value={r[key]}
onChange={(e) =>
change({
[key]: e.target.value,
})
}
/>
</label>
))}
<label className="block">
Pagamento efectivamente comprovado
<select
aria-label={
'PPC L8 ' +
(index + 1) +
' — Efectivo'
}
value={r.effective}
onChange={(e) =>
change({
effective: e.target.value as
| ''
| 'yes'
| 'no',
})
}
>
<option value="">
Informação em falta
</option>
<option value="yes">
Sim — comprovativo reconciliado
</option>
<option value="no">
Não — apenas previsão/outro estado
</option>
</select>
</label>
<FiscalProofParcelFields
value={r}
change={change}
label={'PPC L8 ' + (index + 1)}
/>
<button
type="button"
onClick={() =>
patch({
ppc: {
...b.ppc,
rows: b.ppc.rows.filter(
(x) => x.id !== r.id,
),
},
})
}
>
Retirar PPC {index + 1}
</button>
</fieldset>
);
})}
</fieldset>
<SimulationIrpcEconomic
book={b}
save={save}
residentConfirmed={
a.current &&
a.book.scope === 'resident_general' &&
b.subjectNuit === a.subjectNuit &&
b.year === a.year &&
b.assessmentKey === a.key
}
/>
<SimulationIrpcInternational book={b} save={save} />
<SimulationIrpcInvestment draft={draft} book={b} save={save} />
<SimulationIrpcPec book={b} save={save} />
<SimulationIrpcCarry book={b} save={save} />
{[
...(b.pecPayments ? [] : [['pec', 'PEC'] as const]),
...(b.internationalCredits
? []
: [
[
'credits',
'Créditos com regras próprias',
] as const,
]),
].map(([key, label]) => (
<fieldset key={key} className="rounded border p-3">
<legend>{label} — tratamento separado</legend>
<label className="block">
Estado — {label}
<select
aria-label={'Estado L8 — ' + label}
value={b[key].state}
onChange={(e) =>
patch({
[key]: {
...b[key],
state: e.target.value as
| ''
| 'known'
| 'absent',
},
})
}
>
<option value="">Informação em falta</option>
<option value="absent">
Ausência declarada com suporte
</option>
<option value="known">
Valor conhecido — positivo exige regra
própria
</option>
</select>
</label>
{(b[key].state === 'known' || b[key].amount) && (
<label className="block">
Montante declarado
<input
aria-label={'Montante L8 — ' + label}
value={b[key].amount}
onChange={(e) =>
patch({
[key]: {
...b[key],
amount: e.target.value,
},
})
}
/>
</label>
)}
<label className="block">
Suporte
<input
aria-label={'Suporte L8 — ' + label}
value={b[key].reference}
onChange={(e) =>
patch({
[key]: {
...b[key],
reference: e.target.value,
},
})
}
/>
</label>
</fieldset>
))}
<button
type="button"
onClick={() => {
try {
save(recordAnnual(draft, b));
} catch (e) {
setError(String(e));
}
}}
>
Calcular e guardar saldo anual L8
</button>
<button
type="button"
onClick={() => {
try {
const html = annualHtml(draft),
url = URL.createObjectURL(
new Blob([html], {
type: 'text/html;charset=utf-8',
}),
),
link = document.createElement('a');
link.href = url;
link.download = 'IRPC-L8-saldo-simulado.html';
link.click();
URL.revokeObjectURL(url);
setError('');
} catch (e) {
setError(String(e));
}
}}
>
Exportar saldo anual L8
</button>
{annualReportRows(draft).map((r, n) => (
<p key={n}>
{r[1]} — {r[2]} — {r[3]}
</p>
))}
</section>
);
} catch (e) {
return (
<p role="alert">
PreparaçãoL8 não recuperável:{String(e)}. Originais e históricos
conservados.
</p>
);
}
}