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:
2025-12-11 02:19:05 -05:00
parent 40210c454e
commit df2cf418ea
48 changed files with 247 additions and 61 deletions

View File

@@ -35,6 +35,7 @@ export default function AddAssetDialog({open, onOpenChange}: Props) {
}
setErrors(newErrors);
return isValid;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,6 +51,7 @@ class ApiClient {
error: response.statusText,
} as ApiError;
}
return {} as T;
}

View File

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

View File

@@ -17,6 +17,7 @@ export const formatCurrencyCompact = (value: number): string => {
if (Math.abs(value) >= 1000) {
return `$${(value / 1000).toFixed(0)}k`;
}
return formatCurrency(value);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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