feat: establish reservation portal foundation

This commit is contained in:
2026-08-04 15:24:08 +01:00
commit c96264ae96
226 changed files with 32232 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Domain\Inventory\Models\SnipeAsset;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class AssetController extends Controller
{
public function index(Request $request): Response
{
$filters = $request->validate([
'search' => ['nullable', 'string', 'max:100'],
'category' => ['nullable', 'string', 'max:100'],
'location' => ['nullable', 'string', 'max:100'],
]);
$assets = SnipeAsset::query()
->where('active', true)
->where('requestable', true)
->when($filters['search'] ?? null, function ($query, string $search) {
$query->where(function ($query) use ($search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('asset_tag', 'like', "%{$search}%")
->orWhere('model_name', 'like', "%{$search}%");
});
})
->when($filters['category'] ?? null, fn ($query, string $category) => $query->where('category_name', $category))
->when($filters['location'] ?? null, fn ($query, string $location) => $query->where('location_name', $location))
->orderBy('name')
->paginate(12)
->withQueryString();
return Inertia::render('assets/index', [
'assets' => $assets,
'filters' => $filters,
'categories' => SnipeAsset::query()->where('active', true)->whereNotNull('category_name')->distinct()->orderBy('category_name')->pluck('category_name'),
'locations' => SnipeAsset::query()->where('active', true)->whereNotNull('location_name')->distinct()->orderBy('location_name')->pluck('location_name'),
'lastSyncedAt' => SnipeAsset::max('synced_at'),
]);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers;
use App\Domain\Reservations\Enums\ReservationStatus;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
public function __invoke(Request $request): Response
{
$query = $request->user()->reservations();
$next = (clone $query)
->with('items.asset:id,name,asset_tag,category_name')
->where('ends_at', '>', now())
->whereNotIn('status', [ReservationStatus::Rejected, ReservationStatus::Cancelled, ReservationStatus::Expired])
->orderBy('starts_at')
->first();
return Inertia::render('dashboard', [
'summary' => [
'pending' => (clone $query)->where('status', ReservationStatus::PendingApproval)->count(),
'approved' => (clone $query)->whereIn('status', [ReservationStatus::Approved, ReservationStatus::Ready])->count(),
'inUse' => (clone $query)->whereIn('status', [ReservationStatus::Delivered, ReservationStatus::InUse, ReservationStatus::Overdue])->count(),
],
'nextReservation' => $next,
'recentReservations' => (clone $query)
->with('items.asset:id,name,asset_tag')
->latest()
->limit(5)
->get(),
]);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers;
use App\Domain\Inventory\Models\SnipeAsset;
use App\Domain\Reservations\Services\ReservationService;
use App\Http\Requests\StoreReservationRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class ReservationController extends Controller
{
public function index(Request $request): Response
{
return Inertia::render('reservations/index', [
'reservations' => $request->user()->reservations()
->with('items.asset:id,name,asset_tag')
->latest()
->paginate(15),
]);
}
public function create(Request $request): Response
{
return Inertia::render('reservations/create', [
'assets' => SnipeAsset::query()
->where('active', true)
->where('requestable', true)
->whereNull('assigned_id')
->orderBy('name')
->get(['id', 'name', 'asset_tag', 'model_name', 'category_name', 'location_name']),
'selectedAssetId' => $request->integer('asset'),
]);
}
public function store(StoreReservationRequest $request, ReservationService $service): RedirectResponse
{
/** @var array{purpose:string, usage_location:string, starts_at:string, ends_at:string, asset_ids:list<int>} $data */
$data = $request->safe()->only(['purpose', 'usage_location', 'starts_at', 'ends_at', 'asset_ids']);
$reservation = $service->createPending($request->user(), $data);
return to_route('dashboard')->with('success', "Requisição {$reservation->reference} submetida para aprovação.");
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\Settings\ProfileDeleteRequest;
use App\Http\Requests\Settings\ProfileUpdateRequest;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Show the user's profile settings page.
*/
public function edit(Request $request): Response
{
return Inertia::render('settings/profile', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => $request->session()->get('status'),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
Inertia::flash('toast', ['type' => 'success', 'message' => __('Profile updated.')]);
return to_route('profile.edit');
}
/**
* Delete the user's profile.
*/
public function destroy(ProfileDeleteRequest $request): RedirectResponse
{
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\Settings\PasswordUpdateRequest;
use App\Http\Requests\Settings\TwoFactorAuthenticationRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Validation\Rules\Password;
use Inertia\Inertia;
use Inertia\Response;
use Laravel\Fortify\Features;
class SecurityController extends Controller
{
/**
* Show the user's security settings page.
*/
public function edit(TwoFactorAuthenticationRequest $request): Response
{
$props = [
'canManageTwoFactor' => Features::canManageTwoFactorAuthentication(),
'canManagePasskeys' => Features::canManagePasskeys(),
'passkeys' => Features::canManagePasskeys()
? $request->user()
->passkeys()
->select(['id', 'name', 'credential', 'created_at', 'last_used_at'])
->latest()
->get()
->map(fn ($passkey) => [
'id' => $passkey->id,
'name' => $passkey->name,
'authenticator' => $passkey->authenticator,
'created_at_diff' => $passkey->created_at->diffForHumans(),
'last_used_at_diff' => $passkey->last_used_at?->diffForHumans(),
])
->values()
->all()
: [],
'passwordRules' => Password::defaults()->toPasswordRulesString(),
];
if (Features::canManageTwoFactorAuthentication()) {
$request->ensureStateIsValid();
$props['twoFactorEnabled'] = $request->user()->hasEnabledTwoFactorAuthentication();
$props['requiresConfirmation'] = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm');
}
return Inertia::render('settings/security', $props);
}
/**
* Update the user's password.
*/
public function update(PasswordUpdateRequest $request): RedirectResponse
{
$request->user()->update([
'password' => $request->password,
]);
Inertia::flash('toast', ['type' => 'success', 'message' => __('Password updated.')]);
return back();
}
}