From b7a52e9e00c598735d6561deb2a335b50100d406 Mon Sep 17 00:00:00 2001 From: nala Date: Wed, 28 Oct 2026 19:10:40 +0200 Subject: [PATCH] Prepara proposta interactiva de qualificacao por processo --- docs/proposta-qualificacao-propostas.md | 91 ++++ resources/js/pages/preview/qualification.tsx | 503 +++++++++++++++++++ routes/preview.php | 1 + 3 files changed, 595 insertions(+) create mode 100644 docs/proposta-qualificacao-propostas.md create mode 100644 resources/js/pages/preview/qualification.tsx diff --git a/docs/proposta-qualificacao-propostas.md b/docs/proposta-qualificacao-propostas.md new file mode 100644 index 0000000..c480fa4 --- /dev/null +++ b/docs/proposta-qualificacao-propostas.md @@ -0,0 +1,91 @@ +# Proposta de âmbito para qualificação das propostas + +Estado: demonstração para validação do produto. Não há implementação de +qualificação na base de negócio nem regras empresariais activadas. +Data: 28 de Outubro de 2026. + +## Demonstração disponível + +`/proposta/qualificacao` apresenta requisitos editáveis por categoria, +três concorrentes fictícios, resultados por requisito, referências de +evidência, fundamentos e um resumo para conferência. A simulação fixa os +requisitos ao iniciar a análise e conserva a versão anterior em memória +quando se prepara outra. Recarregar a página elimina as alterações locais. +Não há gravação, importação dos exemplos, carregamento de anexos ou alteração +de propostas, fornecedores, pontuações, adjudicações ou orçamento. + +Os dois requisitos iniciais são exemplos genéricos. Não se adoptaram +certificados, valores de facturação, capital, crédito ou prazos dos modelos +históricos como requisitos universais para empresas aderentes. + +## Primeiro incremento proposto para a aplicação + +1. Compras define os requisitos documentais do processo, com categoria, + descrição e indicação da obrigatoriedade segundo a regra desse processo. + Categorias: jurídica, económico-financeira, fiscal, técnica e administrativa. + A categoria não cria um limite financeiro nem uma exigência documental. + Para este primeiro âmbito, propõe-se definição por Compras e análise por + Compras, Finanças ou chefia do departamento autorizada, com analista distinto + do solicitante e sujeito aos conflitos existentes. Administração consulta. +2. Ao iniciar a análise, guardar a versão dos requisitos e a base das propostas + a que se aplicam. Correcções posteriores criam nova versão, mantendo + requisitos, resultados e fundamentos anteriores. Resultados antigos não + são apresentados como análise válida de uma base nova. +3. O avaliador identificado regista, por proposta e requisito, «por analisar», + «cumprido», «não cumprido» ou «não aplicável». Um resultado preenchido + exige fundamento; «cumprido» e «não cumprido» exigem também referência + de evidência. «Não aplicável» exige justificação e não constitui, por si, + dispensa de um requisito declarado obrigatório. + Registos de analistas diferentes não se substituem; correcções conservam + o autor e a versão anterior. Não há quórum ou consenso presumido. +4. O primeiro âmbito é documental: apresentar pendências, não conformidades + e resultados para conferência, sem admitir/excluir concorrentes ou decidir + a adjudicação automaticamente. Não substituir a comparação ponderada + actual por menor preço, preço corrigido ou preço avaliado. +5. Manter os perfis de consulta existentes para propostas, o isolamento por + empresa/departamento, a segregação do solicitante, os conflitos de interesses, + a auditoria e a revalidação dos operadores nas transacções. A implementação + deverá recusar bases ou versões desactualizadas e testar concorrência e + rollback. Nenhum resultado será uma certificação automática da evidência. + +Esta validação é do âmbito de produto, não da política de uma empresa real. +O circuito formal de admissão/exclusão, quem fixa/aprova requisitos, quem +analisa e quem confere, o número de intervenientes e os efeitos de cada +resultado deverão ser definidos explicitamente antes de activar esses efeitos. +Não se propõe presumir um júri ou quórum igual para todas as empresas. + +## Fora deste primeiro âmbito + +- Exclusão automática ou alteração dos critérios de elegibilidade usados + actualmente na adjudicação. +- Novos métodos de classificação, preços corrigidos/avaliados ou valores + financeiros importados dos exemplos históricos. +- Certificação de documentos, assinaturas verificadas ou contactos externos. +- Publicação de concurso, propostas seladas, prazos legais universais ou + presunção de que os critérios foram divulgados antes de receber propostas. +- Cancelamento de ordens emitidas, estornos, devoluções ou reversões financeiras. + +## Validação solicitada + +Validar o primeiro incremento documental: **requisitos e resultados por +processo, com evidências, fundamento e versões preservadas, sem exclusão ou +adjudicação automática**. A demonstração permite experimentar o preenchimento +antes dessa decisão. A implementação com dados de negócio fica pendente. + +A orientação de continuidade actual pede «definir com a empresa o próximo +âmbito pendente antes de o implementar» em `docs/orientacoes-continuidade.md`. +As alterações locais desse ficheiro e de `docs/compatibilidade.md` e +`docs/desenvolvimento.md` foram preservadas, sem as incluir nesta entrega. + +## Verificações da preparação + +A rota respondeu HTTP 200 com componente `preview/qualification`, utilizador +nulo e apenas propriedades gerais da consulta pública. Três testes existentes +de consulta pública passaram, com 39 asserções. TypeScript, build, Pint e +lint/formatação dos ficheiros desta proposta passaram. Não se repetiu a bateria +completa de negócio: não houve alterações nesses circuitos ou no esquema. + +A verificação global de formatação assinala `docs/compatibilidade.md`, que +contém alterações locais alheias a esta entrega; não foi corrigido nem incluído +no commit. O navegador devolveu `The browser access host is unavailable`. +Não se declara revisão visual nem teste das interacções no navegador concluído. diff --git a/resources/js/pages/preview/qualification.tsx b/resources/js/pages/preview/qualification.tsx new file mode 100644 index 0000000..2e976e6 --- /dev/null +++ b/resources/js/pages/preview/qualification.tsx @@ -0,0 +1,503 @@ +import { Head } from '@inertiajs/react'; +import { Plus, RotateCcw } from 'lucide-react'; +import { useState } from 'react'; +import { PageTitle, Panel } from '@/components/procurement'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; + +type Criterion = { + id: number; + label: string; + category: string; + required: boolean; +}; +type Finding = { + result: 'pending' | 'met' | 'not_met' | 'not_applicable'; + evidence: string; + reason: string; +}; +type Answers = Record; +type Version = { number: number; criteria: Criterion[]; answers: Answers }; +const suppliers = ['Concorrente A', 'Concorrente B', 'Concorrente C']; +const initialCriteria: Criterion[] = [ + { + id: 1, + label: 'Documento definido pela empresa para este processo', + category: 'administrative', + required: false, + }, + { + id: 2, + label: 'Conformidade com a especificação definida no processo', + category: 'technical', + required: false, + }, +]; +const categories: Record = { + legal: 'Jurídica', + financial: 'Económico-financeira', + fiscal: 'Fiscal', + technical: 'Técnica', + administrative: 'Administrativa', +}; +const results: Record = { + pending: 'Por analisar', + met: 'Cumprido', + not_met: 'Não cumprido', + not_applicable: 'Não aplicável', +}; +const emptyFinding: Finding = { result: 'pending', evidence: '', reason: '' }; +const key = (criterion: number, supplier: number) => `${criterion}-${supplier}`; +const complete = (finding: Finding) => + finding.result !== 'pending' && + finding.reason.trim().length >= 10 && + (finding.result === 'not_applicable' || + finding.evidence.trim().length >= 3); +const selectClass = 'h-10 w-full rounded-md border bg-background px-3 text-sm'; + +export default function QualificationProposal() { + const [criteria, setCriteria] = useState(initialCriteria); + const [answers, setAnswers] = useState({}); + const [started, setStarted] = useState(false); + const [version, setVersion] = useState(1); + const [history, setHistory] = useState([]); + const [nextId, setNextId] = useState(3); + const validCriteria = + criteria.length > 0 && + criteria.every((criterion) => criterion.label.trim().length >= 3) && + new Set( + criteria.map((criterion) => + criterion.label.trim().toLocaleLowerCase('pt-PT'), + ), + ).size === criteria.length; + const updateCriterion = (id: number, change: Partial) => + setCriteria( + criteria.map((criterion) => + criterion.id === id ? { ...criterion, ...change } : criterion, + ), + ); + const finding = (criterion: number, supplier: number) => + answers[key(criterion, supplier)] ?? emptyFinding; + const updateFinding = ( + criterion: number, + supplier: number, + change: Partial, + ) => + setAnswers({ + ...answers, + [key(criterion, supplier)]: { + ...finding(criterion, supplier), + ...change, + }, + }); + const revise = () => { + setHistory([ + ...history, + { + number: version, + criteria: criteria.map((criterion) => ({ ...criterion })), + answers: structuredClone(answers), + }, + ]); + setVersion(version + 1); + setStarted(false); + setAnswers({}); + }; + const reset = () => { + setCriteria(initialCriteria); + setAnswers({}); + setStarted(false); + setVersion(1); + setHistory([]); + setNextId(3); + }; + return ( + <> + +
+ + + Repor exemplos + + } + /> +
+ Esta demonstração usa concorrentes fictícios e não guarda + dados. Os dois requisitos iniciais são exemplos editáveis. + Nenhuma escolha aqui altera fornecedores, pontuações ou + adjudicações da aplicação. +
+ +
+ {criteria.map((criterion) => ( +
+ + + + {!started && ( + + )} +
+ ))} +
+
+ {started ? ( + + ) : ( + <> + + + + )} +
+ {!validCriteria && ( +

+ Defina pelo menos um requisito com um nome distinto + de três ou mais caracteres. +

+ )} +
+ + {!started ? ( +

+ Defina os requisitos e inicie a simulação para + preencher a análise. +

+ ) : ( +
+ {criteria.map((criterion) => ( +
+

+ {criterion.label} +

+

+ {categories[criterion.category]} ·{' '} + {criterion.required + ? 'Declarado obrigatório neste exemplo' + : 'Sem obrigatoriedade declarada'} +

+
+ {suppliers.map((supplier, index) => { + const answer = finding( + criterion.id, + index, + ); + return ( +
+

+ {supplier} +

+ + +