2025-07-12 19:59:22 +02:00
|
|
|
import LedDisplay from '@/components/Display/LedDisplay';
|
|
|
|
|
import InlineForm from '@/components/Display/InlineForm';
|
|
|
|
|
import ProgressBar from '@/components/Display/ProgressBar';
|
|
|
|
|
import StatsBox from '@/components/Display/StatsBox';
|
2025-08-01 00:56:26 +02:00
|
|
|
import OnboardingFlow from '@/components/Onboarding/OnboardingFlow';
|
2025-07-10 15:24:15 +02:00
|
|
|
import { Head } from '@inertiajs/react';
|
2025-07-10 18:04:58 +02:00
|
|
|
import { useEffect, useState } from 'react';
|
2025-07-10 15:24:15 +02:00
|
|
|
|
2025-07-10 18:04:58 +02:00
|
|
|
interface PurchaseSummary {
|
|
|
|
|
total_shares: number;
|
|
|
|
|
total_investment: number;
|
|
|
|
|
average_cost_per_share: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface CurrentPrice {
|
|
|
|
|
current_price: number | null;
|
|
|
|
|
}
|
2025-07-10 15:24:15 +02:00
|
|
|
|
2025-07-13 00:18:45 +02:00
|
|
|
interface Milestone {
|
|
|
|
|
target: number;
|
|
|
|
|
description: string;
|
|
|
|
|
created_at: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-10 15:24:15 +02:00
|
|
|
export default function Dashboard() {
|
2025-07-10 18:04:58 +02:00
|
|
|
const [purchaseData, setPurchaseData] = useState<PurchaseSummary>({
|
|
|
|
|
total_shares: 0,
|
|
|
|
|
total_investment: 0,
|
|
|
|
|
average_cost_per_share: 0,
|
|
|
|
|
});
|
2025-07-13 02:10:52 +02:00
|
|
|
|
2025-07-10 18:04:58 +02:00
|
|
|
const [priceData, setPriceData] = useState<CurrentPrice>({
|
|
|
|
|
current_price: null,
|
|
|
|
|
});
|
2025-07-13 02:10:52 +02:00
|
|
|
|
2025-07-13 00:18:45 +02:00
|
|
|
const [milestones, setMilestones] = useState<Milestone[]>([]);
|
2025-07-13 01:13:36 +02:00
|
|
|
const [selectedMilestoneIndex, setSelectedMilestoneIndex] = useState(0);
|
2025-07-12 19:59:22 +02:00
|
|
|
const [showProgressBar, setShowProgressBar] = useState(false);
|
|
|
|
|
const [showStatsBox, setShowStatsBox] = useState(false);
|
2025-07-13 01:07:16 +02:00
|
|
|
const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | 'price' | null>(null);
|
2025-07-10 18:04:58 +02:00
|
|
|
const [loading, setLoading] = useState(true);
|
2025-08-01 00:56:26 +02:00
|
|
|
const [needsOnboarding, setNeedsOnboarding] = useState(false);
|
|
|
|
|
const [currentAsset, setCurrentAsset] = useState<any>(null);
|
2025-07-10 18:04:58 +02:00
|
|
|
|
2025-08-01 00:56:26 +02:00
|
|
|
// Fetch purchase summary, current price, milestones, and check onboarding
|
2025-07-10 18:04:58 +02:00
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
try {
|
2025-08-01 00:56:26 +02:00
|
|
|
const [purchaseResponse, priceResponse, milestonesResponse, assetResponse] = await Promise.all([
|
2025-07-10 18:04:58 +02:00
|
|
|
fetch('/purchases/summary'),
|
|
|
|
|
fetch('/pricing/current'),
|
2025-07-13 00:18:45 +02:00
|
|
|
fetch('/milestones'),
|
2025-08-01 00:56:26 +02:00
|
|
|
fetch('/assets/current'),
|
2025-07-10 18:04:58 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (purchaseResponse.ok) {
|
|
|
|
|
const purchases = await purchaseResponse.json();
|
|
|
|
|
setPurchaseData(purchases);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (priceResponse.ok) {
|
|
|
|
|
const price = await priceResponse.json();
|
|
|
|
|
setPriceData(price);
|
|
|
|
|
}
|
2025-07-13 00:18:45 +02:00
|
|
|
|
|
|
|
|
if (milestonesResponse.ok) {
|
|
|
|
|
const milestonesData = await milestonesResponse.json();
|
|
|
|
|
setMilestones(milestonesData);
|
|
|
|
|
}
|
2025-08-01 00:56:26 +02:00
|
|
|
|
|
|
|
|
if (assetResponse.ok) {
|
|
|
|
|
const assetData = await assetResponse.json();
|
|
|
|
|
setCurrentAsset(assetData.asset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if onboarding is needed after all data is loaded
|
|
|
|
|
await checkOnboardingStatus();
|
2025-07-10 18:04:58 +02:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to fetch data:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fetchData();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-08-01 00:56:26 +02:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-10 18:04:58 +02:00
|
|
|
// Refresh data after successful purchase
|
|
|
|
|
const handlePurchaseSuccess = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const purchaseResponse = await fetch('/purchases/summary');
|
|
|
|
|
if (purchaseResponse.ok) {
|
|
|
|
|
const purchases = await purchaseResponse.json();
|
|
|
|
|
setPurchaseData(purchases);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to refresh purchase data:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-13 00:18:45 +02:00
|
|
|
// Refresh milestones after successful creation
|
|
|
|
|
const handleMilestoneSuccess = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const milestonesResponse = await fetch('/milestones');
|
|
|
|
|
if (milestonesResponse.ok) {
|
|
|
|
|
const milestonesData = await milestonesResponse.json();
|
|
|
|
|
setMilestones(milestonesData);
|
2025-07-13 01:13:36 +02:00
|
|
|
// Reset to first milestone when milestones change
|
|
|
|
|
setSelectedMilestoneIndex(0);
|
2025-07-13 00:18:45 +02:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to refresh milestone data:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-13 01:13:36 +02:00
|
|
|
// Handle milestone selection
|
|
|
|
|
const handleMilestoneSelect = (index: number) => {
|
|
|
|
|
setSelectedMilestoneIndex(index);
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-13 01:07:16 +02:00
|
|
|
// Refresh price data after successful update
|
|
|
|
|
const handlePriceSuccess = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const priceResponse = await fetch('/pricing/current');
|
|
|
|
|
if (priceResponse.ok) {
|
|
|
|
|
const price = await priceResponse.json();
|
|
|
|
|
setPriceData(price);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to refresh price data:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-10 18:04:58 +02:00
|
|
|
|
|
|
|
|
// Calculate portfolio stats
|
2025-07-13 02:10:52 +02:00
|
|
|
const currentValue = priceData.current_price
|
|
|
|
|
? purchaseData.total_shares * priceData.current_price
|
2025-07-10 18:04:58 +02:00
|
|
|
: undefined;
|
2025-07-13 02:10:52 +02:00
|
|
|
|
|
|
|
|
const profitLoss = currentValue
|
|
|
|
|
? currentValue - purchaseData.total_investment
|
2025-07-10 18:04:58 +02:00
|
|
|
: undefined;
|
2025-07-13 02:10:52 +02:00
|
|
|
|
2025-07-10 18:04:58 +02:00
|
|
|
const profitLossPercentage = profitLoss && purchaseData.total_investment > 0
|
|
|
|
|
? (profitLoss / purchaseData.total_investment) * 100
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const statsData = {
|
|
|
|
|
totalShares: purchaseData.total_shares,
|
|
|
|
|
totalInvestment: purchaseData.total_investment,
|
|
|
|
|
averageCostPerShare: purchaseData.average_cost_per_share,
|
|
|
|
|
currentPrice: priceData.current_price || undefined,
|
|
|
|
|
currentValue,
|
|
|
|
|
profitLoss,
|
|
|
|
|
profitLossPercentage,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Head title="Dashboard" />
|
|
|
|
|
<div className="min-h-screen bg-black flex items-center justify-center">
|
|
|
|
|
<div className="text-red-500 font-mono text-lg animate-pulse">
|
|
|
|
|
LOADING...
|
2025-07-10 15:24:15 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-07-10 18:04:58 +02:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-12 19:59:22 +02:00
|
|
|
// Toggle handlers with cascading behavior
|
|
|
|
|
const handleLedClick = () => {
|
|
|
|
|
const newShowProgressBar = !showProgressBar;
|
|
|
|
|
setShowProgressBar(newShowProgressBar);
|
|
|
|
|
if (!newShowProgressBar) {
|
|
|
|
|
// If hiding progress bar, also hide stats box
|
|
|
|
|
setShowStatsBox(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleProgressClick = () => {
|
|
|
|
|
setShowStatsBox(!showStatsBox);
|
2025-07-13 02:10:52 +02:00
|
|
|
setActiveForm(null)
|
2025-07-12 19:59:22 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-01 00:56:26 +02:00
|
|
|
// 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} />
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-10 18:04:58 +02:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Head title="VWCE Tracker" />
|
2025-07-13 02:10:52 +02:00
|
|
|
|
2025-07-12 19:59:22 +02:00
|
|
|
{/* Stacked Layout */}
|
2025-07-13 00:03:34 +02:00
|
|
|
<div className="min-h-screen bg-black">
|
|
|
|
|
<div className="w-full max-w-4xl mx-auto px-4">
|
|
|
|
|
{/* Box 1: LED Number Display - Fixed position from top */}
|
|
|
|
|
<div className="pt-32">
|
2025-07-13 02:10:52 +02:00
|
|
|
<LedDisplay
|
2025-07-13 00:03:34 +02:00
|
|
|
value={purchaseData.total_shares}
|
|
|
|
|
onClick={handleLedClick}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-07-13 02:10:52 +02:00
|
|
|
|
2025-07-12 19:59:22 +02:00
|
|
|
{/* Box 2: Progress Bar (toggleable) */}
|
2025-07-13 02:10:52 +02:00
|
|
|
<div style={{ display: showProgressBar ? 'block' : 'none' }}>
|
|
|
|
|
<ProgressBar
|
2025-07-13 00:30:19 +02:00
|
|
|
currentShares={purchaseData.total_shares}
|
|
|
|
|
milestones={milestones}
|
2025-07-13 01:13:36 +02:00
|
|
|
selectedMilestoneIndex={selectedMilestoneIndex}
|
2025-07-12 19:59:22 +02:00
|
|
|
onClick={handleProgressClick}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-07-13 02:10:52 +02:00
|
|
|
|
2025-07-12 19:59:22 +02:00
|
|
|
{/* Box 3: Stats Box (toggleable) */}
|
2025-07-13 02:10:52 +02:00
|
|
|
<div style={{ display: showStatsBox ? 'block' : 'none' }}>
|
|
|
|
|
<StatsBox
|
2025-07-12 19:59:22 +02:00
|
|
|
stats={statsData}
|
2025-07-13 00:18:45 +02:00
|
|
|
milestones={milestones}
|
2025-07-13 01:13:36 +02:00
|
|
|
selectedMilestoneIndex={selectedMilestoneIndex}
|
|
|
|
|
onMilestoneSelect={handleMilestoneSelect}
|
2025-07-12 19:59:22 +02:00
|
|
|
onAddPurchase={() => setActiveForm('purchase')}
|
|
|
|
|
onAddMilestone={() => setActiveForm('milestone')}
|
2025-07-13 01:07:16 +02:00
|
|
|
onUpdatePrice={() => setActiveForm('price')}
|
2025-07-12 19:59:22 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-07-13 02:10:52 +02:00
|
|
|
|
2025-07-12 19:59:22 +02:00
|
|
|
{/* Box 4: Forms (only when active form is set) */}
|
2025-07-13 02:10:52 +02:00
|
|
|
<div style={{ display: activeForm && showProgressBar && showStatsBox ? 'block' : 'none' }}>
|
2025-07-12 19:59:22 +02:00
|
|
|
<InlineForm
|
|
|
|
|
type={activeForm}
|
|
|
|
|
onClose={() => setActiveForm(null)}
|
|
|
|
|
onPurchaseSuccess={handlePurchaseSuccess}
|
2025-07-13 00:18:45 +02:00
|
|
|
onMilestoneSuccess={handleMilestoneSuccess}
|
2025-07-13 01:07:16 +02:00
|
|
|
onPriceSuccess={handlePriceSuccess}
|
2025-07-12 19:59:22 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-07-10 15:24:15 +02:00
|
|
|
</div>
|
2025-07-10 18:04:58 +02:00
|
|
|
</>
|
2025-07-10 15:24:15 +02:00
|
|
|
);
|
|
|
|
|
}
|