Complete implementation of Phase 1 enhancements including: Backend: - UserProfile model for storing user preferences (timezone, trading style, risk tolerance) - DailyRoutine model for scheduling routines (morning, active_trading, evening) - RoutineExecution model for tracking routine execution history - Notification model for managing all types of notifications - DailyChecklist model for daily task tracking with completion percentage - HabitTracker model for tracking habits and streaks Services: - RoutineService: Handles routine execution with task registry pattern - RoutineScheduler: Async scheduler for automated routine execution - NotificationService: Comprehensive notification creation and delivery system - Support for price alerts, news, routines, reminders, and performance notifications API Endpoints (daily_helper router): - User profile: CRUD operations, get/update preferences - Daily routines: Create, list, execute, track history - Notifications: CRUD, mark read, batch operations - Daily checklists: CRUD, item management, completion tracking - Habits: Create, track, log completions, manage streaks - Dashboard: Summary endpoint for daily helper overview Frontend Components: - UserProfileSetup: Complete user profile configuration with preferences - NotificationCenter: Bell icon with dropdown, notification management - HabitTracker: Habit creation, streak tracking, gamification with fire emojis - DailyChecklistPanel: Checklist management with completion percentage Schemas: - Full Pydantic schemas for request/response validation - Type-safe API contracts Features: - Timezone support for international users - Trading style and risk tolerance preferences - Automated routine execution with task registry - Real-time notifications with priority levels - Habit streaks with motivational badges - Daily checklist with persistent state - Completion percentage tracking - Notes and metadata support All components are production-ready with error handling and user feedback.
353 lines
13 KiB
TypeScript
353 lines
13 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Save, X, AlertCircle } from 'lucide-react';
|
|
|
|
interface UserProfile {
|
|
id?: number;
|
|
email?: string;
|
|
username?: string;
|
|
timezone: string;
|
|
preferred_trading_start: string;
|
|
preferred_trading_end: string;
|
|
risk_tolerance: string;
|
|
trading_style: string;
|
|
daily_target?: number;
|
|
max_loss?: number;
|
|
notifications_enabled: boolean;
|
|
email_reports: boolean;
|
|
sms_enabled: boolean;
|
|
push_notifications: boolean;
|
|
phone_number?: string;
|
|
}
|
|
|
|
interface UserProfileSetupProps {
|
|
onClose: () => void;
|
|
onSaved?: (profile: UserProfile) => void;
|
|
}
|
|
|
|
const UserProfileSetup: React.FC<UserProfileSetupProps> = ({ onClose, onSaved }) => {
|
|
const [profile, setProfile] = useState<UserProfile>({
|
|
timezone: 'UTC',
|
|
preferred_trading_start: '09:00',
|
|
preferred_trading_end: '17:00',
|
|
risk_tolerance: 'moderate',
|
|
trading_style: 'day_trader',
|
|
notifications_enabled: true,
|
|
email_reports: true,
|
|
sms_enabled: false,
|
|
push_notifications: true,
|
|
});
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Load existing profile
|
|
loadProfile();
|
|
}, []);
|
|
|
|
const loadProfile = async () => {
|
|
try {
|
|
const response = await fetch('/api/daily-helper/profile');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setProfile(data);
|
|
}
|
|
} catch (err) {
|
|
// Profile doesn't exist yet, start fresh
|
|
console.log('Starting with default profile');
|
|
}
|
|
};
|
|
|
|
const handleChange = (field: keyof UserProfile, value: any) => {
|
|
setProfile(prev => ({
|
|
...prev,
|
|
[field]: value
|
|
}));
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
setLoading(true);
|
|
setError('');
|
|
setSuccess(false);
|
|
|
|
try {
|
|
const isUpdate = profile.id;
|
|
const method = isUpdate ? 'PUT' : 'POST';
|
|
const endpoint = '/api/daily-helper/profile';
|
|
|
|
const response = await fetch(endpoint, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(profile),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to save profile');
|
|
}
|
|
|
|
const savedProfile = await response.json();
|
|
setProfile(savedProfile);
|
|
setSuccess(true);
|
|
|
|
if (onSaved) {
|
|
onSaved(savedProfile);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
onClose();
|
|
}, 1500);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Error saving profile');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-gray-900 rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-gray-700">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-bold text-white">User Profile Setup</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-200"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-6 p-4 bg-red-900 bg-opacity-30 border border-red-600 rounded-lg flex items-start gap-3">
|
|
<AlertCircle size={20} className="text-red-500 flex-shrink-0 mt-0.5" />
|
|
<p className="text-red-300">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{success && (
|
|
<div className="mb-6 p-4 bg-green-900 bg-opacity-30 border border-green-600 rounded-lg">
|
|
<p className="text-green-300">✓ Profile saved successfully!</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-6">
|
|
{/* Email and Username */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Email
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={profile.email || ''}
|
|
onChange={(e) => handleChange('email', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
|
placeholder="your@email.com"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Username
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={profile.username || ''}
|
|
onChange={(e) => handleChange('username', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
|
placeholder="your_username"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Timezone and Trading Style */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Timezone
|
|
</label>
|
|
<select
|
|
value={profile.timezone}
|
|
onChange={(e) => handleChange('timezone', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
|
|
>
|
|
<option>UTC</option>
|
|
<option>EST</option>
|
|
<option>CST</option>
|
|
<option>MST</option>
|
|
<option>PST</option>
|
|
<option>GMT</option>
|
|
<option>CET</option>
|
|
<option>JST</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Trading Style
|
|
</label>
|
|
<select
|
|
value={profile.trading_style}
|
|
onChange={(e) => handleChange('trading_style', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
|
|
>
|
|
<option value="scalper">Scalper (Minutes)</option>
|
|
<option value="day_trader">Day Trader (Hours)</option>
|
|
<option value="swing_trader">Swing Trader (Days)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Trading Hours */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Trading Start Time (HH:MM)
|
|
</label>
|
|
<input
|
|
type="time"
|
|
value={profile.preferred_trading_start}
|
|
onChange={(e) => handleChange('preferred_trading_start', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Trading End Time (HH:MM)
|
|
</label>
|
|
<input
|
|
type="time"
|
|
value={profile.preferred_trading_end}
|
|
onChange={(e) => handleChange('preferred_trading_end', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Risk Profile */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Risk Tolerance
|
|
</label>
|
|
<select
|
|
value={profile.risk_tolerance}
|
|
onChange={(e) => handleChange('risk_tolerance', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
|
|
>
|
|
<option value="conservative">Conservative (0.5-1%)</option>
|
|
<option value="moderate">Moderate (1-2%)</option>
|
|
<option value="aggressive">Aggressive (2-5%)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Daily Max Loss ($)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={profile.max_loss || ''}
|
|
onChange={(e) => handleChange('max_loss', e.target.value ? parseFloat(e.target.value) : null)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
|
placeholder="500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Daily Target */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Daily Profit Target ($)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={profile.daily_target || ''}
|
|
onChange={(e) => handleChange('daily_target', e.target.value ? parseFloat(e.target.value) : null)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
|
placeholder="1000"
|
|
/>
|
|
</div>
|
|
|
|
{/* Notifications Settings */}
|
|
<div className="border-t border-gray-700 pt-6">
|
|
<h3 className="text-lg font-semibold text-white mb-4">Notification Preferences</h3>
|
|
|
|
<div className="space-y-3">
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={profile.push_notifications}
|
|
onChange={(e) => handleChange('push_notifications', e.target.checked)}
|
|
className="w-4 h-4 rounded bg-gray-800 border-gray-600"
|
|
/>
|
|
<span className="text-gray-300">Push Notifications</span>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={profile.email_reports}
|
|
onChange={(e) => handleChange('email_reports', e.target.checked)}
|
|
className="w-4 h-4 rounded bg-gray-800 border-gray-600"
|
|
/>
|
|
<span className="text-gray-300">Email Reports</span>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={profile.sms_enabled}
|
|
onChange={(e) => handleChange('sms_enabled', e.target.checked)}
|
|
className="w-4 h-4 rounded bg-gray-800 border-gray-600"
|
|
/>
|
|
<span className="text-gray-300">SMS Alerts</span>
|
|
</label>
|
|
|
|
{profile.sms_enabled && (
|
|
<div className="ml-7">
|
|
<input
|
|
type="tel"
|
|
value={profile.phone_number || ''}
|
|
onChange={(e) => handleChange('phone_number', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
|
placeholder="+1 (555) 123-4567"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={profile.notifications_enabled}
|
|
onChange={(e) => handleChange('notifications_enabled', e.target.checked)}
|
|
className="w-4 h-4 rounded bg-gray-800 border-gray-600"
|
|
/>
|
|
<span className="text-gray-300">All Notifications Enabled</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex gap-4 mt-8">
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={loading}
|
|
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 text-white font-medium py-2 px-4 rounded flex items-center justify-center gap-2 transition-colors"
|
|
>
|
|
<Save size={20} />
|
|
{loading ? 'Saving...' : 'Save Profile'}
|
|
</button>
|
|
<button
|
|
onClick={onClose}
|
|
disabled={loading}
|
|
className="flex-1 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 text-white font-medium py-2 px-4 rounded transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UserProfileSetup;
|