All files / packages/app/features/receipt-components receipt-item-price-input.tsx

95.23% Statements 20/21
88.23% Branches 15/17
100% Functions 5/5
95.23% Lines 20/21

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                                                    3712x         4091x 4091x 4091x 4091x   4091x       3x     3x       4091x 4091x   486x   4091x 4091x   4091x 80x             4011x     3447x         4x 4x 4x                                                  
import type React from "react";
 
import { useTranslation } from "react-i18next";
import { z } from "zod";
 
import { useLocale } from "~app/hooks/use-locale";
import { useTrpcMutationState } from "~app/hooks/use-trpc-mutation-state";
import { formatCurrency, getCurrencySymbol } from "~app/utils/currency";
import { useAppForm } from "~app/utils/forms";
import { useTRPC } from "~app/utils/trpc";
import { priceSchema, priceSchemaDecimal } from "~app/utils/validation";
import { Spinner } from "~components/spinner";
import { Text } from "~components/text";
import { cn } from "~components/utils";
import { View } from "~components/view";
 
import { useActionsHooksContext, useReceiptContext } from "./context";
import { useCanEdit } from "./hooks";
import type { Item } from "./state";
 
type Props = {
	item: Item;
	isDisabled: boolean;
	className?: string;
};
 
export const ReceiptItemPriceInput: React.FC<Props> = ({
	item,
	isDisabled: isExternalDisabled,
	className = "",
}) => {
	const { t } = useTranslation("receipts");
	const { currencyCode, receiptDisabled } = useReceiptContext();
	const canEdit = useCanEdit();
	const { updateItemPrice } = useActionsHooksContext();
 
	const form = useAppForm({
		defaultValues: { value: item.price },
		validators: { onChange: z.object({ value: priceSchema }) },
		onSubmit: ({ value }) => {
			Iif (value.value === item.price) {
				return;
			}
			updateItemPrice(item.id, value.value);
		},
	});
 
	const trpc = useTRPC();
	const updateMutationState = useTrpcMutationState<"receiptItems.update">(
		trpc.receiptItems.update.mutationKey(),
		(vars) => vars.update.type === "price" && vars.id === item.id,
	);
	const locale = useLocale();
	const isDisabled = isExternalDisabled || receiptDisabled;
 
	if (!canEdit) {
		return (
			<View className="flex-row items-center gap-1">
				<Text>{formatCurrency(locale, currencyCode, item.price)}</Text>
			</View>
		);
	}
 
	return (
		<form.AppField name="value">
			{(field) => (
				<field.NumberField
					value={field.state.value}
					onValueChange={field.setValue}
					name={field.name}
					onBlur={() => {
						field.handleBlur();
						Eif (!isDisabled && updateMutationState?.status !== "pending") {
							void field.form.handleSubmit();
						}
					}}
					fieldError={
						field.state.meta.isDirty ? field.state.meta.errors : undefined
					}
					fractionDigits={priceSchemaDecimal}
					aria-label={t("item.form.price.label")}
					className={cn("shrink-0 basis-40", className)}
					labelPlacement="outside-left"
					mutation={updateMutationState}
					isDisabled={isDisabled}
					endContent={
						<View className="flex-row gap-2">
							{updateMutationState?.status === "pending" ? (
								<Spinner size="sm" />
							) : null}
							<Text>{getCurrencySymbol(locale, currencyCode)}</Text>
						</View>
					}
				/>
			)}
		</form.AppField>
	);
};