diff --git a/app/Http/Controllers/Milestones/MilestoneController.php b/app/Http/Controllers/Milestones/MilestoneController.php index aee0ef7..871b9a5 100644 --- a/app/Http/Controllers/Milestones/MilestoneController.php +++ b/app/Http/Controllers/Milestones/MilestoneController.php @@ -3,40 +3,32 @@ namespace App\Http\Controllers\Milestones; use App\Http\Controllers\Controller; +use App\Models\Milestone; use Illuminate\Http\Request; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\JsonResponse; class MilestoneController extends Controller { - /** - * Store a new milestone. - */ - public function store(Request $request): JsonResponse + public function store(Request $request): RedirectResponse { $request->validate([ 'target' => 'required|integer|min:1', 'description' => 'required|string|max:255', ]); - // For now, just return success without persisting to database - // This allows the frontend form to work properly - return response()->json([ - 'message' => 'Milestone created successfully', - 'milestone' => [ - 'target' => $request->target, - 'description' => $request->description, - 'created_at' => now(), - ] - ], 201); + Milestone::create([ + 'target' => $request->target, + 'description' => $request->description, + ]); + + return back()->with('success', 'Milestone created successfully'); } - /** - * Get all milestones. - */ public function index(): JsonResponse { - // For now, return empty array - // Later this could fetch from database - return response()->json([]); + $milestones = Milestone::orderBy('target')->get(); + + return response()->json($milestones); } } \ No newline at end of file diff --git a/app/Models/Milestone.php b/app/Models/Milestone.php new file mode 100644 index 0000000..98efa38 --- /dev/null +++ b/app/Models/Milestone.php @@ -0,0 +1,21 @@ + 'integer', + ]; +} diff --git a/database/migrations/2025_07_12_221324_create_milestones_table.php b/database/migrations/2025_07_12_221324_create_milestones_table.php new file mode 100644 index 0000000..da034cc --- /dev/null +++ b/database/migrations/2025_07_12_221324_create_milestones_table.php @@ -0,0 +1,29 @@ +id(); + $table->integer('target'); + $table->string('description'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('milestones'); + } +}; diff --git a/resources/js/components/Display/ProgressBar.tsx b/resources/js/components/Display/ProgressBar.tsx index a777d3a..97e0b4b 100644 --- a/resources/js/components/Display/ProgressBar.tsx +++ b/resources/js/components/Display/ProgressBar.tsx @@ -1,45 +1,14 @@ import { cn } from '@/lib/utils'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { useState } from 'react'; interface ProgressBarProps { - value: number; className?: string; onClick?: () => void; } export default function ProgressBar({ - value, className, onClick }: ProgressBarProps) { - const [currentMilestoneIndex, setCurrentMilestoneIndex] = useState(0); - - // Milestone definitions - const milestones = [ - { target: 1500, label: '1.5K', color: 'bg-blue-500' }, - { target: 3000, label: '3K', color: 'bg-green-500' }, - { target: 4500, label: '4.5K', color: 'bg-yellow-500' }, - { target: 6000, label: '6K', color: 'bg-red-500' }, - ]; - - const currentMilestone = milestones[currentMilestoneIndex]; - const progress = Math.min((value / currentMilestone.target) * 100, 100); - const isCompleted = value >= currentMilestone.target; - - // Milestone navigation - const nextMilestone = () => { - setCurrentMilestoneIndex((prev) => - prev < milestones.length - 1 ? prev + 1 : 0 - ); - }; - - const prevMilestone = () => { - setCurrentMilestoneIndex((prev) => - prev > 0 ? prev - 1 : milestones.length - 1 - ); - }; - return (
void; onAddMilestone?: () => void; @@ -18,6 +25,7 @@ interface StatsBoxProps { export default function StatsBox({ stats, + milestones = [], className, onAddPurchase, onAddMilestone @@ -123,6 +131,40 @@ export default function StatsBox({
)} + {/* Milestones */} + {milestones.length > 0 && ( +
+
Milestones
+
+ {milestones.map((milestone, index) => { + const isReached = stats.totalShares >= milestone.target; + return ( +
+
+
+ + {milestone.target.toLocaleString()} + +
+
+ {milestone.description} +
+
+ ); + })} +
+
+ )} + {/* Action Buttons */}
diff --git a/resources/js/components/Transactions/AddPurchaseForm.tsx b/resources/js/components/Transactions/AddPurchaseForm.tsx index edc42ba..68732b2 100644 --- a/resources/js/components/Transactions/AddPurchaseForm.tsx +++ b/resources/js/components/Transactions/AddPurchaseForm.tsx @@ -1,5 +1,4 @@ import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import InputError from '@/components/InputError'; diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 6a295a2..b2b0f80 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -15,6 +15,12 @@ interface CurrentPrice { current_price: number | null; } +interface Milestone { + target: number; + description: string; + created_at: string; +} + export default function Dashboard() { const [purchaseData, setPurchaseData] = useState({ total_shares: 0, @@ -26,18 +32,20 @@ export default function Dashboard() { current_price: null, }); + const [milestones, setMilestones] = useState([]); const [showProgressBar, setShowProgressBar] = useState(false); const [showStatsBox, setShowStatsBox] = useState(false); const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | null>(null); const [loading, setLoading] = useState(true); - // Fetch purchase summary and current price + // Fetch purchase summary, current price, and milestones useEffect(() => { const fetchData = async () => { try { - const [purchaseResponse, priceResponse] = await Promise.all([ + const [purchaseResponse, priceResponse, milestonesResponse] = await Promise.all([ fetch('/purchases/summary'), fetch('/pricing/current'), + fetch('/milestones'), ]); if (purchaseResponse.ok) { @@ -49,6 +57,11 @@ export default function Dashboard() { const price = await priceResponse.json(); setPriceData(price); } + + if (milestonesResponse.ok) { + const milestonesData = await milestonesResponse.json(); + setMilestones(milestonesData); + } } catch (error) { console.error('Failed to fetch data:', error); } finally { @@ -72,6 +85,19 @@ export default function Dashboard() { } }; + // Refresh milestones after successful creation + const handleMilestoneSuccess = async () => { + try { + const milestonesResponse = await fetch('/milestones'); + if (milestonesResponse.ok) { + const milestonesData = await milestonesResponse.json(); + setMilestones(milestonesData); + } + } catch (error) { + console.error('Failed to refresh milestone data:', error); + } + }; + // Calculate portfolio stats const currentValue = priceData.current_price @@ -141,7 +167,6 @@ export default function Dashboard() { {/* Box 2: Progress Bar (toggleable) */}
@@ -150,6 +175,7 @@ export default function Dashboard() {
setActiveForm('purchase')} onAddMilestone={() => setActiveForm('milestone')} /> @@ -161,9 +187,7 @@ export default function Dashboard() { type={activeForm} onClose={() => setActiveForm(null)} onPurchaseSuccess={handlePurchaseSuccess} - onMilestoneSuccess={() => { - console.log('Milestone added successfully'); - }} + onMilestoneSuccess={handleMilestoneSuccess} />
diff --git a/tests/Feature/MilestoneTest.php b/tests/Feature/MilestoneTest.php new file mode 100644 index 0000000..d86bb84 --- /dev/null +++ b/tests/Feature/MilestoneTest.php @@ -0,0 +1,59 @@ + 1500, + 'description' => 'First milestone' + ]); + + $this->assertDatabaseHas('milestones', [ + 'target' => 1500, + 'description' => 'First milestone' + ]); + + $this->assertEquals(1500, $milestone->target); + $this->assertEquals('First milestone', $milestone->description); + } + + public function test_can_fetch_milestones_via_api(): void + { + // Create test milestones + Milestone::create(['target' => 1500, 'description' => 'First milestone']); + Milestone::create(['target' => 3000, 'description' => 'Second milestone']); + + $response = $this->get('/milestones'); + + $response->assertStatus(200); + $response->assertJsonCount(2); + $response->assertJson([ + ['target' => 1500, 'description' => 'First milestone'], + ['target' => 3000, 'description' => 'Second milestone'] + ]); + } + + public function test_milestones_ordered_by_target(): void + { + // Create milestones in reverse order + Milestone::create(['target' => 3000, 'description' => 'Third']); + Milestone::create(['target' => 1000, 'description' => 'First']); + Milestone::create(['target' => 2000, 'description' => 'Second']); + + $response = $this->get('/milestones'); + + $milestones = $response->json(); + $this->assertEquals(1000, $milestones[0]['target']); + $this->assertEquals(2000, $milestones[1]['target']); + $this->assertEquals(3000, $milestones[2]['target']); + } +}