Merge pull request 'refactor: Update ListDetailPage and listDetailStore for improved type safety and state management' (#67) from ph5 into prod
Reviewed-on: #67
This commit is contained in:
commit
5c57ac8080
@ -443,7 +443,6 @@ import VToggleSwitch from '@/components/valerie/VToggleSwitch.vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import { useCategoryStore } from '@/stores/categoryStore';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import ExpenseCard from '@/components/ExpenseCard.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@ -555,9 +554,10 @@ const newItem = ref<{ name: string; quantity?: number | string; category_id?: nu
|
||||
const itemNameInputRef = ref<InstanceType<typeof VInput> | null>(null);
|
||||
|
||||
const categoryOptions = computed(() => {
|
||||
type CategoryOption = { id: number; name: string };
|
||||
return [
|
||||
{ label: 'No Category', value: null },
|
||||
...categories.value.map(c => ({ label: c.name, value: c.id })),
|
||||
...categories.value.map((c: CategoryOption) => ({ label: c.name, value: c.id })),
|
||||
];
|
||||
});
|
||||
|
||||
@ -610,7 +610,8 @@ const showCreateExpenseForm = ref(false);
|
||||
// Define a more specific type for the offline item payload
|
||||
interface OfflineCreateItemPayload {
|
||||
name: string;
|
||||
quantity?: string | number; // Align with the target type from the linter error
|
||||
quantity?: string | number;
|
||||
category_id?: number | null;
|
||||
}
|
||||
|
||||
const formatCurrency = (value: string | number | undefined | null): string => {
|
||||
@ -1064,7 +1065,7 @@ watch(showCostSummaryDialog, (newVal) => {
|
||||
|
||||
// --- Expense and Settlement Status Logic ---
|
||||
const listDetailStore = useListDetailStore();
|
||||
const expenses = computed(() => listDetailStore.getExpenses);
|
||||
const { getExpenses: expenses, currentList: listFromStore } = storeToRefs(listDetailStore);
|
||||
const allFetchedGroups = ref<Group[]>([]);
|
||||
|
||||
const getGroupName = (groupId: number): string => {
|
||||
@ -1309,6 +1310,10 @@ const handleExpenseCreated = (expense: any) => {
|
||||
if (list.value?.id) {
|
||||
listDetailStore.fetchListWithExpenses(String(list.value.id));
|
||||
}
|
||||
// Also, update the main expenses from the store
|
||||
if (listDetailStore.currentList?.id) {
|
||||
listDetailStore.fetchListWithExpenses(String(listDetailStore.currentList.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (item: ItemWithUI, event: Event) => {
|
||||
@ -1366,10 +1371,11 @@ const isExpenseExpanded = (expenseId: number) => {
|
||||
const groupedItems = computed(() => {
|
||||
if (!list.value?.items) return [];
|
||||
const groups: Record<string, { categoryName: string; items: ItemWithUI[] }> = {};
|
||||
type Category = { id: number; name: string };
|
||||
|
||||
list.value.items.forEach(item => {
|
||||
const categoryId = item.category_id;
|
||||
const category = categories.value.find(c => c.id === categoryId);
|
||||
const category = categories.value.find((c: Category) => c.id === categoryId);
|
||||
const categoryName = category ? category.name : t('listDetailPage.items.noCategory');
|
||||
|
||||
if (!groups[categoryName]) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { apiClient, API_ENDPOINTS } from '@/services/api'
|
||||
import type {
|
||||
Expense,
|
||||
@ -15,132 +16,127 @@ export interface ListWithExpenses extends List {
|
||||
expenses: Expense[]
|
||||
}
|
||||
|
||||
interface ListDetailState {
|
||||
currentList: ListWithExpenses | null
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
isSettlingSplit: boolean
|
||||
}
|
||||
export const useListDetailStore = defineStore('listDetail', () => {
|
||||
// State
|
||||
const currentList = ref<ListWithExpenses | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isSettlingSplit = ref(false)
|
||||
|
||||
export const useListDetailStore = defineStore('listDetail', {
|
||||
state: (): ListDetailState => ({
|
||||
currentList: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isSettlingSplit: false,
|
||||
}),
|
||||
// Getters (as computed properties or methods)
|
||||
const getExpenses = computed((): Expense[] => {
|
||||
return currentList.value?.expenses || []
|
||||
})
|
||||
|
||||
actions: {
|
||||
async fetchListWithExpenses(listId: string) {
|
||||
if (!listId || listId === 'undefined' || listId === 'null') {
|
||||
this.error = 'Invalid list ID provided.';
|
||||
console.warn(`fetchListWithExpenses called with invalid ID: ${listId}`);
|
||||
return;
|
||||
}
|
||||
this.isLoading = true
|
||||
this.error = null
|
||||
try {
|
||||
// Get list details
|
||||
const listEndpoint = API_ENDPOINTS.LISTS.BY_ID(listId)
|
||||
const listResponse = await apiClient.get(listEndpoint)
|
||||
const listData = listResponse.data as List
|
||||
|
||||
// Get expenses for this list
|
||||
const expensesEndpoint = API_ENDPOINTS.LISTS.EXPENSES(listId)
|
||||
const expensesResponse = await apiClient.get(expensesEndpoint)
|
||||
const expensesData = expensesResponse.data as Expense[]
|
||||
|
||||
// Combine into ListWithExpenses
|
||||
this.currentList = {
|
||||
...listData,
|
||||
expenses: expensesData,
|
||||
} as ListWithExpenses
|
||||
} catch (err: any) {
|
||||
this.error = err.response?.data?.detail || err.message || 'Failed to fetch list details'
|
||||
this.currentList = null
|
||||
console.error('Error fetching list details:', err)
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
async settleExpenseSplit(payload: {
|
||||
list_id_for_refetch: string // ID of the list to refetch after settlement
|
||||
expense_split_id: number
|
||||
activity_data: SettlementActivityCreate
|
||||
}): Promise<boolean> {
|
||||
this.isSettlingSplit = true
|
||||
this.error = null
|
||||
try {
|
||||
// Call the actual API endpoint using generic post method
|
||||
const endpoint = `/financials/expense_splits/${payload.expense_split_id}/settle`
|
||||
const response = await apiClient.post(endpoint, payload.activity_data)
|
||||
|
||||
// Refresh list data to show updated statuses
|
||||
if (payload.list_id_for_refetch) {
|
||||
await this.fetchListWithExpenses(payload.list_id_for_refetch)
|
||||
} else if (this.currentList?.id) {
|
||||
// Fallback if list_id_for_refetch is not provided but currentList exists
|
||||
await this.fetchListWithExpenses(String(this.currentList.id))
|
||||
} else {
|
||||
console.warn(
|
||||
'Could not refetch list details: list_id_for_refetch not provided and no currentList available.',
|
||||
)
|
||||
const getPaidAmountForSplit = (splitId: number): number => {
|
||||
let totalPaid = 0
|
||||
if (currentList.value && currentList.value.expenses) {
|
||||
for (const expense of currentList.value.expenses) {
|
||||
const split = expense.splits.find((s) => s.id === splitId)
|
||||
if (split && split.settlement_activities) {
|
||||
totalPaid = split.settlement_activities.reduce((sum, activity) => {
|
||||
return sum + parseFloat(activity.amount_paid)
|
||||
}, 0)
|
||||
break
|
||||
}
|
||||
|
||||
this.isSettlingSplit = false
|
||||
return true // Indicate success
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.detail || err.message || 'Failed to settle expense split.'
|
||||
this.error = errorMessage
|
||||
console.error('Error settling expense split:', err)
|
||||
this.isSettlingSplit = false
|
||||
return false // Indicate failure
|
||||
}
|
||||
},
|
||||
}
|
||||
return totalPaid
|
||||
}
|
||||
|
||||
setError(errorMessage: string) {
|
||||
this.error = errorMessage
|
||||
this.isLoading = false
|
||||
},
|
||||
},
|
||||
const getExpenseSplitById = (splitId: number): ExpenseSplit | undefined => {
|
||||
if (!currentList.value || !currentList.value.expenses) return undefined
|
||||
for (const expense of currentList.value.expenses) {
|
||||
const split = expense.splits.find((s) => s.id === splitId)
|
||||
if (split) return split
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
getters: {
|
||||
getList(state: ListDetailState): ListWithExpenses | null {
|
||||
return state.currentList
|
||||
},
|
||||
getExpenses(state: ListDetailState): Expense[] {
|
||||
return state.currentList?.expenses || []
|
||||
},
|
||||
getPaidAmountForSplit:
|
||||
(state: ListDetailState) =>
|
||||
(splitId: number): number => {
|
||||
let totalPaid = 0
|
||||
if (state.currentList && state.currentList.expenses) {
|
||||
for (const expense of state.currentList.expenses) {
|
||||
const split = expense.splits.find((s) => s.id === splitId)
|
||||
if (split && split.settlement_activities) {
|
||||
totalPaid = split.settlement_activities.reduce((sum, activity) => {
|
||||
return sum + parseFloat(activity.amount_paid)
|
||||
}, 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalPaid
|
||||
},
|
||||
getExpenseSplitById:
|
||||
(state: ListDetailState) =>
|
||||
(splitId: number): ExpenseSplit | undefined => {
|
||||
if (!state.currentList || !state.currentList.expenses) return undefined
|
||||
for (const expense of state.currentList.expenses) {
|
||||
const split = expense.splits.find((s) => s.id === splitId)
|
||||
if (split) return split
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
// Actions (as functions)
|
||||
async function fetchListWithExpenses(listId: string) {
|
||||
if (!listId || listId === 'undefined' || listId === 'null') {
|
||||
error.value = 'Invalid list ID provided.'
|
||||
console.warn(`fetchListWithExpenses called with invalid ID: ${listId}`)
|
||||
return
|
||||
}
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
// Get list details
|
||||
const listEndpoint = API_ENDPOINTS.LISTS.BY_ID(listId)
|
||||
const listResponse = await apiClient.get(listEndpoint)
|
||||
const listData = listResponse.data as List
|
||||
|
||||
// Get expenses for this list
|
||||
const expensesEndpoint = API_ENDPOINTS.LISTS.EXPENSES(listId)
|
||||
const expensesResponse = await apiClient.get(expensesEndpoint)
|
||||
const expensesData = expensesResponse.data as Expense[]
|
||||
|
||||
// Combine into ListWithExpenses
|
||||
currentList.value = {
|
||||
...listData,
|
||||
expenses: expensesData,
|
||||
} as ListWithExpenses
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || err.message || 'Failed to fetch list details'
|
||||
currentList.value = null
|
||||
console.error('Error fetching list details:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function settleExpenseSplit(payload: {
|
||||
list_id_for_refetch: string
|
||||
expense_split_id: number
|
||||
activity_data: SettlementActivityCreate
|
||||
}): Promise<boolean> {
|
||||
isSettlingSplit.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const endpoint = `/financials/expense_splits/${payload.expense_split_id}/settle`
|
||||
await apiClient.post(endpoint, payload.activity_data)
|
||||
|
||||
if (payload.list_id_for_refetch) {
|
||||
await fetchListWithExpenses(payload.list_id_for_refetch)
|
||||
} else if (currentList.value?.id) {
|
||||
await fetchListWithExpenses(String(currentList.value.id))
|
||||
} else {
|
||||
console.warn(
|
||||
'Could not refetch list details: list_id_for_refetch not provided and no currentList available.',
|
||||
)
|
||||
}
|
||||
|
||||
isSettlingSplit.value = false
|
||||
return true
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.detail || err.message || 'Failed to settle expense split.'
|
||||
error.value = errorMessage
|
||||
console.error('Error settling expense split:', err)
|
||||
isSettlingSplit.value = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function setError(errorMessage: string) {
|
||||
error.value = errorMessage
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
currentList,
|
||||
isLoading,
|
||||
error,
|
||||
isSettlingSplit,
|
||||
fetchListWithExpenses,
|
||||
settleExpenseSplit,
|
||||
setError,
|
||||
getExpenses,
|
||||
getPaidAmountForSplit,
|
||||
getExpenseSplitById,
|
||||
}
|
||||
})
|
||||
|
||||
// Assuming List interface might be defined in fe/src/types/list.ts
|
||||
|
@ -101,7 +101,7 @@ export interface Expense {
|
||||
description: string
|
||||
total_amount: string // String representation of Decimal
|
||||
currency: string
|
||||
expense_date: string
|
||||
expense_date?: string
|
||||
split_type: string
|
||||
list_id?: number | null
|
||||
group_id?: number | null
|
||||
@ -126,29 +126,3 @@ export interface Expense {
|
||||
|
||||
export type SplitType = 'EQUAL' | 'EXACT_AMOUNTS' | 'PERCENTAGE' | 'SHARES' | 'ITEM_BASED';
|
||||
export type SettlementStatus = 'unpaid' | 'paid' | 'partially_paid';
|
||||
|
||||
export interface Expense {
|
||||
id: number;
|
||||
description: string;
|
||||
total_amount: string; // Decimal is string
|
||||
currency: string;
|
||||
expense_date?: string;
|
||||
split_type: SplitType;
|
||||
list_id?: number;
|
||||
group_id?: number;
|
||||
item_id?: number;
|
||||
paid_by_user_id: number;
|
||||
is_recurring: boolean;
|
||||
recurrence_pattern?: any;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
version: number;
|
||||
created_by_user_id: number;
|
||||
splits: ExpenseSplit[];
|
||||
paid_by_user?: UserPublic;
|
||||
overall_settlement_status: SettlementStatus;
|
||||
next_occurrence?: string;
|
||||
last_occurrence?: string;
|
||||
parent_expense_id?: number;
|
||||
generated_expenses: Expense[];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user