All files / packages/app/features/receipt-components add-receipt-item-form.tsx

100% Statements 23/23
100% Branches 10/10
100% Functions 10/10
100% Lines 22/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157                                              88x             88x 45x 45x 45x 45x 45x   192x   45x 45x   45x       45x               10x   9x         9x           45x   45x                   105x                                       92x                                       79x                                     740x   101x                                
import React from "react";
 
import { useTranslation } from "react-i18next";
import { z } from "zod";
 
import { useTrpcMutationState } from "~app/hooks/use-trpc-mutation-state";
import { useAppForm } from "~app/utils/forms";
import { useTRPC } from "~app/utils/trpc";
import {
	priceSchema,
	priceSchemaDecimal,
	quantitySchema,
	quantitySchemaDecimal,
	receiptItemNameSchema,
} from "~app/utils/validation";
import { Button } from "~components/button";
import { Card } from "~components/card";
import type { InputHandler } from "~components/input";
import { emptyInputHandler } from "~components/input.base";
import { View } from "~components/view";
 
import { useActionsHooksContext, useReceiptContext } from "./context";
 
const formSchema = z.object({
	name: receiptItemNameSchema,
	price: priceSchema,
	quantity: quantitySchema,
});
type Form = z.infer<typeof formSchema>;
 
export const AddReceiptItemForm: React.FC = () => {
	const { t } = useTranslation("receipts");
	const { receiptId, receiptDisabled } = useReceiptContext();
	const { addItem } = useActionsHooksContext();
	const trpc = useTRPC();
	const addItemMutationState = useTrpcMutationState<"receiptItems.add">(
		trpc.receiptItems.add.mutationKey(),
		(vars) => vars.receiptId === receiptId,
	);
	const isPending = addItemMutationState?.status === "pending";
	const nameFieldRef = React.useRef<InputHandler>(emptyInputHandler);
 
	const defaultValues: Partial<Form> = {
		name: "",
		quantity: 1,
	};
	const form = useAppForm({
		defaultValues: defaultValues as Form,
		validators: {
			onMount: formSchema,
			onChange: formSchema,
			onSubmit: formSchema,
		},
		onSubmit: ({ value }) => {
			addItem(value.name, value.price, value.quantity, {
				onSuccess: () => {
					form.reset(
						{ ...(defaultValues as Form), price: 0 },
						{ keepDefaultValues: true },
					);
					// There's a race condition between a field becoming enabled again and this focus
					setTimeout(() => nameFieldRef.current.focus(), 0);
				},
			});
		},
	});
 
	const isDisabled = receiptDisabled || isPending;
 
	return (
		<Card>
			<form.AppForm>
				<form.Form
					className="flex flex-col gap-4"
					aria-label={t("item.form.label")}
				>
					<View className="flex-row gap-4">
						<form.AppField name="name">
							{(field) => (
								<field.TextField
									value={field.state.value}
									onValueChange={field.setValue}
									name={field.name}
									onBlur={field.handleBlur}
									label={t("item.form.name.label")}
									isRequired
									autoFocus
									fieldError={
										field.state.meta.isDirty
											? field.state.meta.errors
											: undefined
									}
									mutation={addItemMutationState}
									ref={nameFieldRef}
								/>
							)}
						</form.AppField>
						<form.AppField name="price">
							{(field) => (
								<field.NumberField
									value={field.state.value}
									onValueChange={field.setValue}
									name={field.name}
									onBlur={field.handleBlur}
									isRequired
									minValue={0}
									fractionDigits={priceSchemaDecimal}
									label={t("item.form.price.label")}
									fieldError={
										field.state.meta.isDirty
											? field.state.meta.errors
											: undefined
									}
									isDisabled={isPending}
								/>
							)}
						</form.AppField>
						<form.AppField name="quantity">
							{(field) => (
								<field.NumberField
									value={field.state.value}
									onValueChange={field.setValue}
									name={field.name}
									onBlur={field.handleBlur}
									isRequired
									minValue={0}
									fractionDigits={quantitySchemaDecimal}
									label={t("item.form.quantity.label")}
									fieldError={
										field.state.meta.isDirty
											? field.state.meta.errors
											: undefined
									}
									isDisabled={isDisabled}
								/>
							)}
						</form.AppField>
					</View>
					<form.Subscribe selector={(state) => state.canSubmit}>
						{(canSubmit) => (
							<Button
								color="primary"
								isDisabled={!canSubmit || isDisabled}
								className="w-full"
								isLoading={isPending}
								type="submit"
							>
								{t("item.form.saveButton")}
							</Button>
						)}
					</form.Subscribe>
				</form.Form>
			</form.AppForm>
		</Card>
	);
};