feat: complete reservation lifecycle
This commit is contained in:
@@ -1,8 +1,19 @@
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { CalendarDays, Plus } from 'lucide-react';
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
import { Ban, CalendarDays, 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({
|
||||
@@ -10,10 +21,38 @@ export default function ReservationsIndex({
|
||||
}: {
|
||||
reservations: Paginated<Reservation>;
|
||||
}) {
|
||||
const [selected, setSelected] = useState<Reservation | null>(null);
|
||||
const cancelForm = useForm({ reason: '' });
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,16 +72,17 @@ export default function ReservationsIndex({
|
||||
/>
|
||||
{reservations.data.length ? (
|
||||
<div className="overflow-hidden rounded-xl border bg-card">
|
||||
<div className="hidden grid-cols-[150px_1fr_220px_170px] 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">
|
||||
<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_170px] md:items-center"
|
||||
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}
|
||||
@@ -72,6 +112,22 @@ export default function ReservationsIndex({
|
||||
<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>
|
||||
@@ -88,6 +144,94 @@ export default function ReservationsIndex({
|
||||
</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>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user