Files
reservas/resources/js/pages/reservations/index.tsx

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' }],
};