Implementa admissão formal por política e adopção do processo
This commit is contained in:
@@ -82,6 +82,12 @@ const groups: { label: string; entries: Entry[] }[] = [
|
||||
demo: '/consulta/investimento',
|
||||
roles: ['admin', 'finance', 'procurement'],
|
||||
},
|
||||
{
|
||||
label: 'Política de admissão',
|
||||
path: '/politica-admissao',
|
||||
demo: '/proposta/admissao',
|
||||
roles: ['admin'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
import { FormErrors, PageTitle, Panel } from '@/components/procurement';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { date, roleLabels } from '@/types/procurement';
|
||||
|
||||
type Policy = {
|
||||
id: number;
|
||||
revision: number;
|
||||
enabled: boolean;
|
||||
roles: string[];
|
||||
required_count: number;
|
||||
reason: string;
|
||||
created_at: string;
|
||||
};
|
||||
type Props = {
|
||||
latest: Policy | null;
|
||||
policies: {
|
||||
data: Policy[];
|
||||
prev_page_url: string | null;
|
||||
next_page_url: string | null;
|
||||
};
|
||||
};
|
||||
export default function AdmissionPolicy({ latest, policies }: Props) {
|
||||
return (
|
||||
<PolicyForm key={latest?.id ?? 0} latest={latest} policies={policies} />
|
||||
);
|
||||
}
|
||||
|
||||
function PolicyForm({ latest, policies }: Props) {
|
||||
const form = useForm({
|
||||
previous_id: latest?.id ?? 0,
|
||||
enabled: false,
|
||||
roles: [] as string[],
|
||||
required_count: '',
|
||||
reason: '',
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Head title="Política de admissão" />
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6 p-5 lg:p-8">
|
||||
<PageTitle
|
||||
eyebrow="Configuração da empresa"
|
||||
title="Política de admissão"
|
||||
description="Sistema de Gestão de Compras (SGC) · Configuração da empresa"
|
||||
/>
|
||||
<Panel title="Nova versão da política">
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Defina os perfis e a quantidade de pessoas distintas.
|
||||
Activar permite novas adopções expressas pelos
|
||||
processos; desactivar impede novas adopções. As versões
|
||||
já adoptadas mantêm-se. O limite técnico de 50
|
||||
confirmações não define uma regra da empresa.
|
||||
</p>
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post('/politica-admissao');
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<Label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.data.enabled}
|
||||
onChange={(e) =>
|
||||
form.setData('enabled', e.target.checked)
|
||||
}
|
||||
/>
|
||||
Activar para novas adopções
|
||||
</Label>
|
||||
<fieldset className="space-y-2">
|
||||
<legend className="mb-2 text-sm font-medium">
|
||||
Perfis autorizados a conferir
|
||||
</legend>
|
||||
{['finance', 'department_head'].map((role) => (
|
||||
<Label
|
||||
key={role}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.data.roles.includes(role)}
|
||||
onChange={(e) =>
|
||||
form.setData(
|
||||
'roles',
|
||||
e.target.checked
|
||||
? [...form.data.roles, role]
|
||||
: form.data.roles.filter(
|
||||
(r) => r !== role,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
{roleLabels[role] ?? role}
|
||||
{role === 'department_head' &&
|
||||
' do departamento do processo'}
|
||||
</Label>
|
||||
))}
|
||||
</fieldset>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="required-count">
|
||||
Quantidade de pessoas distintas exigida
|
||||
</Label>
|
||||
<Input
|
||||
id="required-count"
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
required
|
||||
value={form.data.required_count}
|
||||
onChange={(e) =>
|
||||
form.setData(
|
||||
'required_count',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="policy-reason">
|
||||
Fundamento desta versão
|
||||
</Label>
|
||||
<textarea
|
||||
id="policy-reason"
|
||||
className="min-h-24 w-full rounded-md border p-3"
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={4000}
|
||||
value={form.data.reason}
|
||||
onChange={(e) =>
|
||||
form.setData('reason', e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button disabled={form.processing}>
|
||||
Registar versão {latest ? latest.revision + 1 : 1}
|
||||
</Button>
|
||||
</form>
|
||||
</Panel>
|
||||
<Panel title="Versões preservadas">
|
||||
<div className="space-y-4">
|
||||
{policies.data.length === 0 && (
|
||||
<p>A empresa ainda não definiu uma política.</p>
|
||||
)}
|
||||
{policies.data.map((p) => (
|
||||
<article
|
||||
key={p.id}
|
||||
className="rounded-md border p-4"
|
||||
>
|
||||
<h2 className="font-semibold">
|
||||
Versão {p.revision} ·{' '}
|
||||
{p.enabled ? 'Activada' : 'Desactivada'} ·{' '}
|
||||
{date(p.created_at)}
|
||||
</h2>
|
||||
<p>
|
||||
{p.required_count} pessoa(s) ·{' '}
|
||||
{p.roles
|
||||
.map((r) => roleLabels[r] ?? r)
|
||||
.join(', ')}
|
||||
</p>
|
||||
<p className="text-sm whitespace-pre-wrap">
|
||||
{p.reason}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
<div className="flex gap-4">
|
||||
{policies.prev_page_url && (
|
||||
<Link href={policies.prev_page_url}>
|
||||
Anteriores
|
||||
</Link>
|
||||
)}
|
||||
{policies.next_page_url && (
|
||||
<Link href={policies.next_page_url}>
|
||||
Seguintes
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import { Head, Link, router, useForm } from '@inertiajs/react';
|
||||
import { FormErrors, PageTitle, Panel } from '@/components/procurement';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { date, roleLabels } from '@/types/procurement';
|
||||
|
||||
type Adoption = {
|
||||
id: number;
|
||||
revision: number;
|
||||
admission_policy_id: number;
|
||||
reason: string;
|
||||
snapshot: {
|
||||
roles: string[];
|
||||
required_count: number;
|
||||
policy_revision: number;
|
||||
};
|
||||
};
|
||||
type Proposal = {
|
||||
id: number;
|
||||
reference: string;
|
||||
supplier: { name: string } | null;
|
||||
basisHash: string | null;
|
||||
clear: boolean;
|
||||
previousId: number;
|
||||
};
|
||||
type Review = {
|
||||
id: number;
|
||||
verdict: string;
|
||||
reason: string;
|
||||
created_at: string;
|
||||
snapshot: { name: string; purchasing_role: string };
|
||||
};
|
||||
type Decision = {
|
||||
id: number;
|
||||
revision: number;
|
||||
quotation_id: number;
|
||||
outcome: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
current: boolean;
|
||||
canReview: boolean;
|
||||
created_at: string;
|
||||
snapshot: { proponent: { name: string } };
|
||||
reviews: Review[];
|
||||
};
|
||||
type Props = {
|
||||
requisition: { id: number; number: string };
|
||||
policy: { id: number; revision: number; enabled: boolean } | null;
|
||||
adoption: Adoption | null;
|
||||
selected: Adoption | null;
|
||||
adoptions: Adoption[];
|
||||
proposals: Proposal[];
|
||||
history: {
|
||||
data: Decision[];
|
||||
prev_page_url: string | null;
|
||||
next_page_url: string | null;
|
||||
};
|
||||
canAdopt: boolean;
|
||||
canPropose: boolean;
|
||||
closed: boolean;
|
||||
};
|
||||
const selectClass = 'h-10 w-full rounded-md border bg-background px-3 text-sm';
|
||||
const outcomes: Record<string, string> = {
|
||||
admit: 'Admitir',
|
||||
exclude: 'Excluir',
|
||||
clarify: 'Solicitar esclarecimento interno',
|
||||
};
|
||||
const statuses: Record<string, string> = {
|
||||
pending: 'A aguardar conferência',
|
||||
confirmed: 'Decisão confirmada',
|
||||
returned: 'Tentativa devolvida',
|
||||
};
|
||||
function Reason({
|
||||
value,
|
||||
onChange,
|
||||
id,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
id: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={id}>Fundamento</Label>
|
||||
<textarea
|
||||
className="min-h-24 w-full rounded-md border p-3"
|
||||
id={id}
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={4000}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function Adopt({
|
||||
requisition,
|
||||
policy,
|
||||
adoption,
|
||||
}: Pick<Props, 'requisition' | 'policy' | 'adoption'>) {
|
||||
const form = useForm({
|
||||
policy_id: policy?.id ?? 0,
|
||||
previous_id: adoption?.id ?? 0,
|
||||
reason: '',
|
||||
});
|
||||
if (!policy?.enabled || adoption?.admission_policy_id === policy.id)
|
||||
return (
|
||||
<p className="text-sm">
|
||||
{!policy?.enabled
|
||||
? 'Sem política activa para nova adopção. A Administração configura a política da empresa.'
|
||||
: 'O processo já adoptou a versão activa.'}
|
||||
</p>
|
||||
);
|
||||
return (
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post(`/requisicoes/${requisition.id}/admissao/adoptar`);
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<p>
|
||||
Adoptar expressamente a política versão {policy.revision}. A
|
||||
adopção exige decisões e conferências próprias; não permite
|
||||
regressar ao circuito sem admissão.
|
||||
</p>
|
||||
<Reason
|
||||
id="adopt-reason"
|
||||
value={form.data.reason}
|
||||
onChange={(v) => form.setData('reason', v)}
|
||||
/>
|
||||
<Button disabled={form.processing}>
|
||||
Adoptar política neste processo
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
function Propose({ proposal, id }: { proposal: Proposal; id: number }) {
|
||||
const form = useForm({
|
||||
quotation_id: proposal.id,
|
||||
previous_id: proposal.previousId,
|
||||
basis_hash: proposal.basisHash ?? '',
|
||||
outcome: '',
|
||||
reason: '',
|
||||
});
|
||||
if (!proposal.basisHash)
|
||||
return (
|
||||
<p className="text-sm">
|
||||
Actualize a qualificação documental para poder propor uma
|
||||
decisão.
|
||||
</p>
|
||||
);
|
||||
return (
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post(`/requisicoes/${id}/admissao/decisoes`);
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`outcome-${proposal.id}`}>
|
||||
Proposta de decisão
|
||||
</Label>
|
||||
<select
|
||||
id={`outcome-${proposal.id}`}
|
||||
className={selectClass}
|
||||
required
|
||||
value={form.data.outcome}
|
||||
onChange={(e) => form.setData('outcome', e.target.value)}
|
||||
>
|
||||
<option value="">Seleccione</option>
|
||||
{Object.entries(outcomes).map(([value, label]) => (
|
||||
<option
|
||||
key={value}
|
||||
value={value}
|
||||
disabled={value === 'admit' && !proposal.clear}
|
||||
>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{!proposal.clear && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
A admissão está impedida: faltam análises, há
|
||||
pendências/divergências ou requisitos obrigatórios por
|
||||
resolver.
|
||||
</p>
|
||||
)}
|
||||
<Reason
|
||||
id={`propose-reason-${proposal.id}`}
|
||||
value={form.data.reason}
|
||||
onChange={(v) => form.setData('reason', v)}
|
||||
/>
|
||||
<Button disabled={form.processing}>Registar nova tentativa</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
function Confer({ decision }: { decision: Decision }) {
|
||||
const form = useForm({ verdict: '', reason: '' });
|
||||
return (
|
||||
<form
|
||||
className="mt-4 space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post(`/admissoes/${decision.id}/conferir`);
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<Label htmlFor={`review-${decision.id}`}>
|
||||
Conferência independente
|
||||
</Label>
|
||||
<select
|
||||
id={`review-${decision.id}`}
|
||||
required
|
||||
className={selectClass}
|
||||
value={form.data.verdict}
|
||||
onChange={(e) => form.setData('verdict', e.target.value)}
|
||||
>
|
||||
<option value="">Seleccione</option>
|
||||
<option value="approved">Confirmar a decisão proposta</option>
|
||||
<option value="returned">Devolver esta tentativa</option>
|
||||
</select>
|
||||
<Reason
|
||||
id={`review-reason-${decision.id}`}
|
||||
value={form.data.reason}
|
||||
onChange={(v) => form.setData('reason', v)}
|
||||
/>
|
||||
<Button disabled={form.processing}>Registar conferência</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
export default function Admission({
|
||||
requisition,
|
||||
policy,
|
||||
adoption,
|
||||
selected,
|
||||
adoptions,
|
||||
proposals,
|
||||
history,
|
||||
canAdopt,
|
||||
canPropose,
|
||||
closed,
|
||||
}: Props) {
|
||||
const base = `/requisicoes/${requisition.id}`;
|
||||
return (
|
||||
<>
|
||||
<Head title={`Admissão · ${requisition.number}`} />
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6 p-5 lg:p-8">
|
||||
<PageTitle
|
||||
eyebrow="Compras"
|
||||
title={`Admissão de propostas · ${requisition.number}`}
|
||||
description="Sistema de Gestão de Compras (SGC) · Decisões e conferências preservadas"
|
||||
/>
|
||||
<nav className="mb-5 flex flex-wrap gap-4 text-sm">
|
||||
<Link href={`${base}/processo`}>Processo completo</Link>
|
||||
<Link href={`${base}/qualificacao`}>
|
||||
Qualificação documental
|
||||
</Link>
|
||||
<Link href={`${base}/cotacoes`}>Comparação</Link>
|
||||
</nav>
|
||||
<Panel title="Regime do processo">
|
||||
<p className="mb-4">
|
||||
{adoption
|
||||
? `Adopção ${adoption.revision}: política ${adoption.snapshot.policy_revision}, ${adoption.snapshot.required_count} pessoa(s) distinta(s), ${adoption.snapshot.roles.map((r) => roleLabels[r] ?? r).join(', ')}.`
|
||||
: 'Sem adopção: mantém-se o circuito de comparação e adjudicação existente.'}
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Admissão não autoriza compra. Mantêm-se pontuação,
|
||||
conflitos, orçamento, aprovações e o mínimo existente de
|
||||
três fornecedores elegíveis com duas avaliações por
|
||||
proposta. Esclarecimentos são registos internos, sem
|
||||
envio de comunicações.
|
||||
</p>
|
||||
{closed && (
|
||||
<p>
|
||||
O processo está fechado para alterações de admissão.
|
||||
</p>
|
||||
)}
|
||||
{canAdopt && (
|
||||
<Adopt
|
||||
key={policy?.id}
|
||||
requisition={requisition}
|
||||
policy={policy}
|
||||
adoption={adoption}
|
||||
/>
|
||||
)}
|
||||
</Panel>
|
||||
{adoptions.length > 0 && (
|
||||
<Panel title="Versão consultada">
|
||||
<Label htmlFor="adoption-version">
|
||||
Histórico de adopções
|
||||
</Label>
|
||||
<select
|
||||
id="adoption-version"
|
||||
className={`${selectClass} mt-2`}
|
||||
value={selected?.id ?? ''}
|
||||
onChange={(e) =>
|
||||
router.get(`${base}/admissao`, {
|
||||
version: e.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
{adoptions.map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
Adopção {a.revision} · política{' '}
|
||||
{a.snapshot.policy_revision}
|
||||
{a.id === adoption?.id
|
||||
? ' · actual'
|
||||
: ' · histórica'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-3 text-sm whitespace-pre-wrap">
|
||||
{selected?.reason}
|
||||
</p>
|
||||
</Panel>
|
||||
)}
|
||||
{canPropose && (
|
||||
<Panel title="Propor decisões">
|
||||
<div className="space-y-5">
|
||||
{proposals.map((p) => (
|
||||
<article
|
||||
key={`${p.id}-${p.previousId}-${p.basisHash}`}
|
||||
className="rounded-md border p-4"
|
||||
>
|
||||
<h2 className="mb-4 font-semibold">
|
||||
{p.supplier?.name ?? 'Fornecedor'} ·{' '}
|
||||
{p.reference}
|
||||
</h2>
|
||||
<Propose proposal={p} id={requisition.id} />
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
<Panel title="Tentativas e conferências">
|
||||
<div className="space-y-5">
|
||||
{history.data.length === 0 && (
|
||||
<p>Ainda não há tentativas nesta adopção.</p>
|
||||
)}
|
||||
{history.data.map((d) => (
|
||||
<article
|
||||
key={d.id}
|
||||
className="rounded-md border p-4"
|
||||
>
|
||||
<h2 className="font-semibold">
|
||||
{proposals.find(
|
||||
(p) => p.id === d.quotation_id,
|
||||
)?.supplier?.name ??
|
||||
`Proposta ${d.quotation_id}`}{' '}
|
||||
· tentativa {d.revision} ·{' '}
|
||||
{outcomes[d.outcome]}
|
||||
</h2>
|
||||
<p className="text-sm">
|
||||
{statuses[d.status]}
|
||||
{!d.current &&
|
||||
' · histórica ou base desactualizada'}{' '}
|
||||
· {date(d.created_at)} ·{' '}
|
||||
{d.snapshot.proponent.name}
|
||||
</p>
|
||||
<p className="my-3 whitespace-pre-wrap">
|
||||
{d.reason}
|
||||
</p>
|
||||
{d.reviews.map((r) => (
|
||||
<div
|
||||
key={r.id}
|
||||
className="mt-2 rounded-md bg-muted p-3 text-sm"
|
||||
>
|
||||
<p>
|
||||
{r.snapshot.name} ·{' '}
|
||||
{roleLabels[
|
||||
r.snapshot.purchasing_role
|
||||
] ??
|
||||
r.snapshot.purchasing_role}{' '}
|
||||
·{' '}
|
||||
{r.verdict === 'approved'
|
||||
? 'Confirmou'
|
||||
: 'Devolveu'}{' '}
|
||||
· {date(r.created_at)}
|
||||
</p>
|
||||
<p className="whitespace-pre-wrap">
|
||||
{r.reason}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
{d.canReview && <Confer decision={d} />}
|
||||
</article>
|
||||
))}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Devolver impede a conclusão da tentativa e exige
|
||||
nova proposta fundamentada. Não exclui
|
||||
automaticamente o concorrente.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
{history.prev_page_url && (
|
||||
<Link href={history.prev_page_url}>
|
||||
Anteriores
|
||||
</Link>
|
||||
)}
|
||||
{history.next_page_url && (
|
||||
<Link href={history.next_page_url}>
|
||||
Seguintes
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -50,6 +50,9 @@ const labels: Record<string, string> = {
|
||||
const events: Record<string, string> = {
|
||||
'qualification.defined': 'Versão dos requisitos documentais registada',
|
||||
'qualification.assessed': 'Versão da análise documental registada',
|
||||
'admission.adopted': 'Política de admissão adoptada expressamente',
|
||||
'admission.proposed': 'Tentativa de decisão de admissão registada',
|
||||
'admission.reviewed': 'Conferência independente de admissão registada',
|
||||
'opening.recorded': 'Versão da acta de abertura registada',
|
||||
'requisition.created': 'Requisição criada',
|
||||
'requisition.updated': 'Requisição actualizada',
|
||||
|
||||
@@ -555,12 +555,20 @@ export default function QualificationPage(props: Props) {
|
||||
</p>
|
||||
)}
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950">
|
||||
Requisitos e resultados documentais para conferência. Não
|
||||
admitem ou excluem concorrentes, certificam evidências ou
|
||||
alteram a comparação ponderada e a adjudicação. Não
|
||||
aplicável exige fundamento e não dispensa automaticamente um
|
||||
requisito obrigatório.
|
||||
Os requisitos e resultados documentais apoiam a conferência;
|
||||
não constituem uma decisão formal nem certificam evidências.
|
||||
Num processo que adoptou o regime de admissão, estas versões
|
||||
integram a base da decisão e alterações exigem reapreciação.
|
||||
Não aplicável exige fundamento e não dispensa
|
||||
automaticamente um requisito obrigatório. As pontuações
|
||||
mantêm o cálculo ponderado existente.
|
||||
</div>
|
||||
<Link
|
||||
className="text-sm underline"
|
||||
href={`/requisicoes/${requisition.id}/admissao`}
|
||||
>
|
||||
Consultar regime, decisões e conferências de admissão
|
||||
</Link>
|
||||
<Link
|
||||
className="text-sm underline"
|
||||
href={`/requisicoes/${requisition.id}/cotacoes`}
|
||||
|
||||
@@ -54,6 +54,16 @@ type Report = {
|
||||
requester: string;
|
||||
};
|
||||
comparison: { ready: boolean; top_count: number; rows: ComparisonRow[] };
|
||||
admission: {
|
||||
id: number;
|
||||
revision: number;
|
||||
reason: string;
|
||||
snapshot: {
|
||||
policy_revision: number;
|
||||
required_count: number;
|
||||
roles: string[];
|
||||
};
|
||||
} | null;
|
||||
qualification: {
|
||||
id: number;
|
||||
revision: number;
|
||||
@@ -159,6 +169,17 @@ export default function ReviewReport({ report }: { report: Report }) {
|
||||
a qualificação documental são apresentadas separadamente.
|
||||
</div>
|
||||
<Panel title="Identificação do pedido">
|
||||
<p className="mb-4 text-sm">
|
||||
{report.admission
|
||||
? `Regime de admissão adoptado: política ${report.admission.snapshot.policy_revision}, ${report.admission.snapshot.required_count} confirmações independentes. A elegibilidade da comparação inclui admissão confirmada na base actual.`
|
||||
: 'Sem adopção do regime de admissão: mantém-se o circuito de comparação existente.'}{' '}
|
||||
<Link
|
||||
className="underline print:hidden"
|
||||
href={`/requisicoes/${r.id}/admissao`}
|
||||
>
|
||||
Consultar decisões e conferências de admissão
|
||||
</Link>
|
||||
</p>
|
||||
{r.status !== 'approved' && (
|
||||
<p className="mb-3 text-sm text-muted-foreground">
|
||||
O pedido não está em estado aprovado. As pontuações
|
||||
@@ -282,10 +303,9 @@ export default function ReviewReport({ report }: { report: Report }) {
|
||||
</ul>
|
||||
) : (
|
||||
<p>
|
||||
Sem impedimentos
|
||||
assinalados por esta
|
||||
matriz. Não equivale
|
||||
a admissão formal.
|
||||
{report.admission
|
||||
? 'Sem impedimentos nesta matriz, incluindo admissão confirmada na base actual. Não autoriza compra.'
|
||||
: 'Sem impedimentos assinalados por esta matriz. Não equivale a admissão formal.'}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -122,6 +122,12 @@ export default function Comparison({
|
||||
>
|
||||
Qualificação documental
|
||||
</Link>
|
||||
<Link
|
||||
className="text-sm font-medium underline"
|
||||
href={`/requisicoes/${requisition.id}/admissao`}
|
||||
>
|
||||
Admissão e exclusão de propostas
|
||||
</Link>
|
||||
<Link
|
||||
className="text-sm font-medium underline"
|
||||
href={`/requisicoes/${requisition.id}/relatorio`}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
@viteReactRefresh
|
||||
@vite(['resources/css/app.css', 'resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"])
|
||||
<x-inertia::head>
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
<title>Sistema de Gestão de Compras (SGC)</title>
|
||||
</x-inertia::head>
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
|
||||
Reference in New Issue
Block a user