File size: 4,127 Bytes
4ae345a
3b74a95
4ae345a
 
 
 
3b74a95
4ae345a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b74a95
 
 
 
 
4ae345a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b74a95
4ae345a
 
 
 
 
 
 
 
 
 
 
3b74a95
4ae345a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
<script lang="ts">
	import { browser } from '$app/environment';
	import { formatCents, parseAmountToCents, parsePercentToBp } from '$lib/domain/money';
	import { allocate } from '$lib/domain/split';
	import type { PublicUser, SplitMode } from '$lib/domain/types';
	import type { ExpenseFormValues } from '$lib/forms/types';
	import { untrack } from 'svelte';
	import FormError from './FormError.svelte';

	let {
		members,
		currency,
		values,
		amount,
		error
	}: {
		members: PublicUser[];
		currency: string;
		values: ExpenseFormValues;
		amount: string;
		error?: string;
	} = $props();

	const modes: { value: SplitMode; label: string }[] = [
		{ value: 'equal', label: 'Equally' },
		{ value: 'percent', label: 'By %' },
		{ value: 'exact', label: 'Exact' }
	];

	// one-time snapshot of props at mount; not meant to react to later prop changes
	let mode = $state<SplitMode>(untrack(() => values.mode));
	let participants = $state<string[]>(untrack(() => [...values.participants]));
	let pct = $state<Record<string, string>>(untrack(() => ({ ...values.pct })));
	let exact = $state<Record<string, string>>(untrack(() => ({ ...values.exact })));

	const amountCents = $derived(parseAmountToCents(amount) ?? 0);
	const selected = $derived(members.filter((m) => participants.includes(m.id)));
	const equalShares = $derived<Record<string, number>>(
		amountCents > 0 && selected.length > 0
			? allocate(
					amountCents,
					selected.map((m) => ({ id: m.id, weight: 1 }))
				)
			: {}
	);
	const pctTotalBp = $derived(
		selected.reduce((acc, m) => acc + (parsePercentToBp(pct[m.id] ?? '') ?? 0), 0)
	);
	const exactTotal = $derived(
		selected.reduce((acc, m) => acc + (parseAmountToCents(exact[m.id] ?? '') ?? 0), 0)
	);
</script>

<fieldset class="space-y-3">
	<legend class="text-sm font-medium text-gray-700">Split</legend>

	<div class="grid grid-cols-3 gap-1 rounded-lg bg-gray-100 p-1">
		{#each modes as m (m.value)}
			<label
				class="cursor-pointer rounded-md py-2 text-center text-sm {mode === m.value
					? 'bg-white font-semibold shadow'
					: 'text-gray-600'}"
			>
				<input type="radio" name="mode" value={m.value} bind:group={mode} class="sr-only" />
				{m.label}
			</label>
		{/each}
	</div>

	<ul class="divide-y divide-gray-200 rounded-lg border border-gray-200 bg-white">
		{#each members as m (m.id)}
			{@const on = participants.includes(m.id)}
			<li class="flex items-center gap-3 px-3 py-2">
				<input
					type="checkbox"
					name="participants"
					value={m.id}
					bind:group={participants}
					id="p_{m.id}"
					class="h-5 w-5 shrink-0"
				/>
				<label for="p_{m.id}" class="flex-1 font-normal {on ? '' : 'text-gray-400'}"
					>{m.displayName}</label
				>
				{#if mode === 'equal'}
					<span class="text-sm text-gray-500 tabular-nums"
						>{on && amountCents > 0 ? formatCents(equalShares[m.id] ?? 0, currency) : ''}</span
					>
				{:else if mode === 'percent'}
					<div class="flex w-28 items-center gap-1">
						<input
							name="pct_{m.id}"
							inputmode="decimal"
							placeholder="0"
							bind:value={pct[m.id]}
							disabled={browser && !on}
							class="text-right"
							aria-label="Percent for {m.displayName}"
						/>
						<span class="text-sm text-gray-500">%</span>
					</div>
				{:else}
					<input
						name="exact_{m.id}"
						inputmode="decimal"
						placeholder="0.00"
						bind:value={exact[m.id]}
						disabled={browser && !on}
						class="w-28 text-right"
						aria-label="Amount for {m.displayName}"
					/>
				{/if}
			</li>
		{/each}
	</ul>

	{#if mode === 'percent'}
		<p class="text-sm tabular-nums {pctTotalBp === 10000 ? 'text-green-700' : 'text-amber-700'}">
			Total {(pctTotalBp / 100).toFixed(2)}% · {((10000 - pctTotalBp) / 100).toFixed(2)}% remaining
		</p>
	{:else if mode === 'exact'}
		<p
			class="text-sm tabular-nums {exactTotal === amountCents && amountCents > 0
				? 'text-green-700'
				: 'text-amber-700'}"
		>
			Assigned {formatCents(exactTotal, currency)} · {formatCents(
				amountCents - exactTotal,
				currency
			)} remaining
		</p>
	{/if}
	<FormError message={error} />
</fieldset>