Fix
This commit is contained in:
1 parent
959249ab22
commit
caa6020ae8
4 files changed
+132
-21
No files matched your search
+9
-2
@@ -90,7 +90,10 @@ export default function BudgetDashboard() {
|
|||||||
}));
|
}));
|
||||||
setTransactions(transactionsWithDates);
|
setTransactions(transactionsWithDates);
|
||||||
setSetupCompleted(userData.setupCompleted || false);
|
setSetupCompleted(userData.setupCompleted || false);
|
||||||
setShowSetupWizard(!(userData.setupCompleted || false));
|
|
||||||
|
// Show setup wizard if setup is not completed OR if there are no accounts
|
||||||
|
const shouldShowWizard = !(userData.setupCompleted || false) || (userData.accounts || []).length === 0;
|
||||||
|
setShowSetupWizard(shouldShowWizard);
|
||||||
} else {
|
} else {
|
||||||
// New user - show setup wizard
|
// New user - show setup wizard
|
||||||
setShowSetupWizard(true);
|
setShowSetupWizard(true);
|
||||||
@@ -686,7 +689,9 @@ export default function BudgetDashboard() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Always visible components */}
|
{/* Components visible after setup completion */}
|
||||||
|
{setupCompleted && (
|
||||||
|
<>
|
||||||
<DataInput
|
<DataInput
|
||||||
onTransactionsAdded={handleTransactionsAdded}
|
onTransactionsAdded={handleTransactionsAdded}
|
||||||
envelopes={envelopes.map(env => ({ id: env.id, name: env.name }))}
|
envelopes={envelopes.map(env => ({ id: env.id, name: env.name }))}
|
||||||
@@ -698,6 +703,8 @@ export default function BudgetDashboard() {
|
|||||||
onIncomeAdded={handleTransactionsAdded}
|
onIncomeAdded={handleTransactionsAdded}
|
||||||
onAccountUpdate={handleAccountUpdate}
|
onAccountUpdate={handleAccountUpdate}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,14 @@ export default function AuthModal({ isOpen, onClose, onAuthSuccess }: AuthModalP
|
|||||||
onAuthSuccess();
|
onAuthSuccess();
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error.message);
|
// Provide more user-friendly error messages
|
||||||
|
let errorMessage = error.message;
|
||||||
|
if (error.message === 'Invalid credentials') {
|
||||||
|
errorMessage = 'The email or password you entered is incorrect. Please try again.';
|
||||||
|
} else if (error.message === 'User already exists') {
|
||||||
|
errorMessage = 'An account with this email already exists. Please try logging in instead.';
|
||||||
|
}
|
||||||
|
setError(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-18
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { DataService } from '../lib/dataService';
|
import { DataService } from '../lib/dataService';
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
@@ -12,6 +12,7 @@ interface Account {
|
|||||||
accountNumber?: string;
|
accountNumber?: string;
|
||||||
color: string;
|
color: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
defaultPaycheckAmount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Envelope {
|
interface Envelope {
|
||||||
@@ -35,7 +36,6 @@ type WizardStep = 'welcome' | 'data-source' | 'accounts' | 'envelopes' | 'income
|
|||||||
|
|
||||||
export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardProps) {
|
export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardProps) {
|
||||||
const [currentStep, setCurrentStep] = useState<WizardStep>('welcome');
|
const [currentStep, setCurrentStep] = useState<WizardStep>('welcome');
|
||||||
const [dataSource, setDataSource] = useState<'import' | 'manual' | null>(null);
|
|
||||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||||
const [envelopes, setEnvelopes] = useState<Envelope[]>([]);
|
const [envelopes, setEnvelopes] = useState<Envelope[]>([]);
|
||||||
|
|
||||||
@@ -64,6 +64,32 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
const [incomeAllocations, setIncomeAllocations] = useState<{ [envelopeId: string]: { type: 'percentage' | 'fixed'; value: number } }>({});
|
const [incomeAllocations, setIncomeAllocations] = useState<{ [envelopeId: string]: { type: 'percentage' | 'fixed'; value: number } }>({});
|
||||||
const [defaultPaycheckAmounts, setDefaultPaycheckAmounts] = useState<{ [accountId: string]: number }>({});
|
const [defaultPaycheckAmounts, setDefaultPaycheckAmounts] = useState<{ [accountId: string]: number }>({});
|
||||||
|
|
||||||
|
// Loading and error states
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Modal ESC key handling and click outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
if (showAddAccount || showAddEnvelope) {
|
||||||
|
setShowAddAccount(false);
|
||||||
|
setShowAddEnvelope(false);
|
||||||
|
} else {
|
||||||
|
onSkip(); // Close the entire wizard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [showAddAccount, showAddEnvelope, onSkip]);
|
||||||
|
|
||||||
|
const handleBackdropClick = (event: React.MouseEvent) => {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
onSkip();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const accountTypes = [
|
const accountTypes = [
|
||||||
{ value: 'checking', label: 'Checking Account', icon: '🏦' },
|
{ value: 'checking', label: 'Checking Account', icon: '🏦' },
|
||||||
{ value: 'savings', label: 'Savings Account', icon: '💰' },
|
{ value: 'savings', label: 'Savings Account', icon: '💰' },
|
||||||
@@ -79,12 +105,26 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleAddAccount = () => {
|
const handleAddAccount = () => {
|
||||||
if (newAccount.name && newAccount.type) {
|
if (!newAccount.name || !newAccount.type) return;
|
||||||
|
|
||||||
|
// Validation: unique name
|
||||||
|
if (accounts.some(acc => acc.name.toLowerCase() === newAccount.name!.toLowerCase())) {
|
||||||
|
alert('Account name must be unique');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation: balance >= 0
|
||||||
|
const balance = newAccount.balance || 0;
|
||||||
|
if (balance < 0) {
|
||||||
|
alert('Balance cannot be negative');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const account: Account = {
|
const account: Account = {
|
||||||
id: `${newAccount.type}-${Date.now()}`,
|
id: `${newAccount.type}-${Date.now()}`,
|
||||||
name: newAccount.name,
|
name: newAccount.name,
|
||||||
type: newAccount.type as Account['type'],
|
type: newAccount.type as Account['type'],
|
||||||
balance: newAccount.balance || 0,
|
balance,
|
||||||
institution: newAccount.institution,
|
institution: newAccount.institution,
|
||||||
accountNumber: newAccount.accountNumber,
|
accountNumber: newAccount.accountNumber,
|
||||||
color: newAccount.color || 'bg-blue-500',
|
color: newAccount.color || 'bg-blue-500',
|
||||||
@@ -98,11 +138,17 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
});
|
});
|
||||||
setShowAddAccount(false);
|
setShowAddAccount(false);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddEnvelope = () => {
|
const handleAddEnvelope = () => {
|
||||||
if (newEnvelope.name && newEnvelope.accountId && newEnvelope.allocated !== undefined) {
|
if (!newEnvelope.name || !newEnvelope.accountId || newEnvelope.allocated === undefined) return;
|
||||||
|
|
||||||
|
// Validation: unique name
|
||||||
|
if (envelopes.some(env => env.name.toLowerCase() === newEnvelope.name!.toLowerCase())) {
|
||||||
|
alert('Envelope name must be unique');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const envelope: Envelope = {
|
const envelope: Envelope = {
|
||||||
id: `env-${Date.now()}`,
|
id: `env-${Date.now()}`,
|
||||||
name: newEnvelope.name,
|
name: newEnvelope.name,
|
||||||
@@ -118,11 +164,17 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
color: 'bg-green-500',
|
color: 'bg-green-500',
|
||||||
});
|
});
|
||||||
setShowAddEnvelope(false);
|
setShowAddEnvelope(false);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleComplete = async () => {
|
const handleComplete = async () => {
|
||||||
|
if (!userId) {
|
||||||
|
setErrorMessage('No user ID provided');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSaving(true);
|
||||||
|
setErrorMessage(null);
|
||||||
try {
|
try {
|
||||||
|
DataService.setUserId(userId);
|
||||||
// Apply income allocation settings to envelopes
|
// Apply income allocation settings to envelopes
|
||||||
const envelopesWithAllocations = envelopes.map(envelope => {
|
const envelopesWithAllocations = envelopes.map(envelope => {
|
||||||
const allocation = incomeAllocations[envelope.id];
|
const allocation = incomeAllocations[envelope.id];
|
||||||
@@ -148,7 +200,7 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
return account;
|
return account;
|
||||||
});
|
});
|
||||||
|
|
||||||
await DataService.saveUserData(userId, {
|
await DataService.saveUserData({
|
||||||
accounts: accountsWithDefaults,
|
accounts: accountsWithDefaults,
|
||||||
envelopes: envelopesWithAllocations,
|
envelopes: envelopesWithAllocations,
|
||||||
transactions: [],
|
transactions: [],
|
||||||
@@ -157,6 +209,9 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
onComplete(accountsWithDefaults, envelopesWithAllocations);
|
onComplete(accountsWithDefaults, envelopesWithAllocations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving setup data:', error);
|
console.error('Error saving setup data:', error);
|
||||||
|
setErrorMessage('Failed to save setup data. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,7 +220,7 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="text-6xl mb-4">💰</div>
|
<div className="text-6xl mb-4">💰</div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome to Envelope Budgeting</h1>
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome to Envelope Budgeting</h1>
|
||||||
<p className="text-gray-600 mb-4">Let's set up your budget to get started with secure, organized financial tracking.</p>
|
<p className="text-gray-600 mb-4">Let's set up your budget to get started with secure, organized financial tracking.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -189,13 +244,12 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">How would you like to set up your data?</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">How would you like to set up your data?</h2>
|
||||||
<p className="text-gray-600">Choose how you'd like to get started with your budget.</p>
|
<p className="text-gray-600">Choose how you'd like to get started with your budget.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDataSource('import');
|
|
||||||
setCurrentStep('accounts');
|
setCurrentStep('accounts');
|
||||||
}}
|
}}
|
||||||
className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 transition-colors"
|
className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 transition-colors"
|
||||||
@@ -207,7 +261,6 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDataSource('manual');
|
|
||||||
setCurrentStep('accounts');
|
setCurrentStep('accounts');
|
||||||
}}
|
}}
|
||||||
className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 transition-colors"
|
className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 transition-colors"
|
||||||
@@ -436,7 +489,7 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
<option key={account.id} value={account.id}>{account.name}</option>
|
<option key={account.id} value={account.id}>{account.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p className="text-sm text-gray-500 mt-2">We'll create a set of common envelopes (Rent, Groceries, Utilities, Savings, Transport, Entertainment, Emergency). You can edit amounts later.</p>
|
<p className="text-sm text-gray-500 mt-2">We'll create a set of common envelopes (Rent, Groceries, Utilities, Savings, Transport, Entertainment, Emergency). You can edit amounts later.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -488,7 +541,8 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
}
|
}
|
||||||
setCurrentStep('income-setup');
|
setCurrentStep('income-setup');
|
||||||
}}
|
}}
|
||||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
|
disabled={includeStandardEnvelopes && !standardEnvelopeAccountId}
|
||||||
|
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Next: Set Up Income →
|
Next: Set Up Income →
|
||||||
</button>
|
</button>
|
||||||
@@ -668,6 +722,27 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Total Percentage Warning */}
|
||||||
|
{(() => {
|
||||||
|
const totalPercentage = envelopes.reduce((sum, envelope) => {
|
||||||
|
const allocation = incomeAllocations[envelope.id];
|
||||||
|
if (allocation?.type === 'percentage') {
|
||||||
|
return sum + (allocation.value || 0);
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
if (totalPercentage > 100) {
|
||||||
|
return (
|
||||||
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||||
|
<p className="text-sm text-red-700">
|
||||||
|
Warning: Total percentage allocation ({totalPercentage.toFixed(1)}%) exceeds 100%. Please adjust your allocations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
@@ -809,11 +884,17 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||||
|
<p className="text-sm text-red-700">{errorMessage}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleComplete}
|
onClick={handleComplete}
|
||||||
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg hover:bg-blue-700 transition-colors"
|
disabled={isSaving}
|
||||||
|
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Start Budgeting!
|
{isSaving ? 'Saving...' : 'Start Budgeting!'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentStep('envelopes')}
|
onClick={() => setCurrentStep('envelopes')}
|
||||||
@@ -826,8 +907,17 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" onClick={handleBackdropClick}>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 max-w-2xl w-full">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 max-w-2xl w-full max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={onSkip}
|
||||||
|
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||||
|
aria-label="Close setup wizard"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Progress Indicator */}
|
{/* Progress Indicator */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ export class AuthService {
|
|||||||
|
|
||||||
return result.user;
|
return result.user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Only log non-authentication errors to avoid console spam
|
||||||
|
if (!(error instanceof Error) || !error.message.includes('User already exists')) {
|
||||||
console.error('Sign up error:', error);
|
console.error('Sign up error:', error);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,6 +53,7 @@ export class AuthService {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
|
// Don't log authentication errors to console as they may contain sensitive info
|
||||||
throw new Error(error.error || 'Login failed');
|
throw new Error(error.error || 'Login failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +67,10 @@ export class AuthService {
|
|||||||
|
|
||||||
return result.user;
|
return result.user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Only log non-authentication errors to avoid console spam
|
||||||
|
if (!(error instanceof Error) || !error.message.includes('Invalid credentials')) {
|
||||||
console.error('Sign in error:', error);
|
console.error('Sign in error:', error);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user