Basic milestones
This commit is contained in:
parent
b469423d81
commit
3c00e5732f
6 changed files with 193 additions and 2 deletions
42
app/Http/Controllers/Milestones/MilestoneController.php
Normal file
42
app/Http/Controllers/Milestones/MilestoneController.php
Normal 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([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 && (
|
||||||
|
|
@ -273,6 +275,18 @@ export default function LedCounter({
|
||||||
<span className="font-mono">ADD</span>
|
<span className="font-mono">ADD</span>
|
||||||
</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}
|
||||||
|
|
|
||||||
33
resources/js/components/Display/MilestoneModal.tsx
Normal file
33
resources/js/components/Display/MilestoneModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
resources/js/components/Milestones/AddMilestoneForm.tsx
Normal file
82
resources/js/components/Milestones/AddMilestoneForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue