334 lines
15 KiB
TypeScript
334 lines
15 KiB
TypeScript
import { Head, Link, useForm } from '@inertiajs/react';
|
|
import { Ban, CalendarDays, MessageSquareText, Plus } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import InputError from '@/components/input-error';
|
|
import { PageHeading } from '@/components/page-heading';
|
|
import { ReservationStatus } from '@/components/reservation-status';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Label } from '@/components/ui/label';
|
|
import type { Paginated, Reservation } from '@/types';
|
|
|
|
export default function ReservationsIndex({
|
|
reservations,
|
|
}: {
|
|
reservations: Paginated<Reservation>;
|
|
}) {
|
|
const [selected, setSelected] = useState<Reservation | null>(null);
|
|
const [clarification, setClarification] = useState<Reservation | null>(
|
|
null,
|
|
);
|
|
const cancelForm = useForm({ reason: '' });
|
|
const clarificationForm = useForm({ response: '' });
|
|
const format = new Intl.DateTimeFormat('pt-PT', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
});
|
|
const cancellableStatuses = [
|
|
'draft',
|
|
'pending_approval',
|
|
'awaiting_information',
|
|
'approved',
|
|
'ready',
|
|
];
|
|
|
|
function openCancellation(reservation: Reservation) {
|
|
cancelForm.reset();
|
|
cancelForm.clearErrors();
|
|
setSelected(reservation);
|
|
}
|
|
|
|
function submitCancellation(event: React.FormEvent) {
|
|
event.preventDefault();
|
|
|
|
if (!selected) {
|
|
return;
|
|
}
|
|
|
|
cancelForm.post(`/requisicoes/${selected.uuid}/cancelar`, {
|
|
preserveScroll: true,
|
|
onSuccess: () => setSelected(null),
|
|
});
|
|
}
|
|
|
|
function openClarification(reservation: Reservation) {
|
|
clarificationForm.reset();
|
|
clarificationForm.clearErrors();
|
|
setClarification(reservation);
|
|
}
|
|
|
|
function submitClarification(event: React.FormEvent) {
|
|
event.preventDefault();
|
|
|
|
if (!clarification) {
|
|
return;
|
|
}
|
|
|
|
clarificationForm.post(`/requisicoes/${clarification.uuid}/responder`, {
|
|
preserveScroll: true,
|
|
onSuccess: () => setClarification(null),
|
|
});
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head title="As minhas requisições" />
|
|
<main className="mx-auto flex w-full max-w-7xl flex-1 flex-col gap-7 p-5 sm:p-8">
|
|
<PageHeading
|
|
eyebrow="Histórico"
|
|
title="As minhas requisições"
|
|
description="Acompanhe decisões, datas e equipamentos associados aos seus pedidos."
|
|
action={
|
|
<Button asChild>
|
|
<Link href="/requisicoes/nova">
|
|
<Plus /> Nova requisição
|
|
</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
{reservations.data.length ? (
|
|
<div className="overflow-hidden rounded-xl border bg-card">
|
|
<div className="hidden grid-cols-[150px_1fr_220px_150px_110px] gap-4 border-b bg-slate-50 px-5 py-3 text-xs font-medium text-muted-foreground uppercase md:grid dark:bg-slate-900">
|
|
<span>Referência</span>
|
|
<span>Equipamentos</span>
|
|
<span>Período</span>
|
|
<span>Estado</span>
|
|
<span>Ações</span>
|
|
</div>
|
|
{reservations.data.map((reservation) => (
|
|
<div
|
|
key={reservation.id}
|
|
className="grid gap-3 border-b px-5 py-4 last:border-0 md:grid-cols-[150px_1fr_220px_150px_110px] md:items-center"
|
|
>
|
|
<span className="font-mono text-sm font-medium">
|
|
{reservation.reference}
|
|
</span>
|
|
<div className="flex flex-col items-start gap-1">
|
|
{reservation.status ===
|
|
'awaiting_information' && (
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={() =>
|
|
openClarification(reservation)
|
|
}
|
|
>
|
|
<MessageSquareText /> Responder
|
|
</Button>
|
|
)}
|
|
<p className="text-sm font-medium">
|
|
{reservation.items
|
|
.map((item) => item.asset.name)
|
|
.join(', ')}
|
|
</p>
|
|
<p className="mt-1 truncate text-xs text-muted-foreground">
|
|
{reservation.purpose}
|
|
</p>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
<p>
|
|
{format.format(
|
|
new Date(reservation.starts_at),
|
|
)}
|
|
</p>
|
|
<p>
|
|
{format.format(
|
|
new Date(reservation.ends_at),
|
|
)}
|
|
</p>
|
|
</div>
|
|
<ReservationStatus
|
|
status={reservation.status}
|
|
/>
|
|
<div>
|
|
{cancellableStatuses.includes(
|
|
reservation.status,
|
|
) && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() =>
|
|
openCancellation(reservation)
|
|
}
|
|
>
|
|
<Ban /> Cancelar
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="rounded-xl border border-dashed py-16 text-center">
|
|
<CalendarDays className="mx-auto size-9 text-slate-300" />
|
|
<h2 className="mt-4 font-medium">
|
|
Ainda não criou requisições
|
|
</h2>
|
|
<Button asChild variant="link">
|
|
<Link href="/equipamentos">
|
|
Consultar equipamentos
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{reservations.last_page > 1 && (
|
|
<nav className="flex justify-center gap-2">
|
|
{reservations.links.map((link, index) => (
|
|
<Button
|
|
key={index}
|
|
asChild={Boolean(link.url)}
|
|
disabled={!link.url}
|
|
variant={link.active ? 'default' : 'outline'}
|
|
size="sm"
|
|
>
|
|
{link.url ? (
|
|
<Link
|
|
href={link.url}
|
|
preserveScroll
|
|
dangerouslySetInnerHTML={{
|
|
__html: link.label,
|
|
}}
|
|
/>
|
|
) : (
|
|
<span
|
|
dangerouslySetInnerHTML={{
|
|
__html: link.label,
|
|
}}
|
|
/>
|
|
)}
|
|
</Button>
|
|
))}
|
|
</nav>
|
|
)}
|
|
|
|
<Dialog
|
|
open={selected !== null}
|
|
onOpenChange={(open) => !open && setSelected(null)}
|
|
>
|
|
<DialogContent>
|
|
<form onSubmit={submitCancellation}>
|
|
<DialogHeader>
|
|
<DialogTitle>Cancelar requisição</DialogTitle>
|
|
<DialogDescription>
|
|
{selected?.reference}: indique o motivo. O
|
|
cancelamento liberta imediatamente os
|
|
equipamentos reservados.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="my-5">
|
|
<Label htmlFor="cancellation-reason">
|
|
Motivo do cancelamento
|
|
</Label>
|
|
<textarea
|
|
id="cancellation-reason"
|
|
value={cancelForm.data.reason}
|
|
onChange={(event) =>
|
|
cancelForm.setData(
|
|
'reason',
|
|
event.target.value,
|
|
)
|
|
}
|
|
rows={4}
|
|
className="mt-2 w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
autoFocus
|
|
/>
|
|
<InputError
|
|
message={cancelForm.errors.reason}
|
|
/>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setSelected(null)}
|
|
>
|
|
Manter requisição
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
variant="destructive"
|
|
disabled={cancelForm.processing}
|
|
>
|
|
<Ban />
|
|
{cancelForm.processing
|
|
? 'A cancelar…'
|
|
: 'Confirmar cancelamento'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog
|
|
open={clarification !== null}
|
|
onOpenChange={(open) => !open && setClarification(null)}
|
|
>
|
|
<DialogContent>
|
|
<form onSubmit={submitClarification}>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
Prestar esclarecimentos
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{clarification?.decision_reason ??
|
|
'O aprovador pediu informação adicional para poder decidir.'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="my-5">
|
|
<Label htmlFor="clarification-response">
|
|
Resposta
|
|
</Label>
|
|
<textarea
|
|
id="clarification-response"
|
|
value={clarificationForm.data.response}
|
|
onChange={(event) =>
|
|
clarificationForm.setData(
|
|
'response',
|
|
event.target.value,
|
|
)
|
|
}
|
|
rows={5}
|
|
className="mt-2 w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
autoFocus
|
|
/>
|
|
<InputError
|
|
message={clarificationForm.errors.response}
|
|
/>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setClarification(null)}
|
|
>
|
|
Voltar
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={clarificationForm.processing}
|
|
>
|
|
<MessageSquareText /> Enviar resposta
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
ReservationsIndex.layout = {
|
|
breadcrumbs: [{ title: 'Requisições', href: '/requisicoes' }],
|
|
};
|