692 lines
32 KiB
TypeScript
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>
|
|
);
|
|
}
|