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

92.3% Statements 36/39
75% Branches 6/8
90.9% Functions 20/22
91.89% Lines 34/37

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                                                    1032x 1403x 1403x 1403x 1403x 1403x 1403x   3726x 3351x 1403x   3726x         1351x   1847x     1351x   1152x     1351x   3726x         760x   3330x                   1351x                 12x 8x           3713x 17366x   3713x               17366x                   17366x               2x 2x       2x 4x   2x       7452x   3726x        
import type React from "react";
 
import { useQueries } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { isNonNullish } from "remeda";
 
import { LoadablePeer } from "~app/components/app/loadable-peer";
import { LoadablePeerAvatar } from "~app/components/app/loadable-peer-avatar";
import { useTrpcMutationStates } from "~app/hooks/use-trpc-mutation-state";
import { useTRPC } from "~app/utils/trpc";
import { AvatarGroup } from "~components/avatar";
import { Select } from "~components/select";
import { Text } from "~components/text";
import { View } from "~components/view";
import type { PeerId } from "~db/ids";
 
import { useActionsHooksContext, useReceiptContext } from "./context";
import { useCanEdit, useIsOwner } from "./hooks";
import type { Item } from "./state";
import { SORT_USERS } from "./utils";
 
type Props = {
	item: Item;
	className?: string;
};
 
export const ReceiptItemConsumers: React.FC<Props> = ({ item, className }) => {
	const { t } = useTranslation("receipts");
	const { participants } = useReceiptContext();
	const { addItemConsumer, removeItemConsumer } = useActionsHooksContext();
	const isOwner = useIsOwner();
	const canEdit = useCanEdit();
	const trpc = useTRPC();
 
	const allParticipantsIds = participants.map(({ peerId }) => peerId);
	const addedParticipantsIds = item.consumers.map(({ peerId }) => peerId);
	const notAddedParticipantsIds = new Set(
		allParticipantsIds.filter(
			(participantId) => !addedParticipantsIds.includes(participantId),
		),
	);
 
	const addConsumerMutationStates =
		useTrpcMutationStates<"receiptItemConsumers.add">(
			trpc.receiptItemConsumers.add.mutationKey(),
			({ itemId }) => itemId === item.id,
		);
	const removeConsumerMutationStates =
		useTrpcMutationStates<"receiptItemConsumers.remove">(
			trpc.receiptItemConsumers.remove.mutationKey(),
			({ itemId }) => itemId === item.id,
		);
 
	const peerNames = useQueries({
		queries: participants.map(({ peerId }) =>
			isOwner
				? trpc.peers.get.queryOptions({ id: peerId })
				: trpc.peers.getForeign.queryOptions({ id: peerId }),
		),
		combine: (queries) =>
			queries.reduce<Record<PeerId, string>>(
				(acc, { data }) =>
					data
						? {
								...acc,
								["remoteId" in data ? data.remoteId : data.id]: data.name,
							}
						: acc,
				{},
			),
	});
 
	return (
		<Select
			className={className}
			label={t("item.consumer.label")}
			placeholder={t("item.consumer.placeholder")}
			selectionMode="multiple"
			selectedKeys={addedParticipantsIds}
			disabledKeys={[
				...addConsumerMutationStates
					.filter((state) => state.status === "pending")
					.map((variables) => variables.variables?.peerId),
				...removeConsumerMutationStates
					.filter((state) => state.status === "pending")
					.map((variables) => variables.variables?.peerId),
			].filter(isNonNullish)}
			renderValue={(selectedParticipants) => {
				const peerIds = selectedParticipants
					.map((participant) => participant.peerId)
					.filter(isNonNullish);
				return (
					<View className="flex-row items-center gap-2">
						<AvatarGroup
							className={canEdit ? "cursor-pointer" : undefined}
							size="sm"
							max={3}
						>
							{peerIds.map((peerId) => (
								<LoadablePeerAvatar
									key={peerId}
									id={peerId}
									foreign={!isOwner}
								/>
							))}
						</AvatarGroup>
						<Text className="text-nowrap">
							{t("item.consumer.group", {
								consumers: peerIds
									.map((peerId) => peerNames[peerId])
									.filter(isNonNullish),
							})}
						</Text>
					</View>
				);
			}}
			onSelectionChange={(nextSelected) => {
				for (const id of addedParticipantsIds.filter(
					(candidateId) => !nextSelected.includes(candidateId),
				)) {
					removeItemConsumer(item.id, id);
				}
				for (const id of nextSelected.filter((candidateId) =>
					notAddedParticipantsIds.has(candidateId),
				)) {
					addItemConsumer(item.id, id, 1);
				}
			}}
			items={participants.toSorted(SORT_USERS)}
			getKey={({ peerId }) => peerId}
		>
			{({ peerId }) => <LoadablePeer id={peerId} foreign={!isOwner} />}
		</Select>
	);
};