Spaces:
Sleeping
Sleeping
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>
|