Fix milestone issues

This commit is contained in:
myrmidex 2025-07-13 00:18:45 +02:00
parent 2ea5107106
commit 6e08bd80ed
8 changed files with 193 additions and 58 deletions

View file

@ -3,40 +3,32 @@
namespace App\Http\Controllers\Milestones; namespace App\Http\Controllers\Milestones;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Milestone;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
class MilestoneController extends Controller class MilestoneController extends Controller
{ {
/** public function store(Request $request): RedirectResponse
* Store a new milestone.
*/
public function store(Request $request): JsonResponse
{ {
$request->validate([ $request->validate([
'target' => 'required|integer|min:1', 'target' => 'required|integer|min:1',
'description' => 'required|string|max:255', 'description' => 'required|string|max:255',
]); ]);
// For now, just return success without persisting to database Milestone::create([
// This allows the frontend form to work properly 'target' => $request->target,
return response()->json([ 'description' => $request->description,
'message' => 'Milestone created successfully', ]);
'milestone' => [
'target' => $request->target, return back()->with('success', 'Milestone created successfully');
'description' => $request->description,
'created_at' => now(),
]
], 201);
} }
/**
* Get all milestones.
*/
public function index(): JsonResponse public function index(): JsonResponse
{ {
// For now, return empty array $milestones = Milestone::orderBy('target')->get();
// Later this could fetch from database
return response()->json([]); return response()->json($milestones);
} }
} }

21
app/Models/Milestone.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @method static create(array $array)
* @method static orderBy(string $string)
*/
class Milestone extends Model
{
protected $fillable = [
'target',
'description',
];
protected $casts = [
'target' => 'integer',
];
}

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('milestones', function (Blueprint $table) {
$table->id();
$table->integer('target');
$table->string('description');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('milestones');
}
};

View file

@ -1,45 +1,14 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { useState } from 'react';
interface ProgressBarProps { interface ProgressBarProps {
value: number;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
} }
export default function ProgressBar({ export default function ProgressBar({
value,
className, className,
onClick onClick
}: ProgressBarProps) { }: 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 ( return (
<div <div
className={cn( className={cn(

View file

@ -1,6 +1,12 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
interface Milestone {
target: number;
description: string;
created_at: string;
}
interface StatsBoxProps { interface StatsBoxProps {
stats: { stats: {
totalShares: number; totalShares: number;
@ -11,6 +17,7 @@ interface StatsBoxProps {
profitLoss?: number; profitLoss?: number;
profitLossPercentage?: number; profitLossPercentage?: number;
}; };
milestones?: Milestone[];
className?: string; className?: string;
onAddPurchase?: () => void; onAddPurchase?: () => void;
onAddMilestone?: () => void; onAddMilestone?: () => void;
@ -18,6 +25,7 @@ interface StatsBoxProps {
export default function StatsBox({ export default function StatsBox({
stats, stats,
milestones = [],
className, className,
onAddPurchase, onAddPurchase,
onAddMilestone onAddMilestone
@ -123,6 +131,40 @@ export default function StatsBox({
</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>
</div>
);
})}
</div>
</div>
)}
{/* Action Buttons */} {/* Action Buttons */}
<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="flex items-center justify-center space-x-4">

View file

@ -1,5 +1,4 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import InputError from '@/components/InputError'; import InputError from '@/components/InputError';

View file

@ -15,6 +15,12 @@ interface CurrentPrice {
current_price: number | null; current_price: number | null;
} }
interface Milestone {
target: number;
description: string;
created_at: string;
}
export default function Dashboard() { export default function Dashboard() {
const [purchaseData, setPurchaseData] = useState<PurchaseSummary>({ const [purchaseData, setPurchaseData] = useState<PurchaseSummary>({
total_shares: 0, total_shares: 0,
@ -26,18 +32,20 @@ export default function Dashboard() {
current_price: null, current_price: null,
}); });
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' | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Fetch purchase summary and current price // Fetch purchase summary, current price, and milestones
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
const [purchaseResponse, priceResponse] = await Promise.all([ const [purchaseResponse, priceResponse, milestonesResponse] = await Promise.all([
fetch('/purchases/summary'), fetch('/purchases/summary'),
fetch('/pricing/current'), fetch('/pricing/current'),
fetch('/milestones'),
]); ]);
if (purchaseResponse.ok) { if (purchaseResponse.ok) {
@ -49,6 +57,11 @@ export default function Dashboard() {
const price = await priceResponse.json(); const price = await priceResponse.json();
setPriceData(price); setPriceData(price);
} }
if (milestonesResponse.ok) {
const milestonesData = await milestonesResponse.json();
setMilestones(milestonesData);
}
} catch (error) { } catch (error) {
console.error('Failed to fetch data:', error); console.error('Failed to fetch data:', error);
} finally { } 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 // Calculate portfolio stats
const currentValue = priceData.current_price const currentValue = priceData.current_price
@ -141,7 +167,6 @@ export default function Dashboard() {
{/* Box 2: Progress Bar (toggleable) */} {/* Box 2: Progress Bar (toggleable) */}
<div className="mt-4" style={{ display: showProgressBar ? 'block' : 'none' }}> <div className="mt-4" style={{ display: showProgressBar ? 'block' : 'none' }}>
<ProgressBar <ProgressBar
value={purchaseData.total_shares}
onClick={handleProgressClick} onClick={handleProgressClick}
/> />
</div> </div>
@ -150,6 +175,7 @@ export default function Dashboard() {
<div className="mt-4" style={{ display: showStatsBox ? 'block' : 'none' }}> <div className="mt-4" style={{ display: showStatsBox ? 'block' : 'none' }}>
<StatsBox <StatsBox
stats={statsData} stats={statsData}
milestones={milestones}
onAddPurchase={() => setActiveForm('purchase')} onAddPurchase={() => setActiveForm('purchase')}
onAddMilestone={() => setActiveForm('milestone')} onAddMilestone={() => setActiveForm('milestone')}
/> />
@ -161,9 +187,7 @@ export default function Dashboard() {
type={activeForm} type={activeForm}
onClose={() => setActiveForm(null)} onClose={() => setActiveForm(null)}
onPurchaseSuccess={handlePurchaseSuccess} onPurchaseSuccess={handlePurchaseSuccess}
onMilestoneSuccess={() => { onMilestoneSuccess={handleMilestoneSuccess}
console.log('Milestone added successfully');
}}
/> />
</div> </div>
</div> </div>

View file

@ -0,0 +1,59 @@
<?php
namespace Tests\Feature;
use App\Models\Milestone;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class MilestoneTest extends TestCase
{
use RefreshDatabase;
public function test_can_create_milestone(): void
{
$milestone = Milestone::create([
'target' => 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']);
}
}