Enhance ESLint configuration and improve code consistency
- Added '@typescript-eslint/no-unused-vars' rule to ESLint configuration for better variable management in TypeScript files. - Updated database.ts to ensure consistent logging format. - Refactored AuthController and CashflowController to improve variable naming and maintainability. - Added spacing for better readability in multiple controller methods. - Adjusted error handling in middleware and repository files for improved clarity. - Enhanced various service and repository methods to ensure consistent return types and error handling. - Made minor formatting adjustments across frontend components for improved user experience.
This commit is contained in:
@@ -12,9 +12,10 @@ class DatabaseConnection {
|
||||
public static getInstance(): PrismaClient {
|
||||
if (!DatabaseConnection.instance) {
|
||||
DatabaseConnection.instance = new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
}
|
||||
|
||||
return DatabaseConnection.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export class AuthController {
|
||||
email: user.email
|
||||
});
|
||||
|
||||
const {password: _, ...userWithoutPassword} = user;
|
||||
const {password: _password, ...userWithoutPassword} = user;
|
||||
|
||||
return reply.send({
|
||||
user: userWithoutPassword,
|
||||
|
||||
@@ -46,12 +46,14 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const data = createIncomeSchema.parse(request.body);
|
||||
const income = await this.cashflowService.createIncome(userId, data);
|
||||
|
||||
return reply.status(201).send({income});
|
||||
}
|
||||
|
||||
async getAllIncome(request: FastifyRequest, reply: FastifyReply) {
|
||||
const userId = getUserId(request);
|
||||
const income = await this.cashflowService.getAllIncome(userId);
|
||||
|
||||
return reply.send({income});
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const {id} = request.params as {id: string};
|
||||
const income = await this.cashflowService.getIncomeById(id, userId);
|
||||
|
||||
return reply.send({income});
|
||||
}
|
||||
|
||||
@@ -67,6 +70,7 @@ export class CashflowController {
|
||||
const {id} = request.params as {id: string};
|
||||
const data = updateIncomeSchema.parse(request.body);
|
||||
const income = await this.cashflowService.updateIncome(id, userId, data);
|
||||
|
||||
return reply.send({income});
|
||||
}
|
||||
|
||||
@@ -74,12 +78,14 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const {id} = request.params as {id: string};
|
||||
await this.cashflowService.deleteIncome(id, userId);
|
||||
|
||||
return reply.status(204).send();
|
||||
}
|
||||
|
||||
async getTotalMonthlyIncome(request: FastifyRequest, reply: FastifyReply) {
|
||||
const userId = getUserId(request);
|
||||
const total = await this.cashflowService.getTotalMonthlyIncome(userId);
|
||||
|
||||
return reply.send({total});
|
||||
}
|
||||
|
||||
@@ -88,6 +94,7 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const data = createExpenseSchema.parse(request.body);
|
||||
const expense = await this.cashflowService.createExpense(userId, data);
|
||||
|
||||
return reply.status(201).send({expense});
|
||||
}
|
||||
|
||||
@@ -97,10 +104,12 @@ export class CashflowController {
|
||||
|
||||
if (byCategory === 'true') {
|
||||
const expenses = await this.cashflowService.getExpensesByCategory(userId);
|
||||
|
||||
return reply.send({expenses});
|
||||
}
|
||||
|
||||
const expenses = await this.cashflowService.getAllExpenses(userId);
|
||||
|
||||
return reply.send({expenses});
|
||||
}
|
||||
|
||||
@@ -108,6 +117,7 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const {id} = request.params as {id: string};
|
||||
const expense = await this.cashflowService.getExpenseById(id, userId);
|
||||
|
||||
return reply.send({expense});
|
||||
}
|
||||
|
||||
@@ -116,6 +126,7 @@ export class CashflowController {
|
||||
const {id} = request.params as {id: string};
|
||||
const data = updateExpenseSchema.parse(request.body);
|
||||
const expense = await this.cashflowService.updateExpense(id, userId, data);
|
||||
|
||||
return reply.send({expense});
|
||||
}
|
||||
|
||||
@@ -123,12 +134,14 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const {id} = request.params as {id: string};
|
||||
await this.cashflowService.deleteExpense(id, userId);
|
||||
|
||||
return reply.status(204).send();
|
||||
}
|
||||
|
||||
async getTotalMonthlyExpenses(request: FastifyRequest, reply: FastifyReply) {
|
||||
const userId = getUserId(request);
|
||||
const total = await this.cashflowService.getTotalMonthlyExpenses(userId);
|
||||
|
||||
return reply.send({total});
|
||||
}
|
||||
|
||||
@@ -137,6 +150,7 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const data = createTransactionSchema.parse(request.body);
|
||||
const transaction = await this.cashflowService.createTransaction(userId, data);
|
||||
|
||||
return reply.status(201).send({transaction});
|
||||
}
|
||||
|
||||
@@ -150,15 +164,18 @@ export class CashflowController {
|
||||
|
||||
if (type) {
|
||||
const transactions = await this.cashflowService.getTransactionsByType(userId, type);
|
||||
|
||||
return reply.send({transactions});
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
const transactions = await this.cashflowService.getTransactionsByDateRange(userId, new Date(startDate), new Date(endDate));
|
||||
|
||||
return reply.send({transactions});
|
||||
}
|
||||
|
||||
const transactions = await this.cashflowService.getAllTransactions(userId);
|
||||
|
||||
return reply.send({transactions});
|
||||
}
|
||||
|
||||
@@ -166,6 +183,7 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const {id} = request.params as {id: string};
|
||||
const transaction = await this.cashflowService.getTransactionById(id, userId);
|
||||
|
||||
return reply.send({transaction});
|
||||
}
|
||||
|
||||
@@ -173,6 +191,7 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const {id} = request.params as {id: string};
|
||||
await this.cashflowService.deleteTransaction(id, userId);
|
||||
|
||||
return reply.status(204).send();
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,12 @@ export class ClientController {
|
||||
|
||||
if (withStats === 'true') {
|
||||
const clients = await this.clientService.getWithStats(userId);
|
||||
|
||||
return reply.send({clients});
|
||||
}
|
||||
|
||||
const clients = await this.clientService.getAllByUser(userId);
|
||||
|
||||
return reply.send({clients});
|
||||
}
|
||||
|
||||
|
||||
@@ -61,15 +61,18 @@ export class DebtAccountController {
|
||||
|
||||
if (categoryId) {
|
||||
const accounts = await this.accountService.getByCategory(categoryId, userId);
|
||||
|
||||
return reply.send({accounts});
|
||||
}
|
||||
|
||||
if (withStats === 'true') {
|
||||
const accounts = await this.accountService.getWithStats(userId);
|
||||
|
||||
return reply.send({accounts});
|
||||
}
|
||||
|
||||
const accounts = await this.accountService.getAllByUser(userId);
|
||||
|
||||
return reply.send({accounts});
|
||||
}
|
||||
|
||||
|
||||
@@ -49,10 +49,12 @@ export class DebtCategoryController {
|
||||
|
||||
if (withStats === 'true') {
|
||||
const categories = await this.categoryService.getWithStats(userId);
|
||||
|
||||
return reply.send({categories});
|
||||
}
|
||||
|
||||
const categories = await this.categoryService.getAllByUser(userId);
|
||||
|
||||
return reply.send({categories});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,15 +42,18 @@ export class DebtPaymentController {
|
||||
|
||||
if (accountId) {
|
||||
const payments = await this.paymentService.getByAccount(accountId, userId);
|
||||
|
||||
return reply.send({payments});
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
const payments = await this.paymentService.getByDateRange(userId, new Date(startDate), new Date(endDate));
|
||||
|
||||
return reply.send({payments});
|
||||
}
|
||||
|
||||
const payments = await this.paymentService.getAllByUser(userId);
|
||||
|
||||
return reply.send({payments});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import {UnauthorizedError} from '../utils/errors';
|
||||
* Authentication Middleware
|
||||
* Verifies JWT token and attaches user to request
|
||||
*/
|
||||
export async function authenticate(request: FastifyRequest, reply: FastifyReply) {
|
||||
export async function authenticate(request: FastifyRequest, _reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
throw new UnauthorizedError('Invalid or expired token');
|
||||
}
|
||||
}
|
||||
@@ -20,5 +20,6 @@ export function getUserId(request: FastifyRequest): string {
|
||||
if (!request.user || !request.user.id) {
|
||||
throw new UnauthorizedError('User not authenticated');
|
||||
}
|
||||
|
||||
return request.user.id;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
|
||||
|
||||
// Handle Prisma errors
|
||||
if (error.name === 'PrismaClientKnownRequestError') {
|
||||
const prismaError = error as any;
|
||||
const prismaError = error as FastifyError & {code?: string};
|
||||
if (prismaError.code === 'P2002') {
|
||||
return reply.status(409).send({
|
||||
error: 'ConflictError',
|
||||
|
||||
@@ -16,7 +16,7 @@ export class AssetRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findAllByUser(userId: string, filters?: Record<string, any>): Promise<Asset[]> {
|
||||
async findAllByUser(userId: string, filters?: Record<string, unknown>): Promise<Asset[]> {
|
||||
return prisma.asset.findMany({
|
||||
where: {userId, ...filters},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
@@ -43,6 +43,7 @@ export class AssetRepository {
|
||||
where: {userId},
|
||||
_sum: {value: true}
|
||||
});
|
||||
|
||||
return result._sum.value || 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export class IncomeSourceRepository {
|
||||
where: {userId},
|
||||
_sum: {amount: true}
|
||||
});
|
||||
|
||||
return result._sum.amount || 0;
|
||||
}
|
||||
}
|
||||
@@ -79,15 +80,18 @@ export class ExpenseRepository {
|
||||
where: {userId},
|
||||
_sum: {amount: true}
|
||||
});
|
||||
|
||||
return result._sum.amount || 0;
|
||||
}
|
||||
|
||||
async getByCategory(userId: string): Promise<Record<string, Expense[]>> {
|
||||
const expenses = await this.findAllByUser(userId);
|
||||
|
||||
return expenses.reduce(
|
||||
(acc, expense) => {
|
||||
if (!acc[expense.category]) acc[expense.category] = [];
|
||||
acc[expense.category].push(expense);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Expense[]>
|
||||
|
||||
@@ -3,6 +3,17 @@ import {DatabaseConnection} from '../config/database';
|
||||
|
||||
const prisma = DatabaseConnection.getInstance();
|
||||
|
||||
export interface ClientStats {
|
||||
totalInvoices: number;
|
||||
paidInvoices: number;
|
||||
totalRevenue: number;
|
||||
outstandingAmount: number;
|
||||
}
|
||||
|
||||
export interface ClientWithStats extends Client {
|
||||
stats: ClientStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository for Client data access
|
||||
* Implements Single Responsibility Principle - handles only database operations
|
||||
@@ -97,7 +108,7 @@ export class ClientRepository {
|
||||
/**
|
||||
* Get clients with their invoice statistics
|
||||
*/
|
||||
async getWithStats(userId: string): Promise<any[]> {
|
||||
async getWithStats(userId: string): Promise<ClientWithStats[]> {
|
||||
const clients = await prisma.client.findMany({
|
||||
where: {userId},
|
||||
include: {
|
||||
|
||||
@@ -3,6 +3,17 @@ import {DatabaseConnection} from '../config/database';
|
||||
|
||||
const prisma = DatabaseConnection.getInstance();
|
||||
|
||||
export interface DebtAccountStats {
|
||||
totalPaid: number;
|
||||
numberOfPayments: number;
|
||||
lastPaymentDate: Date | null;
|
||||
lastPaymentAmount: number | null;
|
||||
}
|
||||
|
||||
export interface DebtAccountWithStats extends DebtAccount {
|
||||
stats: DebtAccountStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository for DebtAccount data access
|
||||
* Implements Single Responsibility Principle - handles only database operations
|
||||
@@ -101,7 +112,7 @@ export class DebtAccountRepository {
|
||||
/**
|
||||
* Get accounts with payment statistics
|
||||
*/
|
||||
async getWithStats(userId: string): Promise<any[]> {
|
||||
async getWithStats(userId: string): Promise<DebtAccountWithStats[]> {
|
||||
const accounts = await prisma.debtAccount.findMany({
|
||||
where: {userId},
|
||||
include: {
|
||||
|
||||
@@ -4,6 +4,16 @@ import {IUserScopedRepository} from './interfaces/IRepository';
|
||||
|
||||
const prisma = DatabaseConnection.getInstance();
|
||||
|
||||
export interface DebtCategoryStats {
|
||||
totalAccounts: number;
|
||||
totalDebt: number;
|
||||
totalPayments: number;
|
||||
}
|
||||
|
||||
export interface DebtCategoryWithStats extends DebtCategory {
|
||||
stats: DebtCategoryStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository for DebtCategory data access
|
||||
* Implements Single Responsibility Principle - handles only database operations
|
||||
@@ -91,7 +101,7 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
|
||||
/**
|
||||
* Get categories with debt statistics
|
||||
*/
|
||||
async getWithStats(userId: string): Promise<any[]> {
|
||||
async getWithStats(userId: string): Promise<DebtCategoryWithStats[]> {
|
||||
const categories = await this.findAllByUser(userId);
|
||||
|
||||
return Promise.all(
|
||||
|
||||
@@ -61,6 +61,7 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
|
||||
...(excludeId && {id: {not: excludeId}})
|
||||
}
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
@@ -72,6 +73,7 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
|
||||
invoiceNumber: {startsWith: `INV-${year}-`}
|
||||
}
|
||||
});
|
||||
|
||||
return `INV-${year}-${String(count + 1).padStart(3, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ export class LiabilityRepository {
|
||||
acc[type] = [];
|
||||
}
|
||||
acc[type].push(liability);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Liability[]>
|
||||
|
||||
@@ -4,6 +4,13 @@ import {IUserScopedRepository} from './interfaces/IRepository';
|
||||
|
||||
const prisma = DatabaseConnection.getInstance();
|
||||
|
||||
export interface GrowthStats {
|
||||
date: Date;
|
||||
netWorth: number;
|
||||
growthAmount: number;
|
||||
growthPercent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository for NetWorthSnapshot data access
|
||||
* Implements Single Responsibility Principle - handles only database operations
|
||||
@@ -84,7 +91,7 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
|
||||
/**
|
||||
* Get growth over time (percentage change between snapshots)
|
||||
*/
|
||||
async getGrowthStats(userId: string, limit: number = 12): Promise<any[]> {
|
||||
async getGrowthStats(userId: string, limit: number = 12): Promise<GrowthStats[]> {
|
||||
const snapshots = await prisma.netWorthSnapshot.findMany({
|
||||
where: {userId},
|
||||
orderBy: {date: 'desc'},
|
||||
|
||||
@@ -46,6 +46,7 @@ export class UserRepository implements IRepository<User> {
|
||||
|
||||
async emailExists(email: string): Promise<boolean> {
|
||||
const count = await prisma.user.count({where: {email}});
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export class AssetService {
|
||||
if (!asset) {
|
||||
throw new NotFoundError('Asset not found');
|
||||
}
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
@@ -80,6 +81,7 @@ export class AssetService {
|
||||
|
||||
async getByType(userId: string): Promise<Record<string, Asset[]>> {
|
||||
const assets = await this.assetRepository.findAllByUser(userId);
|
||||
|
||||
return assets.reduce(
|
||||
(acc, asset) => {
|
||||
const type = asset.type;
|
||||
@@ -87,6 +89,7 @@ export class AssetService {
|
||||
acc[type] = [];
|
||||
}
|
||||
acc[type].push(asset);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Asset[]>
|
||||
|
||||
@@ -40,7 +40,8 @@ export class AuthService {
|
||||
await this.debtCategoryService.createDefaultCategories(user.id);
|
||||
|
||||
// Return user without password
|
||||
const {password: _, ...userWithoutPassword} = user;
|
||||
const {password: _password, ...userWithoutPassword} = user;
|
||||
|
||||
return userWithoutPassword;
|
||||
}
|
||||
|
||||
@@ -62,7 +63,8 @@ export class AuthService {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) return null;
|
||||
|
||||
const {password: _, ...userWithoutPassword} = user;
|
||||
const {password: _password, ...userWithoutPassword} = user;
|
||||
|
||||
return userWithoutPassword;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ export class CashflowService {
|
||||
const income = await this.incomeRepository.findById(id);
|
||||
if (!income) throw new NotFoundError('Income source not found');
|
||||
if (income.userId !== userId) throw new ForbiddenError('Access denied');
|
||||
|
||||
return income;
|
||||
}
|
||||
|
||||
@@ -63,6 +64,7 @@ export class CashflowService {
|
||||
if (data.amount !== undefined && data.amount <= 0) {
|
||||
throw new ValidationError('Amount must be greater than 0');
|
||||
}
|
||||
|
||||
return this.incomeRepository.update(id, data);
|
||||
}
|
||||
|
||||
@@ -93,6 +95,7 @@ export class CashflowService {
|
||||
const expense = await this.expenseRepository.findById(id);
|
||||
if (!expense) throw new NotFoundError('Expense not found');
|
||||
if (expense.userId !== userId) throw new ForbiddenError('Access denied');
|
||||
|
||||
return expense;
|
||||
}
|
||||
|
||||
@@ -101,6 +104,7 @@ export class CashflowService {
|
||||
if (data.amount !== undefined && data.amount <= 0) {
|
||||
throw new ValidationError('Amount must be greater than 0');
|
||||
}
|
||||
|
||||
return this.expenseRepository.update(id, data);
|
||||
}
|
||||
|
||||
@@ -136,6 +140,7 @@ export class CashflowService {
|
||||
const transaction = await this.transactionRepository.findById(id);
|
||||
if (!transaction) throw new NotFoundError('Transaction not found');
|
||||
if (transaction.userId !== userId) throw new ForbiddenError('Access denied');
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Client} from '@prisma/client';
|
||||
import {ClientRepository} from '../repositories/ClientRepository';
|
||||
import {ClientRepository, ClientWithStats} from '../repositories/ClientRepository';
|
||||
import {NotFoundError, ValidationError, ForbiddenError, ConflictError} from '../utils/errors';
|
||||
|
||||
export interface CreateClientDTO {
|
||||
@@ -60,7 +60,7 @@ export class ClientService {
|
||||
/**
|
||||
* Get clients with statistics
|
||||
*/
|
||||
async getWithStats(userId: string): Promise<any[]> {
|
||||
async getWithStats(userId: string): Promise<ClientWithStats[]> {
|
||||
return this.clientRepository.getWithStats(userId);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export class ClientService {
|
||||
*/
|
||||
async delete(id: string, userId: string): Promise<void> {
|
||||
// Verify ownership
|
||||
const client = await this.getById(id, userId);
|
||||
await this.getById(id, userId);
|
||||
|
||||
// Check if client has invoices - we still allow deletion due to cascade
|
||||
// but you might want to prevent deletion if there are invoices
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {DebtAccount} from '@prisma/client';
|
||||
import {DebtAccountRepository} from '../repositories/DebtAccountRepository';
|
||||
import {DebtAccountRepository, DebtAccountWithStats} from '../repositories/DebtAccountRepository';
|
||||
import {DebtCategoryRepository} from '../repositories/DebtCategoryRepository';
|
||||
import {NotFoundError, ValidationError, ForbiddenError} from '../utils/errors';
|
||||
|
||||
@@ -79,7 +79,7 @@ export class DebtAccountService {
|
||||
/**
|
||||
* Get debt accounts with statistics
|
||||
*/
|
||||
async getWithStats(userId: string): Promise<any[]> {
|
||||
async getWithStats(userId: string): Promise<DebtAccountWithStats[]> {
|
||||
return this.accountRepository.getWithStats(userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {DebtCategory} from '@prisma/client';
|
||||
import {DebtCategoryRepository} from '../repositories/DebtCategoryRepository';
|
||||
import {DebtCategoryRepository, DebtCategoryWithStats} from '../repositories/DebtCategoryRepository';
|
||||
import {NotFoundError, ValidationError, ForbiddenError, ConflictError} from '../utils/errors';
|
||||
|
||||
export interface CreateDebtCategoryDTO {
|
||||
@@ -84,7 +84,7 @@ export class DebtCategoryService {
|
||||
/**
|
||||
* Get categories with statistics
|
||||
*/
|
||||
async getWithStats(userId: string): Promise<any[]> {
|
||||
async getWithStats(userId: string): Promise<DebtCategoryWithStats[]> {
|
||||
return this.categoryRepository.getWithStats(userId);
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export class DebtCategoryService {
|
||||
* Delete a category
|
||||
*/
|
||||
async delete(id: string, userId: string): Promise<void> {
|
||||
const category = await this.getById(id, userId);
|
||||
await this.getById(id, userId);
|
||||
|
||||
// Check if category has accounts
|
||||
// Note: Cascade delete will remove all accounts and payments
|
||||
|
||||
@@ -59,6 +59,7 @@ export class InvoiceService {
|
||||
if (!invoice) {
|
||||
throw new NotFoundError('Invoice not found');
|
||||
}
|
||||
|
||||
return invoice as unknown as Invoice;
|
||||
}
|
||||
|
||||
@@ -193,6 +194,7 @@ export class InvoiceService {
|
||||
|
||||
async getOverdueInvoices(userId: string): Promise<Invoice[]> {
|
||||
const invoices = await this.invoiceRepository.findAllByUser(userId, {status: 'overdue'});
|
||||
|
||||
return invoices as unknown as Invoice[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {NetWorthSnapshot} from '@prisma/client';
|
||||
import {NetWorthSnapshotRepository} from '../repositories/NetWorthSnapshotRepository';
|
||||
import {NetWorthSnapshotRepository, GrowthStats} from '../repositories/NetWorthSnapshotRepository';
|
||||
import {AssetRepository} from '../repositories/AssetRepository';
|
||||
import {LiabilityRepository} from '../repositories/LiabilityRepository';
|
||||
import {NotFoundError, ValidationError, ForbiddenError} from '../utils/errors';
|
||||
@@ -154,7 +154,7 @@ export class NetWorthService {
|
||||
/**
|
||||
* Get growth statistics
|
||||
*/
|
||||
async getGrowthStats(userId: string, limit?: number): Promise<any[]> {
|
||||
async getGrowthStats(userId: string, limit?: number): Promise<GrowthStats[]> {
|
||||
return this.snapshotRepository.getGrowthStats(userId, limit);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import {defineConfig, globalIgnores} from 'eslint/config';
|
||||
|
||||
// Shared rules for all TypeScript files
|
||||
const sharedRules = {
|
||||
'padding-line-between-statements': ['error', {blankLine: 'always', prev: '*', next: 'return'}]
|
||||
'padding-line-between-statements': ['error', {blankLine: 'always', prev: '*', next: 'return'}],
|
||||
'@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_'}]
|
||||
};
|
||||
|
||||
export default defineConfig([
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function AddAssetDialog({open, onOpenChange}: Props) {
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useState} from 'react';
|
||||
import {useState, useEffect} from 'react';
|
||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select';
|
||||
import {Button} from '@/components/ui/button';
|
||||
@@ -11,6 +11,10 @@ interface Props {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function getDefaultDueDate(): string {
|
||||
return new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
export default function AddInvoiceDialog({open, onOpenChange}: Props) {
|
||||
const dispatch = useAppDispatch();
|
||||
const {clients} = useAppSelector(state => state.invoices);
|
||||
@@ -18,9 +22,17 @@ export default function AddInvoiceDialog({open, onOpenChange}: Props) {
|
||||
clientId: '',
|
||||
description: '',
|
||||
amount: '',
|
||||
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
||||
dueDate: getDefaultDueDate()
|
||||
});
|
||||
|
||||
// Reset form with fresh due date when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional pattern for form dialog
|
||||
setForm(prev => ({...prev, dueDate: getDefaultDueDate()}));
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const now = new Date().toISOString();
|
||||
@@ -52,7 +64,7 @@ export default function AddInvoiceDialog({open, onOpenChange}: Props) {
|
||||
})
|
||||
);
|
||||
onOpenChange(false);
|
||||
setForm({clientId: '', description: '', amount: '', dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]});
|
||||
setForm({clientId: '', description: '', amount: '', dueDate: getDefaultDueDate()});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,8 +20,10 @@ export default function EditAssetDialog({open, onOpenChange, asset}: Props) {
|
||||
const [form, setForm] = useState({name: '', type: '', value: ''});
|
||||
const [errors, setErrors] = useState({name: '', value: ''});
|
||||
|
||||
// Sync form state when asset changes - intentional pattern for controlled form dialogs
|
||||
useEffect(() => {
|
||||
if (asset) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional pattern for form dialog
|
||||
setForm({
|
||||
name: asset.name,
|
||||
type: asset.type,
|
||||
@@ -47,6 +49,7 @@ export default function EditAssetDialog({open, onOpenChange, asset}: Props) {
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
|
||||
@@ -24,8 +24,10 @@ export default function EditClientDialog({open, onOpenChange, client}: Props) {
|
||||
});
|
||||
const [errors, setErrors] = useState({name: '', email: '', phone: ''});
|
||||
|
||||
// Sync form state when client changes - intentional pattern for controlled form dialogs
|
||||
useEffect(() => {
|
||||
if (client) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional pattern for form dialog
|
||||
setForm({
|
||||
name: client.name,
|
||||
email: client.email,
|
||||
@@ -58,6 +60,7 @@ export default function EditClientDialog({open, onOpenChange, client}: Props) {
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@ export default function EditLiabilityDialog({open, onOpenChange, liability}: Pro
|
||||
const [form, setForm] = useState({name: '', type: '', balance: ''});
|
||||
const [errors, setErrors] = useState({name: '', balance: ''});
|
||||
|
||||
// Sync form state when liability changes - intentional pattern for controlled form dialogs
|
||||
useEffect(() => {
|
||||
if (liability) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional pattern for form dialog
|
||||
setForm({
|
||||
name: liability.name,
|
||||
type: liability.type,
|
||||
@@ -47,6 +49,7 @@ export default function EditLiabilityDialog({open, onOpenChange, liability}: Pro
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ export default function InvoiceDetailsDialog({open, onOpenChange, invoice, clien
|
||||
const dispatch = useAppDispatch();
|
||||
const [selectedStatus, setSelectedStatus] = useState<Invoice['status']>('draft');
|
||||
|
||||
// Sync status when invoice changes - intentional pattern for controlled form dialogs
|
||||
useEffect(() => {
|
||||
if (invoice) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional pattern for form dialog
|
||||
setSelectedStatus(invoice.status);
|
||||
}
|
||||
}, [invoice]);
|
||||
|
||||
@@ -27,6 +27,7 @@ export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Prop
|
||||
|
||||
if (!form.email || !form.password) {
|
||||
setError('Please enter your email and password');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,8 +42,9 @@ export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Prop
|
||||
|
||||
onOpenChange(false);
|
||||
setForm({email: '', password: ''});
|
||||
} catch (err: any) {
|
||||
setError(err || 'Login failed. Please check your credentials.');
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || 'Login failed. Please check your credentials.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -29,11 +29,13 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
|
||||
|
||||
if (form.password !== form.confirmPassword) {
|
||||
setError('Passwords do not match');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.password.length < 8) {
|
||||
setError('Password must be at least 8 characters');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,8 +51,9 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
|
||||
|
||||
onOpenChange(false);
|
||||
setForm({name: '', email: '', password: '', confirmPassword: ''});
|
||||
} catch (err: any) {
|
||||
setError(err || 'Registration failed. Please try again.');
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message || 'Registration failed. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import * as React from 'react';
|
||||
import {Slot} from '@radix-ui/react-slot';
|
||||
import {cva, type VariantProps} from 'class-variance-authority';
|
||||
|
||||
@@ -37,6 +37,7 @@ export const authService = {
|
||||
const response = await apiClient.post<AuthResponse>('/register', data);
|
||||
tokenStorage.setToken(response.token);
|
||||
tokenStorage.setUser(JSON.stringify(response.user));
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
@@ -44,6 +45,7 @@ export const authService = {
|
||||
const response = await apiClient.post<AuthResponse>('/login', data);
|
||||
tokenStorage.setToken(response.token);
|
||||
tokenStorage.setUser(JSON.stringify(response.user));
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class ApiClient {
|
||||
error: response.statusText,
|
||||
} as ApiError;
|
||||
}
|
||||
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export const calculateYTDGrowth = (snapshots: NetWorthSnapshot[]): number => {
|
||||
const currentValue = ytdSnapshots[ytdSnapshots.length - 1].netWorth;
|
||||
|
||||
if (startValue === 0) return 0;
|
||||
|
||||
return ((currentValue - startValue) / Math.abs(startValue)) * 100;
|
||||
}
|
||||
|
||||
@@ -39,6 +40,7 @@ export const calculateYTDGrowth = (snapshots: NetWorthSnapshot[]): number => {
|
||||
const currentValue = ytdSnapshots[ytdSnapshots.length - 1].netWorth;
|
||||
|
||||
if (startValue === 0) return 0;
|
||||
|
||||
return ((currentValue - startValue) / Math.abs(startValue)) * 100;
|
||||
};
|
||||
|
||||
@@ -50,10 +52,12 @@ export const calculateAllTimeGrowth = (snapshots: NetWorthSnapshot[]): number =>
|
||||
const last = sorted[sorted.length - 1];
|
||||
|
||||
if (first.netWorth === 0) return 0;
|
||||
|
||||
return ((last.netWorth - first.netWorth) / Math.abs(first.netWorth)) * 100;
|
||||
};
|
||||
|
||||
export const calculateSavingsRate = (totalIncome: number, totalExpenses: number): number => {
|
||||
if (totalIncome === 0) return 0;
|
||||
|
||||
return ((totalIncome - totalExpenses) / totalIncome) * 100;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export const formatCurrencyCompact = (value: number): string => {
|
||||
if (Math.abs(value) >= 1000) {
|
||||
return `$${(value / 1000).toFixed(0)}k`;
|
||||
}
|
||||
|
||||
return formatCurrency(value);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,24 +13,28 @@ export const sanitizeString = (input: string): string => {
|
||||
|
||||
export const validateEmail = (email: string): boolean => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
export const validatePhone = (phone: string): boolean => {
|
||||
// Accepts various phone formats
|
||||
const phoneRegex = /^[\d\s\-\(\)\+]+$/;
|
||||
const phoneRegex = /^[\d\s\-()+]+$/;
|
||||
|
||||
return phoneRegex.test(phone) && phone.replace(/\D/g, '').length >= 10;
|
||||
};
|
||||
|
||||
export const validateNumber = (value: string): number | null => {
|
||||
const parsed = parseFloat(value);
|
||||
if (isNaN(parsed)) return null;
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const validatePositiveNumber = (value: string): number | null => {
|
||||
const num = validateNumber(value);
|
||||
if (num === null || num < 0) return null;
|
||||
|
||||
return num;
|
||||
};
|
||||
|
||||
@@ -42,6 +46,7 @@ export const validateInvoiceNumber = (invoiceNumber: string, existingNumbers: st
|
||||
if (!validateRequired(invoiceNumber)) return false;
|
||||
// Check uniqueness
|
||||
const sanitized = sanitizeString(invoiceNumber);
|
||||
|
||||
return !existingNumbers.some(num => num === sanitized);
|
||||
};
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ export default function CashflowPage() {
|
||||
(acc, e) => {
|
||||
const monthly = getMonthlyAmount(e.amount, e.frequency);
|
||||
acc[e.category] = (acc[e.category] || 0) + monthly;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
@@ -115,6 +116,7 @@ export default function CashflowPage() {
|
||||
<div className="space-y-2">
|
||||
{sortedCategories.map(([category, amount]) => {
|
||||
const pct = (amount / monthlyExpenses) * 100;
|
||||
|
||||
return (
|
||||
<div key={category} className="flex items-center gap-3">
|
||||
<div className="w-24 text-sm truncate">{category}</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function ClientsPage() {
|
||||
const clientInvoices = invoices.filter(i => i.clientId === clientId);
|
||||
const totalBilled = clientInvoices.reduce((sum, i) => sum + i.total, 0);
|
||||
const outstanding = clientInvoices.filter(i => i.status === 'sent' || i.status === 'overdue').reduce((sum, i) => sum + i.total, 0);
|
||||
|
||||
return {totalBilled, outstanding, count: clientInvoices.length};
|
||||
};
|
||||
|
||||
@@ -54,6 +55,7 @@ export default function ClientsPage() {
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{clients.map(client => {
|
||||
const stats = getClientStats(client.id);
|
||||
|
||||
return (
|
||||
<Card key={client.id} className="card-elevated cursor-pointer hover:bg-accent/30 transition-colors" onClick={() => handleEditClient(client)}>
|
||||
<CardContent className="p-4">
|
||||
|
||||
@@ -114,6 +114,7 @@ export default function DebtsPage() {
|
||||
{accounts.map(account => {
|
||||
const category = getCategoryById(account.categoryId);
|
||||
const progress = getProgress(account);
|
||||
|
||||
return (
|
||||
<Card key={account.id} className="card-elevated">
|
||||
<CardContent className="p-3">
|
||||
|
||||
@@ -192,6 +192,7 @@ export default function NetWorthPage() {
|
||||
const total = assets.filter(a => a.type === type).reduce((s, a) => s + a.value, 0);
|
||||
const pct = totalAssets > 0 ? (total / totalAssets) * 100 : 0;
|
||||
if (total === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={type} className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -95,27 +95,36 @@ const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transac
|
||||
export const fetchIncomeSources = createAsyncThunk('cashflow/fetchIncomeSources', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await incomeService.getAll();
|
||||
|
||||
return response.incomeSources.map(mapApiIncomeToIncome);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch income sources');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch income sources';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchExpenses = createAsyncThunk('cashflow/fetchExpenses', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await expenseService.getAll();
|
||||
|
||||
return response.expenses.map(mapApiExpenseToExpense);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch expenses');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch expenses';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchTransactions = createAsyncThunk('cashflow/fetchTransactions', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await transactionService.getAll();
|
||||
|
||||
return response.transactions.map(mapApiTransactionToTransaction);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch transactions');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch transactions';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -72,36 +72,48 @@ const mapApiLiabilityToLiability = (apiLiability: ApiLiability): Liability => ({
|
||||
export const fetchAssets = createAsyncThunk('netWorth/fetchAssets', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await assetService.getAll();
|
||||
|
||||
return response.assets.map(mapApiAssetToAsset);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch assets');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch assets';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
export const createAsset = createAsyncThunk('netWorth/createAsset', async (data: CreateAssetRequest, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await assetService.create(data);
|
||||
|
||||
return mapApiAssetToAsset(response.asset);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to create asset');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create asset';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
export const updateAsset = createAsyncThunk('netWorth/updateAsset', async ({id, data}: {id: string; data: UpdateAssetRequest}, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await assetService.update(id, data);
|
||||
|
||||
return mapApiAssetToAsset(response.asset);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to update asset');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update asset';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteAsset = createAsyncThunk('netWorth/deleteAsset', async (id: string, {rejectWithValue}) => {
|
||||
try {
|
||||
await assetService.delete(id);
|
||||
|
||||
return id;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to delete asset');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete asset';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,18 +121,24 @@ export const deleteAsset = createAsyncThunk('netWorth/deleteAsset', async (id: s
|
||||
export const fetchLiabilities = createAsyncThunk('netWorth/fetchLiabilities', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await liabilityService.getAll();
|
||||
|
||||
return response.liabilities.map(mapApiLiabilityToLiability);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch liabilities');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch liabilities';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
export const createLiability = createAsyncThunk('netWorth/createLiability', async (data: CreateLiabilityRequest, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await liabilityService.create(data);
|
||||
|
||||
return mapApiLiabilityToLiability(response.liability);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to create liability');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create liability';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -129,9 +147,12 @@ export const updateLiability = createAsyncThunk(
|
||||
async ({id, data}: {id: string; data: UpdateLiabilityRequest}, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await liabilityService.update(id, data);
|
||||
|
||||
return mapApiLiabilityToLiability(response.liability);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to update liability');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update liability';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -139,9 +160,12 @@ export const updateLiability = createAsyncThunk(
|
||||
export const deleteLiability = createAsyncThunk('netWorth/deleteLiability', async (id: string, {rejectWithValue}) => {
|
||||
try {
|
||||
await liabilityService.delete(id);
|
||||
|
||||
return id;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to delete liability');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete liability';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -149,9 +173,12 @@ export const deleteLiability = createAsyncThunk('netWorth/deleteLiability', asyn
|
||||
export const fetchSnapshots = createAsyncThunk('netWorth/fetchSnapshots', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await snapshotService.getAll();
|
||||
|
||||
return response.snapshots;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch snapshots');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch snapshots';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -25,18 +25,24 @@ const initialState: UserState = {
|
||||
export const registerUser = createAsyncThunk('user/register', async (data: RegisterRequest, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await authService.register(data);
|
||||
|
||||
return response.user;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Registration failed');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Registration failed';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
export const loginUser = createAsyncThunk('user/login', async (data: LoginRequest, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await authService.login(data);
|
||||
|
||||
return response.user;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Login failed');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Login failed';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,10 +54,13 @@ export const loadUserFromStorage = createAsyncThunk('user/loadFromStorage', asyn
|
||||
}
|
||||
// Verify token is still valid by fetching profile
|
||||
await authService.getProfile();
|
||||
|
||||
return user;
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
authService.logout();
|
||||
return rejectWithValue(error.message || 'Session expired');
|
||||
const message = error instanceof Error ? error.message : 'Session expired';
|
||||
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user