Regista abertura de propostas e actas imutaveis por versao
This commit is contained in:
@@ -0,0 +1,560 @@
|
||||
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';
|
||||
|
||||
type Participant = {
|
||||
name: string;
|
||||
capacity: string;
|
||||
group: 'jury' | 'participant';
|
||||
};
|
||||
type Proposal = {
|
||||
id: number;
|
||||
reference: string;
|
||||
currency: string;
|
||||
supplier: { name: string } | null;
|
||||
subtotal_minor: number;
|
||||
tax_minor: number;
|
||||
total_minor: number;
|
||||
};
|
||||
type Opening = {
|
||||
id: number;
|
||||
revision: number;
|
||||
opened_at: string;
|
||||
timezone: string;
|
||||
location: string;
|
||||
reference: string;
|
||||
reason: string;
|
||||
notes: string | null;
|
||||
participants: Participant[];
|
||||
annexes: string[];
|
||||
created_at: string;
|
||||
snapshot: { author: { name: string }; proposals: Proposal[] };
|
||||
};
|
||||
type Props = {
|
||||
requisition: { id: number; number: string; justification: string };
|
||||
proposals: Proposal[];
|
||||
basisHash: string;
|
||||
latestId: number;
|
||||
sourceChanged: boolean;
|
||||
canRecord: boolean;
|
||||
openings: {
|
||||
data: Opening[];
|
||||
prev_page_url: string | null;
|
||||
next_page_url: string | null;
|
||||
};
|
||||
};
|
||||
const selectClass = 'h-10 w-full rounded-md border bg-background px-3 text-sm';
|
||||
const amount = (minor: number, currency: string) =>
|
||||
`${(minor / 100).toLocaleString('pt-PT', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency}`;
|
||||
const sessionDate = (utc: string, timezone: string) =>
|
||||
new Date(`${utc.replace(' ', 'T')}Z`).toLocaleString('pt-PT', {
|
||||
timeZone: timezone,
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
|
||||
function OpeningForm({
|
||||
requisition,
|
||||
proposals,
|
||||
basisHash,
|
||||
latestId,
|
||||
}: Pick<Props, 'requisition' | 'proposals' | 'basisHash' | 'latestId'>) {
|
||||
const form = useForm({
|
||||
previous_id: latestId,
|
||||
basis_hash: basisHash,
|
||||
opened_at: '',
|
||||
timezone: 'Africa/Johannesburg',
|
||||
submission_deadline: '',
|
||||
reference: '',
|
||||
location: '',
|
||||
reason: '',
|
||||
notes: '',
|
||||
quotation_ids: [] as number[],
|
||||
participants: [
|
||||
{ name: '', capacity: '', group: 'participant' },
|
||||
] as Participant[],
|
||||
annexes_text: '',
|
||||
confirmed: false,
|
||||
});
|
||||
const participant = (index: number, change: Partial<Participant>) =>
|
||||
form.setData(
|
||||
'participants',
|
||||
form.data.participants.map((entry, position) =>
|
||||
position === index ? { ...entry, ...change } : entry,
|
||||
),
|
||||
);
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
form.transform(({ annexes_text, ...data }) => ({
|
||||
...data,
|
||||
annexes: annexes_text
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean),
|
||||
}));
|
||||
form.post(`/requisicoes/${requisition.id}/abertura`, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}}
|
||||
className="space-y-5"
|
||||
>
|
||||
{form.data.basis_hash !== basisHash && (
|
||||
<div className="rounded-lg border border-amber-300 p-4 text-sm">
|
||||
<p>
|
||||
As propostas foram actualizadas. Confira os valores
|
||||
abaixo; os textos introduzidos foram mantidos.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
form.setData('basis_hash', basisHash);
|
||||
form.setData('confirmed', false);
|
||||
form.setData(
|
||||
'quotation_ids',
|
||||
form.data.quotation_ids.filter((id) =>
|
||||
proposals.some(
|
||||
(proposal) => proposal.id === id,
|
||||
),
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Usar a base actual para nova conferência
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Label>
|
||||
Referência do processo ou concurso
|
||||
<Input
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={150}
|
||||
value={form.data.reference}
|
||||
onChange={(event) =>
|
||||
form.setData('reference', event.target.value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label>
|
||||
Local da sessão
|
||||
<Input
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={300}
|
||||
value={form.data.location}
|
||||
onChange={(event) =>
|
||||
form.setData('location', event.target.value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label>
|
||||
Data e hora da sessão
|
||||
<Input
|
||||
type="datetime-local"
|
||||
required
|
||||
value={form.data.opened_at}
|
||||
onChange={(event) =>
|
||||
form.setData('opened_at', event.target.value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label>
|
||||
Fuso horário
|
||||
<select
|
||||
className={selectClass}
|
||||
value={form.data.timezone}
|
||||
onChange={(event) =>
|
||||
form.setData('timezone', event.target.value)
|
||||
}
|
||||
>
|
||||
<option value="Africa/Johannesburg">
|
||||
Africa/Johannesburg (UTC+2)
|
||||
</option>
|
||||
<option value="Africa/Maputo">
|
||||
Africa/Maputo (UTC+2)
|
||||
</option>
|
||||
<option value="UTC">UTC</option>
|
||||
</select>
|
||||
</Label>
|
||||
<Label>
|
||||
Limite declarado de apresentação (opcional)
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={form.data.submission_deadline}
|
||||
onChange={(event) =>
|
||||
form.setData(
|
||||
'submission_deadline',
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
<fieldset className="space-y-3">
|
||||
<legend className="mb-3 text-sm font-semibold">
|
||||
Propostas incluídas na sessão
|
||||
</legend>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Os valores são copiados da aplicação. Seleccione e confira
|
||||
as propostas incluídas.
|
||||
</p>
|
||||
{proposals.map((proposal) => (
|
||||
<Label
|
||||
key={proposal.id}
|
||||
className="flex items-start gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.data.quotation_ids.includes(
|
||||
proposal.id,
|
||||
)}
|
||||
onChange={(event) =>
|
||||
form.setData(
|
||||
'quotation_ids',
|
||||
event.target.checked
|
||||
? [
|
||||
...form.data.quotation_ids,
|
||||
proposal.id,
|
||||
]
|
||||
: form.data.quotation_ids.filter(
|
||||
(id) => id !== proposal.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
{proposal.supplier?.name ??
|
||||
'Fornecedor indisponível'}{' '}
|
||||
· {proposal.reference}
|
||||
<span className="mt-1 block text-xs text-muted-foreground">
|
||||
Sem IVA:{' '}
|
||||
{amount(
|
||||
proposal.subtotal_minor,
|
||||
proposal.currency,
|
||||
)}{' '}
|
||||
· IVA:{' '}
|
||||
{amount(proposal.tax_minor, proposal.currency)}{' '}
|
||||
· Total:{' '}
|
||||
{amount(
|
||||
proposal.total_minor,
|
||||
proposal.currency,
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
))}
|
||||
</fieldset>
|
||||
<fieldset className="space-y-3">
|
||||
<legend className="mb-3 text-sm font-semibold">
|
||||
Intervenientes declarados
|
||||
</legend>
|
||||
{form.data.participants.map((entry, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="grid gap-3 rounded-lg border p-3 sm:grid-cols-3"
|
||||
>
|
||||
<Label>
|
||||
Nome
|
||||
<Input
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={150}
|
||||
value={entry.name}
|
||||
onChange={(event) =>
|
||||
participant(index, {
|
||||
name: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label>
|
||||
Qualidade ou função
|
||||
<Input
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={150}
|
||||
value={entry.capacity}
|
||||
onChange={(event) =>
|
||||
participant(index, {
|
||||
capacity: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label>
|
||||
Participação
|
||||
<select
|
||||
className={selectClass}
|
||||
value={entry.group}
|
||||
onChange={(event) =>
|
||||
participant(index, {
|
||||
group:
|
||||
event.target.value === 'jury'
|
||||
? 'jury'
|
||||
: 'participant',
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="participant">
|
||||
Participante
|
||||
</option>
|
||||
<option value="jury">Júri</option>
|
||||
</select>
|
||||
</Label>
|
||||
{form.data.participants.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
form.setData(
|
||||
'participants',
|
||||
form.data.participants.filter(
|
||||
(_, position) => position !== index,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
Retirar interveniente
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={form.data.participants.length >= 50}
|
||||
onClick={() =>
|
||||
form.setData('participants', [
|
||||
...form.data.participants,
|
||||
{ name: '', capacity: '', group: 'participant' },
|
||||
])
|
||||
}
|
||||
>
|
||||
Adicionar interveniente
|
||||
</Button>
|
||||
</fieldset>
|
||||
<Label>
|
||||
Referências de anexos (uma por linha)
|
||||
<textarea
|
||||
className="min-h-20 rounded-md border bg-background p-3 text-sm"
|
||||
value={form.data.annexes_text}
|
||||
onChange={(event) =>
|
||||
form.setData('annexes_text', event.target.value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
São referências declaradas; não são ficheiros carregados nem
|
||||
assinaturas verificadas.
|
||||
</p>
|
||||
<Label>
|
||||
Observações
|
||||
<textarea
|
||||
className="min-h-24 rounded-md border bg-background p-3 text-sm"
|
||||
maxLength={6000}
|
||||
value={form.data.notes}
|
||||
onChange={(event) =>
|
||||
form.setData('notes', event.target.value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label>
|
||||
Fundamento do registo ou da correcção
|
||||
<textarea
|
||||
className="min-h-20 rounded-md border bg-background p-3 text-sm"
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={4000}
|
||||
value={form.data.reason}
|
||||
onChange={(event) =>
|
||||
form.setData('reason', event.target.value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.data.confirmed}
|
||||
onChange={(event) =>
|
||||
form.setData('confirmed', event.target.checked)
|
||||
}
|
||||
required
|
||||
/>
|
||||
<span>
|
||||
Conferi os dados da sessão e as propostas seleccionadas
|
||||
antes de guardar esta versão.
|
||||
</span>
|
||||
</Label>
|
||||
<FormErrors errors={form.errors} />
|
||||
<Button disabled={form.processing || !proposals.length}>
|
||||
{latestId
|
||||
? 'Registar nova versão da acta'
|
||||
: 'Registar acta de abertura'}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OpeningPage(props: Props) {
|
||||
const { requisition, openings, canRecord, sourceChanged, latestId } = props;
|
||||
return (
|
||||
<>
|
||||
<Head title={`Abertura · ${requisition.number}`} />
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6 p-5 lg:p-8">
|
||||
<PageTitle
|
||||
eyebrow={requisition.number}
|
||||
title="Abertura de propostas"
|
||||
description={requisition.justification}
|
||||
action={
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={`/requisicoes/${requisition.id}/processo`}
|
||||
>
|
||||
Ver processo
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<p className="rounded-xl border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||
Registo documental opcional antes da adjudicação. A acta não
|
||||
avalia propostas, verifica assinaturas ou autoriza compras.
|
||||
Uma correcção gera outra versão e conserva as anteriores.
|
||||
</p>
|
||||
{sourceChanged && (
|
||||
<p className="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950">
|
||||
Há propostas não incluídas ou dados actuais diferentes
|
||||
da última acta. Alterações posteriores não entram
|
||||
automaticamente nos documentos guardados.
|
||||
</p>
|
||||
)}
|
||||
{canRecord ? (
|
||||
<Panel
|
||||
title={
|
||||
latestId
|
||||
? 'Nova versão da acta'
|
||||
: 'Registar sessão de abertura'
|
||||
}
|
||||
>
|
||||
<OpeningForm key={latestId} {...props} />
|
||||
</Panel>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
O registo está disponível a Compras num pedido aprovado
|
||||
e sem adjudicação. As versões anteriores continuam
|
||||
disponíveis.
|
||||
</p>
|
||||
)}
|
||||
{openings.data.map((opening) => (
|
||||
<Panel
|
||||
key={opening.id}
|
||||
title={`${opening.reference} · Versão ${opening.revision}`}
|
||||
description={`${sessionDate(opening.opened_at, opening.timezone)} · ${opening.timezone} · ${opening.location}`}
|
||||
>
|
||||
<p className="text-sm">
|
||||
Registada por {opening.snapshot.author.name} em{' '}
|
||||
{sessionDate(opening.created_at, opening.timezone)}.
|
||||
</p>
|
||||
<p className="mt-3 text-sm whitespace-pre-wrap">
|
||||
{opening.reason}
|
||||
</p>
|
||||
<ul className="mt-4 divide-y">
|
||||
{opening.snapshot.proposals.map((proposal) => (
|
||||
<li
|
||||
key={proposal.id}
|
||||
className="flex flex-wrap justify-between gap-2 py-3 text-sm"
|
||||
>
|
||||
<span>
|
||||
{proposal.supplier?.name ??
|
||||
'Fornecedor indisponível'}{' '}
|
||||
· {proposal.reference}
|
||||
</span>
|
||||
<strong>
|
||||
{amount(
|
||||
proposal.total_minor,
|
||||
proposal.currency,
|
||||
)}
|
||||
</strong>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<details className="mt-3 text-sm">
|
||||
<summary className="cursor-pointer underline">
|
||||
Intervenientes, observações e referências de
|
||||
anexos
|
||||
</summary>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{opening.participants.map((entry, index) => (
|
||||
<li key={index}>
|
||||
{entry.name} · {entry.capacity} ·{' '}
|
||||
{entry.group === 'jury'
|
||||
? 'Júri'
|
||||
: 'Participante'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-3 whitespace-pre-wrap">
|
||||
{opening.notes || 'Sem observações.'}
|
||||
</p>
|
||||
<ul className="mt-3 list-inside list-disc">
|
||||
{opening.annexes.map((annex, index) => (
|
||||
<li key={index}>{annex}</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
<Button asChild variant="outline" className="mt-5">
|
||||
<a
|
||||
href={`/actas/${opening.id}/imprimir`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Abrir acta para impressão
|
||||
</a>
|
||||
</Button>
|
||||
</Panel>
|
||||
))}
|
||||
{!openings.data.length && (
|
||||
<Panel title="Ainda não existe acta">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
A ausência deste registo opcional não cria uma nova
|
||||
etapa obrigatória.
|
||||
</p>
|
||||
</Panel>
|
||||
)}
|
||||
<nav
|
||||
aria-label="Versões das actas"
|
||||
className="flex justify-between gap-3"
|
||||
>
|
||||
{openings.prev_page_url ? (
|
||||
<Button asChild variant="outline">
|
||||
<Link href={openings.prev_page_url}>
|
||||
Versões mais recentes
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{openings.next_page_url && (
|
||||
<Button asChild variant="outline">
|
||||
<Link href={openings.next_page_url}>
|
||||
Versões anteriores
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
OpeningPage.layout = {
|
||||
breadcrumbs: [
|
||||
{ title: 'Requisições', href: '/requisicoes' },
|
||||
{ title: 'Abertura de propostas', href: '#' },
|
||||
],
|
||||
};
|
||||
@@ -48,6 +48,7 @@ const labels: Record<string, string> = {
|
||||
issued: 'Emitida',
|
||||
};
|
||||
const events: Record<string, string> = {
|
||||
'opening.recorded': 'Versão da acta de abertura registada',
|
||||
'requisition.created': 'Requisição criada',
|
||||
'requisition.updated': 'Requisição actualizada',
|
||||
'requisition.submitted': 'Requisição submetida',
|
||||
|
||||
@@ -110,6 +110,12 @@ export default function Comparison({
|
||||
>
|
||||
Adjudicação, aprovações do valor final e ordem de compra
|
||||
</Link>
|
||||
<Link
|
||||
className="text-sm font-medium underline"
|
||||
href={`/requisicoes/${requisition.id}/abertura`}
|
||||
>
|
||||
Sessão de abertura e actas
|
||||
</Link>
|
||||
<Link
|
||||
className="text-sm text-teal-700"
|
||||
href={`/requisicoes/${requisition.id}`}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Acta de abertura · {{ $opening->reference }} · SGC</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 17mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 24px auto; max-width: 960px; color: #111; font: 12px/1.5 Arial, sans-serif; }
|
||||
header { border-bottom: 3px solid #ed1c24; padding-bottom: 12px; }
|
||||
h1 { font-size: 22px; margin-bottom: 4px; } h2 { font-size: 15px; margin-top: 24px; }
|
||||
p, td, th { overflow-wrap: anywhere; } .pre { white-space: pre-wrap; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 12px; }
|
||||
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; vertical-align: top; }
|
||||
th { background: #f2f2f2; } thead { display: table-header-group; } tr { break-inside: avoid; }
|
||||
.notice { border: 1px solid #aaa; padding: 10px; margin-top: 16px; } .small { color: #444; font-size: 10px; }
|
||||
.signature { min-width: 120px; padding-top: 28px; } footer { border-top: 1px solid #ddd; margin-top: 28px; padding-top: 12px; }
|
||||
button { padding: 10px 18px; } @media print { body { margin: 0; max-width: none; } .no-print { display: none; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button type="button" class="no-print" onclick="window.print()">Imprimir acta</button>
|
||||
<header><strong>SGC · Sistema de Gestão de Compras</strong><p>{{ $snapshot['company']['name'] }}</p></header>
|
||||
<h1>Acta de sessão de abertura de propostas</h1>
|
||||
<p><strong>{{ $opening->reference }}</strong> · Versão {{ $opening->revision }} · {{ $snapshot['requisition']['number'] }}</p>
|
||||
<p class="pre">Objecto: {{ $snapshot['requisition']['justification'] }}</p>
|
||||
<p>Data e hora declaradas da sessão: {{ \Carbon\CarbonImmutable::parse($opening->opened_at, 'UTC')->timezone($opening->timezone)->format('d/m/Y H:i') }}<br>Fuso horário: {{ $opening->timezone }}<br>Local: {{ $opening->location }}</p>
|
||||
@if($opening->submission_deadline)<p>Limite declarado para apresentação: {{ \Carbon\CarbonImmutable::parse($opening->submission_deadline, 'UTC')->timezone($opening->timezone)->format('d/m/Y H:i') }}</p>@endif
|
||||
<div class="notice">Registo documental para conferência e assinatura. Os nomes são declarados; o SGC não valida assinaturas nem certifica a realização da sessão. Esta acta não constitui avaliação, recomendação, adjudicação ou autorização financeira.</div>
|
||||
<h2>Intervenientes declarados</h2>
|
||||
<table><thead><tr><th>Nome</th><th>Qualidade</th><th>Participação</th><th>Assinatura manuscrita</th></tr></thead><tbody>
|
||||
@foreach($opening->participants as $participant)<tr><td>{{ $participant['name'] }}</td><td>{{ $participant['capacity'] }}</td><td>{{ $participant['group'] === 'jury' ? 'Júri' : 'Participante' }}</td><td class="signature">____________________</td></tr>@endforeach
|
||||
</tbody></table>
|
||||
<h2>Mapa das propostas registadas na aplicação</h2>
|
||||
<p>Valores preservados à data deste registo, para conferência com os dados da sessão. Não inclui propostas adicionadas ou alteradas posteriormente.</p>
|
||||
<table><thead><tr><th>Ordem</th><th>Concorrente e referência</th><th>Sem IVA</th><th>IVA</th><th>Total</th></tr></thead><tbody>
|
||||
@foreach($snapshot['proposals'] as $index => $proposal)<tr><td>{{ $index + 1 }}</td><td>{{ $proposal['supplier']['name'] ?? 'Fornecedor indisponível' }}<br>{{ $proposal['reference'] }}</td><td>{{ number_format($proposal['subtotal_minor'] / 100, 2, ',', '.') }} {{ $proposal['currency'] }}</td><td>{{ number_format($proposal['tax_minor'] / 100, 2, ',', '.') }} {{ $proposal['currency'] }}</td><td>{{ number_format($proposal['total_minor'] / 100, 2, ',', '.') }} {{ $proposal['currency'] }}</td></tr>@endforeach
|
||||
</tbody></table>
|
||||
<h2>Observações</h2><p class="pre">{{ $opening->notes ?: 'Sem observações registadas.' }}</p>
|
||||
<h2>Referências de anexos</h2>
|
||||
@if(count($opening->annexes))<ul>@foreach($opening->annexes as $annex)<li>{{ $annex }}</li>@endforeach</ul>@else<p>Sem referências de anexos registadas.</p>@endif
|
||||
<p class="small">As referências acima não significam que os ficheiros ou assinaturas foram carregados ou verificados no SGC.</p>
|
||||
<h2>Fundamento do registo desta versão</h2><p class="pre">{{ $opening->reason }}</p>
|
||||
<footer class="small">Registado por {{ $snapshot['author']['name'] }} em {{ \Carbon\CarbonImmutable::parse($opening->created_at, 'UTC')->timezone($opening->timezone)->format('d/m/Y H:i') }} · {{ $opening->timezone }}.<br>SGC · Sistema de Gestão de Compras · Documento do processo {{ $snapshot['requisition']['number'] }}.</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user