Files
splitt-the-bill/app/components/BalanceSummary.vue
T
2026-07-01 15:43:18 +02:00

153 lines
6.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { ParticipantView, SettlementView } from '../types'
const props = defineProps<{
slug: string
participants: ParticipantView[]
netBalances: Record<number, number>
settlements: SettlementView[]
currency: string
}>()
const emit = defineEmits<{ settled: [] }>()
const settlingKey = ref<string | null>(null)
function nameOf(id: number) {
return props.participants.find(p => p.id === id)?.name ?? 'Unknown'
}
function paypalLinkOf(id: number) {
return props.participants.find(p => p.id === id)?.paypalLink || null
}
function initialOf(participantName: string) {
return participantName.trim().charAt(0).toUpperCase() || '?'
}
function keyOf(settlement: SettlementView) {
return `${settlement.fromId}-${settlement.toId}-${settlement.amountCents}`
}
async function markSettled(settlement: SettlementView) {
const key = keyOf(settlement)
settlingKey.value = key
try {
await $fetch(`/api/pots/${props.slug}/payments`, {
method: 'POST',
body: { fromId: settlement.fromId, toId: settlement.toId, amountCents: settlement.amountCents },
})
emit('settled')
}
finally {
settlingKey.value = null
}
}
</script>
<template>
<div class="space-y-4">
<p v-if="participants.length === 0" class="rounded-2xl border border-dashed border-stone-300 p-4 text-center text-sm text-stone-500 dark:border-stone-700 dark:text-stone-400">
No one's in this Pot yet. Add people from the settings page.
</p>
<ul v-else class="space-y-2">
<li
v-for="participant in participants"
:key="participant.id"
class="flex items-center justify-between gap-3 rounded-2xl border border-stone-200 bg-white p-3 shadow-sm dark:border-stone-800 dark:bg-stone-900"
>
<div class="flex min-w-0 items-center gap-3">
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-orange-100 text-sm font-semibold text-orange-700 dark:bg-orange-900/40 dark:text-orange-300">
{{ initialOf(participant.name) }}
</div>
<span class="truncate font-medium text-stone-900 dark:text-stone-100">{{ participant.name }}</span>
<a
v-if="participant.paypalLink"
:href="participant.paypalLink"
target="_blank"
rel="noopener noreferrer"
class="shrink-0 text-xs font-medium text-orange-600 hover:underline dark:text-orange-400"
>
PayPal
</a>
</div>
<span
class="shrink-0 text-sm font-semibold"
:class="{
'text-emerald-600 dark:text-emerald-400': (netBalances[participant.id] ?? 0) > 0,
'text-rose-600 dark:text-rose-400': (netBalances[participant.id] ?? 0) < 0,
'text-stone-400 dark:text-stone-500': (netBalances[participant.id] ?? 0) === 0,
}"
>
<template v-if="(netBalances[participant.id] ?? 0) === 0">
settled up
</template>
<template v-else>
{{ (netBalances[participant.id] ?? 0) > 0 ? '+' : '' }}{{ formatCurrency(Math.abs(netBalances[participant.id] ?? 0), currency) }}
</template>
</span>
</li>
</ul>
<div v-if="participants.length > 0" class="space-y-2">
<h2 class="text-sm font-medium text-orange-600 dark:text-orange-400">
Suggested payments
</h2>
<p v-if="settlements.length === 0" class="text-sm text-stone-500 dark:text-stone-400">
Everyone is settled up.
</p>
<ul v-else class="space-y-2">
<li
v-for="settlement in settlements"
:key="keyOf(settlement)"
class="space-y-2 rounded-2xl border border-orange-100 bg-gradient-to-r from-orange-50 to-amber-50 p-3 dark:border-orange-900/60 dark:from-orange-950/40 dark:to-amber-950/30"
>
<div class="flex items-center gap-2">
<div class="flex min-w-0 flex-1 items-center gap-2">
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white text-xs font-semibold text-orange-700 shadow-sm ring-1 ring-orange-200 dark:bg-stone-900 dark:text-orange-300 dark:ring-orange-800">
{{ initialOf(nameOf(settlement.fromId)) }}
</div>
<span class="truncate text-sm font-medium text-stone-700 dark:text-stone-300">{{ nameOf(settlement.fromId) }}</span>
<svg class="h-4 w-4 shrink-0 text-orange-400 dark:text-orange-500" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 10a.75.75 0 0 1 .75-.75h10.638L11.29 6.15a.75.75 0 1 1 1.02-1.1l4.5 4.25a.75.75 0 0 1 0 1.1l-4.5 4.25a.75.75 0 1 1-1.02-1.1l3.098-3.1H3.75A.75.75 0 0 1 3 10Z" clip-rule="evenodd" />
</svg>
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white text-xs font-semibold text-orange-700 shadow-sm ring-1 ring-orange-200 dark:bg-stone-900 dark:text-orange-300 dark:ring-orange-800">
{{ initialOf(nameOf(settlement.toId)) }}
</div>
<span class="truncate text-sm font-medium text-stone-700 dark:text-stone-300">{{ nameOf(settlement.toId) }}</span>
</div>
<span class="shrink-0 rounded-full bg-white px-2.5 py-1 text-sm font-bold text-orange-700 shadow-sm dark:bg-stone-900 dark:text-orange-300">
{{ formatCurrency(settlement.amountCents, currency) }}
</span>
</div>
<div class="flex items-center justify-end gap-2">
<a
v-if="paypalLinkOf(settlement.toId)"
:href="paypalLinkOf(settlement.toId)!"
target="_blank"
rel="noopener noreferrer"
class="rounded-full border border-orange-300 px-3 py-1 text-xs font-medium text-orange-700 transition-colors hover:bg-white dark:border-orange-800 dark:text-orange-300 dark:hover:bg-stone-900"
>
Pay via PayPal
</a>
<button
type="button"
:disabled="settlingKey === keyOf(settlement)"
class="rounded-full bg-orange-500 px-3 py-1 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-orange-600 disabled:opacity-50"
@click="markSettled(settlement)"
>
{{ settlingKey === keyOf(settlement) ? 'Marking…' : 'Mark as paid' }}
</button>
</div>
</li>
</ul>
</div>
</div>
</template>