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

1149 lines
57 KiB
TypeScript

import { inputText } from '../lib/input-text.ts';
import {
newSpecialCombinations,
specialCombinationsSource,
} from '@/lib/irpc-special-combinations';
import type { NormalBook } from '@/lib/irpc-normal';
import {
newSpecialInput,
newSpecialExpense,
newAcceleratedAsset,
specialAccelerationSource,
specialCessationPriorSource,
specialStartPeriodSource,
specialMonthlySource,
specialSources,
type SpecialInput,
type Qualification,
} from '@/lib/irpc-special';
type Props = {
draft: Record<string, unknown>;
book: NormalBook;
save: (b: NormalBook) => void;
};
const states = [
['', 'Informação em falta'],
['known', 'Tratamento conhecido — qualificar condições'],
['absent', 'Ausência declarada com suporte'],
['not_applicable', 'Tratamento não aplicável — justificar'],
] as const;
export function QualificationFields({
label,
value,
onChange,
}: {
label: string;
value: Qualification;
onChange: (v: Qualification) => void;
}) {
const field = (
key: 'from' | 'to' | 'reference' | 'periodReference',
text: string,
) => (
<label className="block">
{text}
<input
aria-label={label + ' — ' + text}
value={value[key]}
onChange={(e) => onChange({ ...value, [key]: e.target.value })}
/>
</label>
);
return (
<div className="space-y-2">
<label className="block">
Estado — {label}
<select
aria-label={'Estado — ' + label}
value={value.state}
onChange={(e) =>
onChange({
...value,
state: e.target.value as Qualification['state'],
eligible: '',
})
}
>
{states.map(([v, t]) => (
<option key={v} value={v}>
{t}
</option>
))}
</select>
</label>
{value.state === 'known' && (
<label className="block">
Condições — {label}
<select
aria-label={'Condições — ' + label}
value={value.eligible}
onChange={(e) =>
onChange({
...value,
eligible: e.target
.value as Qualification['eligible'],
})
}
>
<option value="">Por esclarecer</option>
<option value="yes">
Sim — condições documentadas
</option>
<option value="no">
Não — condição negativa documentada
</option>
</select>
</label>
)}
{field(
'reference',
'Prova das condições, ausência ou não aplicação',
)}
{field('from', 'Período — início (AAAA-MM-DD)')}
{field('to', 'Período — termo (AAAA-MM-DD)')}
{field(
'periodReference',
'Suporte do período e da vigência aplicável',
)}
</div>
);
}
export function SimulationIrpcSpecial({ draft, book: b, save }: Props) {
const i = b.special;
const start = () => {
const next = newSpecialInput(),
p = b.periodsFx ?? b.schedules?.period;
for (const part of [next.expenses, next.cooperative, next.acceleration])
Object.assign(part.qualification, {
from: p?.from ?? b.year + '-01-01',
to: p?.to ?? b.year + '-12-31',
});
save({ ...b, special: next });
};
if (!i)
return (
<section className="rounded border p-3">
<h5>L7 — benefícios e casos especiais</h5>
<p>
Preparação opcional e separada do percursoL6 já entregue.
Localização e sector não activam benefícios. A liquidação
final permanece emL8.
</p>
<button type="button" onClick={start}>
Preparar qualificação L7
</button>
</section>
);
const change = (patch: Partial<SpecialInput>) =>
save({ ...b, special: { ...i, ...patch } });
const yesno = (
label: string,
value: string,
onChange: (v: '' | 'yes' | 'no') => void,
) => (
<label className="block">
{label}
<select
aria-label={label}
value={value}
onChange={(e) => onChange(e.target.value as '' | 'yes' | 'no')}
>
<option value="">Informação em falta</option>
<option value="yes">Sim — documentado</option>
<option value="no">Não — documentado</option>
</select>
</label>
);
return (
<section
aria-label="L7 — qualificação e tratamentos"
className="space-y-3 rounded border p-3"
>
<h5>L7 — qualificação comum e tratamentos documentados</h5>
<p>
Reutiliza sujeito, NUIT, período, despesas e activos L6.
Contexto herdado:sector{' '}
{inputText(draft.sector_id ?? 'por identificar')};província{' '}
{inputText(draft.province_id ?? 'por identificar')}. A
localização, por si só, não concede benefícios. Os valores L6
continuam como referência;os efeitosL7 são apresentados
separadamente. Saldo final a pagar/recuperar:âmbitoL8.
</p>
{!i.combinations ? (
<button
type="button"
onClick={() =>
change({ combinations: newSpecialCombinations() })
}
>
Preparar cumulações dos tratamentos L7 registados
</button>
) : (
<fieldset
aria-label="Cumulações L7"
className="space-y-2 rounded border p-3"
>
<legend>
Cumulações — tratamentos, incompatibilidades e limites
</legend>
<p>
{specialCombinationsSource} Reutiliza a cooperativa,os
activos,as despesas e os períodos já declarados abaixo.
Esta preparação discrimina os benefícios em vez da
confirmação global de ausência de outros
benefícios;conserva os valores anteriores e não altera
automaticamente qualificações ou activos.
</p>
<label className="block">
Suporte da combinação no sujeito/projecto/período
<input
aria-label="Suporte da combinação L7"
value={i.combinations.reference}
onChange={(e) =>
change({
combinations: {
...i.combinations!,
reference: e.target.value,
},
})
}
/>
</label>
<label className="block">
Benefícios específicosCBF
<select
aria-label="Benefícios específicos CBF na combinação"
value={i.combinations.specificState}
onChange={(e) =>
change({
combinations: {
...i.combinations!,
specificState: e.target.value as
| ''
| 'absent'
| 'known',
specificException: '',
},
})
}
>
<option value="">Informação em falta</option>
<option value="absent">
Ausência declarada com suporte
</option>
<option value="known">
Existem — identificar artigo e âmbito
</option>
</select>
</label>
<label className="block">
Suporte da presença ou ausência de benefícios
específicos
<input
aria-label="Suporte dos benefícios específicos CBF"
value={i.combinations.specificReference}
onChange={(e) =>
change({
combinations: {
...i.combinations!,
specificReference: e.target.value,
},
})
}
/>
</label>
{i.combinations.specificState === 'known' && (
<label className="block">
Excepção expressa à não cumulação
<select
aria-label="Excepção de cumulação CBF"
value={i.combinations.specificException}
onChange={(e) =>
change({
combinations: {
...i.combinations!,
specificException: e.target
.value as
| ''
| 'absent'
| 'claimed',
},
})
}
>
<option value="">Informação em falta</option>
<option value="absent">
Ausência documentada — combinação
incompatível
</option>
<option value="claimed">
Excepção invocada — correspondência própria
em falta
</option>
</select>
</label>
)}
<label className="block">
Outros benefícios além dos tratamentos registados
<select
aria-label="Outros benefícios na combinação L7"
value={i.combinations.otherState}
onChange={(e) =>
change({
combinations: {
...i.combinations!,
otherState: e.target.value as
| ''
| 'absent'
| 'known',
},
})
}
>
<option value="">Informação em falta</option>
<option value="absent">
Ausência declarada com suporte
</option>
<option value="known">
Existem — tratamento próprio por corresponder
</option>
</select>
</label>
<label className="block">
Suporte dos outros benefícios ou ausência
<input
aria-label="Suporte dos outros benefícios L7"
value={i.combinations.otherReference}
onChange={(e) =>
change({
combinations: {
...i.combinations!,
otherReference: e.target.value,
},
})
}
/>
</label>
<p>
Não somar de novo a amortização contabilística ou a
parcela anterior recuperada. A redução cooperativa
afecta só a colecta sobre a base elegível;o imposto
autónomo continua separado. Uma incompatibilidade não
escolhe o benefício a conservar nem apaga o declarado.
Outros benefícios/excepções sem regra correspondida
mantêm o totalL7 indeterminado.
</p>
<button
type="button"
onClick={() => {
const next = { ...i };
delete next.combinations;
save({ ...b, special: next });
}}
>
Manter tratamentos sem preparar cumulação
</button>
</fieldset>
)}
<fieldset className="space-y-2 rounded border p-3">
<legend>Despesas — tributação autónoma61(4)</legend>
<p>{specialSources.expenses}</p>
<QualificationFields
label="Despesas autónomas"
value={i.expenses.qualification}
onChange={(qualification) =>
change({ expenses: { ...i.expenses, qualification } })
}
/>
<button
type="button"
onClick={() =>
change({
expenses: {
...i.expenses,
rows: [
...i.expenses.rows,
...(b.schedules?.rows ?? [])
.filter(
(r) =>
r.rule === 'undocumented' &&
!i.expenses.rows.some(
(x) =>
x.scheduleId === r.id,
),
)
.map((r) => ({
...newSpecialExpense(
'expense-' + r.id,
),
scheduleId: r.id,
})),
],
},
})
}
>
Reutilizar despesas36g de L6 para qualificar
</button>
{i.expenses.rows.map((r, index) => {
const origin = b.schedules?.rows.find(
(x) => x.id === r.scheduleId,
),
patch = (p: Partial<typeof r>) =>
change({
expenses: {
...i.expenses,
rows: i.expenses.rows.map((x) =>
x.id === r.id ? { ...x, ...p } : x,
),
},
});
return (
<fieldset key={r.id} className="rounded border p-2">
<legend>
{origin
? origin.document + '/' + origin.line
: 'Parcela por corresponder'}{' '}
— {origin?.amount ?? 'Indeterminado'} MZN
</legend>
<p>
O acréscimo36g já pertence aL6;esta linha só
apura o eventual imposto autónomo.
</p>
<label className="block">
Natureza legal da despesa
<select
aria-label={
'Natureza da despesa ' + (index + 1)
}
value={r.nature}
onChange={(e) =>
patch({
nature: e.target
.value as typeof r.nature,
})
}
>
<option value="">
Informação em falta
</option>
<option value="undocumented">
Encargo não devidamente documentado
</option>
<option value="confidential">
Despesa confidencial
</option>
<option value="illicit">
Despesa ilícita
</option>
<option value="other">
Outra natureza — correspondência própria
</option>
</select>
</label>
{yesno(
'Incidência61(4) — parcela ' + (index + 1),
r.eligible,
(eligible) => patch({ eligible }),
)}
<label className="block">
Suporte da classificação e incidência
<input
aria-label={
'Suporte da despesa ' + (index + 1)
}
value={r.reference}
onChange={(e) =>
patch({ reference: e.target.value })
}
/>
</label>
<button
type="button"
onClick={() =>
change({
expenses: {
...i.expenses,
rows: i.expenses.rows.filter(
(x) => x.id !== r.id,
),
},
})
}
>
Retirar qualificação da parcela {index + 1}
</button>
</fieldset>
);
})}
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>Cooperativas abrangidas — artigo12</legend>
<p>
{specialSources.cooperative} Este incremento exige base
integralmente elegível;parcelas mistas continuam explícitas.
</p>
<QualificationFields
label="Cooperativa"
value={i.cooperative.qualification}
onChange={(qualification) =>
change({
cooperative: { ...i.cooperative, qualification },
})
}
/>
{(i.cooperative.qualification.state === 'known' ||
i.cooperative.kind !== '' ||
i.cooperative.allIncomeEligible !== '' ||
i.cooperative.withheldIncome !== '') && (
<>
<label className="block">
Tipo de cooperativa documentado
<select
value={i.cooperative.kind}
onChange={(e) =>
change({
cooperative: {
...i.cooperative,
kind: e.target
.value as typeof i.cooperative.kind,
},
})
}
>
<option value="">Por identificar</option>
<option value="agrarian">Agrária</option>
<option value="craft">Artesanato</option>
<option value="cultural">Cultural</option>
<option value="other">
Outra — não correspondida
</option>
</select>
</label>
{yesno(
'Base ordinária integralmente elegível',
i.cooperative.allIncomeEligible,
(allIncomeEligible) =>
change({
cooperative: {
...i.cooperative,
allIncomeEligible,
},
}),
)}
<label className="block">
Rendimentos sujeitos a retenção
<select
value={i.cooperative.withheldIncome}
onChange={(e) =>
change({
cooperative: {
...i.cooperative,
withheldIncome: e.target
.value as typeof i.cooperative.withheldIncome,
},
})
}
>
<option value="">Informação em falta</option>
<option value="absent">
Ausência documentada
</option>
<option value="known">
Existem — segregação própria em falta
</option>
</select>
</label>
</>
)}
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>Amortizações aceleradas — CBF16</legend>
<p>
{i.combinations
? specialAccelerationSource(i)
: i.acceleration.monthly?.continuation
? specialCessationPriorSource
: i.acceleration.monthly?.startPeriod
? specialStartPeriodSource
: specialSources.acceleration}{' '}
Apenas activos já suportados e método constante;prepare
explicitamente o recorte anual ou de início de actividade.
</p>
<QualificationFields
label="Amortização acelerada"
value={i.acceleration.qualification}
onChange={(qualification) =>
change({
acceleration: { ...i.acceleration, qualification },
})
}
/>
{i.acceleration.qualification.state === 'known' && (
<>
{yesno(
'EnquadramentoCBF3 e pressupostos9 documentados',
i.acceleration.cbfRequirements,
(cbfRequirements) =>
change({
acceleration: {
...i.acceleration,
cbfRequirements,
},
}),
)}
<label className="block">
ReconhecimentoCBF10
<select
value={i.acceleration.recognition}
onChange={(e) =>
change({
acceleration: {
...i.acceleration,
recognition: e.target
.value as typeof i.acceleration.recognition,
},
})
}
>
<option value="">Informação em falta</option>
<option value="documented">
Instrumento/reconhecimento documentado
</option>
<option value="not_required">
Excepção prevista — identificar norma e
suporte
</option>
</select>
</label>
<label className="block">
Instrumento ou norma da excepção
<input
value={i.acceleration.recognitionReference}
onChange={(e) =>
change({
acceleration: {
...i.acceleration,
recognitionReference:
e.target.value,
},
})
}
/>
</label>
{!i.combinations &&
yesno(
'Ausência documentada de outros benefícios a cumular',
i.acceleration.noOtherBenefits,
(noOtherBenefits) =>
change({
acceleration: {
...i.acceleration,
noOtherBenefits,
},
}),
)}
</>
)}
{!i.acceleration.monthly ? (
<button
type="button"
onClick={() =>
change({
acceleration: {
...i.acceleration,
monthly: { version: 1, reference: '' },
},
})
}
>
Preparar CBF16 com duodécimos
</button>
) : (
<fieldset
aria-label="CBF16 por duodécimos"
className="space-y-2 rounded border p-2"
>
<legend>CBF16 — reutilizar duodécimos L6</legend>
<p>
{i.combinations
? specialAccelerationSource(i)
: i.acceleration.monthly.continuation
? specialCessationPriorSource
: i.acceleration.monthly.startPeriod
? specialStartPeriodSource
: specialMonthlySource}{' '}
Conserva datas,opção mensal e valores do activo
existente;preencha no mapaL6 apenas o que falta ou
precisa de correcção.
</p>
<label className="block">
Suporte da reutilização dos duodécimos L6
<input
aria-label="Suporte dos duodécimos CBF16"
value={i.acceleration.monthly.reference}
onChange={(e) =>
change({
acceleration: {
...i.acceleration,
monthly: {
...i.acceleration.monthly!,
reference: e.target.value,
},
},
})
}
/>
</label>
{!i.acceleration.monthly.continuation &&
(!i.acceleration.monthly.startPeriod ? (
<button
type="button"
onClick={() =>
change({
acceleration: {
...i.acceleration,
monthly: {
...i.acceleration.monthly!,
startPeriod: {
version: 1,
reference: '',
},
},
},
})
}
>
Preparar CBF16 para início de actividade
</button>
) : (
<fieldset
aria-label="CBF16 no início de actividade"
className="rounded border p-2"
>
<legend>
CBF16 — período curto de início de
actividade
</legend>
<p>
Período L6:
{b.periodsFx?.from ||
'Por identificar'}{' '}
a {b.periodsFx?.to || 'Por identificar'}
;tipo:
{b.periodsFx?.kind || 'Por identificar'}
. Reutiliza o período declarado;esta
opção não altera datas,activos ou
enquadramento.
</p>
<label className="block">
Suporte do início de actividade
<input
aria-label="Suporte do início de actividade CBF16"
value={
i.acceleration.monthly
.startPeriod.reference
}
onChange={(e) =>
change({
acceleration: {
...i.acceleration,
monthly: {
...i.acceleration
.monthly!,
startPeriod: {
version: 1,
reference:
e.target
.value,
},
},
},
})
}
/>
</label>
<button
type="button"
disabled={
b.periodsFx?.kind !== 'start' ||
!b.periodsFx.reference.trim()
}
onClick={() =>
change({
acceleration: {
...i.acceleration,
monthly: {
...i.acceleration
.monthly!,
startPeriod: {
version: 1,
reference:
b.periodsFx!
.reference,
},
},
},
})
}
>
Reutilizar suporte do início de
actividade L6
</button>
<button
type="button"
onClick={() => {
const monthly = {
...i.acceleration.monthly!,
};
delete monthly.startPeriod;
change({
acceleration: {
...i.acceleration,
monthly,
},
});
}}
>
Manter duodécimos no exercício anual
</button>
</fieldset>
))}
{!i.acceleration.monthly.startPeriod &&
(!i.acceleration.monthly.continuation ? (
<button
type="button"
onClick={() =>
change({
acceleration: {
...i.acceleration,
monthly: {
...i.acceleration.monthly!,
continuation: {
version: 1,
period: '',
reference: '',
historyReference: '',
monthlyFromStart: '',
},
},
},
})
}
>
Preparar CBF16 para cessação e quotas
anteriores
</button>
) : (
<fieldset
aria-label="CBF16 cessação e quotas anteriores"
className="rounded border p-2"
>
<legend>
CBF16 — cessação e quotas anteriores
</legend>
<p>
{specialCessationPriorSource} Reutiliza
o mapaL6;nenhum saldo declarado ou
histórico é alterado. PeríodoL6:
{b.periodsFx?.from ||
b.year + '-01-01'}{' '}
a {b.periodsFx?.to || b.year + '-12-31'}
;tipo:{b.periodsFx?.kind || 'anual'}.
Corrija dados de origem nos mapasL6
quando necessário.
</p>
<label className="block">
Período a qualificar
<select
aria-label="Período CBF16 cessação e quotas anteriores"
value={
i.acceleration.monthly
.continuation.period
}
onChange={(e) =>
change({
acceleration: {
...i.acceleration,
monthly: {
...i.acceleration
.monthly!,
continuation: {
...i
.acceleration
.monthly!
.continuation!,
period: e.target
.value as
| 'annual'
| 'cessation'
| '',
},
},
},
})
}
>
<option value="">
Informação em falta
</option>
<option value="annual">
Ano civil2026 — activo e quotas
anteriores
</option>
<option value="cessation">
Cessação da actividade em2026 —
período curtoL6
</option>
</select>
</label>
<label className="block">
Suporte do período e condiçõesCBF16
<input
aria-label="Suporte CBF16 cessação"
value={
i.acceleration.monthly
.continuation.reference
}
onChange={(e) =>
change({
acceleration: {
...i.acceleration,
monthly: {
...i.acceleration
.monthly!,
continuation: {
...i
.acceleration
.monthly!
.continuation!,
reference:
e.target
.value,
},
},
},
})
}
/>
</label>
<label className="block">
Suporte das quotas anteriores ou
ausência e da aplicaçãoCBF16
<input
aria-label="Suporte das quotas anteriores CBF16"
value={
i.acceleration.monthly
.continuation
.historyReference
}
onChange={(e) =>
change({
acceleration: {
...i.acceleration,
monthly: {
...i.acceleration
.monthly!,
continuation: {
...i
.acceleration
.monthly!
.continuation!,
historyReference:
e.target
.value,
},
},
},
})
}
/>
</label>
{yesno(
'Duodécimos desde a entrada emuso documentados',
i.acceleration.monthly.continuation
.monthlyFromStart,
(monthlyFromStart) =>
change({
acceleration: {
...i.acceleration,
monthly: {
...i.acceleration
.monthly!,
continuation: {
...i.acceleration
.monthly!
.continuation!,
monthlyFromStart,
},
},
},
}),
)}
<p>
Cessação da actividade e saída do activo
são eventos distintos. Só a saída
documentada emL6 exclui o mês do evento.
A recuperação de quotas anteriores é
aritmética para esta simulação;saldo
oficial não é consumido.
</p>
<button
type="button"
onClick={() => {
const monthly = {
...i.acceleration.monthly!,
};
delete monthly.continuation;
change({
acceleration: {
...i.acceleration,
monthly,
},
});
}}
>
Manter recorte anual sem quotas
anteriores
</button>
</fieldset>
))}
<p>
Não altera automaticamente o activo nem transforma
ausência de documentos em elegibilidade. A versão
anterior permanece no histórico depois de calcular e
guardar.
</p>
<button
type="button"
onClick={() => {
const acceleration = { ...i.acceleration };
delete acceleration.monthly;
change({ acceleration });
}}
>
Manter preparação anual CBF16
</button>
</fieldset>
)}
<button
type="button"
onClick={() =>
change({
acceleration: {
...i.acceleration,
rows: [
...i.acceleration.rows,
newAcceleratedAsset(crypto.randomUUID()),
],
},
})
}
>
Qualificar activo existente paraCBF16
</button>
{i.acceleration.rows.map((r, index) => {
const patch = (p: Partial<typeof r>) =>
change({
acceleration: {
...i.acceleration,
rows: i.acceleration.rows.map((x) =>
x.id === r.id ? { ...x, ...p } : x,
),
},
});
return (
<fieldset key={r.id} className="rounded border p-2">
<legend>Activo qualificado {index + 1}</legend>
<label className="block">
Activo do mapaL6
<select
value={r.assetId}
onChange={(e) =>
patch({ assetId: e.target.value })
}
>
<option value="">
Seleccione activo existente
</option>
{b.depreciation?.assets.map((a) => (
<option key={a.id} value={a.id}>
{a.description} — {a.assetKey}
</option>
))}
</select>
</label>
<label className="block">
Afectação documentada
<select
value={r.kind}
onChange={(e) =>
patch({
kind: e.target
.value as typeof r.kind,
})
}
>
<option value="">Por identificar</option>
<option value="new_project_building">
Imóvel novo utilizado no projecto
</option>
<option value="rehabilitated_project_building">
Imóvel reabilitado — regra ainda por
corresponder
</option>
<option value="industrial_equipment">
Máquina/equipamento industrial
</option>
<option value="agroindustrial_equipment">
Máquina/equipamento agro-industrial
</option>
<option value="other">
Outro — correspondência própria
</option>
</select>
</label>
{yesno(
'Condições do activoCBF16 — ' + (index + 1),
r.eligible,
(eligible) => patch({ eligible }),
)}
<label className="block">
Prova da afectação e condições do activo
<input
value={r.reference}
onChange={(e) =>
patch({ reference: e.target.value })
}
/>
</label>
<button
type="button"
onClick={() =>
change({
acceleration: {
...i.acceleration,
rows: i.acceleration.rows.filter(
(x) => x.id !== r.id,
),
},
})
}
>
Retirar tratamento do activo {index + 1}
</button>
</fieldset>
);
})}
</fieldset>
<p>
Use «Calcular e guardar» abaixo para gravar uma nova versão e
apresentar os efeitos. Os históricos anteriores são conservados.
M20/M22 mantém apenas correspondências demonstradas;camposL7 não
são preenchidos por aproximação.
</p>
</section>
);
}