599 lines
27 KiB
TypeScript
599 lines
27 KiB
TypeScript
import { inputText } from '../lib/input-text.ts';
|
|
import { useState } from 'react';
|
|
import {
|
|
closureBook,
|
|
closureSources,
|
|
newClosure,
|
|
newClosureCredit,
|
|
saveClosureBook,
|
|
recordClosure,
|
|
adoptPreviousCredit,
|
|
closureReportRows,
|
|
type ClosureInput,
|
|
type ClosureCredit,
|
|
type ClosureBook,
|
|
} from '@/lib/iva-closure';
|
|
export function AcquisitionClosure({
|
|
draft,
|
|
onChange,
|
|
}: {
|
|
draft: Record<string, unknown>;
|
|
onChange: (field: string, value: string) => void;
|
|
}) {
|
|
const [error, setError] = useState('');
|
|
let b: ClosureBook;
|
|
try {
|
|
b = closureBook(draft);
|
|
} catch (e) {
|
|
return <p role="alert">{String(e)}</p>;
|
|
}
|
|
const save = (next: ClosureBook) => {
|
|
try {
|
|
onChange('modelo_a_snapshot', saveClosureBook(draft, next));
|
|
setError('');
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
};
|
|
const change = (id: string, p: Partial<ClosureInput>) =>
|
|
save({
|
|
...b,
|
|
rows: b.rows.map((r) => (r.id === id ? { ...r, ...p } : r)),
|
|
});
|
|
const sources = closureSources(draft);
|
|
const entities = [
|
|
...new Set(b.rows.map((r) => r.entityRef.trim()).filter(Boolean)),
|
|
];
|
|
const creditChange = (
|
|
r: ClosureInput,
|
|
id: string,
|
|
p: Partial<ClosureCredit>,
|
|
) =>
|
|
change(r.id, {
|
|
credits: r.credits.map((c) => (c.id === id ? { ...c, ...p } : c)),
|
|
});
|
|
const choice = (
|
|
r: ClosureInput,
|
|
key: 'firstDeclaration' | 'creditsComplete',
|
|
label: string,
|
|
) => (
|
|
<label className="block">
|
|
{label}
|
|
<select
|
|
value={r[key]}
|
|
onChange={(e) => change(r.id, { [key]: e.target.value })}
|
|
>
|
|
<option value="">Por confirmar</option>
|
|
<option value="yes">Sim</option>
|
|
<option value="no">Não</option>
|
|
</select>
|
|
</label>
|
|
);
|
|
return (
|
|
<details className="rounded border p-3">
|
|
<summary>L3-A/B — crédito,fecho e pagamentos por período</summary>
|
|
<p>
|
|
Reutiliza resultados congelados de L2. Crédito anterior,saldo
|
|
calculado,pagamento registado e transporte são distintos.
|
|
Identifique o âmbito fiscal da entidade,sem consolidar
|
|
oportunidades implicitamente.
|
|
</p>
|
|
<p>
|
|
Primeira declaração em prazo,2026/MZN e reporte integral ou
|
|
afectação de parcela ao pedidoD. SubstituiçõesC,acompanhamento
|
|
de reembolsoD e restituição de bebidasE continuam próprios. A
|
|
parcela preparada para pedido é separada do reporte,sem
|
|
concessão automática. Prazo/data são declarados,não calculados
|
|
pelo portal. Uma simulação não submete declarações nem efectua
|
|
pagamentos.
|
|
</p>
|
|
{error && <p role="alert">{error}</p>}
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
save({
|
|
...b,
|
|
rows: [
|
|
...b.rows,
|
|
newClosure(
|
|
crypto.randomUUID(),
|
|
sources.length === 1 ? sources[0] : null,
|
|
entities.length === 1
|
|
? entities[0]
|
|
: inputText(
|
|
draft.fiscal_entity_ref ??
|
|
draft.organization_name ??
|
|
'',
|
|
),
|
|
),
|
|
],
|
|
})
|
|
}
|
|
>
|
|
Preparar fecho de outro período
|
|
</button>
|
|
{b.rows.map((r) => (
|
|
<fieldset className="rounded border p-3" key={r.id}>
|
|
<legend>
|
|
Fecho — {r.period || 'período por escolher'}
|
|
</legend>
|
|
<label>
|
|
Resultado L2 congelado
|
|
<select
|
|
value={sources.findIndex(
|
|
(s) =>
|
|
JSON.stringify(s) ===
|
|
JSON.stringify(r.source),
|
|
)}
|
|
onChange={(e) => {
|
|
const s = sources[Number(e.target.value)];
|
|
if (s)
|
|
change(r.id, {
|
|
source: structuredClone(s),
|
|
period: s.period,
|
|
});
|
|
}}
|
|
>
|
|
<option value={-1}>Escolha versão e período</option>
|
|
{sources.map((s, i) => (
|
|
<option key={i} value={i}>
|
|
{s.period} — revisão{s.revision} —{' '}
|
|
{s.calculatedAt}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Âmbito fiscal da entidade
|
|
<input
|
|
value={r.entityRef}
|
|
onChange={(e) =>
|
|
change(r.id, { entityRef: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Natureza do fecho
|
|
<select
|
|
value={r.origin}
|
|
onChange={(e) =>
|
|
change(r.id, {
|
|
origin: e.target
|
|
.value as ClosureInput['origin'],
|
|
})
|
|
}
|
|
>
|
|
<option value="projected">
|
|
Pressuposto de simulação
|
|
</option>
|
|
<option value="effective">
|
|
Dados efectivos declarados
|
|
</option>
|
|
</select>
|
|
</label>
|
|
{choice(
|
|
r,
|
|
'firstDeclaration',
|
|
'É a primeira declaração deste período (ou pressuposto explícito)',
|
|
)}
|
|
<label>
|
|
Data de entrega ou prevista
|
|
<input
|
|
type="date"
|
|
value={r.submissionDate}
|
|
onChange={(e) =>
|
|
change(r.id, { submissionDate: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Situação do prazo
|
|
<select
|
|
value={r.filing}
|
|
onChange={(e) =>
|
|
change(r.id, {
|
|
filing: e.target
|
|
.value as ClosureInput['filing'],
|
|
})
|
|
}
|
|
>
|
|
<option value="">Por identificar</option>
|
|
<option value="timely">
|
|
Dentro do prazo declarado
|
|
</option>
|
|
<option value="late">
|
|
Fora do prazo — procedimento próprio
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Fundamento/referência do prazo declarado
|
|
<input
|
|
value={r.filingRef}
|
|
onChange={(e) =>
|
|
change(r.id, { filingRef: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
<h5>Créditos de origem identificada</h5>
|
|
{r.credits.map((c) => (
|
|
<fieldset className="rounded border p-2" key={c.id}>
|
|
<legend>
|
|
{c.kind === 'opening'
|
|
? '19 — saldo de abertura'
|
|
: c.kind === 'previous'
|
|
? '19 — transporte anterior'
|
|
: '20 — crédito comunicado'}
|
|
</legend>
|
|
{c.kind === 'previous' ? (
|
|
<label>
|
|
Adoptar expressamente o reporte anterior
|
|
<select
|
|
value={c.previousId}
|
|
onChange={(e) => {
|
|
try {
|
|
const p = b.result?.rows.find(
|
|
(p) =>
|
|
p.id === e.target.value,
|
|
);
|
|
if (p)
|
|
creditChange(
|
|
r,
|
|
c.id,
|
|
adoptPreviousCredit(
|
|
c,
|
|
p,
|
|
),
|
|
);
|
|
} catch (err) {
|
|
setError(String(err));
|
|
}
|
|
}}
|
|
>
|
|
<option value="">
|
|
Escolha o fecho anterior
|
|
</option>
|
|
{b.result?.rows
|
|
.filter(
|
|
(p) =>
|
|
p.id !== r.id &&
|
|
p.period < r.period,
|
|
)
|
|
.map((p) => (
|
|
<option key={p.id} value={p.id}>
|
|
{p.period} — {p.entityRef} —{' '}
|
|
{p.status === 'determined'
|
|
? 'calculado'
|
|
: 'indeterminado'}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
) : (
|
|
<>
|
|
<label>
|
|
Período de origem do crédito/saldo
|
|
<input
|
|
type="month"
|
|
value={c.period}
|
|
onChange={(e) =>
|
|
creditChange(r, c.id, {
|
|
period: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Documento/referência do crédito
|
|
<input
|
|
value={c.reference}
|
|
onChange={(e) =>
|
|
creditChange(r, c.id, {
|
|
reference: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Montante disponível documentado (MZN)
|
|
<input
|
|
value={c.amount}
|
|
onChange={(e) =>
|
|
creditChange(r, c.id, {
|
|
amount: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
{c.kind === 'communication' && (
|
|
<label>
|
|
Data da comunicação recebida
|
|
<input
|
|
type="date"
|
|
value={c.date}
|
|
onChange={(e) =>
|
|
creditChange(r, c.id, {
|
|
date: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
</>
|
|
)}
|
|
{c.kind === 'previous' && (
|
|
<>
|
|
<p>
|
|
Origem:{c.reference};{c.period}
|
|
;apresentação{c.amount}. Cálculo pela
|
|
fracção exacta;origem alterada exige
|
|
adopção expressa.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
try {
|
|
const p = b.result?.rows.find(
|
|
(p) =>
|
|
p.id === c.previousId,
|
|
);
|
|
if (!p)
|
|
throw Error(
|
|
'Fecho anterior não recuperável.',
|
|
);
|
|
creditChange(
|
|
r,
|
|
c.id,
|
|
adoptPreviousCredit(c, p),
|
|
);
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Adoptar versão actual deste reporte
|
|
</button>
|
|
</>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
change(r.id, {
|
|
credits: r.credits.filter(
|
|
(v) => v.id !== c.id,
|
|
),
|
|
})
|
|
}
|
|
>
|
|
Remover vínculo de crédito desta preparação
|
|
</button>
|
|
</fieldset>
|
|
))}
|
|
{(['opening', 'communication', 'previous'] as const).map(
|
|
(kind) => (
|
|
<button
|
|
type="button"
|
|
key={kind}
|
|
onClick={() =>
|
|
change(r.id, {
|
|
credits: [
|
|
...r.credits,
|
|
newClosureCredit(
|
|
crypto.randomUUID(),
|
|
kind,
|
|
),
|
|
],
|
|
})
|
|
}
|
|
>
|
|
{kind === 'opening'
|
|
? 'Identificar saldo inicial'
|
|
: kind === 'communication'
|
|
? 'Identificar crédito comunicado'
|
|
: 'Reutilizar reporte anterior'}
|
|
</button>
|
|
),
|
|
)}
|
|
{choice(
|
|
r,
|
|
'creditsComplete',
|
|
'Reconciliei todos os créditos;lista vazia confirma expressamente ausência',
|
|
)}
|
|
<label>
|
|
Juros23
|
|
<select
|
|
value={r.interest}
|
|
onChange={(e) =>
|
|
change(r.id, {
|
|
interest: e.target
|
|
.value as ClosureInput['interest'],
|
|
})
|
|
}
|
|
>
|
|
<option value="">Indeterminados</option>
|
|
<option value="none">
|
|
Sem juros aplicáveis — motivo identificado
|
|
</option>
|
|
<option value="documented">
|
|
Montante identificado,sem cálculo de taxa pelo
|
|
portal
|
|
</option>
|
|
</select>
|
|
</label>
|
|
{r.interest === 'documented' && (
|
|
<label>
|
|
Montante de juros (MZN)
|
|
<input
|
|
value={r.interestAmount}
|
|
onChange={(e) =>
|
|
change(r.id, {
|
|
interestAmount: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
{r.interest && (
|
|
<label>
|
|
Fonte/motivo dos juros ou da sua inexistência
|
|
<input
|
|
value={r.interestRef}
|
|
onChange={(e) =>
|
|
change(r.id, {
|
|
interestRef: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
<label>
|
|
Destino do crédito25
|
|
<select
|
|
value={r.destination}
|
|
onChange={(e) =>
|
|
change(r.id, {
|
|
destination: e.target
|
|
.value as ClosureInput['destination'],
|
|
})
|
|
}
|
|
>
|
|
<option value="">Por escolher</option>
|
|
<option value="report">
|
|
Reportar integralmente26;sem pedido27 neste
|
|
percurso
|
|
</option>
|
|
<option value="refund">
|
|
Destinar parcela ao pedido27 — acompanhar emD
|
|
</option>
|
|
</select>
|
|
</label>
|
|
{r.destination === 'refund' && (
|
|
<label>
|
|
Parcela do cr?dito destinada ao pedido27 (MZN)
|
|
<input
|
|
value={r.refundAmount ?? ''}
|
|
onChange={(e) =>
|
|
change(r.id, {
|
|
refundAmount: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
<h5>Pagamentos registados,distintos do saldo calculado</h5>
|
|
{r.destination === 'refund' && (
|
|
<label>
|
|
Parcela do crédito destinada ao pedido27 (MZN)
|
|
<input
|
|
value={r.refundAmount ?? ''}
|
|
onChange={(e) =>
|
|
change(r.id, {
|
|
refundAmount: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
{r.payments.map((p) => (
|
|
<fieldset className="rounded border p-2" key={p.id}>
|
|
{(['reference', 'date', 'amount'] as const).map(
|
|
(k, i) => (
|
|
<label key={k}>
|
|
{
|
|
[
|
|
'Referência do pagamento',
|
|
'Data do pagamento',
|
|
'Montante do pagamento (MZN)',
|
|
][i]
|
|
}
|
|
<input
|
|
type={
|
|
k === 'date' ? 'date' : 'text'
|
|
}
|
|
value={p[k]}
|
|
onChange={(e) =>
|
|
change(r.id, {
|
|
payments: r.payments.map(
|
|
(v) =>
|
|
v.id === p.id
|
|
? {
|
|
...v,
|
|
[k]: e
|
|
.target
|
|
.value,
|
|
}
|
|
: v,
|
|
),
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
),
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
change(r.id, {
|
|
payments: r.payments.filter(
|
|
(v) => v.id !== p.id,
|
|
),
|
|
})
|
|
}
|
|
>
|
|
Remover pagamento desta preparação
|
|
</button>
|
|
</fieldset>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
change(r.id, {
|
|
payments: [
|
|
...r.payments,
|
|
{
|
|
id: crypto.randomUUID(),
|
|
reference: '',
|
|
date: '',
|
|
amount: '',
|
|
},
|
|
],
|
|
})
|
|
}
|
|
>
|
|
Registar referência de pagamento
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
save({
|
|
...b,
|
|
rows: b.rows.filter((v) => v.id !== r.id),
|
|
})
|
|
}
|
|
>
|
|
Remover fecho em preparação (histórico conservado)
|
|
</button>
|
|
</fieldset>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
try {
|
|
save(recordClosure(draft, b));
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Calcular e guardar fecho A/B
|
|
</button>
|
|
{closureReportRows(draft).map(([, label, value, source], i) => (
|
|
<p key={i}>
|
|
<strong>{label}</strong>: {value}
|
|
<small className="block">{source}</small>
|
|
</p>
|
|
))}
|
|
</details>
|
|
);
|
|
}
|