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

692 lines
32 KiB
TypeScript

import { inputText } from '../lib/input-text.ts';
import { useState } from 'react';
import {
travelBook,
travelConditions,
travelCostKinds,
travelCurrent,
travelReportRows,
travelSource,
newTravel,
newTravelCost,
newTravelPeriod,
recordTravel,
saveTravelBook,
switchTravelResources,
type TravelBook,
type TravelRow,
type TravelPeriod,
type TravelCost,
} from '@/lib/iva-travel';
export function SimulationTravel({
draft,
onChange,
}: {
draft: Record<string, unknown>;
onChange: (field: string, value: string) => void;
}) {
const [error, setError] = useState('');
let book: TravelBook;
try {
book = travelBook(draft);
} catch (e) {
return <p role="alert">{String(e)}</p>;
}
const save = (next: TravelBook) => {
try {
onChange('modelo_a_snapshot', saveTravelBook(draft, next));
setError('');
} catch (e) {
setError(String(e));
}
};
const change = (id: string, patch: Partial<TravelRow>) =>
save({
...book,
rows: book.rows.map((r) => (r.id === id ? { ...r, ...patch } : r)),
});
const period = (id: string, patch: Partial<TravelPeriod>) =>
save({
...book,
periods: book.periods.map((p) =>
p.id === id ? { ...p, ...patch } : p,
),
});
const cost = (row: TravelRow, id: string, patch: Partial<TravelCost>) =>
change(row.id, {
costs: row.costs.map((c) => (c.id === id ? { ...c, ...patch } : c)),
});
return (
<details className="space-y-3 rounded border p-3">
<summary>
Agências de viagens — pacote, meios utilizados e margem do
período
</summary>
<p>
Preço único, actuação em nome próprio e meios de terceiros
determinam o percurso específico. Meios próprios, intermediação
e revenda simples conservam o regime geral aplicável. Reutilize
documentos e referências; não é necessário criar certificados ou
aprovadores.
</p>
<p>
A margem é global por operador e período do registo simulado,
com âmbito completo declarado. Não somar margem por pacote nem
misturar dados efectivos e projectados. A base não é IVA devido
nem dedução. Facturação oficial, regularização e apuramento
integral continuam fora deste cálculo.
</p>
<p>
<strong>Continuidade do trabalho fiscal:</strong> este
incremento trata margem e eventos, sem calcular IVA devido ou
dedução. A base e os eventos conservam as referências para o
apuramento e liquidação restantes; deduções e regularizações
seguem os seus percursos próprios e não ficam concluídas por
esta entrega.
</p>
<p>{travelSource}</p>
{error && <p role="alert">{error}</p>}
<button
type="button"
onClick={() =>
save({
...book,
rows: [
...book.rows,
newTravel(
crypto.randomUUID(),
inputText(draft.currency ?? ''),
),
],
})
}
>
Acrescentar operação de viagem
</button>
{book.rows.map((row) => (
<details key={row.id} className="rounded border p-3">
<summary>
{row.description || 'Viagem por identificar'} —{' '}
{row.period || 'período do registo por indicar'}
</summary>
<div className="grid gap-3 md:grid-cols-2">
{(
[
[
'description',
'Viagem ou conjunto de serviços',
],
[
'operatorRef',
'Operador: referência da agência/organizador',
],
[
'operationRef',
'Referência da operação / pressuposto',
],
[
'period',
'Período do registo de contraprestação e custos',
],
[
'documentRef',
'Documento existente / referência da previsão',
],
[
'documentDate',
'Data do documento / prevista',
],
[
'scope',
'Âmbito: pacote, parcela, período e condições',
],
['evidence', 'Factos / pressupostos expressos'],
[
'agencyRef',
'Sede/estabelecimento a partir do qual presta e referência',
],
['currency', 'Moeda original do registo'],
] as const
).map(([field, label]) => (
<label key={field}>
{label}
<input
type={
field === 'period'
? 'month'
: field === 'documentDate'
? 'date'
: 'text'
}
value={row[field]}
onChange={(e) =>
change(row.id, {
[field]: e.target.value,
})
}
/>
</label>
))}
<label>
Natureza dos dados
<select
value={row.origin}
onChange={(e) =>
change(row.id, {
origin: e.target
.value as TravelRow['origin'],
})
}
>
<option value="projected">
Projectados / pressupostos
</option>
<option value="effective">
Efectivos declarados
</option>
</select>
</label>
<label>
País do estabelecimento prestador
<select
value={row.agencyCountry}
onChange={(e) =>
change(row.id, {
agencyCountry: e.target.value,
})
}
>
<option value="">Por esclarecer</option>
<option value="MZ">Moçambique</option>
<option value="foreign">Estrangeiro</option>
</select>
</label>
<label>
Meios utilizados na viagem
<select
value={row.resources}
onChange={(e) =>
change(
row.id,
switchTravelResources(
row,
e.target.value,
),
)
}
>
<option value="">Por esclarecer</option>
<option value="third_party">
Apenas meios de terceiros
</option>
<option value="own">
Apenas meios próprios
</option>
<option value="mixed">
Meios próprios e de terceiros
</option>
</select>
</label>
<label>
Revenda retalhista de viagem organizada por
grossista
<select
value={row.retailer}
onChange={(e) =>
change(row.id, {
retailer: e.target
.value as TravelRow['retailer'],
})
}
>
<option value="">Por esclarecer</option>
<option value="yes">Sim</option>
<option value="no">Não</option>
</select>
</label>
{row.retailer === 'yes' && (
<>
<label>
Acrescenta serviços não meramente
acessórios/complementares e apresenta novo
conjunto em nome próprio?
<select
value={row.newPackage}
onChange={(e) =>
change(row.id, {
newPackage: e.target
.value as TravelRow['newPackage'],
})
}
>
<option value="">Por esclarecer</option>
<option value="yes">Sim</option>
<option value="no">Não</option>
</select>
</label>
<label>
Referência do novo conjunto e serviços
acrescidos
<input
value={row.packageRef}
onChange={(e) =>
change(row.id, {
packageRef: e.target.value,
})
}
/>
</label>
</>
)}
</div>
<fieldset>
<legend>Factos do percurso</legend>
{travelConditions.map(([key, label]) => (
<label className="block" key={key}>
{label}
<select
value={row.conditions[key] ?? ''}
onChange={(e) =>
change(row.id, {
conditions: {
...row.conditions,
[key]: e.target.value as
| ''
| 'yes'
| 'no',
},
})
}
>
<option value="">Por esclarecer</option>
<option value="yes">Sim</option>
<option value="no">Não</option>
</select>
</label>
))}
</fieldset>
{['third_party', 'mixed'].includes(row.resources) && (
<>
<label>
Parcela especial identificada como tributável no
âmbito nacional
<select
value={row.domesticTaxable}
onChange={(e) =>
change(row.id, {
domesticTaxable: e.target
.value as TravelRow['domesticTaxable'],
})
}
>
<option value="">Por esclarecer</option>
<option value="yes">
Sim, segundo fundamento indicado
</option>
<option value="no">Não</option>
</select>
</label>
<label>
Fundamento do enquadramento tributável
<input
value={row.taxRef}
onChange={(e) =>
change(row.id, {
taxRef: e.target.value,
})
}
/>
</label>
<label>
Contraprestação registada no período, com IVA
incluído
<input
inputMode="decimal"
value={row.grossPrice}
onChange={(e) =>
change(row.id, {
grossPrice: e.target.value,
})
}
/>
</label>
{row.resources === 'mixed' && (
<>
<label>
Contraprestação da parte que utiliza
meios de terceiros, com IVA
<input
inputMode="decimal"
value={row.thirdPartyGross}
onChange={(e) =>
change(row.id, {
thirdPartyGross:
e.target.value,
})
}
/>
</label>
<label>
Fundamento da repartição entre meios
próprios e terceiros
<input
value={row.allocationRef}
onChange={(e) =>
change(row.id, {
allocationRef:
e.target.value,
})
}
/>
</label>
</>
)}
<p>
Custos registados neste mesmo período; indicar
montantes com IVA incluído. A lista vazia só
representa ausência declarada de custos no
âmbito completo. Publicidade, câmbio,
comunicações da agência e comissões de venda não
reduzem esta margem41(3); a sua eventual dedução
não é calculada aqui.
</p>
<button
type="button"
onClick={() =>
change(row.id, {
costs: [
...row.costs,
newTravelCost(crypto.randomUUID()),
],
})
}
>
Acrescentar custo da viagem
</button>
{row.costs.map((c) => (
<fieldset key={c.id}>
<legend>
{c.description ||
'Custo por identificar'}
</legend>
<label>
Natureza do custo
<select
value={c.kind}
onChange={(e) =>
cost(row, c.id, {
kind: e.target.value,
})
}
>
<option value="">
Por esclarecer
</option>
{travelCostKinds.map(
([id, label]) => (
<option key={id} value={id}>
{label}
</option>
),
)}
</select>
</label>
{(
[
[
'description',
'Custo identificado',
],
[
'amount',
'Montante com IVA incluído',
],
[
'ref',
'Referência existente do custo / pressuposto',
],
] as const
).map(([field, label]) => (
<label key={field}>
{label}
<input
value={c[field]}
onChange={(e) =>
cost(row, c.id, {
[field]: e.target.value,
})
}
/>
</label>
))}
{['direct', 'other'].includes(c.kind) &&
(
[
[
'thirdParty',
'Fornecimento por terceiro',
],
[
'directBenefit',
'Benefício directo do cliente durante a viagem',
],
] as const
).map(([field, label]) => (
<label key={field}>
{label}
<select
value={c[field]}
onChange={(e) =>
cost(row, c.id, {
[field]:
e.target.value,
})
}
>
<option value="">
Por esclarecer
</option>
<option value="yes">
Sim
</option>
<option value="no">
Não
</option>
</select>
</label>
))}
</fieldset>
))}
</>
)}
<p>
Alterar meios ou percurso conserva referências e custos
ocultos; reveja as confirmações próprias. Alterações de
custos ou preço ficam em nova versão referenciada, sem
regularização automática.
</p>
</details>
))}
<h4>Registo global por operador/período</h4>
<button
type="button"
onClick={() =>
save({
...book,
periods: [
...book.periods,
newTravelPeriod(crypto.randomUUID()),
],
})
}
>
Acrescentar período de margem
</button>
{book.periods.map((p) => (
<fieldset key={p.id}>
<legend>
{p.period || 'Período por indicar'} —{' '}
{p.operatorRef || 'operador por indicar'}
</legend>
{(
[
['period', 'Período do registo global'],
[
'operatorRef',
'Mesma referência do operador nas operações',
],
['scope', 'Âmbito completo do registo simulado'],
[
'registerRef',
'Referência do registo ou pressuposto de completude',
],
] as const
).map(([field, label]) => (
<label key={field}>
{label}
<input
type={field === 'period' ? 'month' : 'text'}
value={p[field]}
onChange={(e) =>
period(p.id, { [field]: e.target.value })
}
/>
</label>
))}
<label>
Contraprestações e custos completos neste âmbito/período
<select
value={p.complete}
onChange={(e) =>
period(p.id, {
complete: e.target
.value as TravelPeriod['complete'],
})
}
>
<option value="">Por esclarecer</option>
<option value="yes">Sim</option>
<option value="no">Não, registo parcial</option>
</select>
</label>
<label>
Excesso negativo do período anterior
<select
value={p.priorMode}
onChange={(e) =>
period(p.id, { priorMode: e.target.value })
}
>
<option value="">Por esclarecer</option>
<option value="none">
Inexistência expressamente indicada
</option>
<option value="manual">
Montante anterior com referência
</option>
<option value="previous">
Reutilizar período imediatamente anterior
calculado do mesmo operador
</option>
</select>
</label>
{p.priorMode === 'manual' && (
<>
<label>
Excesso negativo anterior
<input
value={p.priorAmount}
onChange={(e) =>
period(p.id, {
priorAmount: e.target.value,
})
}
/>
</label>
<label>
Referência do registo anterior / alteração
fundamentada
<input
value={p.priorRef}
onChange={(e) =>
period(p.id, {
priorRef: e.target.value,
})
}
/>
</label>
</>
)}
{p.priorMode !== 'previous' && (
<label>
Se o excesso indicado divergir do anterior
calculado, confirmar a correcção referenciada após
conferir o impacto na base e reporte
<select
value={p.priorOverride}
onChange={(e) =>
period(p.id, {
priorOverride: e.target
.value as TravelPeriod['priorOverride'],
})
}
>
<option value="">
Conferir divergência se existir
</option>
<option value="yes">
Confirmo usar o valor indicado e a
referência da correcção
</option>
<option value="no">
Conservar/reutilizar o anterior
</option>
</select>
</label>
)}
</fieldset>
))}
{!!book.rows.length && (
<button
type="button"
onClick={() => {
try {
save(recordTravel(draft, book));
} catch (e) {
setError(String(e));
}
}}
>
Classificar viagens e apurar margem do período
</button>
)}
{book.result && (
<>
<p role="status">
{travelCurrent(draft, book)
? 'Resultados correspondem ao registo guardado.'
: 'Entradas alteradas; resultado histórico conservado. Reveja o delta.'}{' '}
{book.history?.length ?? 0} versões anteriores. Sem IVA
devido ou dedução calculados.
</p>
<dl>
{travelReportRows(draft).map(
([, label, value, origin], i) => (
<div key={i}>
<dt>{label}</dt>
<dd>
{value}
<small className="block">
{origin}
</small>
</dd>
</div>
),
)}
</dl>
</>
)}
</details>
);
}