Refactor API integration across multiple components; introduce centralized API configuration, enhance error handling, and improve type safety in API calls for better maintainability and user experience.
This commit is contained in:
parent
7836672f64
commit
262505c898
102
be/app/core/api_config.py
Normal file
102
be/app/core/api_config.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
from typing import Dict, Any
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
# API Version
|
||||||
|
API_VERSION = "v1"
|
||||||
|
|
||||||
|
# API Prefix
|
||||||
|
API_PREFIX = f"/api/{API_VERSION}"
|
||||||
|
|
||||||
|
# API Endpoints
|
||||||
|
class APIEndpoints:
|
||||||
|
# Auth
|
||||||
|
AUTH = {
|
||||||
|
"LOGIN": "/auth/login",
|
||||||
|
"SIGNUP": "/auth/signup",
|
||||||
|
"REFRESH_TOKEN": "/auth/refresh-token",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Users
|
||||||
|
USERS = {
|
||||||
|
"PROFILE": "/users/profile",
|
||||||
|
"UPDATE_PROFILE": "/users/profile",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lists
|
||||||
|
LISTS = {
|
||||||
|
"BASE": "/lists",
|
||||||
|
"BY_ID": "/lists/{id}",
|
||||||
|
"ITEMS": "/lists/{list_id}/items",
|
||||||
|
"ITEM": "/lists/{list_id}/items/{item_id}",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Groups
|
||||||
|
GROUPS = {
|
||||||
|
"BASE": "/groups",
|
||||||
|
"BY_ID": "/groups/{id}",
|
||||||
|
"LISTS": "/groups/{group_id}/lists",
|
||||||
|
"MEMBERS": "/groups/{group_id}/members",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Invites
|
||||||
|
INVITES = {
|
||||||
|
"BASE": "/invites",
|
||||||
|
"BY_ID": "/invites/{id}",
|
||||||
|
"ACCEPT": "/invites/{id}/accept",
|
||||||
|
"DECLINE": "/invites/{id}/decline",
|
||||||
|
}
|
||||||
|
|
||||||
|
# OCR
|
||||||
|
OCR = {
|
||||||
|
"PROCESS": "/ocr/process",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Financials
|
||||||
|
FINANCIALS = {
|
||||||
|
"EXPENSES": "/financials/expenses",
|
||||||
|
"EXPENSE": "/financials/expenses/{id}",
|
||||||
|
"SETTLEMENTS": "/financials/settlements",
|
||||||
|
"SETTLEMENT": "/financials/settlements/{id}",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health
|
||||||
|
HEALTH = {
|
||||||
|
"CHECK": "/health",
|
||||||
|
}
|
||||||
|
|
||||||
|
# API Metadata
|
||||||
|
API_METADATA = {
|
||||||
|
"title": settings.API_TITLE,
|
||||||
|
"description": settings.API_DESCRIPTION,
|
||||||
|
"version": settings.API_VERSION,
|
||||||
|
"openapi_url": settings.API_OPENAPI_URL,
|
||||||
|
"docs_url": settings.API_DOCS_URL,
|
||||||
|
"redoc_url": settings.API_REDOC_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
# API Tags
|
||||||
|
API_TAGS = [
|
||||||
|
{"name": "Authentication", "description": "Authentication and authorization endpoints"},
|
||||||
|
{"name": "Users", "description": "User management endpoints"},
|
||||||
|
{"name": "Lists", "description": "Shopping list management endpoints"},
|
||||||
|
{"name": "Groups", "description": "Group management endpoints"},
|
||||||
|
{"name": "Invites", "description": "Group invitation management endpoints"},
|
||||||
|
{"name": "OCR", "description": "Optical Character Recognition endpoints"},
|
||||||
|
{"name": "Financials", "description": "Financial management endpoints"},
|
||||||
|
{"name": "Health", "description": "Health check endpoints"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Helper function to get full API URL
|
||||||
|
def get_api_url(endpoint: str, **kwargs) -> str:
|
||||||
|
"""
|
||||||
|
Get the full API URL for an endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
endpoint: The endpoint path
|
||||||
|
**kwargs: Path parameters to format the endpoint
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The full API URL
|
||||||
|
"""
|
||||||
|
formatted_endpoint = endpoint.format(**kwargs)
|
||||||
|
return f"{API_PREFIX}{formatted_endpoint}"
|
@ -4,8 +4,9 @@ import uvicorn
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from app.api.api_router import api_router # Import the main combined router
|
from app.api.api_router import api_router
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
from app.core.api_config import API_METADATA, API_TAGS
|
||||||
# Import database and models if needed for startup/shutdown events later
|
# Import database and models if needed for startup/shutdown events later
|
||||||
# from . import database, models
|
# from . import database, models
|
||||||
|
|
||||||
@ -18,12 +19,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# --- FastAPI App Instance ---
|
# --- FastAPI App Instance ---
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=settings.API_TITLE,
|
**API_METADATA,
|
||||||
description=settings.API_DESCRIPTION,
|
openapi_tags=API_TAGS
|
||||||
version=settings.API_VERSION,
|
|
||||||
openapi_url=settings.API_OPENAPI_URL,
|
|
||||||
docs_url=settings.API_DOCS_URL,
|
|
||||||
redoc_url=settings.API_REDOC_URL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- CORS Middleware ---
|
# --- CORS Middleware ---
|
||||||
|
@ -1,70 +1,72 @@
|
|||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import axios, { type AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import { useAuthStore } from 'stores/auth';
|
import { API_BASE_URL, API_VERSION, API_ENDPOINTS } from 'src/config/api-config';
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
|
||||||
interface ComponentCustomProperties {
|
|
||||||
$axios: AxiosInstance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Create axios instance
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}/api/v1`,
|
baseURL: API_BASE_URL,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request interceptor for adding auth token
|
// Request interceptor
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const authStore = useAuthStore();
|
const token = localStorage.getItem('token');
|
||||||
if (authStore.accessToken) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${authStore.accessToken}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
return Promise.reject(new Error(error.message));
|
return Promise.reject(error);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Response interceptor for handling errors
|
// Response interceptor
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error) => {
|
async (error) => {
|
||||||
const authStore = useAuthStore();
|
const originalRequest = error.config;
|
||||||
|
|
||||||
|
// If error is 401 and we haven't tried to refresh token yet
|
||||||
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true;
|
||||||
|
|
||||||
// If the error is 401 and we have a refresh token, try to refresh the access token
|
|
||||||
if (error.response?.status === 401 && authStore.refreshToken) {
|
|
||||||
try {
|
try {
|
||||||
await authStore.refreshAccessToken();
|
const refreshToken = localStorage.getItem('refreshToken');
|
||||||
// Retry the original request
|
if (!refreshToken) {
|
||||||
const config = error.config;
|
throw new Error('No refresh token available');
|
||||||
config.headers.Authorization = `Bearer ${authStore.accessToken}`;
|
}
|
||||||
return api(config);
|
|
||||||
} catch (error) {
|
// Call refresh token endpoint
|
||||||
// If refresh fails, clear tokens and redirect to login
|
const response = await api.post('/api/v1/auth/refresh-token', {
|
||||||
authStore.logout();
|
refresh_token: refreshToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { access_token } = response.data;
|
||||||
|
localStorage.setItem('token', access_token);
|
||||||
|
|
||||||
|
// Retry the original request with new token
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${access_token}`;
|
||||||
|
return api(originalRequest);
|
||||||
|
} catch (refreshError) {
|
||||||
|
// If refresh token fails, clear storage and redirect to login
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
return Promise.reject(
|
return Promise.reject(refreshError);
|
||||||
new Error(error instanceof Error ? error.message : 'Failed to refresh token'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a 401 without refresh token or refresh failed, clear tokens and redirect
|
return Promise.reject(error);
|
||||||
if (error.response?.status === 401) {
|
}
|
||||||
authStore.logout();
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(new Error(error.response?.data?.detail || error.message));
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
app.config.globalProperties.$axios = api;
|
app.config.globalProperties.$axios = axios;
|
||||||
|
app.config.globalProperties.$api = api;
|
||||||
});
|
});
|
||||||
|
|
||||||
export { api };
|
export { api };
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { api } from 'src/boot/axios';
|
import { apiClient, API_ENDPOINTS } from 'src/config/api';
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
@ -73,10 +73,10 @@ watch(isOpen, (newVal) => {
|
|||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await api.post('/api/v1/lists', {
|
await apiClient.post(API_ENDPOINTS.LISTS.BASE, {
|
||||||
name: listName.value,
|
name: listName.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
groupId: selectedGroup.value?.value,
|
group_id: selectedGroup.value?.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
$q.notify({
|
$q.notify({
|
||||||
@ -92,10 +92,10 @@ const onSubmit = async () => {
|
|||||||
// Close modal and emit created event
|
// Close modal and emit created event
|
||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
emit('created');
|
emit('created');
|
||||||
} catch {
|
} catch (error: unknown) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: 'Failed to create list',
|
message: error instanceof Error ? error.message : 'Failed to create list',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
106
fe/src/config/api-config.ts
Normal file
106
fe/src/config/api-config.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// API Version
|
||||||
|
export const API_VERSION = 'v1';
|
||||||
|
|
||||||
|
// API Base URL
|
||||||
|
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
// API Endpoints
|
||||||
|
export const API_ENDPOINTS = {
|
||||||
|
// Auth
|
||||||
|
AUTH: {
|
||||||
|
LOGIN: '/auth/login',
|
||||||
|
SIGNUP: '/auth/signup',
|
||||||
|
REFRESH_TOKEN: '/auth/refresh-token',
|
||||||
|
LOGOUT: '/auth/logout',
|
||||||
|
VERIFY_EMAIL: '/auth/verify-email',
|
||||||
|
RESET_PASSWORD: '/auth/reset-password',
|
||||||
|
FORGOT_PASSWORD: '/auth/forgot-password',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Users
|
||||||
|
USERS: {
|
||||||
|
PROFILE: '/users/me',
|
||||||
|
UPDATE_PROFILE: '/users/me',
|
||||||
|
PASSWORD: '/users/password',
|
||||||
|
AVATAR: '/users/avatar',
|
||||||
|
SETTINGS: '/users/settings',
|
||||||
|
NOTIFICATIONS: '/users/notifications',
|
||||||
|
PREFERENCES: '/users/preferences',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lists
|
||||||
|
LISTS: {
|
||||||
|
BASE: '/lists',
|
||||||
|
BY_ID: (id: string) => `/lists/${id}`,
|
||||||
|
ITEMS: (listId: string) => `/lists/${listId}/items`,
|
||||||
|
ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`,
|
||||||
|
SHARE: (listId: string) => `/lists/${listId}/share`,
|
||||||
|
UNSHARE: (listId: string) => `/lists/${listId}/unshare`,
|
||||||
|
COMPLETE: (listId: string) => `/lists/${listId}/complete`,
|
||||||
|
REOPEN: (listId: string) => `/lists/${listId}/reopen`,
|
||||||
|
ARCHIVE: (listId: string) => `/lists/${listId}/archive`,
|
||||||
|
RESTORE: (listId: string) => `/lists/${listId}/restore`,
|
||||||
|
DUPLICATE: (listId: string) => `/lists/${listId}/duplicate`,
|
||||||
|
EXPORT: (listId: string) => `/lists/${listId}/export`,
|
||||||
|
IMPORT: '/lists/import',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Groups
|
||||||
|
GROUPS: {
|
||||||
|
BASE: '/groups',
|
||||||
|
BY_ID: (id: string) => `/groups/${id}`,
|
||||||
|
LISTS: (groupId: string) => `/groups/${groupId}/lists`,
|
||||||
|
MEMBERS: (groupId: string) => `/groups/${groupId}/members`,
|
||||||
|
MEMBER: (groupId: string, userId: string) => `/groups/${groupId}/members/${userId}`,
|
||||||
|
LEAVE: (groupId: string) => `/groups/${groupId}/leave`,
|
||||||
|
DELETE: (groupId: string) => `/groups/${groupId}`,
|
||||||
|
SETTINGS: (groupId: string) => `/groups/${groupId}/settings`,
|
||||||
|
ROLES: (groupId: string) => `/groups/${groupId}/roles`,
|
||||||
|
ROLE: (groupId: string, roleId: string) => `/groups/${groupId}/roles/${roleId}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Invites
|
||||||
|
INVITES: {
|
||||||
|
BASE: '/invites',
|
||||||
|
BY_ID: (id: string) => `/invites/${id}`,
|
||||||
|
ACCEPT: (id: string) => `/invites/${id}/accept`,
|
||||||
|
DECLINE: (id: string) => `/invites/${id}/decline`,
|
||||||
|
REVOKE: (id: string) => `/invites/${id}/revoke`,
|
||||||
|
LIST: '/invites',
|
||||||
|
PENDING: '/invites/pending',
|
||||||
|
SENT: '/invites/sent',
|
||||||
|
},
|
||||||
|
|
||||||
|
// OCR
|
||||||
|
OCR: {
|
||||||
|
PROCESS: '/ocr/process',
|
||||||
|
STATUS: (jobId: string) => `/ocr/status/${jobId}`,
|
||||||
|
RESULT: (jobId: string) => `/ocr/result/${jobId}`,
|
||||||
|
BATCH: '/ocr/batch',
|
||||||
|
CANCEL: (jobId: string) => `/ocr/cancel/${jobId}`,
|
||||||
|
HISTORY: '/ocr/history',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Financials
|
||||||
|
FINANCIALS: {
|
||||||
|
EXPENSES: '/financials/expenses',
|
||||||
|
EXPENSE: (id: string) => `/financials/expenses/${id}`,
|
||||||
|
SETTLEMENTS: '/financials/settlements',
|
||||||
|
SETTLEMENT: (id: string) => `/financials/settlements/${id}`,
|
||||||
|
BALANCES: '/financials/balances',
|
||||||
|
BALANCE: (userId: string) => `/financials/balances/${userId}`,
|
||||||
|
REPORTS: '/financials/reports',
|
||||||
|
REPORT: (id: string) => `/financials/reports/${id}`,
|
||||||
|
CATEGORIES: '/financials/categories',
|
||||||
|
CATEGORY: (id: string) => `/financials/categories/${id}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Health
|
||||||
|
HEALTH: {
|
||||||
|
CHECK: '/health',
|
||||||
|
VERSION: '/health/version',
|
||||||
|
STATUS: '/health/status',
|
||||||
|
METRICS: '/health/metrics',
|
||||||
|
LOGS: '/health/logs',
|
||||||
|
},
|
||||||
|
};
|
18
fe/src/config/api.ts
Normal file
18
fe/src/config/api.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { api } from 'boot/axios';
|
||||||
|
import { API_BASE_URL, API_VERSION, API_ENDPOINTS } from './api-config';
|
||||||
|
|
||||||
|
// Helper function to get full API URL
|
||||||
|
export const getApiUrl = (endpoint: string): string => {
|
||||||
|
return `${API_BASE_URL}/api/${API_VERSION}${endpoint}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to make API calls
|
||||||
|
export const apiClient = {
|
||||||
|
get: (endpoint: string, config = {}) => api.get(getApiUrl(endpoint), config),
|
||||||
|
post: (endpoint: string, data = {}, config = {}) => api.post(getApiUrl(endpoint), data, config),
|
||||||
|
put: (endpoint: string, data = {}, config = {}) => api.put(getApiUrl(endpoint), data, config),
|
||||||
|
patch: (endpoint: string, data = {}, config = {}) => api.patch(getApiUrl(endpoint), data, config),
|
||||||
|
delete: (endpoint: string, config = {}) => api.delete(getApiUrl(endpoint), config),
|
||||||
|
};
|
||||||
|
|
||||||
|
export { API_ENDPOINTS };
|
@ -1,10 +1,264 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<h1 class="text-h4 q-mb-md">Account</h1>
|
<h1 class="text-h4 q-mb-md">Account Settings</h1>
|
||||||
<p>Your account settings will appear here.</p>
|
|
||||||
|
<div v-if="loading" class="text-center">
|
||||||
|
<q-spinner-dots color="primary" size="2em" />
|
||||||
|
<p>Loading profile...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-banner v-else-if="error" inline-actions class="text-white bg-red q-mb-md">
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon name="warning" />
|
||||||
|
</template>
|
||||||
|
{{ error }}
|
||||||
|
<template v-slot:action>
|
||||||
|
<q-btn flat color="white" label="Retry" @click="fetchProfile" />
|
||||||
|
</template>
|
||||||
|
</q-banner>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<q-form @submit="onSubmit" class="q-gutter-md">
|
||||||
|
<!-- Profile Section -->
|
||||||
|
<q-card class="q-mb-md">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Profile Information</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-input
|
||||||
|
v-model="profile.name"
|
||||||
|
label="Name"
|
||||||
|
:rules="[(val) => !!val || 'Name is required']"
|
||||||
|
outlined
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-input
|
||||||
|
v-model="profile.email"
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
:rules="[(val) => !!val || 'Email is required']"
|
||||||
|
outlined
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
label="Save Changes"
|
||||||
|
:loading="saving"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<!-- Password Section -->
|
||||||
|
<q-card class="q-mb-md">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Change Password</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-input
|
||||||
|
v-model="password.current"
|
||||||
|
label="Current Password"
|
||||||
|
type="password"
|
||||||
|
:rules="[(val) => !!val || 'Current password is required']"
|
||||||
|
outlined
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-input
|
||||||
|
v-model="password.new"
|
||||||
|
label="New Password"
|
||||||
|
type="password"
|
||||||
|
:rules="[(val) => !!val || 'New password is required']"
|
||||||
|
outlined
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
label="Change Password"
|
||||||
|
:loading="changingPassword"
|
||||||
|
@click="onChangePassword"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<!-- Notifications Section -->
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Notification Preferences</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<q-list>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Email Notifications</q-item-label>
|
||||||
|
<q-item-label caption>Receive email notifications for important updates</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-toggle v-model="preferences.emailNotifications" @update:model-value="onPreferenceChange" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>List Updates</q-item-label>
|
||||||
|
<q-item-label caption>Get notified when lists are updated</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-toggle v-model="preferences.listUpdates" @update:model-value="onPreferenceChange" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Group Activities</q-item-label>
|
||||||
|
<q-item-label caption>Receive notifications for group activities</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-toggle v-model="preferences.groupActivities" @update:model-value="onPreferenceChange" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-form>
|
||||||
|
</template>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Component logic will go here
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { apiClient, API_ENDPOINTS } from 'src/config/api';
|
||||||
|
|
||||||
|
interface Profile {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Password {
|
||||||
|
current: string;
|
||||||
|
new: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Preferences {
|
||||||
|
emailNotifications: boolean;
|
||||||
|
listUpdates: boolean;
|
||||||
|
groupActivities: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const loading = ref(true);
|
||||||
|
const saving = ref(false);
|
||||||
|
const changingPassword = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
const profile = ref<Profile>({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const password = ref<Password>({
|
||||||
|
current: '',
|
||||||
|
new: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const preferences = ref<Preferences>({
|
||||||
|
emailNotifications: true,
|
||||||
|
listUpdates: true,
|
||||||
|
groupActivities: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchProfile = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(API_ENDPOINTS.USERS.PROFILE);
|
||||||
|
profile.value = response.data;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error('Failed to fetch profile:', err);
|
||||||
|
error.value = err instanceof Error ? err.message : 'Failed to load profile';
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.value,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await apiClient.put(API_ENDPOINTS.USERS.UPDATE_PROFILE, profile.value);
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Profile updated successfully',
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error('Failed to update profile:', err);
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: err instanceof Error ? err.message : 'Failed to update profile',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangePassword = async () => {
|
||||||
|
changingPassword.value = true;
|
||||||
|
try {
|
||||||
|
await apiClient.put(API_ENDPOINTS.USERS.PASSWORD, password.value);
|
||||||
|
password.value = { current: '', new: '' };
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Password changed successfully',
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error('Failed to change password:', err);
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: err instanceof Error ? err.message : 'Failed to change password',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
changingPassword.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPreferenceChange = async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.put(API_ENDPOINTS.USERS.PREFERENCES, preferences.value);
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Preferences updated successfully',
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error('Failed to update preferences:', err);
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: err instanceof Error ? err.message : 'Failed to update preferences',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchProfile();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { api } from 'boot/axios';
|
import { apiClient, API_ENDPOINTS } from 'src/config/api';
|
||||||
import { useAuthStore } from 'stores/auth';
|
import { useAuthStore } from 'stores/auth';
|
||||||
import { copyToClipboard, useQuasar } from 'quasar';
|
import { copyToClipboard, useQuasar } from 'quasar';
|
||||||
|
|
||||||
@ -67,9 +67,7 @@ const fetchGroupDetails = async () => {
|
|||||||
if (!groupId.value) return;
|
if (!groupId.value) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/groups/${groupId.value}`, {
|
const response = await apiClient.get(API_ENDPOINTS.GROUPS.BY_ID(groupId.value));
|
||||||
headers: { Authorization: `Bearer ${authStore.accessToken}` },
|
|
||||||
});
|
|
||||||
group.value = response.data;
|
group.value = response.data;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('Error fetching group details:', error);
|
console.error('Error fetching group details:', error);
|
||||||
@ -88,13 +86,9 @@ const generateInviteCode = async () => {
|
|||||||
generatingInvite.value = true;
|
generatingInvite.value = true;
|
||||||
inviteCode.value = null;
|
inviteCode.value = null;
|
||||||
try {
|
try {
|
||||||
const response = await api.post(
|
const response = await apiClient.post(API_ENDPOINTS.INVITES.BASE, {
|
||||||
`/groups/${groupId.value}/invites`,
|
group_id: groupId.value,
|
||||||
{},
|
});
|
||||||
{
|
|
||||||
headers: { Authorization: `Bearer ${authStore.accessToken}` },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
inviteCode.value = response.data.invite_code;
|
inviteCode.value = response.data.invite_code;
|
||||||
$q.notify({
|
$q.notify({
|
||||||
color: 'positive',
|
color: 'positive',
|
||||||
|
@ -87,7 +87,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { api } from '../boot/axios'; // Ensure this path is correct
|
import { apiClient, API_ENDPOINTS } from 'src/config/api';
|
||||||
import { useQuasar, QInput } from 'quasar';
|
import { useQuasar, QInput } from 'quasar';
|
||||||
|
|
||||||
interface Group {
|
interface Group {
|
||||||
@ -113,7 +113,7 @@ const joinInviteCodeInput = ref<QInput | null>(null);
|
|||||||
const fetchGroups = async () => {
|
const fetchGroups = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await api.get<Group[]>('/groups');
|
const response = await apiClient.get(API_ENDPOINTS.GROUPS.BASE);
|
||||||
groups.value = Array.isArray(response.data) ? response.data : [];
|
groups.value = Array.isArray(response.data) ? response.data : [];
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('Error fetching groups:', error);
|
console.error('Error fetching groups:', error);
|
||||||
@ -136,15 +136,15 @@ const openCreateGroupDialog = () => {
|
|||||||
|
|
||||||
const handleCreateGroup = async () => {
|
const handleCreateGroup = async () => {
|
||||||
if (!newGroupName.value || newGroupName.value.trim() === '') {
|
if (!newGroupName.value || newGroupName.value.trim() === '') {
|
||||||
void newGroupNameInput.value?.validate(); // Assuming QInput has a validate method
|
void newGroupNameInput.value?.validate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
creatingGroup.value = true;
|
creatingGroup.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await api.post<{ data: Group }>('/groups', {
|
const response = await apiClient.post(API_ENDPOINTS.GROUPS.BASE, {
|
||||||
name: newGroupName.value,
|
name: newGroupName.value,
|
||||||
});
|
});
|
||||||
const newGroup = response.data?.data; // Safely access .data
|
const newGroup = response.data;
|
||||||
|
|
||||||
if (newGroup && typeof newGroup.id === 'string' && typeof newGroup.name === 'string') {
|
if (newGroup && typeof newGroup.id === 'string' && typeof newGroup.name === 'string') {
|
||||||
groups.value.push(newGroup);
|
groups.value.push(newGroup);
|
||||||
@ -184,9 +184,7 @@ const handleJoinGroup = async () => {
|
|||||||
}
|
}
|
||||||
joiningGroup.value = true;
|
joiningGroup.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await api.post<Group>('/groups/join', {
|
const response = await apiClient.post(API_ENDPOINTS.INVITES.ACCEPT(inviteCodeToJoin.value));
|
||||||
invite_code: inviteCodeToJoin.value,
|
|
||||||
});
|
|
||||||
const joinedGroup = response.data;
|
const joinedGroup = response.data;
|
||||||
|
|
||||||
if (joinedGroup && typeof joinedGroup.id === 'string' && typeof joinedGroup.name === 'string') {
|
if (joinedGroup && typeof joinedGroup.id === 'string' && typeof joinedGroup.name === 'string') {
|
||||||
|
@ -161,7 +161,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { api } from 'boot/axios';
|
import { apiClient, API_ENDPOINTS } from 'src/config/api';
|
||||||
import { useQuasar, QFile } from 'quasar';
|
import { useQuasar, QFile } from 'quasar';
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
@ -184,11 +184,6 @@ interface List {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListStatus {
|
|
||||||
list_updated_at: string;
|
|
||||||
latest_item_updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
@ -223,13 +218,13 @@ const fetchListDetails = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const response = await api.get<List>(
|
const response = await apiClient.get(
|
||||||
`/api/v1/lists/${String(route.params.id)}`
|
API_ENDPOINTS.LISTS.BY_ID(String(route.params.id))
|
||||||
);
|
);
|
||||||
list.value = response.data;
|
list.value = response.data as List;
|
||||||
lastListUpdate.value = response.data.updated_at;
|
lastListUpdate.value = response.data.updated_at;
|
||||||
// Find the latest item update time
|
// Find the latest item update time
|
||||||
lastItemUpdate.value = response.data.items.reduce((latest, item) => {
|
lastItemUpdate.value = response.data.items.reduce((latest: string, item: Item) => {
|
||||||
return item.updated_at > latest ? item.updated_at : latest;
|
return item.updated_at > latest ? item.updated_at : latest;
|
||||||
}, '');
|
}, '');
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@ -248,10 +243,13 @@ const fetchListDetails = async () => {
|
|||||||
|
|
||||||
const checkForUpdates = async () => {
|
const checkForUpdates = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get<ListStatus>(
|
const response = await apiClient.get(
|
||||||
`/api/v1/lists/${String(route.params.id)}/status`
|
API_ENDPOINTS.LISTS.BY_ID(String(route.params.id))
|
||||||
);
|
);
|
||||||
const { list_updated_at, latest_item_updated_at } = response.data;
|
const { updated_at: list_updated_at } = response.data as List;
|
||||||
|
const latest_item_updated_at = response.data.items.reduce((latest: string, item: Item) => {
|
||||||
|
return item.updated_at > latest ? item.updated_at : latest;
|
||||||
|
}, '');
|
||||||
|
|
||||||
// If either the list or any item has been updated, refresh the data
|
// If either the list or any item has been updated, refresh the data
|
||||||
if (
|
if (
|
||||||
@ -268,7 +266,7 @@ const checkForUpdates = async () => {
|
|||||||
|
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
// Poll every 15 seconds
|
// Poll every 15 seconds
|
||||||
pollingInterval.value = window.setInterval(() => { void checkForUpdates(); }, 15000);
|
pollingInterval.value = setInterval(checkForUpdates, 15000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
@ -283,11 +281,11 @@ const onAddItem = async () => {
|
|||||||
|
|
||||||
addingItem.value = true;
|
addingItem.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await api.post<Item>(
|
const response = await apiClient.post(
|
||||||
`/api/v1/lists/${list.value.id}/items`,
|
API_ENDPOINTS.LISTS.ITEMS(String(list.value.id)),
|
||||||
newItem.value
|
newItem.value
|
||||||
);
|
);
|
||||||
list.value.items.push(response.data);
|
list.value.items.push(response.data as Item);
|
||||||
newItem.value = { name: '' };
|
newItem.value = { name: '' };
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
@ -302,8 +300,8 @@ const onAddItem = async () => {
|
|||||||
const updateItem = async (item: Item) => {
|
const updateItem = async (item: Item) => {
|
||||||
item.updating = true;
|
item.updating = true;
|
||||||
try {
|
try {
|
||||||
const response = await api.put<Item>(
|
const response = await apiClient.put(
|
||||||
`/api/v1/lists/${list.value.id}/items/${item.id}`,
|
API_ENDPOINTS.LISTS.ITEM(String(list.value.id), String(item.id)),
|
||||||
{
|
{
|
||||||
name: editingItemName.value,
|
name: editingItemName.value,
|
||||||
quantity: editingItemQuantity.value,
|
quantity: editingItemQuantity.value,
|
||||||
@ -311,7 +309,7 @@ const updateItem = async (item: Item) => {
|
|||||||
version: item.version,
|
version: item.version,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
Object.assign(item, response.data);
|
Object.assign(item, response.data as Item);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if ((err as { response?: { status?: number } }).response?.status === 409) {
|
if ((err as { response?: { status?: number } }).response?.status === 409) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
@ -342,20 +340,23 @@ const handleOcrUpload = async (file: File | null) => {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
const response = await api.post<{ items: string[] }>(
|
const response = await apiClient.post(
|
||||||
`/api/v1/lists/${list.value.id}/ocr`, formData, {
|
API_ENDPOINTS.OCR.PROCESS,
|
||||||
headers: {
|
formData,
|
||||||
'Content-Type': 'multipart/form-data'
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
ocrItems.value = response.data.items.map((name) => ({ name }));
|
ocrItems.value = response.data.items;
|
||||||
|
showOcrDialog.value = true;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
ocrError.value = (err as Error).message || 'Failed to process image';
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: (err as { response?: { data?: { detail?: string } } }).response?.data?.detail || 'Failed to process image',
|
message: ocrError.value,
|
||||||
});
|
});
|
||||||
ocrError.value = (err as { response?: { data?: { detail?: string } } }).response?.data?.detail || 'Failed to process image';
|
|
||||||
} finally {
|
} finally {
|
||||||
ocrLoading.value = false;
|
ocrLoading.value = false;
|
||||||
}
|
}
|
||||||
@ -369,8 +370,8 @@ const addOcrItems = async () => {
|
|||||||
for (const item of ocrItems.value) {
|
for (const item of ocrItems.value) {
|
||||||
if (!item.name) continue;
|
if (!item.name) continue;
|
||||||
|
|
||||||
const response = await api.post<Item>(
|
const response = await apiClient.post<Item>(
|
||||||
`/api/v1/lists/${list.value.id}/items`,
|
API_ENDPOINTS.LISTS.ITEMS(list.value.id),
|
||||||
{ name: item.name, quantity: 1 }
|
{ name: item.name, quantity: 1 }
|
||||||
);
|
);
|
||||||
list.value.items.push(response.data);
|
list.value.items.push(response.data);
|
||||||
|
@ -19,7 +19,13 @@
|
|||||||
|
|
||||||
<div v-else-if="filteredLists.length === 0">
|
<div v-else-if="filteredLists.length === 0">
|
||||||
<p>{{ noListsMessage }}</p>
|
<p>{{ noListsMessage }}</p>
|
||||||
<!-- TODO: Add a button to create a new list -->
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="add"
|
||||||
|
label="Create New List"
|
||||||
|
@click="showCreateModal = true"
|
||||||
|
class="q-mt-md"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-list v-else bordered separator>
|
<q-list v-else bordered separator>
|
||||||
@ -52,14 +58,29 @@
|
|||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<!-- TODO: Add FAB for creating a new list if props.groupId is defined or it's personal lists view -->
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-btn
|
||||||
|
fab
|
||||||
|
color="primary"
|
||||||
|
icon="add"
|
||||||
|
@click="showCreateModal = true"
|
||||||
|
:label="currentGroupId ? 'Create Group List' : 'Create List'"
|
||||||
|
/>
|
||||||
|
</q-page-sticky>
|
||||||
|
|
||||||
|
<CreateListModal
|
||||||
|
v-model="showCreateModal"
|
||||||
|
:groups="availableGroups"
|
||||||
|
@created="fetchLists"
|
||||||
|
/>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router'; // To potentially get groupId from route params
|
import { useRoute } from 'vue-router';
|
||||||
import { api } from 'boot/axios';
|
import { apiClient, API_ENDPOINTS } from 'src/config/api';
|
||||||
|
import CreateListModal from 'components/CreateListModal.vue';
|
||||||
import {
|
import {
|
||||||
QSpinnerDots,
|
QSpinnerDots,
|
||||||
QBanner,
|
QBanner,
|
||||||
@ -70,32 +91,39 @@ import {
|
|||||||
QItemLabel,
|
QItemLabel,
|
||||||
QBadge,
|
QBadge,
|
||||||
QBtn,
|
QBtn,
|
||||||
|
QPageSticky,
|
||||||
} from 'quasar'; // Explicitly import Quasar components
|
} from 'quasar'; // Explicitly import Quasar components
|
||||||
|
|
||||||
// Define the structure of a List based on ListPublic schema
|
|
||||||
interface List {
|
interface List {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string | null;
|
description?: string;
|
||||||
|
is_complete: boolean;
|
||||||
|
updated_at: string;
|
||||||
created_by_id: number;
|
created_by_id: number;
|
||||||
group_id?: number | null;
|
group_id?: number | null;
|
||||||
is_complete: boolean;
|
created_at: string;
|
||||||
created_at: string; // Assuming datetime is serialized as string
|
|
||||||
updated_at: string; // Assuming datetime is serialized as string
|
|
||||||
version: number;
|
version: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Group {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
groupId?: number | string; // Can be passed as prop, or we might use route.params
|
groupId?: number | string; // Can be passed as prop, or we might use route.params
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const route = useRoute(); // Access route if needed
|
const route = useRoute();
|
||||||
|
|
||||||
const lists = ref<List[]>([]);
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
const lists = ref<List[]>([]);
|
||||||
|
const availableGroups = ref<{ label: string; value: number }[]>([]);
|
||||||
|
const groupName = ref<string | null>(null);
|
||||||
|
|
||||||
|
const showCreateModal = ref(false);
|
||||||
|
|
||||||
// Determine the actual groupId to use (from prop or route param)
|
|
||||||
const currentGroupId = computed(() => {
|
const currentGroupId = computed(() => {
|
||||||
if (props.groupId) {
|
if (props.groupId) {
|
||||||
return typeof props.groupId === 'string' ? parseInt(props.groupId, 10) : props.groupId;
|
return typeof props.groupId === 'string' ? parseInt(props.groupId, 10) : props.groupId;
|
||||||
@ -106,12 +134,27 @@ const currentGroupId = computed(() => {
|
|||||||
return null; // No specific group selected, show personal lists or all accessible
|
return null; // No specific group selected, show personal lists or all accessible
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fetchGroupName = async () => {
|
||||||
|
if (!currentGroupId.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
API_ENDPOINTS.GROUPS.BY_ID(String(currentGroupId.value))
|
||||||
|
);
|
||||||
|
groupName.value = (response.data as Group).name;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch group name:', err);
|
||||||
|
groupName.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pageTitle = computed(() => {
|
const pageTitle = computed(() => {
|
||||||
if (currentGroupId.value) {
|
if (currentGroupId.value) {
|
||||||
// TODO: Fetch group name if we want to display "Lists for Group X"
|
return groupName.value
|
||||||
return `Lists for Group ${currentGroupId.value}`;
|
? `Lists for ${groupName.value}`
|
||||||
|
: `Lists for Group ${currentGroupId.value}`;
|
||||||
}
|
}
|
||||||
return 'All My Lists'; // Changed from 'My Lists' to be more descriptive
|
return 'All My Lists';
|
||||||
});
|
});
|
||||||
|
|
||||||
const noListsMessage = computed(() => {
|
const noListsMessage = computed(() => {
|
||||||
@ -121,12 +164,28 @@ const noListsMessage = computed(() => {
|
|||||||
return 'You have no lists yet. Create a personal list or join a group to see shared lists.';
|
return 'You have no lists yet. Create a personal list or join a group to see shared lists.';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fetchGroups = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(API_ENDPOINTS.GROUPS.BASE);
|
||||||
|
availableGroups.value = (response.data as Group[]).map((group) => ({
|
||||||
|
label: group.name,
|
||||||
|
value: group.id,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch groups:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchLists = async () => {
|
const fetchLists = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const response = await api.get<List[]>('/lists'); // API returns all accessible lists
|
const endpoint = currentGroupId.value
|
||||||
lists.value = response.data;
|
? API_ENDPOINTS.GROUPS.LISTS(String(currentGroupId.value))
|
||||||
|
: API_ENDPOINTS.LISTS.BASE;
|
||||||
|
|
||||||
|
const response = await apiClient.get(endpoint);
|
||||||
|
lists.value = response.data as List[];
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error('Failed to fetch lists:', err);
|
console.error('Failed to fetch lists:', err);
|
||||||
error.value =
|
error.value =
|
||||||
@ -136,7 +195,11 @@ const fetchLists = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(fetchLists);
|
onMounted(() => {
|
||||||
|
fetchLists();
|
||||||
|
fetchGroups();
|
||||||
|
fetchGroupName();
|
||||||
|
});
|
||||||
|
|
||||||
const filteredLists = computed(() => {
|
const filteredLists = computed(() => {
|
||||||
if (currentGroupId.value) {
|
if (currentGroupId.value) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { api } from 'boot/axios';
|
import { apiClient, API_ENDPOINTS } from 'src/config/api';
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
@ -46,7 +46,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
formData.append('username', email);
|
formData.append('username', email);
|
||||||
formData.append('password', password);
|
formData.append('password', password);
|
||||||
|
|
||||||
const response = await api.post('/auth/login', formData, {
|
const response = await apiClient.post(API_ENDPOINTS.AUTH.LOGIN, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
@ -58,21 +58,17 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const signup = async (userData: { name: string; email: string; password: string }) => {
|
const signup = async (userData: { name: string; email: string; password: string }) => {
|
||||||
const response = await api.post('/auth/signup', userData);
|
const response = await apiClient.post(API_ENDPOINTS.AUTH.SIGNUP, userData);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
clearTokens();
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshAccessToken = async () => {
|
const refreshAccessToken = async () => {
|
||||||
if (!refreshToken.value) {
|
if (!refreshToken.value) {
|
||||||
throw new Error('No refresh token available');
|
throw new Error('No refresh token available');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.post('/auth/refresh', {
|
const response = await apiClient.post(API_ENDPOINTS.AUTH.REFRESH_TOKEN, {
|
||||||
refresh_token: refreshToken.value,
|
refresh_token: refreshToken.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -85,6 +81,10 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
clearTokens();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
accessToken,
|
accessToken,
|
||||||
@ -99,7 +99,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
setUser,
|
setUser,
|
||||||
login,
|
login,
|
||||||
signup,
|
signup,
|
||||||
logout,
|
|
||||||
refreshAccessToken,
|
refreshAccessToken,
|
||||||
|
logout,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user