44 - Hide price tracking UI and functionality for v0.3.0
This commit is contained in:
parent
195e316da5
commit
bace06d993
6 changed files with 58 additions and 421 deletions
|
|
@ -1,14 +1,12 @@
|
||||||
import AddEntryForm from '@/components/Transactions/AddEntryForm';
|
import AddEntryForm from '@/components/Transactions/AddEntryForm';
|
||||||
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
|
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
|
||||||
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type FormType = 'purchase' | 'milestone' | 'price';
|
type FormType = 'purchase' | 'milestone';
|
||||||
|
|
||||||
interface InlineFormProps {
|
interface InlineFormProps {
|
||||||
type: FormType | null;
|
type: FormType | null;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
priceTrackingEnabled?: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSuccess?: (type: FormType) => void;
|
onSuccess?: (type: FormType) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -17,7 +15,6 @@ interface InlineFormProps {
|
||||||
export default function InlineForm({
|
export default function InlineForm({
|
||||||
type,
|
type,
|
||||||
unit = 'units',
|
unit = 'units',
|
||||||
priceTrackingEnabled = false,
|
|
||||||
onClose,
|
onClose,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
className,
|
className,
|
||||||
|
|
@ -42,17 +39,11 @@ export default function InlineForm({
|
||||||
{type === 'purchase' ? (
|
{type === 'purchase' ? (
|
||||||
<AddEntryForm
|
<AddEntryForm
|
||||||
unit={unit}
|
unit={unit}
|
||||||
priceTrackingEnabled={priceTrackingEnabled}
|
|
||||||
onSuccess={handleSuccess}
|
|
||||||
onCancel={onClose}
|
|
||||||
/>
|
|
||||||
) : type === 'milestone' ? (
|
|
||||||
<AddMilestoneForm
|
|
||||||
onSuccess={handleSuccess}
|
onSuccess={handleSuccess}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UpdatePriceForm
|
<AddMilestoneForm
|
||||||
onSuccess={handleSuccess}
|
onSuccess={handleSuccess}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,6 @@ import type { Milestone } from '@/types/domain';
|
||||||
interface StatsBoxProps {
|
interface StatsBoxProps {
|
||||||
stats: {
|
stats: {
|
||||||
totalShares: number;
|
totalShares: number;
|
||||||
totalInvestment: number;
|
|
||||||
averageCostPerShare: number;
|
|
||||||
currentPrice?: number;
|
|
||||||
currentValue?: number;
|
|
||||||
profitLoss?: number;
|
|
||||||
profitLossPercentage?: number;
|
|
||||||
};
|
};
|
||||||
unit?: string;
|
unit?: string;
|
||||||
milestones?: Milestone[];
|
milestones?: Milestone[];
|
||||||
|
|
@ -21,9 +15,6 @@ interface StatsBoxProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
onAddPurchase?: () => void;
|
onAddPurchase?: () => void;
|
||||||
onAddMilestone?: () => void;
|
onAddMilestone?: () => void;
|
||||||
onUpdatePrice?: () => void;
|
|
||||||
assetSymbol?: string;
|
|
||||||
priceTrackingEnabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StatsBox({
|
export default function StatsBox({
|
||||||
|
|
@ -35,9 +26,6 @@ export default function StatsBox({
|
||||||
className,
|
className,
|
||||||
onAddPurchase,
|
onAddPurchase,
|
||||||
onAddMilestone,
|
onAddMilestone,
|
||||||
onUpdatePrice,
|
|
||||||
assetSymbol,
|
|
||||||
priceTrackingEnabled = false,
|
|
||||||
}: StatsBoxProps) {
|
}: StatsBoxProps) {
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
|
||||||
|
|
@ -46,22 +34,6 @@ export default function StatsBox({
|
||||||
const nextIndex = (selectedMilestoneIndex + 1) % milestones.length;
|
const nextIndex = (selectedMilestoneIndex + 1) % milestones.length;
|
||||||
onMilestoneSelect(nextIndex);
|
onMilestoneSelect(nextIndex);
|
||||||
};
|
};
|
||||||
const formatCurrency = (amount: number) => {
|
|
||||||
return new Intl.NumberFormat('de-DE', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'EUR',
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(amount);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCurrencyDetailed = (amount: number) => {
|
|
||||||
return new Intl.NumberFormat('de-DE', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'EUR',
|
|
||||||
minimumFractionDigits: 4,
|
|
||||||
}).format(amount);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -72,28 +44,20 @@ export default function StatsBox({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="w-full border-4 border-red-500 p-2 bg-black space-y-4 glow-red">
|
<div className="w-full border-4 border-red-500 p-2 bg-black space-y-4 glow-red">
|
||||||
{/* STATS Title and Current Price */}
|
|
||||||
<div className="flex justify-between items-center mb-6 relative">
|
<div className="flex justify-between items-center mb-6 relative">
|
||||||
<ComponentTitle>Stats</ComponentTitle>
|
<ComponentTitle>Stats</ComponentTitle>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 relative">
|
<div className="flex items-center space-x-2 relative">
|
||||||
{priceTrackingEnabled && stats.currentPrice && (
|
|
||||||
<div className="text-red-500 text-sm font-mono tracking-wider">
|
|
||||||
{assetSymbol ?? 'PRICE'}: {formatCurrencyDetailed(stats.currentPrice)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action Dropdown */}
|
{/* Action Dropdown */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||||
className="flex items-center justify-center px-2 py-1 rounded border border-red-500/50 text-red-500 hover:bg-red-800/40 hover:text-red-300 transition-colors text-sm"
|
className="flex items-center justify-center px-2 py-1 rounded border border-red-500/50 text-red-500 hover:bg-red-800/40 hover:text-red-300 transition-colors text-sm"
|
||||||
aria-label="Add actions"
|
aria-label="Add actions"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
|
||||||
{isDropdownOpen && (
|
{isDropdownOpen && (
|
||||||
<div className="absolute top-full right-0 mt-2 bg-black border-2 border-red-500/50 rounded shadow-lg min-w-40 z-10">
|
<div className="absolute top-full right-0 mt-2 bg-black border-2 border-red-500/50 rounded shadow-lg min-w-40 z-10">
|
||||||
{onAddPurchase && (
|
{onAddPurchase && (
|
||||||
|
|
@ -113,22 +77,11 @@ export default function StatsBox({
|
||||||
onAddMilestone();
|
onAddMilestone();
|
||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-4 py-2 text-red-400 hover:bg-red-600/20 hover:text-red-300 transition-colors transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0"
|
className="w-full text-left px-4 py-2 text-red-400 hover:bg-red-600/20 hover:text-red-300 transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0"
|
||||||
>
|
>
|
||||||
ADD MILESTONE
|
ADD MILESTONE
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{priceTrackingEnabled && onUpdatePrice && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
onUpdatePrice();
|
|
||||||
setIsDropdownOpen(false);
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-red-400 hover:bg-red-600/20 hover:text-red-300 transition-colors transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0"
|
|
||||||
>
|
|
||||||
UPDATE PRICE
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -146,78 +99,45 @@ export default function StatsBox({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Milestone Table */}
|
{/* Milestone Table */}
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<div className="text-red-500 underline font-bold mb-3 font-mono">MILESTONES</div>
|
<div className="text-red-500 underline font-bold mb-3 font-mono">MILESTONES</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-sm font-mono">
|
<table className="w-full text-sm font-mono">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left text-red-500 text-xs py-2">DESCRIPTION</th>
|
<th className="text-left text-red-500 text-xs py-2">DESCRIPTION</th>
|
||||||
<th className="text-right text-red-500 text-xs py-2">{unit.toUpperCase()}</th>
|
<th className="text-right text-red-500 text-xs py-2">{unit.toUpperCase()}</th>
|
||||||
{priceTrackingEnabled && <th className="text-right text-red-500 text-xs py-2 pr-4">SWR 3%</th>}
|
</tr>
|
||||||
{priceTrackingEnabled && <th className="text-right text-red-500 text-xs py-2">SWR 4%</th>}
|
</thead>
|
||||||
</tr>
|
<tbody>
|
||||||
</thead>
|
<tr className="text-red-500 font-bold">
|
||||||
<tbody>
|
<td className="py-1 pr-4">CURRENT</td>
|
||||||
{/* Current position row */}
|
|
||||||
<tr className="text-red-500 font-bold">
|
|
||||||
<td className="py-1 pr-4">CURRENT</td>
|
|
||||||
<td className="text-right py-1 pr-4">
|
|
||||||
{Math.floor(stats.totalShares).toLocaleString()}
|
|
||||||
</td>
|
|
||||||
{priceTrackingEnabled && (
|
|
||||||
<td className="text-right py-1 pr-4">
|
|
||||||
{stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.03) : 'N/A'}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{priceTrackingEnabled && (
|
|
||||||
<td className="text-right py-1">
|
<td className="text-right py-1">
|
||||||
{stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.04) : 'N/A'}
|
{Math.floor(stats.totalShares).toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
)}
|
</tr>
|
||||||
</tr>
|
|
||||||
|
|
||||||
{/* Render milestones after current */}
|
{milestones.map((milestone, index) => (
|
||||||
{milestones.map((milestone, index) => {
|
|
||||||
const swr3 = stats.currentPrice ? milestone.target * stats.currentPrice * 0.03 : 0;
|
|
||||||
const swr4 = stats.currentPrice ? milestone.target * stats.currentPrice * 0.04 : 0;
|
|
||||||
|
|
||||||
const isSelectedMilestone = index === selectedMilestoneIndex;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
isSelectedMilestone
|
index === selectedMilestoneIndex
|
||||||
? "bg-red-500 text-black"
|
? "bg-red-500 text-black"
|
||||||
: "text-red-500 font-bold"
|
: "text-red-500 font-bold"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<td className="py-1 pr-4">
|
<td className="py-1 pr-4">{milestone.description}</td>
|
||||||
{milestone.description}
|
<td className="text-right py-1">
|
||||||
</td>
|
|
||||||
<td className="text-right py-1 pr-4">
|
|
||||||
{Math.floor(milestone.target).toLocaleString()}
|
{Math.floor(milestone.target).toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
{priceTrackingEnabled && (
|
|
||||||
<td className="text-right py-1 pr-4">
|
|
||||||
{stats.currentPrice ? formatCurrency(swr3) : 'N/A'}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{priceTrackingEnabled && (
|
|
||||||
<td className="text-right py-1">
|
|
||||||
{stats.currentPrice ? formatCurrency(swr4) : 'N/A'}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
))}
|
||||||
})}
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,26 +7,15 @@ import { FormEventHandler, useState } from 'react';
|
||||||
import ComponentTitle from '@/components/ui/ComponentTitle';
|
import ComponentTitle from '@/components/ui/ComponentTitle';
|
||||||
|
|
||||||
interface CreateTrackerStepProps {
|
interface CreateTrackerStepProps {
|
||||||
onSuccess: (priceTrackingEnabled: boolean) => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps) {
|
export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps) {
|
||||||
const [label, setLabel] = useState('');
|
const [label, setLabel] = useState('');
|
||||||
const [unit, setUnit] = useState('');
|
const [unit, setUnit] = useState('');
|
||||||
const [priceTracking, setPriceTracking] = useState(false);
|
|
||||||
const [symbol, setSymbol] = useState('');
|
|
||||||
const [fullName, setFullName] = useState('');
|
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const togglePriceTracking = (enabled: boolean) => {
|
|
||||||
setPriceTracking(enabled);
|
|
||||||
if (!enabled) {
|
|
||||||
setSymbol('');
|
|
||||||
setFullName('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit: FormEventHandler = async (e) => {
|
const submit: FormEventHandler = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
|
|
@ -43,14 +32,12 @@ export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps)
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
label,
|
label,
|
||||||
unit,
|
unit,
|
||||||
price_tracking_enabled: priceTracking ? 1 : 0,
|
price_tracking_enabled: 0,
|
||||||
symbol: priceTracking ? symbol : null,
|
|
||||||
full_name: priceTracking ? fullName : null,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok || response.status === 201 || response.status === 409) {
|
if (response.ok || response.status === 201 || response.status === 409) {
|
||||||
onSuccess(priceTracking);
|
onSuccess();
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.errors) {
|
if (data.errors) {
|
||||||
|
|
@ -111,59 +98,9 @@ export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps)
|
||||||
<InputError message={errors.unit} />
|
<InputError message={errors.unit} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-red-500/30 p-4 space-y-3">
|
|
||||||
<label className="flex items-center gap-3 cursor-pointer group">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={priceTracking}
|
|
||||||
onChange={(e) => togglePriceTracking(e.target.checked)}
|
|
||||||
className="w-4 h-4 accent-red-500"
|
|
||||||
/>
|
|
||||||
<span className="text-red-400 font-mono text-sm uppercase tracking-wider group-hover:text-red-300">
|
|
||||||
Enable price tracking
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<p className="text-red-400/60 font-mono text-xs">
|
|
||||||
Track market price, portfolio value, and P&L. Requires an asset symbol.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{priceTracking && (
|
|
||||||
<div className="space-y-3 pt-2">
|
|
||||||
<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={symbol}
|
|
||||||
onChange={(e) => setSymbol(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"
|
|
||||||
/>
|
|
||||||
<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={fullName}
|
|
||||||
onChange={(e) => setFullName(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"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.full_name} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={processing || !label || !unit || (priceTracking && !symbol)}
|
disabled={processing || !label || !unit}
|
||||||
className="w-full 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"
|
className="w-full 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" />}
|
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,8 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import AssetSetupForm from '@/components/Assets/AssetSetupForm';
|
|
||||||
import AddEntryForm from '@/components/Transactions/AddEntryForm';
|
import AddEntryForm from '@/components/Transactions/AddEntryForm';
|
||||||
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
|
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
|
||||||
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
|
|
||||||
import CreateTrackerStep from '@/components/Onboarding/CreateTrackerStep';
|
import CreateTrackerStep from '@/components/Onboarding/CreateTrackerStep';
|
||||||
|
|
||||||
function PriceTrackingStep({ onEnable, onSkip }: { onEnable: () => void; onSkip?: () => void }) {
|
|
||||||
const [enabled, setEnabled] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<label className="flex items-center gap-3 cursor-pointer group">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={enabled}
|
|
||||||
onChange={e => setEnabled(e.target.checked)}
|
|
||||||
className="w-4 h-4 accent-red-500"
|
|
||||||
/>
|
|
||||||
<span className="text-red-400 font-mono text-sm uppercase tracking-wider group-hover:text-red-300">
|
|
||||||
Enable price tracking (optional)
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<p className="text-red-400/60 font-mono text-xs">
|
|
||||||
Track the current market price of your asset to see portfolio value and P&L. You can enable this later in settings.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{enabled && (
|
|
||||||
<div className="border border-red-500/30 p-4">
|
|
||||||
<UpdatePriceForm onSuccess={onEnable} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!enabled && (
|
|
||||||
<button
|
|
||||||
onClick={onSkip ?? (() => {})}
|
|
||||||
className="w-full py-2 font-mono text-xs uppercase tracking-wider border border-red-500/50 text-red-400 hover:bg-red-950/30 hover:text-red-300 transition-colors"
|
|
||||||
>
|
|
||||||
Skip and finish
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnboardingStep {
|
interface OnboardingStep {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -51,13 +11,7 @@ interface OnboardingStep {
|
||||||
required: boolean;
|
required: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASSET_STEPS: OnboardingStep[] = [
|
const STEPS: OnboardingStep[] = [
|
||||||
{ id: 'entries', title: 'ADD ENTRIES', description: 'Enter your current holdings', completed: false, required: true },
|
|
||||||
{ id: 'milestones', title: 'SET MILESTONES', description: 'Define your goals', completed: false, required: true },
|
|
||||||
{ id: 'price', title: 'CURRENT PRICE', description: 'Set current asset price (optional)', completed: false, required: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
const SIMPLE_STEPS: OnboardingStep[] = [
|
|
||||||
{ id: 'entries', title: 'STARTING AMOUNT', description: 'Enter your starting amount', completed: false, required: true },
|
{ id: 'entries', title: 'STARTING AMOUNT', description: 'Enter your starting amount', completed: false, required: true },
|
||||||
{ id: 'milestones', title: 'SET MILESTONES', description: 'Define your goals', completed: false, required: true },
|
{ id: 'milestones', title: 'SET MILESTONES', description: 'Define your goals', completed: false, required: true },
|
||||||
];
|
];
|
||||||
|
|
@ -68,7 +22,6 @@ interface OnboardingFlowProps {
|
||||||
|
|
||||||
export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
const [trackerCreated, setTrackerCreated] = useState(false);
|
const [trackerCreated, setTrackerCreated] = useState(false);
|
||||||
const [priceTracking, setPriceTracking] = useState(false);
|
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [steps, setSteps] = useState<OnboardingStep[]>([]);
|
const [steps, setSteps] = useState<OnboardingStep[]>([]);
|
||||||
|
|
||||||
|
|
@ -78,7 +31,6 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data?.tracker) {
|
if (data?.tracker) {
|
||||||
setPriceTracking(data.tracker.price_tracking_enabled ?? false);
|
|
||||||
setTrackerCreated(true);
|
setTrackerCreated(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -87,22 +39,19 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
|
|
||||||
const checkOnboardingStatus = useCallback(async (currentSteps: OnboardingStep[]) => {
|
const checkOnboardingStatus = useCallback(async (currentSteps: OnboardingStep[]) => {
|
||||||
try {
|
try {
|
||||||
const [entriesData, milestonesData, priceData] = await Promise.all([
|
const [entriesData, milestonesData] = await Promise.all([
|
||||||
fetch('/entries/summary').then(r => r.json()),
|
fetch('/entries/summary').then(r => r.json()),
|
||||||
fetch('/milestones').then(r => r.json()),
|
fetch('/milestones').then(r => r.json()),
|
||||||
fetch('/pricing/current').then(r => r.json()),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const hasEntries = entriesData.total_quantity > 0;
|
const hasEntries = entriesData.total_quantity > 0;
|
||||||
const hasMilestones = milestonesData.length > 0;
|
const hasMilestones = milestonesData.length > 0;
|
||||||
const hasPrice = !!priceData.current_price;
|
|
||||||
|
|
||||||
const freshSteps = currentSteps.map(step => ({
|
const freshSteps = currentSteps.map(step => ({
|
||||||
...step,
|
...step,
|
||||||
completed:
|
completed:
|
||||||
(step.id === 'entries' && hasEntries) ||
|
(step.id === 'entries' && hasEntries) ||
|
||||||
(step.id === 'milestones' && hasMilestones) ||
|
(step.id === 'milestones' && hasMilestones),
|
||||||
(step.id === 'price' && hasPrice),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setSteps(freshSteps);
|
setSteps(freshSteps);
|
||||||
|
|
@ -122,14 +71,12 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!trackerCreated) return;
|
if (!trackerCreated) return;
|
||||||
|
|
||||||
const initialSteps = priceTracking ? ASSET_STEPS : SIMPLE_STEPS;
|
setSteps(STEPS);
|
||||||
setSteps(initialSteps);
|
|
||||||
setCurrentStep(0);
|
setCurrentStep(0);
|
||||||
checkOnboardingStatus(initialSteps);
|
checkOnboardingStatus(STEPS);
|
||||||
}, [trackerCreated, priceTracking, checkOnboardingStatus]);
|
}, [trackerCreated, checkOnboardingStatus]);
|
||||||
|
|
||||||
const handleTrackerCreated = (withPriceTracking: boolean) => {
|
const handleTrackerCreated = () => {
|
||||||
setPriceTracking(withPriceTracking);
|
|
||||||
setTrackerCreated(true);
|
setTrackerCreated(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -151,11 +98,9 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
|
|
||||||
switch (step.id) {
|
switch (step.id) {
|
||||||
case 'entries':
|
case 'entries':
|
||||||
return <AddEntryForm onSuccess={handleStepComplete} priceTrackingEnabled={priceTracking} />;
|
return <AddEntryForm onSuccess={handleStepComplete} />;
|
||||||
case 'milestones':
|
case 'milestones':
|
||||||
return <AddMilestoneForm onSuccess={handleStepComplete} />;
|
return <AddMilestoneForm onSuccess={handleStepComplete} />;
|
||||||
case 'price':
|
|
||||||
return <PriceTrackingStep onEnable={handleStepComplete} onSkip={onComplete ?? (() => {})} />;
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -194,7 +139,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
: 'bg-black text-red-400/60 hover:text-red-400 hover:border-red-400'
|
: 'bg-black text-red-400/60 hover:text-red-400 hover:border-red-400'
|
||||||
} ${index > 0 ? 'ml-2' : ''}`}
|
} ${index > 0 ? 'ml-2' : ''}`}
|
||||||
>
|
>
|
||||||
{step.completed ? '[✓]' : step.required ? '[REQ]' : '[OPT]'} {step.title}
|
{step.completed ? '[✓]' : '[REQ]'} {step.title}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -219,7 +164,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
[STATUS] {steps.filter(s => s.completed).length}/{steps.length} STEPS COMPLETE
|
[STATUS] {steps.filter(s => s.completed).length}/{steps.length} STEPS COMPLETE
|
||||||
</p>
|
</p>
|
||||||
<p className="text-red-400/60 font-mono text-xs">
|
<p className="text-red-400/60 font-mono text-xs">
|
||||||
{steps.filter(s => s.required && !s.completed).length} REQUIRED REMAINING
|
{steps.filter(s => !s.completed).length} REQUIRED REMAINING
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,30 +11,23 @@ import ComponentTitle from '@/components/ui/ComponentTitle';
|
||||||
interface EntryFormData {
|
interface EntryFormData {
|
||||||
date: string;
|
date: string;
|
||||||
quantity: string;
|
quantity: string;
|
||||||
unit_price: string;
|
|
||||||
total_cost: string;
|
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddEntryFormProps {
|
interface AddEntryFormProps {
|
||||||
unit?: string;
|
unit?: string;
|
||||||
priceTrackingEnabled?: boolean;
|
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntrySummary {
|
interface EntrySummary {
|
||||||
total_quantity: number;
|
total_quantity: number;
|
||||||
total_cost: number;
|
|
||||||
average_cost_per_unit: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = false, onSuccess, onCancel }: AddEntryFormProps) {
|
export default function AddEntryForm({ unit = 'units', onSuccess, onCancel }: AddEntryFormProps) {
|
||||||
const { data, setData, post, processing, errors, reset } = useForm<EntryFormData>({
|
const { data, setData, post, processing, errors, reset } = useForm<EntryFormData>({
|
||||||
date: todayISO(),
|
date: todayISO(),
|
||||||
quantity: '',
|
quantity: '',
|
||||||
unit_price: '',
|
|
||||||
total_cost: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentHoldings, setCurrentHoldings] = useState<EntrySummary | null>(null);
|
const [currentHoldings, setCurrentHoldings] = useState<EntrySummary | null>(null);
|
||||||
|
|
@ -55,18 +48,6 @@ export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = fa
|
||||||
fetchSummary();
|
fetchSummary();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Auto-calculate total cost when quantity or unit_price changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (data.quantity && data.unit_price) {
|
|
||||||
const quantity = parseFloat(data.quantity);
|
|
||||||
const unitPrice = parseFloat(data.unit_price);
|
|
||||||
|
|
||||||
if (!isNaN(quantity) && !isNaN(unitPrice)) {
|
|
||||||
setData('total_cost', (quantity * unitPrice).toFixed(2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [data.quantity, data.unit_price, setData]);
|
|
||||||
|
|
||||||
const submit: FormEventHandler = (e) => {
|
const submit: FormEventHandler = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -86,7 +67,6 @@ export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = fa
|
||||||
{currentHoldings && currentHoldings.total_quantity > 0 && (
|
{currentHoldings && currentHoldings.total_quantity > 0 && (
|
||||||
<p className="text-sm text-red-400/60 font-mono">
|
<p className="text-sm text-red-400/60 font-mono">
|
||||||
[CURRENT] {currentHoldings.total_quantity.toFixed(6)} {unit}
|
[CURRENT] {currentHoldings.total_quantity.toFixed(6)} {unit}
|
||||||
{priceTrackingEnabled && ` • €${currentHoldings.total_cost.toFixed(2)} spent`}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<form onSubmit={submit} className="space-y-4">
|
<form onSubmit={submit} className="space-y-4">
|
||||||
|
|
@ -120,41 +100,6 @@ export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = fa
|
||||||
<InputError message={errors.quantity} />
|
<InputError message={errors.quantity} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{priceTrackingEnabled && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="unit_price" className="text-red-400 font-mono text-xs uppercase tracking-wider">> Price per {unit} (€)</Label>
|
|
||||||
<Input
|
|
||||||
id="unit_price"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
placeholder="123.45"
|
|
||||||
value={data.unit_price}
|
|
||||||
onChange={(e) => setData('unit_price', 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 placeholder:text-red-400/40 transition-all glow-red"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.unit_price} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="total_cost" className="text-red-400 font-mono text-xs uppercase tracking-wider">> Total Cost (€)</Label>
|
|
||||||
<Input
|
|
||||||
id="total_cost"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
placeholder="1234.56"
|
|
||||||
value={data.total_cost}
|
|
||||||
onChange={(e) => setData('total_cost', 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 placeholder:text-red-400/40 transition-all glow-red"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-red-400/60 mt-1 font-mono">[AUTO-CALC] quantity × price</p>
|
|
||||||
<InputError message={errors.total_cost} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-2">
|
<div className="flex gap-3 pt-2">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -6,47 +6,28 @@ import OnboardingFlow from '@/components/Onboarding/OnboardingFlow';
|
||||||
import TerminalSpinner from '@/components/ui/TerminalSpinner';
|
import TerminalSpinner from '@/components/ui/TerminalSpinner';
|
||||||
import { Head } from '@inertiajs/react';
|
import { Head } from '@inertiajs/react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import type { Milestone, Tracker, TrackerAsset } from '@/types/domain';
|
import type { Milestone, Tracker } from '@/types/domain';
|
||||||
|
|
||||||
interface PurchaseSummary {
|
interface EntrySummary {
|
||||||
total_shares: number;
|
total_shares: number;
|
||||||
total_investment: number;
|
|
||||||
average_cost_per_share: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CurrentPrice {
|
|
||||||
current_price: number | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [purchaseData, setPurchaseData] = useState<PurchaseSummary>({
|
const [totalShares, setTotalShares] = useState(0);
|
||||||
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 [milestones, setMilestones] = useState<Milestone[]>([]);
|
||||||
const [selectedMilestoneIndex, setSelectedMilestoneIndex] = useState(0);
|
const [selectedMilestoneIndex, setSelectedMilestoneIndex] = useState(0);
|
||||||
const [showProgressBar, setShowProgressBar] = useState(false);
|
const [showProgressBar, setShowProgressBar] = useState(false);
|
||||||
const [showStatsBox, setShowStatsBox] = useState(false);
|
const [showStatsBox, setShowStatsBox] = useState(false);
|
||||||
const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | 'price' | null>(null);
|
const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [needsOnboarding, setNeedsOnboarding] = useState(false);
|
const [needsOnboarding, setNeedsOnboarding] = useState(false);
|
||||||
const [tracker, setTracker] = useState<Tracker | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const [entriesResponse, priceResponse, milestonesResponse, trackerResponse] = await Promise.all([
|
const [entriesResponse, milestonesResponse, trackerResponse] = await Promise.all([
|
||||||
fetch('/entries/summary'),
|
fetch('/entries/summary'),
|
||||||
fetch('/pricing/current'),
|
|
||||||
fetch('/milestones'),
|
fetch('/milestones'),
|
||||||
fetch('/tracker'),
|
fetch('/tracker'),
|
||||||
]);
|
]);
|
||||||
|
|
@ -56,19 +37,10 @@ export default function Dashboard() {
|
||||||
|
|
||||||
if (entriesResponse.ok) {
|
if (entriesResponse.ok) {
|
||||||
const entries = await entriesResponse.json();
|
const entries = await entriesResponse.json();
|
||||||
setPurchaseData({
|
setTotalShares(entries.total_quantity);
|
||||||
total_shares: entries.total_quantity,
|
|
||||||
total_investment: entries.total_cost,
|
|
||||||
average_cost_per_share: entries.average_cost_per_unit,
|
|
||||||
});
|
|
||||||
totalQuantity = entries.total_quantity;
|
totalQuantity = entries.total_quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priceResponse.ok) {
|
|
||||||
const price = await priceResponse.json();
|
|
||||||
setPriceData(price);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (milestonesResponse.ok) {
|
if (milestonesResponse.ok) {
|
||||||
const milestonesData = await milestonesResponse.json();
|
const milestonesData = await milestonesResponse.json();
|
||||||
setMilestones(milestonesData);
|
setMilestones(milestonesData);
|
||||||
|
|
@ -78,8 +50,6 @@ export default function Dashboard() {
|
||||||
if (trackerResponse.ok) {
|
if (trackerResponse.ok) {
|
||||||
const { tracker: trackerData } = await trackerResponse.json();
|
const { tracker: trackerData } = await trackerResponse.json();
|
||||||
setTracker(trackerData ?? null);
|
setTracker(trackerData ?? null);
|
||||||
setCurrentAsset(trackerData?.asset ?? null);
|
|
||||||
setPriceTrackingEnabled(trackerData?.price_tracking_enabled ?? false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
|
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
|
||||||
|
|
@ -93,31 +63,24 @@ export default function Dashboard() {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Refresh data after successful entry
|
|
||||||
const handlePurchaseSuccess = async () => {
|
const handlePurchaseSuccess = async () => {
|
||||||
try {
|
try {
|
||||||
const entriesResponse = await fetch('/entries/summary');
|
const entriesResponse = await fetch('/entries/summary');
|
||||||
if (entriesResponse.ok) {
|
if (entriesResponse.ok) {
|
||||||
const entries = await entriesResponse.json();
|
const entries = await entriesResponse.json();
|
||||||
setPurchaseData({
|
setTotalShares(entries.total_quantity);
|
||||||
total_shares: entries.total_quantity,
|
|
||||||
total_investment: entries.total_cost,
|
|
||||||
average_cost_per_share: entries.average_cost_per_unit,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh entry data:', error);
|
console.error('Failed to refresh entry data:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refresh milestones after successful creation
|
|
||||||
const handleMilestoneSuccess = async () => {
|
const handleMilestoneSuccess = async () => {
|
||||||
try {
|
try {
|
||||||
const milestonesResponse = await fetch('/milestones');
|
const milestonesResponse = await fetch('/milestones');
|
||||||
if (milestonesResponse.ok) {
|
if (milestonesResponse.ok) {
|
||||||
const milestonesData = await milestonesResponse.json();
|
const milestonesData = await milestonesResponse.json();
|
||||||
setMilestones(milestonesData);
|
setMilestones(milestonesData);
|
||||||
// Reset to first milestone when milestones change
|
|
||||||
setSelectedMilestoneIndex(0);
|
setSelectedMilestoneIndex(0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -125,53 +88,13 @@ export default function Dashboard() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle milestone selection
|
|
||||||
const handleMilestoneSelect = (index: number) => {
|
const handleMilestoneSelect = (index: number) => {
|
||||||
setSelectedMilestoneIndex(index);
|
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 handleOnboardingComplete = useCallback(async () => {
|
||||||
const [entriesResponse, priceResponse, milestonesResponse, trackerResponse] = await Promise.all([
|
const [entriesResponse, milestonesResponse, trackerResponse] = await Promise.all([
|
||||||
fetch('/entries/summary'),
|
fetch('/entries/summary'),
|
||||||
fetch('/pricing/current'),
|
|
||||||
fetch('/milestones'),
|
fetch('/milestones'),
|
||||||
fetch('/tracker'),
|
fetch('/tracker'),
|
||||||
]);
|
]);
|
||||||
|
|
@ -181,19 +104,10 @@ export default function Dashboard() {
|
||||||
|
|
||||||
if (entriesResponse.ok) {
|
if (entriesResponse.ok) {
|
||||||
const entries = await entriesResponse.json();
|
const entries = await entriesResponse.json();
|
||||||
setPurchaseData({
|
setTotalShares(entries.total_quantity);
|
||||||
total_shares: entries.total_quantity,
|
|
||||||
total_investment: entries.total_cost,
|
|
||||||
average_cost_per_share: entries.average_cost_per_unit,
|
|
||||||
});
|
|
||||||
totalQuantity = entries.total_quantity;
|
totalQuantity = entries.total_quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priceResponse.ok) {
|
|
||||||
const price = await priceResponse.json();
|
|
||||||
setPriceData(price);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (milestonesResponse.ok) {
|
if (milestonesResponse.ok) {
|
||||||
const milestonesData = await milestonesResponse.json();
|
const milestonesData = await milestonesResponse.json();
|
||||||
setMilestones(milestonesData);
|
setMilestones(milestonesData);
|
||||||
|
|
@ -201,10 +115,8 @@ export default function Dashboard() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackerResponse.ok) {
|
if (trackerResponse.ok) {
|
||||||
const trackerData = await trackerResponse.json();
|
const { tracker: trackerData } = await trackerResponse.json();
|
||||||
setTracker(trackerData);
|
setTracker(trackerData ?? null);
|
||||||
setCurrentAsset(trackerData?.asset ?? null);
|
|
||||||
setPriceTrackingEnabled(trackerData?.price_tracking_enabled ?? false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
|
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
|
||||||
|
|
@ -219,22 +131,19 @@ export default function Dashboard() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle handlers with cascading behavior
|
|
||||||
const handleLedClick = () => {
|
const handleLedClick = () => {
|
||||||
const newShowProgressBar = !showProgressBar;
|
const newShowProgressBar = !showProgressBar;
|
||||||
setShowProgressBar(newShowProgressBar);
|
setShowProgressBar(newShowProgressBar);
|
||||||
if (!newShowProgressBar) {
|
if (!newShowProgressBar) {
|
||||||
// If hiding progress bar, also hide stats box
|
|
||||||
setShowStatsBox(false);
|
setShowStatsBox(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProgressClick = () => {
|
const handleProgressClick = () => {
|
||||||
setShowStatsBox(!showStatsBox);
|
setShowStatsBox(!showStatsBox);
|
||||||
setActiveForm(null)
|
setActiveForm(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show onboarding if needed
|
|
||||||
if (needsOnboarding) {
|
if (needsOnboarding) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -248,55 +157,45 @@ export default function Dashboard() {
|
||||||
<>
|
<>
|
||||||
<Head title="incr" />
|
<Head title="incr" />
|
||||||
|
|
||||||
{/* Stacked Layout */}
|
|
||||||
<div className="min-h-screen bg-black">
|
<div className="min-h-screen bg-black">
|
||||||
<div className="w-full max-w-4xl mx-auto px-4">
|
<div className="w-full max-w-4xl mx-auto px-4">
|
||||||
{/* Box 1: LED Number Display - Fixed position from top */}
|
|
||||||
<div className="pt-32">
|
<div className="pt-32">
|
||||||
<LedDisplay
|
<LedDisplay
|
||||||
value={purchaseData.total_shares}
|
value={totalShares}
|
||||||
unit={tracker?.unit}
|
unit={tracker?.unit}
|
||||||
onClick={handleLedClick}
|
onClick={handleLedClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Box 2: Progress Bar (toggleable) */}
|
|
||||||
<div style={{ display: showProgressBar ? 'block' : 'none' }}>
|
<div style={{ display: showProgressBar ? 'block' : 'none' }}>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
currentQuantity={purchaseData.total_shares}
|
currentQuantity={totalShares}
|
||||||
milestones={milestones}
|
milestones={milestones}
|
||||||
selectedMilestoneIndex={selectedMilestoneIndex}
|
selectedMilestoneIndex={selectedMilestoneIndex}
|
||||||
onClick={handleProgressClick}
|
onClick={handleProgressClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Box 3: Stats Box (toggleable) */}
|
|
||||||
<div style={{ display: showStatsBox ? 'block' : 'none' }}>
|
<div style={{ display: showStatsBox ? 'block' : 'none' }}>
|
||||||
<StatsBox
|
<StatsBox
|
||||||
stats={statsData}
|
stats={{ totalShares }}
|
||||||
unit={tracker?.unit}
|
unit={tracker?.unit}
|
||||||
milestones={milestones}
|
milestones={milestones}
|
||||||
selectedMilestoneIndex={selectedMilestoneIndex}
|
selectedMilestoneIndex={selectedMilestoneIndex}
|
||||||
onMilestoneSelect={handleMilestoneSelect}
|
onMilestoneSelect={handleMilestoneSelect}
|
||||||
onAddPurchase={() => setActiveForm('purchase')}
|
onAddPurchase={() => setActiveForm('purchase')}
|
||||||
onAddMilestone={() => setActiveForm('milestone')}
|
onAddMilestone={() => setActiveForm('milestone')}
|
||||||
onUpdatePrice={() => setActiveForm('price')}
|
|
||||||
assetSymbol={currentAsset?.symbol}
|
|
||||||
priceTrackingEnabled={priceTrackingEnabled}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Box 4: Forms (only when active form is set) */}
|
|
||||||
<div style={{ display: activeForm && showProgressBar && showStatsBox ? 'block' : 'none' }}>
|
<div style={{ display: activeForm && showProgressBar && showStatsBox ? 'block' : 'none' }}>
|
||||||
<InlineForm
|
<InlineForm
|
||||||
type={activeForm}
|
type={activeForm}
|
||||||
unit={tracker?.unit}
|
unit={tracker?.unit}
|
||||||
priceTrackingEnabled={priceTrackingEnabled}
|
|
||||||
onClose={() => setActiveForm(null)}
|
onClose={() => setActiveForm(null)}
|
||||||
onSuccess={(type) => {
|
onSuccess={(type) => {
|
||||||
if (type === 'purchase') handlePurchaseSuccess();
|
if (type === 'purchase') handlePurchaseSuccess();
|
||||||
else if (type === 'milestone') handleMilestoneSuccess();
|
else if (type === 'milestone') handleMilestoneSuccess();
|
||||||
else if (type === 'price') handlePriceSuccess();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue