Prepara proposta navegável de recomendação antes da adjudicação

This commit is contained in:
2026-10-28 20:35:17 +02:00
parent d020be53ed
commit 29cd859385
7 changed files with 821 additions and 13 deletions
+13 -8
View File
@@ -3,10 +3,13 @@
Evolução do roteiro: a visão única foi entregue em `76d2463`. O incremento de
planeamento acrescenta itens por empresa, responsáveis, marcos e datas
previstas/realizadas, com ligação posterior às requisições. O registo documental
opcional da abertura e actas por versão também foi implementado. Qualificação
específica e avaliação em novas modalidades continuam por implementar. Estes
avanços não importam os processos históricos nem aprovam as regras de
cancelamento de ordens emitidas.
opcional da abertura e actas por versão também foi implementado. Seguiram-se
qualificação documental, relatório de conferência e admissão/exclusão por
adopção expressa (`d020be5`). A recomendação formal está proposta para validação
em `docs/proposta-recomendacao.md`, com demonstração em
`/proposta/recomendacao`; não está activa na base de negócio. Avaliação em
novas modalidades continua por implementar. Estes avanços não importam os
processos históricos nem aprovam as regras de cancelamento de ordens emitidas.
Análise de 28 de Outubro de 2026. Os quatro ficheiros indicados pelo responsável
são **exemplos**, conforme instrução expressa. Não aprovam regras do SGC,
@@ -68,10 +71,12 @@ preço avaliado não equivalem automaticamente à comparação ponderada actual.
Um eventual novo método exige definição explícita e testes, mantendo o
comportamento dos processos existentes.
O próximo incremento sugerido é uma visão do processo que reúna as etapas,
responsáveis, documentos e decisões já existentes, antes de acrescentar novas
modalidades ou alterar cálculos. Depois, incorporar planeamento e marcos e,
como incremento distinto, concursos e avaliação em etapas.
A visão do processo, o planeamento e os marcos, a abertura, a qualificação
documental e a admissão/exclusão já foram entregues como incrementos próprios.
O próximo circuito proposto é a recomendação formal antes da adjudicação,
sujeita à validação de âmbito e sem alterar a comparação ponderada existente.
Novas modalidades, métodos de classificação, correcções de preços e dispensas
continuam a exigir definição explícita.
## Aspectos que permanecem configuráveis ou por validar
+136
View File
@@ -0,0 +1,136 @@
# Proposta de âmbito para recomendação antes da adjudicação
Estado: proposta para validação após a entrega de admissão/exclusão em
`d020be5`. A demonstração `/proposta/recomendacao` não implementa uma nova
decisão na base de negócio. Data: 28 de Outubro de 2026.
## Resultado preparado para revisão
Uma proposta navegável, com cabeçalho, rodapé e menus partilhados do Sistema
de Gestão de Compras (SGC), permite simular:
- Política própria com perfis e quantidade de pessoas distintas, inicialmente
por definir, e adopção expressa de uma versão.
- Matriz pronta, empate na maior pontuação, avaliações insuficientes e
admissão pendente, todos com concorrentes e pontuações fictícios.
- Escolha manual de Compras, fundamento, conferências independentes,
devolução, novas tentativas e conservação do histórico durante a simulação.
- Mudança de base ou de política adoptada, invalidando o uso de uma tentativa
anterior sem apagar as suas conferências.
- Impressão do relatório demonstrativo, identificado como simulação.
Os dados desaparecem ao recarregar. Os três conferentes fictícios disponíveis
não definem um limite nem um quórum para empresas. Trocar de conferente na
página não autentica outra pessoa e não demonstra segregação efectiva. As
pontuações ilustram decisões de interface; não constituem um novo algoritmo
de comparação. Não se reutilizam nomes ou valores dos exemplos históricos.
## Circuito inicial proposto para o produto
1. Administração da empresa activa uma política de recomendação versionada,
com fundamento, perfis autorizados e quantidade de pessoas distintas.
A política é própria desta etapa, separada da política de admissão. Não se
presume que a autorização de um conferente numa etapa valha noutra.
2. Compras adopta expressamente a versão activa antes de adjudicar. Sem essa
adopção, mantém-se o circuito existente. Neste primeiro âmbito proposto,
adoptar a recomendação exige que o processo já tenha adoptado o regime
formal de admissão; não se adopta nenhum deles automaticamente. Uma nova
política geral não reescreve a versão preservada no processo. Desactivar
impede novas adopções e não elimina as existentes.
3. A recomendação exige matriz pronta e propostas elegíveis com admissão
confirmada na base actual. Preservam-se o mínimo existente de três
fornecedores distintos elegíveis, duas avaliações por proposta, cálculo
ponderado, verificações de conflitos, validade e coerência dos dados.
Não se introduz uma dispensa de propostas ou um novo método de avaliação.
4. Compras indica expressamente a proposta a recomendar e fundamenta a
escolha. Não pode recomendar em pedido do qual seja o solicitante. A
candidata deve pertencer ao conjunto de maior pontuação devolvido pelo
método existente. Em empate, a escolha entre as empatadas exige fundamento;
não há desempate automático nem escolha por ordem de registo. Escolher
abaixo da maior pontuação continua impedido, sem política de excepção.
5. A tentativa preserva a versão adoptada da política, requisitos e análises,
decisões de admissão e conferências relevantes, matriz, avaliações e
proposta escolhida. Identidades, datas e fundamentos são auditados. Uma
nova tentativa acrescenta uma revisão e substitui a anterior para uso
futuro, sem reescrever ou copiar confirmações.
6. Conferem Finanças e/ou chefia do departamento, conforme os perfis e a
quantidade definidos pela empresa. Cada pessoa conta uma vez; deve ser
da mesma empresa, continuar elegível e não ter conflito, nem ser
solicitante ou proponente. A chefia deve pertencer ao departamento do
processo. Não se exige, neste âmbito, que um conferente seja diferente de
todos os intervenientes de outras etapas, mas mantêm-se as segregações e
impedimentos próprios de cada operação. Não se atribuem poderes a júris
ou pessoas externas sem um circuito específico aprovado.
7. As confirmações exigidas concluem a recomendação. Uma devolução encerra
aquela tentativa e exige outra proposta fundamentada; não exclui o
concorrente, cancela o processo ou desfaz uma admissão. Repetir a mesma
conferência é idempotente. Uma resposta diferente não reescreve a anterior.
As confirmações anteriores são revalidadas até à conclusão do quórum;
depois preserva-se a elegibilidade verificada na sua data. Alterações de
base ou de adopção exigem reapreciação e novas conferências.
8. Num processo que adoptou o regime, a adjudicação só pode seleccionar a
mesma proposta de uma recomendação confirmada e actual, preservando a
referência e a base. A coerência volta a ser verificada antes de aprovar
e emitir. Continuam a aplicar-se orçamento, investimento, elegibilidade,
conflitos e aprovações do valor final. Recomendação não autoriza compra.
9. Após existir adjudicação ou ordem, não se alteram a adopção ou as decisões
desta etapa. Uma base alterada antes da emissão exige o circuito de
revisão/cancelamento de adjudicação sem ordem já suportado, quando
autorizado pela política da empresa. Não há desbloqueio por omissão.
Ordens emitidas e processos anteriores não são reescritos.
10. O relatório autenticado e a sua impressão mostram base, candidata,
fundamento, conferências, devoluções e histórico, com permissões de
empresa/departamento. A impressão não representa assinatura digital,
certificação das evidências ou comunicação ao concorrente.
Estas escolhas são o âmbito proposto do produto e não regras legais ou
obrigações universais inferidas dos documentos locais. Os valores, perfis e
decisões empresariais continuam a pertencer a cada aderente.
## Fora do âmbito
- Novos métodos de pontuação, correcções de preços, ajustes, multimoeda,
escolha abaixo da maior pontuação ou dispensa de requisitos/propostas.
- Júris externos, recursos, prazos legais presumidos, assinatura digital,
publicação, envio de emails ou comunicação ao concorrente.
- Alterar reservas, compromissos, facturas ou pagamentos pela recomendação,
cancelar ordens emitidas, estornar ou aplicar retroactivamente o circuito.
- Tornar a recomendação obrigatória em todos os processos ou activar
políticas reais por conta das empresas durante o desenvolvimento.
## Verificações exigidas para a futura implementação
Isolamento e privacidade, identidade relida no servidor, autoria, segregação,
perfil/departamento revogado, quórum sem duplicações, conflitos, matriz
incompleta, admissão pendente/desactualizada, empate, escolha fora da maior
pontuação, bases alteradas, devolução e correcções imutáveis, idempotência,
rollback de auditoria, vínculo à adjudicação/emissão e concorrência
PostgreSQL com processos independentes. Uma demonstração local não substitui
estas verificações do circuito autenticado.
## Validação necessária antes de implementar o circuito real
Validar **recomendação manual por Compras, conferências independentes em
quantidade/perfis definidos pela empresa, regime opcional por adopção
expressa após admissão e vínculo à adjudicação da proposta recomendada**.
A orientação de continuidade determina: «definir com a empresa o próximo
âmbito pendente antes de o implementar», em
`docs/orientacoes-continuidade.md`. A validação anterior autorizou admissão
e exclusão; não definiu uma etapa de recomendação com estes efeitos. A
implementação do circuito real aguarda a validação deste âmbito. As
alterações locais dos três documentos de continuidade ficam preservadas.
## Verificações desta preparação
Passaram os 18 testes de entrada e consulta pública, com 290 asserções,
PHPStan, Pint, TypeScript, build e lint/formatação do âmbito. O acesso HTTP
devolveu 200 com componente `preview/recommendation`, identidade pública nula
e sem propriedades de negócio. O título inicial usa Sistema de Gestão de
Compras (SGC). Não houve migração, importação ou publicação de política real.
A bateria completa dos módulos de negócio não foi repetida nesta preparação.
O navegador devolveu `The browser access host is unavailable`. As interacções
e a impressão estão preparadas para revisão, sem revisão visual concluída.
A formatação global de `docs/compatibilidade.md`, alteração local alheia,
permanece pendente e não foi alterada.
+36
View File
@@ -1,5 +1,41 @@
# Validação técnica do SGC — 28 de Outubro de 2026
## Preparação: proposta de recomendação formal
Após `d020be5`, foi preparada `/proposta/recomendacao`, ligada ao menu
**Compras** apenas na consulta demonstrativa. Usa a navegação, cabeçalho e
rodapé partilhados do Sistema de Gestão de Compras (SGC), com dados fictícios
locais. Permite experimentar configuração de perfis/quantidade sem valores
iniciais, adopção explícita, matriz pronta/impedida, empate, escolha manual,
fundamento, conferências, devolução e histórico. A alteração de base ou de
política adoptada torna a tentativa anterior histórica. A impressão é
identificada como simulação. Não há persistência no navegador ou no servidor.
O circuito real proposto está em `docs/proposta-recomendacao.md`. A sua
implementação aguarda validação: política própria por empresa, adopção
opcional após regime de admissão, conferência independente e vínculo da
adjudicação à recomendação confirmada na mesma base. Não se publicaram
políticas, acrescentaram migrações ou alteraram regras de adjudicação,
orçamento, facturação ou pagamento. Os exemplos históricos não fornecem
parâmetros obrigatórios às aderentes.
Passaram **18 testes de entrada e consulta pública, com 290 asserções**,
incluindo as demonstrações de qualificação/admissão/recomendação, identidade
pública nula sem terminar a sessão existente e ausência de endpoint público
de escrita. PHPStan, Pint, TypeScript, build e lint/formatação do âmbito passaram.
Confirmado HTTP 200 com componente `preview/recommendation`, utilizador nulo,
apenas propriedades gerais da consulta pública e título inicial Sistema de
Gestão de Compras (SGC).
A bateria completa do negócio não foi repetida neste incremento de
interface; os resultados de 478 aprovados e 2 ignorados abaixo pertencem à
entrega anterior de admissão. As alterações locais dos três documentos de
continuidade foram preservadas; a formatação global de `docs/compatibilidade.md`
continua fora do âmbito desta entrega.
O controlo do navegador voltou a devolver `The browser access host is
unavailable`. A revisão visual e de interacção, incluindo impressão, continua
pendente e não é substituída pelos testes automatizados.
## Incremento: admissão e exclusão por adopção expressa
O utilizador validou `docs/proposta-admissao-exclusao.md`. A Administração da
+6
View File
@@ -41,6 +41,12 @@ const groups: { label: string; entries: Entry[] }[] = [
demo: '/consulta/ordens',
roles: readers,
},
{
label: 'Recomendação (proposta)',
path: '',
demo: '/proposta/recomendacao',
roles: [],
},
],
},
{
@@ -0,0 +1,621 @@
import { Head, Link } from '@inertiajs/react';
import { Printer } from 'lucide-react';
import { useState } from 'react';
import { controlClass, PageTitle, Panel } from '@/components/procurement';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
type Scenario = 'ready' | 'tie' | 'incomplete' | 'admission';
type Policy = { revision: number; roles: string[]; required: number };
type Review = {
actor: number;
verdict: 'confirmed' | 'returned';
reason: string;
};
type Attempt = {
revision: number;
basis: number;
scenario: Scenario;
candidate: string;
reason: string;
policy: Policy;
reviews: Review[];
};
const reviewers = [
{ id: 1, name: 'Conferente fictício de Finanças A', role: 'finance' },
{ id: 2, name: 'Conferente fictício de Finanças B', role: 'finance' },
{ id: 3, name: 'Chefia fictícia do departamento', role: 'department_head' },
];
const roleNames: Record<string, string> = {
finance: 'Finanças',
department_head: 'Chefia do departamento',
};
const scenarios: Record<Scenario, string> = {
ready: 'Matriz completa e admissões confirmadas',
tie: 'Empate na maior pontuação',
incomplete: 'Avaliações insuficientes',
admission: 'Admissão por confirmar',
};
const candidates = [
'Concorrente fictício A',
'Concorrente fictício B',
'Concorrente fictício C',
];
function status(attempt: Attempt) {
if (attempt.reviews.some((r) => r.verdict === 'returned'))
return 'Tentativa devolvida';
return attempt.reviews.length >= attempt.policy.required
? 'Recomendação confirmada na simulação'
: 'A aguardar conferência independente';
}
function Reason({
id,
value,
onChange,
}: {
id: string;
value: string;
onChange: (value: string) => void;
}) {
return (
<div className="space-y-2">
<Label htmlFor={id}>Fundamento</Label>
<textarea
id={id}
className="min-h-24 w-full rounded-lg border p-3 text-sm"
value={value}
onChange={(e) => onChange(e.target.value)}
minLength={10}
maxLength={4000}
/>
</div>
);
}
export default function RecommendationProposal() {
const [roles, setRoles] = useState<string[]>([]);
const [required, setRequired] = useState('');
const [enabled, setEnabled] = useState(false);
const [adoption, setAdoption] = useState<Policy | null>(null);
const [scenario, setScenario] = useState<Scenario>('ready');
const [basis, setBasis] = useState(1);
const [candidate, setCandidate] = useState('');
const [reason, setReason] = useState('');
const [considered, setConsidered] = useState(false);
const [attempt, setAttempt] = useState<Attempt | null>(null);
const [history, setHistory] = useState<Attempt[]>([]);
const [reviewReasons, setReviewReasons] = useState<Record<number, string>>(
{},
);
const eligible = reviewers.filter((r) => roles.includes(r.role));
const configured =
enabled &&
roles.length > 0 &&
Number.isInteger(Number(required)) &&
Number(required) > 0 &&
Number(required) <= eligible.length;
const ready = scenario === 'ready' || scenario === 'tie';
const top =
scenario === 'tie' ? candidates.slice(0, 2) : candidates.slice(0, 1);
const fresh =
attempt !== null &&
attempt.basis === basis &&
attempt.policy.revision === adoption?.revision;
const final =
attempt !== null &&
(attempt.reviews.some((r) => r.verdict === 'returned') ||
attempt.reviews.length >= attempt.policy.required);
const canPropose =
adoption !== null &&
ready &&
top.includes(candidate) &&
considered &&
reason.trim().length >= 10 &&
(!attempt || !fresh || final);
const usable =
fresh &&
final &&
attempt !== null &&
!attempt.reviews.some((r) => r.verdict === 'returned');
function changeBasis(nextScenario: Scenario) {
setScenario(nextScenario);
setBasis(basis + 1);
setConsidered(false);
}
function propose() {
if (!canPropose || !adoption) return;
if (attempt) setHistory([...history, structuredClone(attempt)]);
setAttempt({
revision: (attempt?.revision ?? 0) + 1,
basis,
scenario,
candidate,
reason: reason.trim(),
policy: structuredClone(adoption),
reviews: [],
});
setReviewReasons({});
setReason('');
setConsidered(false);
}
function review(actor: number, verdict: Review['verdict']) {
if (
!attempt ||
!fresh ||
final ||
attempt.reviews.some((r) => r.actor === actor) ||
!reviewers.some(
(r) => r.id === actor && attempt.policy.roles.includes(r.role),
) ||
(reviewReasons[actor] ?? '').trim().length < 10
)
return;
setAttempt({
...attempt,
reviews: [
...attempt.reviews,
{ actor, verdict, reason: reviewReasons[actor].trim() },
],
});
}
return (
<>
<Head title="Proposta de recomendação formal" />
<div className="mx-auto flex max-w-7xl flex-col gap-6 p-5 lg:p-8 print:p-0">
<PageTitle
eyebrow="Proposta para validação"
title="Recomendação antes da adjudicação"
description="Sistema de Gestão de Compras (SGC) · Percurso proposto por empresa, com decisão manual e conferência independente."
/>
<p className="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950">
Demonstração com dados fictícios. As acções apenas simulam o
circuito nesta página e desaparecem ao recarregar. Trocar de
conferente não autentica pessoas diferentes. O relatório
impresso é uma simulação e não autoriza compras.
</p>
<nav className="flex flex-wrap gap-4 text-sm underline print:hidden">
<Link href="/proposta/principal">Ecrã principal</Link>
<Link href="/proposta/admissao">Admissão de propostas</Link>
<Link href="/consulta/cotacoes">
Comparação demonstrativa
</Link>
</nav>
<div className="print:hidden">
<Panel
title="1. Política e adopção expressa"
description="A política de recomendação seria própria, separada da política de admissão. A Administração configuraria a empresa; Compras adoptaria a versão antes de adjudicar."
>
<div className="grid gap-5 md:grid-cols-2">
<fieldset className="space-y-3">
<legend className="mb-3 text-sm font-medium">
Perfis autorizados a conferir
</legend>
{Object.entries(roleNames).map(
([role, name]) => (
<Label
className="flex items-center gap-2"
key={role}
>
<input
type="checkbox"
checked={roles.includes(role)}
onChange={(e) =>
setRoles(
e.target.checked
? [...roles, role]
: roles.filter(
(r) =>
r !==
role,
),
)
}
/>
{name}
</Label>
),
)}
</fieldset>
<div className="space-y-3">
<Label htmlFor="recommendation-quorum">
Quantidade de pessoas distintas
</Label>
<input
id="recommendation-quorum"
className={controlClass}
type="number"
min={1}
value={required}
onChange={(e) =>
setRequired(e.target.value)
}
/>
<Label className="flex items-center gap-2">
<input
type="checkbox"
checked={enabled}
onChange={(e) =>
setEnabled(e.target.checked)
}
/>
Activar para novas adopções
</Label>
</div>
</div>
<p className="my-4 text-sm text-muted-foreground">
Há três conferentes fictícios nesta demonstração,
distintos do solicitante e do proponente de Compras.
A quantidade disponível no exemplo não é um limite
nem um quórum do produto. Perfis e quantidade
começam por definir.
</p>
<Button
disabled={!configured}
onClick={() => {
setAdoption({
revision: (adoption?.revision ?? 0) + 1,
roles: [...roles],
required: Number(required),
});
setConsidered(false);
}}
>
Simular configuração e adopção de{' '}
{adoption ? 'nova versão' : 'política'}
</Button>
<p role="status" className="mt-4 text-sm">
{adoption
? `Versão adoptada ${adoption.revision}: ${adoption.required} pessoa(s), ${adoption.roles.map((r) => roleNames[r]).join(', ')}. Alterar os campos acima não reescreve esta versão; uma nova adopção exige novas conferências.`
: 'Sem adopção: o circuito existente da aplicação mantém-se.'}
</p>
</Panel>
</div>
<div className="print:hidden">
<Panel
title="2. Base apreciada"
description="O primeiro circuito proposto exige o regime de admissão já adoptado, admissões confirmadas e matriz pronta. Não altera o método de pontuação existente."
>
<div className="flex flex-wrap items-end gap-4">
<div className="min-w-64 flex-1 space-y-2">
<Label htmlFor="recommendation-scenario">
Cenário fictício
</Label>
<select
id="recommendation-scenario"
className={controlClass}
value={scenario}
onChange={(e) =>
changeBasis(e.target.value as Scenario)
}
>
{Object.entries(scenarios).map(
([value, label]) => (
<option key={value} value={value}>
{label}
</option>
),
)}
</select>
</div>
<Button
variant="outline"
onClick={() => changeBasis(scenario)}
>
Simular alteração da base
</Button>
</div>
<p className="my-4 text-sm">
Base demonstrativa {basis} ·{' '}
{ready
? 'Pronta para propor recomendação'
: 'Impedida: resolva a avaliação ou a admissão antes de recomendar'}
. Os três concorrentes são fornecedores distintos;
no cenário pronto têm duas avaliações por proposta.
</p>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<caption className="mb-3 text-left text-xs text-muted-foreground">
Pontuações ilustrativas, sem recálculo de
negócio ou importação dos exemplos
históricos.
</caption>
<thead className="border-b">
<tr>
<th className="p-2">Concorrente</th>
<th className="p-2">
Pontuação ilustrativa
</th>
<th className="p-2">Admissão</th>
<th className="p-2">Situação</th>
</tr>
</thead>
<tbody>
{candidates.map((name, index) => (
<tr key={name} className="border-b">
<td className="p-2">{name}</td>
<td className="p-2">
{scenario === 'incomplete'
? 'Por concluir'
: index === 0 ||
(index === 1 &&
scenario === 'tie')
? '8,80'
: index === 1
? '8,40'
: '7,90'}
</td>
<td className="p-2">
{scenario === 'admission' &&
index === 0
? 'Por confirmar'
: 'Confirmada na simulação'}
</td>
<td className="p-2">
{ready && top.includes(name)
? 'Maior pontuação'
: ready
? 'Fora da maior pontuação'
: 'Comparação impedida'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Panel>
</div>
<div className="print:hidden">
<Panel
title="3. Proposta manual por Compras"
description="Escolha expressa entre as propostas de maior pontuação, com fundamento também em caso de empate. Esta etapa não cria adjudicação."
>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="recommendation-candidate">
Proposta a recomendar
</Label>
<select
id="recommendation-candidate"
className={controlClass}
value={candidate}
onChange={(e) =>
setCandidate(e.target.value)
}
>
<option value="">
Seleccione expressamente
</option>
{candidates.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
</div>
{candidate && ready && !top.includes(candidate) && (
<p
role="alert"
className="text-sm text-destructive"
>
A escolha está fora da maior pontuação. Este
primeiro circuito não admite essa excepção.
</p>
)}
<Reason
id="recommendation-reason"
value={reason}
onChange={setReason}
/>
<Label className="flex items-center gap-2">
<input
type="checkbox"
checked={considered}
onChange={(e) =>
setConsidered(e.target.checked)
}
/>
Conferi a matriz, admissões e fundamento desta
base fictícia.
</Label>
<Button disabled={!canPropose} onClick={propose}>
Simular nova tentativa de recomendação
</Button>
{attempt && fresh && !final && (
<p className="text-sm text-muted-foreground">
A tentativa actual aguarda conferência. Pode
ser devolvida; uma alteração da base exige
outra tentativa.
</p>
)}
</div>
</Panel>
</div>
{attempt && (
<Panel
title={`4. Relatório demonstrativo · Tentativa ${attempt.revision}`}
description="O relatório preservaria a base, a proposta fundamentada e cada conferência. Confirmar recomendação não substitui aprovações da adjudicação ou controlo orçamental."
>
<div className="space-y-3">
<p className="font-semibold">{attempt.candidate}</p>
<p className="text-sm whitespace-pre-wrap">
{attempt.reason}
</p>
<p className="text-sm">
Base {attempt.basis} ·{' '}
{scenarios[attempt.scenario]} · política{' '}
{attempt.policy.revision} ·{' '}
{attempt.policy.required} pessoa(s) distinta(s).
</p>
<p
role="status"
className="rounded-lg bg-muted p-3 text-sm"
>
{status(attempt)}
{!fresh &&
' · Histórica: a base ou a política adoptada mudou. Não utilizável para adjudicar.'}
</p>
{attempt.reviews.map((r) => (
<article
key={r.actor}
className="rounded-lg border p-3 text-sm"
>
<p className="font-medium">
{
reviewers.find(
(a) => a.id === r.actor,
)?.name
}{' '}
·{' '}
{r.verdict === 'confirmed'
? 'Confirmou'
: 'Devolveu'}
</p>
<p className="whitespace-pre-wrap">
{r.reason}
</p>
</article>
))}
{fresh && !final && (
<div className="space-y-4 print:hidden">
{reviewers
.filter(
(r) =>
attempt.policy.roles.includes(
r.role,
) &&
!attempt.reviews.some(
(entry) =>
entry.actor === r.id,
),
)
.map((r) => (
<article
key={r.id}
className="space-y-3 rounded-lg border p-4"
>
<h3 className="text-sm font-semibold">
{r.name}
</h3>
<Reason
id={`recommendation-review-${r.id}`}
value={
reviewReasons[r.id] ??
''
}
onChange={(value) =>
setReviewReasons({
...reviewReasons,
[r.id]: value,
})
}
/>
<div className="flex flex-wrap gap-3">
<Button
disabled={
(
reviewReasons[
r.id
] ?? ''
).trim().length < 10
}
onClick={() =>
review(
r.id,
'confirmed',
)
}
>
Simular confirmação
</Button>
<Button
variant="outline"
disabled={
(
reviewReasons[
r.id
] ?? ''
).trim().length < 10
}
onClick={() =>
review(
r.id,
'returned',
)
}
>
Simular devolução
</Button>
</div>
</article>
))}
</div>
)}
<p className="text-sm">
{usable
? 'Nesta simulação, uma futura adjudicação só poderia usar esta proposta e esta base, sujeita às restantes verificações e aprovações.'
: 'Esta tentativa não pode fundamentar a adjudicação na simulação.'}
</p>
<p className="text-sm text-muted-foreground">
Devolver a recomendação não exclui o concorrente
nem cancela o processo. Uma nova tentativa
começa sem confirmações herdadas.
</p>
<Button
variant="outline"
className="print:hidden"
onClick={() => window.print()}
>
<Printer className="mr-2 size-4" />
Imprimir simulação
</Button>
</div>
</Panel>
)}
{history.length > 0 && (
<Panel title="Tentativas anteriores nesta simulação">
<div className="space-y-4">
{history.toReversed().map((entry) => (
<details
key={entry.revision}
className="rounded-lg border p-3 text-sm"
>
<summary className="cursor-pointer font-medium">
Tentativa {entry.revision} ·{' '}
{entry.candidate} · {status(entry)} ·
histórica, sem autorização actual
</summary>
<p className="mt-3 whitespace-pre-wrap">
{entry.reason}
</p>
<p className="mt-2">
Base {entry.basis} · política{' '}
{entry.policy.revision} ·{' '}
{entry.reviews.length} conferência(s)
preservada(s).
</p>
{entry.reviews.map((r) => (
<p
className="mt-2 whitespace-pre-wrap"
key={r.actor}
>
{
reviewers.find(
(a) => a.id === r.actor,
)?.name
}{' '}
·{' '}
{r.verdict === 'confirmed'
? 'Confirmou'
: 'Devolveu'}{' '}
· {r.reason}
</p>
))}
</details>
))}
</div>
</Panel>
)}
</div>
</>
);
}
+1
View File
@@ -8,6 +8,7 @@ use Inertia\Inertia;
Route::inertia('proposta/principal', 'preview/principal', ['auth' => ['user' => null]])->name('preview.principal');
Route::inertia('proposta/qualificacao', 'preview/qualification', ['auth' => ['user' => null]])->name('preview.qualification');
Route::inertia('proposta/admissao', 'preview/admission', ['auth' => ['user' => null]])->name('preview.admission');
Route::inertia('proposta/recomendacao', 'preview/recommendation', ['auth' => ['user' => null]])->name('preview.recommendation');
Route::get('consulta', function (Request $request) {
$screen = $request->query('ecra', 'overview');
+8 -5
View File
@@ -3,17 +3,19 @@
use App\Models\User;
use Inertia\Testing\AssertableInertia as Assert;
test('visual consultation is accessible without authentication', function () {
$this->get('/consulta')->assertOk()->assertInertia(fn (Assert $page) => $page
->component('preview/index')
test('visual consultation is accessible without authentication', function (string $path, string $component) {
$this->get($path)->assertOk()->assertInertia(fn (Assert $page) => $page
->component($component)
->where('auth.user', null)
->missing('suppliers')->missing('requisitions')->missing('documents'));
->missing('suppliers')->missing('requisitions')->missing('documents')
->missing('adoptions')->missing('policies')->missing('recommendations'));
$this->assertGuest();
});
})->with([['/consulta', 'preview/index'], ['/proposta/qualificacao', 'preview/qualification'], ['/proposta/admissao', 'preview/admission'], ['/proposta/recomendacao', 'preview/recommendation']]);
test('preview does not expose authenticated account data or log it out', function () {
$user = User::factory()->create();
$this->actingAs($user)->get('/consulta')->assertOk()->assertInertia(fn (Assert $page) => $page->where('auth.user', null));
$this->get('/proposta/recomendacao')->assertOk()->assertInertia(fn (Assert $page) => $page->where('auth.user', null)->missing('recommendations')->missing('policies')->missing('documents'));
$this->assertAuthenticatedAs($user);
});
@@ -24,5 +26,6 @@ test('consultation creates no business access or mutation endpoint', function ()
}
$this->post('/fornecedores', ['name' => 'Tentativa pública'])->assertRedirect('/login');
$this->post('/consulta', [])->assertStatus(405);
$this->post('/proposta/recomendacao', [])->assertStatus(405);
expect(User::query()->count())->toBe(0);
});