Prepara proposta navegável de recomendação antes da adjudicação
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user