diff --git a/app/Http/Controllers/AssetController.php b/app/Http/Controllers/AssetController.php index 3880b94..16adc1b 100644 --- a/app/Http/Controllers/AssetController.php +++ b/app/Http/Controllers/AssetController.php @@ -18,15 +18,16 @@ public function index(): JsonResponse public function current(): JsonResponse { - $user = Auth::user(); - $asset = $user->asset; + // Get the first/default user (since no auth) + $user = \App\Models\User::first(); + $asset = $user ? $user->asset : null; return response()->json([ 'asset' => $asset, ]); } - public function setCurrent(Request $request): JsonResponse + public function setCurrent(Request $request) { $validated = $request->validate([ 'symbol' => 'required|string|max:10', @@ -38,14 +39,22 @@ public function setCurrent(Request $request): JsonResponse $validated['full_name'] ?? null ); - $user = Auth::user(); - $user->update(['asset_id' => $asset->id]); + // Get or create the first/default user (since no auth) + $user = \App\Models\User::first(); + + if (!$user) { + // Create a default user if none exists + $user = \App\Models\User::create([ + 'name' => 'Default User', + 'email' => 'user@example.com', + 'password' => 'password', // This will be hashed automatically + 'asset_id' => $asset->id, + ]); + } else { + $user->update(['asset_id' => $asset->id]); + } - return response()->json([ - 'success' => true, - 'message' => 'Asset set successfully!', - 'asset' => $asset, - ]); + return back()->with('success', 'Asset set successfully!'); } public function store(Request $request): JsonResponse diff --git a/app/Http/Controllers/Pricing/PricingController.php b/app/Http/Controllers/Pricing/PricingController.php index 8dba55f..2a484be 100644 --- a/app/Http/Controllers/Pricing/PricingController.php +++ b/app/Http/Controllers/Pricing/PricingController.php @@ -11,8 +11,9 @@ class PricingController extends Controller { public function current(): JsonResponse { - $user = auth()->user(); - $assetId = $user->asset_id; + // Get the first/default user (since no auth) + $user = \App\Models\User::first(); + $assetId = $user ? $user->asset_id : null; $price = AssetPrice::current($assetId); @@ -28,9 +29,10 @@ public function update(Request $request) 'price' => 'required|numeric|min:0.0001', ]); - $user = auth()->user(); + // Get the first/default user (since no auth) + $user = \App\Models\User::first(); - if (!$user->asset_id) { + if (!$user || !$user->asset_id) { return back()->withErrors(['asset' => 'Please set an asset first.']); } @@ -41,8 +43,9 @@ public function update(Request $request) public function history(Request $request): JsonResponse { - $user = auth()->user(); - $assetId = $user->asset_id; + // Get the first/default user (since no auth) + $user = \App\Models\User::first(); + $assetId = $user ? $user->asset_id : null; $limit = $request->get('limit', 30); $history = AssetPrice::history($assetId, $limit); @@ -52,8 +55,9 @@ public function history(Request $request): JsonResponse public function forDate(Request $request, string $date): JsonResponse { - $user = auth()->user(); - $assetId = $user->asset_id; + // Get the first/default user (since no auth) + $user = \App\Models\User::first(); + $assetId = $user ? $user->asset_id : null; $price = AssetPrice::forDate($date, $assetId); diff --git a/app/Http/Controllers/Transactions/PurchaseController.php b/app/Http/Controllers/Transactions/PurchaseController.php index b0bd1a1..648e304 100644 --- a/app/Http/Controllers/Transactions/PurchaseController.php +++ b/app/Http/Controllers/Transactions/PurchaseController.php @@ -17,7 +17,7 @@ public function index(): JsonResponse return response()->json($purchases); } - public function store(Request $request): JsonResponse + public function store(Request $request) { $validated = $request->validate([ 'date' => 'required|date|before_or_equal:today', @@ -41,10 +41,7 @@ public function store(Request $request): JsonResponse 'total_cost' => $validated['total_cost'], ]); - return response()->json([ - 'success' => true, - 'message' => 'Purchase added successfully!', - ]); + return back()->with('success', 'Purchase added successfully!'); } public function summary() diff --git a/database/migrations/2025_07_31_222423_create_assets_table.php b/database/migrations/0001_01_01_000000_create_assets_table.php similarity index 100% rename from database/migrations/2025_07_31_222423_create_assets_table.php rename to database/migrations/0001_01_01_000000_create_assets_table.php diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000001_create_users_table.php similarity index 100% rename from database/migrations/0001_01_01_000000_create_users_table.php rename to database/migrations/0001_01_01_000001_create_users_table.php diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000002_create_cache_table.php similarity index 100% rename from database/migrations/0001_01_01_000001_create_cache_table.php rename to database/migrations/0001_01_01_000002_create_cache_table.php diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000003_create_jobs_table.php similarity index 100% rename from database/migrations/0001_01_01_000002_create_jobs_table.php rename to database/migrations/0001_01_01_000003_create_jobs_table.php diff --git a/resources/js/components/Assets/AssetSetupForm.tsx b/resources/js/components/Assets/AssetSetupForm.tsx new file mode 100644 index 0000000..315d72b --- /dev/null +++ b/resources/js/components/Assets/AssetSetupForm.tsx @@ -0,0 +1,156 @@ +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import InputError from '@/components/InputError'; +import { useForm } from '@inertiajs/react'; +import { LoaderCircle } from 'lucide-react'; +import { FormEventHandler, useState, useEffect } from 'react'; +import ComponentTitle from '@/components/ui/ComponentTitle'; + +interface AssetFormData { + symbol: string; + full_name: string; + [key: string]: string; +} + +interface AssetSetupFormProps { + onSuccess?: () => void; + onCancel?: () => void; +} + +export default function AssetSetupForm({ onSuccess, onCancel }: AssetSetupFormProps) { + const { data, setData, post, processing, errors } = useForm({ + symbol: '', + full_name: '', + }); + + // Load existing asset data on mount + useEffect(() => { + const fetchCurrentAsset = async () => { + try { + const response = await fetch('/assets/current'); + if (response.ok) { + const assetData = await response.json(); + if (assetData.asset) { + setData({ + symbol: assetData.asset.symbol || '', + full_name: assetData.asset.full_name || '', + }); + } + } + } catch (error) { + console.error('Failed to fetch current asset:', error); + } + }; + + fetchCurrentAsset(); + }, []); + + const [suggestions] = useState([ + { symbol: 'VWCE', full_name: 'Vanguard FTSE All-World UCITS ETF' }, + { symbol: 'VTI', full_name: 'Vanguard Total Stock Market ETF' }, + { symbol: 'SPY', full_name: 'SPDR S&P 500 ETF Trust' }, + { symbol: 'QQQ', full_name: 'Invesco QQQ Trust' }, + { symbol: 'IWDA', full_name: 'iShares Core MSCI World UCITS ETF' }, + ]); + + const submit: FormEventHandler = (e) => { + e.preventDefault(); + + post(route('assets.set-current'), { + onSuccess: () => { + if (onSuccess) onSuccess(); + }, + }); + }; + + const handleSuggestionClick = (suggestion: { symbol: string; full_name: string }) => { + setData({ + symbol: suggestion.symbol, + full_name: suggestion.full_name, + }); + }; + + return ( +
+
+ SET ASSET +

+ [SYSTEM] Specify the asset you want to track +

+ + {/* Quick suggestions */} +
+ +
+ {suggestions.map((suggestion) => ( + + ))} +
+
+ +
+
+ + setData('symbol', e.target.value.toUpperCase())} + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none focus:shadow-[0_0_10px_rgba(239,68,68,0.5)] placeholder:text-red-400/40 transition-all" + /> +

+ [REQUIRED] ticker symbol (e.g. VWCE, VTI, SPY) +

+ +
+ +
+ + setData('full_name', e.target.value)} + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none focus:shadow-[0_0_10px_rgba(239,68,68,0.5)] placeholder:text-red-400/40 transition-all" + /> +

+ [OPTIONAL] human-readable asset name +

+ +
+ +
+ + {onCancel && ( + + )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Onboarding/OnboardingFlow.tsx b/resources/js/components/Onboarding/OnboardingFlow.tsx new file mode 100644 index 0000000..e80f825 --- /dev/null +++ b/resources/js/components/Onboarding/OnboardingFlow.tsx @@ -0,0 +1,234 @@ +import { useState, useEffect } from 'react'; +import AssetSetupForm from '@/components/Assets/AssetSetupForm'; +import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; +import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; +import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm'; + +interface OnboardingStep { + id: string; + title: string; + description: string; + completed: boolean; + required: boolean; +} + +interface OnboardingFlowProps { + onComplete?: () => void; +} + +export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { + const [currentStep, setCurrentStep] = useState(0); + const [steps, setSteps] = useState([ + { + id: 'asset', + title: 'SET ASSET', + description: 'Choose the asset you want to track', + completed: false, + required: true, + }, + { + id: 'purchases', + title: 'ADD PURCHASES', + description: 'Enter your current holdings', + completed: false, + required: true, + }, + { + id: 'milestones', + title: 'SET MILESTONES', + description: 'Define your investment goals', + completed: false, + required: true, + }, + { + id: 'price', + title: 'CURRENT PRICE', + description: 'Set current asset price', + completed: false, + required: true, + }, + ]); + + // Check onboarding status on mount + useEffect(() => { + checkOnboardingStatus(); + }, []); + + const checkOnboardingStatus = async () => { + try { + // Check asset + const assetResponse = await fetch('/assets/current'); + const assetData = await assetResponse.json(); + const hasAsset = !!assetData.asset; + + // Check purchases + const purchaseResponse = await fetch('/purchases/summary'); + const purchaseData = await purchaseResponse.json(); + const hasPurchases = purchaseData.total_shares > 0; + + // Check milestones + const milestonesResponse = await fetch('/milestones'); + const milestonesData = await milestonesResponse.json(); + const hasMilestones = milestonesData.length > 0; + + // Check current price + const priceResponse = await fetch('/pricing/current'); + const priceData = await priceResponse.json(); + const hasPrice = !!priceData.current_price; + + setSteps(prev => prev.map(step => ({ + ...step, + completed: + (step.id === 'asset' && hasAsset) || + (step.id === 'purchases' && hasPurchases) || + (step.id === 'milestones' && hasMilestones) || + (step.id === 'price' && hasPrice) + }))); + + // Find first incomplete required step + const firstIncompleteStep = steps.findIndex(step => + step.required && !step.completed + ); + + if (firstIncompleteStep !== -1) { + setCurrentStep(firstIncompleteStep); + } else { + // All required steps complete, check if we should call onComplete + const allRequiredComplete = steps.filter(s => s.required).every(s => s.completed); + if (allRequiredComplete && onComplete) { + onComplete(); + } + } + } catch (error) { + console.error('Failed to check onboarding status:', error); + } + }; + + const handleStepComplete = async () => { + // Mark current step as completed + setSteps(prev => prev.map((step, index) => + index === currentStep ? { ...step, completed: true } : step + )); + + // Refresh onboarding status + await checkOnboardingStatus(); + + // Move to next incomplete step or complete onboarding + const nextIncompleteStep = steps.findIndex((step, index) => + index > currentStep && step.required && !step.completed + ); + + if (nextIncompleteStep !== -1) { + setCurrentStep(nextIncompleteStep); + } else { + // All required steps complete + const allRequiredComplete = steps.filter(s => s.required).every(s => s.completed); + if (allRequiredComplete && onComplete) { + onComplete(); + } + } + }; + + const handleStepSelect = (stepIndex: number) => { + setCurrentStep(stepIndex); + }; + + const renderStepContent = () => { + const step = steps[currentStep]; + + switch (step.id) { + case 'asset': + return ( + + ); + case 'purchases': + return ( + + ); + case 'milestones': + return ( + + ); + case 'price': + return ( + + ); + default: + return null; + } + }; + + return ( +
+
+ {/* Terminal-style border with red glow */} +
+ {/* Header */} +
+

+ [SYSTEM] ONBOARDING SEQUENCE +

+

+ Initialize your asset tracking system +

+
+ + {/* Progress indicator */} +
+
+ {steps.map((step, index) => ( + + ))} +
+ +
+

+ {steps[currentStep].description} +

+

+ STEP {currentStep + 1}/{steps.length} +

+
+
+ + {/* Step content */} +
+ {renderStepContent()} +
+ + {/* Status footer */} +
+
+

+ [STATUS] {steps.filter(s => s.completed).length}/{steps.length} STEPS COMPLETE +

+

+ {steps.filter(s => s.required && !s.completed).length} REQUIRED REMAINING +

+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Pricing/UpdatePriceForm.tsx b/resources/js/components/Pricing/UpdatePriceForm.tsx index fe99e3b..8a99c2b 100644 --- a/resources/js/components/Pricing/UpdatePriceForm.tsx +++ b/resources/js/components/Pricing/UpdatePriceForm.tsx @@ -23,7 +23,7 @@ interface UpdatePriceFormProps { export default function UpdatePriceForm({ currentPrice, className, onSuccess, onCancel }: UpdatePriceFormProps) { const { data, setData, post, processing, errors } = useForm({ date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format - price: currentPrice?.toString() || '', + price: currentPrice?.toString() || '100.00', }); const submit: FormEventHandler = (e) => { @@ -33,8 +33,13 @@ export default function UpdatePriceForm({ currentPrice, className, onSuccess, on onSuccess: () => { // Keep the date, reset only price if needed // User might want to update same day multiple times - if (onSuccess) onSuccess(); + if (onSuccess) { + onSuccess(); + } }, + onError: (errors) => { + console.error('Price update failed:', errors); + } }); }; diff --git a/resources/js/components/Transactions/AddPurchaseForm.tsx b/resources/js/components/Transactions/AddPurchaseForm.tsx index c360177..94c254b 100644 --- a/resources/js/components/Transactions/AddPurchaseForm.tsx +++ b/resources/js/components/Transactions/AddPurchaseForm.tsx @@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label'; import InputError from '@/components/InputError'; import { useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; -import { FormEventHandler, useEffect } from 'react'; +import { FormEventHandler, useEffect, useState } from 'react'; import ComponentTitle from '@/components/ui/ComponentTitle'; interface PurchaseFormData { @@ -20,6 +20,12 @@ interface AddPurchaseFormProps { onCancel?: () => void; } +interface PurchaseSummary { + total_shares: number; + total_investment: number; + average_cost_per_share: number; +} + export default function AddPurchaseForm({ onSuccess, onCancel }: AddPurchaseFormProps) { const { data, setData, post, processing, errors, reset } = useForm({ date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format @@ -28,6 +34,25 @@ export default function AddPurchaseForm({ onSuccess, onCancel }: AddPurchaseForm total_cost: '', }); + const [currentHoldings, setCurrentHoldings] = useState(null); + + // Load existing holdings data on mount + useEffect(() => { + const fetchCurrentHoldings = async () => { + try { + const response = await fetch('/purchases/summary'); + if (response.ok) { + const summary = await response.json(); + setCurrentHoldings(summary); + } + } catch (error) { + console.error('Failed to fetch current holdings:', error); + } + }; + + fetchCurrentHoldings(); + }, []); + // Auto-calculate total cost when shares or price changes useEffect(() => { if (data.shares && data.price_per_share) { @@ -59,6 +84,11 @@ export default function AddPurchaseForm({ onSuccess, onCancel }: AddPurchaseForm
ADD PURCHASE + {currentHoldings && currentHoldings.total_shares > 0 && ( +

+ [CURRENT] {currentHoldings.total_shares.toFixed(6)} shares • €{currentHoldings.total_investment.toFixed(2)} invested +

+ )}
diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 23c7449..deb9369 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -2,6 +2,7 @@ import LedDisplay from '@/components/Display/LedDisplay'; import InlineForm from '@/components/Display/InlineForm'; import ProgressBar from '@/components/Display/ProgressBar'; import StatsBox from '@/components/Display/StatsBox'; +import OnboardingFlow from '@/components/Onboarding/OnboardingFlow'; import { Head } from '@inertiajs/react'; import { useEffect, useState } from 'react'; @@ -38,15 +39,18 @@ export default function Dashboard() { const [showStatsBox, setShowStatsBox] = useState(false); const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | 'price' | null>(null); const [loading, setLoading] = useState(true); + const [needsOnboarding, setNeedsOnboarding] = useState(false); + const [currentAsset, setCurrentAsset] = useState(null); - // Fetch purchase summary, current price, and milestones + // Fetch purchase summary, current price, milestones, and check onboarding useEffect(() => { const fetchData = async () => { try { - const [purchaseResponse, priceResponse, milestonesResponse] = await Promise.all([ + const [purchaseResponse, priceResponse, milestonesResponse, assetResponse] = await Promise.all([ fetch('/purchases/summary'), fetch('/pricing/current'), fetch('/milestones'), + fetch('/assets/current'), ]); if (purchaseResponse.ok) { @@ -63,6 +67,14 @@ export default function Dashboard() { const milestonesData = await milestonesResponse.json(); setMilestones(milestonesData); } + + if (assetResponse.ok) { + const assetData = await assetResponse.json(); + setCurrentAsset(assetData.asset); + } + + // Check if onboarding is needed after all data is loaded + await checkOnboardingStatus(); } catch (error) { console.error('Failed to fetch data:', error); } finally { @@ -73,6 +85,33 @@ export default function Dashboard() { fetchData(); }, []); + // Check if user needs onboarding + const checkOnboardingStatus = async () => { + try { + const [assetResponse, purchaseResponse, milestonesResponse] = await Promise.all([ + fetch('/assets/current'), + fetch('/purchases/summary'), + fetch('/milestones'), + ]); + + const assetData = await assetResponse.json(); + const purchaseData = await purchaseResponse.json(); + const milestonesData = await milestonesResponse.json(); + + const hasAsset = !!assetData.asset; + const hasPurchases = purchaseData.total_shares > 0; + const hasMilestones = milestonesData.length > 0; + + // User needs onboarding if any required step is missing + const needsOnboarding = !hasAsset || !hasPurchases || !hasMilestones; + setNeedsOnboarding(needsOnboarding); + } catch (error) { + console.error('Failed to check onboarding status:', error); + // If we can't check, assume onboarding is needed + setNeedsOnboarding(true); + } + }; + // Refresh data after successful purchase const handlePurchaseSuccess = async () => { try { @@ -171,6 +210,50 @@ export default function Dashboard() { setActiveForm(null) }; + // Handle onboarding completion + const handleOnboardingComplete = async () => { + // Refresh all data and check onboarding status + await checkOnboardingStatus(); + + // Refresh individual data sets + const [purchaseResponse, priceResponse, milestonesResponse, assetResponse] = await Promise.all([ + fetch('/purchases/summary'), + fetch('/pricing/current'), + fetch('/milestones'), + fetch('/assets/current'), + ]); + + if (purchaseResponse.ok) { + const purchases = await purchaseResponse.json(); + setPurchaseData(purchases); + } + + if (priceResponse.ok) { + const price = await priceResponse.json(); + setPriceData(price); + } + + if (milestonesResponse.ok) { + const milestonesData = await milestonesResponse.json(); + setMilestones(milestonesData); + } + + if (assetResponse.ok) { + const assetData = await assetResponse.json(); + setCurrentAsset(assetData.asset); + } + }; + + // Show onboarding if needed + if (needsOnboarding) { + return ( + <> + + + + ); + } + return ( <>