Files
2026-11-03 11:51:44 +02:00

1023 lines
44 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Head, Link, router, useForm } from '@inertiajs/react';
import { useEffect, useState } from 'react';
import {
FormErrors,
PageTitle,
Panel,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
import CorrectionSourceSearch from '@/components/correction-source-search';
import CoordinationRecordFilters, {
type CoordinationFilters,
} from '@/components/coordination-record-filters';
type Fields = Record<string, string | number | boolean | null>;
type Application = {
operation_id: number;
reference: string;
quantity_milli: number;
};
type RecordVersion = {
canApprove?: boolean;
canReturn?: boolean;
canSuspend?: boolean;
canEdit?: boolean;
canFulfil?: boolean;
canCancel?: boolean;
id: number;
record_key: string;
version: number;
state: string;
data: Fields & { applications?: Application[] };
reason: string;
created_at: string;
snapshot: {
effective_allocated_milli?: number;
reversed_operations?: number[];
item: { name: string; code: string; unit: string };
warehouse: { name: string };
substitute?: { name: string; code: string; unit: string };
order?: { number: string };
};
};
type Source = {
id: number;
label: string;
stock_item_id: number | null;
quantity_milli: number;
reference?: string;
needed_on?: string;
};
type Props = {
recordFilters?: CoordinationFilters;
lineSearch?: string;
lineFormKey?: string | null;
issueSearch?: string;
issueRecordId?: number | null;
focusRecordId?: number | null;
focusChanged?: boolean;
kind: 'reservation' | 'delivery' | 'alternative';
ready: boolean;
canPrepare: boolean;
canReview: boolean;
eligible: Record<string, boolean>;
pending: Record<string, number>;
reversedOperations?: number[];
records: RecordVersion[];
recordPagination?: {
current_page: number;
last_page: number;
total: number;
prev_page_url: string | null;
next_page_url: string | null;
} | null;
history: Record<string, RecordVersion[]>;
items: { id: number; name: string; code: string; unit: string }[];
warehouses: { id: number; name: string; code: string }[];
requestLines: Source[];
orderLines: Source[];
operations: {
id: number;
reference: string;
stock_item_id: number;
warehouse_id: number;
quantity_milli: number;
}[];
};
const titles = {
reservation: 'Reservas de necessidades',
delivery: 'Entregas confirmadas',
alternative: 'Alternativas técnicas',
};
const states: Record<string, string> = {
draft: 'Em análise técnica',
returned: 'Devolvida para correcção',
approved: 'Validada tecnicamente',
suspended: 'Suspensa',
active: 'Activa',
cancelled: 'Cancelada',
fulfilled: 'Satisfeita por saídas',
};
const compatibility: Record<string, string> = {
equivalent: 'Equivalente técnica',
conditional: 'Substituição condicionada',
incompatible: 'Não compatível',
pending: 'Pendente de análise',
};
const quantity = (milli: number) =>
(milli / 1000).toLocaleString('pt-MZ', { maximumFractionDigits: 3 });
function Prepare({ props, old }: { props: Props; old?: RecordVersion }) {
const [orderLines, setOrderLines] = useState(props.orderLines);
const [requestLines, setRequestLines] = useState(props.requestLines);
const lineFormKey = `${props.kind}:${old?.id ?? 'new'}`;
useEffect(() => {
if (!props.lineFormKey || props.lineFormKey === lineFormKey) {
setOrderLines(props.orderLines);
setRequestLines(props.requestLines);
}
}, [props.lineFormKey, props.orderLines, props.requestLines, lineFormKey]);
const defaults: Fields = {
stock_item_id: '',
warehouse_id: '',
...(props.kind === 'alternative'
? {
substitute_id: '',
application: '',
compatibility: 'pending',
conditions: '',
evidence: '',
factor_numerator: 1,
factor_denominator: 1,
valid_until: '',
}
: props.kind === 'reservation'
? {
requisition_item_id: '',
quantity: '',
needed_on: '',
additional: false,
reference: '',
}
: {
purchase_order_item_id: '',
quantity: '',
expected_on: '',
confirmed_on: '',
evidence: '',
}),
};
if (old) {
for (const key of Object.keys(defaults))
if (key in old.data) defaults[key] = old.data[key];
if (old.data.quantity_milli !== undefined)
defaults.quantity = Number(old.data.quantity_milli) / 1000;
}
const form = useForm({
operation_key: crypto.randomUUID(),
record_key: old?.record_key,
version: old?.version ?? 0,
action: 'save',
reason: '',
data: defaults,
});
const update = (key: string, value: Fields[string]) =>
form.setData('data', { ...form.data.data, [key]: value });
const field = (
key: string,
label: string,
type = 'text',
optional = false,
) => (
<label className="grid gap-1 text-sm">
{label}
{optional && ' (opcional)'}
<input
className={controlClass}
type={type}
required={!optional}
min={type === 'number' ? 0 : undefined}
step={type === 'number' ? '0.001' : undefined}
maxLength={type === 'text' ? 4000 : undefined}
value={String(form.data.data[key] ?? '')}
onChange={(e) => update(key, e.target.value)}
/>
</label>
);
const selector = (
key: string,
label: string,
entries: { id: number; label: string }[],
) => (
<label className="grid gap-1 text-sm">
{label}
<select
required
className={controlClass}
value={String(form.data.data[key])}
onChange={(e) => update(key, e.target.value)}
>
<option value="">Seleccionar cadastro existente</option>
{entries.map((entry) => (
<option value={entry.id} key={entry.id}>
{entry.label}
</option>
))}
</select>
</label>
);
const sources = props.kind === 'delivery' ? orderLines : requestLines;
const searchLines = (value: string) =>
router.get(
'/compras/fornecimento',
{
view: props.kind === 'delivery' ? 'deliveries' : 'reservations',
records_page: props.recordPagination?.current_page,
...props.recordFilters,
...(props.focusRecordId
? { record_id: props.focusRecordId }
: {}),
line_search: value,
line_form_key: lineFormKey,
selected_order_line_id:
form.data.data.purchase_order_item_id || undefined,
selected_request_line_id:
form.data.data.requisition_item_id || undefined,
},
{
only: [
'orderLines',
'requestLines',
'lineSearch',
'lineFormKey',
],
preserveState: true,
preserveScroll: true,
},
);
return (
<form
className="mt-4 space-y-4"
onSubmit={(e) => {
e.preventDefault();
form.transform((input) => {
const data = { ...input.data };
if (props.kind !== 'alternative') {
data.quantity_milli = Math.round(
Number(data.quantity) * 1000,
);
delete data.quantity;
}
return { ...input, data };
});
form.post(`/compras/fornecimento/${props.kind}`, {
preserveScroll: true,
onSuccess: () =>
form.setData('operation_key', crypto.randomUUID()),
});
}}
>
<FormErrors errors={form.errors} />
{props.kind !== 'alternative' && (
<CorrectionSourceSearch
initial={props.lineSearch}
search={searchLines}
title={
props.kind === 'delivery'
? 'Procurar encomenda emitida'
: 'Procurar requisição aprovada'
}
label="Número ou parte do número"
/>
)}
<div className="grid gap-4 md:grid-cols-2">
{props.kind !== 'alternative' && (
<label className="grid gap-1 text-sm">
{props.kind === 'delivery'
? 'Linha da ordem emitida'
: 'Reutilizar necessidade aprovada (opcional)'}
<select
required={props.kind === 'delivery'}
className={controlClass}
value={String(
form.data.data[
props.kind === 'delivery'
? 'purchase_order_item_id'
: 'requisition_item_id'
],
)}
onChange={(e) => {
const source = sources.find(
(s) => s.id === Number(e.target.value),
);
form.setData('data', {
...form.data.data,
[props.kind === 'delivery'
? 'purchase_order_item_id'
: 'requisition_item_id']:
e.target.value,
...(source
? {
...(source.stock_item_id
? {
stock_item_id:
source.stock_item_id,
}
: {}),
quantity:
source.quantity_milli / 1000,
...(source.reference
? {
reference:
source.reference,
needed_on:
source.needed_on ??
'',
}
: {}),
}
: {}),
});
}}
>
<option value="">
{props.kind === 'delivery'
? 'Seleccionar ordem e linha'
: 'Necessidade nova sem requisição anterior'}
</option>
{sources.map((source) => (
<option key={source.id} value={source.id}>
{source.label}
</option>
))}
</select>
</label>
)}
{selector(
'stock_item_id',
'Artigo e unidade',
props.items.map((i) => ({
id: i.id,
label: `${i.code} · ${i.name} (${i.unit})`,
})),
)}
{selector(
'warehouse_id',
'Armazém de destino ou utilização',
props.warehouses.map((w) => ({
id: w.id,
label: `${w.code} · ${w.name}`,
})),
)}
{props.kind === 'alternative' ? (
<>
{selector(
'substitute_id',
'Artigo substituto — neste sentido',
props.items
.filter(
(i) =>
i.id !==
Number(form.data.data.stock_item_id),
)
.map((i) => ({
id: i.id,
label: `${i.code} · ${i.name} (${i.unit})`,
})),
)}
{field(
'application',
'Aplicação ou equipamento autorizado',
)}
<label className="grid gap-1 text-sm">
Resultado da análise
<select
className={controlClass}
value={String(form.data.data.compatibility)}
onChange={(e) =>
update('compatibility', e.target.value)
}
>
{Object.entries(compatibility).map(
([key, label]) => (
<option key={key} value={key}>
{key === 'equivalent'
? 'Equivalente — a validar'
: label}
</option>
),
)}
</select>
</label>
{form.data.data.compatibility === 'conditional' &&
field(
'conditions',
'Condições obrigatórias de utilização',
)}
{field(
'evidence',
'Referência das especificações comparadas e evidência técnica',
)}
{field('valid_until', 'Validade da análise', 'date')}
</>
) : (
<>
{field(
'quantity',
props.kind === 'delivery'
? 'Quantidade acumulada confirmada na linha (inclui recepções anteriores)'
: 'Quantidade na unidade do artigo',
'number',
)}
{props.kind === 'reservation' ? (
<>
{field(
'needed_on',
'Data da necessidade',
'date',
)}
{!form.data.data.requisition_item_id &&
field(
'reference',
'Referência única da necessidade',
)}
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={Boolean(
form.data.data.additional,
)}
onChange={(e) =>
update(
'additional',
e.target.checked,
)
}
/>
Procura adicional à previsão habitual
</label>
</>
) : (
<>
{field(
'expected_on',
'Disponível para utilização em',
'date',
)}
{field(
'confirmed_on',
'Data da confirmação do fornecedor',
'date',
)}
{field(
'evidence',
'Referência da confirmação recebida',
)}
</>
)}
</>
)}
</div>
{props.kind === 'alternative' && (
<details>
<summary className="cursor-pointer text-sm font-medium">
Conversão técnica de quantidades
</summary>
<p className="my-3 text-sm text-muted-foreground">
Quantidade do substituto = quantidade original ×
numerador ÷ denominador. O factor pertence apenas a esta
relação e necessita da validação técnica.
</p>
<div className="grid gap-4 md:grid-cols-2">
{field('factor_numerator', 'Numerador', 'number')}
{field('factor_denominator', 'Denominador', 'number')}
</div>
</details>
)}
<label className="grid gap-1 text-sm">
Fundamento da preparação ou alteração
<textarea
className="rounded-lg border p-3"
required
minLength={10}
value={form.data.reason}
onChange={(e) => form.setData('reason', e.target.value)}
/>
</label>
<Button disabled={form.processing || !props.ready}>
Guardar{' '}
{props.kind === 'alternative'
? 'análise para validação'
: 'registo'}
</Button>
</form>
);
}
function Actions({ props, record }: { props: Props; record: RecordVersion }) {
const [action, setAction] = useState('');
const [issueSearch, setIssueSearch] = useState(props.issueSearch ?? '');
const [operationOptions, setOperationOptions] = useState(props.operations);
useEffect(() => {
if (props.issueRecordId === record.id)
setOperationOptions(props.operations);
}, [props.issueRecordId, props.operations, record.id]);
const form = useForm({
operation_key: crypto.randomUUID(),
record_key: record.record_key,
version: record.version,
action: '',
reason: '',
data: { stock_operation_id: '', quantity_milli: 0 },
quantity: '',
});
const choices =
props.kind === 'alternative'
? [
...(record.canApprove
? [['approve', 'Validar análise']]
: []),
...(record.canReturn
? [['return', 'Devolver para correcção']]
: []),
...(record.canSuspend
? [['suspend', 'Suspender aprovação']]
: []),
]
: [
...(record.canFulfil
? [['fulfil', 'Associar saída efectiva']]
: []),
...(record.canCancel
? [['cancel', 'Cancelar pendência']]
: []),
];
const searchIssues = () =>
router.get(
'/compras/fornecimento',
{
view: 'reservations',
records_page: props.recordPagination?.current_page,
...props.recordFilters,
...(props.focusRecordId
? { record_id: props.focusRecordId }
: {}),
issue_record_id: record.id,
issue_search: issueSearch,
selected_issue_id:
form.data.data.stock_operation_id || undefined,
},
{
only: ['operations', 'issueSearch', 'issueRecordId'],
preserveState: true,
preserveScroll: true,
},
);
return (
<div className="mt-4 space-y-3">
{props.canPrepare &&
(props.kind === 'alternative'
? ['draft', 'returned'].includes(record.state)
: record.canEdit) && (
<details>
<summary className="cursor-pointer text-sm font-medium">
Rever os dados desta preparação
</summary>
<Prepare props={props} old={record} />
</details>
)}
{choices.length > 0 && (
<label className="grid gap-1 text-sm">
Acção da minha responsabilidade
<select
className={controlClass}
value={action}
onChange={(e) => {
setAction(e.target.value);
form.setData('action', e.target.value);
}}
>
<option value="">Seleccionar acção</option>
{choices.map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
</label>
)}
{action && (
<form
className="space-y-3"
onSubmit={(e) => {
e.preventDefault();
form.transform((input) => ({
...input,
data: {
...input.data,
quantity_milli: Math.round(
Number(input.quantity) * 1000,
),
},
}));
form.post(`/compras/fornecimento/${props.kind}`, {
preserveScroll: true,
});
}}
>
<FormErrors errors={form.errors} />
{action === 'fulfil' && (
<div className="grid gap-4 md:grid-cols-2">
<details className="rounded border p-3 md:col-span-2">
<summary className="cursor-pointer text-sm">
Procurar saída pela referência
</summary>
<div className="mt-3 flex flex-wrap items-end gap-3">
<label className="grid gap-1 text-sm">
Referência ou parte da referência
<input
className={controlClass}
maxLength={100}
value={issueSearch}
onChange={(event) =>
setIssueSearch(
event.target.value,
)
}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
searchIssues();
}
}}
/>
</label>
<Button
type="button"
variant="outline"
onClick={searchIssues}
>
Procurar
</Button>
</div>
<p className="mt-2 text-sm text-muted-foreground">
A pesquisa usa o artigo e o armazém desta
necessidade e conserva os dados já
introduzidos.
</p>
</details>
<label className="grid gap-1 text-sm">
Saída já registada
<select
required
className={controlClass}
value={form.data.data.stock_operation_id}
onChange={(e) =>
form.setData('data', {
...form.data.data,
stock_operation_id: e.target.value,
})
}
>
<option value="">
Seleccionar saída existente
</option>
{operationOptions
.filter(
(o) =>
o.stock_item_id ===
Number(
record.data
.stock_item_id,
) &&
o.warehouse_id ===
Number(
record.data
.warehouse_id,
),
)
.map((o) => (
<option key={o.id} value={o.id}>
{o.reference} ·{' '}
{quantity(o.quantity_milli)}
</option>
))}
</select>
</label>
<label className="grid gap-1 text-sm">
Quantidade satisfeita nesta saída
<input
className={controlClass}
type="number"
step="0.001"
min="0.001"
required
value={form.data.quantity}
onChange={(e) =>
form.setData('quantity', e.target.value)
}
/>
</label>
</div>
)}
<label className="grid gap-1 text-sm">
Fundamento da decisão
<textarea
className="rounded-lg border p-3"
required
minLength={10}
value={form.data.reason}
onChange={(e) =>
form.setData('reason', e.target.value)
}
/>
</label>
<Button disabled={form.processing}>Confirmar</Button>
</form>
)}
</div>
);
}
function Facts({
record: r,
pending,
reversed,
}: {
record: RecordVersion;
pending?: number;
reversed?: number[];
}) {
const corrected = reversed ?? r.snapshot.reversed_operations ?? [];
const applied =
r.snapshot.effective_allocated_milli ??
r.data.applications?.reduce((sum, a) => sum + a.quantity_milli, 0) ??
0;
return (
<div className="space-y-2 text-sm">
{r.snapshot.substitute && (
<>
<p>
Substituto:{' '}
<strong>
{r.snapshot.substitute.code} ·{' '}
{r.snapshot.substitute.name}
</strong>
</p>
<p>
{compatibility[String(r.data.compatibility)]} ·
Aplicação: {String(r.data.application)}
</p>
<p>
Válida até {String(r.data.valid_until)} · Conversão:{' '}
{String(r.data.factor_numerator)} /{' '}
{String(r.data.factor_denominator)}
</p>
{r.data.conditions && (
<p>Condições: {String(r.data.conditions)}</p>
)}
<p>Evidência: {String(r.data.evidence)}</p>
</>
)}
{r.data.quantity_milli !== undefined && (
<p>
{r.snapshot.order
? 'Quantidade acumulada confirmada: '
: 'Quantidade: '}
<strong>
{quantity(Number(r.data.quantity_milli))}{' '}
{r.snapshot.item.unit}
</strong>
{r.data.applications &&
` · Pendente: ${quantity(pending ?? Number(r.data.quantity_milli) - applied)}`}
</p>
)}
{r.data.needed_on && (
<p>
Necessária em {String(r.data.needed_on)} ·{' '}
{r.data.additional
? 'Procura adicional'
: 'Incluída na previsão habitual'}{' '}
· {String(r.data.reference)}
</p>
)}
{r.data.expected_on && (
<p>
{r.snapshot.order?.number} · Disponibilidade prevista:{' '}
{String(r.data.expected_on)} · Confirmação de{' '}
{String(r.data.confirmed_on)}
</p>
)}
{r.data.applications?.map((a, i) => (
<p key={i}>
Saída {a.reference}: {quantity(a.quantity_milli)}{' '}
{r.snapshot.item.unit}
{corrected.includes(a.operation_id) &&
' · Estornada; não satisfaz a pendência actual'}
</p>
))}
</div>
);
}
export default function Coordination(props: Props) {
const view =
props.kind === 'reservation'
? 'reservations'
: props.kind === 'delivery'
? 'deliveries'
: 'alternatives';
const allowedStates =
props.kind === 'alternative'
? ['draft', 'returned', 'approved', 'suspended']
: props.kind === 'reservation'
? ['active', 'cancelled', 'fulfilled']
: ['active', 'cancelled'];
return (
<>
<Head title={titles[props.kind]} />
<div className="space-y-6 p-5 lg:p-8">
<PageTitle
eyebrow="Compras · Fornecimento e reposição"
title={titles[props.kind]}
description={
props.kind === 'alternative'
? 'Relações direccionais por artigo, aplicação e local. A validação num sentido não autoriza o sentido inverso.'
: props.kind === 'reservation'
? 'Necessidades conhecidas por artigo e local, satisfeitas por saídas já registadas no armazém.'
: 'Confirmações do fornecedor sobre ordens existentes. Apenas as quantidades pendentes entram no calendário.'
}
/>
{!props.ready && (
<p role="status">Instalação do fornecimento pendente.</p>
)}
<CoordinationRecordFilters
key={view}
view={view}
filters={
props.recordFilters ?? {
record_search: '',
record_state: '',
}
}
states={Object.fromEntries(
allowedStates.map((state) => [state, states[state]]),
)}
/>
<Panel title="Responsabilidade desta etapa">
<a
className="mb-3 inline-block text-sm underline"
href={`/compras/fornecimento.csv?${new URLSearchParams({ view, ...props.recordFilters, ...(props.focusRecordId ? { record_id: String(props.focusRecordId) } : {}) }).toString()}`}
>
{props.focusRecordId
? 'Exportar este processo (CSV)'
: 'Exportar resultados filtrados (CSV)'}
</a>
<p className="text-sm text-muted-foreground">
{props.kind === 'alternative'
? 'A equipa técnica prepara a análise; um validador independente decide. Relações pendentes, incompatíveis, expiradas ou suspensas ficam excluídas das alternativas elegíveis. Uma substituição condicionada exige confirmação das condições na utilização.'
: props.kind === 'reservation'
? 'O responsável acompanha a necessidade planeada e associa saídas efectivas já registadas. Esta reserva de planeamento não bloqueia stock físico nem repete saídas. A reserva física das requisições internas tem acompanhamento próprio no armazém.'
: 'Compras regista a confirmação e as alterações de prazo. Armazém regista a recepção no circuito existente. Mercadoria importada retida fica fora das entradas utilizáveis do calendário.'}
</p>
</Panel>
{props.focusRecordId && (
<div className="space-y-2 text-sm">
<Link
href={`/compras/fornecimento?view=${props.kind === 'alternative' ? 'alternatives' : props.kind === 'reservation' ? 'reservations' : 'deliveries'}`}
className="underline"
>
Ver todos os registos
</Link>
{props.focusChanged && (
<p role="status">
Este registo foi actualizado. Está a consultar a
versão actual; as versões anteriores permanecem
no histórico.
</p>
)}
</div>
)}
{props.canPrepare && !props.focusRecordId && (
<Panel
title={
props.kind === 'alternative'
? 'Preparar análise técnica'
: props.kind === 'reservation'
? 'Reservar uma necessidade'
: 'Confirmar uma entrega'
}
>
<details>
<summary className="cursor-pointer font-medium">
Abrir preparação
</summary>
<Prepare props={props} />
</details>
</Panel>
)}
{props.records.length === 0 && (
<Panel title="Sem registos">
<p className="text-sm text-muted-foreground">
Ainda não existem registos para a sua empresa nesta
área.
</p>
</Panel>
)}
{props.records.map((r) => (
<Panel
key={r.id}
title={`${r.snapshot.item.code} · ${r.snapshot.item.name}`}
description={`${r.snapshot.warehouse.name} · ${states[r.state]} · Versão ${r.version}`}
>
<div id={`supply-record-${r.id}`} />
<Facts
record={r}
pending={
props.kind === 'reservation'
? props.pending[r.record_key]
: undefined
}
reversed={props.reversedOperations}
/>
{props.kind === 'reservation' &&
r.state === 'fulfilled' &&
(props.pending[r.record_key] ?? 0) > 0 && (
<p className="mt-2 text-sm font-medium">
Pendência reaberta por estorno de saída. A
associação anterior permanece no histórico.
</p>
)}
{props.kind === 'delivery' && r.state === 'active' && (
<p className="mt-2 text-sm font-medium">
Ainda por receber:{' '}
{quantity(props.pending[r.record_key] ?? 0)}{' '}
{r.snapshot.item.unit}
</p>
)}
{props.kind === 'alternative' && (
<p className="mt-3 text-sm font-medium">
{props.eligible[r.record_key]
? 'Equivalência vigente para esta aplicação e sentido'
: 'Excluída da utilização automática; consulte estado, condições, validade e cadastro'}
</p>
)}
<p className="mt-3 text-sm text-muted-foreground">
{r.reason}
</p>
<Actions props={props} record={r} />
<details className="mt-4">
<summary className="cursor-pointer text-sm">
Histórico preservado
</summary>
<ol className="mt-3 space-y-4">
{props.history[r.record_key]?.map((h) => (
<li
className="border-l-2 pl-4 text-sm"
key={h.id}
>
<p className="font-medium">
Versão {h.version} ·{' '}
{states[h.state]} ·{' '}
{new Date(
h.created_at,
).toLocaleString('pt-MZ')}
</p>
<p className="mb-2">{h.reason}</p>
<Facts record={h} />
</li>
))}
</ol>
</details>
</Panel>
))}
{props.recordPagination && (
<nav
aria-label="Paginação do acompanhamento de fornecimentos"
className="flex flex-wrap items-center justify-between gap-3 text-sm"
>
<span>
{props.recordPagination.total} registos · Página{' '}
{props.recordPagination.current_page} de{' '}
{props.recordPagination.last_page}
</span>
<div className="flex gap-4">
{props.recordPagination.prev_page_url && (
<Link
className="underline"
href={props.recordPagination.prev_page_url}
>
Anterior
</Link>
)}
{props.recordPagination.next_page_url && (
<Link
className="underline"
href={props.recordPagination.next_page_url}
>
Seguinte
</Link>
)}
</div>
</nav>
)}
<p className="flex flex-wrap gap-5 text-sm">
<Link
className="underline"
href="/compras/fornecimento?view=calendar"
>
Reposição e calendário
</Link>
<Link className="underline" href="/stock/operacoes">
Saídas e inventários
</Link>
<Link className="underline" href="/recepcoes">
Recepções
</Link>
</p>
</div>
</>
);
}