incr/resources/js/pages/dashboard.tsx

307 lines
11 KiB
TypeScript

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 TerminalSpinner from '@/components/ui/TerminalSpinner';
import { Head } from '@inertiajs/react';
import { useCallback, useEffect, useState } from 'react';
import type { Milestone, Tracker, TrackerAsset } from '@/types/domain';
interface PurchaseSummary {
total_shares: number;
total_investment: number;
average_cost_per_share: number;
}
interface CurrentPrice {
current_price: number | null;
}
export default function Dashboard() {
const [purchaseData, setPurchaseData] = useState<PurchaseSummary>({
total_shares: 0,
total_investment: 0,
average_cost_per_share: 0,
});
const [priceData, setPriceData] = useState<CurrentPrice>({
current_price: null,
});
const [milestones, setMilestones] = useState<Milestone[]>([]);
const [selectedMilestoneIndex, setSelectedMilestoneIndex] = useState(0);
const [showProgressBar, setShowProgressBar] = useState(false);
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 [tracker, setTracker] = useState<Tracker | null>(null);
const [currentAsset, setCurrentAsset] = useState<TrackerAsset | null>(null);
const [priceTrackingEnabled, setPriceTrackingEnabled] = useState(false);
// Fetch entry summary, current price, milestones, and check onboarding
useEffect(() => {
const fetchData = async () => {
try {
const [entriesResponse, priceResponse, milestonesResponse, trackerResponse] = await Promise.all([
fetch('/entries/summary'),
fetch('/pricing/current'),
fetch('/milestones'),
fetch('/tracker'),
]);
let totalQuantity = 0;
let milestonesCount = 0;
if (entriesResponse.ok) {
const entries = await entriesResponse.json();
setPurchaseData({
total_shares: entries.total_quantity,
total_investment: entries.total_cost,
average_cost_per_share: entries.average_cost_per_unit,
});
totalQuantity = entries.total_quantity;
}
if (priceResponse.ok) {
const price = await priceResponse.json();
setPriceData(price);
}
if (milestonesResponse.ok) {
const milestonesData = await milestonesResponse.json();
setMilestones(milestonesData);
milestonesCount = milestonesData.length;
}
if (trackerResponse.ok) {
const trackerData = await trackerResponse.json();
setTracker(trackerData);
setCurrentAsset(trackerData?.asset ?? null);
setPriceTrackingEnabled(trackerData?.price_tracking_enabled ?? false);
}
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
// Refresh data after successful entry
const handlePurchaseSuccess = async () => {
try {
const entriesResponse = await fetch('/entries/summary');
if (entriesResponse.ok) {
const entries = await entriesResponse.json();
setPurchaseData({
total_shares: entries.total_quantity,
total_investment: entries.total_cost,
average_cost_per_share: entries.average_cost_per_unit,
});
}
} catch (error) {
console.error('Failed to refresh entry data:', error);
}
};
// Refresh milestones after successful creation
const handleMilestoneSuccess = async () => {
try {
const milestonesResponse = await fetch('/milestones');
if (milestonesResponse.ok) {
const milestonesData = await milestonesResponse.json();
setMilestones(milestonesData);
// Reset to first milestone when milestones change
setSelectedMilestoneIndex(0);
}
} catch (error) {
console.error('Failed to refresh milestone data:', error);
}
};
// Handle milestone selection
const handleMilestoneSelect = (index: number) => {
setSelectedMilestoneIndex(index);
};
// 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);
}
};
// Calculate portfolio stats
const currentValue = priceData.current_price
? purchaseData.total_shares * priceData.current_price
: undefined;
const profitLoss = currentValue
? currentValue - purchaseData.total_investment
: undefined;
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,
};
// Handle onboarding completion — must be before any early returns (Rules of Hooks)
const handleOnboardingComplete = useCallback(async () => {
const [entriesResponse, priceResponse, milestonesResponse, trackerResponse] = await Promise.all([
fetch('/entries/summary'),
fetch('/pricing/current'),
fetch('/milestones'),
fetch('/tracker'),
]);
let totalQuantity = 0;
let milestonesCount = 0;
if (entriesResponse.ok) {
const entries = await entriesResponse.json();
setPurchaseData({
total_shares: entries.total_quantity,
total_investment: entries.total_cost,
average_cost_per_share: entries.average_cost_per_unit,
});
totalQuantity = entries.total_quantity;
}
if (priceResponse.ok) {
const price = await priceResponse.json();
setPriceData(price);
}
if (milestonesResponse.ok) {
const milestonesData = await milestonesResponse.json();
setMilestones(milestonesData);
milestonesCount = milestonesData.length;
}
if (trackerResponse.ok) {
const trackerData = await trackerResponse.json();
setTracker(trackerData);
setCurrentAsset(trackerData?.asset ?? null);
setPriceTrackingEnabled(trackerData?.price_tracking_enabled ?? false);
}
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
}, []);
if (loading) {
return (
<>
<Head title="Dashboard" />
<TerminalSpinner fullScreen />
</>
);
}
// 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);
setActiveForm(null)
};
// Show onboarding if needed
if (needsOnboarding) {
return (
<>
<Head title="Asset Tracker - Setup" />
<OnboardingFlow onComplete={handleOnboardingComplete} />
</>
);
}
return (
<>
<Head title="incr" />
{/* Stacked Layout */}
<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">
<LedDisplay
value={purchaseData.total_shares}
unit={tracker?.unit}
onClick={handleLedClick}
/>
</div>
{/* Box 2: Progress Bar (toggleable) */}
<div style={{ display: showProgressBar ? 'block' : 'none' }}>
<ProgressBar
currentQuantity={purchaseData.total_shares}
milestones={milestones}
selectedMilestoneIndex={selectedMilestoneIndex}
onClick={handleProgressClick}
/>
</div>
{/* Box 3: Stats Box (toggleable) */}
<div style={{ display: showStatsBox ? 'block' : 'none' }}>
<StatsBox
stats={statsData}
unit={tracker?.unit}
milestones={milestones}
selectedMilestoneIndex={selectedMilestoneIndex}
onMilestoneSelect={handleMilestoneSelect}
onAddPurchase={() => setActiveForm('purchase')}
onAddMilestone={() => setActiveForm('milestone')}
onUpdatePrice={() => setActiveForm('price')}
assetSymbol={currentAsset?.symbol}
priceTrackingEnabled={priceTrackingEnabled}
/>
</div>
{/* Box 4: Forms (only when active form is set) */}
<div style={{ display: activeForm && showProgressBar && showStatsBox ? 'block' : 'none' }}>
<InlineForm
type={activeForm}
unit={tracker?.unit}
priceTrackingEnabled={priceTrackingEnabled}
onClose={() => setActiveForm(null)}
onSuccess={(type) => {
if (type === 'purchase') handlePurchaseSuccess();
else if (type === 'milestone') handleMilestoneSuccess();
else if (type === 'price') handlePriceSuccess();
}}
/>
</div>
</div>
</div>
</>
);
}