328 lines
16 KiB
TypeScript
328 lines
16 KiB
TypeScript
import { Head, Link, router, useForm } from '@inertiajs/react';
|
|
import {
|
|
ArrowLeft,
|
|
CalendarClock,
|
|
Check,
|
|
MapPin,
|
|
PackageCheck,
|
|
} from 'lucide-react';
|
|
import type { FormEvent } from 'react';
|
|
import InputError from '@/components/input-error';
|
|
import { PageHeading } from '@/components/page-heading';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import type { Asset } from '@/types';
|
|
|
|
export default function CreateReservation({
|
|
assets,
|
|
selectedAssetId,
|
|
period,
|
|
periodSelected,
|
|
policy,
|
|
}: {
|
|
assets: Asset[];
|
|
selectedAssetId: number;
|
|
period: { starts_at?: string; ends_at?: string };
|
|
periodSelected: boolean;
|
|
policy: {
|
|
minNoticeHours: number;
|
|
maxDurationHours: number;
|
|
turnaroundMinutes: number;
|
|
maxAssets: number;
|
|
};
|
|
}) {
|
|
const form = useForm({
|
|
purpose: '',
|
|
usage_location: '',
|
|
starts_at: period.starts_at ?? '',
|
|
ends_at: period.ends_at ?? '',
|
|
asset_ids: selectedAssetId ? [selectedAssetId] : ([] as number[]),
|
|
terms_accepted: false,
|
|
});
|
|
function submit(event: FormEvent) {
|
|
event.preventDefault();
|
|
form.post('/requisicoes');
|
|
}
|
|
function toggleAsset(id: number, checked: boolean) {
|
|
form.setData(
|
|
'asset_ids',
|
|
checked
|
|
? [...form.data.asset_ids, id]
|
|
: 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 (
|
|
<>
|
|
<Head title="Nova requisição" />
|
|
<main className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-7 p-5 sm:p-8">
|
|
<PageHeading
|
|
eyebrow="Novo pedido"
|
|
title="Criar requisição"
|
|
description="Selecione o período, os equipamentos e indique como serão utilizados."
|
|
action={
|
|
<Button asChild variant="ghost">
|
|
<Link href="/equipamentos">
|
|
<ArrowLeft /> Voltar ao catálogo
|
|
</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
<p className="rounded-xl border bg-muted/40 px-4 py-3 text-sm text-muted-foreground">
|
|
Regras atuais: antecedência mínima de{' '}
|
|
{policy.minNoticeHours} h, duração máxima de{' '}
|
|
{policy.maxDurationHours} h, até {policy.maxAssets}{' '}
|
|
equipamentos
|
|
{policy.turnaroundMinutes > 0
|
|
? ` e margem operacional de ${policy.turnaroundMinutes} min.`
|
|
: '.'}
|
|
</p>
|
|
<form
|
|
onSubmit={submit}
|
|
className="grid gap-6 lg:grid-cols-[1fr_320px]"
|
|
>
|
|
<div className="space-y-6">
|
|
<Card className="border-slate-200 shadow-none dark:border-slate-800">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<CalendarClock className="size-5 text-primary" />{' '}
|
|
Período e utilização
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-5 sm:grid-cols-2">
|
|
<Field
|
|
label="Início"
|
|
error={form.errors.starts_at}
|
|
>
|
|
<Input
|
|
type="datetime-local"
|
|
value={form.data.starts_at}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'starts_at',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field label="Fim" error={form.errors.ends_at}>
|
|
<Input
|
|
type="datetime-local"
|
|
value={form.data.ends_at}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'ends_at',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<div className="sm:col-span-2">
|
|
<Field
|
|
label="Local de utilização"
|
|
error={form.errors.usage_location}
|
|
>
|
|
<Input
|
|
value={form.data.usage_location}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'usage_location',
|
|
e.target.value,
|
|
)
|
|
}
|
|
placeholder="Ex.: Auditório principal"
|
|
/>
|
|
</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
|
|
id="purpose"
|
|
value={form.data.purpose}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'purpose',
|
|
e.target.value,
|
|
)
|
|
}
|
|
rows={4}
|
|
placeholder="Descreva brevemente a atividade e a necessidade do equipamento…"
|
|
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"
|
|
/>
|
|
<InputError message={form.errors.purpose} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="border-slate-200 shadow-none dark:border-slate-800">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<PackageCheck className="size-5 text-primary" />{' '}
|
|
{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);
|
|
|
|
return (
|
|
<label
|
|
key={asset.id}
|
|
className={`flex cursor-pointer items-start gap-3 rounded-lg border p-4 transition ${checked ? 'border-primary bg-primary/5' : 'hover:bg-slate-50 dark:hover:bg-slate-900'}`}
|
|
>
|
|
<Checkbox
|
|
checked={checked}
|
|
onCheckedChange={(value) =>
|
|
toggleAsset(
|
|
asset.id,
|
|
Boolean(value),
|
|
)
|
|
}
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex justify-between gap-3">
|
|
<span className="font-medium">
|
|
{asset.name}
|
|
</span>
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
{asset.asset_tag}
|
|
</span>
|
|
</div>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
{asset.category_name} ·{' '}
|
|
{asset.model_name}
|
|
</p>
|
|
{asset.location_name && (
|
|
<p className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
|
|
<MapPin className="size-3" />
|
|
{asset.location_name}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</label>
|
|
);
|
|
})}
|
|
<InputError message={form.errors.asset_ids} />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
<aside>
|
|
<Card className="sticky top-6 border-slate-200 shadow-none dark:border-slate-800">
|
|
<CardHeader>
|
|
<CardTitle>Resumo</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-5">
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Equipamentos selecionados
|
|
</p>
|
|
<p className="mt-1 text-2xl font-semibold">
|
|
{form.data.asset_ids.length}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-start gap-3 border-t pt-5">
|
|
<Checkbox
|
|
id="terms"
|
|
checked={form.data.terms_accepted}
|
|
onCheckedChange={(value) =>
|
|
form.setData(
|
|
'terms_accepted',
|
|
Boolean(value),
|
|
)
|
|
}
|
|
/>
|
|
<Label
|
|
htmlFor="terms"
|
|
className="text-xs leading-5 font-normal"
|
|
>
|
|
Confirmo que conheço as condições de
|
|
utilização e que entregarei os
|
|
equipamentos no estado e prazo
|
|
acordados.
|
|
</Label>
|
|
</div>
|
|
<InputError
|
|
message={form.errors.terms_accepted}
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
className="w-full"
|
|
disabled={form.processing}
|
|
>
|
|
<Check />{' '}
|
|
{form.processing
|
|
? 'A submeter…'
|
|
: 'Submeter para aprovação'}
|
|
</Button>
|
|
<p className="text-center text-xs text-muted-foreground">
|
|
O equipamento fica pré-reservado até à
|
|
decisão.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</aside>
|
|
</form>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Field({
|
|
label,
|
|
error,
|
|
children,
|
|
}: {
|
|
label: string;
|
|
error?: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<Label>{label}</Label>
|
|
<div className="mt-2">{children}</div>
|
|
<InputError message={error} />
|
|
</div>
|
|
);
|
|
}
|
|
CreateReservation.layout = {
|
|
breadcrumbs: [
|
|
{ title: 'Requisições', href: '/requisicoes' },
|
|
{ title: 'Nova', href: '/requisicoes/nova' },
|
|
],
|
|
};
|