Compare commits

...

20 Commits

Author SHA1 Message Date
mo
f3fdbc0592 Merge pull request 'ph4' (#53) from ph4 into prod
Reviewed-on: #53
2025-06-04 17:51:17 +02:00
Mohamad
5c882996a9 Enhance financials API and list expense retrieval
All checks were successful
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Successful in 1m23s
- Updated the `check_list_access_for_financials` function to allow access for list creators and members.
- Refactored the `list_expenses` endpoint to support filtering by `list_id`, `group_id`, and `isRecurring`, providing more flexible expense retrieval options.
- Introduced a new `read_list_expenses` endpoint to fetch expenses associated with a specific list, ensuring proper permission checks.
- Enhanced expense retrieval logic in the `get_expenses_for_list` and `get_user_accessible_expenses` functions to include settlement activities.
- Updated frontend API configuration to reflect new endpoint paths and ensure consistency across the application.
2025-06-04 17:50:19 +02:00
Mohamad
6306e70df7 Merge branch 'ph4' of https://github.com/whtvrboo/mitlist into ph4 2025-06-03 12:07:28 +02:00
whtvrboo
dbfbe7922e
Merge pull request #14 from whtvrboo/fix/expense-api-pathing
Fix: Correct API endpoint pathing for expenses to resolve 404 errors
2025-06-03 12:05:08 +02:00
google-labs-jules[bot]
57b913d135 Fix: Correct API endpoint pathing for expenses to resolve 404 errors
The expenses frontend was encountering 404 errors due to mismatched API paths
between the frontend calls and backend routing.

This commit addresses the issue by:

1. Modifying backend API routing in `be/app/api/v1/api.py`:
   - Added a `/financials` prefix to the `financials.router`. Expense endpoints are now served under `/api/v1/financials/expenses`.

2. Updating frontend API configuration in `fe/src/config/api-config.ts`:
   - Prepended `/api/v1` to all paths within the `API_ENDPOINTS.FINANCIALS` object to match the new backend structure (e.g., `API_ENDPOINTS.FINANCIALS.EXPENSES` is now `/api/v1/financials/expenses`).

3. Updating frontend expense service in `fe/src/services/expenseService.ts`:
   - Replaced hardcoded relative URLs with the updated constants from `API_ENDPOINTS.FINANCIALS`.
   - Ensured `API_ENDPOINTS` is correctly imported.

These changes align the frontend API calls with the backend endpoint definitions,
resolving the 404 errors.
2025-06-03 10:04:42 +00:00
mohamad
d623c4b27c Enhance i18n support and PWA configuration
- Increased the maximum file size for caching in PWA settings from 5MB to 15MB in vite.config.ts.
- Updated import paths for i18n messages in main.ts for consistency.
- Simplified the i18n index by removing unnecessary keys and using shorthand for language imports.
- Added debug output in LoginPage.vue to log current locale and available messages for easier troubleshooting.
2025-06-02 18:07:41 +02:00
mohamad
fc49e830fc Update Dockerfile to use npm install and modify PWA theme and background colors in vite.config.ts 2025-06-02 18:07:41 +02:00
mohamad
af6324ddef Update vue-i18n dependency to version 9.9.1 in package.json 2025-06-02 18:07:41 +02:00
mohamad
6924a016c8 Add project documentation and production deployment guide
- Introduced comprehensive project documentation for the Shared Household Management PWA, detailing project overview, goals, features, user experience philosophy, technology stack, and development roadmap.
- Added a production deployment guide using Docker Compose and Gitea Actions, outlining setup, configuration, and deployment processes.
- Updated favicon and icon assets for improved branding and user experience across devices.
2025-06-02 18:07:41 +02:00
mohamad
0fcc94ae8d Update OAuth redirect URIs to production environment
- Changed the Google and Apple redirect URIs in the configuration to point to the production URLs.
- This update ensures that the application correctly redirects users to the appropriate authentication endpoints in the live environment.
2025-06-02 18:07:41 +02:00
mohamad
c0aa654e83 Update OAuth redirect URIs and API routing structure
- Changed the Google and Apple redirect URIs in the configuration to include the API version in the path.
- Reorganized the inclusion of OAuth routes in the main application to ensure they are properly prefixed and accessible.

These updates aim to enhance the API structure and ensure consistency in the authentication flow.
2025-06-02 18:07:41 +02:00
mohamad
ec361fe9ab Refactor API routing and update login URLs
- Updated the OAuth routes to be included under the main API prefix for better organization.
- Changed the Google login URL in the SocialLoginButtons component to reflect the new API structure.

These changes aim to improve the clarity and consistency of the API routing and enhance the login flow for users.
2025-06-02 18:07:41 +02:00
mohamad
9d404d04d5 Update OAuth redirect URIs and API routing structure
- Changed the Google and Apple redirect URIs in the configuration to include the API version in the path.
- Reorganized the inclusion of OAuth routes in the main application to ensure they are properly prefixed and accessible.

These updates aim to enhance the API structure and ensure consistency in the authentication flow.
2025-06-02 18:07:41 +02:00
mohamad
92c70813fb Refactor API routing and update login URLs
- Updated the OAuth routes to be included under the main API prefix for better organization.
- Changed the Google login URL in the SocialLoginButtons component to reflect the new API structure.

These changes aim to improve the clarity and consistency of the API routing and enhance the login flow for users.
2025-06-02 18:07:28 +02:00
whtvrboo
82205f6158
Merge pull request #13 from whtvrboo/i18n-feature-pages
feat(i18n): Internationalize remaining app pages
2025-06-02 00:13:53 +02:00
google-labs-jules[bot]
2a2045c24a feat(i18n): Internationalize remaining app pages
This commit completes the internationalization (i18n) for several key pages
within the frontend application.

The following pages have been updated to support multiple languages:
- AccountPage.vue
- SignupPage.vue
- ListDetailPage.vue (including items, OCR, expenses, and cost summary)
- MyChoresPage.vue
- PersonalChoresPage.vue
- IndexPage.vue

Key changes include:
- Extraction of all user-facing strings from these Vue components.
- Addition of new translation keys and their English values to `fe/src/i18n/en.json`.
- Modification of the Vue components to use the Vue I18n plugin's `$t()` (template)
  and `t()` (script) functions for displaying translated strings.
- Dynamic messages, notifications, and form validation messages are now also
  internationalized.
- The language files `de.json`, `es.json`, and `fr.json` have been updated
  with the new keys, using the English text as placeholders for future
  translation.

This effort significantly expands the i18n coverage of the application,
making it more accessible to a wider audience.
2025-06-01 22:13:36 +00:00
whtvrboo
c1ebd16e5a
Merge pull request #12 from whtvrboo/feat/i18n-more-pages
feat: Internationalize AuthCallback, Chores, ErrorNotFound, GroupDeta…
2025-06-01 23:52:54 +02:00
google-labs-jules[bot]
554814ad63 feat: Internationalize AuthCallback, Chores, ErrorNotFound, GroupDetail pages
This commit introduces internationalization for several pages:
- AuthCallbackPage.vue
- ChoresPage.vue (a comprehensive page with many elements)
- ErrorNotFound.vue
- GroupDetailPage.vue (including sub-sections for members, invites, chores summary, and expenses summary)

Key changes:
- Integrated `useI18n` in each listed page to handle translatable strings.
- Replaced hardcoded text in templates and relevant script sections (notifications, dynamic messages, fallbacks, etc.) with `t('key')` calls.
- Added new translation keys, organized under page-specific namespaces (e.g., `authCallbackPage`, `choresPage`, `errorNotFoundPage`, `groupDetailPage`), to `fe/src/i18n/en.json`.
- Added corresponding keys with placeholder translations (prefixed with DE:, FR:, ES:) to `fe/src/i18n/de.json`, `fe/src/i18n/fr.json`, and `fe/src/i18n/es.json`.
- Reused existing translation keys (e.g., for chore frequency options) where applicable.
2025-06-01 21:51:01 +00:00
whtvrboo
f2609f53ec
Merge pull request #11 from whtvrboo/fix/google-oauth-redirect
Fix: Resolve Google OAuth redirection issue
2025-06-01 23:14:10 +02:00
google-labs-jules[bot]
4fef642970 Fix: Resolve Google OAuth redirection issue
This commit addresses an issue where you, when clicking the "Continue with Google"
button, were redirected back to the login page instead of to Google's
authentication page.

The following changes were made:

1.  **Frontend Redirect:**
    *   Modified `fe/src/components/SocialLoginButtons.vue` to make the "Continue with Google" button redirect to the correct backend API endpoint (`/auth/google/login`) using the configured `API_BASE_URL`.

2.  **Backend Route Confirmation:**
    *   Verified that the backend OAuth routes in `be/app/api/auth/oauth.py` are correctly included in `be/app/main.py` under the `/auth` prefix, making them accessible.

3.  **OAuth Credentials Configuration:**
    *   Added `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` placeholders to `env.production.template` to guide you in setting up your OAuth credentials.
    *   Added instructional comments in `be/app/config.py` regarding the necessity of these environment variables and the correct configuration of `GOOGLE_REDIRECT_URI`.

With these changes, and assuming the necessary Google Cloud OAuth credentials
(Client ID, Client Secret) and redirect URIs are correctly configured in the
environment, the Google OAuth flow should now function as expected.
2025-06-01 21:13:48 +00:00
9 changed files with 265 additions and 135 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(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"])

View File

@ -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"])

View File

@ -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

View File

@ -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()

View File

@ -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`,

View File

@ -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')

View File

@ -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>

View File

@ -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
},

View File

@ -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
},
},
})