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

454 lines
20 KiB
TypeScript

import { inputText } from '../lib/input-text.ts';
import { SimulationRemissionImports } from './simulation-remission-imports';
import { SimulationObjectiveImports } from './simulation-objective-imports';
import { SimulationMiners } from './simulation-miners';
import { SimulationReturnees } from './simulation-returnees';
import { SimulationDigital } from './simulation-digital';
import { SimulationTravel } from './simulation-travel';
import { SimulationTravelEvents } from './simulation-travel-events';
import { SimulationForeignOperations } from './simulation-foreign-operations';
import { SimulationCurrencyEvents } from './simulation-currency-events';
import { SimulationPropertyTransfers } from './simulation-property-transfers';
import { SimulationGoodsPresumptions } from './simulation-goods-presumptions';
import { InvestmentContextAssessment } from './investment-context-assessment';
import { useEffect, useRef, useState } from 'react';
import {
monthlyIvaInput,
monthlyIvaKey,
monthlySalesFromDraft,
readMonthlyIvaRecord,
readMonthlyIvaSettings,
recoverMonthlyIvaRecord,
monthlyIvaCashTreatment,
type MonthlyIvaInput,
type MonthlyIvaSettings,
} from '@/lib/monthly-iva';
import { fiscalQaRows } from '@/lib/fiscal-qa-preview';
import { type ApiSimulationExecutionV1 } from '@/lib/simulation-contract';
import { SimulationModeloA } from './simulation-modelo-a';
import { SimulationAcquisitions } from './simulation-acquisitions';
import { SimulationImportExemptions } from './simulation-import-exemptions';
import { SimulationPublicAuthorityBase } from './simulation-public-authority-base';
import { SimulationCommissionBase } from './simulation-commission-base';
import { SimulationActSaleBase } from './simulation-act-sale-base';
import { SimulationFuelBase } from './simulation-fuel-base';
import { SimulationNoncashBase } from './simulation-noncash-base';
import { SimulationCompositionBase } from './simulation-composition-base';
import { SimulationExclusionBase } from './simulation-exclusion-base';
import { SimulationValueReview } from './simulation-value-review';
import { SimulationServiceNormal } from './simulation-service-normal';
import { SimulationGoodsLocation } from './simulation-goods-location';
import { SimulationServiceLocation } from './simulation-service-location';
import { SimulationListedServices } from './simulation-listed-services';
import { SimulationReducedBases } from './simulation-reduced-bases';
import { SimulationOwnName } from './simulation-own-name';
import { SimulationGoodsAllocation } from './simulation-goods-allocation';
import { SimulationSecondhand } from './simulation-secondhand';
import { SimulationOfferPreparations } from './simulation-offer-preparations';
import { SimulationOperations } from './simulation-operations';
export function SimulationMonthlyIva({
draft,
busy,
onCalculate,
onChange,
}: {
draft: Record<string, unknown>;
busy: boolean;
onCalculate: (
input: MonthlyIvaInput,
) => Promise<ApiSimulationExecutionV1 | null | undefined>;
onChange: (field: string, value: string) => void;
}) {
const settings = readMonthlyIvaSettings(
inputText(draft.monthly_iva_settings ?? ''),
);
const record = readMonthlyIvaRecord(
inputText(draft.monthly_iva_snapshot ?? ''),
);
const [retry, setRetry] = useState(0);
const [error, setError] = useState('');
const [available, setAvailable] = useState<boolean | null>(null);
const attempt = useRef('');
const mounted = useRef(true);
const latestSnapshot = useRef(draft.monthly_iva_snapshot);
latestSnapshot.current = draft.monthly_iva_snapshot;
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const callbacks = useRef({ onCalculate, onChange });
callbacks.current = { onCalculate, onChange };
let input: MonthlyIvaInput | null = null;
let inputError = '';
let source = '';
try {
source = monthlySalesFromDraft(draft).source;
input = monthlyIvaInput(draft, settings);
} catch (cause) {
inputError =
cause instanceof Error
? cause.message
: 'Reveja a projecção de vendas.';
}
const key = input ? monthlyIvaKey(input) : '';
const current = !!(input && record && monthlyIvaKey(record.input) === key);
const completed = current && record?.execution.status === 'completed';
const pendingRecord =
record && ['queued', 'running'].includes(record.execution.status)
? record
: null;
const actionKey = pendingRecord
? `recover:${pendingRecord.execution.id}`
: key;
const saveSettings = (patch: Partial<MonthlyIvaSettings>) =>
onChange(
'monthly_iva_settings',
JSON.stringify({ ...settings, ...patch }),
);
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) setAvailable(payload?.data?.enabled === true);
})
.catch(() => {
if (active)
setError(
'Não foi possível consultar a disponibilidade da regra. As vendas foram preservadas.',
);
});
return () => {
active = false;
};
}, [retry]);
useEffect(() => {
if (!settings.auto || busy || attempt.current === actionKey) return;
if (
!pendingRecord &&
(!input || input.currency !== 'MZN' || !available || completed)
)
return;
const request = pendingRecord?.input ?? input!;
const savedSnapshot = draft.monthly_iva_snapshot;
const timer = window.setTimeout(() => {
attempt.current = actionKey;
setError('');
void (async () => {
try {
let execution;
if (pendingRecord) {
execution = (
await recoverMonthlyIvaRecord(pendingRecord)
).execution;
} else
execution =
await callbacks.current.onCalculate(request);
if (!execution)
throw new Error(
'O cálculo mensal não foi concluído. As vendas e a versão anterior foram preservadas; tente novamente.',
);
if (
!mounted.current ||
(pendingRecord &&
latestSnapshot.current !== savedSnapshot)
)
return;
callbacks.current.onChange(
'monthly_iva_snapshot',
JSON.stringify({ input: request, execution }),
);
} catch (cause) {
if (mounted.current)
setError(
cause instanceof Error
? cause.message
: 'Não foi possível calcular o IVA mensal.',
);
}
})();
}, 700);
return () => window.clearTimeout(timer);
}, [actionKey, key, settings.auto, busy, available, completed, retry]);
const snapshot = record?.execution.result_snapshot;
const projection = snapshot?.monthly_projection as
| {
rows?: Array<{
date: string;
sales_declared: string | null;
net_sales: string | null;
rate: string | null;
vat: string | null;
gross_sales: string | null;
status: string;
}>;
}
| undefined;
return (
<section
className="space-y-3 rounded border border-emerald-700 p-4"
aria-label="IVA mensal das vendas projectadas"
>
<InvestmentContextAssessment
raw={draft.context_applicability_snapshot}
/>
<h3 className="font-semibold">IVA mensal das vendas projectadas</h3>
<p>{source}</p>
<p>
Declare se os preços projectados são sem IVA ou com IVA
incluído. Nenhuma taxa é fixada neste formulário; o servidor
selecciona a versão publicada.
</p>
<label className="block">
Primeiro mês da projecção
<input
type="month"
value={settings.start}
min="2026-01"
max="2026-12"
onChange={(event) =>
saveSettings({ start: event.target.value })
}
className="block rounded border p-2"
/>
</label>
<label className="block">
Tratamento do IVA nos preços projectados
<select
value={settings.basis}
onChange={(event) =>
saveSettings({
basis: event.target
.value as MonthlyIvaSettings['basis'],
})
}
className="block rounded border p-2"
>
<option value="unknown">Por indicar</option>
<option value="net">Vendas sem IVA (base líquida)</option>
<option value="included">Vendas com IVA incluído</option>
</select>
</label>
<label className="block">
<input
type="checkbox"
checked={settings.declared}
onChange={(event) =>
saveSettings({ declared: event.target.checked })
}
/>{' '}
Declaro que as vendas projectadas são tributáveis à taxa geral
interna e não incluem as operações excluídas deste lote.
</label>
<p className="text-sm">
Sem importações, taxas reduzidas, isenções, regimes especiais,
deduções, reembolsos ou apuramento final. Valores com IVA
incluído permanecem indeterminados neste lote; não se aplica a
taxa ao preço bruto.
</p>
<p className="text-sm">{monthlyIvaCashTreatment}</p>
{!!draft.currency && draft.currency !== 'MZN' && (
<p role="alert">
Moeda do projecto: {inputText(draft.currency)}. A regra
disponível aceita apenas MZN. As vendas e a moeda são
preservadas, sem conversão; o IVA mensal não pode ser
determinado por esta versão.
</p>
)}
{inputError && <p role="alert">{inputError}</p>}
{available === null && !error && (
<p role="status">
A consultar a disponibilidade da versão publicada…
</p>
)}
{available === false && (
<p role="status">
A versão publicada de IVA está indisponível neste ambiente
para novas execuções. Os pressupostos e históricos
mantêm-se.
</p>
)}
{record && !current && (
<p role="alert">
As vendas ou os pressupostos mudaram. Os resultados abaixo
são históricos e estão desactualizados; não são usados como
resultados da projecção actual.
</p>
)}
{settings.auto && !completed && current && (
<p role="status">
Execução mensal guardada:{' '}
{record?.execution.status === 'queued'
? 'em fila'
: record?.execution.status === 'running'
? 'em processamento'
: 'não concluída'}
. Pode voltar a consultar sem duplicar.
</p>
)}
{busy && (
<p role="status">
A actualizar os resultados dependentes das vendas…
</p>
)}
{error && <p role="alert">{error}</p>}
<button
type="button"
disabled={
busy ||
(!pendingRecord &&
(!available || !input || input.currency !== 'MZN'))
}
onClick={() => {
attempt.current = '';
setRetry((value) => value + 1);
saveSettings({ auto: true });
}}
className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50"
>
{pendingRecord
? 'Recuperar execução mensal admitida pelo mesmo ID'
: 'Calcular / actualizar IVA mensal e guardar versão'}
</button>
{pendingRecord && (
<p>
Consulta da execução {pendingRecord.execution.id}. A
suspensão de novos cálculos não impede esta recuperação; não
é criado outro pedido.
</p>
)}
{settings.auto && (
<button
type="button"
className="ml-3 underline"
onClick={() => saveSettings({ auto: false })}
>
Pausar actualização automática
</button>
)}
{input && (
<p className="text-sm">
{input.monthly_sales.length} meses; moeda{' '}
{input.currency || 'por indicar'}. Campo vazio significa
informação em falta; zero declarado permanece zero. Depois
da primeira execução, alterações das vendas actualizam o
cálculo enquanto a actualização automática estiver ligada.
</p>
)}
{projection?.rows && (
<div className="overflow-x-auto" tabIndex={0}>
<table className="min-w-full text-sm">
<caption>
IVA mensal —{' '}
{current
? 'versão correspondente às entradas'
: 'versão histórica'}
</caption>
<thead>
<tr>
{[
'Mês',
'Vendas declaradas',
'Vendas tributáveis sem IVA',
'Taxa aplicada (%)',
'IVA calculado',
'Total com IVA',
'Estado',
].map((label) => (
<th key={label} className="p-2 text-left">
{label}
</th>
))}
</tr>
</thead>
<tbody>
{projection.rows.map((row) => (
<tr key={row.date}>
<th scope="row" className="p-2">
{row.date.slice(0, 7)}
</th>
<td>
{row.sales_declared ?? 'Sem informação'}
</td>
<td>{row.net_sales ?? 'Indeterminado'}</td>
<td>{row.rate ?? 'Indeterminada'}</td>
<td>{row.vat ?? 'Indeterminado'}</td>
<td>
{row.gross_sales ?? 'Indeterminado'}
</td>
<td>
{{
calculated: 'Calculado',
missing_sales:
'Sem informação de vendas',
unsupported_basis:
'Base líquida não determinada',
indeterminate:
'Enquadramento ou regra indeterminados',
}[row.status] ?? 'Indeterminado'}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{snapshot && (
<details>
<summary>
Ver fundamento e relatório da versão guardada
</summary>
<dl>
{fiscalQaRows(snapshot).map(([label, value], index) => (
<div key={index}>
<dt className="font-semibold">{label}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
</details>
)}
<SimulationOperations draft={draft} onChange={onChange} />
<SimulationAcquisitions draft={draft} onChange={onChange} />
<SimulationImportExemptions draft={draft} onChange={onChange} />
<SimulationObjectiveImports draft={draft} onChange={onChange} />
<SimulationMiners draft={draft} onChange={onChange} />
<SimulationRemissionImports draft={draft} onChange={onChange} />
<SimulationReturnees draft={draft} onChange={onChange} />
<SimulationPublicAuthorityBase draft={draft} onChange={onChange} />
<SimulationCommissionBase draft={draft} onChange={onChange} />
<SimulationActSaleBase draft={draft} onChange={onChange} />
<SimulationFuelBase draft={draft} onChange={onChange} />
<SimulationNoncashBase draft={draft} onChange={onChange} />
<SimulationCompositionBase draft={draft} onChange={onChange} />
<SimulationExclusionBase draft={draft} onChange={onChange} />
<SimulationValueReview draft={draft} onChange={onChange} />
<SimulationServiceNormal draft={draft} onChange={onChange} />
<SimulationGoodsLocation draft={draft} onChange={onChange} />
<SimulationServiceLocation draft={draft} onChange={onChange} />
<SimulationListedServices draft={draft} onChange={onChange} />
<SimulationReducedBases draft={draft} onChange={onChange} />
<SimulationOwnName draft={draft} onChange={onChange} />
<SimulationGoodsAllocation draft={draft} onChange={onChange} />
<SimulationForeignOperations draft={draft} onChange={onChange} />
<SimulationDigital draft={draft} onChange={onChange} />
<SimulationTravel draft={draft} onChange={onChange} />
<SimulationTravelEvents draft={draft} onChange={onChange} />
<SimulationCurrencyEvents draft={draft} onChange={onChange} />
<SimulationPropertyTransfers draft={draft} onChange={onChange} />
<SimulationGoodsPresumptions draft={draft} onChange={onChange} />
<SimulationSecondhand draft={draft} onChange={onChange} />
<SimulationOfferPreparations draft={draft} onChange={onChange} />
<SimulationModeloA draft={draft} />
</section>
);
}