From 6e76ce9c68ea8b724b00906da79a37aca1dfdd45 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 2 May 2026 18:33:41 +0200 Subject: [PATCH] 34 - Frontend: AddEntryForm, generalize unit labels, update LedDisplay/StatsBox/ProgressBar/InlineForm --- .../js/components/Display/InlineForm.tsx | 24 +-- .../js/components/Display/LedDisplay.tsx | 7 + .../js/components/Display/ProgressBar.tsx | 18 +- resources/js/components/Display/StatsBox.tsx | 6 +- .../components/Onboarding/OnboardingFlow.tsx | 4 +- .../components/Transactions/AddEntryForm.tsx | 181 ++++++++++++++++++ resources/js/pages/dashboard.tsx | 26 ++- 7 files changed, 234 insertions(+), 32 deletions(-) create mode 100644 resources/js/components/Transactions/AddEntryForm.tsx diff --git a/resources/js/components/Display/InlineForm.tsx b/resources/js/components/Display/InlineForm.tsx index 7dd4ca8..1254e81 100644 --- a/resources/js/components/Display/InlineForm.tsx +++ b/resources/js/components/Display/InlineForm.tsx @@ -1,11 +1,12 @@ +import AddEntryForm from '@/components/Transactions/AddEntryForm'; import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; -import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm'; import { cn } from '@/lib/utils'; -import ComponentTitle from '@/components/ui/ComponentTitle'; interface InlineFormProps { type: 'purchase' | 'milestone' | 'price' | null; + unit?: string; + priceTrackingEnabled?: boolean; onClose: () => void; onPurchaseSuccess?: () => void; onMilestoneSuccess?: () => void; @@ -15,31 +16,30 @@ interface InlineFormProps { export default function InlineForm({ type, + unit = 'units', + priceTrackingEnabled = false, onClose, onPurchaseSuccess, onMilestoneSuccess, onPriceSuccess, - className + className, }: InlineFormProps) { if (!type) return null; - const title = type === 'purchase' ? 'ADD PURCHASE' : type === 'milestone' ? 'ADD MILESTONE' : 'UPDATE PRICE'; - return (
- {/* Header */}
- - {/* Form Content */}
{type === 'purchase' ? ( - { if (onPurchaseSuccess) onPurchaseSuccess(); onClose(); diff --git a/resources/js/components/Display/LedDisplay.tsx b/resources/js/components/Display/LedDisplay.tsx index 77d24a8..4ca24a8 100644 --- a/resources/js/components/Display/LedDisplay.tsx +++ b/resources/js/components/Display/LedDisplay.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; interface LedDisplayProps { value: number; + unit?: string; className?: string; animate?: boolean; onClick?: () => void; @@ -10,6 +11,7 @@ interface LedDisplayProps { export default function LedDisplay({ value, + unit, className, onClick }: LedDisplayProps) { @@ -55,6 +57,11 @@ export default function LedDisplay({ {formattedValue}
+ {unit && ( +
+ {unit} +
+ )}
); } diff --git a/resources/js/components/Display/ProgressBar.tsx b/resources/js/components/Display/ProgressBar.tsx index 4343cb5..ccf594d 100644 --- a/resources/js/components/Display/ProgressBar.tsx +++ b/resources/js/components/Display/ProgressBar.tsx @@ -7,28 +7,26 @@ interface Milestone { } interface ProgressBarProps { - currentShares: number; + currentQuantity: number; milestones: Milestone[]; selectedMilestoneIndex?: number; className?: string; onClick?: () => void; } -export default function ProgressBar({ - currentShares, +export default function ProgressBar({ + currentQuantity, milestones, selectedMilestoneIndex = 0, className, onClick }: ProgressBarProps) { - // Get the selected milestone for progress calculation - const selectedMilestone = milestones.length > 0 && selectedMilestoneIndex < milestones.length - ? milestones[selectedMilestoneIndex] + const selectedMilestone = milestones.length > 0 && selectedMilestoneIndex < milestones.length + ? milestones[selectedMilestoneIndex] : null; - - // Calculate progress percentage - const progressPercentage = selectedMilestone - ? Math.min((currentShares / selectedMilestone.target) * 100, 100) + + const progressPercentage = selectedMilestone + ? Math.min((currentQuantity / selectedMilestone.target) * 100, 100) : 0; return (
void; @@ -32,6 +33,7 @@ interface StatsBoxProps { export default function StatsBox({ stats, + unit = 'units', milestones = [], selectedMilestoneIndex = 0, onMilestoneSelect, @@ -107,7 +109,7 @@ export default function StatsBox({ }} 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 PURCHASE + ADD ENTRY )} {onAddMilestone && ( @@ -157,7 +159,7 @@ export default function StatsBox({ DESCRIPTION - SHARES + {unit.toUpperCase()} {priceTrackingEnabled && SWR 3%} {priceTrackingEnabled && SWR 4%} diff --git a/resources/js/components/Onboarding/OnboardingFlow.tsx b/resources/js/components/Onboarding/OnboardingFlow.tsx index 04f6615..78aa7a0 100644 --- a/resources/js/components/Onboarding/OnboardingFlow.tsx +++ b/resources/js/components/Onboarding/OnboardingFlow.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import AssetSetupForm from '@/components/Assets/AssetSetupForm'; -import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; +import AddEntryForm from '@/components/Transactions/AddEntryForm'; import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm'; import CreateTrackerStep from '@/components/Onboarding/CreateTrackerStep'; @@ -138,7 +138,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { switch (step.id) { case 'entries': - return ; + return ; case 'milestones': return ; case 'price': diff --git a/resources/js/components/Transactions/AddEntryForm.tsx b/resources/js/components/Transactions/AddEntryForm.tsx new file mode 100644 index 0000000..fee5885 --- /dev/null +++ b/resources/js/components/Transactions/AddEntryForm.tsx @@ -0,0 +1,181 @@ +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import InputError from '@/components/InputError'; +import { useForm } from '@inertiajs/react'; +import { LoaderCircle } from 'lucide-react'; +import { FormEventHandler, useEffect, useState } from 'react'; +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) { + const { data, setData, post, processing, errors, reset } = useForm({ + date: new Date().toISOString().split('T')[0], + quantity: '', + unit_price: '', + total_cost: '', + }); + + const [currentHoldings, setCurrentHoldings] = useState(null); + + useEffect(() => { + const fetchSummary = async () => { + try { + const response = await fetch('/entries/summary'); + if (response.ok) { + const summary = await response.json(); + setCurrentHoldings(summary); + } + } catch (error) { + console.error('Failed to fetch entry summary:', error); + } + }; + + 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(); + + post(route('entries.store'), { + onSuccess: () => { + reset(); + setData('date', new Date().toISOString().split('T')[0]); + if (onSuccess) onSuccess(); + }, + }); + }; + + return ( +
+
+ ADD ENTRY + {currentHoldings && currentHoldings.total_quantity > 0 && ( +

+ [CURRENT] {currentHoldings.total_quantity.toFixed(6)} {unit} + {priceTrackingEnabled && ` • €${currentHoldings.total_cost.toFixed(2)} spent`} +

+ )} +
+
+ + setData('date', e.target.value)} + max={new Date().toISOString().split('T')[0]} + 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 transition-all glow-red" + /> + +
+ +
+ + setData('quantity', 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" + /> + +
+ + {priceTrackingEnabled && ( + <> +
+ + 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" + /> + +
+ +
+ + 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" + /> +

[AUTO-CALC] quantity × price

+ +
+ + )} + +
+ + {onCancel && ( + + )} +
+
+
+
+ ); +} diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index de0b334..d11d379 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -23,12 +23,20 @@ interface Milestone { created_at: string; } -interface CurrentAsset { +interface TrackerAsset { id: number; symbol: string; full_name: string | null; } +interface Tracker { + id: number; + label: string; + unit: string; + price_tracking_enabled: boolean; + asset: TrackerAsset | null; +} + export default function Dashboard() { const [purchaseData, setPurchaseData] = useState({ total_shares: 0, @@ -47,7 +55,8 @@ export default function Dashboard() { const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | 'price' | null>(null); const [loading, setLoading] = useState(true); const [needsOnboarding, setNeedsOnboarding] = useState(false); - const [currentAsset, setCurrentAsset] = useState(null); + const [tracker, setTracker] = useState(null); + const [currentAsset, setCurrentAsset] = useState(null); const [priceTrackingEnabled, setPriceTrackingEnabled] = useState(false); // Fetch entry summary, current price, milestones, and check onboarding @@ -86,9 +95,10 @@ export default function Dashboard() { } if (trackerResponse.ok) { - const tracker = await trackerResponse.json(); - setCurrentAsset(tracker?.asset ?? null); - setPriceTrackingEnabled(tracker?.price_tracking_enabled ?? false); + const trackerData = await trackerResponse.json(); + setTracker(trackerData); + setCurrentAsset(trackerData?.asset ?? null); + setPriceTrackingEnabled(trackerData?.price_tracking_enabled ?? false); } setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0); @@ -263,6 +273,7 @@ export default function Dashboard() {
@@ -270,7 +281,7 @@ export default function Dashboard() { {/* Box 2: Progress Bar (toggleable) */}
setActiveForm(null)} onPurchaseSuccess={handlePurchaseSuccess} onMilestoneSuccess={handleMilestoneSuccess}