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

View file

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

View file

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

View file

@ -16,9 +16,10 @@ interface PriceUpdateFormData {
interface UpdatePriceFormProps {
currentPrice?: number;
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>({
date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format
price: currentPrice?.toString() || '',
@ -31,6 +32,7 @@ export default function UpdatePriceForm({ currentPrice, className }: UpdatePrice
onSuccess: () => {
// Keep the date, reset only price if needed
// 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 [showProgressBar, setShowProgressBar] = 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);
// 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
const currentValue = priceData.current_price
@ -180,6 +193,7 @@ export default function Dashboard() {
milestones={milestones}
onAddPurchase={() => setActiveForm('purchase')}
onAddMilestone={() => setActiveForm('milestone')}
onUpdatePrice={() => setActiveForm('price')}
/>
</div>
@ -190,6 +204,7 @@ export default function Dashboard() {
onClose={() => setActiveForm(null)}
onPurchaseSuccess={handlePurchaseSuccess}
onMilestoneSuccess={handleMilestoneSuccess}
onPriceSuccess={handlePriceSuccess}
/>
</div>
</div>