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']);
+ }
+}