feat: complete reservation lifecycle
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import {
|
||||
Boxes,
|
||||
CalendarClock,
|
||||
MapPin,
|
||||
PackageCheck,
|
||||
Search,
|
||||
@@ -24,7 +25,14 @@ import type { Asset, Paginated } from '@/types';
|
||||
|
||||
type Props = {
|
||||
assets: Paginated<Asset>;
|
||||
filters: { search?: string; category?: string; location?: string };
|
||||
filters: {
|
||||
search?: string;
|
||||
category?: string;
|
||||
location?: string;
|
||||
starts_at?: string;
|
||||
ends_at?: string;
|
||||
};
|
||||
periodSelected: boolean;
|
||||
categories: string[];
|
||||
locations: string[];
|
||||
lastSyncedAt: string | null;
|
||||
@@ -36,10 +44,13 @@ export default function AssetsIndex({
|
||||
categories,
|
||||
locations,
|
||||
lastSyncedAt,
|
||||
periodSelected,
|
||||
}: Props) {
|
||||
const [search, setSearch] = useState(filters.search ?? '');
|
||||
const [category, setCategory] = useState(filters.category ?? 'all');
|
||||
const [location, setLocation] = useState(filters.location ?? 'all');
|
||||
const [startsAt, setStartsAt] = useState(filters.starts_at ?? '');
|
||||
const [endsAt, setEndsAt] = useState(filters.ends_at ?? '');
|
||||
|
||||
function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
@@ -49,6 +60,8 @@ export default function AssetsIndex({
|
||||
search: search || undefined,
|
||||
category: category === 'all' ? undefined : category,
|
||||
location: location === 'all' ? undefined : location,
|
||||
starts_at: startsAt || undefined,
|
||||
ends_at: endsAt || undefined,
|
||||
},
|
||||
{ preserveState: true },
|
||||
);
|
||||
@@ -66,9 +79,9 @@ export default function AssetsIndex({
|
||||
|
||||
<form
|
||||
onSubmit={submit}
|
||||
className="grid gap-3 rounded-xl border bg-card p-4 md:grid-cols-[1fr_220px_220px_auto]"
|
||||
className="grid gap-3 rounded-xl border bg-card p-4 lg:grid-cols-6"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="relative lg:col-span-2">
|
||||
<Search className="absolute top-2.5 left-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
@@ -92,6 +105,26 @@ export default function AssetsIndex({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<CalendarClock className="absolute top-2.5 left-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="datetime-local"
|
||||
aria-label="Início do período"
|
||||
value={startsAt}
|
||||
onChange={(event) =>
|
||||
setStartsAt(event.target.value)
|
||||
}
|
||||
className="pl-9"
|
||||
required={Boolean(endsAt)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
aria-label="Fim do período"
|
||||
value={endsAt}
|
||||
onChange={(event) => setEndsAt(event.target.value)}
|
||||
required={Boolean(startsAt)}
|
||||
/>
|
||||
<Select value={location} onValueChange={setLocation}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Localização" />
|
||||
@@ -131,7 +164,13 @@ export default function AssetsIndex({
|
||||
{assets.data.length > 0 ? (
|
||||
<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{assets.data.map((asset) => (
|
||||
<AssetCard key={asset.id} asset={asset} />
|
||||
<AssetCard
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
periodSelected={periodSelected}
|
||||
startsAt={filters.starts_at}
|
||||
endsAt={filters.ends_at}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
) : (
|
||||
@@ -180,7 +219,24 @@ export default function AssetsIndex({
|
||||
);
|
||||
}
|
||||
|
||||
function AssetCard({ asset }: { asset: Asset }) {
|
||||
function AssetCard({
|
||||
asset,
|
||||
periodSelected,
|
||||
startsAt,
|
||||
endsAt,
|
||||
}: {
|
||||
asset: Asset;
|
||||
periodSelected: boolean;
|
||||
startsAt?: string;
|
||||
endsAt?: string;
|
||||
}) {
|
||||
const requestParams = new URLSearchParams({ asset: String(asset.id) });
|
||||
|
||||
if (startsAt && endsAt) {
|
||||
requestParams.set('starts_at', startsAt);
|
||||
requestParams.set('ends_at', endsAt);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="group gap-4 overflow-hidden border-slate-200 py-0 shadow-none transition hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md dark:border-slate-800">
|
||||
<div className="flex h-36 items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-950">
|
||||
@@ -203,7 +259,7 @@ function AssetCard({ asset }: { asset: Asset }) {
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-emerald-700">
|
||||
Disponível
|
||||
{periodSelected ? 'Livre no período' : 'Requisitável'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2 text-xs text-muted-foreground">
|
||||
@@ -217,7 +273,9 @@ function AssetCard({ asset }: { asset: Asset }) {
|
||||
)}
|
||||
</div>
|
||||
<Button asChild className="w-full">
|
||||
<Link href={`/requisicoes/nova?asset=${asset.id}`}>
|
||||
<Link
|
||||
href={`/requisicoes/nova?${requestParams.toString()}`}
|
||||
>
|
||||
Requisitar equipamento
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
209
resources/js/pages/reservations/calendar.tsx
Normal file
209
resources/js/pages/reservations/calendar.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { ArrowLeft, ArrowRight, CalendarDays, Plus } from 'lucide-react';
|
||||
import { PageHeading } from '@/components/page-heading';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Reservation } from '@/types';
|
||||
|
||||
const weekdays = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
|
||||
|
||||
export default function ReservationCalendar({
|
||||
month,
|
||||
reservations,
|
||||
}: {
|
||||
month: string;
|
||||
reservations: Reservation[];
|
||||
}) {
|
||||
const [year, monthNumber] = month.split('-').map(Number);
|
||||
const monthStart = new Date(year, monthNumber - 1, 1);
|
||||
const firstWeekday = (monthStart.getDay() + 6) % 7;
|
||||
const daysInMonth = new Date(year, monthNumber, 0).getDate();
|
||||
const cellCount = Math.ceil((firstWeekday + daysInMonth) / 7) * 7;
|
||||
const gridStart = new Date(year, monthNumber - 1, 1 - firstWeekday);
|
||||
const days = Array.from({ length: cellCount }, (_, index) => {
|
||||
const date = new Date(gridStart);
|
||||
date.setDate(gridStart.getDate() + index);
|
||||
|
||||
return date;
|
||||
});
|
||||
const title = new Intl.DateTimeFormat('pt-PT', {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}).format(monthStart);
|
||||
const previousMonth = monthKey(new Date(year, monthNumber - 2, 1));
|
||||
const nextMonth = monthKey(new Date(year, monthNumber, 1));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="O meu calendário" />
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-1 flex-col gap-7 p-5 sm:p-8">
|
||||
<PageHeading
|
||||
eyebrow="Planeamento pessoal"
|
||||
title="O meu calendário"
|
||||
description="Consulte as suas requisições no mês e identifique rapidamente períodos sobrepostos."
|
||||
action={
|
||||
<Button asChild>
|
||||
<Link href="/requisicoes/nova">
|
||||
<Plus /> Nova requisição
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="overflow-hidden rounded-xl border bg-card">
|
||||
<header className="flex items-center justify-between border-b p-4">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link
|
||||
href={`/requisicoes/calendario?month=${previousMonth}`}
|
||||
>
|
||||
<ArrowLeft /> Anterior
|
||||
</Link>
|
||||
</Button>
|
||||
<h2 className="text-lg font-semibold capitalize">
|
||||
{title}
|
||||
</h2>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link
|
||||
href={`/requisicoes/calendario?month=${nextMonth}`}
|
||||
>
|
||||
Seguinte <ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-7 border-b bg-slate-50 text-center text-xs font-medium text-muted-foreground uppercase dark:bg-slate-900">
|
||||
{weekdays.map((day) => (
|
||||
<div key={day} className="px-1 py-3">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-7">
|
||||
{days.map((day) => {
|
||||
const dayStart = new Date(
|
||||
day.getFullYear(),
|
||||
day.getMonth(),
|
||||
day.getDate(),
|
||||
);
|
||||
const dayEnd = new Date(
|
||||
day.getFullYear(),
|
||||
day.getMonth(),
|
||||
day.getDate() + 1,
|
||||
);
|
||||
const entries = reservations.filter(
|
||||
(reservation) =>
|
||||
new Date(reservation.starts_at) < dayEnd &&
|
||||
new Date(reservation.ends_at) > dayStart,
|
||||
);
|
||||
const isCurrentMonth =
|
||||
day.getMonth() === monthNumber - 1;
|
||||
const isToday = sameDay(day, new Date());
|
||||
|
||||
return (
|
||||
<div
|
||||
key={day.toISOString()}
|
||||
className={cn(
|
||||
'min-h-28 border-r border-b p-1.5 last:border-r-0 sm:min-h-36 sm:p-2',
|
||||
!isCurrentMonth &&
|
||||
'bg-slate-50/70 text-muted-foreground dark:bg-slate-950/40',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex size-7 items-center justify-center rounded-full text-xs',
|
||||
isToday &&
|
||||
'bg-primary font-semibold text-primary-foreground',
|
||||
)}
|
||||
>
|
||||
{day.getDate()}
|
||||
</span>
|
||||
<div className="mt-1 space-y-1">
|
||||
{entries
|
||||
.slice(0, 3)
|
||||
.map((reservation) => (
|
||||
<div
|
||||
key={reservation.id}
|
||||
title={`${reservation.reference} — ${reservation.items.map((item) => item.asset.name).join(', ')}`}
|
||||
className={cn(
|
||||
'truncate rounded border-l-2 bg-slate-100 px-1.5 py-1 text-[10px] font-medium sm:text-xs dark:bg-slate-900',
|
||||
statusBorder(
|
||||
reservation.status,
|
||||
),
|
||||
)}
|
||||
>
|
||||
{reservation.reference}
|
||||
</div>
|
||||
))}
|
||||
{entries.length > 3 && (
|
||||
<p className="px-1 text-[10px] text-muted-foreground">
|
||||
+{entries.length - 3} pedido(s)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-4 text-xs text-muted-foreground">
|
||||
<Legend color="bg-amber-500" label="Pendente" />
|
||||
<Legend
|
||||
color="bg-emerald-500"
|
||||
label="Confirmada ou em curso"
|
||||
/>
|
||||
<Legend
|
||||
color="bg-slate-400"
|
||||
label="Fechada sem utilização"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{reservations.length === 0 && (
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<CalendarDays className="size-4" /> Não existem
|
||||
requisições neste mês.
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Legend({ color, label }: { color: string; label: string }) {
|
||||
return (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className={cn('size-2 rounded-full', color)} /> {label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function monthKey(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function sameDay(left: Date, right: Date): boolean {
|
||||
return (
|
||||
left.getFullYear() === right.getFullYear() &&
|
||||
left.getMonth() === right.getMonth() &&
|
||||
left.getDate() === right.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
function statusBorder(status: string): string {
|
||||
if (['cancelled', 'rejected', 'expired', 'no_show'].includes(status)) {
|
||||
return 'border-slate-400 opacity-60';
|
||||
}
|
||||
|
||||
if (['approved', 'ready', 'delivered', 'in_use'].includes(status)) {
|
||||
return 'border-emerald-500';
|
||||
}
|
||||
|
||||
return 'border-amber-500';
|
||||
}
|
||||
|
||||
ReservationCalendar.layout = {
|
||||
breadcrumbs: [
|
||||
{ title: 'Requisições', href: '/requisicoes' },
|
||||
{ title: 'Calendário', href: '/requisicoes/calendario' },
|
||||
],
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
import { Head, Link, router, useForm } from '@inertiajs/react';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CalendarClock,
|
||||
@@ -19,15 +19,19 @@ import type { Asset } from '@/types';
|
||||
export default function CreateReservation({
|
||||
assets,
|
||||
selectedAssetId,
|
||||
period,
|
||||
periodSelected,
|
||||
}: {
|
||||
assets: Asset[];
|
||||
selectedAssetId: number;
|
||||
period: { starts_at?: string; ends_at?: string };
|
||||
periodSelected: boolean;
|
||||
}) {
|
||||
const form = useForm({
|
||||
purpose: '',
|
||||
usage_location: '',
|
||||
starts_at: '',
|
||||
ends_at: '',
|
||||
starts_at: period.starts_at ?? '',
|
||||
ends_at: period.ends_at ?? '',
|
||||
asset_ids: selectedAssetId ? [selectedAssetId] : ([] as number[]),
|
||||
terms_accepted: false,
|
||||
});
|
||||
@@ -43,6 +47,12 @@ export default function CreateReservation({
|
||||
: form.data.asset_ids.filter((item) => item !== id),
|
||||
);
|
||||
}
|
||||
function refreshAvailability() {
|
||||
router.get('/requisicoes/nova', {
|
||||
starts_at: form.data.starts_at,
|
||||
ends_at: form.data.ends_at,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -117,6 +127,24 @@ export default function CreateReservation({
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={refreshAvailability}
|
||||
disabled={
|
||||
!form.data.starts_at ||
|
||||
!form.data.ends_at
|
||||
}
|
||||
>
|
||||
<CalendarClock /> Verificar equipamentos
|
||||
livres
|
||||
</Button>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
Confirme a disponibilidade sempre que
|
||||
alterar o período.
|
||||
</p>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Label htmlFor="purpose">Finalidade</Label>
|
||||
<textarea
|
||||
@@ -140,10 +168,18 @@ export default function CreateReservation({
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<PackageCheck className="size-5 text-primary" />{' '}
|
||||
Equipamentos
|
||||
{periodSelected
|
||||
? 'Equipamentos livres no período'
|
||||
: 'Equipamentos requisitáveis'}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{assets.length === 0 && (
|
||||
<p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||||
Não existem equipamentos livres para o
|
||||
período escolhido.
|
||||
</p>
|
||||
)}
|
||||
{assets.map((asset) => {
|
||||
const checked =
|
||||
form.data.asset_ids.includes(asset.id);
|
||||
|
||||
@@ -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