Improve book-in form field clarity
Restyle booking fields as high-contrast cards, add visible required/optional indicators and ARIA requirements, and make lookup and form errors more prominent.
This commit is contained in:
parent
3113830a42
commit
694c2a2292
1 changed files with 73 additions and 25 deletions
|
|
@ -16,6 +16,28 @@ import type { Customer, Supplier } from "@/lib/types";
|
|||
/** Ten photos is plenty for one booking. */
|
||||
const MAX_PHOTOS = 10;
|
||||
|
||||
/** Fields stand out from their card: white well, firm border. */
|
||||
const FIELD =
|
||||
"min-h-[56px] border-2 border-foreground/30 bg-background text-base shadow-sm focus-visible:border-primary";
|
||||
const AREA =
|
||||
"border-2 border-foreground/30 bg-background text-base shadow-sm focus-visible:border-primary";
|
||||
const PICKER =
|
||||
"min-h-[56px] w-full border-2 border-foreground/30 bg-background text-base shadow-sm";
|
||||
const CARD = "space-y-2 rounded-2xl border border-foreground/15 bg-muted/60 p-4 shadow-sm";
|
||||
|
||||
/** Says out loud whether a field has to be filled in. */
|
||||
function Tag({ required }: { required?: boolean }) {
|
||||
return required ? (
|
||||
<span className="rounded-full bg-destructive/10 px-2 py-0.5 text-xs font-semibold text-destructive">
|
||||
Required
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-full bg-foreground/10 px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
||||
Optional
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
type Lookups = { customers: Customer[]; suppliers: Supplier[] };
|
||||
|
||||
/**
|
||||
|
|
@ -239,8 +261,11 @@ export function BookInForm() {
|
|||
Photos first: the machine is in front of you when you start, and
|
||||
the photo is the one thing the form will not book in without.
|
||||
*/}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="photos">Photos of the machine</Label>
|
||||
<div className={CARD}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label htmlFor="photos">Photos of the machine</Label>
|
||||
<Tag required />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Take a photo of the machine as it arrived.
|
||||
</p>
|
||||
|
|
@ -257,7 +282,7 @@ export function BookInForm() {
|
|||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="min-h-[56px] w-full text-base"
|
||||
className={PICKER}
|
||||
disabled={photos.length >= MAX_PHOTOS}
|
||||
onClick={() => fileRef.current?.click()}
|
||||
>
|
||||
|
|
@ -280,11 +305,14 @@ export function BookInForm() {
|
|||
"Whose machine is it?" reachable as a label for the picker.
|
||||
*/}
|
||||
<div
|
||||
className="space-y-2"
|
||||
className={CARD}
|
||||
role="group"
|
||||
aria-labelledby="customer-label"
|
||||
>
|
||||
<Label id="customer-label">Whose machine is it?</Label>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label id="customer-label">Whose machine is it?</Label>
|
||||
<Tag required />
|
||||
</div>
|
||||
{lookupsLoaded ? (
|
||||
<CustomerSearch
|
||||
value={customerId}
|
||||
|
|
@ -296,7 +324,7 @@ export function BookInForm() {
|
|||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="min-h-[56px] w-full text-base"
|
||||
className={PICKER}
|
||||
disabled={lookupsLoading}
|
||||
onClick={() => void loadLookups("customer")}
|
||||
>
|
||||
|
|
@ -304,65 +332,85 @@ export function BookInForm() {
|
|||
</Button>
|
||||
)}
|
||||
{lookupsError?.which === "customer" ? (
|
||||
<p className="rounded-md border border-destructive/40 p-3 text-sm">
|
||||
<p className="rounded-md border-2 border-destructive bg-destructive/10 p-3 text-sm font-medium text-destructive">
|
||||
{lookupsError.message}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="machine">What is the machine?</Label>
|
||||
<div className={CARD}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label htmlFor="machine">What is the machine?</Label>
|
||||
<Tag required />
|
||||
</div>
|
||||
<Input
|
||||
id="machine"
|
||||
aria-required="true"
|
||||
value={machine}
|
||||
onChange={(e) => setMachine(e.target.value)}
|
||||
placeholder="e.g. Welding machine"
|
||||
className="min-h-[56px] text-base"
|
||||
className={FIELD}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="serial_no">Serial number</Label>
|
||||
<div className={CARD}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label htmlFor="serial_no">Serial number</Label>
|
||||
<Tag required />
|
||||
</div>
|
||||
<Input
|
||||
id="serial_no"
|
||||
aria-required="true"
|
||||
value={serialNo}
|
||||
onChange={(e) => setSerialNo(e.target.value)}
|
||||
placeholder="e.g. WM-9"
|
||||
className="min-h-[56px] text-base"
|
||||
className={FIELD}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fault">What does the customer say is wrong?</Label>
|
||||
<div className={CARD}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label htmlFor="fault">What does the customer say is wrong?</Label>
|
||||
<Tag required />
|
||||
</div>
|
||||
<Textarea
|
||||
id="fault"
|
||||
aria-required="true"
|
||||
value={fault}
|
||||
onChange={(e) => setFault(e.target.value)}
|
||||
placeholder="e.g. Will not strike"
|
||||
rows={3}
|
||||
className={AREA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="supplier_scope">What must the supplier look at?</Label>
|
||||
<div className={CARD}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label htmlFor="supplier_scope">What must the supplier look at?</Label>
|
||||
<Tag />
|
||||
</div>
|
||||
<Textarea
|
||||
id="supplier_scope"
|
||||
value={supplierScope}
|
||||
onChange={(e) => setSupplierScope(e.target.value)}
|
||||
placeholder="e.g. Strip and quote the strike mechanism"
|
||||
rows={3}
|
||||
className={AREA}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Optional. Leave it empty if you are not sure yet.
|
||||
Leave it empty if you are not sure yet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="space-y-2"
|
||||
className={CARD}
|
||||
role="group"
|
||||
aria-labelledby="supplier-label"
|
||||
>
|
||||
<Label id="supplier-label">Which supplier?</Label>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label id="supplier-label">Which supplier?</Label>
|
||||
<Tag />
|
||||
</div>
|
||||
{lookupsLoaded ? (
|
||||
<SupplierSearch
|
||||
value={supplierId}
|
||||
|
|
@ -374,7 +422,7 @@ export function BookInForm() {
|
|||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="min-h-[56px] w-full text-base"
|
||||
className={PICKER}
|
||||
disabled={lookupsLoading}
|
||||
onClick={() => void loadLookups("supplier")}
|
||||
>
|
||||
|
|
@ -382,19 +430,19 @@ export function BookInForm() {
|
|||
</Button>
|
||||
)}
|
||||
{lookupsError?.which === "supplier" ? (
|
||||
<p className="rounded-md border border-destructive/40 p-3 text-sm">
|
||||
<p className="rounded-md border-2 border-destructive bg-destructive/10 p-3 text-sm font-medium text-destructive">
|
||||
{lookupsError.message}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Optional — the office can pick the supplier later.
|
||||
The office can pick the supplier later.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<p
|
||||
role="alert"
|
||||
className="rounded-md border border-destructive/40 p-3 text-sm"
|
||||
className="rounded-md border-2 border-destructive bg-destructive/10 p-3 text-sm font-medium text-destructive"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
|
|
@ -404,7 +452,7 @@ export function BookInForm() {
|
|||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="min-h-[56px] w-full text-base"
|
||||
className={PICKER}
|
||||
onClick={() => router.push("/repairs-app/" + createdRepairId)}
|
||||
>
|
||||
Open the repair
|
||||
|
|
|
|||
Loading…
Reference in a new issue