434 lines
19 KiB
TypeScript
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>
|
|
);
|
|
}
|