Compare commits

..

No commits in common. "f3fdbc059256962ab065c3e3f0c86f242dbf71cd" and "1f7abcbd85211dda680f280649d9ff9aaca9639b" have entirely different histories.

9 changed files with 134 additions and 264 deletions

View File

@ -19,7 +19,7 @@ api_router_v1.include_router(lists.router, prefix="/lists", tags=["Lists"])
api_router_v1.include_router(items.router, tags=["Items"]) api_router_v1.include_router(items.router, tags=["Items"])
api_router_v1.include_router(ocr.router, prefix="/ocr", tags=["OCR"]) api_router_v1.include_router(ocr.router, prefix="/ocr", tags=["OCR"])
api_router_v1.include_router(costs.router, prefix="/costs", tags=["Costs"]) api_router_v1.include_router(costs.router, prefix="/costs", tags=["Costs"])
api_router_v1.include_router(financials.router, prefix="/financials", tags=["Financials"]) api_router_v1.include_router(financials.router)
api_router_v1.include_router(chores.router, prefix="/chores", tags=["Chores"]) api_router_v1.include_router(chores.router, prefix="/chores", tags=["Chores"])
# Add other v1 endpoint routers here later # Add other v1 endpoint routers here later
# e.g., api_router_v1.include_router(users.router, prefix="/users", tags=["Users"]) # e.g., api_router_v1.include_router(users.router, prefix="/users", tags=["Users"])

View File

@ -39,7 +39,7 @@ router = APIRouter()
# --- Helper for permissions --- # --- Helper for permissions ---
async def check_list_access_for_financials(db: AsyncSession, list_id: int, user_id: int, action: str = "access financial data for"): async def check_list_access_for_financials(db: AsyncSession, list_id: int, user_id: int, action: str = "access financial data for"):
try: try:
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=user_id, require_creator=False) await crud_list.check_list_permission(db=db, list_id=list_id, user_id=user_id, require_member=True)
except ListPermissionError as e: except ListPermissionError as e:
logger.warning(f"ListPermissionError in check_list_access_for_financials for list {list_id}, user {user_id}, action '{action}': {e.detail}") logger.warning(f"ListPermissionError in check_list_access_for_financials for list {list_id}, user {user_id}, action '{action}': {e.detail}")
raise ListPermissionError(list_id, action=action) raise ListPermissionError(list_id, action=action)
@ -135,41 +135,17 @@ async def get_expense(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to view this expense") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to view this expense")
return expense return expense
@router.get("/expenses", response_model=PyList[ExpensePublic], summary="List Expenses", tags=["Expenses"]) @router.get("/lists/{list_id}/expenses", response_model=PyList[ExpensePublic], summary="List Expenses for a List", tags=["Expenses", "Lists"])
async def list_expenses( async def list_list_expenses(
list_id: Optional[int] = Query(None, description="Filter by list ID"), list_id: int,
group_id: Optional[int] = Query(None, description="Filter by group ID"),
isRecurring: Optional[bool] = Query(None, description="Filter by recurring expenses"),
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=200), limit: int = Query(100, ge=1, le=200),
db: AsyncSession = Depends(get_transactional_session), db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user), current_user: UserModel = Depends(current_active_user),
): ):
""" logger.info(f"User {current_user.email} listing expenses for list ID {list_id}")
List expenses with optional filters. await check_list_access_for_financials(db, list_id, current_user.id)
If list_id is provided, returns expenses for that list (user must have list access). expenses = await crud_expense.get_expenses_for_list(db, list_id=list_id, skip=skip, limit=limit)
If group_id is provided, returns expenses for that group (user must be group member).
If both are provided, returns expenses for the list (list_id takes precedence).
If neither is provided, returns all expenses the user has access to.
"""
logger.info(f"User {current_user.email} listing expenses with filters: list_id={list_id}, group_id={group_id}, isRecurring={isRecurring}")
if list_id:
# Use existing list expenses endpoint logic
await check_list_access_for_financials(db, list_id, current_user.id)
expenses = await crud_expense.get_expenses_for_list(db, list_id=list_id, skip=skip, limit=limit)
elif group_id:
# Use existing group expenses endpoint logic
await crud_group.check_group_membership(db, group_id=group_id, user_id=current_user.id, action="list expenses for")
expenses = await crud_expense.get_expenses_for_group(db, group_id=group_id, skip=skip, limit=limit)
else:
# Get all expenses the user has access to (user's personal expenses + group expenses + list expenses)
expenses = await crud_expense.get_user_accessible_expenses(db, user_id=current_user.id, skip=skip, limit=limit)
# Apply recurring filter if specified
if isRecurring is not None:
expenses = [expense for expense in expenses if bool(expense.recurrence_rule) == isRecurring]
return expenses return expenses
@router.get("/groups/{group_id}/expenses", response_model=PyList[ExpensePublic], summary="List Expenses for a Group", tags=["Expenses", "Groups"]) @router.get("/groups/{group_id}/expenses", response_model=PyList[ExpensePublic], summary="List Expenses for a Group", tags=["Expenses", "Groups"])

View File

@ -13,7 +13,6 @@ from app.schemas.message import Message # For simple responses
from app.crud import list as crud_list from app.crud import list as crud_list
from app.crud import group as crud_group # Need for group membership check from app.crud import group as crud_group # Need for group membership check
from app.schemas.list import ListStatus from app.schemas.list import ListStatus
from app.schemas.expense import ExpensePublic # Import ExpensePublic
from app.core.exceptions import ( from app.core.exceptions import (
GroupMembershipError, GroupMembershipError,
ListNotFoundError, ListNotFoundError,
@ -216,53 +215,24 @@ async def read_list_status(
current_user: UserModel = Depends(current_active_user), current_user: UserModel = Depends(current_active_user),
): ):
""" """
Retrieves the completion status for a specific list Retrieves the last update time for the list and its items, plus item count.
if the user has permission (creator or group member). Used for polling to check if a full refresh is needed.
Requires user to have permission to view the list.
""" """
logger.info(f"User {current_user.email} requesting status for list ID: {list_id}") # Verify user has access to the list first
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id) list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
if not list_db:
# Calculate status # Check if list exists at all for correct error code
total_items = len(list_db.items) exists = await crud_list.get_list_by_id(db, list_id)
completed_items = sum(1 for item in list_db.items if item.is_complete) if not exists:
raise ListNotFoundError(list_id)
try: raise ListPermissionError(list_id, "access this list's status")
completion_percentage = (completed_items / total_items * 100) if total_items > 0 else 0
except ZeroDivisionError:
completion_percentage = 0
return ListStatus(
list_id=list_db.id,
total_items=total_items,
completed_items=completed_items,
completion_percentage=completion_percentage,
last_updated=list_db.updated_at
)
@router.get( # Fetch the status details
"/{list_id}/expenses", list_status = await crud_list.get_list_status(db=db, list_id=list_id)
response_model=PyList[ExpensePublic], if not list_status:
summary="Get Expenses for List", # Should not happen if check_list_permission passed, but handle defensively
tags=["Lists", "Expenses"] logger.error(f"Could not retrieve status for list {list_id} even though permission check passed.")
) raise ListStatusNotFoundError(list_id)
async def read_list_expenses(
list_id: int, return list_status
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=200),
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""
Retrieves expenses associated with a specific list
if the user has permission (creator or group member).
"""
from app.crud import expense as crud_expense
logger.info(f"User {current_user.email} requesting expenses for list ID: {list_id}")
# Check if user has permission to access this list
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
# Get expenses for this list
expenses = await crud_expense.get_expenses_for_list(db, list_id=list_id, skip=skip, limit=limit)
return expenses

View File

@ -203,8 +203,7 @@ async def create_expense(db: AsyncSession, expense_in: ExpenseCreate, current_us
selectinload(ExpenseModel.list), selectinload(ExpenseModel.list),
selectinload(ExpenseModel.group), selectinload(ExpenseModel.group),
selectinload(ExpenseModel.item), selectinload(ExpenseModel.item),
selectinload(ExpenseModel.splits).selectinload(ExpenseSplitModel.user), selectinload(ExpenseModel.splits).selectinload(ExpenseSplitModel.user)
selectinload(ExpenseModel.splits).selectinload(ExpenseSplitModel.settlement_activities)
) )
) )
result = await db.execute(stmt) result = await db.execute(stmt)
@ -536,7 +535,6 @@ async def get_expense_by_id(db: AsyncSession, expense_id: int) -> Optional[Expen
select(ExpenseModel) select(ExpenseModel)
.options( .options(
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user)), selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user)),
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.settlement_activities)),
selectinload(ExpenseModel.paid_by_user), selectinload(ExpenseModel.paid_by_user),
selectinload(ExpenseModel.list), selectinload(ExpenseModel.list),
selectinload(ExpenseModel.group), selectinload(ExpenseModel.group),
@ -552,10 +550,7 @@ async def get_expenses_for_list(db: AsyncSession, list_id: int, skip: int = 0, l
.where(ExpenseModel.list_id == list_id) .where(ExpenseModel.list_id == list_id)
.order_by(ExpenseModel.expense_date.desc(), ExpenseModel.created_at.desc()) .order_by(ExpenseModel.expense_date.desc(), ExpenseModel.created_at.desc())
.offset(skip).limit(limit) .offset(skip).limit(limit)
.options( .options(selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user))) # Also load user for each split
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user)),
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.settlement_activities))
)
) )
return result.scalars().all() return result.scalars().all()
@ -565,49 +560,7 @@ async def get_expenses_for_group(db: AsyncSession, group_id: int, skip: int = 0,
.where(ExpenseModel.group_id == group_id) .where(ExpenseModel.group_id == group_id)
.order_by(ExpenseModel.expense_date.desc(), ExpenseModel.created_at.desc()) .order_by(ExpenseModel.expense_date.desc(), ExpenseModel.created_at.desc())
.offset(skip).limit(limit) .offset(skip).limit(limit)
.options( .options(selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user)))
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user)),
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.settlement_activities))
)
)
return result.scalars().all()
async def get_user_accessible_expenses(db: AsyncSession, user_id: int, skip: int = 0, limit: int = 100) -> Sequence[ExpenseModel]:
"""
Get all expenses that a user has access to:
- Expenses they paid for
- Expenses in groups they are members of
- Expenses for lists they have access to
"""
from app.models import UserGroup as UserGroupModel, List as ListModel # Import here to avoid circular imports
# Build the query for accessible expenses
# 1. Expenses paid by the user
paid_by_condition = ExpenseModel.paid_by_user_id == user_id
# 2. Expenses in groups where user is a member
group_member_subquery = select(UserGroupModel.group_id).where(UserGroupModel.user_id == user_id)
group_expenses_condition = ExpenseModel.group_id.in_(group_member_subquery)
# 3. Expenses for lists where user is creator or has access (simplified to creator for now)
user_lists_subquery = select(ListModel.id).where(ListModel.created_by_id == user_id)
list_expenses_condition = ExpenseModel.list_id.in_(user_lists_subquery)
# Combine all conditions with OR
combined_condition = paid_by_condition | group_expenses_condition | list_expenses_condition
result = await db.execute(
select(ExpenseModel)
.where(combined_condition)
.order_by(ExpenseModel.expense_date.desc(), ExpenseModel.created_at.desc())
.offset(skip).limit(limit)
.options(
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user)),
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.settlement_activities)),
selectinload(ExpenseModel.paid_by_user),
selectinload(ExpenseModel.list),
selectinload(ExpenseModel.group)
)
) )
return result.scalars().all() return result.scalars().all()

View File

@ -2,7 +2,7 @@
export const API_VERSION = 'v1' export const API_VERSION = 'v1'
// API Base URL // API Base URL
export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'http://localhost:8000' export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'https://mitlistbe.mohamad.dev'
// API Endpoints // API Endpoints
export const API_ENDPOINTS = { export const API_ENDPOINTS = {
@ -34,7 +34,6 @@ export const API_ENDPOINTS = {
BY_ID: (id: string) => `/lists/${id}`, BY_ID: (id: string) => `/lists/${id}`,
ITEMS: (listId: string) => `/lists/${listId}/items`, ITEMS: (listId: string) => `/lists/${listId}/items`,
ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`, ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`,
EXPENSES: (listId: string) => `/lists/${listId}/expenses`,
SHARE: (listId: string) => `/lists/${listId}/share`, SHARE: (listId: string) => `/lists/${listId}/share`,
UNSHARE: (listId: string) => `/lists/${listId}/unshare`, UNSHARE: (listId: string) => `/lists/${listId}/unshare`,
COMPLETE: (listId: string) => `/lists/${listId}/complete`, COMPLETE: (listId: string) => `/lists/${listId}/complete`,

View File

@ -1,27 +1,25 @@
import { createApp } from 'vue' import { createApp } from 'vue';
import { createPinia } from 'pinia' import { createPinia } from 'pinia';
import * as Sentry from '@sentry/vue' import * as Sentry from '@sentry/vue';
import { BrowserTracing } from '@sentry/tracing' import { BrowserTracing } from '@sentry/tracing';
import App from './App.vue' import App from './App.vue';
import router from './router' import router from './router';
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n';
import enMessages from './i18n/en.json' // Import en.json directly import enMessages from './i18n/en.json'; // Import en.json directly
import deMessages from './i18n/de.json' import deMessages from './i18n/de.json';
import frMessages from './i18n/fr.json' import frMessages from './i18n/fr.json';
import esMessages from './i18n/es.json' import esMessages from './i18n/es.json';
// Global styles // Global styles
import './assets/main.scss' import './assets/main.scss';
// API client (from your axios boot file) // API client (from your axios boot file)
import { api, globalAxios } from '@/services/api' // Renamed from boot/axios to services/api import { api, globalAxios } from '@/services/api'; // Renamed from boot/axios to services/api
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth';
// Vue I18n setup (from your i18n boot file) // Vue I18n setup (from your i18n boot file)
// // export type MessageLanguages = keyof typeof messages; // // export type MessageLanguages = keyof typeof messages;
// // export type MessageSchema = (typeof messages)['en-US']; // // export type MessageSchema = (typeof messages)['en-US'];
// // export type MessageLanguages = keyof typeof messages;
// // export type MessageSchema = (typeof messages)['en-US'];
// // declare module 'vue-i18n' { // // declare module 'vue-i18n' {
// // export interface DefineLocaleMessage extends MessageSchema {} // // export interface DefineLocaleMessage extends MessageSchema {}
@ -31,52 +29,52 @@ import { useAuthStore } from '@/stores/auth'
// // export interface DefineNumberFormat {} // // export interface DefineNumberFormat {}
// // } // // }
const i18n = createI18n({ const i18n = createI18n({
legacy: false, // Recommended for Vue 3 legacy: false, // Recommended for Vue 3
locale: 'en', // Default locale locale: 'en', // Default locale
fallbackLocale: 'en', // Fallback locale fallbackLocale: 'en', // Fallback locale
messages: { messages: {
en: enMessages, en: enMessages,
de: deMessages, de: deMessages,
fr: frMessages, fr: frMessages,
es: esMessages, es: esMessages,
}, },
}) });
const app = createApp(App) const app = createApp(App);
const pinia = createPinia() const pinia = createPinia();
app.use(pinia) app.use(pinia);
// Initialize Sentry // Initialize Sentry
Sentry.init({ Sentry.init({
app, app,
dsn: import.meta.env.VITE_SENTRY_DSN, dsn: import.meta.env.VITE_SENTRY_DSN,
integrations: [ integrations: [
new BrowserTracing({ new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router), routingInstrumentation: Sentry.vueRouterInstrumentation(router),
tracingOrigins: ['localhost', /^\//], tracingOrigins: ['localhost', /^\//],
}), }),
], ],
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
// We recommend adjusting this value in production // We recommend adjusting this value in production
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
// Set environment // Set environment
environment: import.meta.env.MODE, environment: import.meta.env.MODE,
}) });
// Initialize auth state before mounting the app // Initialize auth state before mounting the app
const authStore = useAuthStore() const authStore = useAuthStore();
if (authStore.accessToken) { if (authStore.accessToken) {
authStore.fetchCurrentUser().catch((error) => { authStore.fetchCurrentUser().catch(error => {
console.error('Failed to initialize current user state:', error) console.error('Failed to initialize current user state:', error);
// The fetchCurrentUser action handles token clearing on failure. // The fetchCurrentUser action handles token clearing on failure.
}) });
} }
app.use(router) app.use(router);
app.use(i18n) app.use(i18n);
// Make API instance globally available (optional, prefer provide/inject or store) // Make API instance globally available (optional, prefer provide/inject or store)
app.config.globalProperties.$api = api app.config.globalProperties.$api = api;
app.config.globalProperties.$axios = globalAxios // The original axios instance if needed app.config.globalProperties.$axios = globalAxios; // The original axios instance if needed
app.mount('#app') app.mount('#app');

View File

@ -9,7 +9,7 @@
</VAlert> </VAlert>
<VCard v-else-if="lists.length === 0 && !loading" variant="empty-state" empty-icon="clipboard" <VCard v-else-if="lists.length === 0 && !loading" variant="empty-state" empty-icon="clipboard"
:empty-title="t(noListsMessageKey)"> :empty-title="t(noListsMessageKey.value)">
<template #default> <template #default>
<p v-if="!currentGroupId">{{ t('listsPage.emptyState.personalGlobalInfo') }}</p> <p v-if="!currentGroupId">{{ t('listsPage.emptyState.personalGlobalInfo') }}</p>
<p v-else>{{ t('listsPage.emptyState.groupSpecificInfo') }}</p> <p v-else>{{ t('listsPage.emptyState.groupSpecificInfo') }}</p>
@ -74,6 +74,7 @@ import { useStorage } from '@vueuse/core';
import VAlert from '@/components/valerie/VAlert.vue'; // Adjust path as needed import VAlert from '@/components/valerie/VAlert.vue'; // Adjust path as needed
import VCard from '@/components/valerie/VCard.vue'; // Adjust path as needed import VCard from '@/components/valerie/VCard.vue'; // Adjust path as needed
import VButton from '@/components/valerie/VButton.vue'; // Adjust path as needed import VButton from '@/components/valerie/VButton.vue'; // Adjust path as needed
import { animate } from 'motion';
const { t } = useI18n(); const { t } = useI18n();
@ -193,38 +194,33 @@ const loadCachedData = () => {
}; };
const fetchLists = async () => { const fetchLists = async () => {
loading.value = true;
error.value = null; error.value = null;
try { try {
const endpoint = currentGroupId.value const endpoint = currentGroupId.value
? API_ENDPOINTS.GROUPS.LISTS(String(currentGroupId.value)) ? API_ENDPOINTS.GROUPS.LISTS(String(currentGroupId.value))
: API_ENDPOINTS.LISTS.BASE; : API_ENDPOINTS.LISTS.BASE;
const response = await apiClient.get(endpoint); const response = await apiClient.get(endpoint);
lists.value = (response.data as (List & { items: Item[] })[]).map(list => ({ lists.value = response.data as (List & { items: Item[] })[];
...list, cachedLists.value = JSON.parse(JSON.stringify(response.data));
items: list.items || []
}));
cachedLists.value = JSON.parse(JSON.stringify(lists.value));
cachedTimestamp.value = Date.now(); cachedTimestamp.value = Date.now();
} catch (err: unknown) { } catch (err: unknown) {
error.value = err instanceof Error ? err.message : t('listsPage.errors.fetchFailed'); error.value = err instanceof Error ? err.message : t('listsPage.errors.fetchFailed');
console.error(error.value, err); console.error(error.value, err);
if (cachedLists.value.length === 0) lists.value = []; if (cachedLists.value.length === 0) lists.value = [];
} finally {
loading.value = false;
} }
}; };
const fetchListsAndGroups = async () => { const fetchListsAndGroups = async () => {
loading.value = true; loading.value = true;
try { await Promise.all([
await Promise.all([ fetchLists(),
fetchLists(), fetchAllAccessibleGroups()
fetchAllAccessibleGroups() ]);
]); await fetchCurrentViewGroupName();
await fetchCurrentViewGroupName(); loading.value = false;
} catch (err) {
console.error('Error in fetchListsAndGroups:', err);
} finally {
loading.value = false;
}
}; };
const availableGroupsForModal = computed(() => { const availableGroupsForModal = computed(() => {
@ -240,10 +236,7 @@ const getGroupName = (groupId?: number | null): string | undefined => {
} }
const onListCreated = (newList: List & { items: Item[] }) => { const onListCreated = (newList: List & { items: Item[] }) => {
lists.value.push({ lists.value.push(newList);
...newList,
items: newList.items || []
});
cachedLists.value = JSON.parse(JSON.stringify(lists.value)); cachedLists.value = JSON.parse(JSON.stringify(lists.value));
cachedTimestamp.value = Date.now(); cachedTimestamp.value = Date.now();
// Consider animating new list card in if desired // Consider animating new list card in if desired
@ -342,11 +335,7 @@ const addNewItem = async (list: List, event: Event) => {
list.items = list.items.filter(i => i.tempId !== localTempId); list.items = list.items.filter(i => i.tempId !== localTempId);
inputElement.value = originalInputValue; inputElement.value = originalInputValue;
inputElement.disabled = false; inputElement.disabled = false;
inputElement.style.transition = 'border-color 0.5s ease'; animate(inputElement, { borderColor: ['red', '#ccc'] }, { duration: 0.5 });
inputElement.style.borderColor = 'red';
setTimeout(() => {
inputElement.style.borderColor = '#ccc';
}, 500);
} }
}; };
@ -898,4 +887,4 @@ onUnmounted(() => {
.item-appear { .item-appear {
animation: item-appear 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards; animation: item-appear 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
import type { Expense, RecurrencePattern } from '@/types/expense' import type { Expense, RecurrencePattern } from '@/types/expense'
import { api, API_ENDPOINTS } from '@/services/api' import { api } from '@/services/api'
export interface CreateExpenseData { export interface CreateExpenseData {
description: string description: string
@ -32,21 +32,21 @@ export interface UpdateExpenseData extends Partial<CreateExpenseData> {
export const expenseService = { export const expenseService = {
async createExpense(data: CreateExpenseData): Promise<Expense> { async createExpense(data: CreateExpenseData): Promise<Expense> {
const response = await api.post<Expense>(API_ENDPOINTS.FINANCIALS.EXPENSES, data) const response = await api.post<Expense>('/expenses', data)
return response.data return response.data
}, },
async updateExpense(id: number, data: UpdateExpenseData): Promise<Expense> { async updateExpense(id: number, data: UpdateExpenseData): Promise<Expense> {
const response = await api.put<Expense>(API_ENDPOINTS.FINANCIALS.EXPENSE(id.toString()), data) const response = await api.put<Expense>(`/expenses/${id}`, data)
return response.data return response.data
}, },
async deleteExpense(id: number): Promise<void> { async deleteExpense(id: number): Promise<void> {
await api.delete(API_ENDPOINTS.FINANCIALS.EXPENSE(id.toString())) await api.delete(`/expenses/${id}`)
}, },
async getExpense(id: number): Promise<Expense> { async getExpense(id: number): Promise<Expense> {
const response = await api.get<Expense>(API_ENDPOINTS.FINANCIALS.EXPENSE(id.toString())) const response = await api.get<Expense>(`/expenses/${id}`)
return response.data return response.data
}, },
@ -55,7 +55,7 @@ export const expenseService = {
group_id?: number group_id?: number
isRecurring?: boolean isRecurring?: boolean
}): Promise<Expense[]> { }): Promise<Expense[]> {
const response = await api.get<Expense[]>(API_ENDPOINTS.FINANCIALS.EXPENSES, { params }) const response = await api.get<Expense[]>('/expenses', { params })
return response.data return response.data
}, },

View File

@ -1,11 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { apiClient, API_ENDPOINTS } from '@/config/api' import { apiClient, API_ENDPOINTS } from '@/services/api'
import type { import type { Expense, ExpenseSplit, SettlementActivity, SettlementActivityPublic } from '@/types/expense'
Expense,
ExpenseSplit,
SettlementActivity,
SettlementActivityPublic,
} from '@/types/expense'
import type { SettlementActivityCreate } from '@/types/expense' import type { SettlementActivityCreate } from '@/types/expense'
import type { List } from '@/types/list' import type { List } from '@/types/list'
import type { AxiosResponse } from 'axios' import type { AxiosResponse } from 'axios'
@ -35,21 +30,9 @@ export const useListDetailStore = defineStore('listDetail', {
this.isLoading = true this.isLoading = true
this.error = null this.error = null
try { try {
// Get list details const endpoint = API_ENDPOINTS.LISTS.BY_ID(listId)
const listEndpoint = API_ENDPOINTS.LISTS.BY_ID(listId) const response = await apiClient.get(endpoint)
const listResponse = await apiClient.get(listEndpoint) this.currentList = response.data as ListWithExpenses
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) { } catch (err: any) {
this.error = err.response?.data?.detail || err.message || 'Failed to fetch list details' this.error = err.response?.data?.detail || err.message || 'Failed to fetch list details'
this.currentList = null this.currentList = null
@ -67,9 +50,11 @@ export const useListDetailStore = defineStore('listDetail', {
this.isSettlingSplit = true this.isSettlingSplit = true
this.error = null this.error = null
try { try {
// Call the actual API endpoint using generic post method // Call the actual API endpoint
const endpoint = `/financials/expense_splits/${payload.expense_split_id}/settle` const response = await apiClient.settleExpenseSplit(
const response = await apiClient.post(endpoint, payload.activity_data) payload.expense_split_id,
payload.activity_data,
)
console.log('Settlement activity created:', response.data) console.log('Settlement activity created:', response.data)
// Refresh list data to show updated statuses // Refresh list data to show updated statuses
@ -111,31 +96,31 @@ export const useListDetailStore = defineStore('listDetail', {
}, },
getPaidAmountForSplit: getPaidAmountForSplit:
(state: ListDetailState) => (state: ListDetailState) =>
(splitId: number): number => { (splitId: number): number => {
let totalPaid = 0 let totalPaid = 0
if (state.currentList && state.currentList.expenses) { if (state.currentList && state.currentList.expenses) {
for (const expense of state.currentList.expenses) { for (const expense of state.currentList.expenses) {
const split = expense.splits.find((s) => s.id === splitId) const split = expense.splits.find((s) => s.id === splitId)
if (split && split.settlement_activities) { if (split && split.settlement_activities) {
totalPaid = split.settlement_activities.reduce((sum, activity) => { totalPaid = split.settlement_activities.reduce((sum, activity) => {
return sum + parseFloat(activity.amount_paid) return sum + parseFloat(activity.amount_paid)
}, 0) }, 0)
break break
}
} }
} }
} return totalPaid
return totalPaid },
},
getExpenseSplitById: getExpenseSplitById:
(state: ListDetailState) => (state: ListDetailState) =>
(splitId: number): ExpenseSplit | undefined => { (splitId: number): ExpenseSplit | undefined => {
if (!state.currentList || !state.currentList.expenses) return undefined if (!state.currentList || !state.currentList.expenses) return undefined
for (const expense of state.currentList.expenses) { for (const expense of state.currentList.expenses) {
const split = expense.splits.find((s) => s.id === splitId) const split = expense.splits.find((s) => s.id === splitId)
if (split) return split if (split) return split
} }
return undefined return undefined
}, },
}, },
}) })