Enhance ListDetailPage with collapsible expense items and improved UI
This commit is contained in:
parent
fc09848a33
commit
0aa88d0af7
@ -41,6 +41,10 @@
|
|||||||
size="sm">{{
|
size="sm">{{
|
||||||
$t('listDetailPage.buttons.addViaOcr') }}
|
$t('listDetailPage.buttons.addViaOcr') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-sm btn-primary" @click="showCreateExpenseForm = true" :disabled="!isOnline"
|
||||||
|
icon-left="plus" size="sm">
|
||||||
|
{{ $t('listDetailPage.expensesSection.addExpenseButton') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="list.description" class="neo-description-internal">{{ list.description }}</p>
|
<p v-if="list.description" class="neo-description-internal">{{ list.description }}</p>
|
||||||
@ -161,16 +165,9 @@
|
|||||||
@click.stop />
|
@click.stop />
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Expenses Section -->
|
<!-- Expenses Section -->
|
||||||
<section v-if="list && !itemsAreLoading" class="neo-expenses-section">
|
<section v-if="list && !itemsAreLoading" class="neo-expenses-section">
|
||||||
<div class="neo-expenses-header">
|
|
||||||
<h2 class="neo-expenses-title">{{ $t('listDetailPage.expensesSection.title') }}</h2>
|
|
||||||
<button class="btn btn-sm btn-primary" @click="showCreateExpenseForm = true" icon-left="plus">
|
|
||||||
{{ $t('listDetailPage.expensesSection.addExpenseButton') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<VCard v-if="listDetailStore.isLoading && expenses.length === 0" class="py-10 text-center">
|
<VCard v-if="listDetailStore.isLoading && expenses.length === 0" class="py-10 text-center">
|
||||||
<VSpinner :label="$t('listDetailPage.expensesSection.loading')" size="lg" />
|
<VSpinner :label="$t('listDetailPage.expensesSection.loading')" size="lg" />
|
||||||
</VCard>
|
</VCard>
|
||||||
@ -186,46 +183,79 @@
|
|||||||
empty-icon="receipt" :empty-title="$t('listDetailPage.expensesSection.emptyStateTitle')"
|
empty-icon="receipt" :empty-title="$t('listDetailPage.expensesSection.emptyStateTitle')"
|
||||||
:empty-message="$t('listDetailPage.expensesSection.emptyStateMessage')" class="mt-4">
|
:empty-message="$t('listDetailPage.expensesSection.emptyStateMessage')" class="mt-4">
|
||||||
</VCard>
|
</VCard>
|
||||||
<div v-else>
|
<div v-else class="neo-expense-list">
|
||||||
<div v-for="expense in expenses" :key="expense.id" class="neo-expense-card">
|
<div v-for="expense in expenses" :key="expense.id" class="neo-expense-item-wrapper">
|
||||||
|
<div class="neo-expense-item" @click="toggleExpense(expense.id)"
|
||||||
|
:class="{ 'is-expanded': isExpenseExpanded(expense.id) }">
|
||||||
|
<div class="expense-main-content">
|
||||||
|
<div class="expense-icon-container">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="12" x2="12" y1="2" y2="22"></line>
|
||||||
|
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="expense-text-content">
|
||||||
<div class="neo-expense-header">
|
<div class="neo-expense-header">
|
||||||
{{ expense.description }} - {{ formatCurrency(expense.total_amount) }}
|
{{ expense.description }}
|
||||||
|
</div>
|
||||||
|
<div class="neo-expense-details">
|
||||||
|
{{ formatCurrency(expense.total_amount) }} —
|
||||||
|
{{ $t('listDetailPage.expensesSection.paidBy') }} <strong>{{ expense.paid_by_user?.name ||
|
||||||
|
expense.paid_by_user?.email }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="expense-side-content">
|
||||||
<span class="neo-expense-status" :class="getStatusClass(expense.overall_settlement_status)">
|
<span class="neo-expense-status" :class="getStatusClass(expense.overall_settlement_status)">
|
||||||
{{ getOverallExpenseStatusText(expense.overall_settlement_status) }}
|
{{ getOverallExpenseStatusText(expense.overall_settlement_status) }}
|
||||||
</span>
|
</span>
|
||||||
|
<div class="expense-toggle-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
class="feather feather-chevron-down">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="neo-expense-details">
|
|
||||||
{{ $t('listDetailPage.expensesSection.paidBy') }} <strong>{{ expense.paid_by_user?.name ||
|
|
||||||
expense.paid_by_user?.email || `User ID:
|
|
||||||
${expense.paid_by_user_id}` }}</strong>
|
|
||||||
{{ $t('listDetailPage.expensesSection.onDate') }} {{ new Date(expense.expense_date).toLocaleDateString()
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible content -->
|
||||||
|
<div v-if="isExpenseExpanded(expense.id)" class="neo-splits-container">
|
||||||
<div class="neo-splits-list">
|
<div class="neo-splits-list">
|
||||||
<div v-for="split in expense.splits" :key="split.id" class="neo-split-item">
|
<div v-for="split in expense.splits" :key="split.id" class="neo-split-item">
|
||||||
<div class="neo-split-details">
|
<div class="split-col split-user">
|
||||||
<strong>{{ split.user?.name || split.user?.email || `User ID: ${split.user_id}` }}</strong> {{
|
<strong>{{ split.user?.name || split.user?.email || `User ID: ${split.user_id}` }}</strong>
|
||||||
$t('listDetailPage.expensesSection.owes') }} {{
|
</div>
|
||||||
formatCurrency(split.owed_amount) }}
|
<div class="split-col split-owes">
|
||||||
|
{{ $t('listDetailPage.expensesSection.owes') }} <strong>{{
|
||||||
|
formatCurrency(split.owed_amount) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="split-col split-status">
|
||||||
<span class="neo-expense-status" :class="getStatusClass(split.status)">
|
<span class="neo-expense-status" :class="getStatusClass(split.status)">
|
||||||
{{ getSplitStatusText(split.status) }}
|
{{ getSplitStatusText(split.status) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="neo-split-details">
|
<div class="split-col split-paid-info">
|
||||||
|
<div v-if="split.paid_at" class="paid-details">
|
||||||
{{ $t('listDetailPage.expensesSection.paidAmount') }} {{ getPaidAmountForSplitDisplay(split) }}
|
{{ $t('listDetailPage.expensesSection.paidAmount') }} {{ getPaidAmountForSplitDisplay(split) }}
|
||||||
<span v-if="split.paid_at"> {{ $t('listDetailPage.expensesSection.onDate') }} {{ new
|
<span v-if="split.paid_at"> {{ $t('listDetailPage.expensesSection.onDate') }} {{ new
|
||||||
Date(split.paid_at).toLocaleDateString() }}</span>
|
Date(split.paid_at).toLocaleDateString() }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="split.user_id === authStore.user?.id && split.status !== ExpenseSplitStatusEnum.PAID"
|
</div>
|
||||||
|
<div class="split-col split-action">
|
||||||
|
<button
|
||||||
|
v-if="split.user_id === authStore.user?.id && split.status !== ExpenseSplitStatusEnum.PAID"
|
||||||
class="btn btn-sm btn-primary" @click="openSettleShareModal(expense, split)"
|
class="btn btn-sm btn-primary" @click="openSettleShareModal(expense, split)"
|
||||||
:disabled="isSettlementLoading">
|
:disabled="isSettlementLoading">
|
||||||
{{ $t('listDetailPage.expensesSection.settleShareButton') }}
|
{{ $t('listDetailPage.expensesSection.settleShareButton') }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
<ul v-if="split.settlement_activities && split.settlement_activities.length > 0"
|
<ul v-if="split.settlement_activities && split.settlement_activities.length > 0"
|
||||||
class="neo-settlement-activities">
|
class="neo-settlement-activities">
|
||||||
<li v-for="activity in split.settlement_activities" :key="activity.id">
|
<li v-for="activity in split.settlement_activities" :key="activity.id">
|
||||||
{{ $t('listDetailPage.expensesSection.activityLabel') }} {{ formatCurrency(activity.amount_paid) }}
|
{{ $t('listDetailPage.expensesSection.activityLabel') }} {{
|
||||||
|
formatCurrency(activity.amount_paid) }}
|
||||||
{{
|
{{
|
||||||
$t('listDetailPage.expensesSection.byUser') }} {{ activity.payer?.name || `User
|
$t('listDetailPage.expensesSection.byUser') }} {{ activity.payer?.name || `User
|
||||||
${activity.paid_by_user_id}` }} {{ $t('listDetailPage.expensesSection.onDate') }} {{ new
|
${activity.paid_by_user_id}` }} {{ $t('listDetailPage.expensesSection.onDate') }} {{ new
|
||||||
@ -236,7 +266,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Create Expense Form -->
|
<!-- Create Expense Form -->
|
||||||
<CreateExpenseForm v-if="showCreateExpenseForm" :list-id="list?.id" :group-id="list?.group_id ?? undefined"
|
<CreateExpenseForm v-if="showCreateExpenseForm" :list-id="list?.id" :group-id="list?.group_id ?? undefined"
|
||||||
@ -1277,55 +1309,103 @@ const handleDragEnd = async (evt: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const expandedExpenses = ref<Set<number>>(new Set());
|
||||||
|
|
||||||
|
const toggleExpense = (expenseId: number) => {
|
||||||
|
const newSet = new Set(expandedExpenses.value);
|
||||||
|
if (newSet.has(expenseId)) {
|
||||||
|
newSet.delete(expenseId);
|
||||||
|
} else {
|
||||||
|
// Optional: collapse others when one is opened
|
||||||
|
// newSet.clear();
|
||||||
|
newSet.add(expenseId);
|
||||||
|
}
|
||||||
|
expandedExpenses.value = newSet;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExpenseExpanded = (expenseId: number) => {
|
||||||
|
return expandedExpenses.value.has(expenseId);
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Existing styles */
|
/* Existing styles */
|
||||||
|
|
||||||
.neo-expenses-section {
|
.neo-expenses-section {
|
||||||
margin-top: 3rem;
|
padding: 0;
|
||||||
padding: 1.2rem;
|
margin-top: 1.2rem;
|
||||||
border: 3px solid #111;
|
|
||||||
border-radius: 18px;
|
|
||||||
background: var(--light);
|
|
||||||
box-shadow: 6px 6px 0 #111;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.neo-expenses-header {
|
.neo-expense-list {
|
||||||
|
background-color: rgb(255, 248, 240);
|
||||||
|
/* Container for expense items */
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #f0e5d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neo-expense-item-wrapper {
|
||||||
|
border-bottom: 1px solid #f0e5d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neo-expense-item-wrapper:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neo-expense-item {
|
||||||
|
padding: 1rem 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 1.5rem;
|
transition: background-color 0.2s ease;
|
||||||
flex-wrap: wrap;
|
}
|
||||||
|
|
||||||
|
.neo-expense-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.neo-expense-item.is-expanded .expense-toggle-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expense-main-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.neo-expenses-title {
|
.expense-icon-container {
|
||||||
font-size: 1.75rem;
|
color: #d99a53;
|
||||||
font-weight: 900;
|
|
||||||
color: #111;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.neo-expense-card {
|
.expense-text-content {
|
||||||
background: var(--light);
|
display: flex;
|
||||||
border: 3px solid #111;
|
flex-direction: column;
|
||||||
border-radius: 18px;
|
}
|
||||||
box-shadow: 6px 6px 0 #111;
|
|
||||||
margin-bottom: 2rem;
|
.expense-side-content {
|
||||||
padding: 1.2rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expense-toggle-icon {
|
||||||
|
color: #888;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.neo-expense-header {
|
.neo-expense-header {
|
||||||
font-size: 1.3rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.neo-expense-details,
|
.neo-expense-details,
|
||||||
.neo-split-details {
|
.neo-split-details {
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
color: #333;
|
color: #555;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1345,6 +1425,7 @@ const handleDragEnd = async (evt: any) => {
|
|||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
|
color: #22c55e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-unpaid {
|
.status-unpaid {
|
||||||
@ -1362,21 +1443,63 @@ const handleDragEnd = async (evt: any) => {
|
|||||||
color: #22c55e;
|
color: #22c55e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.neo-splits-container {
|
||||||
|
padding: 0.5rem 1.2rem 1.2rem;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.neo-splits-list {
|
.neo-splits-list {
|
||||||
margin-top: 1rem;
|
margin-top: 0rem;
|
||||||
padding-left: 1rem;
|
padding-left: 0;
|
||||||
border-left: 2px solid #eee;
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.neo-split-item {
|
.neo-split-item {
|
||||||
padding: 0.5rem 0;
|
padding: 0.75rem 0;
|
||||||
border-bottom: 1px dashed #f0f0f0;
|
border-bottom: 1px dashed #f0e5d8;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"user owes status paid action"
|
||||||
|
"activities activities activities activities activities";
|
||||||
|
grid-template-columns: 1.5fr 1fr 1fr 1.5fr auto;
|
||||||
|
gap: 0.5rem 1rem;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.neo-split-item:last-child {
|
.neo-split-item:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.split-col.split-user {
|
||||||
|
grid-area: user;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-col.split-owes {
|
||||||
|
grid-area: owes;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-col.split-status {
|
||||||
|
grid-area: status;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-col.split-paid-info {
|
||||||
|
grid-area: paid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-col.split-action {
|
||||||
|
grid-area: action;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-col.neo-settlement-activities {
|
||||||
|
grid-area: activities;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #555;
|
||||||
|
padding-left: 1em;
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.neo-settlement-activities {
|
.neo-settlement-activities {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: #555;
|
color: #555;
|
||||||
@ -1396,8 +1519,9 @@ const handleDragEnd = async (evt: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-padding {
|
.page-padding {
|
||||||
padding: 1rem;
|
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
|
padding-block-start: 1rem;
|
||||||
|
padding-block-end: 5rem;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
@ -1490,13 +1614,14 @@ const handleDragEnd = async (evt: any) => {
|
|||||||
.neo-item-list {
|
.neo-item-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 1.2rem;
|
padding: 1.2rem;
|
||||||
|
padding-inline: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
background: var(--light);
|
background: var(--light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.neo-list-item {
|
.neo-list-item {
|
||||||
padding: 1rem 1.2rem;
|
padding: 1rem 0;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user