739 lines
36 KiB
TypeScript
739 lines
36 KiB
TypeScript
import { inputText } from '../lib/input-text.ts';
|
|
import { SimulationOfferWorkflow } from './simulation-offer-workflow';
|
|
import { useState } from 'react';
|
|
import {
|
|
newOfferPreparation,
|
|
offerPreparationBook,
|
|
offerPreparationSource,
|
|
offerReusableData,
|
|
recordOfferPreparations,
|
|
reuseOfferData,
|
|
saveOfferPreparationBook,
|
|
emptyOfferRequest,
|
|
recordOfferRequest,
|
|
offerRequestLabels,
|
|
offerScenarios,
|
|
type OfferRequest,
|
|
type OfferPreparation,
|
|
type OfferPreparationBook,
|
|
} from '@/lib/iva-offer-preparations';
|
|
|
|
export function SimulationOfferPreparations({
|
|
draft,
|
|
onChange,
|
|
}: {
|
|
draft: Record<string, unknown>;
|
|
onChange: (field: string, value: string) => void;
|
|
}) {
|
|
const [error, setError] = useState('');
|
|
let book: OfferPreparationBook;
|
|
let reusable: ReturnType<typeof offerReusableData>;
|
|
try {
|
|
book = offerPreparationBook(draft);
|
|
reusable = offerReusableData(draft);
|
|
} catch (cause) {
|
|
return <p role="alert">{String(cause)}</p>;
|
|
}
|
|
const run = (action: () => void) => {
|
|
try {
|
|
action();
|
|
setError('');
|
|
} catch (cause) {
|
|
setError(String(cause));
|
|
}
|
|
};
|
|
const save = (next: OfferPreparationBook) =>
|
|
onChange('modelo_a_snapshot', saveOfferPreparationBook(draft, next));
|
|
const change = (id: string, patch: Partial<OfferPreparation>) =>
|
|
run(() =>
|
|
save({
|
|
...book,
|
|
rows: book.rows.map((row) =>
|
|
row.id === id ? { ...row, ...patch } : row,
|
|
),
|
|
}),
|
|
);
|
|
const question = (
|
|
row: OfferPreparation,
|
|
key:
|
|
| 'nationalInstitution'
|
|
| 'publicSocialInstitution'
|
|
| 'goodsSuitable'
|
|
| 'publicInterestUse'
|
|
| 'mitigation',
|
|
label: string,
|
|
) => (
|
|
<label className="block" key={key}>
|
|
{label}
|
|
<select
|
|
className="block rounded border p-2"
|
|
value={row[key]}
|
|
onChange={(e) => change(row.id, { [key]: e.target.value })}
|
|
>
|
|
<option value="">Por indicar</option>
|
|
<option value="yes">Sim</option>
|
|
<option value="no">Não</option>
|
|
</select>
|
|
</label>
|
|
);
|
|
return (
|
|
<details className="space-y-3 rounded border p-3">
|
|
<summary>
|
|
Ofertas a instituições e para calamidades — preparar rascunhos
|
|
</summary>
|
|
<p>
|
|
Prepare apenas as ofertas relevantes para a oportunidade{' '}
|
|
{inputText(draft.context_opportunity || 'entrada geral')}. Pode
|
|
guardar dados incompletos e reutilizar referências existentes. A
|
|
reutilização preenche apenas campos vazios; as suas edições
|
|
mantêm-se.
|
|
</p>
|
|
<p>
|
|
Procedimento operacional indicado pela Tempo:prepare o pedido
|
|
escrito,registe a apresentação externa,a decisão com despacho e
|
|
o formulário aplicável. Antes do deferimento,compare o cenário
|
|
normal com uma hipótese condicionada. Apresentar o pedido não
|
|
concede isenção. Estes cenários não alteram a dedução no
|
|
campo11; os restantes enquadramentos continuam disponíveis.
|
|
</p>
|
|
{error && <p role="alert">{error}</p>}
|
|
<button
|
|
type="button"
|
|
className="underline"
|
|
onClick={() =>
|
|
run(() =>
|
|
save({
|
|
...book,
|
|
rows: [
|
|
...book.rows,
|
|
newOfferPreparation(
|
|
crypto.randomUUID(),
|
|
'P084',
|
|
inputText(draft.currency ?? ''),
|
|
),
|
|
],
|
|
}),
|
|
)
|
|
}
|
|
>
|
|
Preparar oferta a instituição
|
|
</button>{' '}
|
|
<button
|
|
type="button"
|
|
className="underline"
|
|
onClick={() =>
|
|
run(() =>
|
|
save({
|
|
...book,
|
|
rows: [
|
|
...book.rows,
|
|
newOfferPreparation(
|
|
crypto.randomUUID(),
|
|
'P085',
|
|
inputText(draft.currency ?? ''),
|
|
),
|
|
],
|
|
}),
|
|
)
|
|
}
|
|
>
|
|
Preparar oferta para calamidade
|
|
</button>
|
|
{book.rows.map((row) => (
|
|
<details key={row.id} className="rounded border p-3">
|
|
<summary>
|
|
{row.branch} —{' '}
|
|
{row.description || 'Bens por identificar'}
|
|
</summary>
|
|
<p>
|
|
{row.branch === 'P084'
|
|
? 'Artigo14(2)(a): aquisição ou importação de bens para ofertas a instituições nacionais de interesse público e relevantes fins sociais.'
|
|
: 'Artigo14(2)(b): aquisição de bens para ofertas destinadas a atenuar efeitos de calamidades naturais.'}{' '}
|
|
{offerPreparationSource}
|
|
</p>
|
|
{row.branch === 'P084' && (
|
|
<label>
|
|
Operação
|
|
<select
|
|
className="block rounded border p-2"
|
|
value={row.operation}
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
operation: e.target
|
|
.value as OfferPreparation['operation'],
|
|
})
|
|
}
|
|
>
|
|
<option value="">Por indicar</option>
|
|
<option value="acquisition">Aquisição</option>
|
|
<option value="import">Importação</option>
|
|
</select>
|
|
</label>
|
|
)}
|
|
<label>
|
|
Reutilizar dados desta oportunidade
|
|
<select
|
|
className="block rounded border p-2"
|
|
value=""
|
|
onChange={(e) =>
|
|
run(() => {
|
|
const source = reusable.find(
|
|
(item) => item.key === e.target.value,
|
|
);
|
|
if (source)
|
|
save({
|
|
...book,
|
|
rows: book.rows.map((item) =>
|
|
item.id === row.id
|
|
? reuseOfferData(
|
|
item,
|
|
source,
|
|
)
|
|
: item,
|
|
),
|
|
});
|
|
})
|
|
}
|
|
>
|
|
<option value="">
|
|
Escolher referência existente
|
|
</option>
|
|
{reusable
|
|
.filter(
|
|
(item) =>
|
|
(row.branch === 'P084' ||
|
|
item.operation === 'acquisition') &&
|
|
(!row.operation ||
|
|
item.operation === row.operation),
|
|
)
|
|
.map((item) => (
|
|
<option key={item.key} value={item.key}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
{row.reusedFrom && (
|
|
<p>
|
|
Referência reutilizada: {row.reusedFrom.key}. A
|
|
origem foi conservada; a aquisição/importação
|
|
original não foi alterada.
|
|
</p>
|
|
)}
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
{(
|
|
[
|
|
'description',
|
|
'period',
|
|
'currency',
|
|
'amount',
|
|
'counterparty',
|
|
'documentRef',
|
|
'documentDate',
|
|
'beneficiary',
|
|
'purpose',
|
|
'evidence',
|
|
] as const
|
|
).map((key, index) => (
|
|
<label key={key}>
|
|
{
|
|
[
|
|
'Bens destinados à oferta',
|
|
'Período previsto / efectivo',
|
|
'Moeda',
|
|
'Valor de referência, sem cálculo fiscal',
|
|
'Fornecedor / importador',
|
|
'Documento / referência da previsão',
|
|
'Data do documento / prevista',
|
|
'Instituição / destinatário da oferta',
|
|
'Finalidade concreta da oferta',
|
|
'Fundamento factual / pressupostos',
|
|
][index]
|
|
}
|
|
<input
|
|
className="block rounded border p-2"
|
|
type={
|
|
key === 'period'
|
|
? 'month'
|
|
: key === 'documentDate'
|
|
? 'date'
|
|
: 'text'
|
|
}
|
|
value={row[key]}
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
[key]: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
))}
|
|
</div>
|
|
{row.branch === 'P084' ? (
|
|
<fieldset>
|
|
<legend>
|
|
Condições próprias da oferta à instituição
|
|
</legend>
|
|
{question(
|
|
row,
|
|
'nationalInstitution',
|
|
'A instituição beneficiária é nacional',
|
|
)}
|
|
{question(
|
|
row,
|
|
'publicSocialInstitution',
|
|
'A instituição tem interesse público e relevantes fins sociais',
|
|
)}
|
|
{question(
|
|
row,
|
|
'goodsSuitable',
|
|
'Os bens são inteiramente adequados à natureza da instituição',
|
|
)}
|
|
{question(
|
|
row,
|
|
'publicInterestUse',
|
|
'A instituição utilizará os bens em actividades de evidente interesse público',
|
|
)}
|
|
</fieldset>
|
|
) : (
|
|
<fieldset>
|
|
<legend>
|
|
Finalidade própria da oferta para calamidade
|
|
</legend>
|
|
<label>
|
|
Calamidade e contexto afectados
|
|
<input
|
|
className="block rounded border p-2"
|
|
value={row.disaster}
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
disaster: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Natureza da calamidade
|
|
<input
|
|
className="block rounded border p-2"
|
|
value={row.disasterKind}
|
|
placeholder="Cheias, tempestades, secas, ciclones, sismos ou outra de idêntica natureza"
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
disasterKind: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
{question(
|
|
row,
|
|
'mitigation',
|
|
'Os bens destinam-se a ofertas para atenuar os efeitos da calamidade identificada',
|
|
)}
|
|
</fieldset>
|
|
)}
|
|
{row.branch === 'P085' && (
|
|
<label>
|
|
Finalidade humanitária da resposta à emergência
|
|
<textarea
|
|
className="block w-full rounded border p-2"
|
|
value={row.humanitarianPurpose ?? ''}
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
humanitarianPurpose: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
<label>
|
|
Documentos disponíveis e respectivas referências
|
|
<textarea
|
|
className="block w-full rounded border p-2"
|
|
value={row.availableDocuments ?? ''}
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
availableDocuments: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
{!!row.documentRef && (
|
|
<button
|
|
type="button"
|
|
className="underline"
|
|
onClick={() => {
|
|
const reference = [
|
|
row.documentRef,
|
|
row.documentDate,
|
|
]
|
|
.filter(Boolean)
|
|
.join(', ');
|
|
if (
|
|
!(row.availableDocuments ?? '').includes(
|
|
reference,
|
|
)
|
|
)
|
|
change(row.id, {
|
|
availableDocuments: [
|
|
row.availableDocuments,
|
|
reference,
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n'),
|
|
});
|
|
}}
|
|
>
|
|
Juntar referência do documento já indicado
|
|
</button>
|
|
)}
|
|
{(() => {
|
|
const request = row.request ?? emptyOfferRequest();
|
|
const edit = (patch: Partial<OfferRequest>) =>
|
|
change(row.id, {
|
|
request: { ...request, ...patch },
|
|
});
|
|
return (
|
|
<fieldset className="rounded border p-3">
|
|
<legend>Pedido e âmbito da decisão</legend>
|
|
<p>
|
|
Registe o estado declarado e os elementos
|
|
disponíveis. Este registo não apresenta um
|
|
pedido à autoridade nem concede o benefício.
|
|
Conserve o âmbito exacto de cada decisão,
|
|
mesmo que cubra apenas parte dos bens ou
|
|
períodos.
|
|
</p>
|
|
<label>
|
|
Estado declarado do pedido
|
|
<select
|
|
className="block rounded border p-2"
|
|
value={request.state}
|
|
onChange={(e) =>
|
|
edit({
|
|
state: e.target
|
|
.value as OfferRequest['state'],
|
|
})
|
|
}
|
|
>
|
|
{Object.entries(offerRequestLabels).map(
|
|
([value, label]) => (
|
|
<option
|
|
key={value}
|
|
value={value}
|
|
>
|
|
{label}
|
|
</option>
|
|
),
|
|
)}
|
|
</select>
|
|
</label>
|
|
{(
|
|
[
|
|
'reference',
|
|
'presentedOn',
|
|
'decisionRef',
|
|
'decisionOn',
|
|
'goods',
|
|
'periodFrom',
|
|
'periodTo',
|
|
'conditions',
|
|
] as const
|
|
).map((key, index) => (
|
|
<label className="block" key={key}>
|
|
{
|
|
[
|
|
'Referência do pedido',
|
|
'Data de apresentação, se disponível',
|
|
'Documento / referência da decisão, se disponível',
|
|
'Data da decisão, se disponível',
|
|
'Bens abrangidos pela decisão',
|
|
'Início do período abrangido',
|
|
'Fim do período abrangido',
|
|
'Condições, limites ou fundamento da decisão',
|
|
][index]
|
|
}
|
|
<input
|
|
className="block rounded border p-2"
|
|
type={
|
|
key === 'presentedOn' ||
|
|
key === 'decisionOn'
|
|
? 'date'
|
|
: 'text'
|
|
}
|
|
placeholder={
|
|
key === 'periodFrom' ||
|
|
key === 'periodTo'
|
|
? 'AAAA-MM ou AAAA-MM-DD, conforme a decisão'
|
|
: undefined
|
|
}
|
|
value={request[key]}
|
|
onChange={(e) =>
|
|
edit({ [key]: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
))}
|
|
<label>
|
|
Operação abrangida pela decisão
|
|
<select
|
|
className="block rounded border p-2"
|
|
value={request.operation}
|
|
onChange={(e) =>
|
|
edit({
|
|
operation: e.target
|
|
.value as OfferRequest['operation'],
|
|
})
|
|
}
|
|
>
|
|
<option value="">Por indicar</option>
|
|
<option value="acquisition">
|
|
Aquisição
|
|
</option>
|
|
<option value="import">
|
|
Importação
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Âmbito de operações literal, se a decisão
|
|
abranger mais do que a opção indicada
|
|
<input
|
|
className="block rounded border p-2"
|
|
value={request.operationScope ?? ''}
|
|
onChange={(e) =>
|
|
edit({
|
|
operationScope: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Âmbito temporal literal, se não estiver
|
|
limitado pelas datas indicadas
|
|
<input
|
|
className="block rounded border p-2"
|
|
placeholder="Conservar, por exemplo, a indicação sem termo ou até revogação do documento"
|
|
value={request.periodScope ?? ''}
|
|
onChange={(e) =>
|
|
edit({
|
|
periodScope: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className="underline"
|
|
onClick={() =>
|
|
edit({
|
|
goods:
|
|
request.goods ||
|
|
row.description,
|
|
operation:
|
|
request.operation ||
|
|
row.operation,
|
|
periodFrom:
|
|
request.periodFrom ||
|
|
row.period,
|
|
periodTo:
|
|
request.periodTo || row.period,
|
|
})
|
|
}
|
|
>
|
|
Reutilizar contexto nos campos de âmbito
|
|
vazios
|
|
</button>{' '}
|
|
<button
|
|
type="button"
|
|
className="rounded border p-2"
|
|
onClick={() =>
|
|
run(() =>
|
|
save({
|
|
...book,
|
|
rows: book.rows.map((item) =>
|
|
item.id === row.id
|
|
? recordOfferRequest(
|
|
item,
|
|
)
|
|
: item,
|
|
),
|
|
}),
|
|
)
|
|
}
|
|
>
|
|
Registar estado declarado e âmbito
|
|
</button>
|
|
{(row.requestRecords ?? []).map(
|
|
(record, index) => (
|
|
<p key={index}>
|
|
{
|
|
offerRequestLabels[
|
|
record.request.state
|
|
]
|
|
}{' '}
|
|
— {record.recordedAt}; bens:{' '}
|
|
{record.request.goods ||
|
|
'por indicar'}
|
|
; operação:{' '}
|
|
{record.request.operationScope ||
|
|
record.request.operation ||
|
|
'por indicar'}
|
|
; período:{' '}
|
|
{record.request.periodScope ||
|
|
`${record.request.periodFrom || 'por indicar'} a ${record.request.periodTo || 'por indicar'}`}
|
|
; condições:{' '}
|
|
{record.request.conditions ||
|
|
'por indicar'}
|
|
; decisão:{' '}
|
|
{record.request.decisionRef ||
|
|
'por indicar'}
|
|
. Este âmbito não se estende
|
|
automaticamente à restante operação
|
|
ou oportunidade.
|
|
</p>
|
|
),
|
|
)}
|
|
</fieldset>
|
|
);
|
|
})()}
|
|
<SimulationOfferWorkflow
|
|
row={row}
|
|
draft={draft}
|
|
onChange={(patch) => change(row.id, patch)}
|
|
/>
|
|
<fieldset className="rounded border p-3">
|
|
<legend>
|
|
Cenário normal e cenário condicionado à isenção
|
|
</legend>
|
|
<label>
|
|
IVA no cenário normal, indicado em documento ou
|
|
pressuposto
|
|
<input
|
|
className="block rounded border p-2"
|
|
inputMode="decimal"
|
|
value={row.normalVat ?? ''}
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
normalVat: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Documento ou pressuposto do IVA normal
|
|
<input
|
|
className="block rounded border p-2"
|
|
value={row.normalVatSource ?? ''}
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
normalVatSource: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Assumir, apenas para comparar, a hipótese
|
|
condicionada de isenção
|
|
<select
|
|
className="block rounded border p-2"
|
|
value={row.conditionalAssumption ?? ''}
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
conditionalAssumption: e.target
|
|
.value as '' | 'yes' | 'no',
|
|
})
|
|
}
|
|
>
|
|
<option value="">Por indicar</option>
|
|
<option value="yes">
|
|
Sim, como hipótese condicionada
|
|
</option>
|
|
<option value="no">Não</option>
|
|
</select>
|
|
</label>
|
|
{(() => {
|
|
const scenarios = offerScenarios(row);
|
|
return (
|
|
<>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Cenário</th>
|
|
<th>IVA ({row.currency})</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>
|
|
Normal — valor indicado, sem
|
|
taxa presumida
|
|
</td>
|
|
<td>
|
|
{scenarios.normalVat ??
|
|
'Indeterminado'}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>
|
|
Condicionado à isenção —
|
|
hipótese, não concessão
|
|
</td>
|
|
<td>
|
|
{scenarios.conditionalVat ??
|
|
'Indeterminado'}
|
|
</td>
|
|
</tr>
|
|
{scenarios.applied && (
|
|
<tr>
|
|
<td>
|
|
Tratamento deferido
|
|
apenas no âmbito do
|
|
despacho
|
|
</td>
|
|
<td>
|
|
{scenarios.appliedVat}
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
<p>{scenarios.conditionalReason}</p>
|
|
<p>
|
|
O cenário condicionado não altera
|
|
imposto,dedução,totais ou o estado do
|
|
pedido. O tratamento deferido exige
|
|
despacho registado e correspondência
|
|
expressa desta linha ao âmbito;alterar
|
|
os dados exige nova conferência.
|
|
</p>
|
|
</>
|
|
);
|
|
})()}
|
|
</fieldset>
|
|
</details>
|
|
))}
|
|
{!!book.rows.length && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className="rounded border p-2"
|
|
onClick={() =>
|
|
run(() => save(recordOfferPreparations(book)))
|
|
}
|
|
>
|
|
Guardar versão dos rascunhos de ofertas
|
|
</button>
|
|
<p role="status">
|
|
{book.saved
|
|
? 'Versão de preparação conservada; alterações seguintes não substituem essa versão.'
|
|
: 'Preparação em curso; pode guardar mesmo com informação em falta.'}{' '}
|
|
Estado e âmbito do tratamento indicados em cada linha.{' '}
|
|
{book.history?.length ?? 0} versões anteriores.
|
|
</p>
|
|
</>
|
|
)}
|
|
</details>
|
|
);
|
|
}
|