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

434 lines
19 KiB
TypeScript

import { useState } from 'react';
import {
ispcActivities,
ispcEligibilityBook,
saveIspcEligibilityBook,
recordIspcEligibility,
ispcEligibilityCurrent,
ispcReuseDiff,
ispcEligibilityReportRows,
type IspcEligibilityInput,
type IspcEligibilityBook,
} from '@/lib/ispc-eligibility';
const factLabels: Partial<Record<keyof IspcEligibilityInput, string>> = {
territory: 'Exerce a actividade em território nacional',
organizedRequired:
'Está obrigado a possuir contabilidade organizada para os impostos sobre o rendimento',
sharesInIspc: 'Tem participações noutras sociedades comerciais em ISPC',
sharesAnonymous:
'Tem participações em sociedades anónimas ou sociedades cujos sócios não são identificáveis',
otherActivities: 'Exerce outras actividades fora dos âmbitos identificados',
unlistedServices: 'Presta serviços fora dos previstos na Lei',
forecastConfirmed:
'Previsão da declaração de início confirmada pela AT (ou pressuposto expresso na simulação)',
generalRegimeChosen:
'Optou pela aplicação de IVA/IRPS/IRPC com permanência mínima de três anos',
generalThreeYearsCompleted:
'Já cumpriu o período mínimo de três anos dessa opção',
};
export function SimulationIspcEligibility({
draft,
onChange,
}: {
draft: Record<string, unknown>;
onChange: (field: string, value: string) => void;
}) {
const [error, setError] = useState(''),
[reusePreview, setReusePreview] = useState<ReturnType<
typeof ispcReuseDiff
> | null>(null);
if (draft.fiscal_component !== 'ispc') return null;
let book: IspcEligibilityBook;
try {
book = ispcEligibilityBook(draft);
} catch (e) {
return <p role="alert">{String(e)}</p>;
}
const i = book.input,
save = (next: IspcEligibilityBook) => {
try {
onChange(
'modelo_a_snapshot',
saveIspcEligibilityBook(draft, next),
);
setError('');
} catch (e) {
setError(String(e));
}
},
change = (patch: Partial<IspcEligibilityInput>) =>
save({ ...book, input: { ...i, ...patch } }),
diff = ispcReuseDiff(draft, book),
service = i.activities.some((a) =>
['services12', 'liberal15'].includes(a),
),
liberal = i.activities.includes('liberal15'),
earlyOutside =
i.activities.includes('other') ||
i.territory === 'no' ||
i.mode === 'occasional';
const fact = (field: keyof typeof factLabels) => (
<label className="block" key={field}>
{factLabels[field]}
<select
className="block rounded border p-2"
value={String(i[field])}
onChange={(e) => change({ [field]: e.target.value })}
>
<option value="">Por esclarecer</option>
<option value="yes">Sim</option>
<option value="no">Não</option>
</select>
</label>
);
const text = (
field: keyof IspcEligibilityInput,
label: string,
type = 'text',
) => (
<label className="block" key={field}>
{label}
<input
className="block rounded border p-2"
type={type}
value={String(i[field])}
onChange={(e) => change({ [field]: e.target.value })}
/>
</label>
);
return (
<section
aria-label="ISPC — enquadramento e opção"
className="space-y-3 rounded border p-4"
>
<h4 className="font-semibold">ISPC — enquadramento e opção</h4>
<p>
Avaliação preliminar para 2026, em MZN, segundo a Lei 9/2025.
Distingue condições de acesso, opção e cálculo do imposto.
Guarde esta avaliação e prossiga no apuramento trimestral
abaixo.
</p>
{error && <p role="alert">{error}</p>}
{diff.length > 0 && (
<button
type="button"
className="underline"
onClick={() => setReusePreview(diff)}
>
Conferir dados do perfil e da actividade para reutilizar
</button>
)}
{reusePreview && (
<aside className="rounded border p-3">
<p>
A adopção altera as entradas deste cenário. O resultado
guardado passa a histórico até novo cálculo; os outros
livros e a opção registada são preservados.
</p>
{reusePreview.map((v) => (
<p key={v.field}>
{(
{
personType: 'Tipo de pessoa',
establishments: 'Estabelecimentos',
activityDescription: 'Actividade',
entityRef: 'Entidade',
} as Record<string, string>
)[v.field] ?? v.field}
: {v.before || 'Por preencher'} → {v.after}
</p>
))}
<button
type="button"
onClick={() => {
change(
Object.fromEntries(
reusePreview.map((v) => [v.field, v.after]),
),
);
setReusePreview(null);
}}
>
Confirmar adopção destes dados
</button>
<button type="button" onClick={() => setReusePreview(null)}>
Manter as minhas entradas
</button>
</aside>
)}
<div className="grid gap-3 md:grid-cols-2">
{text(
'entityRef',
'Entidade de referência (reutilizável; sem NUIT obrigatório para preparar)',
)}
<label>
Tipo de pessoa
<select
className="block rounded border p-2"
value={i.personType}
onChange={(e) =>
change({
personType: e.target
.value as IspcEligibilityInput['personType'],
})
}
>
<option value="">Por identificar</option>
<option value="singular">Pessoa singular</option>
<option value="collective">Pessoa colectiva</option>
</select>
</label>
{text('assessmentDate', 'Data de avaliação', 'date')}
<label>
Natureza dos dados
<select
className="block rounded border p-2"
value={i.origin}
onChange={(e) =>
change({
origin: e.target
.value as IspcEligibilityInput['origin'],
})
}
>
<option value="projected">
Pressupostos da simulação
</option>
<option value="effective">
Dados efectivos declarados
</option>
</select>
</label>
<label>
Moeda dos valores ISPC
<select
className="block rounded border p-2"
value={i.currency}
onChange={(e) => change({ currency: e.target.value })}
>
<option value="">Por identificar</option>
<option value="MZN">MZN</option>
{i.currency && i.currency !== 'MZN' && (
<option value={i.currency}>
{i.currency} — fora da cobertura deste cálculo
</option>
)}
</select>
</label>
<label>
Âmbito da preparação
<select
className="block rounded border p-2"
value={i.mode}
onChange={(e) =>
change({
mode: e.target
.value as IspcEligibilityInput['mode'],
})
}
>
<option value="regular">Actividade habitual</option>
<option value="occasional">
Operação ocasional — percurso próprio
</option>
</select>
</label>
{fact('territory')}
</div>
<fieldset className="space-y-2 rounded border p-3">
<legend>Actividades do contribuinte</legend>
<p>
Identifique todas as actividades, incluindo as exercidas
fora desta oportunidade. Não inferimos o regime pelo sector
ou pela nacionalidade.
</p>
{ispcActivities.map(([kind, label]) => (
<label className="block" key={kind}>
<input
type="checkbox"
checked={i.activities.includes(kind)}
onChange={(e) =>
change({
activities: e.target.checked
? [...i.activities, kind]
: i.activities.filter(
(a) => a !== kind,
),
})
}
/>{' '}
{label}
</label>
))}
{text(
'activityDescription',
'Descrição da actividade e natureza dos serviços, quando existam',
)}
</fieldset>
{!earlyOutside && (
<>
<fieldset className="grid gap-3 rounded border p-3 md:grid-cols-2">
<legend>Volume anual de referência</legend>
<label>
Referência de acesso
<select
className="block rounded border p-2"
value={i.basis}
onChange={(e) =>
change({
basis: e.target
.value as IspcEligibilityInput['basis'],
...(!i.referenceYear
? {
referenceYear:
e.target.value ===
'previous_year'
? '2025'
: e.target.value ===
'start_forecast'
? '2026'
: '',
}
: {}),
})
}
>
<option value="">Por identificar</option>
<option value="previous_year">
Volume de negócios do ano anterior
</option>
<option value="start_forecast">
Previsão da declaração de início de
actividade
</option>
</select>
</label>
{text('referenceYear', 'Ano do valor de referência')}
{text(
'annualReference',
'Volume anual de referência em MZN',
)}
{text(
'annualReferenceRef',
'Origem do valor ou pressuposto (opcional)',
)}
<p className="md:col-span-2">
Não copiar receita trimestral ou do ano em curso
como receita do ano anterior. Alterar a origem não
reinterpreta o valor já editado; reveja o montante e
o ano.
</p>
{i.basis === 'start_forecast' && (
<>
{fact('forecastConfirmed')}
{text(
'forecastConfirmationRef',
'Referência da confirmação da previsão, se efectiva',
)}
</>
)}
</fieldset>
<fieldset className="space-y-3 rounded border p-3">
<legend>Condições aplicáveis ao contribuinte</legend>
{text(
'establishments',
'Número de estabelecimentos da actividade, incluindo fora deste dossier',
)}
{fact('organizedRequired')}
{fact('sharesInIspc')}
{fact('sharesAnonymous')}
{fact('otherActivities')}
{service && fact('unlistedServices')}
{liberal && (
<>
{text(
'liberalDaysSameEntity',
'Maior duração anual dos serviços à mesma entidade, em dias',
)}
{text(
'liberalClientRef',
'Referência do cliente/contrato ou pressuposto da duração',
)}
<p>
O limite de 183 dias refere-se aos serviços
do artigo 8(1)c. A permanência liberal tem
limite próprio de cinco anos; a contagem
transitória começa em 2026.
</p>
</>
)}
{fact('generalRegimeChosen')}
{i.generalRegimeChosen === 'yes' && (
<>
{fact('generalThreeYearsCompleted')}
{text(
'generalRegimeRef',
'Referência da opção e do cumprimento dos três anos',
)}
</>
)}
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>Opção — separada da compatibilidade</legend>
<label>
Situação da opção
<select
className="block rounded border p-2"
value={i.option}
onChange={(e) =>
change({
option: e.target
.value as IspcEligibilityInput['option'],
})
}
>
<option value="preparation">
Em preparação — sem adesão automática
</option>
<option value="opted">
Opção registada no documento/cenário
</option>
<option value="not_opted">
Não optou pelo ISPC
</option>
</select>
</label>
{i.option === 'opted' && (
<>
{text('optionReference', 'Referência da opção')}
{text('optionDate', 'Data da opção', 'date')}
</>
)}
</fieldset>
</>
)}
<button
type="button"
className="rounded bg-emerald-800 p-2 text-white"
onClick={() => {
try {
save(recordIspcEligibility(draft, book));
} catch (e) {
setError(String(e));
}
}}
>
Avaliar e guardar enquadramento ISPC
</button>
{book.result && (
<p role="status">
{ispcEligibilityCurrent(draft, book)
? 'Resultado correspondente às entradas guardadas.'
: 'Entradas alteradas; resultado histórico preservado. Reveja e guarde nova avaliação.'}
</p>
)}
{ispcEligibilityReportRows(draft).map((r, index) => (
<p key={index}>
<strong>{r[1]}:</strong> {r[2]}
<small className="block">{r[3]}</small>
</p>
))}
</section>
);
}