Move buttons into dropdown

This commit is contained in:
myrmidex 2025-07-13 01:07:16 +02:00
parent 4e0ae175fb
commit 6c1f4a14c3
5 changed files with 163 additions and 144 deletions

View file

@ -18,7 +18,7 @@ public function current(): JsonResponse
]); ]);
} }
public function update(Request $request): JsonResponse public function update(Request $request)
{ {
$validated = $request->validate([ $validated = $request->validate([
'date' => 'required|date|before_or_equal:today', 'date' => 'required|date|before_or_equal:today',
@ -27,11 +27,7 @@ public function update(Request $request): JsonResponse
$assetPrice = AssetPrice::updatePrice($validated['date'], $validated['price']); $assetPrice = AssetPrice::updatePrice($validated['date'], $validated['price']);
return response()->json([ return back()->with('success', 'Asset price updated successfully!');
'success' => true,
'message' => 'Asset price updated successfully!',
'data' => $assetPrice,
]);
} }
public function history(Request $request): JsonResponse public function history(Request $request): JsonResponse

View file

@ -1,13 +1,15 @@
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm';
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
interface InlineFormProps { interface InlineFormProps {
type: 'purchase' | 'milestone' | null; type: 'purchase' | 'milestone' | 'price' | null;
onClose: () => void; onClose: () => void;
onPurchaseSuccess?: () => void; onPurchaseSuccess?: () => void;
onMilestoneSuccess?: () => void; onMilestoneSuccess?: () => void;
onPriceSuccess?: () => void;
className?: string; className?: string;
} }
@ -16,11 +18,12 @@ export default function InlineForm({
onClose, onClose,
onPurchaseSuccess, onPurchaseSuccess,
onMilestoneSuccess, onMilestoneSuccess,
onPriceSuccess,
className className
}: InlineFormProps) { }: InlineFormProps) {
if (!type) return null; if (!type) return null;
const title = type === 'purchase' ? 'ADD PURCHASE' : 'ADD MILESTONE'; const title = type === 'purchase' ? 'ADD PURCHASE' : type === 'milestone' ? 'ADD MILESTONE' : 'UPDATE PRICE';
return ( return (
<div <div
@ -54,13 +57,20 @@ export default function InlineForm({
onClose(); onClose();
}} }}
/> />
) : ( ) : type === 'milestone' ? (
<AddMilestoneForm <AddMilestoneForm
onSuccess={() => { onSuccess={() => {
if (onMilestoneSuccess) onMilestoneSuccess(); if (onMilestoneSuccess) onMilestoneSuccess();
onClose(); onClose();
}} }}
/> />
) : (
<UpdatePriceForm
onSuccess={() => {
if (onPriceSuccess) onPriceSuccess();
onClose();
}}
/>
)} )}
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useState } from 'react';
interface Milestone { interface Milestone {
target: number; target: number;
@ -21,6 +22,7 @@ interface StatsBoxProps {
className?: string; className?: string;
onAddPurchase?: () => void; onAddPurchase?: () => void;
onAddMilestone?: () => void; onAddMilestone?: () => void;
onUpdatePrice?: () => void;
} }
export default function StatsBox({ export default function StatsBox({
@ -28,8 +30,10 @@ export default function StatsBox({
milestones = [], milestones = [],
className, className,
onAddPurchase, onAddPurchase,
onAddMilestone onAddMilestone,
onUpdatePrice
}: StatsBoxProps) { }: StatsBoxProps) {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('de-DE', { return new Intl.NumberFormat('de-DE', {
style: 'currency', style: 'currency',
@ -50,149 +54,141 @@ export default function StatsBox({
return ( return (
<div <div
className={cn( className={cn(
"bg-black border-4 border-gray-800 rounded-lg", "bg-black p-8",
"shadow-2xl shadow-red-500/20", "transition-all duration-300",
"p-6 space-y-4",
className className
)} )}
> >
{/* Current Price */} <div className="w-full border-4 border-red-500 p-2 bg-black space-y-4">
{stats.currentPrice && ( {/* STATS Title and Current Price */}
<div className="text-center border-b border-red-500/30 pb-4"> <div className="flex justify-between items-center mb-6 relative">
<div className="text-red-400/70 text-sm font-medium tracking-wide mb-2"> <h2 className="text-red-500 text-lg font-mono font-bold tracking-wider">
current price STATS
</div> </h2>
<div className="text-red-500 text-2xl md:text-3xl font-mono-display tracking-wider"> <div className="flex items-center space-x-4 relative">
{formatCurrencyDetailed(stats.currentPrice)} {stats.currentPrice && (
</div> <div className="text-red-500 text-sm font-mono tracking-wider">
</div> VWCE: {formatCurrencyDetailed(stats.currentPrice)}
)}
{/* Portfolio Stats Grid */}
<div className="grid grid-cols-2 gap-4 text-sm font-mono">
{/* Total Investment */}
<div className="space-y-1">
<div className="text-red-400/70 text-xs">Total Investment</div>
<div className="text-red-400">
{formatCurrency(stats.totalInvestment)}
</div>
</div>
{/* Current Value */}
{stats.currentValue && (
<div className="space-y-1">
<div className="text-red-400/70 text-xs">Current Value</div>
<div className="text-red-400">
{formatCurrency(stats.currentValue)}
</div>
</div>
)}
{/* Average Cost */}
<div className="space-y-1">
<div className="text-red-400/70 text-xs">Avg Cost/Share</div>
<div className="text-red-400">
{formatCurrencyDetailed(stats.averageCostPerShare)}
</div>
</div>
{/* Profit/Loss */}
{stats.profitLoss !== undefined && (
<div className="space-y-1">
<div className="text-red-400/70 text-xs">P&L</div>
<div className={cn(
"font-bold",
stats.profitLoss >= 0 ? "text-green-400" : "text-red-400"
)}>
{stats.profitLoss >= 0 ? '+' : ''}{formatCurrency(stats.profitLoss)}
</div>
</div>
)}
</div>
{/* Withdrawal Estimates */}
{stats.currentValue && (
<div className="border-t border-red-500/30 pt-4">
<div className="text-red-400/70 text-xs mb-2">Annual Withdrawal (Safe)</div>
<div className="grid grid-cols-2 gap-4 text-sm font-mono">
<div className="space-y-1">
<div className="text-red-400/60 text-xs">3% Rule</div>
<div className="text-red-400">
{formatCurrency(stats.currentValue * 0.03)}
</div> </div>
</div> )}
<div className="space-y-1">
<div className="text-red-400/60 text-xs">4% Rule</div> {/* Action Dropdown */}
<div className="text-red-400"> <div className="relative">
{formatCurrency(stats.currentValue * 0.04)} <button
</div> onClick={() => setIsDropdownOpen(!isDropdownOpen)}
</div> className="flex items-center justify-center px-2 py-1 rounded bg-red-600/20 border border-red-500/50 text-red-400 hover:bg-red-600/40 hover:text-red-300 transition-colors text-sm"
</div> aria-label="Add actions"
</div> >
)} <Plus className="w-4 h-4" />
</button>
{/* Milestones */}
{milestones.length > 0 && ( {/* Dropdown Menu */}
<div className="border-t border-red-500/30 pt-4"> {isDropdownOpen && (
<div className="text-red-400/70 text-xs mb-3">Milestones</div> <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="space-y-2 max-h-32 overflow-y-auto"> {onAddPurchase && (
{milestones.map((milestone, index) => { <button
const isReached = stats.totalShares >= milestone.target; onClick={() => {
return ( onAddPurchase();
<div key={index} className="flex items-center justify-between text-sm"> setIsDropdownOpen(false);
<div className="flex items-center space-x-2"> }}
<div className={cn( 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"
"w-2 h-2 rounded-full", >
isReached ? "bg-green-400" : "bg-gray-600" ADD PURCHASE
)} /> </button>
<span className={cn( )}
"font-mono", {onAddMilestone && (
isReached ? "text-green-400" : "text-red-400" <button
)}> onClick={() => {
{milestone.target.toLocaleString()} onAddMilestone();
</span> setIsDropdownOpen(false);
</div> }}
<div className={cn( className="w-full text-left px-4 py-2 text-blue-400 hover:bg-blue-600/20 hover:text-blue-300 transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0"
"text-xs truncate ml-2", >
isReached ? "text-green-400/70" : "text-red-400/70" ADD MILESTONE
)}> </button>
{milestone.description} )}
</div> {onUpdatePrice && (
<button
onClick={() => {
onUpdatePrice();
setIsDropdownOpen(false);
}}
className="w-full text-left px-4 py-2 text-green-400 hover:bg-green-600/20 hover:text-green-300 transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0"
>
UPDATE PRICE
</button>
)}
</div> </div>
); )}
})} </div>
</div> </div>
</div> </div>
)}
{/* Action Buttons */} {/* Milestone Table */}
<div className="border-t border-red-500/30 pt-4"> <div className="border-t border-red-500/30 pt-4">
<div className="flex items-center justify-center space-x-4"> <div className="text-red-400/70 text-xs mb-3">MILESTONES</div>
{/* Add Purchase Button */} <div className="overflow-x-auto">
{onAddPurchase && ( <table className="w-full text-sm font-mono">
<button <thead>
onClick={onAddPurchase} <tr className="border-b border-red-500/20">
className="flex items-center space-x-2 px-4 py-2 rounded bg-red-600/20 border border-red-500/50 text-red-400 hover:bg-red-600/40 hover:text-red-300 transition-colors text-sm" <th className="text-left text-red-400/60 text-xs py-2">DESCRIPTION</th>
aria-label="Add purchase" <th className="text-right text-red-400/60 text-xs py-2">SHARES</th>
> <th className="text-right text-red-400/60 text-xs py-2">SWR 3%</th>
<Plus className="w-4 h-4" /> <th className="text-right text-red-400/60 text-xs py-2">SWR 4%</th>
<span className="font-mono">ADD PURCHASE</span> </tr>
</button> </thead>
)} <tbody>
{/* Create combined array with current position and milestones, sorted by target */}
{/* Add Milestone Button */} {[
{onAddMilestone && ( ...milestones.map(m => ({ ...m, isCurrent: false })),
<button {
onClick={onAddMilestone} target: stats.totalShares,
className="flex items-center space-x-2 px-4 py-2 rounded bg-blue-600/20 border border-blue-500/50 text-blue-400 hover:bg-blue-600/40 hover:text-blue-300 transition-colors text-sm" description: 'CURRENT',
aria-label="Add milestone" created_at: '',
> isCurrent: true
<Plus className="w-4 h-4" /> }
<span className="font-mono">ADD MILESTONE</span> ]
</button> .sort((a, b) => a.target - b.target)
)} .map((item, index) => {
const swr3 = stats.currentPrice ? item.target * stats.currentPrice * 0.03 : 0;
const swr4 = stats.currentPrice ? item.target * stats.currentPrice * 0.04 : 0;
return (
<tr
key={index}
className={cn(
"border-b border-red-500/10",
item.isCurrent
? "bg-red-500/10 text-red-300"
: stats.totalShares >= item.target
? "text-green-400/80"
: "text-red-400/70"
)}
>
<td className="py-2 pr-4">
{item.isCurrent ? (
<span className="font-bold">{item.description}</span>
) : (
item.description
)}
</td>
<td className="text-right py-2 pr-4">
{Math.floor(item.target).toLocaleString()}
</td>
<td className="text-right py-2 pr-4">
{stats.currentPrice ? formatCurrency(swr3) : 'N/A'}
</td>
<td className="text-right py-2">
{stats.currentPrice ? formatCurrency(swr4) : 'N/A'}
</td>
</tr>
);
})}
</tbody>
</table>
</div> </div>
</div> </div>
</div>
</div> </div>
); );
} }

View file

@ -16,9 +16,10 @@ interface PriceUpdateFormData {
interface UpdatePriceFormProps { interface UpdatePriceFormProps {
currentPrice?: number; currentPrice?: number;
className?: string; className?: string;
onSuccess?: () => void;
} }
export default function UpdatePriceForm({ currentPrice, className }: UpdatePriceFormProps) { export default function UpdatePriceForm({ currentPrice, className, onSuccess }: UpdatePriceFormProps) {
const { data, setData, post, processing, errors } = useForm<PriceUpdateFormData>({ const { data, setData, post, processing, errors } = useForm<PriceUpdateFormData>({
date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format
price: currentPrice?.toString() || '', price: currentPrice?.toString() || '',
@ -31,6 +32,7 @@ export default function UpdatePriceForm({ currentPrice, className }: UpdatePrice
onSuccess: () => { onSuccess: () => {
// Keep the date, reset only price if needed // Keep the date, reset only price if needed
// User might want to update same day multiple times // User might want to update same day multiple times
if (onSuccess) onSuccess();
}, },
}); });
}; };

View file

@ -35,7 +35,7 @@ export default function Dashboard() {
const [milestones, setMilestones] = useState<Milestone[]>([]); const [milestones, setMilestones] = useState<Milestone[]>([]);
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' | null>(null); const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | 'price' | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Fetch purchase summary, current price, and milestones // Fetch purchase summary, current price, and milestones
@ -98,6 +98,19 @@ export default function Dashboard() {
} }
}; };
// 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 // Calculate portfolio stats
const currentValue = priceData.current_price const currentValue = priceData.current_price
@ -180,6 +193,7 @@ export default function Dashboard() {
milestones={milestones} milestones={milestones}
onAddPurchase={() => setActiveForm('purchase')} onAddPurchase={() => setActiveForm('purchase')}
onAddMilestone={() => setActiveForm('milestone')} onAddMilestone={() => setActiveForm('milestone')}
onUpdatePrice={() => setActiveForm('price')}
/> />
</div> </div>
@ -190,6 +204,7 @@ export default function Dashboard() {
onClose={() => setActiveForm(null)} onClose={() => setActiveForm(null)}
onPurchaseSuccess={handlePurchaseSuccess} onPurchaseSuccess={handlePurchaseSuccess}
onMilestoneSuccess={handleMilestoneSuccess} onMilestoneSuccess={handleMilestoneSuccess}
onPriceSuccess={handlePriceSuccess}
/> />
</div> </div>
</div> </div>