173 lines
5.1 KiB
PHP
173 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Http\Requests\ValidateSelectionRequest;
|
|
use App\Models\MasterImage;
|
|
use App\Models\Reservation;
|
|
use App\Services\SelectionMapper;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class PublicGridController extends Controller
|
|
{
|
|
public function show()
|
|
{
|
|
$meta = $this->meta();
|
|
|
|
return inertia('PublicGrid/Index', [
|
|
'master_image_url' => $meta['master_image_url'],
|
|
'master_version' => $meta['master_version'],
|
|
'cell_size' => $meta['cell_size'],
|
|
]);
|
|
}
|
|
|
|
public function meta(): array
|
|
{
|
|
$masterPath = config('pixel_grid.master_path');
|
|
$url = Storage::disk('public')->url($masterPath);
|
|
|
|
$master = MasterImage::latest()->first();
|
|
|
|
$version = $master ? $master->version : 1;
|
|
|
|
return [
|
|
'master_image_url' => $url.'?v='.$version,
|
|
'master_version' => $version,
|
|
'cell_size' => (int) config('pixel_grid.cell_size'),
|
|
];
|
|
}
|
|
|
|
public function price(): array
|
|
{
|
|
return [
|
|
'price_per_cell' => (int) config('pixel_grid.price_per_cell'),
|
|
];
|
|
}
|
|
|
|
public function availability(): array
|
|
{
|
|
// Stub for MVP — returns empty array
|
|
return [
|
|
'occupied' => [],
|
|
];
|
|
}
|
|
|
|
public function validateSelection(ValidateSelectionRequest $request, SelectionMapper $mapper)
|
|
{
|
|
$data = $request->validated();
|
|
|
|
$offsetX = $data['offsetX'] ?? 0;
|
|
$offsetY = $data['offsetY'] ?? 0;
|
|
|
|
$cellSize = (int) config('pixel_grid.cell_size', 20);
|
|
|
|
$mapped = $mapper->mapPixelsToCells(
|
|
(float) $data['x'],
|
|
(float) $data['y'],
|
|
(float) $data['w'],
|
|
(float) $data['h'],
|
|
(float) $offsetX,
|
|
(float) $offsetY,
|
|
$cellSize
|
|
);
|
|
|
|
return response()->json(['selection' => $mapped]);
|
|
}
|
|
|
|
public function reserveSelection(ValidateSelectionRequest $request, SelectionMapper $mapper)
|
|
{
|
|
$data = $request->validated();
|
|
|
|
$offsetX = $data['offsetX'] ?? 0;
|
|
$offsetY = $data['offsetY'] ?? 0;
|
|
$cellSize = (int) config('pixel_grid.cell_size', 20);
|
|
|
|
$mapped = $mapper->mapPixelsToCells(
|
|
(float) $data['x'],
|
|
(float) $data['y'],
|
|
(float) $data['w'],
|
|
(float) $data['h'],
|
|
(float) $offsetX,
|
|
(float) $offsetY,
|
|
$cellSize
|
|
);
|
|
|
|
// Hold duration in minutes
|
|
$holdMinutes = (int) config('pixel_grid.hold_minutes', 5);
|
|
$now = Carbon::now();
|
|
$expiresAt = $now->copy()->addMinutes($holdMinutes);
|
|
|
|
// Transaction + FOR UPDATE style check for overlapping active reservations
|
|
$result = DB::transaction(function () use ($mapped, $expiresAt, $now) {
|
|
$overlaps = Reservation::where(function ($q) use ($mapped) {
|
|
$q->whereRaw('NOT (x + w <= ? OR x >= ? OR y + h <= ? OR y >= ?)', [
|
|
$mapped['x'],
|
|
$mapped['x'] + $mapped['w'],
|
|
$mapped['y'],
|
|
$mapped['y'] + $mapped['h'],
|
|
]);
|
|
})->where(function ($q) use ($now) {
|
|
$q->where('status', 'held')->where('reserved_until', '>', $now)
|
|
->orWhere('status', 'confirmed');
|
|
})->lockForUpdate()->exists();
|
|
|
|
if ($overlaps) {
|
|
return null;
|
|
}
|
|
|
|
$reservation = Reservation::create([
|
|
'x' => $mapped['x'],
|
|
'y' => $mapped['y'],
|
|
'w' => $mapped['w'],
|
|
'h' => $mapped['h'],
|
|
'status' => 'held',
|
|
'reserved_until' => $expiresAt,
|
|
]);
|
|
|
|
return $reservation;
|
|
});
|
|
|
|
if (! $result) {
|
|
return response()->json(['message' => 'Selection conflicts with an existing reservation'], 409);
|
|
}
|
|
|
|
return response()->json(['reservation_id' => $result->id, 'expires_at' => $result->reserved_until]);
|
|
}
|
|
|
|
public function confirmReservation(Request $request)
|
|
{
|
|
$id = $request->input('reservation_id');
|
|
|
|
if (! $id) {
|
|
return response()->json(['message' => 'reservation_id required'], 422);
|
|
}
|
|
|
|
$now = Carbon::now();
|
|
|
|
$reservation = Reservation::where('id', $id)->lockForUpdate()->first();
|
|
|
|
if (! $reservation) {
|
|
return response()->json(['message' => 'not_found'], 404);
|
|
}
|
|
|
|
if ($reservation->status !== 'held') {
|
|
return response()->json(['message' => 'invalid_status'], 409);
|
|
}
|
|
|
|
if ($reservation->reserved_until && $reservation->reserved_until->lessThanOrEqualTo($now)) {
|
|
$reservation->status = 'expired';
|
|
$reservation->save();
|
|
|
|
return response()->json(['message' => 'expired'], 409);
|
|
}
|
|
|
|
$reservation->status = 'confirmed';
|
|
$reservation->save();
|
|
|
|
return response()->json(['reservation_id' => $reservation->id, 'status' => 'confirmed']);
|
|
}
|
|
}
|