Add cost summary feature to ListDetailPage; implement API endpoints for costs and enhance UI with a dialog for displaying cost details, including user balances and total costs.
This commit is contained in:
parent
f6a50e0d6a
commit
7bbec7ad5f
@ -86,6 +86,13 @@ export const API_ENDPOINTS = {
|
||||
HISTORY: '/ocr/history',
|
||||
},
|
||||
|
||||
// Costs
|
||||
COSTS: {
|
||||
BASE: '/costs',
|
||||
LIST_SUMMARY: (listId: string | number) => `/costs/lists/${listId}/cost-summary`,
|
||||
GROUP_BALANCE_SUMMARY: (groupId: string | number) => `/costs/groups/${groupId}/balance-summary`,
|
||||
},
|
||||
|
||||
// Financials
|
||||
FINANCIALS: {
|
||||
EXPENSES: '/financials/expenses',
|
||||
|
@ -19,6 +19,13 @@
|
||||
<div class="row items-center q-mb-md">
|
||||
<h1 class="text-h4 q-mb-none">{{ list.name }}</h1>
|
||||
<q-space />
|
||||
<q-btn
|
||||
color="info"
|
||||
icon="insights"
|
||||
label="Cost Summary"
|
||||
class="q-mr-sm"
|
||||
@click="showCostSummaryDialog = true"
|
||||
/>
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="camera_alt"
|
||||
@ -96,6 +103,66 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Cost Summary Dialog -->
|
||||
<q-dialog v-model="showCostSummaryDialog">
|
||||
<q-card style="min-width: 500px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">List Cost Summary</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="costSummaryLoading" class="text-center">
|
||||
<q-spinner-dots color="primary" size="2em" />
|
||||
<p>Loading cost summary...</p>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-else-if="costSummaryError" class="text-white bg-red q-py-sm">
|
||||
<q-icon name="warning" />
|
||||
{{ costSummaryError }}
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-else-if="listCostSummary">
|
||||
<div class="q-mb-md">
|
||||
<div><strong>Total List Cost:</strong> {{ formatCurrency(listCostSummary.total_list_cost) }}</div>
|
||||
<div><strong>Equal Share Per User:</strong> {{ formatCurrency(listCostSummary.equal_share_per_user) }}</div>
|
||||
<div><strong>Participating Users:</strong> {{ listCostSummary.num_participating_users }}</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
title="User Balances"
|
||||
:rows="listCostSummary.user_balances"
|
||||
:columns="userBalancesColumns"
|
||||
row-key="user_id"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
>
|
||||
<template v-slot:body-cell-items_added_value="props">
|
||||
<q-td :props="props">
|
||||
{{ formatCurrency(props.row.items_added_value) }}
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-amount_due="props">
|
||||
<q-td :props="props">
|
||||
{{ formatCurrency(props.row.amount_due) }}
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-balance="props">
|
||||
<q-td :props="props">
|
||||
<q-badge :color="props.row.balance >= 0 ? 'positive' : 'negative'">
|
||||
{{ formatCurrency(props.row.balance) }}
|
||||
</q-badge>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Close" color="primary" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Add Item Form -->
|
||||
<q-form @submit="onAddItem" class="q-mb-lg">
|
||||
<div class="row q-col-gutter-md">
|
||||
@ -204,7 +271,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { apiClient, API_ENDPOINTS } from 'src/config/api';
|
||||
import { useQuasar, QFile } from 'quasar';
|
||||
@ -231,6 +298,23 @@ interface List {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface UserCostShare {
|
||||
user_id: number;
|
||||
user_identifier: string;
|
||||
items_added_value: string | number; // API returns string, but we'll parse to 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[];
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const $q = useQuasar();
|
||||
|
||||
@ -267,6 +351,26 @@ const pendingAction = ref<(() => Promise<void>) | null>(null);
|
||||
// Add ref for item name input
|
||||
const itemNameInput = ref<{ focus: () => void } | null>(null);
|
||||
|
||||
// Cost Summary
|
||||
const showCostSummaryDialog = ref(false);
|
||||
const listCostSummary = ref<ListCostSummaryData | null>(null);
|
||||
const costSummaryLoading = ref(false);
|
||||
const costSummaryError = ref<string | null>(null);
|
||||
|
||||
const userBalancesColumns = [
|
||||
{ name: 'user_identifier', label: 'User', field: 'user_identifier', align: 'left' as const, sortable: true },
|
||||
{ name: 'items_added_value', label: 'Items Added Value', field: 'items_added_value', align: 'right' as const, sortable: true },
|
||||
{ name: 'amount_due', label: 'Amount Due', field: 'amount_due', align: 'right' as const, sortable: true },
|
||||
{ name: 'balance', label: 'Balance', field: 'balance', align: 'right' as const, sortable: true },
|
||||
];
|
||||
|
||||
const formatCurrency = (value: string | number | undefined | null): string => {
|
||||
if (value === undefined || value === null) return '$0.00';
|
||||
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
||||
if (isNaN(numValue)) return '$0.00';
|
||||
return `$${numValue.toFixed(2)}`;
|
||||
};
|
||||
|
||||
const fetchListDetails = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
@ -282,6 +386,7 @@ const fetchListDetails = async () => {
|
||||
lastItemUpdate.value = response.data.items.reduce((latest: string, item: Item) => {
|
||||
return item.updated_at > latest ? item.updated_at : latest;
|
||||
}, '');
|
||||
await fetchListCostSummary(); // Fetch cost summary after list details
|
||||
} catch (err: unknown) {
|
||||
console.error('Failed to fetch list details:', err);
|
||||
error.value =
|
||||
@ -521,6 +626,36 @@ const deleteItem = async (item: Item) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchListCostSummary = async () => {
|
||||
if (!list.value || list.value.id === 0) return;
|
||||
costSummaryLoading.value = true;
|
||||
costSummaryError.value = null;
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
API_ENDPOINTS.COSTS.LIST_SUMMARY(list.value.id)
|
||||
);
|
||||
listCostSummary.value = response.data;
|
||||
} catch (err: unknown) {
|
||||
console.error('Error fetching cost summary:', err);
|
||||
costSummaryError.value =
|
||||
(err as { response?: { data?: { detail?: string } } }).response?.data?.detail ||
|
||||
'Failed to load cost summary. Please try again.';
|
||||
listCostSummary.value = null; // Clear any old data
|
||||
} finally {
|
||||
costSummaryLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Call fetchListCostSummary when the dialog is opened
|
||||
watch(showCostSummaryDialog, (newValue) => {
|
||||
if (newValue && !listCostSummary.value && list.value.id !== 0) {
|
||||
void fetchListCostSummary();
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for changes in items that might affect cost summary
|
||||
// e.g., price changes, item completion, which are handled by fetchListDetails then fetchListCostSummary
|
||||
|
||||
// Add keyboard event listeners
|
||||
onMounted(() => {
|
||||
console.log('Component mounted, route params:', route.params);
|
||||
@ -556,4 +691,9 @@ onUnmounted(() => {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.q-table__title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user