Compare commits
20 Commits
1f7abcbd85
...
f3fdbc0592
Author | SHA1 | Date | |
---|---|---|---|
f3fdbc0592 | |||
![]() |
5c882996a9 | ||
![]() |
6306e70df7 | ||
![]() |
dbfbe7922e | ||
![]() |
57b913d135 | ||
![]() |
d623c4b27c | ||
![]() |
fc49e830fc | ||
![]() |
af6324ddef | ||
![]() |
6924a016c8 | ||
![]() |
0fcc94ae8d | ||
![]() |
c0aa654e83 | ||
![]() |
ec361fe9ab | ||
![]() |
9d404d04d5 | ||
![]() |
92c70813fb | ||
![]() |
82205f6158 | ||
![]() |
2a2045c24a | ||
![]() |
c1ebd16e5a | ||
![]() |
554814ad63 | ||
![]() |
f2609f53ec | ||
![]() |
4fef642970 |
@ -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(ocr.router, prefix="/ocr", tags=["OCR"])
|
||||
api_router_v1.include_router(costs.router, prefix="/costs", tags=["Costs"])
|
||||
api_router_v1.include_router(financials.router)
|
||||
api_router_v1.include_router(financials.router, prefix="/financials", tags=["Financials"])
|
||||
api_router_v1.include_router(chores.router, prefix="/chores", tags=["Chores"])
|
||||
# Add other v1 endpoint routers here later
|
||||
# e.g., api_router_v1.include_router(users.router, prefix="/users", tags=["Users"])
|
@ -39,7 +39,7 @@ router = APIRouter()
|
||||
# --- Helper for permissions ---
|
||||
async def check_list_access_for_financials(db: AsyncSession, list_id: int, user_id: int, action: str = "access financial data for"):
|
||||
try:
|
||||
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=user_id, require_member=True)
|
||||
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=user_id, require_creator=False)
|
||||
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}")
|
||||
raise ListPermissionError(list_id, action=action)
|
||||
@ -135,17 +135,41 @@ async def get_expense(
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to view this expense")
|
||||
return expense
|
||||
|
||||
@router.get("/lists/{list_id}/expenses", response_model=PyList[ExpensePublic], summary="List Expenses for a List", tags=["Expenses", "Lists"])
|
||||
async def list_list_expenses(
|
||||
list_id: int,
|
||||
@router.get("/expenses", response_model=PyList[ExpensePublic], summary="List Expenses", tags=["Expenses"])
|
||||
async def list_expenses(
|
||||
list_id: Optional[int] = Query(None, description="Filter by list ID"),
|
||||
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),
|
||||
limit: int = Query(100, ge=1, le=200),
|
||||
db: AsyncSession = Depends(get_transactional_session),
|
||||
current_user: UserModel = Depends(current_active_user),
|
||||
):
|
||||
logger.info(f"User {current_user.email} listing expenses for list ID {list_id}")
|
||||
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)
|
||||
"""
|
||||
List expenses with optional filters.
|
||||
If list_id is provided, returns expenses for that list (user must have list access).
|
||||
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
|
||||
|
||||
@router.get("/groups/{group_id}/expenses", response_model=PyList[ExpensePublic], summary="List Expenses for a Group", tags=["Expenses", "Groups"])
|
||||
|
@ -13,6 +13,7 @@ from app.schemas.message import Message # For simple responses
|
||||
from app.crud import list as crud_list
|
||||
from app.crud import group as crud_group # Need for group membership check
|
||||
from app.schemas.list import ListStatus
|
||||
from app.schemas.expense import ExpensePublic # Import ExpensePublic
|
||||
from app.core.exceptions import (
|
||||
GroupMembershipError,
|
||||
ListNotFoundError,
|
||||
@ -215,24 +216,53 @@ async def read_list_status(
|
||||
current_user: UserModel = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Retrieves the last update time for the list and its items, plus item count.
|
||||
Used for polling to check if a full refresh is needed.
|
||||
Requires user to have permission to view the list.
|
||||
Retrieves the completion status for a specific list
|
||||
if the user has permission (creator or group member).
|
||||
"""
|
||||
# Verify user has access to the list first
|
||||
logger.info(f"User {current_user.email} requesting status for list ID: {list_id}")
|
||||
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
|
||||
if not list_db:
|
||||
# Check if list exists at all for correct error code
|
||||
exists = await crud_list.get_list_by_id(db, list_id)
|
||||
if not exists:
|
||||
raise ListNotFoundError(list_id)
|
||||
raise ListPermissionError(list_id, "access this list's status")
|
||||
|
||||
# Calculate status
|
||||
total_items = len(list_db.items)
|
||||
completed_items = sum(1 for item in list_db.items if item.is_complete)
|
||||
|
||||
try:
|
||||
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
|
||||
)
|
||||
|
||||
# Fetch the status details
|
||||
list_status = await crud_list.get_list_status(db=db, list_id=list_id)
|
||||
if not list_status:
|
||||
# Should not happen if check_list_permission passed, but handle defensively
|
||||
logger.error(f"Could not retrieve status for list {list_id} even though permission check passed.")
|
||||
raise ListStatusNotFoundError(list_id)
|
||||
|
||||
return list_status
|
||||
@router.get(
|
||||
"/{list_id}/expenses",
|
||||
response_model=PyList[ExpensePublic],
|
||||
summary="Get Expenses for List",
|
||||
tags=["Lists", "Expenses"]
|
||||
)
|
||||
async def read_list_expenses(
|
||||
list_id: int,
|
||||
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
|
@ -203,7 +203,8 @@ async def create_expense(db: AsyncSession, expense_in: ExpenseCreate, current_us
|
||||
selectinload(ExpenseModel.list),
|
||||
selectinload(ExpenseModel.group),
|
||||
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)
|
||||
@ -535,6 +536,7 @@ async def get_expense_by_id(db: AsyncSession, expense_id: int) -> Optional[Expen
|
||||
select(ExpenseModel)
|
||||
.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),
|
||||
@ -550,7 +552,10 @@ async def get_expenses_for_list(db: AsyncSession, list_id: int, skip: int = 0, l
|
||||
.where(ExpenseModel.list_id == list_id)
|
||||
.order_by(ExpenseModel.expense_date.desc(), ExpenseModel.created_at.desc())
|
||||
.offset(skip).limit(limit)
|
||||
.options(selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user))) # Also load user for each split
|
||||
.options(
|
||||
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user)),
|
||||
selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.settlement_activities))
|
||||
)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
@ -560,7 +565,49 @@ async def get_expenses_for_group(db: AsyncSession, group_id: int, skip: int = 0,
|
||||
.where(ExpenseModel.group_id == group_id)
|
||||
.order_by(ExpenseModel.expense_date.desc(), ExpenseModel.created_at.desc())
|
||||
.offset(skip).limit(limit)
|
||||
.options(selectinload(ExpenseModel.splits).options(selectinload(ExpenseSplitModel.user)))
|
||||
.options(
|
||||
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()
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
export const API_VERSION = 'v1'
|
||||
|
||||
// API Base URL
|
||||
export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'https://mitlistbe.mohamad.dev'
|
||||
export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'http://localhost:8000'
|
||||
|
||||
// API Endpoints
|
||||
export const API_ENDPOINTS = {
|
||||
@ -34,6 +34,7 @@ export const API_ENDPOINTS = {
|
||||
BY_ID: (id: string) => `/lists/${id}`,
|
||||
ITEMS: (listId: string) => `/lists/${listId}/items`,
|
||||
ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`,
|
||||
EXPENSES: (listId: string) => `/lists/${listId}/expenses`,
|
||||
SHARE: (listId: string) => `/lists/${listId}/share`,
|
||||
UNSHARE: (listId: string) => `/lists/${listId}/unshare`,
|
||||
COMPLETE: (listId: string) => `/lists/${listId}/complete`,
|
||||
|
104
fe/src/main.ts
104
fe/src/main.ts
@ -1,25 +1,27 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import * as Sentry from '@sentry/vue';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import enMessages from './i18n/en.json'; // Import en.json directly
|
||||
import deMessages from './i18n/de.json';
|
||||
import frMessages from './i18n/fr.json';
|
||||
import esMessages from './i18n/es.json';
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { BrowserTracing } from '@sentry/tracing'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import enMessages from './i18n/en.json' // Import en.json directly
|
||||
import deMessages from './i18n/de.json'
|
||||
import frMessages from './i18n/fr.json'
|
||||
import esMessages from './i18n/es.json'
|
||||
|
||||
// Global styles
|
||||
import './assets/main.scss';
|
||||
import './assets/main.scss'
|
||||
|
||||
// API client (from your axios boot file)
|
||||
import { api, globalAxios } from '@/services/api'; // Renamed from boot/axios to services/api
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { api, globalAxios } from '@/services/api' // Renamed from boot/axios to services/api
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// Vue I18n setup (from your i18n boot file)
|
||||
// // export type MessageLanguages = keyof typeof messages;
|
||||
// // export type MessageSchema = (typeof messages)['en-US'];
|
||||
// // export type MessageLanguages = keyof typeof messages;
|
||||
// // export type MessageSchema = (typeof messages)['en-US'];
|
||||
|
||||
// // declare module 'vue-i18n' {
|
||||
// // export interface DefineLocaleMessage extends MessageSchema {}
|
||||
@ -29,52 +31,52 @@ import { useAuthStore } from '@/stores/auth';
|
||||
// // export interface DefineNumberFormat {}
|
||||
// // }
|
||||
const i18n = createI18n({
|
||||
legacy: false, // Recommended for Vue 3
|
||||
locale: 'en', // Default locale
|
||||
fallbackLocale: 'en', // Fallback locale
|
||||
messages: {
|
||||
en: enMessages,
|
||||
de: deMessages,
|
||||
fr: frMessages,
|
||||
es: esMessages,
|
||||
},
|
||||
});
|
||||
legacy: false, // Recommended for Vue 3
|
||||
locale: 'en', // Default locale
|
||||
fallbackLocale: 'en', // Fallback locale
|
||||
messages: {
|
||||
en: enMessages,
|
||||
de: deMessages,
|
||||
fr: frMessages,
|
||||
es: esMessages,
|
||||
},
|
||||
})
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
|
||||
// Initialize Sentry
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||
integrations: [
|
||||
new BrowserTracing({
|
||||
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
|
||||
tracingOrigins: ['localhost', /^\//],
|
||||
}),
|
||||
],
|
||||
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 1.0,
|
||||
// Set environment
|
||||
environment: import.meta.env.MODE,
|
||||
});
|
||||
app,
|
||||
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||
integrations: [
|
||||
new BrowserTracing({
|
||||
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
|
||||
tracingOrigins: ['localhost', /^\//],
|
||||
}),
|
||||
],
|
||||
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 1.0,
|
||||
// Set environment
|
||||
environment: import.meta.env.MODE,
|
||||
})
|
||||
|
||||
// Initialize auth state before mounting the app
|
||||
const authStore = useAuthStore();
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.accessToken) {
|
||||
authStore.fetchCurrentUser().catch(error => {
|
||||
console.error('Failed to initialize current user state:', error);
|
||||
// The fetchCurrentUser action handles token clearing on failure.
|
||||
});
|
||||
authStore.fetchCurrentUser().catch((error) => {
|
||||
console.error('Failed to initialize current user state:', error)
|
||||
// The fetchCurrentUser action handles token clearing on failure.
|
||||
})
|
||||
}
|
||||
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
|
||||
// Make API instance globally available (optional, prefer provide/inject or store)
|
||||
app.config.globalProperties.$api = api;
|
||||
app.config.globalProperties.$axios = globalAxios; // The original axios instance if needed
|
||||
app.config.globalProperties.$api = api
|
||||
app.config.globalProperties.$axios = globalAxios // The original axios instance if needed
|
||||
|
||||
app.mount('#app');
|
||||
app.mount('#app')
|
||||
|
@ -9,7 +9,7 @@
|
||||
</VAlert>
|
||||
|
||||
<VCard v-else-if="lists.length === 0 && !loading" variant="empty-state" empty-icon="clipboard"
|
||||
:empty-title="t(noListsMessageKey.value)">
|
||||
:empty-title="t(noListsMessageKey)">
|
||||
<template #default>
|
||||
<p v-if="!currentGroupId">{{ t('listsPage.emptyState.personalGlobalInfo') }}</p>
|
||||
<p v-else>{{ t('listsPage.emptyState.groupSpecificInfo') }}</p>
|
||||
@ -74,7 +74,6 @@ import { useStorage } from '@vueuse/core';
|
||||
import VAlert from '@/components/valerie/VAlert.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 { animate } from 'motion';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@ -194,33 +193,38 @@ const loadCachedData = () => {
|
||||
};
|
||||
|
||||
const fetchLists = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const endpoint = currentGroupId.value
|
||||
? API_ENDPOINTS.GROUPS.LISTS(String(currentGroupId.value))
|
||||
: API_ENDPOINTS.LISTS.BASE;
|
||||
const response = await apiClient.get(endpoint);
|
||||
lists.value = response.data as (List & { items: Item[] })[];
|
||||
cachedLists.value = JSON.parse(JSON.stringify(response.data));
|
||||
lists.value = (response.data as (List & { items: Item[] })[]).map(list => ({
|
||||
...list,
|
||||
items: list.items || []
|
||||
}));
|
||||
cachedLists.value = JSON.parse(JSON.stringify(lists.value));
|
||||
cachedTimestamp.value = Date.now();
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : t('listsPage.errors.fetchFailed');
|
||||
console.error(error.value, err);
|
||||
if (cachedLists.value.length === 0) lists.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchListsAndGroups = async () => {
|
||||
loading.value = true;
|
||||
await Promise.all([
|
||||
fetchLists(),
|
||||
fetchAllAccessibleGroups()
|
||||
]);
|
||||
await fetchCurrentViewGroupName();
|
||||
loading.value = false;
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchLists(),
|
||||
fetchAllAccessibleGroups()
|
||||
]);
|
||||
await fetchCurrentViewGroupName();
|
||||
} catch (err) {
|
||||
console.error('Error in fetchListsAndGroups:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const availableGroupsForModal = computed(() => {
|
||||
@ -236,7 +240,10 @@ const getGroupName = (groupId?: number | null): string | undefined => {
|
||||
}
|
||||
|
||||
const onListCreated = (newList: List & { items: Item[] }) => {
|
||||
lists.value.push(newList);
|
||||
lists.value.push({
|
||||
...newList,
|
||||
items: newList.items || []
|
||||
});
|
||||
cachedLists.value = JSON.parse(JSON.stringify(lists.value));
|
||||
cachedTimestamp.value = Date.now();
|
||||
// Consider animating new list card in if desired
|
||||
@ -335,7 +342,11 @@ const addNewItem = async (list: List, event: Event) => {
|
||||
list.items = list.items.filter(i => i.tempId !== localTempId);
|
||||
inputElement.value = originalInputValue;
|
||||
inputElement.disabled = false;
|
||||
animate(inputElement, { borderColor: ['red', '#ccc'] }, { duration: 0.5 });
|
||||
inputElement.style.transition = 'border-color 0.5s ease';
|
||||
inputElement.style.borderColor = 'red';
|
||||
setTimeout(() => {
|
||||
inputElement.style.borderColor = '#ccc';
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
@ -887,4 +898,4 @@ onUnmounted(() => {
|
||||
.item-appear {
|
||||
animation: item-appear 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Expense, RecurrencePattern } from '@/types/expense'
|
||||
import { api } from '@/services/api'
|
||||
import { api, API_ENDPOINTS } from '@/services/api'
|
||||
|
||||
export interface CreateExpenseData {
|
||||
description: string
|
||||
@ -32,21 +32,21 @@ export interface UpdateExpenseData extends Partial<CreateExpenseData> {
|
||||
|
||||
export const expenseService = {
|
||||
async createExpense(data: CreateExpenseData): Promise<Expense> {
|
||||
const response = await api.post<Expense>('/expenses', data)
|
||||
const response = await api.post<Expense>(API_ENDPOINTS.FINANCIALS.EXPENSES, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async updateExpense(id: number, data: UpdateExpenseData): Promise<Expense> {
|
||||
const response = await api.put<Expense>(`/expenses/${id}`, data)
|
||||
const response = await api.put<Expense>(API_ENDPOINTS.FINANCIALS.EXPENSE(id.toString()), data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async deleteExpense(id: number): Promise<void> {
|
||||
await api.delete(`/expenses/${id}`)
|
||||
await api.delete(API_ENDPOINTS.FINANCIALS.EXPENSE(id.toString()))
|
||||
},
|
||||
|
||||
async getExpense(id: number): Promise<Expense> {
|
||||
const response = await api.get<Expense>(`/expenses/${id}`)
|
||||
const response = await api.get<Expense>(API_ENDPOINTS.FINANCIALS.EXPENSE(id.toString()))
|
||||
return response.data
|
||||
},
|
||||
|
||||
@ -55,7 +55,7 @@ export const expenseService = {
|
||||
group_id?: number
|
||||
isRecurring?: boolean
|
||||
}): Promise<Expense[]> {
|
||||
const response = await api.get<Expense[]>('/expenses', { params })
|
||||
const response = await api.get<Expense[]>(API_ENDPOINTS.FINANCIALS.EXPENSES, { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { apiClient, API_ENDPOINTS } from '@/services/api'
|
||||
import type { Expense, ExpenseSplit, SettlementActivity, SettlementActivityPublic } from '@/types/expense'
|
||||
import { apiClient, API_ENDPOINTS } from '@/config/api'
|
||||
import type {
|
||||
Expense,
|
||||
ExpenseSplit,
|
||||
SettlementActivity,
|
||||
SettlementActivityPublic,
|
||||
} from '@/types/expense'
|
||||
import type { SettlementActivityCreate } from '@/types/expense'
|
||||
import type { List } from '@/types/list'
|
||||
import type { AxiosResponse } from 'axios'
|
||||
@ -30,9 +35,21 @@ export const useListDetailStore = defineStore('listDetail', {
|
||||
this.isLoading = true
|
||||
this.error = null
|
||||
try {
|
||||
const endpoint = API_ENDPOINTS.LISTS.BY_ID(listId)
|
||||
const response = await apiClient.get(endpoint)
|
||||
this.currentList = response.data as ListWithExpenses
|
||||
// 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
|
||||
@ -50,11 +67,9 @@ export const useListDetailStore = defineStore('listDetail', {
|
||||
this.isSettlingSplit = true
|
||||
this.error = null
|
||||
try {
|
||||
// Call the actual API endpoint
|
||||
const response = await apiClient.settleExpenseSplit(
|
||||
payload.expense_split_id,
|
||||
payload.activity_data,
|
||||
)
|
||||
// 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)
|
||||
console.log('Settlement activity created:', response.data)
|
||||
|
||||
// Refresh list data to show updated statuses
|
||||
@ -96,31 +111,31 @@ export const useListDetailStore = defineStore('listDetail', {
|
||||
},
|
||||
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
|
||||
(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) return split
|
||||
if (split && split.settlement_activities) {
|
||||
totalPaid = split.settlement_activities.reduce((sum, activity) => {
|
||||
return sum + parseFloat(activity.amount_paid)
|
||||
}, 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
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
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user