Compare commits
No commits in common. "f3fdbc059256962ab065c3e3f0c86f242dbf71cd" and "1f7abcbd85211dda680f280649d9ff9aaca9639b" have entirely different histories.
f3fdbc0592
...
1f7abcbd85
@ -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"])
|
@ -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"])
|
||||||
|
@ -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
|
|
@ -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()
|
||||||
|
|
||||||
|
@ -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`,
|
||||||
|
104
fe/src/main.ts
104
fe/src/main.ts
@ -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');
|
@ -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>
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user