Fix milestone issues
This commit is contained in:
parent
2ea5107106
commit
6e08bd80ed
8 changed files with 193 additions and 58 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
21
app/Models/Milestone.php
Normal file
21
app/Models/Milestone.php
Normal 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',
|
||||
];
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
interface Milestone {
|
||||
target: number;
|
||||
description: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface StatsBoxProps {
|
||||
stats: {
|
||||
totalShares: number;
|
||||
|
|
@ -11,6 +17,7 @@ interface StatsBoxProps {
|
|||
profitLoss?: number;
|
||||
profitLossPercentage?: number;
|
||||
};
|
||||
milestones?: Milestone[];
|
||||
className?: string;
|
||||
onAddPurchase?: () => 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({
|
|||
</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 */}
|
||||
<div className="border-t border-red-500/30 pt-4">
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<PurchaseSummary>({
|
||||
total_shares: 0,
|
||||
|
|
@ -26,18 +32,20 @@ export default function Dashboard() {
|
|||
current_price: null,
|
||||
});
|
||||
|
||||
const [milestones, setMilestones] = useState<Milestone[]>([]);
|
||||
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) */}
|
||||
<div className="mt-4" style={{ display: showProgressBar ? 'block' : 'none' }}>
|
||||
<ProgressBar
|
||||
value={purchaseData.total_shares}
|
||||
onClick={handleProgressClick}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -150,6 +175,7 @@ export default function Dashboard() {
|
|||
<div className="mt-4" style={{ display: showStatsBox ? 'block' : 'none' }}>
|
||||
<StatsBox
|
||||
stats={statsData}
|
||||
milestones={milestones}
|
||||
onAddPurchase={() => 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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
59
tests/Feature/MilestoneTest.php
Normal file
59
tests/Feature/MilestoneTest.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue