196 lines
7.7 KiB
TypeScript
196 lines
7.7 KiB
TypeScript
import { inputText } from '../lib/input-text.ts';
|
|
import { useEffect, useState } from 'react';
|
|
import type { MonthlyIvaInput } from '@/lib/monthly-iva';
|
|
|
|
export type PublishedIvaInput =
|
|
| MonthlyIvaInput
|
|
| {
|
|
scenario_kind: 'fiscal_rule_published';
|
|
base: string;
|
|
currency: 'MZN';
|
|
reference_date: string;
|
|
operation: 'internal_general_net';
|
|
scope_declared: boolean;
|
|
};
|
|
|
|
export function SimulationPublishedIva({
|
|
busy,
|
|
onCalculate,
|
|
draft,
|
|
onChange,
|
|
}: {
|
|
draft: Record<string, unknown>;
|
|
onChange: (field: string, value: string) => void;
|
|
busy: boolean;
|
|
onCalculate: (input: PublishedIvaInput) => Promise<unknown>;
|
|
}) {
|
|
const [available, setAvailable] = useState(false);
|
|
const [message, setMessage] = useState(
|
|
'A consultar disponibilidade do cálculo de IVA…',
|
|
);
|
|
const base = inputText(draft.published_iva_base ?? '');
|
|
const date = inputText(draft.published_iva_date ?? '');
|
|
const currency = inputText(draft.published_iva_currency ?? '');
|
|
const declared = draft.published_iva_scope === 'yes';
|
|
const setBase = (value: string) => onChange('published_iva_base', value);
|
|
const setDate = (value: string) => onChange('published_iva_date', value);
|
|
const setCurrency = (value: string) =>
|
|
onChange('published_iva_currency', value);
|
|
const setDeclared = (value: boolean) =>
|
|
onChange('published_iva_scope', value ? 'yes' : 'no');
|
|
const [error, setError] = useState('');
|
|
useEffect(() => {
|
|
let active = true;
|
|
fetch('/api/v1/fiscal-consumer', {
|
|
credentials: 'same-origin',
|
|
headers: { Accept: 'application/json' },
|
|
})
|
|
.then(async (response) => {
|
|
if (!response.ok) throw new Error();
|
|
const payload = await response.json();
|
|
if (!active) return;
|
|
setAvailable(payload?.data?.enabled === true);
|
|
setMessage(
|
|
typeof payload?.data?.message === 'string'
|
|
? payload.data.message
|
|
: 'Cálculo de IVA indisponível.',
|
|
);
|
|
})
|
|
.catch(() => {
|
|
if (active)
|
|
setMessage(
|
|
'Não foi possível consultar a disponibilidade do IVA. As entradas e históricos foram preservados.',
|
|
);
|
|
});
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, []);
|
|
|
|
async function calculate() {
|
|
if (
|
|
!/^\d+(?:\.\d+)?$/.test(base) ||
|
|
base.length > 40 ||
|
|
!date ||
|
|
currency !== 'MZN'
|
|
) {
|
|
setError(
|
|
'Indique uma base líquida decimal válida, a moeda MZN e a data de exigibilidade. As entradas foram preservadas.',
|
|
);
|
|
return;
|
|
}
|
|
setError('');
|
|
await onCalculate({
|
|
scenario_kind: 'fiscal_rule_published',
|
|
base,
|
|
currency: 'MZN',
|
|
reference_date: date,
|
|
operation: 'internal_general_net',
|
|
scope_declared: declared,
|
|
});
|
|
// Refresh after admission/refusal so a suspension is reflected without discarding inputs.
|
|
try {
|
|
const response = await fetch('/api/v1/fiscal-consumer', {
|
|
credentials: 'same-origin',
|
|
headers: { Accept: 'application/json' },
|
|
});
|
|
if (!response.ok) return;
|
|
const payload = await response.json();
|
|
setAvailable(payload?.data?.enabled === true);
|
|
if (typeof payload?.data?.message === 'string')
|
|
setMessage(payload.data.message);
|
|
} catch {
|
|
/* The execution flow already preserves and reports connection errors. */
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section
|
|
id="published-iva"
|
|
tabIndex={-1}
|
|
aria-label="IVA — taxa geral"
|
|
className="space-y-3 rounded border border-emerald-700 p-4"
|
|
>
|
|
<h4 className="font-semibold">IVA — taxa geral</h4>
|
|
<p role="status">{message}</p>
|
|
<p className="text-sm">
|
|
Cobertura: exigibilidade em 2026, operação interna à taxa geral.
|
|
A base tributável é indicada por si; não é copiada do orçamento.
|
|
Sem isenções, taxas reduzidas, bases especiais, importações,
|
|
deduções, declaração ou pagamento. O cálculo não valida o
|
|
enquadramento jurídico.
|
|
</p>
|
|
<fieldset disabled={busy} className="space-y-3">
|
|
<label className="block">
|
|
Base tributável líquida (decimal, com ponto)
|
|
<input
|
|
aria-label="Base tributável líquida"
|
|
inputMode="decimal"
|
|
value={base}
|
|
onChange={(e) => setBase(e.target.value)}
|
|
className="block rounded border p-2"
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
Moeda da base
|
|
<select
|
|
aria-label="Moeda da base IVA"
|
|
value={currency}
|
|
onChange={(e) => setCurrency(e.target.value)}
|
|
className="block rounded border p-2"
|
|
>
|
|
<option value="">Seleccione</option>
|
|
<option value="MZN">MZN</option>
|
|
{currency && currency !== 'MZN' && (
|
|
<option value={currency}>
|
|
{currency} — moeda declarada
|
|
</option>
|
|
)}
|
|
</select>
|
|
</label>
|
|
<label className="block">
|
|
Data de exigibilidade declarada
|
|
<input
|
|
aria-label="Data de exigibilidade IVA"
|
|
type="date"
|
|
min="2026-01-01"
|
|
max="2026-12-31"
|
|
value={date}
|
|
onChange={(e) => setDate(e.target.value)}
|
|
className="block rounded border p-2"
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
<input
|
|
type="checkbox"
|
|
checked={declared}
|
|
onChange={(e) => setDeclared(e.target.checked)}
|
|
/>{' '}
|
|
Declaro operação interna à taxa geral, base líquida já
|
|
apurada e ausência das excepções acima.
|
|
</label>
|
|
<p className="text-sm">
|
|
Sem esta declaração, o resultado fica indeterminado. O valor
|
|
calculado conserva a precisão decimal; não é saldo a pagar.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
disabled={!available || busy}
|
|
onClick={() => void calculate()}
|
|
className="rounded bg-emerald-800 px-4 py-2 text-white disabled:opacity-50"
|
|
>
|
|
{busy ? 'A calcular…' : 'Calcular IVA e gravar'}
|
|
</button>
|
|
</fieldset>
|
|
{currency && currency !== 'MZN' && (
|
|
<p role="alert">
|
|
Moeda: {currency}. A versão disponível aceita apenas MZN. O
|
|
valor declarado foi preservado; não foi feita conversão.
|
|
Este cálculo permanece indisponível para esta moeda.
|
|
</p>
|
|
)}
|
|
{error && <p role="alert">{error}</p>}
|
|
</section>
|
|
);
|
|
}
|