
This commit introduces several new components to enhance the list detail functionality: - **CostSummaryDialog.vue**: A modal for displaying cost summaries, including total costs, user balances, and a detailed breakdown of expenses. - **ExpenseSection.vue**: A section for managing and displaying expenses, featuring loading states, error handling, and collapsible item details. - **ItemsList.vue**: A component for rendering and managing a list of items with drag-and-drop functionality, including a new item input field. - **ListItem.vue**: A detailed item component that supports editing, deleting, and displaying item statuses. - **OcrDialog.vue**: A modal for handling OCR file uploads and displaying extracted items. - **SettleShareModal.vue**: A modal for settling shares among users, allowing input of settlement amounts. - **Error handling utility**: A new utility function for extracting user-friendly error messages from API responses. These additions aim to improve user interaction and streamline the management of costs and expenses within the application.
142 lines
5.1 KiB
Vue
142 lines
5.1 KiB
Vue
<template>
|
|
<VModal :model-value="modelValue" :title="$t('listDetailPage.modals.costSummary.title')"
|
|
@update:modelValue="$emit('update:modelValue', false)" size="lg">
|
|
<template #default>
|
|
<div v-if="loading" class="text-center">
|
|
<VSpinner :label="$t('listDetailPage.loading.costSummary')" />
|
|
</div>
|
|
<VAlert v-else-if="error" type="error" :message="error" />
|
|
<div v-else-if="summary">
|
|
<div class="mb-3 cost-overview">
|
|
<p><strong>{{ $t('listDetailPage.modals.costSummary.totalCostLabel') }}</strong> {{
|
|
formatCurrency(summary.total_list_cost) }}</p>
|
|
<p><strong>{{ $t('listDetailPage.modals.costSummary.equalShareLabel') }}</strong> {{
|
|
formatCurrency(summary.equal_share_per_user) }}</p>
|
|
<p><strong>{{ $t('listDetailPage.modals.costSummary.participantsLabel') }}</strong> {{
|
|
summary.num_participating_users }}</p>
|
|
</div>
|
|
<h4>{{ $t('listDetailPage.modals.costSummary.userBalancesHeader') }}</h4>
|
|
<div class="table-container mt-2">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t('listDetailPage.modals.costSummary.tableHeaders.user') }}</th>
|
|
<th class="text-right">{{
|
|
$t('listDetailPage.modals.costSummary.tableHeaders.itemsAddedValue') }}</th>
|
|
<th class="text-right">{{ $t('listDetailPage.modals.costSummary.tableHeaders.amountDue')
|
|
}}</th>
|
|
<th class="text-right">{{ $t('listDetailPage.modals.costSummary.tableHeaders.balance')
|
|
}}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="userShare in summary.user_balances" :key="userShare.user_id">
|
|
<td>{{ userShare.user_identifier }}</td>
|
|
<td class="text-right">{{ formatCurrency(userShare.items_added_value) }}</td>
|
|
<td class="text-right">{{ formatCurrency(userShare.amount_due) }}</td>
|
|
<td class="text-right">
|
|
<VBadge :text="formatCurrency(userShare.balance)"
|
|
:variant="parseFloat(String(userShare.balance)) >= 0 ? 'settled' : 'pending'" />
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<p v-else>{{ $t('listDetailPage.modals.costSummary.emptyState') }}</p>
|
|
</template>
|
|
<template #footer>
|
|
<VButton variant="primary" @click="$emit('update:modelValue', false)">{{ $t('listDetailPage.buttons.close')
|
|
}}</VButton>
|
|
</template>
|
|
</VModal>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { defineProps, defineEmits } from 'vue';
|
|
import type { PropType } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import VModal from '@/components/valerie/VModal.vue';
|
|
import VSpinner from '@/components/valerie/VSpinner.vue';
|
|
import VAlert from '@/components/valerie/VAlert.vue';
|
|
import VBadge from '@/components/valerie/VBadge.vue';
|
|
import VButton from '@/components/valerie/VButton.vue';
|
|
|
|
interface UserCostShare {
|
|
user_id: number;
|
|
user_identifier: string;
|
|
items_added_value: string | number;
|
|
amount_due: string | number;
|
|
balance: string | number;
|
|
}
|
|
|
|
interface ListCostSummaryData {
|
|
list_id: number;
|
|
list_name: string;
|
|
total_list_cost: string | number;
|
|
num_participating_users: number;
|
|
equal_share_per_user: string | number;
|
|
user_balances: UserCostShare[];
|
|
}
|
|
|
|
defineProps({
|
|
modelValue: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
summary: {
|
|
type: Object as PropType<ListCostSummaryData | null>,
|
|
default: null,
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
error: {
|
|
type: String as PropType<string | null>,
|
|
default: null,
|
|
},
|
|
});
|
|
|
|
defineEmits(['update:modelValue']);
|
|
|
|
const { t } = useI18n();
|
|
|
|
const formatCurrency = (value: string | number | undefined | null): string => {
|
|
if (value === undefined || value === null) return '$0.00';
|
|
if (typeof value === 'string' && !value.trim()) return '$0.00';
|
|
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
return isNaN(numValue) ? '$0.00' : `$${numValue.toFixed(2)}`;
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.cost-overview p {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.table-container {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.table th,
|
|
.table td {
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.table th {
|
|
text-align: left;
|
|
font-weight: 600;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.text-right {
|
|
text-align: right;
|
|
}
|
|
</style> |