Files
FamilyHUB/frontend/src/components/TransactionDetailScreen.vue
T

260 lines
11 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.
<template>
<div class="min-h-screen bg-[#0A0A0F] dark">
<div class="mx-auto max-w-md min-h-screen flex flex-col relative">
<!-- Header -->
<header
v-motion
:initial="{ opacity: 0, y: -20 }"
:enter="{ opacity: 1, y: 0 }"
class="flex items-center justify-between px-5 pt-6 pb-4"
>
<div class="flex items-center gap-3">
<div
:class="[
'w-11 h-11 rounded-[16px] bg-gradient-to-br flex items-center justify-center shadow-lg',
transaction.type === 'income'
? 'from-emerald-500 to-green-600 shadow-emerald-500/20'
: 'from-rose-500 to-red-600 shadow-rose-500/20'
]"
>
<TrendingUp v-if="transaction.type === 'income'" :size="20" :stroke-width="2.5" class="text-white" />
<TrendingDown v-else :size="20" :stroke-width="2.5" class="text-white" />
</div>
<div>
<p class="text-[11px] text-zinc-500 font-normal mb-0.5">Transaction details</p>
<h1 class="text-[17px] text-white font-semibold tracking-tight">{{ transaction.title }}</h1>
</div>
</div>
<button
@click="emit('close')"
class="w-10 h-10 rounded-[14px] bg-[#1A1A24] flex items-center justify-center hover:bg-[#222230] transition-colors border border-white/5"
>
<X :size="18" :stroke-width="2" class="text-zinc-400" />
</button>
</header>
<!-- Main Content -->
<main class="flex-1 overflow-y-auto px-5 pb-6">
<div class="space-y-4">
<!-- Transaction Summary Card -->
<div
v-motion
:initial="{ opacity: 0, y: 20 }"
:enter="{ opacity: 1, y: 0, transition: { delay: 100 } }"
:class="[
'rounded-[20px] bg-gradient-to-br p-5 border relative overflow-hidden',
transaction.type === 'income'
? 'from-emerald-600/20 via-emerald-500/10 to-green-600/20 border-emerald-500/20'
: 'from-rose-600/20 via-rose-500/10 to-red-600/20 border-rose-500/20'
]"
>
<div
:class="[
'absolute -top-10 -right-10 w-32 h-32 rounded-full blur-3xl opacity-10',
transaction.type === 'income' ? 'bg-emerald-500' : 'bg-rose-500'
]"
/>
<div
:class="[
'absolute -bottom-8 -left-8 w-24 h-24 rounded-full blur-3xl opacity-10',
transaction.type === 'income' ? 'bg-green-500' : 'bg-red-500'
]"
/>
<div class="relative z-10">
<!-- Amount -->
<div class="text-center mb-5">
<p
:class="[
'text-[13px] font-medium mb-2',
transaction.type === 'income' ? 'text-emerald-300' : 'text-rose-300'
]"
>
{{ transaction.type === 'income' ? 'Income' : 'Expense' }}
</p>
<h2 class="text-white text-[42px] font-bold tracking-tight">
{{ transaction.type === 'income' ? '+' : '-' }}${{ transaction.amount.toFixed(2) }}
</h2>
</div>
<!-- Info Grid -->
<div class="grid grid-cols-2 gap-3">
<div class="bg-white/5 backdrop-blur-sm rounded-[14px] p-3 border border-white/10">
<div class="flex items-center gap-2 mb-1">
<Calendar :size="14" :stroke-width="2" class="text-zinc-400" />
<p class="text-zinc-500 text-[11px] font-medium">Date</p>
</div>
<p class="text-white text-[13px] font-semibold">{{ transaction.date }}</p>
</div>
<div class="bg-white/5 backdrop-blur-sm rounded-[14px] p-3 border border-white/10">
<div class="flex items-center gap-2 mb-1">
<Clock :size="14" :stroke-width="2" class="text-zinc-400" />
<p class="text-zinc-500 text-[11px] font-medium">Time</p>
</div>
<p class="text-white text-[13px] font-semibold">{{ transaction.time }}</p>
</div>
<div class="bg-white/5 backdrop-blur-sm rounded-[14px] p-3 border border-white/10 col-span-2">
<div class="flex items-center gap-2 mb-1">
<Tag :size="14" :stroke-width="2" class="text-zinc-400" />
<p class="text-zinc-500 text-[11px] font-medium">Category</p>
</div>
<p class="text-white text-[13px] font-semibold">{{ transaction.category }}</p>
</div>
<div v-if="transaction.description" class="bg-white/5 backdrop-blur-sm rounded-[14px] p-3 border border-white/10 col-span-2">
<div class="flex items-center gap-2 mb-1">
<Receipt :size="14" :stroke-width="2" class="text-zinc-400" />
<p class="text-zinc-500 text-[11px] font-medium">Description</p>
</div>
<p class="text-white text-[13px]">{{ transaction.description }}</p>
</div>
</div>
</div>
</div>
<!-- Items List -->
<div
v-if="hasItems"
v-motion
:initial="{ opacity: 0, y: 20 }"
:enter="{ opacity: 1, y: 0, transition: { delay: 200 } }"
>
<div class="flex items-center gap-2 mb-3">
<ShoppingBag :size="16" :stroke-width="2" class="text-purple-400" />
<h2 class="text-white text-[15px] font-semibold">
Items ({{ transaction.items?.length }})
</h2>
</div>
<div class="space-y-2">
<div
v-for="(item, index) in transaction.items"
:key="item.id"
v-motion
:initial="{ opacity: 0, x: -10 }"
:enter="{ opacity: 1, x: 0, transition: { delay: 250 + index * 50 } }"
class="rounded-[16px] bg-[#16161F] border border-white/[0.06] p-4"
>
<div class="flex items-start justify-between mb-3">
<div class="flex-1 min-w-0">
<h3 class="text-white text-[14px] font-semibold mb-1">{{ item.name }}</h3>
<div class="flex items-center gap-3">
<div class="flex items-center gap-1">
<Hash :size="12" :stroke-width="2" class="text-zinc-600" />
<span class="text-zinc-500 text-[12px]">×{{ item.quantity }}</span>
</div>
<div class="flex items-center gap-1">
<span class="text-zinc-500 text-[12px]">${{ item.price.toFixed(2) }} each</span>
</div>
</div>
</div>
<div class="text-right">
<p class="text-white text-[15px] font-bold">
${{ (item.price * item.quantity).toFixed(2) }}
</p>
</div>
</div>
<div v-if="item.discount && item.discount > 0" class="pt-3 border-t border-white/[0.06]">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<Percent :size="14" :stroke-width="2" class="text-emerald-400" />
<span class="text-emerald-400 text-[12px] font-medium">Discount</span>
</div>
<span class="text-emerald-400 text-[12px] font-semibold">
-${{ (item.discount * item.quantity).toFixed(2) }}
</span>
</div>
</div>
</div>
</div>
<!-- Summary -->
<div
v-motion
:initial="{ opacity: 0, y: 20 }"
:enter="{ opacity: 1, y: 0, transition: { delay: 300 + (transaction.items?.length || 0) * 50 } }"
class="mt-4 rounded-[16px] bg-[#16161F] border border-white/[0.06] p-4 space-y-2"
>
<div class="flex items-center justify-between">
<span class="text-zinc-500 text-[13px]">Subtotal</span>
<span class="text-white text-[13px] font-semibold">${{ subtotal.toFixed(2) }}</span>
</div>
<div v-if="totalDiscount > 0" class="flex items-center justify-between">
<span class="text-emerald-400 text-[13px]">Total Discount</span>
<span class="text-emerald-400 text-[13px] font-semibold">
-${{ totalDiscount.toFixed(2) }}
</span>
</div>
<div class="pt-2 border-t border-white/[0.06] flex items-center justify-between">
<span class="text-white text-[14px] font-semibold">Total</span>
<span class="text-white text-[16px] font-bold">
${{ (subtotal - totalDiscount).toFixed(2) }}
</span>
</div>
</div>
</div>
<!-- No Items Message -->
<div
v-else
v-motion
:initial="{ opacity: 0, y: 20 }"
:enter="{ opacity: 1, y: 0, transition: { delay: 200 } }"
class="rounded-[16px] bg-[#16161F] border border-white/[0.06] p-8 text-center"
>
<ShoppingBag :size="48" :stroke-width="1.5" class="text-zinc-700 mx-auto mb-3" />
<p class="text-zinc-500 text-[14px]">No items in this transaction</p>
</div>
</div>
</main>
<!-- Footer Actions -->
<div class="px-5 py-4 border-t border-white/[0.06]">
<button
@click="emit('close')"
class="w-full py-3.5 rounded-[14px] bg-gradient-to-br from-purple-500 to-blue-600 hover:shadow-lg hover:shadow-purple-500/30 text-white text-[14px] font-semibold transition-all active:scale-95"
>
Close
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import {
X,
Calendar,
Clock,
Tag,
Receipt,
ShoppingBag,
TrendingDown,
TrendingUp,
Percent,
Hash,
} from 'lucide-vue-next'
import type { Transaction } from '../types/transaction'
interface Props {
transaction: Transaction
}
const props = defineProps<Props>()
const emit = defineEmits<{
close: []
}>()
const hasItems = computed(() => props.transaction.items && props.transaction.items.length > 0)
const subtotal = computed(() =>
props.transaction.items?.reduce((sum, item) => sum + (item.price * item.quantity), 0) || 0
)
const totalDiscount = computed(() =>
props.transaction.items?.reduce((sum, item) => sum + ((item.discount || 0) * item.quantity), 0) || 0
)
</script>