diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 4ccb2c33..d856e80a 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -109,14 +109,14 @@ public function postQuickcode(QuickCodeRequest $request): JsonResponse|RedirectR return $request->expectsJson() ? response()->json(null, 205) - : redirect()->route('tracker.index'); + : redirect()->route('volunteer.index'); } /** * Display the banned notice */ public function getBanned(): InertiaResponse|RedirectResponse { - if (!Auth::user()?->isBanned()) return redirect()->route('tracker.index'); + if (!Auth::user()?->isBanned()) return redirect()->route('volunteer.index'); return Inertia::render('Suspended'); } diff --git a/app/Http/Controllers/DepartmentController.php b/app/Http/Controllers/DepartmentController.php index 7afaa615..835cccc6 100644 --- a/app/Http/Controllers/DepartmentController.php +++ b/app/Http/Controllers/DepartmentController.php @@ -5,25 +5,33 @@ use App\Http\Requests\DepartmentStoreRequest; use App\Http\Requests\DepartmentUpdateRequest; use App\Models\Department; +use App\Models\Event; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; class DepartmentController extends Controller { /** * Display a listing of the resource. */ - public function index(): JsonResponse { - $this->authorize('viewAny', Department::class); - return response()->json(['departments' => Department::all()]); + public function index(Request $request, Event $event): JsonResponse|RedirectResponse { + $this->authorize('viewForEvent', [Department::class, $event]); + return $request->expectsJson() + ? response()->json(['departments' => $event->departments]) + : redirect()->route('events.show', $event); } /** * Store a newly created resource in storage. */ - public function store(DepartmentStoreRequest $request): JsonResponse { + public function store(DepartmentStoreRequest $request, Event $event): JsonResponse|RedirectResponse { $department = new Department($request->validated()); + $department->event_id = $event->id; $department->save(); - session()->flash('success', 'Department created.'); - return response()->json(['department' => $department]); + + return $request->expectsJson() + ? response()->json(['department' => $department]) + : redirect()->back()->withSuccess("Created department {$department->name}."); } /** @@ -37,21 +45,27 @@ public function show(Department $department): JsonResponse { /** * Update the specified resource in storage. */ - public function update(DepartmentUpdateRequest $request, Department $department): JsonResponse { + public function update(DepartmentUpdateRequest $request, Department $department): JsonResponse|RedirectResponse { $department->update($request->validated()); - return response()->json(['department' => $department]); + + return $request->expectsJson() + ? response()->json(['department' => $department]) + : redirect()->back()->withSuccess("Updated department {$department->name}."); } /** * Remove the specified resource from storage. */ - public function destroy(Department $department): JsonResponse { + public function destroy(Request $request, Department $department): JsonResponse|RedirectResponse { $this->authorize('delete', $department); // TODO: Once determination is made on what to do with soft-deletables, we may want to ensure relations get // deleted or soft-deleted along with the parent. At the moment, we just try to gracefully handle the parent, // Department in this case, being soft-deleted. $department->delete(); - return response()->json(null, 205); + + return $request->expectsJson() + ? response()->json(null, 205) + : redirect()->back()->withSuccess("Deleted department {$department->name}."); } } diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index a1a33844..970e155a 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -4,54 +4,120 @@ use App\Http\Requests\EventStoreRequest; use App\Http\Requests\EventUpdateRequest; +use App\Models\Department; use App\Models\Event; +use App\Models\Reward; +use App\Models\Setting; +use App\Models\TimeBonus; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Inertia\Inertia; +use Inertia\Response as InertiaResponse; class EventController extends Controller { /** * Display a listing of the resource. */ - public function index(): JsonResponse { + public function index(Request $request): JsonResponse|InertiaResponse|RedirectResponse { $this->authorize('viewAny', Event::class); - return response()->json(['events' => Event::all()]); + + if ($request->expectsJson()) return response()->json(['events' => Event::all()]); + + $event = Setting::activeEvent(); + if ($event) { + $request->session()->reflash(); + return redirect()->route('events.show', [$event->id]); + } + + return Inertia::render('EventCrud', [ + 'event' => null, + 'events' => Event::orderBy('name')->get(), + 'departments' => null, + 'rewards' => null, + 'bonuses' => null, + ]); } /** * Store a newly created resource in storage. */ - public function store(EventStoreRequest $request): JsonResponse { + public function store(EventStoreRequest $request): JsonResponse|RedirectResponse { $event = new Event($request->validated()); - $event->save(); - session()->flash('success', 'Event created.'); - return response()->json(['event' => $event]); + $event->id = $event->newUniqueId(); + + // Clone departments from another event and use a transaction to store them and the event itself together + if ($request->has('cloneEvent')) { + $newDepartments = []; + $sourceDepartments = Department::forEvent($request->input('cloneEvent'))->get(['name', 'hidden']); + foreach ($sourceDepartments as $source) { + $newDepartments[] = ['name' => $source->name, 'hidden' => $source->hidden, 'event_id' => $event->id]; + } + + DB::transaction(function () use ($event, $newDepartments) { + $event->save(); + Department::fillAndInsert($newDepartments); + }); + } else { + $event->save(); + } + + return $request->expectsJson() + ? response()->json(['event' => $event]) + : redirect()->route('events.show', [$event->id])->withSuccess("Created event {$event->name}."); } /** * Display the specified resource. */ - public function show(Event $event): JsonResponse { + public function show(Request $request, Event $event): JsonResponse|InertiaResponse { $this->authorize('view', $event); - return response()->json(['event' => $event]); + $user = $request->user(); + + return $request->expectsJson() + ? response()->json(['event' => $event]) + : Inertia::render('EventCrud', [ + 'event' => $event, + 'events' => fn () => $user->can('viewAny', Event::class) ? Event::orderBy('name')->get() : null, + 'departments' => fn () => $user->can('viewForEvent', [Department::class, $event]) ? $event->departments : null, + 'rewards' => fn () => $user->can('viewForEvent', [Reward::class, $event]) ? $event->rewards : null, + 'bonuses' => fn () => $user->can('viewForEvent', [TimeBonus::class, $event]) + ? $event->timeBonuses() + ->with('departments', fn ($query) => $query->select('id')) + ->get() + ->map(fn ($bonus) => $bonus->toArrayWithDepartmentIds()) + : null, + ]); } /** * Update the specified resource in storage. */ - public function update(EventUpdateRequest $request, Event $event): JsonResponse { + public function update(EventUpdateRequest $request, Event $event): JsonResponse|RedirectResponse { $event->update($request->validated()); - return response()->json(['event' => $event]); + return $request->expectsJson() + ? response()->json(['event' => $event]) + : redirect()->back()->withSuccess('Renamed event.'); } /** * Remove the specified resource from storage. */ - public function destroy(Event $event): JsonResponse { + public function destroy(Request $request, Event $event): JsonResponse|RedirectResponse { $this->authorize('delete', $event); + $isActive = $event->isActive(); + // TODO: Once determination is made on what to do with soft-deletables, we may want to ensure relations get // deleted or soft-deleted along with the parent. At the moment, we just try to gracefully handle the parent, // Event in this case, being soft-deleted. $event->delete(); - return response()->json(null, 205); + + if ($isActive) Setting::set('active-event', null); + + return $request->expectsJson() + ? response()->json(null, 205) + : redirect()->route('events.index')->withSuccess("Deleted event {$event->name}."); } } diff --git a/app/Http/Controllers/ManagementController.php b/app/Http/Controllers/ManagementController.php index 16bb7a6c..847f64e8 100644 --- a/app/Http/Controllers/ManagementController.php +++ b/app/Http/Controllers/ManagementController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers; use App\Models\Activity; -use App\Models\Department; use App\Models\Event; use App\Models\Reward; use App\Models\Setting; @@ -48,7 +47,9 @@ public function getManageIndex(?Event $event = null, ?User $user = null): Respon 'event' => $event, 'events' => fn () => Event::orderBy('name')->get(), 'rewards' => fn () => Reward::forEvent($event)->orderBy('hours')->get(), - 'departments' => fn () => Department::orderBy('hidden')->orderBy('name')->get(), + 'departments' => fn () => $event + ? $event->departments()->orderBy('hidden')->orderBy('name')->get() + : null, 'ongoingEntries' => Inertia::defer( fn () => TimeEntry::with(['user', 'department']) ->forEvent($event) @@ -69,55 +70,6 @@ public function getManageIndex(?Event $event = null, ?User $user = null): Respon ]); } - /** - * Render the departments admin page - */ - public function getAdminDepartments(): View { - return view('admin.departments', ['departments' => Department::all()]); - } - - /** - * Render the events admin page - */ - public function getAdminEvents(): View { - return view('admin.events', ['events' => Event::all()]); - } - - /** - * Render the rewards admin page - */ - public function getAdminRewards(?Event $event = null): View|RedirectResponse { - // Get the event and redirect to the page for the active event, if applicable - if (!$event) { - $event = Setting::activeEvent(); - if ($event) return redirect()->route('admin.event.rewards', $event); - } - - return view('admin.rewards', [ - 'event' => $event, - 'events' => Event::all(), - 'rewards' => $event?->rewards, - ]); - } - - /** - * Render the bonuses admin page - */ - public function getAdminBonuses(?Event $event = null): View|RedirectResponse { - // Get the event and redirect to the page for the active event, if applicable - if (!$event) { - $event = Setting::activeEvent(); - if ($event) return redirect()->route('admin.event.bonuses', $event); - } - - return view('admin.bonuses', [ - 'event' => $event, - 'events' => Event::all(), - 'departments' => Department::all()->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE), - 'bonuses' => $event?->timeBonuses()?->with('departments')?->get(), - ]); - } - /** * Render the reports list admin page */ diff --git a/app/Http/Controllers/NotificationController.php b/app/Http/Controllers/NotificationController.php index cd99a83c..658eee20 100644 --- a/app/Http/Controllers/NotificationController.php +++ b/app/Http/Controllers/NotificationController.php @@ -42,6 +42,6 @@ public function postAcknowledge(Request $request): JsonResponse|RedirectResponse $user->unreadNotifications()->update(['read_at' => now()]); return $request->expectsJson() ? response()->json(null, 205) - : redirect()->route('tracker.index'); + : redirect()->route('volunteer.index'); } } diff --git a/app/Http/Controllers/RewardController.php b/app/Http/Controllers/RewardController.php index 53ea29b0..e5f22ac1 100644 --- a/app/Http/Controllers/RewardController.php +++ b/app/Http/Controllers/RewardController.php @@ -7,25 +7,31 @@ use App\Models\Event; use App\Models\Reward; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; class RewardController extends Controller { /** * Display a listing of the resource. */ - public function index(Event $event): JsonResponse { - $this->authorize('viewAny', Reward::class); - return response()->json(['rewards' => $event->rewards]); + public function index(Request $request, Event $event): JsonResponse|RedirectResponse { + $this->authorize('viewForEvent', [Reward::class, $event]); + return $request->expectsJson() + ? response()->json(['rewards' => $event->rewards]) + : redirect()->route('events.show', $event); } /** * Store a newly created resource in storage. */ - public function store(RewardStoreRequest $request, Event $event): JsonResponse { + public function store(RewardStoreRequest $request, Event $event): JsonResponse|RedirectResponse { $reward = new Reward($request->validated()); $reward->event_id = $event->id; $reward->save(); - session()->flash('success', 'Reward created.'); - return response()->json(['reward' => $reward]); + + return $request->expectsJson() + ? response()->json(['reward' => $reward]) + : redirect()->back()->withSuccess("Created reward {$reward->name}."); } /** @@ -39,17 +45,24 @@ public function show(Reward $reward): JsonResponse { /** * Update the specified resource in storage. */ - public function update(RewardUpdateRequest $request, Reward $reward): JsonResponse { + public function update(RewardUpdateRequest $request, Reward $reward): JsonResponse|RedirectResponse { $reward->update($request->validated()); - return response()->json(['reward' => $reward]); + + return $request->expectsJson() + ? response()->json(['reward' => $reward]) + : redirect()->back()->withSuccess("Updated reward {$reward->name}."); } /** * Remove the specified resource from storage. */ - public function destroy(Reward $reward): JsonResponse { + public function destroy(Request $request, Reward $reward): JsonResponse|RedirectResponse { $this->authorize('delete', $reward); + $reward->delete(); - return response()->json(null, 205); + + return $request->expectsJson() + ? response()->json(null, 205) + : redirect()->back()->withSuccess("Deleted reward {$reward->name}."); } } diff --git a/app/Http/Controllers/TimeBonusController.php b/app/Http/Controllers/TimeBonusController.php index e72f30aa..3ac2f2c1 100644 --- a/app/Http/Controllers/TimeBonusController.php +++ b/app/Http/Controllers/TimeBonusController.php @@ -7,26 +7,37 @@ use App\Models\Event; use App\Models\TimeBonus; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Support\Carbon; class TimeBonusController extends Controller { /** * Display a listing of the resource. */ - public function index(Event $event): JsonResponse { - $this->authorize('viewAny', TimeBonus::class); - return response()->json(['bonuses' => $event->timeBonuses()->with('departments')->get()]); + public function index(Request $request, Event $event): JsonResponse|RedirectResponse { + $this->authorize('viewForEvent', [TimeBonus::class, $event]); + return $request->expectsJson() + ? response()->json(['bonuses' => $event->timeBonuses()->with('departments')->get()]) + : redirect()->route('events.show', $event); } /** * Store a newly created resource in storage. */ - public function store(TimeBonusStoreRequest $request, Event $event): JsonResponse { - $bonus = new TimeBonus($request->safe()->except('departments')); + public function store(TimeBonusStoreRequest $request, Event $event): JsonResponse|RedirectResponse { + $bonus = new TimeBonus; + $bonus->start = $request->date('start')->timezone(config('app.timezone')); + $bonus->stop = $request->date('stop')->timezone(config('app.timezone')); + $bonus->modifier = $request->float('modifier'); $bonus->event_id = $event->id; $bonus->save(); + $bonus->departments()->sync($request->validated('departments')); - session()->flash('success', 'Time bonus created.'); - return response()->json(['bonus' => $bonus]); + + return $request->expectsJson() + ? response()->json(['bonus' => $bonus]) + : redirect()->back()->withSuccess('Created time bonus.'); } /** @@ -40,18 +51,32 @@ public function show(TimeBonus $bonus): JsonResponse { /** * Update the specified resource in storage. */ - public function update(TimeBonusUpdateRequest $request, TimeBonus $bonus): JsonResponse { - $bonus->update($request->safe()->except('departments')); - $bonus->departments()->sync($request->validated('departments')); - return response()->json(['bonus' => $bonus]); + public function update(TimeBonusUpdateRequest $request, TimeBonus $bonus): JsonResponse|RedirectResponse { + $changes = $request->validated(); + $departments = isset($changes['departments']) ? $changes['departments'] : null; + unset($changes['departments']); + + if (isset($changes['start'])) $changes['start'] = Carbon::parse($changes['start'])->timezone(config('app.timezone')); + if (isset($changes['stop'])) $changes['stop'] = Carbon::parse($changes['stop'])->timezone(config('app.timezone')); + $bonus->update($changes); + + if ($departments !== null) $bonus->departments()->sync($request->validated('departments')); + + return $request->expectsJson() + ? response()->json(['bonus' => $bonus]) + : redirect()->back()->withSuccess('Updated time bonus.'); } /** * Remove the specified resource from storage. */ - public function destroy(TimeBonus $bonus): JsonResponse { + public function destroy(Request $request, TimeBonus $bonus): JsonResponse|RedirectResponse { $this->authorize('delete', $bonus); + $bonus->delete(); - return response()->json(null, 205); + + return $request->expectsJson() + ? response()->json(null, 205) + : redirect()->back()->withSuccess('Deleted time bonus.'); } } diff --git a/app/Http/Controllers/TrackerController.php b/app/Http/Controllers/TrackerController.php deleted file mode 100644 index 67cef715..00000000 --- a/app/Http/Controllers/TrackerController.php +++ /dev/null @@ -1,174 +0,0 @@ -unreadNotifications()->exists()) return redirect()->route('notifications.index'); - - return Inertia::render('VolunteerHome', [ - 'volunteer' => fn () => $user->getVolunteerInfo(), - 'departments' => fn () => $user->isManager() ? Department::all() : Department::whereHidden(false)->get(), - 'rewards' => fn () => Setting::activeEvent()?->rewards, - 'telegramSetupUrl' => fn () => $user->getTelegramSetupUrl(), - 'hasTelegram' => fn () => !empty($user->tg_chat_id), - ]); - } - - /** - * Check in (start a time entry) for a department - */ - public function postCheckIn(CheckInRequest $request): JsonResponse|RedirectResponse { - /** @var User */ - $user = Auth::user(); - - // Make sure there's an active event - $event = Setting::activeEvent(); - if (!$event) { - $error = 'There is no active event to check in to.'; - return $request->expectsJson() - ? response()->json(['error' => $error], 409) - : redirect()->back()->withError($error)->withInput(); - } - - // Ensure there isn't already an ongoing time entry - if ($user->timeEntries()->forEvent()->ongoing()->exists()) { - $error = 'Cannot check in with an already-ongoing time entry.'; - return $request->expectsJson() - ? response()->json(['error' => $error], 409) - : redirect()->back()->withError($error)->withInput(); - } - - // Create a new time entry - $entry = new TimeEntry([ - 'user_id' => $user->id, - 'creator_user_id' => $user->id, - 'event_id' => $event->id, - 'department_id' => $request->validated('department_id'), - 'start' => now(), - ]); - $entry->save(); - - return $request->expectsJson() - ? response()->json(['time_entry' => $entry]) - : redirect()->route('tracker.index'); - } - - /** - * Check out for the ongoing time entry - */ - public function postCheckOut(Request $request, ?TimeEntry $timeEntry = null): JsonResponse|RedirectResponse { - /** @var User */ - $user = Auth::user(); - - if (!$timeEntry) { - // Make sure there's an active event - $event = Setting::activeEvent(); - if (!$event) { - $error = 'There is no active event to check in to.'; - return $request->expectsJson() - ? response()->json(['error' => $error], 409) - : redirect()->back()->withError($error)->withInput(); - } - - // Get the ongoing time entry - $timeEntry = $user->timeEntries()->forEvent()->ongoing()->first(); - if (!$timeEntry) { - $error = 'There is no ongoing time entry to check out for.'; - return $request->expectsJson() - ? response()->json(['error' => $error], 409) - : redirect()->back()->withError($error)->withInput(); - } - } - - // End the time entry if the user's permitted - $this->authorize('update', $timeEntry); - $timeEntry->stop = now(); - $timeEntry->save(); - - return $request->expectsJson() - ? response()->json(['time_entry' => $timeEntry, 'stats' => $user->getTimeStats()]) - : redirect()->route('tracker.index'); - } - - /** - * Get time statistics for a user - */ - public function getStats(User $user, ?Event $event = null): JsonResponse { - $this->authorize('viewAny', [TimeEntry::class, $user]); - return response()->json(['time' => $user->getTimeStats($event)]); - } - - /** - * Create a time entry for a user - */ - public function storeTimeEntry(TimeEntryStoreRequest $request, User $user): JsonResponse { - // Make sure we have an event - $input = $request->safe(); - if (!isset($input['event_id'])) $input['event_id'] = Setting::activeEvent()?->id; - if (!$input['event_id']) return response()->json(['error' => 'No event to create time entry for.'], 409); - - // Authorize the creation based on the target event - $event = Event::findOrFail($input['event_id']); - $this->authorize('create', [TimeEntry::class, $user, $event]); - - // Don't allow an ongoing entry to be created if there already is one - if (!isset($input['stop'])) { - if ($user->timeEntries()->ongoing()->forEvent($input['event_id'])->exists()) { - return response()->json(['error' => 'User already has an ongoing time entry.'], 409); - } - } - - // Create the time entry - $entry = new TimeEntry; - $entry->start = isset($input['start']) ? Carbon::parse($input['start'])->timezone(config('app.timezone')) : now(); - $entry->stop = isset($input['stop']) ? Carbon::parse($input['stop'])->timezone(config('app.timezone')) : null; - $entry->notes = $input['notes'] ?? null; - $entry->event_id = $input['event_id']; - $entry->department_id = $input['department_id']; - $entry->user_id = $user->id; - $entry->creator_user_id = $request->user()->id; - $entry->save(); - - return response()->json(['time_entry' => $entry, 'raw' => Carbon::parse($input['start'])]); - } - - /** - * Delete a time entry - */ - public function destroyTimeEntry(TimeEntry $timeEntry) { - $this->authorize('delete', $timeEntry); - $timeEntry->delete(); - return response()->json(null, 205); - } - - /** - * Display the lockdown notice - */ - public function getLockdown(): InertiaResponse|RedirectResponse { - if (!Setting::isLockedDown()) return redirect()->route('tracker.index'); - return Inertia::render('Maintenance'); - } -} diff --git a/app/Http/Controllers/VolunteerController.php b/app/Http/Controllers/VolunteerController.php new file mode 100644 index 00000000..9b49b49b --- /dev/null +++ b/app/Http/Controllers/VolunteerController.php @@ -0,0 +1,135 @@ +unreadNotifications()->exists()) return redirect()->route('notifications.index'); + + return Inertia::render('VolunteerHome', [ + 'volunteer' => fn () => $user->getVolunteerInfo(), + 'departments' => fn () => $user->isManager() + ? Setting::activeEvent()?->departments()?->orderBy('name')?->get() + : Setting::activeEvent()?->departments()?->orderBy('name')?->whereHidden(false)?->get(), + 'rewards' => fn () => Setting::activeEvent()?->rewards, + 'telegramSetupUrl' => fn () => $user->getTelegramSetupUrl(), + 'hasTelegram' => fn () => !empty($user->tg_chat_id), + ]); + } + + /** + * Check in (start a time entry) for a department + */ + public function checkIn(CheckInRequest $request, Event $event): JsonResponse|RedirectResponse { + /** @var User */ + $user = Auth::user(); + + // Ensure there isn't already an ongoing time entry + if ($user->timeEntries()->forEvent($event)->ongoing()->exists()) { + $error = 'Cannot check in with an already-ongoing time entry.'; + return $request->expectsJson() + ? response()->json(['error' => $error], 409) + : redirect()->back()->withError($error); + } + + // Create a new time entry + $entry = new TimeEntry($request->validated()); + $entry->start = now(); + $entry->user_id = $user->id; + $entry->creator_user_id = $user->id; + $entry->event_id = $event->id; + $entry->save(); + + return $request->expectsJson() + ? response()->json(['time_entry' => $entry]) + : redirect()->route('volunteer.index'); + } + + /** + * Check out an ongoing time entry + */ + public function checkOut(Request $request, TimeEntry $timeEntry): JsonResponse|RedirectResponse { + $this->authorize('update', $timeEntry); + + // Ensure the time entry is actually ongoing + if (!$timeEntry->isOngoing()) { + $error = 'The time entry has already been checked out.'; + return $request->expectsJson() + ? response()->json(['error' => $error], 409) + : redirect()->back()->withError($error); + } + + // End the time entry + $timeEntry->stop = now(); + $timeEntry->save(); + + return $request->expectsJson() + ? response()->json(['time_entry' => $timeEntry, 'stats' => $timeEntry->user->getTimeStats()]) + : redirect()->route('volunteer.index'); + } + + /** + * Create a time entry for a user + */ + public function storeTimeEntry(TimeEntryStoreRequest $request, Event $event, User $user): JsonResponse { + // Don't allow an ongoing entry to be created if there already is one + $input = $request->safe(); + if (!isset($input['stop'])) { + if ($user->timeEntries()->ongoing()->forEvent($event)->exists()) { + return response()->json(['error' => 'User already has an ongoing time entry.'], 409); + } + } + + // Create the time entry + $entry = new TimeEntry($request->validated()); + $entry->user_id = $user->id; + $entry->creator_user_id = $request->user()->id; + $entry->event_id = $event->id; + $entry->save(); + + return $request->expectsJson() + ? response()->json(['time_entry' => $entry]) + : redirect()->back()->withSuccess('Created time entry.'); + } + + /** + * Delete a time entry + */ + public function destroyTimeEntry(Request $request, TimeEntry $timeEntry): JsonResponse { + $this->authorize('delete', $timeEntry); + + $timeEntry->delete(); + + return $request->expectsJson() + ? response()->json(null, 205) + : redirect()->back()->withSuccess('Deleted time entry.'); + } + + /** + * Display the lockdown notice + */ + public function lockdown(): InertiaResponse|RedirectResponse { + if (!Setting::isLockedDown()) return redirect()->route('volunteer.index'); + return Inertia::render('Maintenance'); + } +} diff --git a/app/Http/Middleware/RedirectDuringLockdown.php b/app/Http/Middleware/RedirectDuringLockdown.php index 634aeb7f..16c19f86 100644 --- a/app/Http/Middleware/RedirectDuringLockdown.php +++ b/app/Http/Middleware/RedirectDuringLockdown.php @@ -15,7 +15,7 @@ class RedirectDuringLockdown { * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next */ public function handle(Request $request, Closure $next): Response { - if (Setting::isLockedDown() && !Auth::user()?->isManager()) return redirect()->route('tracker.lockdown'); + if (Setting::isLockedDown() && !Auth::user()?->isManager()) return redirect()->route('volunteer.lockdown'); return $next($request); } } diff --git a/app/Http/Requests/AttendeeLogStoreRequest.php b/app/Http/Requests/AttendeeLogStoreRequest.php index 712d18c8..15eae421 100644 --- a/app/Http/Requests/AttendeeLogStoreRequest.php +++ b/app/Http/Requests/AttendeeLogStoreRequest.php @@ -3,7 +3,9 @@ namespace App\Http\Requests; use App\Models\AttendeeLog; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class AttendeeLogStoreRequest extends FormRequest { /** @@ -20,7 +22,14 @@ public function authorize(): bool { */ public function rules(): array { return [ - 'name' => 'required|string|max:64', + 'name' => [ + 'required', + 'string', + 'max:64', + Rule::unique(AttendeeLog::class) + ->where(fn (Builder $query) => $query->where('event_id', $this->route('event')->id)) + ->withoutTrashed(), + ], ]; } } diff --git a/app/Http/Requests/AttendeeLogUpdateRequest.php b/app/Http/Requests/AttendeeLogUpdateRequest.php index 45d44036..f9fdd02e 100644 --- a/app/Http/Requests/AttendeeLogUpdateRequest.php +++ b/app/Http/Requests/AttendeeLogUpdateRequest.php @@ -2,7 +2,10 @@ namespace App\Http\Requests; +use App\Models\AttendeeLog; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class AttendeeLogUpdateRequest extends FormRequest { /** @@ -18,9 +21,16 @@ public function authorize(): bool { * @return array */ public function rules(): array { - $requiredOrSometimes = $this->isMethod('PUT') ? 'required' : 'sometimes'; return [ - 'name' => "{$requiredOrSometimes}|string|max:64", + 'name' => [ + $this->isMethod('PUT') ? 'required' : 'sometimes', + 'string', + 'max:64', + Rule::unique(AttendeeLog::class) + ->where(fn (Builder $query) => $query->where('event_id', $this->route('attendeeLog')->event_id)) + ->ignore($this->route('attendeeLog')) + ->withoutTrashed(), + ], ]; } } diff --git a/app/Http/Requests/CheckInRequest.php b/app/Http/Requests/CheckInRequest.php index cf7d29cc..768d6cf6 100644 --- a/app/Http/Requests/CheckInRequest.php +++ b/app/Http/Requests/CheckInRequest.php @@ -2,15 +2,20 @@ namespace App\Http\Requests; +use App\Models\Department; use App\Models\TimeEntry; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Illuminate\Validation\Rule; class CheckInRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ - public function authorize(): bool { - return $this->user()->can('create', [TimeEntry::class, $this->user()]); + public function authorize(): bool|RedirectResponse|JsonResponse { + return $this->user()->can('create', [TimeEntry::class, $this->user(), $this->route('event')]); } /** @@ -20,7 +25,13 @@ public function authorize(): bool { */ public function rules(): array { return [ - 'department_id' => 'required|uuid|exists:App\Models\Department,id', + 'department_id' => [ + 'required', + 'uuid', + Rule::exists(Department::class, 'id') + ->where(fn (Builder $query) => $query->where('event_id', $this->route('event')->id)) + ->withoutTrashed(), + ], ]; } } diff --git a/app/Http/Requests/DepartmentStoreRequest.php b/app/Http/Requests/DepartmentStoreRequest.php index aa629e4c..bfb79e02 100644 --- a/app/Http/Requests/DepartmentStoreRequest.php +++ b/app/Http/Requests/DepartmentStoreRequest.php @@ -3,7 +3,9 @@ namespace App\Http\Requests; use App\Models\Department; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class DepartmentStoreRequest extends FormRequest { /** @@ -20,7 +22,14 @@ public function authorize(): bool { */ public function rules(): array { return [ - 'name' => 'required|string|max:64', + 'name' => [ + 'required', + 'string', + 'max:64', + Rule::unique(Department::class) + ->where(fn (Builder $query) => $query->where('event_id', $this->route('event')->id)) + ->withoutTrashed(), + ], 'hidden' => 'required|boolean', ]; } diff --git a/app/Http/Requests/DepartmentUpdateRequest.php b/app/Http/Requests/DepartmentUpdateRequest.php index 9fc927f3..2c25266d 100644 --- a/app/Http/Requests/DepartmentUpdateRequest.php +++ b/app/Http/Requests/DepartmentUpdateRequest.php @@ -2,7 +2,10 @@ namespace App\Http\Requests; +use App\Models\Department; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class DepartmentUpdateRequest extends FormRequest { /** @@ -20,7 +23,15 @@ public function authorize(): bool { public function rules(): array { $requiredOrSometimes = $this->isMethod('PUT') ? 'required' : 'sometimes'; return [ - 'name' => "{$requiredOrSometimes}|string|max:64", + 'name' => [ + $requiredOrSometimes, + 'string', + 'max:64', + Rule::unique(Department::class) + ->where(fn (Builder $query) => $query->where('event_id', $this->route('department')->event_id)) + ->ignore($this->route('department')) + ->withoutTrashed(), + ], 'hidden' => "{$requiredOrSometimes}|boolean", ]; } diff --git a/app/Http/Requests/EventStoreRequest.php b/app/Http/Requests/EventStoreRequest.php index aed09897..3dea36c7 100644 --- a/app/Http/Requests/EventStoreRequest.php +++ b/app/Http/Requests/EventStoreRequest.php @@ -4,6 +4,7 @@ use App\Models\Event; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class EventStoreRequest extends FormRequest { /** @@ -20,7 +21,18 @@ public function authorize(): bool { */ public function rules(): array { return [ - 'name' => 'required|string|max:64', + 'name' => [ + 'required', + 'string', + 'max:64', + Rule::unique(Event::class)->withoutTrashed(), + ], + 'cloneEvent' => [ + 'sometimes', + 'nullable', + 'uuid', + Rule::exists(Event::class, 'id')->withoutTrashed(), + ], ]; } } diff --git a/app/Http/Requests/EventUpdateRequest.php b/app/Http/Requests/EventUpdateRequest.php index 9b15d88a..963cd1f9 100644 --- a/app/Http/Requests/EventUpdateRequest.php +++ b/app/Http/Requests/EventUpdateRequest.php @@ -2,7 +2,9 @@ namespace App\Http\Requests; +use App\Models\Event; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class EventUpdateRequest extends FormRequest { /** @@ -18,9 +20,13 @@ public function authorize(): bool { * @return array */ public function rules(): array { - $requiredOrSometimes = $this->isMethod('PUT') ? 'required' : 'sometimes'; return [ - 'name' => "{$requiredOrSometimes}|string|max:64", + 'name' => [ + $this->isMethod('PUT') ? 'required' : 'sometimes', + 'string', + 'max:64', + Rule::unique(Event::class)->ignore($this->route('event'))->withoutTrashed(), + ], ]; } } diff --git a/app/Http/Requests/RewardStoreRequest.php b/app/Http/Requests/RewardStoreRequest.php index 6d202e88..e7359a96 100644 --- a/app/Http/Requests/RewardStoreRequest.php +++ b/app/Http/Requests/RewardStoreRequest.php @@ -3,7 +3,9 @@ namespace App\Http\Requests; use App\Models\Reward; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class RewardStoreRequest extends FormRequest { /** @@ -20,7 +22,14 @@ public function authorize(): bool { */ public function rules(): array { return [ - 'name' => 'required|string|max:64', + 'name' => [ + 'required', + 'string', + 'max:64', + Rule::unique(Reward::class) + ->where(fn (Builder $query) => $query->where('event_id', $this->route('event')->id)) + ->withoutTrashed(), + ], 'description' => 'required|string|max:1000', 'hours' => 'required|integer|min:0|max:168', ]; diff --git a/app/Http/Requests/RewardUpdateRequest.php b/app/Http/Requests/RewardUpdateRequest.php index bb974321..6725b2de 100644 --- a/app/Http/Requests/RewardUpdateRequest.php +++ b/app/Http/Requests/RewardUpdateRequest.php @@ -2,7 +2,10 @@ namespace App\Http\Requests; +use App\Models\Reward; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class RewardUpdateRequest extends FormRequest { /** @@ -20,6 +23,15 @@ public function authorize(): bool { public function rules(): array { $requiredOrSometimes = $this->isMethod('PUT') ? 'required' : 'sometimes'; return [ + 'name' => [ + $requiredOrSometimes, + 'string', + 'max:64', + Rule::unique(Reward::class) + ->where(fn (Builder $query) => $query->where('event_id', $this->route('reward')->event_id)) + ->ignore($this->route('reward')) + ->withoutTrashed(), + ], 'name' => "{$requiredOrSometimes}|string|max:64", 'description' => "{$requiredOrSometimes}|string|max:1000", 'hours' => "{$requiredOrSometimes}|integer|min:0|max:168", diff --git a/app/Http/Requests/TimeBonusStoreRequest.php b/app/Http/Requests/TimeBonusStoreRequest.php index 42d6c8d5..7635900d 100644 --- a/app/Http/Requests/TimeBonusStoreRequest.php +++ b/app/Http/Requests/TimeBonusStoreRequest.php @@ -2,8 +2,11 @@ namespace App\Http\Requests; +use App\Models\Department; use App\Models\TimeBonus; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class TimeBonusStoreRequest extends FormRequest { /** @@ -24,7 +27,14 @@ public function rules(): array { 'start' => 'required|date', 'stop' => "required|date|after:{$start}", 'modifier' => 'required|decimal:0,2|min:1|max:10', - 'departments' => 'required|array|exists:App\Models\Department,id', + 'departments' => [ + 'required', + 'list', + 'min:1', + Rule::exists(Department::class, 'id') + ->where(fn (Builder $query) => $query->where('event_id', $this->route('event')->id)) + ->withoutTrashed(), + ], ]; } } diff --git a/app/Http/Requests/TimeBonusUpdateRequest.php b/app/Http/Requests/TimeBonusUpdateRequest.php index a8b1a364..1980859b 100644 --- a/app/Http/Requests/TimeBonusUpdateRequest.php +++ b/app/Http/Requests/TimeBonusUpdateRequest.php @@ -2,7 +2,10 @@ namespace App\Http\Requests; +use App\Models\Department; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class TimeBonusUpdateRequest extends FormRequest { /** @@ -25,7 +28,14 @@ public function rules(): array { 'start' => "{$requiredOrSometimes}|date", 'stop' => "{$requiredOrSometimes}|date{$afterStartRule}", 'modifier' => "{$requiredOrSometimes}|decimal:0,2|min:1|max:10", - 'departments' => "{$requiredOrSometimes}|array|exists:App\Models\Department,id", + 'departments' => [ + $requiredOrSometimes, + 'list', + 'min:1', + Rule::exists(Department::class, 'id') + ->where(fn (Builder $query) => $query->where('event_id', $this->route('bonus')->event_id)) + ->withoutTrashed(), + ], ]; } } diff --git a/app/Http/Requests/TimeEntryStoreRequest.php b/app/Http/Requests/TimeEntryStoreRequest.php index dbd6bdcd..83b759df 100644 --- a/app/Http/Requests/TimeEntryStoreRequest.php +++ b/app/Http/Requests/TimeEntryStoreRequest.php @@ -2,15 +2,18 @@ namespace App\Http\Requests; +use App\Models\Department; +use App\Models\TimeEntry; +use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class TimeEntryStoreRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { - return $this->user()->can('create', [TimeEntry::class, $this->route('user')]) - && $this->user()->isManager(); + return $this->user()->can('create', [TimeEntry::class, $this->route('user'), $this->route('event')]); } /** @@ -22,11 +25,26 @@ public function rules(): array { $start = $this->input('start'); $startRestriction = !$this->has('stop') ? '|before:now' : ''; return [ - 'event_id' => 'sometimes|nullable|uuid|exists:App\Models\Event,id', - 'department_id' => 'required|uuid|exists:App\Models\Department,id', + 'department_id' => [ + 'required', + 'uuid', + Rule::exists(Department::class, 'id') + ->where(fn (Builder $query) => $query->where('event_id', $this->route('event')->id)) + ->withoutTrashed(), + ], 'start' => "sometimes|nullable|required_with:stop|date{$startRestriction}", 'stop' => "sometimes|nullable|date|after:{$start}", 'notes' => 'sometimes|nullable|string|max:255', ]; } + + /** + * Handle a passed validation attempt. + */ + public function passedValidation(): void { + $this->merge([ + 'start' => $this->date('start')?->timezone(config('app.timezone')) ?? now(), + 'stop' => $this->date('stop')?->timezone(config('app.timezone')), + ]); + } } diff --git a/app/Models/Department.php b/app/Models/Department.php index c67ebf22..b283b522 100644 --- a/app/Models/Department.php +++ b/app/Models/Department.php @@ -3,9 +3,12 @@ namespace App\Models; use App\Models\Contracts\HasDisplayName; +use App\Models\Traits\ChecksActiveEvent; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -15,10 +18,12 @@ /** * @property string $name * @property bool $hidden + * @property string $event_id * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at * @property-read string $display_name + * @property-read \App\Models\Event $event * @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\TimeBonus>|\App\Models\TimeBonus[] $timeBonuses * @property-read int|null $time_bonuses_count * @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\TimeEntry>|\App\Models\TimeEntry[] $timeEntries @@ -47,7 +52,7 @@ */ class Department extends Model implements HasDisplayName { /** @use HasFactory<\Database\Factories\DepartmentFactory> */ - use HasFactory, HasUuids, LogsActivity, SoftDeletes; + use ChecksActiveEvent, HasFactory, HasUuids, LogsActivity, SoftDeletes; protected $casts = [ 'hidden' => 'boolean', @@ -68,6 +73,13 @@ public function getDisplayNameAttribute(): string { return !$this->deleted_at ? $this->name : "{$this->name} (del)"; } + /** + * Get the event the department is a part of + */ + public function event(): BelongsTo { + return $this->belongsTo(Event::class)->withTrashed(); + } + /** * Get the time bonuses associated with this department */ @@ -81,4 +93,12 @@ public function timeBonuses(): BelongsToMany { public function timeEntries(): HasMany { return $this->hasMany(TimeEntry::class); } + + /** + * Scope a query to only include departments for an event. + * If the event is not specified, then the active event will be used. + */ + public function scopeForEvent(Builder $query, Event|string|null $event = null): void { + $query->where('event_id', $event->id ?? $event ?? Setting::activeEvent()?->id); + } } diff --git a/app/Models/Event.php b/app/Models/Event.php index d554bf61..a6fa358f 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -18,6 +18,8 @@ * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at * @property-read string $display_name + * @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Department>|\App\Models\Department[] $departments + * @property-read int|null $departments_count * @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\TimeEntry>|\App\Models\TimeEntry[] $timeEntries * @property-read int|null $time_entries_count * @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\TimeBonus>|\App\Models\TimeBonus[] $timeBonuses @@ -78,6 +80,13 @@ public function getDisplayNameAttribute(): string { return !$this->deleted_at ? $this->name : "{$this->name} (del)"; } + /** + * Get the departments associated with this event + */ + public function departments(): HasMany { + return $this->hasMany(Department::class); + } + /** * Get the time entries associated with this event */ diff --git a/app/Models/TimeBonus.php b/app/Models/TimeBonus.php index fc9464b5..a46e7a70 100644 --- a/app/Models/TimeBonus.php +++ b/app/Models/TimeBonus.php @@ -88,4 +88,13 @@ public function departments(): BelongsToMany { public function scopeForEvent(Builder $query, Event|string|null $event = null): void { $query->where('event_id', $event->id ?? $event ?? Setting::activeEvent()?->id); } + + /** + * Builds an array for the time bonus with a list of department IDs + */ + public function toArrayWithDepartmentIds(): array { + $array = $this->toArray(); + $array['departments'] = $this->departments->pluck('id'); + return $array; + } } diff --git a/app/Models/TimeEntry.php b/app/Models/TimeEntry.php index 4281798f..6619ae83 100644 --- a/app/Models/TimeEntry.php +++ b/app/Models/TimeEntry.php @@ -69,14 +69,10 @@ class TimeEntry extends Model { 'auto' => 'boolean', ]; protected $fillable = [ - 'user_id', 'start', 'stop', - 'department_id', 'notes', - 'creator_user_id', - 'auto', - 'event_id', + 'department_id', ]; public function getActivitylogOptions(): LogOptions { diff --git a/app/Models/User.php b/app/Models/User.php index e275fa9c..f9bdd420 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -248,12 +248,13 @@ public function hasAnyBackendAccess(): bool { /** * Gets information about the user's volunteer entities for an event + * + * @return array{user: static, time: array{total: int, bonus: float, day: int, entries: Collection, bonuses: Collection}, reward_claims: Collection} */ public function getVolunteerInfo(?Event $event = null): array { - $time = $this->getTimeStats($event); return [ 'user' => $this, - 'time' => $time, + 'time' => $this->getTimeStats($event), 'reward_claims' => $this->rewardClaims, ]; } @@ -262,7 +263,7 @@ public function getVolunteerInfo(?Event $event = null): array { * Get the total earned time (in seconds) for an event * * @param ?Event $event Event to get the earned time for - if null, then the active event will be used - * @param ?Collection $timeEntries Time entries to look through (to avoid extra queries if already retrieved) + * @param ?Collection $timeEntries Time entries to look through (to avoid extra queries if already retrieved) */ public function getEarnedTime(?Event $event = null, ?Collection $timeEntries = null): int { if (!$event) $event = Setting::activeEvent(); @@ -366,7 +367,7 @@ public function getTimeStats(?Event $event = null, ?Carbon $date = null): array * * @param ?Event $event Event to get the reward info for - if null, then the active event will be used * @param ?float $earnedHours Number of earned hours (to avoid calculating it if it's already known) - * @param ?Collection $timeEntries Time entries to look through (to avoid extra queries if already retrieved) + * @param ?Collection $timeEntries Time entries to look through (to avoid extra queries if already retrieved) * @return array{rewards: Collection, eligible: Collection, claimed: Collection, claims: Collection, earnedHours: float} */ public function getRewardInfo(?Event $event = null, ?float $earnedHours = null, ?Collection $timeEntries = null): array { @@ -465,6 +466,8 @@ public static function createWithAvailableDetails(int $badgeId, string $userType /** * Retrieves both the volunteer and registration information for a given user from ConCat and logs any failed requests + * + * @return array{volunteer: stdClass, registration: stdClass} */ private static function fetchAvailableDetails(int $badgeId): array { $volunteer = null; diff --git a/app/Policies/DepartmentPolicy.php b/app/Policies/DepartmentPolicy.php index f6666784..51346c1a 100644 --- a/app/Policies/DepartmentPolicy.php +++ b/app/Policies/DepartmentPolicy.php @@ -10,14 +10,21 @@ class DepartmentPolicy { * Determine whether the user can view any models. */ public function viewAny(User $user): bool { - return true; + return $user->isManager(); } /** * Determine whether the user can view the model. */ public function view(User $user, Department $department): bool { - return true; + return $user->isManager() || $department->isForActiveEvent(); + } + + /** + * Determine whether the user can view some models that belong to an event. + */ + public function viewForEvent(User $user, ?Event $event): bool { + return $user->isManager() || $event?->isActive(); } /** diff --git a/app/Policies/RewardPolicy.php b/app/Policies/RewardPolicy.php index b38a50c2..45073b1a 100644 --- a/app/Policies/RewardPolicy.php +++ b/app/Policies/RewardPolicy.php @@ -2,6 +2,7 @@ namespace App\Policies; +use App\Models\Event; use App\Models\Reward; use App\Models\User; @@ -10,14 +11,21 @@ class RewardPolicy { * Determine whether the user can view any models. */ public function viewAny(User $user): bool { - return true; + return $user->isManager(); } /** * Determine whether the user can view the model. */ public function view(User $user, Reward $reward): bool { - return true; + return $user->isManager() || $reward->isForActiveEvent(); + } + + /** + * Determine whether the user can view some models that belong to an event. + */ + public function viewForEvent(User $user, ?Event $event): bool { + return $user->isManager() || $event?->isActive(); } /** diff --git a/app/Policies/TimeBonusPolicy.php b/app/Policies/TimeBonusPolicy.php index bc30630b..2758d629 100644 --- a/app/Policies/TimeBonusPolicy.php +++ b/app/Policies/TimeBonusPolicy.php @@ -2,6 +2,7 @@ namespace App\Policies; +use App\Models\Event; use App\Models\TimeBonus; use App\Models\User; @@ -17,7 +18,14 @@ public function viewAny(User $user): bool { * Determine whether the user can view the model. */ public function view(User $user, TimeBonus $timeBonus): bool { - return $user->isManager(); + return $user->isManager() || $timeBonus->isForActiveEvent(); + } + + /** + * Determine whether the user can view some models that belong to an event. + */ + public function viewForEvent(User $user, ?Event $event): bool { + return $user->isManager() || $event?->isActive(); } /** diff --git a/app/Policies/TimeEntryPolicy.php b/app/Policies/TimeEntryPolicy.php index a4a4c7aa..78ad9cdc 100644 --- a/app/Policies/TimeEntryPolicy.php +++ b/app/Policies/TimeEntryPolicy.php @@ -25,8 +25,8 @@ public function view(User $user, TimeEntry $timeEntry): bool { /** * Determine whether the user can create models. */ - public function create(User $creator, User $target, ?Event $event = null): bool { - $validEvent = !$event || $event->isActive(); + public function create(User $creator, User $target, Event $event): bool { + $validEvent = $event->isActive(); return ($creator->id === $target->id && $validEvent && (Kiosk::isSessionAuthorized() || $creator->isStaff())) || ($creator->isManager() && $validEvent) || $creator->isAdmin(); diff --git a/app/Telegram/Commands/Command.php b/app/Telegram/Commands/Command.php index fa2e41c5..1bfe489a 100644 --- a/app/Telegram/Commands/Command.php +++ b/app/Telegram/Commands/Command.php @@ -94,6 +94,6 @@ protected function buildStandardActionsKeyboard(): Keyboard { protected static function trackerLink($text = null): string { if (!$text) $text = config('app.name'); $escaped = htmlspecialchars($text); - return '{$escaped}"; + return '{$escaped}"; } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 7b202bb7..14aab672 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -32,7 +32,7 @@ $middleware->redirectTo( guests: fn (Request $request) => route('auth.login'), - users: fn (Request $request) => route('tracker.index'), + users: fn (Request $request) => route('volunteer.index'), ); $middleware->throttleApi(); diff --git a/config/ziggy.php b/config/ziggy.php index 1114711f..0ad2ebca 100644 --- a/config/ziggy.php +++ b/config/ziggy.php @@ -7,6 +7,6 @@ 'telegram.*', 'auth.callback', 'auth.banned', - 'tracker.lockdown', + 'volunteer.lockdown', ], ]; diff --git a/database/factories/DepartmentFactory.php b/database/factories/DepartmentFactory.php index fd868c88..4e0e2fc1 100644 --- a/database/factories/DepartmentFactory.php +++ b/database/factories/DepartmentFactory.php @@ -16,11 +16,21 @@ class DepartmentFactory extends Factory { */ public function definition(): array { return [ - 'name' => Str::limit(fake()->jobTitle(), 63), - 'hidden' => false, + 'name' => Str::limit(fake()->jobTitle(), 61), + 'hidden' => fake()->boolean(20), + 'event_id' => \App\Models\Event::factory(), ]; } + /** + * Indicate that the model's hidden flag should not be set + */ + public function visible(): static { + return $this->state(fn () => [ + 'hidden' => false, + ]); + } + /** * Indicate that the model's hidden flag should be set */ diff --git a/database/factories/TimeEntryFactory.php b/database/factories/TimeEntryFactory.php index 93f0bb3e..ec20e0da 100644 --- a/database/factories/TimeEntryFactory.php +++ b/database/factories/TimeEntryFactory.php @@ -15,37 +15,47 @@ class TimeEntryFactory extends Factory { * @return array */ public function definition(): array { - $start = new Carbon(fake()->dateTimeInInterval('-4 days', '+3 days 11 hours')); - $stop = $start->avoidMutation() - ->addHours(fake()->numberBetween(1, 12)) - ->addMinutes(fake()->numberBetween(0, 59)) - ->addSeconds(fake()->numberBetween(0, 59)); - + $times = static::randomTimes(fake()->boolean(5)); return [ 'user_id' => \App\Models\User::factory(), - 'start' => $start, - 'stop' => $stop, + 'start' => $times['start'], + 'stop' => $times['stop'], 'department_id' => \App\Models\Department::factory(), 'notes' => fake()->boolean(25) ? fake()->paragraph() : null, 'creator_user_id' => fn (array $attributes) => $attributes['user_id'], 'auto' => fake()->boolean(5), 'event_id' => \App\Models\Event::factory(), - 'created_at' => $start, - 'updated_at' => $stop, + 'created_at' => $times['start'], + 'updated_at' => $times['stop'] ?? $times['start'], ]; } + /** + * Indicate that the time entry is finished (not ongoing) + */ + public function finished(): static { + return $this->state(function () { + $times = static::randomTimes(false); + return [ + 'start' => $times['start'], + 'stop' => $times['stop'], + 'created_at' => $times['start'], + 'updated_at' => $times['stop'], + ]; + }); + } + /** * Indicate that the time entry is ongoing (started within 12 hours ago and hasn't yet stopped) */ public function ongoing(): static { return $this->state(function () { - $start = new Carbon(fake()->dateTimeInInterval('-12 hours', '+12 hours')); + $times = static::randomTimes(true); return [ - 'start' => $start, + 'start' => $times['start'], 'stop' => null, - 'created_at' => $start, - 'updated_at' => $start, + 'created_at' => $times['start'], + 'updated_at' => $times['start'], ]; }); } @@ -85,4 +95,27 @@ public function withoutNotes(): static { 'notes' => null, ]); } + + /** + * Generates random start/stop times + * + * @return array + */ + protected static function randomTimes(bool $ongoing): array { + if ($ongoing) { + return [ + 'start' => new Carbon(fake()->dateTimeInInterval('-12 hours', '+12 hours')), + 'stop' => null, + ]; + } + + $start = new Carbon(fake()->dateTimeInInterval('-4 days', '+3 days 11 hours')); + return [ + 'start' => $start, + 'stop' => $start->avoidMutation() + ->addHours(fake()->numberBetween(1, 12)) + ->addMinutes(fake()->numberBetween(0, 59)) + ->addSeconds(fake()->numberBetween(0, 59)), + ]; + } } diff --git a/database/migrations/2026_05_09_170545_make_departments_event_specific.php b/database/migrations/2026_05_09_170545_make_departments_event_specific.php new file mode 100644 index 00000000..69ebd179 --- /dev/null +++ b/database/migrations/2026_05_09_170545_make_departments_event_specific.php @@ -0,0 +1,88 @@ +uuid('event_id')->nullable()->after('hidden'); + }); + + $eventIds = DB::table('events')->select(['id'])->get()->pluck('id'); + $oldDepts = DB::table('departments')->get(); + $now = now(); + + // Create new departments for each event and update all references to the old ones + foreach ($eventIds as $eventId) { + foreach ($oldDepts as $oldDept) { + $newId = Uuid::uuid7(); + DB::table('departments')->insert([ + 'id' => $newId, + 'name' => $oldDept->name, + 'hidden' => $oldDept->hidden, + 'event_id' => $eventId, + 'created_at' => $oldDept->created_at, + 'updated_at' => $now, + 'deleted_at' => $oldDept->deleted_at, + ]); + + DB::table('time_entries') + ->where('event_id', $eventId) + ->where('department_id', $oldDept->id) + ->update(['department_id' => $newId]); + DB::table('department_time_bonus') + ->join('time_bonuses', 'department_time_bonus.time_bonus_id', '=', 'time_bonuses.id') + ->where('event_id', $eventId) + ->where('department_id', $oldDept->id) + ->update(['department_id' => $newId]); + } + } + + // Delete all old departments + DB::table('departments')->whereNull('event_id')->delete(); + + // Remove nullable and add the foreign key constraint + Schema::table('departments', function (Blueprint $table) { + $table->uuid('event_id')->nullable(false)->change(); + $table->foreign('event_id')->references('id')->on('events')->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + // Drop the event_id column + Schema::table('departments', function (Blueprint $table) { + $table->dropConstrainedForeignId('event_id'); + }); + + $oldDepts = DB::table('departments')->get(); + $now = now(); + + // Delete all departments with duplicate names, keeping the oldest, and update all references to them + foreach ($oldDepts->groupBy('name') as $name => $depts) { + $oldest = $depts->sortBy('created_at')->first(); + $oldest->updated_at = $now; + unset($oldest->event_id); + DB::table('departments')->where('id', $oldest->id)->update((array) $oldest); + + $oldIds = $depts->where('id', '!==', $oldest->id)->pluck('id'); + DB::table('departments')->whereIn('id', $oldIds)->delete(); + DB::table('time_entries') + ->whereIn('department_id', $oldIds) + ->update(['department_id' => $oldest->id]); + DB::table('department_time_bonus') + ->whereIn('department_id', $oldIds) + ->update(['department_id' => $oldest->id]); + } + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index e46fb8cf..f241e4c8 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -16,32 +16,33 @@ class DatabaseSeeder extends Seeder { * Seed the application's database. */ public function run(): void { - // Create some departments - $departments = Department::factory(10)->create(); - Department::factory(4)->hidden()->create(); - - // Create some events - $events = Event::factory(5) - ->hasRewards(4) - ->has(TimeBonus::factory(5)->hasAttached($departments)) - ->create(); - - // Create a bunch of volunteer users with time entries - User::factory(100) + // Create some users + $users = User::factory(100) ->telegramUnlinked() - ->has( - TimeEntry::factory(20) - ->recycle($events) - ->recycle($departments) - ) - ->has( - TimeEntry::factory(1) - ->ongoing() - ->recycle($events) - ->recycle($departments) - ) ->create(); + // Create some events with departments, rewards, time bonuses, and time entries + for ($i = 0; $i < 5; $i++) { + $event = Event::factory() + ->hasRewards(4) + ->create(); + + $departments = Department::factory(15) + ->for($event) + ->create(); + + TimeBonus::factory(5) + ->for($event) + ->hasAttached($departments) + ->create(); + + TimeEntry::factory(1000) + ->for($event) + ->recycle($departments) + ->recycle($users) + ->create(); + } + // Update all time entry activities' creation time to be appropriate $timeActivities = Activity::with('subject')->whereSubjectType(TimeEntry::class)->lazy(); foreach ($timeActivities as $activity) { diff --git a/package-lock.json b/package-lock.json index 0a42e14c..fcc296fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,6 @@ "": { "name": "blfc-tracker", "dependencies": { - "@eonasdan/tempus-dominus": "^6.9.10", "@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.2.0", @@ -72,9 +71,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -275,9 +274,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, @@ -287,9 +286,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, @@ -308,23 +307,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@eonasdan/tempus-dominus": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/@eonasdan/tempus-dominus/-/tempus-dominus-6.10.4.tgz", - "integrity": "sha512-aR1QkKUEFyfpqKuRs3ZnIfMhhB4aAS6WO3CpHn4FTLABe2ia3tm4pb7w+ium3w5b/d+mIKSi6VcbDukU+dtnYA==", - "license": "MIT", - "funding": { - "url": "https://ko-fi.com/eonasdan" - }, - "peerDependencies": { - "@popperjs/core": "^2.11.6" - }, - "peerDependenciesMeta": { - "@popperjs/core\"": { - "optional": true - } - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -835,9 +817,9 @@ } }, "node_modules/@inertiajs/core": { - "version": "2.3.21", - "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.3.21.tgz", - "integrity": "sha512-grHSCUiWDBWqpRxaobyxUJu0FV6HLkkuJwvoNLVkHwkexLvoaLhb9BmtoQydlIYL5pk2O3jcKaGtWJ83JwTB4A==", + "version": "2.3.23", + "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.3.23.tgz", + "integrity": "sha512-d+jcdf91RY5bjT+ivBn2gu9Qsmxx5CacvVmVp7o9H2Lu/k1cBD0m4UZB5YKWfG11aBCucShBxuppEXme7sSEow==", "license": "MIT", "dependencies": { "@types/lodash-es": "^4.17.12", @@ -848,12 +830,12 @@ } }, "node_modules/@inertiajs/vue3": { - "version": "2.3.21", - "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.3.21.tgz", - "integrity": "sha512-gJuOD9HrB6WXpTCUB6yLDHA2yI5YGzhYcGlHCPB6mzt6Lvm7CsQA06CNOyk8eEooz0MYJhFF2V092hU1i866qg==", + "version": "2.3.23", + "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.3.23.tgz", + "integrity": "sha512-W6rhjNCIKQQYZsNFdlFEa+prATbXcIR5k9VMg3PsYfRmqTYTORZspdrTH7JLPMneLuhNiI/Y6VbFR0TxGdv5Fg==", "license": "MIT", "dependencies": { - "@inertiajs/core": "2.3.21", + "@inertiajs/core": "2.3.23", "@types/lodash-es": "^4.17.12", "laravel-precognition": "^1.0.2", "lodash-es": "^4.18.1" @@ -931,9 +913,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.126.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", - "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", + "version": "0.128.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", + "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", "dev": true, "license": "MIT", "funding": { @@ -1063,6 +1045,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1083,6 +1068,9 @@ "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1103,6 +1091,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1123,6 +1114,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1143,6 +1137,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1163,6 +1160,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1352,9 +1352,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", "cpu": [ "arm64" ], @@ -1369,9 +1369,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", "cpu": [ "arm64" ], @@ -1386,9 +1386,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", "cpu": [ "x64" ], @@ -1403,9 +1403,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", "cpu": [ "x64" ], @@ -1420,9 +1420,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz", - "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", + "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", "cpu": [ "arm" ], @@ -1437,13 +1437,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1454,13 +1457,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1471,13 +1477,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1488,13 +1497,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1505,13 +1517,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1522,13 +1537,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1539,9 +1557,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", "cpu": [ "arm64" ], @@ -1556,9 +1574,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz", - "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", + "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", "cpu": [ "wasm32" ], @@ -1566,8 +1584,8 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { @@ -1575,9 +1593,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", "cpu": [ "arm64" ], @@ -1592,9 +1610,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", "cpu": [ "x64" ], @@ -1616,9 +1634,9 @@ "license": "MIT" }, "node_modules/@tailwindcss/node": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.3.tgz", - "integrity": "sha512-dhXFXkW2dGvX4r/fi24gyXM0t1mFMrpykQjqrdA4SuavaMagm4SY1u5G2SCJwu1/0x/5RlZJ2VPjP3mKYQfCkA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", "dev": true, "license": "MIT", "dependencies": { @@ -1628,37 +1646,37 @@ "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.3" + "tailwindcss": "4.2.4" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.3.tgz", - "integrity": "sha512-YyhwSBcxHLS3CU2Mk3dXDuVm8/Ia0+XvfpT8s9YQoICppkUeoobB3hgyGMYbyQ4vn6VgWH9bdv5UnzhTz2NPTQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.3", - "@tailwindcss/oxide-darwin-arm64": "4.2.3", - "@tailwindcss/oxide-darwin-x64": "4.2.3", - "@tailwindcss/oxide-freebsd-x64": "4.2.3", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.3", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.3", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.3", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.3", - "@tailwindcss/oxide-linux-x64-musl": "4.2.3", - "@tailwindcss/oxide-wasm32-wasi": "4.2.3", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.3", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.3" + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.3.tgz", - "integrity": "sha512-0Jmt1U/zPqeKp1+fvgI3qMqrV5b/EcFIbE5Dl5KdPl5Ri6e+95nlYNjfB3w8hJBeASI4IQSnIMz0tdVP1AVO4g==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", "cpu": [ "arm64" ], @@ -1673,9 +1691,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.3.tgz", - "integrity": "sha512-c+/Etn/nghKBhd9fh2diG+3SEV1VTTPLlqH209yleofi28H87Cy6g1vsd3W3kf6r/dR5g4G4TEwHxo2Ydn6yFw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", "cpu": [ "arm64" ], @@ -1690,9 +1708,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.3.tgz", - "integrity": "sha512-1DrKKsdJTLuLWVdpaLZ0j/g9YbCZyP9xnwSqEvl3gY4ZHdXmX7TwVAHkoWUljOq7JK5zvzIGhrYmfE/2DJ5qaA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", "cpu": [ "x64" ], @@ -1707,9 +1725,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.3.tgz", - "integrity": "sha512-HE6HHZYF8k7m80eVQ0RBvRGBdvvLvCpHiT38IRH9JSnBlt1T7gDzWoslWjmpXQFuqlRpzkCpbdKJa3NxWMfgVA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", "cpu": [ "x64" ], @@ -1724,9 +1742,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.3.tgz", - "integrity": "sha512-Li2wVd2kkKlKkTdpo7ujHSv6kxD1UYMvulAraikyvVf6AKNZ/VHbm8XoSNimZ+dF7SOFaDD2VAT64SK7WKcbjQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", "cpu": [ "arm" ], @@ -1741,13 +1759,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.3.tgz", - "integrity": "sha512-otIiImZaHj9MiDK02ItoWxIVcMTZVAX2F1c32bg9y7ecV0AnN5JHDZqIO8LxWsTuig1d+Bjg0cBWn4A9sGJO9Q==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1758,13 +1779,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.3.tgz", - "integrity": "sha512-MmIA32rNEOrjh6wnevlR3OjjlCuwgZ4JMJo7Vrhk4Fk56Vxi7EeF7cekSKwvlrnfcn/ERC1LdcG3sFneU8WdoA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1775,13 +1799,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.3.tgz", - "integrity": "sha512-BiCy1YV0IKO+xbD7gyZnENU4jdwDygeGQjncJoeIE5Kp4UqWHFsKUSJ3pp7vYURrqVzwJX2xD5gQeGnoXp4xPQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1792,13 +1819,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.3.tgz", - "integrity": "sha512-venvyAu0AMKdr0c1Oz23IJJdZ72zSwKyHrLvqQV1cn49vPAJk3AuVtDkJ1ayk1sYI4M4j8Jv6ZGflpaP0QVSXQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1809,9 +1839,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.3.tgz", - "integrity": "sha512-e3kColrZZCdtbwIOc07cNQ2zNf1sTPXTYLjjPlsgsaf+ttzAg/hOlDyEgHoOlBGxM88nPxeVaOGe9ThqVzPncg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1839,9 +1869,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.3.tgz", - "integrity": "sha512-qpwoUPzfu71cppxOtcz4LXMR1brljS13yOcAAnVHKIL++NJvSQKZBKlP39pVowd+G6Mq34YAbf4CUUYdLWL9gQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", "cpu": [ "arm64" ], @@ -1856,9 +1886,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.3.tgz", - "integrity": "sha512-dTRIlLRC5lCRHqO5DLb+A18HCvS394axmzqfnRNLptKVw7WuckpUwo1Z87Yw74mesbeIhnQTA2SZbRcIfVlwxg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", "cpu": [ "x64" ], @@ -1873,15 +1903,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.3.tgz", - "integrity": "sha512-pEvbC/NoOqxvqjy6IgelSakbzwin865CmOxJxmz3CSEbHJ2aF1B2183ALVasN0o6dOGhYfnVJOKKxVoyag+XeA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", + "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.2.3", - "@tailwindcss/oxide": "4.2.3", - "tailwindcss": "4.2.3" + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" @@ -1895,9 +1925,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -1945,9 +1975,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", "dev": true, "license": "MIT", "dependencies": { @@ -2011,59 +2041,59 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", - "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.2", - "@vue/shared": "3.5.32", + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", - "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", - "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.2", - "@vue/compiler-core": "3.5.32", - "@vue/compiler-dom": "3.5.32", - "@vue/compiler-ssr": "3.5.32", - "@vue/shared": "3.5.32", + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.8", + "postcss": "^8.5.14", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", - "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" } }, "node_modules/@vue/language-core": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.7.tgz", - "integrity": "sha512-Gn4q/tRxbpVGLEuARQ43p3YELlNAFgRUVCgW9U5Cr+5q4vfD2bWDWpl3ABbJMXUt5xlE1dF8dkigg2aUq7JYYw==", + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.8.tgz", + "integrity": "sha512-9OiSPQFiAAWNVnXb0d2dcTmcKnFQamhuNES6ayyISrb/mwPWVgoGdAqSfCWqKhQpa3D5gDTcYD+w7ObiheZ81g==", "dev": true, "license": "MIT", "dependencies": { @@ -2077,53 +2107,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", - "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.32" + "@vue/shared": "3.5.34" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", - "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", - "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.32", - "@vue/runtime-core": "3.5.32", - "@vue/shared": "3.5.32", + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", - "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" }, "peerDependencies": { - "vue": "3.5.32" + "vue": "3.5.34" } }, "node_modules/@vue/shared": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", - "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", "license": "MIT" }, "node_modules/@vue/tsconfig": { @@ -2202,12 +2232,12 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", - "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -2474,14 +2504,14 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.21.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.1.tgz", + "integrity": "sha512-8p7DUVq6XJnZEz9W4oSwiwycxBIjHjRzYb3Je3zVN+geKTRQKzAkR/K4PBExlS0090d9nshak6phMUxr3PDjmQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -2875,9 +2905,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { @@ -2901,9 +2931,9 @@ } }, "node_modules/laravel-vite-plugin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.0.1.tgz", - "integrity": "sha512-Bx8sVcLIaZT1d0eisABcmjQ1GZdJpaXcV66A8RhXGp9JgR3iL8jDnvakVDXuH87Tn5S9KNx3VOhmJZW1CSexOg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.1.0.tgz", + "integrity": "sha512-Fzocl+X4eQ9jOi0RwdphYRGkUbPJ3ky1pTAST5Ot18cS2gw6d2vldK2eCrlKDVjtibCjCx5qptYDlA0373n7qg==", "dev": true, "license": "MIT", "dependencies": { @@ -2918,7 +2948,13 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { + "fontaine": "^0.5.0", "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "fontaine": { + "optional": true + } } }, "node_modules/lightningcss": { @@ -3064,6 +3100,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3085,6 +3124,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3106,6 +3148,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3127,6 +3172,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3312,9 +3360,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -3438,14 +3486,14 @@ } }, "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", "dev": true, "license": "MIT", "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", + "confbox": "^0.2.4", + "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, @@ -3459,9 +3507,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", @@ -3665,14 +3713,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", - "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", + "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.126.0", - "@rolldown/pluginutils": "1.0.0-rc.16" + "@oxc-project/types": "=0.128.0", + "@rolldown/pluginutils": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" @@ -3681,27 +3729,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-x64": "1.0.0-rc.16", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" + "@rolldown/binding-android-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-x64": "1.0.0-rc.18", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", - "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", + "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", "dev": true, "license": "MIT" }, @@ -3898,9 +3946,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.3.tgz", - "integrity": "sha512-fA/NX5gMf0ooCLISgB0wScaWgaj6rjTN2SVAwleURjiya7ITNkV+VMmoHtKkldP6CIZoYCZyxb8zP/e2TWoEtQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", "license": "MIT" }, "node_modules/tailwindcss-primeui": { @@ -3913,9 +3961,9 @@ } }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -3995,9 +4043,9 @@ } }, "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "dev": true, "license": "MIT" }, @@ -4104,16 +4152,16 @@ } }, "node_modules/vite": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", - "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", + "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.16", + "postcss": "^8.5.14", + "rolldown": "1.0.0-rc.18", "tinyglobby": "^0.2.16" }, "bin": { @@ -4130,7 +4178,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -4213,16 +4261,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", - "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.32", - "@vue/compiler-sfc": "3.5.32", - "@vue/runtime-dom": "3.5.32", - "@vue/server-renderer": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" }, "peerDependencies": { "typescript": "*" @@ -4243,14 +4291,14 @@ } }, "node_modules/vue-tsc": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.7.tgz", - "integrity": "sha512-zc1tL3HoQni1zGTGrwBVRQb7rGP5SWdu/m4rGB6JcnAC5MT5LFZIxF7Y+EJEnt4hGF23d60rXH7gRjHGb5KQQQ==", + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.8.tgz", + "integrity": "sha512-27vTLJ6Q2370obOd0PFYoYoKnmXJ521uUIedrs3Zhhhg/8YG10VOCMmwt+JQslatpAMTDbnWiitLnoD5VlIvog==", "dev": true, "license": "MIT", "dependencies": { "@volar/typescript": "2.4.28", - "@vue/language-core": "3.2.7" + "@vue/language-core": "3.2.8" }, "bin": { "vue-tsc": "bin/vue-tsc.js" diff --git a/package.json b/package.json index d06a2741..c1999efd 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "lint:fix-unsafe": "biome lint --write --unsafe" }, "dependencies": { - "@eonasdan/tempus-dominus": "^6.9.10", "@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.2.0", diff --git a/resources/js/Components/App/AppNav.vue b/resources/js/Components/App/AppNav.vue index af43d715..96b2dad4 100644 --- a/resources/js/Components/App/AppNav.vue +++ b/resources/js/Components/App/AppNav.vue @@ -79,6 +79,7 @@ import { computed, ref, toRef, useId } from 'vue'; import { useBreakpoints } from '@/lib/media-query'; import { useUser } from '@/lib/user'; +import type { RouteName } from '@/lib/route'; import { faHouseCircleCheck, @@ -93,6 +94,7 @@ import { faClose, faCog, faArrowRightToBracket, + type IconDefinition, } from '@fortawesome/free-solid-svg-icons'; import AppNavItem from './AppNavItem.vue'; import AppNavLogoutItem from './AppNavLogoutItem.vue'; @@ -110,12 +112,18 @@ const showHamburger = toRef(() => isNotSm.value); const expandedMenuHeight = toRef(() => `${(mainMenuItems.value.length + 3) * 3 + 4}rem`); const mainMenuItems = computed(() => { - const items = [ + const items: { + to: RouteName; + label: string; + icon: IconDefinition; + legacy?: boolean; + activeStems?: string[]; + activeExcludeStems?: string[]; + }[] = [ { - to: 'tracker.index', + to: 'volunteer.index', label: 'Home', icon: faHouseCircleCheck, - legacy: false, }, ]; @@ -124,7 +132,6 @@ const mainMenuItems = computed(() => { to: 'management.manage', label: 'Manager Dashboard', icon: faBusinessTime, - legacy: false, }); } @@ -133,7 +140,7 @@ const mainMenuItems = computed(() => { to: 'attendee-logs.index', label: 'Attendee Logs', icon: faBookOpen, - legacy: false, + activeStems: ['attendee-logs', 'events.attendee-logs'], }); } @@ -143,13 +150,12 @@ const mainMenuItems = computed(() => { to: 'users.index', label: 'Users', icon: faUsers, - legacy: false, }, { - to: 'admin.events', + to: 'events.index', label: 'Events', icon: faCalendarDay, - legacy: true, + activeExcludeStems: ['events.attendee-logs'], }, { to: 'admin.reports', @@ -161,7 +167,6 @@ const mainMenuItems = computed(() => { to: 'settings.index', label: 'Configuration', icon: faScrewdriverWrench, - legacy: false, }, ); } diff --git a/resources/js/Components/App/AppNavItem.vue b/resources/js/Components/App/AppNavItem.vue index 55958f47..6f4137d5 100644 --- a/resources/js/Components/App/AppNavItem.vue +++ b/resources/js/Components/App/AppNavItem.vue @@ -48,6 +48,8 @@ const { method = 'get', legacy = false, active = false, + activeStems, + activeExcludeStems, color = 'text-muted-color', activeColor = 'text-primary-contrast', showText = false, @@ -61,6 +63,8 @@ const { method?: Method; legacy?: boolean; active?: boolean; + activeStems?: string[]; + activeExcludeStems?: string[]; color?: string; activeColor?: string; showText?: boolean; @@ -72,18 +76,30 @@ const { const route = useRoute(); const page = usePage(); -const isActive = computed( - () => - active || - Boolean( - page.url && - to && - route() - .current() - ?.includes(to.replace(/\.index$/, '')), - ), -); const isButton = toRef(() => !to || method !== 'get'); +const isActive = computed(() => { + if (active) return true; + if (!page.url || !to) return; + if (!activeStems && !to) return false; + + // Make sure we match to a route name + const current = route().current(); + if (!current) return false; + + // Don't be active for excluded route stems + if (activeExcludeStems) { + for (const stem of activeExcludeStems) { + if (current.startsWith(stem)) return false; + } + } + + // Be active for included route stems + for (const stem of activeStems ?? [getStem(to)]) { + if (current.startsWith(stem)) return true; + } + + return false; +}); /** * Navigates to the given route @@ -91,4 +107,12 @@ const isButton = toRef(() => !to || method !== 'get'); function navigate() { router.visit(route(to!), { method }); } + +/** + * Gets the stem (first word before a `.`) of a route name + */ +function getStem(route: RouteName): string { + const dotIdx = route.indexOf('.'); + return dotIdx !== -1 ? route.substring(0, dotIdx) : route; +} diff --git a/resources/js/Components/App/CrudTable.vue b/resources/js/Components/App/CrudTable.vue new file mode 100644 index 00000000..0ed6b934 --- /dev/null +++ b/resources/js/Components/App/CrudTable.vue @@ -0,0 +1,597 @@ +