Onboarding frontend
This commit is contained in:
parent
17b7ea4aea
commit
5e7032c270
12 changed files with 546 additions and 28 deletions
|
|
@ -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();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Asset set successfully!',
|
||||
'asset' => $asset,
|
||||
]);
|
||||
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 back()->with('success', 'Asset set successfully!');
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
156
resources/js/components/Assets/AssetSetupForm.tsx
Normal file
156
resources/js/components/Assets/AssetSetupForm.tsx
Normal file
|
|
@ -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<AssetFormData>({
|
||||
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 (
|
||||
<div className="w-full">
|
||||
<div className="space-y-4">
|
||||
<ComponentTitle>SET ASSET</ComponentTitle>
|
||||
<p className="text-sm text-red-400/60 font-mono">
|
||||
[SYSTEM] Specify the asset you want to track
|
||||
</p>
|
||||
|
||||
{/* Quick suggestions */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-red-400 font-mono text-xs uppercase tracking-wider">> Quick Select</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{suggestions.map((suggestion) => (
|
||||
<button
|
||||
key={suggestion.symbol}
|
||||
type="button"
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
className="px-3 py-1 bg-black border border-red-500/50 text-red-400 hover:border-red-400 hover:text-red-300 font-mono text-xs uppercase tracking-wider transition-all rounded-none"
|
||||
>
|
||||
{suggestion.symbol}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="symbol" className="text-red-400 font-mono text-xs uppercase tracking-wider">> Asset Symbol</Label>
|
||||
<Input
|
||||
id="symbol"
|
||||
type="text"
|
||||
placeholder="VWCE"
|
||||
value={data.symbol}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<p className="text-xs text-red-400/60 mt-1 font-mono">
|
||||
[REQUIRED] ticker symbol (e.g. VWCE, VTI, SPY)
|
||||
</p>
|
||||
<InputError message={errors.symbol} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="full_name" className="text-red-400 font-mono text-xs uppercase tracking-wider">> Full Name (Optional)</Label>
|
||||
<Input
|
||||
id="full_name"
|
||||
type="text"
|
||||
placeholder="Vanguard FTSE All-World UCITS ETF"
|
||||
value={data.full_name}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<p className="text-xs text-red-400/60 mt-1 font-mono">
|
||||
[OPTIONAL] human-readable asset name
|
||||
</p>
|
||||
<InputError message={errors.full_name} />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={processing || !data.symbol}
|
||||
className="flex-1 bg-red-500 hover:bg-red-500 text-black font-mono text-sm font-bold border-red-500 rounded-none border-2 uppercase tracking-wider transition-all glow-red"
|
||||
>
|
||||
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||
[EXECUTE]
|
||||
</Button>
|
||||
{onCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
className="flex-1 bg-black border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300 font-mono text-sm font-bold rounded-none border-2 uppercase tracking-wider transition-all glow-red"
|
||||
>
|
||||
[ABORT]
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
234
resources/js/components/Onboarding/OnboardingFlow.tsx
Normal file
234
resources/js/components/Onboarding/OnboardingFlow.tsx
Normal file
|
|
@ -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<OnboardingStep[]>([
|
||||
{
|
||||
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 (
|
||||
<AssetSetupForm
|
||||
onSuccess={handleStepComplete}
|
||||
/>
|
||||
);
|
||||
case 'purchases':
|
||||
return (
|
||||
<AddPurchaseForm
|
||||
onSuccess={handleStepComplete}
|
||||
/>
|
||||
);
|
||||
case 'milestones':
|
||||
return (
|
||||
<AddMilestoneForm
|
||||
onSuccess={handleStepComplete}
|
||||
/>
|
||||
);
|
||||
case 'price':
|
||||
return (
|
||||
<UpdatePriceForm
|
||||
onSuccess={handleStepComplete}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-4xl">
|
||||
{/* Terminal-style border with red glow */}
|
||||
<div className="border-2 border-red-500 bg-black shadow-[0_0_20px_rgba(239,68,68,0.3)] p-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-red-400 font-mono text-2xl font-bold uppercase tracking-wider mb-2">
|
||||
[SYSTEM] ONBOARDING SEQUENCE
|
||||
</h1>
|
||||
<p className="text-red-400/60 font-mono text-sm">
|
||||
Initialize your asset tracking system
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
{steps.map((step, index) => (
|
||||
<button
|
||||
key={step.id}
|
||||
onClick={() => handleStepSelect(index)}
|
||||
className={`flex-1 px-4 py-2 font-mono text-xs uppercase tracking-wider border border-red-500/50 transition-all ${
|
||||
index === currentStep
|
||||
? 'bg-red-500 text-black border-red-500'
|
||||
: step.completed
|
||||
? 'bg-red-950/50 text-red-300 border-red-400'
|
||||
: 'bg-black text-red-400/60 hover:text-red-400 hover:border-red-400'
|
||||
} ${index > 0 ? 'ml-2' : ''}`}
|
||||
>
|
||||
{step.completed ? '[✓]' : step.required ? '[REQ]' : '[OPT]'} {step.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-red-400 font-mono text-sm">
|
||||
{steps[currentStep].description}
|
||||
</p>
|
||||
<p className="text-red-400/60 font-mono text-xs mt-1">
|
||||
STEP {currentStep + 1}/{steps.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div className="border border-red-500/30 bg-black/50 p-6">
|
||||
{renderStepContent()}
|
||||
</div>
|
||||
|
||||
{/* Status footer */}
|
||||
<div className="mt-6 pt-4 border-t border-red-500/30">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-red-400/60 font-mono text-xs">
|
||||
[STATUS] {steps.filter(s => s.completed).length}/{steps.length} STEPS COMPLETE
|
||||
</p>
|
||||
<p className="text-red-400/60 font-mono text-xs">
|
||||
{steps.filter(s => s.required && !s.completed).length} REQUIRED REMAINING
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ interface UpdatePriceFormProps {
|
|||
export default function UpdatePriceForm({ currentPrice, className, onSuccess, onCancel }: UpdatePriceFormProps) {
|
||||
const { data, setData, post, processing, errors } = useForm<PriceUpdateFormData>({
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<PurchaseFormData>({
|
||||
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<PurchaseSummary | null>(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
|
|||
<div className="w-full">
|
||||
<div className="space-y-4">
|
||||
<ComponentTitle>ADD PURCHASE</ComponentTitle>
|
||||
{currentHoldings && currentHoldings.total_shares > 0 && (
|
||||
<p className="text-sm text-red-400/60 font-mono">
|
||||
[CURRENT] {currentHoldings.total_shares.toFixed(6)} shares • €{currentHoldings.total_investment.toFixed(2)} invested
|
||||
</p>
|
||||
)}
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="date" className="text-red-400 font-mono text-xs uppercase tracking-wider">> Purchase Date</Label>
|
||||
|
|
|
|||
|
|
@ -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<any>(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 (
|
||||
<>
|
||||
<Head title="Asset Tracker - Setup" />
|
||||
<OnboardingFlow onComplete={handleOnboardingComplete} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="VWCE Tracker" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue