Basic milestones

This commit is contained in:
myrmidex 2025-07-12 18:09:11 +02:00
parent b469423d81
commit 3c00e5732f
6 changed files with 193 additions and 2 deletions

View file

@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Milestones;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class MilestoneController extends Controller
{
/**
* Store a new milestone.
*/
public function store(Request $request): JsonResponse
{
$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);
}
/**
* Get all milestones.
*/
public function index(): JsonResponse
{
// For now, return empty array
// Later this could fetch from database
return response()->json([]);
}
}

View file

@ -12,6 +12,7 @@ interface LedCounterProps {
onStatsToggle?: () => void; onStatsToggle?: () => void;
showStats?: boolean; showStats?: boolean;
onAddPurchase?: () => void; onAddPurchase?: () => void;
onAddMilestone?: () => void;
} }
export default function LedCounter({ export default function LedCounter({
@ -22,7 +23,8 @@ export default function LedCounter({
onHover, onHover,
onStatsToggle, onStatsToggle,
showStats = false, showStats = false,
onAddPurchase onAddPurchase,
onAddMilestone
}: LedCounterProps) { }: LedCounterProps) {
const [displayValue, setDisplayValue] = useState(0); const [displayValue, setDisplayValue] = useState(0);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@ -260,7 +262,7 @@ export default function LedCounter({
</div> </div>
</div> </div>
{/* Right: Add Purchase, Next milestone button and stats toggle */} {/* Right: Add Purchase, Add Milestone, Next milestone button and stats toggle */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{/* Add Purchase Button */} {/* Add Purchase Button */}
{onAddPurchase && ( {onAddPurchase && (
@ -274,6 +276,18 @@ export default function LedCounter({
</button> </button>
)} )}
{/* Add Milestone Button */}
{onAddMilestone && (
<button
onClick={onAddMilestone}
className="flex items-center space-x-1 px-2 py-1 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-xs"
aria-label="Add milestone"
>
<Plus className="w-3 h-3" />
<span className="font-mono">MILE</span>
</button>
)}
<button <button
onClick={nextMilestone} onClick={nextMilestone}
className="text-red-400 hover:text-red-300 transition-colors p-1" className="text-red-400 hover:text-red-300 transition-colors p-1"

View file

@ -0,0 +1,33 @@
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
interface MilestoneModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess?: () => void;
}
export default function MilestoneModal({ isOpen, onClose, onSuccess }: MilestoneModalProps) {
const handleSuccess = () => {
if (onSuccess) {
onSuccess();
}
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="bg-black border-red-500/30 text-red-400 max-w-md">
<DialogHeader>
<DialogTitle className="text-red-500 font-mono tracking-wide">
ADD MILESTONE
</DialogTitle>
</DialogHeader>
<div className="mt-4">
<AddMilestoneForm onSuccess={handleSuccess} />
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,82 @@
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import InputError from '@/components/InputError';
import { useForm } from '@inertiajs/react';
import { LoaderCircle } from 'lucide-react';
import { FormEventHandler } from 'react';
interface MilestoneFormData {
target: string;
description: string;
[key: string]: string;
}
interface AddMilestoneFormProps {
onSuccess?: () => void;
}
export default function AddMilestoneForm({ onSuccess }: AddMilestoneFormProps) {
const { data, setData, post, processing, errors, reset } = useForm<MilestoneFormData>({
target: '',
description: '',
});
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('milestones.store'), {
onSuccess: () => {
reset();
if (onSuccess) {
onSuccess();
}
},
});
};
return (
<div className="w-full max-w-md">
<div className="space-y-4">
<form onSubmit={submit} className="space-y-4">
<div>
<Label htmlFor="target" className="text-red-400">Target Number</Label>
<Input
id="target"
type="number"
step="1"
min="1"
placeholder="1500"
value={data.target}
onChange={(e) => setData('target', e.target.value)}
className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30"
/>
<InputError message={errors.target} />
</div>
<div>
<Label htmlFor="description" className="text-red-400">Description</Label>
<Input
id="description"
type="text"
placeholder="First milestone"
value={data.description}
onChange={(e) => setData('description', e.target.value)}
className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30"
/>
<InputError message={errors.description} />
</div>
<Button
type="submit"
disabled={processing}
className="w-full bg-red-600 hover:bg-red-700 text-white border-red-500"
>
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
Add Milestone
</Button>
</form>
</div>
</div>
);
}

View file

@ -1,4 +1,5 @@
import LedCounter from '@/components/Display/LedCounter'; import LedCounter from '@/components/Display/LedCounter';
import MilestoneModal from '@/components/Display/MilestoneModal';
import PurchaseModal from '@/components/Display/PurchaseModal'; import PurchaseModal from '@/components/Display/PurchaseModal';
import StatsPanel from '@/components/Display/StatsPanel'; import StatsPanel from '@/components/Display/StatsPanel';
import { Head } from '@inertiajs/react'; import { Head } from '@inertiajs/react';
@ -27,6 +28,7 @@ export default function Dashboard() {
const [showStats, setShowStats] = useState(false); const [showStats, setShowStats] = useState(false);
const [showPurchaseModal, setShowPurchaseModal] = useState(false); const [showPurchaseModal, setShowPurchaseModal] = useState(false);
const [showMilestoneModal, setShowMilestoneModal] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Fetch purchase summary and current price // Fetch purchase summary and current price
@ -120,6 +122,7 @@ export default function Dashboard() {
onStatsToggle={() => setShowStats(!showStats)} onStatsToggle={() => setShowStats(!showStats)}
showStats={showStats} showStats={showStats}
onAddPurchase={() => setShowPurchaseModal(true)} onAddPurchase={() => setShowPurchaseModal(true)}
onAddMilestone={() => setShowMilestoneModal(true)}
/> />
{/* Stats Panel */} {/* Stats Panel */}
@ -134,6 +137,16 @@ export default function Dashboard() {
onClose={() => setShowPurchaseModal(false)} onClose={() => setShowPurchaseModal(false)}
onSuccess={handlePurchaseSuccess} onSuccess={handlePurchaseSuccess}
/> />
{/* Milestone Modal */}
<MilestoneModal
isOpen={showMilestoneModal}
onClose={() => setShowMilestoneModal(false)}
onSuccess={() => {
// Could refresh milestone data here if needed
console.log('Milestone added successfully');
}}
/>
</div> </div>
</> </>
); );

View file

@ -2,6 +2,7 @@
use App\Http\Controllers\Transactions\PurchaseController; use App\Http\Controllers\Transactions\PurchaseController;
use App\Http\Controllers\Pricing\PricingController; use App\Http\Controllers\Pricing\PricingController;
use App\Http\Controllers\Milestones\MilestoneController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Inertia\Inertia; use Inertia\Inertia;
@ -29,5 +30,11 @@
Route::get('/date/{date}', [PricingController::class, 'forDate'])->name('for-date'); Route::get('/date/{date}', [PricingController::class, 'forDate'])->name('for-date');
}); });
// Milestone routes
Route::prefix('milestones')->name('milestones.')->group(function () {
Route::get('/', [MilestoneController::class, 'index'])->name('index');
Route::post('/', [MilestoneController::class, 'store'])->name('store');
});
require __DIR__.'/settings.php'; require __DIR__.'/settings.php';
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';