diff --git a/Web/React/package.json b/Web/React/package.json index 1ec8d44..e26924d 100644 --- a/Web/React/package.json +++ b/Web/React/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "start": "vite" }, "dependencies": { "axios": "^1.13.1", diff --git a/Web/React/src/components/Dashboard.tsx b/Web/React/src/components/Dashboard.tsx index 468084a..fcd5fce 100644 --- a/Web/React/src/components/Dashboard.tsx +++ b/Web/React/src/components/Dashboard.tsx @@ -11,7 +11,6 @@ import GroupDetail from './GroupDetail'; import Receipts from './Receipts'; import ReceiptsView from './ReceiptsView'; // <-- adăugat import ReceiptsManual from './ReceiptsManual'; -import ChatBot from './ChatBot'; import Data from './Data'; import Categories from './Categories'; @@ -36,6 +35,7 @@ const Dashboard: React.FC = () => | 'profile' | 'groupDetail' | 'data' + | 'support' >('home'); const [selectedGroupId, setSelectedGroupId] = useState(null); const [recentTx, setRecentTx] = useState([]); @@ -50,7 +50,7 @@ const Dashboard: React.FC = () => { try { - const res = await apiClient.get('/expenses', { + const res = await apiClient.get('/expenses/', { params: { offset: 0, limit: 5, diff --git a/Web/React/src/components/Data.tsx b/Web/React/src/components/Data.tsx index 1b706a7..11d1870 100644 --- a/Web/React/src/components/Data.tsx +++ b/Web/React/src/components/Data.tsx @@ -92,7 +92,7 @@ const Data: React.FC = () => { // or you can pass `category` (name) to backend if your API supports it. // For now, we fetch all for the date range and filter locally. - const res = await apiClient.get('/expenses', { params }); + const res = await apiClient.get('/expenses/', { params }); const items: Expense[] = Array.isArray(res.data) ? res.data : (res.data.data || []); if (cancelled) return; diff --git a/Web/React/src/components/GroupDetail.tsx b/Web/React/src/components/GroupDetail.tsx index 64aed6b..296daad 100644 --- a/Web/React/src/components/GroupDetail.tsx +++ b/Web/React/src/components/GroupDetail.tsx @@ -105,7 +105,7 @@ const GroupDetail: React.FC = ({ groupId, onBack }) => { // Fetch categories - fetch all categories since backend returns all anyway (known bug) // This ensures we can map categories from all users in the group - const categories = await categoryService.getCategories(user?.id); + const categories: Category[] = await categoryService.getCategories(user?.id); const categoryMap = new Map(categories.map(c => [c.id, c.title])); // Get unique user IDs from both expenses and logs @@ -634,7 +634,7 @@ const GroupDetail: React.FC = ({ groupId, onBack }) => { group_id: groupId, }; - await apiClient.post('/expenses', body); + await apiClient.post('/expenses/', body); // Refresh the expense list and statistics await fetchGroupExpenses(); diff --git a/Web/React/src/components/Groups.tsx b/Web/React/src/components/Groups.tsx index 4d75ce6..4a552b7 100644 --- a/Web/React/src/components/Groups.tsx +++ b/Web/React/src/components/Groups.tsx @@ -36,7 +36,7 @@ const Groups: React.FC<{ setLoading(true); try { - const res = await apiClient.get('/groups', { + const res = await apiClient.get('/groups/', { params: { offset: 0, limit: 1000 } }); @@ -133,7 +133,7 @@ const Groups: React.FC<{ body.description = newGroup.description.trim(); } - const createRes = await apiClient.post('/groups', body); + const createRes = await apiClient.post('/groups/', body); // Backend returns APIResponse { success: true, data: { id: groupId } } const responseData = createRes.data; diff --git a/Web/React/src/components/Profile.tsx b/Web/React/src/components/Profile.tsx index da16fb9..4d71acd 100644 --- a/Web/React/src/components/Profile.tsx +++ b/Web/React/src/components/Profile.tsx @@ -110,7 +110,7 @@ const Profile: React.FC = ({ onRequestNavigate }) => { setErrorMessage('Please enter a valid positive number'); return; } - await apiClient.put(`/users/${user.id}/budget?new_budget=${budgetValue}`); + await apiClient.put(`/users/${user.id}/budget/?new_budget=${budgetValue}`); setBudget(budgetValue); setSavedMessage('Budget updated successfully'); setTimeout(() => setSavedMessage(null), 2500); @@ -123,7 +123,7 @@ const Profile: React.FC = ({ onRequestNavigate }) => { setErrorMessage('Please enter both current and new password'); return; } - await apiClient.put('/users/password/change', { old_password: oldPassword, new_password: newPassword }); + await apiClient.put('/users/password/change/', { old_password: oldPassword, new_password: newPassword }); setSavedMessage('Password changed successfully'); setTimeout(() => setSavedMessage(null), 2500); setEditingField(null); @@ -134,7 +134,7 @@ const Profile: React.FC = ({ onRequestNavigate }) => { if (!user) throw new Error('Not authenticated'); const payload: any = { [editingField]: tempValue }; - await apiClient.put(`/users/${user.id}`, payload); + await apiClient.put(`/users/${user.id}/`, payload); // Optimistically update local and global user so UI reflects changes immediately setProfile(prev => ({ ...prev, [editingField as string]: tempValue })); diff --git a/Web/React/src/components/Receipts.tsx b/Web/React/src/components/Receipts.tsx index 4359794..d3f943a 100644 --- a/Web/React/src/components/Receipts.tsx +++ b/Web/React/src/components/Receipts.tsx @@ -51,7 +51,7 @@ const Receipts: React.FC<{ navigate?: (to: string) => void }> = ({ navigate }) = const updatedItems = analysisData.items.filter((_, idx) => idx !== indexToDelete); // Recalculate Total - const newTotal = updatedItems.reduce((sum, item) => sum + item.price, 0); + const newTotal = updatedItems.reduce((sum, item) => sum + (item.price || 0), 0); setAnalysisData({ items: updatedItems, @@ -96,7 +96,7 @@ const Receipts: React.FC<{ navigate?: (to: string) => void }> = ({ navigate }) = setStatusMessage(`Saving item ${i + 1} of ${analysisData.items.length}...`); // Normalize category name from AI (e.g., "Food " -> "food") - const aiCategoryName = item.category.trim(); + const aiCategoryName = (item.category || 'Other').trim(); const aiCategoryKey = aiCategoryName.toLowerCase(); let targetCategory = categoryMap.get(aiCategoryKey); @@ -254,13 +254,13 @@ const Receipts: React.FC<{ navigate?: (to: string) => void }> = ({ navigate }) = {item.category} - {item.quantity > 1 && `x${item.quantity}`} + {item.quantity && item.quantity > 1 && `x${item.quantity}`} {/* Price */}
- {item.price.toFixed(2)} + {(item.price ?? 0).toFixed(2)}
{/* DELETE BUTTON */} diff --git a/Web/React/src/components/ReceiptsManual.tsx b/Web/React/src/components/ReceiptsManual.tsx index b1b02b3..63f81e7 100644 --- a/Web/React/src/components/ReceiptsManual.tsx +++ b/Web/React/src/components/ReceiptsManual.tsx @@ -1,10 +1,11 @@ // src/components/ReceiptsManual.tsx import React, { useState, useEffect } from 'react'; import '../App.css'; -import type { ReceiptItem } from './Receipts'; import { useAuth } from '../hooks/useAuth'; import apiClient from '../services/api-client'; import categoryService, { Category } from '../services/category-service'; +import groupService, { Group } from '../services/group-service'; +import { ReceiptItem } from '../services/receipt-service'; const ReceiptsManual: React.FC<{ onCreated: (it: ReceiptItem) => void; groupId?: number | null }> = ({ onCreated, groupId = null }) => { const [title, setTitle] = useState(''); @@ -13,9 +14,10 @@ const ReceiptsManual: React.FC<{ onCreated: (it: ReceiptItem) => void; groupId?: const [amount, setAmount] = useState(''); const [categories, setCategories] = useState([]); const [selectedCategoryId, setSelectedCategoryId] = useState(null); + const [groups, setGroups] = useState([]); useEffect(() => { - const fetchCategories = async () => { + const fetchData = async () => { try { const cats = await categoryService.getCategories(user?.id); @@ -28,7 +30,7 @@ const ReceiptsManual: React.FC<{ onCreated: (it: ReceiptItem) => void; groupId?: try { const createdData = await categoryService.createCategory(title); // Fetch the created category details - const allCats = await categoryService.getCategories(user?.id); + const allCats : Category[] = await categoryService.getCategories(user?.id); const newCat = allCats.find(c => c.id === createdData.id); if (newCat) createdCats.push(newCat); } catch (err) { @@ -46,12 +48,18 @@ const ReceiptsManual: React.FC<{ onCreated: (it: ReceiptItem) => void; groupId?: setSelectedCategoryId(cats[0].id); } } + + // Fetch groups + if (user?.id) { + const groupsData = await groupService.getUserGroups(user.id); + setGroups(groupsData); + } } catch (err) { - console.error('Failed to fetch categories', err); + console.error('Failed to fetch data', err); } }; - fetchCategories(); - }, []); + fetchData(); + }, [user?.id]); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -79,7 +87,7 @@ const ReceiptsManual: React.FC<{ onCreated: (it: ReceiptItem) => void; groupId?: body.group_id = groupId; } - const res = await apiClient.post('/expenses', body); + const res = await apiClient.post('/expenses/', body); // Backend returns APIResponse { success: true, data: { id: ... } } const responseData = res.data; @@ -150,7 +158,7 @@ const ReceiptsManual: React.FC<{ onCreated: (it: ReceiptItem) => void; groupId?: setAmount(e.target.value)} placeholder="50" required step="any" /> {groupId && ( -
Linked to group: {mockGroups.find(g=>g.id===groupId)?.name}
+
Linked to group: {groups.find(g=>g.id===groupId)?.name}
)}
diff --git a/Web/React/src/components/ReceiptsView.tsx b/Web/React/src/components/ReceiptsView.tsx index 880b75e..d010055 100644 --- a/Web/React/src/components/ReceiptsView.tsx +++ b/Web/React/src/components/ReceiptsView.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import '../App.css'; -import type { ReceiptItem } from './Receipts'; +import type { ReceiptItem } from '../services/receipt-service'; import { useAuth } from '../hooks/useAuth'; import { useCurrency } from '../contexts/CurrencyContext'; import apiClient from '../services/api-client'; @@ -64,7 +64,7 @@ async function fetchPage(pageIndex: number, pageSize: number, filters: any) { // For group expenses, we could use group_ids param, but we don't know all group IDs // So we fetch all and filter client-side based on whether expense has group_id - const res = await apiClient.get('/expenses', { params }); + const res = await apiClient.get('/expenses/', { params }); // Backend returns APIResponse { success: true, data: [expenses] } const responseData = res.data; @@ -110,7 +110,7 @@ async function fetchPage(pageIndex: number, pageSize: number, filters: any) { // Title filtering (backend doesn't support it) if (filters.qTitle) { - filtered = filtered.filter(item => item.title.toLowerCase().includes(filters.qTitle.toLowerCase())); + filtered = filtered.filter(item => item.title?.toLowerCase().includes(filters.qTitle.toLowerCase())); } // Expense type filtering @@ -145,7 +145,7 @@ async function deleteExpense(id: number) { //update API call async function updateExpense(id: number, body: any) { try { - const res = await apiClient.put(`/expenses/${id}`, body); + const res = await apiClient.put(`/expenses/${id}/`, body); return res.data; } catch (err) { console.error('Failed to update expense', err); @@ -196,7 +196,7 @@ const ReceiptsView: React.FC<{ onNeedRefresh?: () => void; refreshKey?: number } // Extract unique categories from items - accumulate, don't replace useEffect(() => { - const newCategories = items.map(item => item.subtitle).filter(Boolean); + const newCategories = items.map(item => item.subtitle).filter((c): c is string => Boolean(c)); setCategories(prev => { const combined = new Set([...prev, ...newCategories]); return Array.from(combined); @@ -522,7 +522,7 @@ const saveEdit = async () => {
{useCurrency().formatAmount(-Math.abs(it.amount))}
-
{it.dateTransaction}
+
{it.dateTransaction || it.dateAdded}
diff --git a/Web/React/src/contexts/AuthContext.tsx b/Web/React/src/contexts/AuthContext.tsx index e2d4066..dd379bc 100644 --- a/Web/React/src/contexts/AuthContext.tsx +++ b/Web/React/src/contexts/AuthContext.tsx @@ -47,7 +47,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const checkAuthStatus = async () => { try { const response = await authService.getMe(); // GET /users/auth/me - // Backend returns APIResponse { success: true, data: UserResponse } + // Backend returns UserResponse - check if wrapped const userData = response.data?.data || response.data; setUser(userData); } catch { @@ -61,7 +61,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { // call login endpoint const response = await authService.login({ email, password }); - // Backend returns APIResponse { success: true, data: { access_token, user } } + console.log('Login response:', response.data); + + // Backend returns AuthResponse - check if wrapped in data object const responseData = response.data?.data || response.data; // If backend indicates 2FA is required, it should return e.g.: @@ -74,6 +76,11 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { return; } + // Store access token in sessionStorage + if (responseData?.access_token) { + sessionStorage.setItem('access_token', responseData.access_token); + } + // Extract user from response if (responseData?.user) { setUser(responseData.user); @@ -81,7 +88,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { // fallback: attempt to fetch /me try { const me = await authService.getMe(); - const userData = me.data?.data || me.data; + const userData = me.data; setUser(userData); } catch (err) { console.error('Login: failed to fetch /me after login', err); @@ -124,10 +131,15 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { }, []); const register = useCallback(async (accountData: any, verificationMethod?: 'email'|'number') => { - // Register endpoint returns { success: true, data: { access_token, id, user } } + // Register endpoint returns AuthResponse { access_token, user } const response = await authService.register(accountData); const responseData = response.data?.data || response.data; + // Store access token in sessionStorage + if (responseData?.access_token) { + sessionStorage.setItem('access_token', responseData.access_token); + } + // Extract user from register response if (responseData?.user) { setUser(responseData.user); @@ -146,9 +158,11 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const logout = useCallback(async () => { try { await authService.logout(); + sessionStorage.removeItem('access_token'); setUser(null); } catch (error) { console.error('Logout error:', error); + sessionStorage.removeItem('access_token'); setUser(null); } }, []); diff --git a/Web/React/src/services/api-client.ts b/Web/React/src/services/api-client.ts index d4b6211..96f722f 100644 --- a/Web/React/src/services/api-client.ts +++ b/Web/React/src/services/api-client.ts @@ -1,17 +1,17 @@ import axios, { CanceledError } from 'axios'; const apiClient = axios.create({ - baseURL: 'http://localhost:8000', - withCredentials: true, + baseURL: 'https://backend.gitpushforce-ubb.com', + // withCredentials: true, // Temporarily disabled to test CORS }) -// apiClient.interceptors.request.use((config) => { -// const token = sessionStorage.getItem('access_token'); -// if(token) { -// config.headers.Authorization = `Bearer ${token}`; -// } -// return config; -// }) +apiClient.interceptors.request.use((config) => { + const token = sessionStorage.getItem('access_token'); + if(token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}) export default apiClient; export {CanceledError}; \ No newline at end of file diff --git a/Web/React/src/services/auth-service.ts b/Web/React/src/services/auth-service.ts index 9398557..5ac0943 100644 --- a/Web/React/src/services/auth-service.ts +++ b/Web/React/src/services/auth-service.ts @@ -26,6 +26,10 @@ interface AuthResponse { updated_at: string; hashed_password: string; }; + data:any; + two_fa_required?: boolean; + two_fa_method?: 'email' | 'number' | 'app'; + temp_token?: string; } interface UserResponse { @@ -34,15 +38,16 @@ interface UserResponse { last_name: string; email: string; phone_number: string; + data:any; } class AuthService { login(credentials: LoginData) { - return apiClient.post('/users/auth/login', credentials); + return apiClient.post('/users/auth/login/', credentials); } register(data: RegisterData) { - return apiClient.post('/users/auth/register', data); + return apiClient.post('/users/auth/register/', data); } getMe() { @@ -50,7 +55,7 @@ class AuthService { } logout() { - return apiClient.post('/users/auth/logout'); + return apiClient.post('/users/auth/logout/'); } } diff --git a/Web/React/src/services/http-service.ts b/Web/React/src/services/http-service.ts index 8816370..38af688 100644 --- a/Web/React/src/services/http-service.ts +++ b/Web/React/src/services/http-service.ts @@ -14,7 +14,7 @@ class HttpService { getAll() { const controller = new AbortController(); - const request = apiClient.get(this.endpoint, { + const request = apiClient.get(this.endpoint + '/', { signal: controller.signal, }); return { request, cancel: () => controller.abort() }; @@ -25,7 +25,7 @@ class HttpService { } create(entity: T) { - return apiClient.post(this.endpoint, entity); + return apiClient.post(this.endpoint + '/', entity); } update(entity: T) { diff --git a/Web/React/src/services/receipt-service.ts b/Web/React/src/services/receipt-service.ts index 4286b50..a7baeb7 100644 --- a/Web/React/src/services/receipt-service.ts +++ b/Web/React/src/services/receipt-service.ts @@ -2,11 +2,23 @@ import apiClient from './api-client'; // Import your configured client export interface ReceiptItem { - name: string; - quantity: number; - price: number; - category: string; - keywords: string[]; + id?: any; + name?: string; + title: string; + subtitle: string; + quantity?: number; + price?: number; + amount: any; + category?: string; + keywords?: string[]; + dateAdded?: string; + dateTransaction?: string; + isGroup?: boolean; + groupId?: number; + groupName?: string; + addedBy?: string; + initial?: string; + userId?: number; } export interface ReceiptAnalysisResult {