Files
Investidor-V2/resources/js/components/simulation-published-iva.tsx
T

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>
);
}