603 lines
29 KiB
TypeScript
603 lines
29 KiB
TypeScript
import { inputText } from '../lib/input-text.ts';
|
||
import { useState } from 'react';
|
||
import {
|
||
newSecondhand,
|
||
switchSecondhandTreatment,
|
||
switchSecondhandCategory,
|
||
secondhandBook,
|
||
secondhandConditions,
|
||
secondhandCategoryConditions,
|
||
secondhandCurrent,
|
||
secondhandReportRows,
|
||
secondhandSource,
|
||
secondhandTreatments,
|
||
secondhandCategories,
|
||
recordSecondhands,
|
||
saveSecondhandBook,
|
||
type SecondhandBook,
|
||
type SecondhandRow,
|
||
} from '@/lib/iva-secondhand';
|
||
|
||
export function SimulationSecondhand({
|
||
draft,
|
||
onChange,
|
||
}: {
|
||
draft: Record<string, unknown>;
|
||
onChange: (field: string, value: string) => void;
|
||
}) {
|
||
const [error, setError] = useState('');
|
||
let book: SecondhandBook;
|
||
try {
|
||
book = secondhandBook(draft);
|
||
} catch (cause) {
|
||
return <p role="alert">{String(cause)}</p>;
|
||
}
|
||
const save = (next: SecondhandBook) => {
|
||
try {
|
||
onChange('modelo_a_snapshot', saveSecondhandBook(draft, next));
|
||
setError('');
|
||
} catch (cause) {
|
||
setError(String(cause));
|
||
}
|
||
};
|
||
const change = (id: string, patch: Partial<SecondhandRow>) =>
|
||
save({
|
||
...book,
|
||
rows: book.rows.map((row) =>
|
||
row.id === id ? { ...row, ...patch } : row,
|
||
),
|
||
});
|
||
return (
|
||
<details className="space-y-3 rounded border p-3">
|
||
<summary>Bens emsegunda mão — revenda pela margem eleilão</summary>
|
||
<p>
|
||
Oportunidade:{' '}
|
||
{inputText(draft.context_opportunity || 'entrada geral')}. Uma
|
||
linha porbem.P125identifica
|
||
categoria/estatuto;P092ouP093determina a base aplicável.Sem
|
||
compensação de perdas entrebens,nova dedução ou IVAcalculado.
|
||
</p>
|
||
<p>
|
||
Revendedor:contraprestação total comIVA−preço compra,parcela
|
||
positiva÷1.16 ebase arredondada à unidade48(6)b.16%é parâmetro
|
||
do divisor,não IVAcalculado neste
|
||
percurso.Leilão:facturado−líquidocomitente−IVApróprio
|
||
identificado;líquido=adjudicação−comissão.Não substituir IVA
|
||
desconhecido porzero.
|
||
</p>
|
||
<p>{secondhandSource}</p>
|
||
{error && <p role="alert">{error}</p>}
|
||
<button
|
||
type="button"
|
||
className="underline"
|
||
onClick={() =>
|
||
save({
|
||
...book,
|
||
rows: [
|
||
...book.rows,
|
||
newSecondhand(
|
||
crypto.randomUUID(),
|
||
inputText(draft.currency ?? ''),
|
||
),
|
||
],
|
||
})
|
||
}
|
||
>
|
||
Acrescentar bem individual
|
||
</button>
|
||
{book.rows.map((row) => (
|
||
<details key={row.id} className="rounded border p-3">
|
||
<summary>
|
||
{row.description || 'Bem por identificar'} —{' '}
|
||
{row.period || 'mês por indicar'}
|
||
</summary>
|
||
<div className="grid gap-3 md:grid-cols-2">
|
||
<label>
|
||
Percurso da margem
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.treatment}
|
||
onChange={(e) =>
|
||
change(
|
||
row.id,
|
||
switchSecondhandTreatment(
|
||
row,
|
||
e.target.value,
|
||
),
|
||
)
|
||
}
|
||
>
|
||
<option value="">Por indicar</option>
|
||
{secondhandTreatments.map((t) => (
|
||
<option key={t.id} value={t.id}>
|
||
{t.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
<label>
|
||
Categoria do bem
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.category}
|
||
onChange={(e) =>
|
||
change(
|
||
row.id,
|
||
switchSecondhandCategory(
|
||
row,
|
||
e.target.value,
|
||
),
|
||
)
|
||
}
|
||
>
|
||
<option value="">Por indicar</option>
|
||
{secondhandCategories.map((c) => (
|
||
<option key={c.id} value={c.id}>
|
||
{c.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
<label>
|
||
Natureza dos dados
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.origin}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
origin: e.target
|
||
.value as SecondhandRow['origin'],
|
||
})
|
||
}
|
||
>
|
||
<option value="projected">
|
||
Projectados / pressupostos
|
||
</option>
|
||
<option value="effective">
|
||
Efectivos declarados
|
||
</option>
|
||
</select>
|
||
</label>
|
||
{(
|
||
[
|
||
['description', 'Bem/operação identificados'],
|
||
['operationRef', 'Referência da operação'],
|
||
[
|
||
'operationDate',
|
||
'Data da operação / prevista',
|
||
],
|
||
['period', 'Mês da operação'],
|
||
['currency', 'Moeda dos montantes'],
|
||
[
|
||
'documentRef',
|
||
'Documento / referência da previsão',
|
||
],
|
||
[
|
||
'documentDate',
|
||
'Data do documento / prevista',
|
||
],
|
||
['goodsRef', 'Identidade do bem individual'],
|
||
[
|
||
'operatorRef',
|
||
'Operador e estatuto revendedor/organizador',
|
||
],
|
||
[
|
||
'categoryRef',
|
||
'Características e referência da classificação',
|
||
],
|
||
[
|
||
'sourceRef',
|
||
'Fornecedor/comitente,origem e referência',
|
||
],
|
||
[
|
||
'scope',
|
||
'Âmbito:bem,partes,operação eperíodo',
|
||
],
|
||
[
|
||
'evidence',
|
||
'Fundamento factual / pressupostos',
|
||
],
|
||
] as const
|
||
).map(([field, label]) => (
|
||
<label key={field}>
|
||
{label}
|
||
<input
|
||
className="block rounded border p-2"
|
||
type={
|
||
[
|
||
'operationDate',
|
||
'documentDate',
|
||
].includes(field)
|
||
? 'date'
|
||
: field === 'period'
|
||
? 'month'
|
||
: 'text'
|
||
}
|
||
value={row[field]}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
[field]: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
))}
|
||
<label>
|
||
Qualidade do fornecedor/comitente
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.sourceType}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
sourceType: e.target.value,
|
||
})
|
||
}
|
||
>
|
||
<option value="">Por esclarecer</option>
|
||
<option value="private">Particular</option>
|
||
<option value="margin_reseller">
|
||
Revendedor sujeito à margem
|
||
</option>
|
||
<option value="exempt_9_12_e">
|
||
Transmissão no âmbito9(12)e
|
||
</option>
|
||
<option value="legacy_35_42">
|
||
Referência histórica a35/42 — conferir
|
||
situação2026
|
||
</option>
|
||
<option value="other">
|
||
Outro enquadramento por esclarecer
|
||
</option>
|
||
</select>
|
||
</label>
|
||
{(
|
||
[
|
||
[
|
||
'domesticPurchase',
|
||
'Aquisição dos bens no território nacional',
|
||
],
|
||
[
|
||
'domesticTaxable',
|
||
'Transmissão interna tributável,não exportação isenta13',
|
||
],
|
||
] as const
|
||
).map(([field, label]) => (
|
||
<label key={field}>
|
||
{label}
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row[field]}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
[field]: e.target.value,
|
||
})
|
||
}
|
||
>
|
||
<option value="">Por esclarecer</option>
|
||
<option value="yes">Sim</option>
|
||
<option value="no">Não</option>
|
||
</select>
|
||
</label>
|
||
))}
|
||
{row.category === 'art' && (
|
||
<>
|
||
<label>
|
||
Tipo de objecto de arte
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.artSubtype}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
artSubtype: e.target.value,
|
||
})
|
||
}
|
||
>
|
||
<option value="">Por indicar</option>
|
||
<option value="original">
|
||
Quadro,pintura ou desenho original
|
||
</option>
|
||
<option value="print">
|
||
Gravura,estampa ou litografia
|
||
</option>
|
||
<option value="sculpture">
|
||
Escultura ou estatuária
|
||
</option>
|
||
<option value="ceramic">
|
||
Cerâmica
|
||
</option>
|
||
</select>
|
||
</label>
|
||
{row.artSubtype === 'print' && (
|
||
<label>
|
||
Tiragem total
|
||
<input
|
||
className="block rounded border p-2"
|
||
value={row.editionCount}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
editionCount:
|
||
e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
)}
|
||
{row.artSubtype === 'ceramic' && (
|
||
<label>
|
||
Cerâmica única,executada eassinada pelo
|
||
artista
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.ceramicUniqueSigned}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
ceramicUniqueSigned:
|
||
e.target.value,
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Por esclarecer
|
||
</option>
|
||
<option value="yes">Sim</option>
|
||
<option value="no">Não</option>
|
||
</select>
|
||
</label>
|
||
)}
|
||
</>
|
||
)}
|
||
{row.category === 'collection' && (
|
||
<>
|
||
<label>
|
||
Tipo de colecção
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.collectionSubtype}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
collectionSubtype:
|
||
e.target.value,
|
||
})
|
||
}
|
||
>
|
||
<option value="">Por indicar</option>
|
||
<option value="postal">
|
||
Postal/filatélica
|
||
</option>
|
||
<option value="scientific">
|
||
Colecções/espécimes científicos3(6)c
|
||
</option>
|
||
<option value="historical">
|
||
Interesse
|
||
histórico,arqueológico,paleontológico,etnográfico
|
||
ou numismático
|
||
</option>
|
||
</select>
|
||
</label>
|
||
{row.collectionSubtype === 'postal' && (
|
||
<label>
|
||
Fora decirculação esem destino aser
|
||
posto emcirculação
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.postalNotCirculating}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
postalNotCirculating:
|
||
e.target.value,
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Por esclarecer
|
||
</option>
|
||
<option value="yes">Sim</option>
|
||
<option value="no">Não</option>
|
||
</select>
|
||
</label>
|
||
)}
|
||
</>
|
||
)}
|
||
{row.category === 'antique' && (
|
||
<label>
|
||
Idade emanos
|
||
<input
|
||
className="block rounded border p-2"
|
||
value={row.ageYears}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
ageYears: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
)}
|
||
{row.treatment === 'reseller_margin' && (
|
||
<>
|
||
<label>
|
||
Opção do revendedor
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.regimeChoice}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
regimeChoice: e.target.value,
|
||
})
|
||
}
|
||
>
|
||
<option value="">Por esclarecer</option>
|
||
<option value="margin">
|
||
Regime da margem
|
||
</option>
|
||
<option value="general">
|
||
Regime geral — outra fórmula
|
||
</option>
|
||
</select>
|
||
</label>
|
||
{(
|
||
[
|
||
[
|
||
'grossSale',
|
||
'Contraprestação total devida pelocliente,comIVA incluído',
|
||
],
|
||
[
|
||
'purchasePrice',
|
||
'Preço compra do mesmo bem',
|
||
],
|
||
[
|
||
'purchaseRef',
|
||
'Justificação/referência do preço compra',
|
||
],
|
||
] as const
|
||
).map(([field, label]) => (
|
||
<label key={field}>
|
||
{label}
|
||
<input
|
||
className="block rounded border p-2"
|
||
value={row[field]}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
[field]: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
))}
|
||
</>
|
||
)}
|
||
{row.treatment === 'auction_margin' &&
|
||
(
|
||
[
|
||
[
|
||
'grossInvoice',
|
||
'Montante facturado ao comprador49(4)',
|
||
],
|
||
['hammerPrice', 'Preço de adjudicação'],
|
||
[
|
||
'principalCommission',
|
||
'Comissão obtida/aobter do comitente',
|
||
],
|
||
[
|
||
'auctionRef',
|
||
'Contrato de comissão de venda emnome próprio eleilão',
|
||
],
|
||
[
|
||
'principalRef',
|
||
'Comitente e referência do líquido',
|
||
],
|
||
[
|
||
'ownVat',
|
||
'IVApróprio relativo àtransmissão,identificado',
|
||
],
|
||
[
|
||
'ownVatRef',
|
||
'Referência/composição do IVApróprio identificado',
|
||
],
|
||
] as const
|
||
).map(([field, label]) => (
|
||
<label key={field}>
|
||
{label}
|
||
<input
|
||
className="block rounded border p-2"
|
||
value={row[field]}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
[field]: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
))}
|
||
</div>
|
||
<p>
|
||
Os montantes ocultos de outro percurso são conservados
|
||
esó entram na fórmula escolhida.Situação antiga35/42não
|
||
activa margem automaticamente em2026;sem novo
|
||
certificado/aprovador.
|
||
</p>
|
||
<fieldset>
|
||
<legend>Condições da classificação eregime</legend>
|
||
{[
|
||
...secondhandConditions,
|
||
...secondhandCategoryConditions(row.category),
|
||
[
|
||
'regimeConfirmed',
|
||
'Condições47/48(1)ou49(1),papéis/contrato equadro temporal conferidos',
|
||
],
|
||
[
|
||
'compositionConfirmed',
|
||
'Composição monetária corresponde ao percurso:revenda comIVA no total,ou leilão comfacturado,líquidocomitente eIVApróprio separados,sem dupla subtracção',
|
||
],
|
||
].map(([key, label]) => (
|
||
<label className="block" key={key}>
|
||
{label}
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.conditions[key] ?? ''}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
conditions: {
|
||
...row.conditions,
|
||
[key]: e.target.value as
|
||
| ''
|
||
| 'yes'
|
||
| 'no',
|
||
},
|
||
})
|
||
}
|
||
>
|
||
<option value="">Por indicar</option>
|
||
<option value="yes">Sim</option>
|
||
<option value="no">Não</option>
|
||
</select>
|
||
</label>
|
||
))}
|
||
</fieldset>
|
||
</details>
|
||
))}
|
||
{!!book.rows.length && (
|
||
<button
|
||
type="button"
|
||
className="rounded bg-emerald-800 p-2 text-white"
|
||
onClick={() => {
|
||
try {
|
||
save(recordSecondhands(draft, book));
|
||
} catch (cause) {
|
||
setError(String(cause));
|
||
}
|
||
}}
|
||
>
|
||
Classificar bens,calcular bases eguardar versão
|
||
</button>
|
||
)}
|
||
{book.result && (
|
||
<>
|
||
<p role="status">
|
||
{secondhandCurrent(draft, book)
|
||
? 'Classificações/bases correspondentes às entradas guardadas.'
|
||
: 'Entradas alteradas;resultados históricos conservados.'}{' '}
|
||
{book.history?.length ?? 0} versões anteriores.Sem
|
||
compensar margens entrebens,calcular IVA ou
|
||
dedução/campo11.
|
||
</p>
|
||
<dl>
|
||
{secondhandReportRows(draft).map(
|
||
([, label, value, origin], index) => (
|
||
<div key={index}>
|
||
<dt className="font-semibold">{label}</dt>
|
||
<dd>
|
||
{value}
|
||
<small className="block">
|
||
{origin}
|
||
</small>
|
||
</dd>
|
||
</div>
|
||
),
|
||
)}
|
||
</dl>
|
||
</>
|
||
)}
|
||
</details>
|
||
);
|
||
}
|