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 AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
|
||||
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type FormType = 'purchase' | 'milestone' | 'price';
|
||||
type FormType = 'purchase' | 'milestone';
|
||||
|
||||
interface InlineFormProps {
|
||||
type: FormType | null;
|
||||
unit?: string;
|
||||
priceTrackingEnabled?: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: (type: FormType) => void;
|
||||
className?: string;
|
||||
|
|
@ -17,7 +15,6 @@ interface InlineFormProps {
|
|||
export default function InlineForm({
|
||||
type,
|
||||
unit = 'units',
|
||||
priceTrackingEnabled = false,
|
||||
onClose,
|
||||
onSuccess,
|
||||
className,
|
||||
|
|
@ -42,17 +39,11 @@ export default function InlineForm({
|
|||
{type === 'purchase' ? (
|
||||
<AddEntryForm
|
||||
unit={unit}
|
||||
priceTrackingEnabled={priceTrackingEnabled}
|
||||
onSuccess={handleSuccess}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
) : type === 'milestone' ? (
|
||||
<AddMilestoneForm
|
||||
onSuccess={handleSuccess}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
) : (
|
||||
<UpdatePriceForm
|
||||
<AddMilestoneForm
|
||||
onSuccess={handleSuccess}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,6 @@ import type { Milestone } from '@/types/domain';
|
|||
interface StatsBoxProps {
|
||||
stats: {
|
||||
totalShares: number;
|
||||
totalInvestment: number;
|
||||
averageCostPerShare: number;
|
||||
currentPrice?: number;
|
||||
currentValue?: number;
|
||||
profitLoss?: number;
|
||||
profitLossPercentage?: number;
|
||||
};
|
||||
unit?: string;
|
||||
milestones?: Milestone[];
|
||||
|
|
@ -21,9 +15,6 @@ interface StatsBoxProps {
|
|||
className?: string;
|
||||
onAddPurchase?: () => void;
|
||||
onAddMilestone?: () => void;
|
||||
onUpdatePrice?: () => void;
|
||||
assetSymbol?: string;
|
||||
priceTrackingEnabled?: boolean;
|
||||
}
|
||||
|
||||
export default function StatsBox({
|
||||
|
|
@ -35,9 +26,6 @@ export default function StatsBox({
|
|||
className,
|
||||
onAddPurchase,
|
||||
onAddMilestone,
|
||||
onUpdatePrice,
|
||||
assetSymbol,
|
||||
priceTrackingEnabled = false,
|
||||
}: StatsBoxProps) {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
|
|
@ -46,22 +34,6 @@ export default function StatsBox({
|
|||
const nextIndex = (selectedMilestoneIndex + 1) % milestones.length;
|
||||
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 (
|
||||
<div
|
||||
|
|
@ -72,17 +44,10 @@ export default function StatsBox({
|
|||
)}
|
||||
>
|
||||
<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">
|
||||
<ComponentTitle>Stats</ComponentTitle>
|
||||
|
||||
<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 */}
|
||||
<div className="relative">
|
||||
<button
|
||||
|
|
@ -93,7 +58,6 @@ export default function StatsBox({
|
|||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{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">
|
||||
{onAddPurchase && (
|
||||
|
|
@ -113,22 +77,11 @@ export default function StatsBox({
|
|||
onAddMilestone();
|
||||
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
|
||||
</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>
|
||||
|
|
@ -155,64 +108,31 @@ export default function StatsBox({
|
|||
<tr>
|
||||
<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>
|
||||
{priceTrackingEnabled && <th className="text-right text-red-500 text-xs py-2 pr-4">SWR 3%</th>}
|
||||
{priceTrackingEnabled && <th className="text-right text-red-500 text-xs py-2">SWR 4%</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* 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">
|
||||
<td className="text-right py-1">
|
||||
{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">
|
||||
{stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.04) : 'N/A'}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
|
||||
{/* Render milestones after current */}
|
||||
{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 (
|
||||
{milestones.map((milestone, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={cn(
|
||||
isSelectedMilestone
|
||||
index === selectedMilestoneIndex
|
||||
? "bg-red-500 text-black"
|
||||
: "text-red-500 font-bold"
|
||||
)}
|
||||
>
|
||||
<td className="py-1 pr-4">
|
||||
{milestone.description}
|
||||
</td>
|
||||
<td className="text-right py-1 pr-4">
|
||||
<td className="py-1 pr-4">{milestone.description}</td>
|
||||
<td className="text-right py-1">
|
||||
{Math.floor(milestone.target).toLocaleString()}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,26 +7,15 @@ import { FormEventHandler, useState } from 'react';
|
|||
import ComponentTitle from '@/components/ui/ComponentTitle';
|
||||
|
||||
interface CreateTrackerStepProps {
|
||||
onSuccess: (priceTrackingEnabled: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps) {
|
||||
const [label, setLabel] = useState('');
|
||||
const [unit, setUnit] = useState('');
|
||||
const [priceTracking, setPriceTracking] = useState(false);
|
||||
const [symbol, setSymbol] = useState('');
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const togglePriceTracking = (enabled: boolean) => {
|
||||
setPriceTracking(enabled);
|
||||
if (!enabled) {
|
||||
setSymbol('');
|
||||
setFullName('');
|
||||
}
|
||||
};
|
||||
|
||||
const submit: FormEventHandler = async (e) => {
|
||||
e.preventDefault();
|
||||
setProcessing(true);
|
||||
|
|
@ -43,14 +32,12 @@ export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps)
|
|||
body: JSON.stringify({
|
||||
label,
|
||||
unit,
|
||||
price_tracking_enabled: priceTracking ? 1 : 0,
|
||||
symbol: priceTracking ? symbol : null,
|
||||
full_name: priceTracking ? fullName : null,
|
||||
price_tracking_enabled: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 201 || response.status === 409) {
|
||||
onSuccess(priceTracking);
|
||||
onSuccess();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
if (data.errors) {
|
||||
|
|
@ -111,59 +98,9 @@ export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps)
|
|||
<InputError message={errors.unit} />
|
||||
</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
|
||||
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"
|
||||
>
|
||||
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,8 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import AssetSetupForm from '@/components/Assets/AssetSetupForm';
|
||||
import AddEntryForm from '@/components/Transactions/AddEntryForm';
|
||||
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
|
||||
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
|
||||
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 {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
@ -51,13 +11,7 @@ interface OnboardingStep {
|
|||
required: boolean;
|
||||
}
|
||||
|
||||
const ASSET_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[] = [
|
||||
const STEPS: OnboardingStep[] = [
|
||||
{ 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 },
|
||||
];
|
||||
|
|
@ -68,7 +22,6 @@ interface OnboardingFlowProps {
|
|||
|
||||
export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||
const [trackerCreated, setTrackerCreated] = useState(false);
|
||||
const [priceTracking, setPriceTracking] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [steps, setSteps] = useState<OnboardingStep[]>([]);
|
||||
|
||||
|
|
@ -78,7 +31,6 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
|||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => {
|
||||
if (data?.tracker) {
|
||||
setPriceTracking(data.tracker.price_tracking_enabled ?? false);
|
||||
setTrackerCreated(true);
|
||||
}
|
||||
})
|
||||
|
|
@ -87,22 +39,19 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
|||
|
||||
const checkOnboardingStatus = useCallback(async (currentSteps: OnboardingStep[]) => {
|
||||
try {
|
||||
const [entriesData, milestonesData, priceData] = await Promise.all([
|
||||
const [entriesData, milestonesData] = await Promise.all([
|
||||
fetch('/entries/summary').then(r => r.json()),
|
||||
fetch('/milestones').then(r => r.json()),
|
||||
fetch('/pricing/current').then(r => r.json()),
|
||||
]);
|
||||
|
||||
const hasEntries = entriesData.total_quantity > 0;
|
||||
const hasMilestones = milestonesData.length > 0;
|
||||
const hasPrice = !!priceData.current_price;
|
||||
|
||||
const freshSteps = currentSteps.map(step => ({
|
||||
...step,
|
||||
completed:
|
||||
(step.id === 'entries' && hasEntries) ||
|
||||
(step.id === 'milestones' && hasMilestones) ||
|
||||
(step.id === 'price' && hasPrice),
|
||||
(step.id === 'milestones' && hasMilestones),
|
||||
}));
|
||||
|
||||
setSteps(freshSteps);
|
||||
|
|
@ -122,14 +71,12 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
|||
useEffect(() => {
|
||||
if (!trackerCreated) return;
|
||||
|
||||
const initialSteps = priceTracking ? ASSET_STEPS : SIMPLE_STEPS;
|
||||
setSteps(initialSteps);
|
||||
setSteps(STEPS);
|
||||
setCurrentStep(0);
|
||||
checkOnboardingStatus(initialSteps);
|
||||
}, [trackerCreated, priceTracking, checkOnboardingStatus]);
|
||||
checkOnboardingStatus(STEPS);
|
||||
}, [trackerCreated, checkOnboardingStatus]);
|
||||
|
||||
const handleTrackerCreated = (withPriceTracking: boolean) => {
|
||||
setPriceTracking(withPriceTracking);
|
||||
const handleTrackerCreated = () => {
|
||||
setTrackerCreated(true);
|
||||
};
|
||||
|
||||
|
|
@ -151,11 +98,9 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
|||
|
||||
switch (step.id) {
|
||||
case 'entries':
|
||||
return <AddEntryForm onSuccess={handleStepComplete} priceTrackingEnabled={priceTracking} />;
|
||||
return <AddEntryForm onSuccess={handleStepComplete} />;
|
||||
case 'milestones':
|
||||
return <AddMilestoneForm onSuccess={handleStepComplete} />;
|
||||
case 'price':
|
||||
return <PriceTrackingStep onEnable={handleStepComplete} onSkip={onComplete ?? (() => {})} />;
|
||||
default:
|
||||
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'
|
||||
} ${index > 0 ? 'ml-2' : ''}`}
|
||||
>
|
||||
{step.completed ? '[✓]' : step.required ? '[REQ]' : '[OPT]'} {step.title}
|
||||
{step.completed ? '[✓]' : '[REQ]'} {step.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -219,7 +164,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
|||
[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
|
||||
{steps.filter(s => !s.completed).length} REQUIRED REMAINING
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,30 +11,23 @@ import ComponentTitle from '@/components/ui/ComponentTitle';
|
|||
interface EntryFormData {
|
||||
date: string;
|
||||
quantity: string;
|
||||
unit_price: string;
|
||||
total_cost: string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface AddEntryFormProps {
|
||||
unit?: string;
|
||||
priceTrackingEnabled?: boolean;
|
||||
onSuccess?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
interface EntrySummary {
|
||||
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>({
|
||||
date: todayISO(),
|
||||
quantity: '',
|
||||
unit_price: '',
|
||||
total_cost: '',
|
||||
});
|
||||
|
||||
const [currentHoldings, setCurrentHoldings] = useState<EntrySummary | null>(null);
|
||||
|
|
@ -55,18 +48,6 @@ export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = fa
|
|||
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) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
@ -86,7 +67,6 @@ export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = fa
|
|||
{currentHoldings && currentHoldings.total_quantity > 0 && (
|
||||
<p className="text-sm text-red-400/60 font-mono">
|
||||
[CURRENT] {currentHoldings.total_quantity.toFixed(6)} {unit}
|
||||
{priceTrackingEnabled && ` • €${currentHoldings.total_cost.toFixed(2)} spent`}
|
||||
</p>
|
||||
)}
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
|
|
@ -120,41 +100,6 @@ export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = fa
|
|||
<InputError message={errors.quantity} />
|
||||
</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">
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -6,47 +6,28 @@ 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';
|
||||
import type { Milestone, Tracker } from '@/types/domain';
|
||||
|
||||
interface PurchaseSummary {
|
||||
interface EntrySummary {
|
||||
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 [totalShares, setTotalShares] = useState(0);
|
||||
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 [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | 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([
|
||||
const [entriesResponse, milestonesResponse, trackerResponse] = await Promise.all([
|
||||
fetch('/entries/summary'),
|
||||
fetch('/pricing/current'),
|
||||
fetch('/milestones'),
|
||||
fetch('/tracker'),
|
||||
]);
|
||||
|
|
@ -56,19 +37,10 @@ export default function Dashboard() {
|
|||
|
||||
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,
|
||||
});
|
||||
setTotalShares(entries.total_quantity);
|
||||
totalQuantity = entries.total_quantity;
|
||||
}
|
||||
|
||||
if (priceResponse.ok) {
|
||||
const price = await priceResponse.json();
|
||||
setPriceData(price);
|
||||
}
|
||||
|
||||
if (milestonesResponse.ok) {
|
||||
const milestonesData = await milestonesResponse.json();
|
||||
setMilestones(milestonesData);
|
||||
|
|
@ -78,8 +50,6 @@ export default function Dashboard() {
|
|||
if (trackerResponse.ok) {
|
||||
const { tracker: trackerData } = await trackerResponse.json();
|
||||
setTracker(trackerData ?? null);
|
||||
setCurrentAsset(trackerData?.asset ?? null);
|
||||
setPriceTrackingEnabled(trackerData?.price_tracking_enabled ?? false);
|
||||
}
|
||||
|
||||
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
|
||||
|
|
@ -93,31 +63,24 @@ export default function Dashboard() {
|
|||
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,
|
||||
});
|
||||
setTotalShares(entries.total_quantity);
|
||||
}
|
||||
} 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) {
|
||||
|
|
@ -125,53 +88,13 @@ export default function Dashboard() {
|
|||
}
|
||||
};
|
||||
|
||||
// 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([
|
||||
const [entriesResponse, milestonesResponse, trackerResponse] = await Promise.all([
|
||||
fetch('/entries/summary'),
|
||||
fetch('/pricing/current'),
|
||||
fetch('/milestones'),
|
||||
fetch('/tracker'),
|
||||
]);
|
||||
|
|
@ -181,19 +104,10 @@ export default function Dashboard() {
|
|||
|
||||
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,
|
||||
});
|
||||
setTotalShares(entries.total_quantity);
|
||||
totalQuantity = entries.total_quantity;
|
||||
}
|
||||
|
||||
if (priceResponse.ok) {
|
||||
const price = await priceResponse.json();
|
||||
setPriceData(price);
|
||||
}
|
||||
|
||||
if (milestonesResponse.ok) {
|
||||
const milestonesData = await milestonesResponse.json();
|
||||
setMilestones(milestonesData);
|
||||
|
|
@ -201,10 +115,8 @@ export default function Dashboard() {
|
|||
}
|
||||
|
||||
if (trackerResponse.ok) {
|
||||
const trackerData = await trackerResponse.json();
|
||||
setTracker(trackerData);
|
||||
setCurrentAsset(trackerData?.asset ?? null);
|
||||
setPriceTrackingEnabled(trackerData?.price_tracking_enabled ?? false);
|
||||
const { tracker: trackerData } = await trackerResponse.json();
|
||||
setTracker(trackerData ?? null);
|
||||
}
|
||||
|
||||
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
|
||||
|
|
@ -219,22 +131,19 @@ export default function Dashboard() {
|
|||
);
|
||||
}
|
||||
|
||||
// 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)
|
||||
setActiveForm(null);
|
||||
};
|
||||
|
||||
// Show onboarding if needed
|
||||
if (needsOnboarding) {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -248,55 +157,45 @@ export default function Dashboard() {
|
|||
<>
|
||||
<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}
|
||||
value={totalShares}
|
||||
unit={tracker?.unit}
|
||||
onClick={handleLedClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Box 2: Progress Bar (toggleable) */}
|
||||
<div style={{ display: showProgressBar ? 'block' : 'none' }}>
|
||||
<ProgressBar
|
||||
currentQuantity={purchaseData.total_shares}
|
||||
currentQuantity={totalShares}
|
||||
milestones={milestones}
|
||||
selectedMilestoneIndex={selectedMilestoneIndex}
|
||||
onClick={handleProgressClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Box 3: Stats Box (toggleable) */}
|
||||
<div style={{ display: showStatsBox ? 'block' : 'none' }}>
|
||||
<StatsBox
|
||||
stats={statsData}
|
||||
stats={{ totalShares }}
|
||||
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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue