Add LOA feature, improve components

This commit is contained in:
Miguel Nogueira 2022-02-07 18:59:22 +00:00
parent d6e248b571
commit 23a191deb9
24 changed files with 780 additions and 90442 deletions

28
app/Absence.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Absence extends Model
{
use HasFactory;
protected $fillable = [
'requesterID',
'start',
'predicted_end',
'available_assist',
'reason',
'status',
'reviewer',
'reviewed_date'
];
public function requester(): BelongsTo
{
return $this->belongsTo('App\User', 'requesterID', 'id');
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Appeal extends Model
{
use HasFactory;
}

View File

@ -0,0 +1,138 @@
<?php
namespace App\Http\Controllers;
use App\Absence;
use App\Http\Requests\StoreAbsenceRequest;
use App\Http\Requests\UpdateAbsenceRequest;
use App\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
class AbsenceController extends Controller
{
/**
* Determines whether someone already has an active leave of absence request
*
* @param User $user The user to check
* @return bool Their status
*/
private function hasActiveRequest(Authenticatable $user): bool {
$absences = Absence::where('requesterID', $user->id)->get();
foreach ($absences as $absence) {
// Or we could adjust the query (using a model scope) to only return valid absences;
// If there are any, refuse to store more, but this approach also works
// A model scope that only returns cancelled, declined and ended absences could also be implemented for future use
if (in_array($absence->status, ['PENDING', 'APPROVED']))
{
return true;
}
}
return false;
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('dashboard.absences.index')
->with('absences', Absence::all());
// display for admin users
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('dashboard.absences.create')
->with('activeRequest', $this->hasActiveRequest(Auth::user()));
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\StoreAbsenceRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreAbsenceRequest $request)
{
$this->authorize('create', Absence::class);
if ($this->hasActiveRequest(Auth::user())) {
return redirect()
->back()
->with('error', __('You already have an active request. Cancel it or let it expire first.'));
}
Absence::create([
'requesterID' => Auth::user()->id,
'start' => $request->start_date,
'predicted_end' => $request->predicted_end,
'available_assist' => $request->invalidAbsenceAgreement == 'on',
'reason' => $request->reason,
'status' => 'PENDING',
]);
return redirect()
->back()
->with('success', 'Absence request submitted for approval. You will receive email confirmation shortly.');
}
/**
* Display the specified resource.
*
* @param \App\Absence $absence
* @return \Illuminate\Http\Response
*/
public function show(Absence $absence)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Absence $absence
* @return \Illuminate\Http\Response
*/
public function edit(Absence $absence)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\UpdateAbsenceRequest $request
* @param \App\Absence $absence
* @return \Illuminate\Http\Response
*/
public function update(UpdateAbsenceRequest $request, Absence $absence)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \App\Absence $absence
* @return \Illuminate\Http\Response
*/
public function destroy(Absence $absence)
{
//
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Appeal;
use Illuminate\Http\Request;
// temp logic
/*
* Appeal types:
* - Discord ban appeal (Will prompt user to login with Discord, if account is not linked)
* - Site ban appeal (Can be filled while logged out, requires a valid email address, won't prompt for Discord auth)
* - Timeout appeal (Will prompt user to login with Discord, if account is not linked)
*
*/
class AppealController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param \App\Appeal $appeal
* @return \Illuminate\Http\Response
*/
public function show(Appeal $appeal)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Appeal $appeal
* @return \Illuminate\Http\Response
*/
public function edit(Appeal $appeal)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Appeal $appeal
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Appeal $appeal)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \App\Appeal $appeal
* @return \Illuminate\Http\Response
*/
public function destroy(Appeal $appeal)
{
//
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class StoreAbsenceRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->hasPermissionTo('reviewer.requestAbsence');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'reason' => 'required|string',
'start_date' => 'required|date',
'predicted_end' => 'required|date|after:start_date',
'available_assist' => 'required|string',
'invalidAbsenceAgreement' => 'required|accepted'
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateAbsenceRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Policies;
use App\Absence;
use App\Response;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class AbsencePolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @param \App\User $user
* @return \Illuminate\Auth\Access\Response|bool
*/
public function viewAny(User $user)
{
if ($user->hasPermissionTo('admin.viewAllAbsences'))
{
return \Illuminate\Auth\Access\Response::allow();
}
return \Illuminate\Auth\Access\Response::deny('Forbidden');
}
/**
* Determine whether the user can view the model.
*
* @param \App\User $user
* @param \App\Absence $absence
* @return \Illuminate\Auth\Access\Response|bool
*/
public function view(User $user, Absence $absence)
{
if ($user->hasPermissionTo('reviewer.viewAbsence') && $user->id == $absence->requesterID)
{
return true;
}
return false;
}
/**
* Determine whether the user can create models.
*
* @param \App\User $user
* @return \Illuminate\Auth\Access\Response|bool
*/
public function create(User $user)
{
return $user->hasPermissionTo('reviewer.requestAbsence');
}
/**
* Determine whether the user can update the model.
*
* @param \App\User $user
* @param \App\Absence $absence
* @return \Illuminate\Auth\Access\Response|bool
*/
public function update(User $user, Absence $absence)
{
return $user->hasPermissionTo('admin.manageAbsences');
}
/**
* Determine whether the user can delete the model.
*
* @param \App\User $user
* @param \App\Absence $absence
* @return \Illuminate\Auth\Access\Response|bool
*/
public function delete(User $user, Absence $absence)
{
return $user->hasPermissionTo('admin.manageAbsences') || $user->hasPermissionTo('reviewer.withdrawAbsence') && $user->id == $absence->requesterID;
}
}

View File

@ -92,11 +92,13 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->hasMany('App\TeamFile', 'uploaded_by');
}
public function keys()
public function absences()
{
return $this->hasMany('App\ApiKey', 'owner_user_id');
return $this->hasMany('App\Absence', 'requesterID');
}
// UTILITY LOGIC
public function isBanned()
@ -129,6 +131,7 @@ class User extends Authenticatable implements MustVerifyEmail
}
}
public function routeNotificationForSlack($notification)
{
return config('slack.webhook.integrationURL');

View File

@ -25,20 +25,26 @@ use Illuminate\View\Component;
class Alert extends Component
{
public $alertType;
public $extraStyling;
public
$alertType,
$extraStyling,
$title,
$icon;
/**
* Create a new component instance.
*
* @param $alertType
* @param string $extraStyling
* @param string $alertType The color the alert should have.
* @param string $title The alert's title
* @param string $icon The alert's icon, placed before the title
* @param string $extraStyling Any extra CSS classes to add
*/
public function __construct($alertType, $extraStyling = '')
public function __construct(string $alertType, string $title = '', string $icon = '', string $extraStyling = '')
{
$this->alertType = $alertType;
$this->extraStyling = $extraStyling;
$this->icon = $icon;
$this->title = $title;
}
/**

View File

@ -0,0 +1,40 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Button extends Component
{
public string
$type,
$icon,
$link,
$target,
$size,
$color,
$disabled,
$id;
public function __construct($id, $color, $type = 'button', $disabled = false, $size = '', $target = '', $link = '', $icon = '')
{
$this->link = $link;
$this->disabled = $disabled;
$this->type = $type;
$this->target = $target;
$this->size = $size;
$this->color = $color;
$this->id = $id;
$this->icon = $icon;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.button');
}
}

View File

@ -268,6 +268,36 @@ return [
'icon' => 'fas fa-user-circle',
'url' => '/profile/settings/account',
],
[
'header' => 'Human Resources',
'can' => 'reviewer.requestAbsence'
],
[
'text' => 'Absence Management',
'icon' => 'fas fa-user-clock',
'can' => 'reviewer.requestAbsence',
'submenu' => [
[
'text' => 'Request LOA',
'icon' => 'far fa-clock',
'can' => 'reviewer.requestAbsence',
'url' => 'tba'
],
[
'text' => 'My LOA Requests',
'icon' => 'fas fa-business-time',
'can' => 'reviewer.viewAbsence',
'url' => 'tba'
],
],
],
[
'text' => 'Absence Requests',
'icon' => 'fas fa-address-card',
'can' => 'admin.manageAbsences',
'route' => 'absences.index'
],
[
'header' => 'h_app_management',
'can' => ['applications.view.all', 'applications.vote'],
@ -517,17 +547,6 @@ return [
],
],
],
[
'name' => 'DatePickApp',
'active' => true,
'files' => [
[
'type' => 'js',
'asset' => false,
'location' => '/js/datepick.js',
],
],
],
[
'name' => 'Fullcalendar',
'active' => true,
@ -615,5 +634,21 @@ return [
],
],
],
[
'name' => 'Flatpickr',
'active' => true,
'files' => [
[
'type' => 'css',
'asset' => false,
'location' => 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css'
],
[
'type' => 'js',
'asset' => false,
'location' => 'https://cdn.jsdelivr.net/npm/flatpickr'
]
]
]
],
];

View File

@ -0,0 +1,20 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class AbsenceFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
//
];
}
}

View File

@ -1,45 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAppealsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('appeals', function (Blueprint $table) {
$table->id();
$table->bigInteger('appellant_id')->unsigned();
$table->bigInteger('appeal_assignee')->unsigned();
$table->enum('appeal_type', [
'discord_ban',
'discord_timeout'
]);
$table->text('appeal_reasoning_desc');
$table->enum('appeal_status', [
'IN_REVISION',
'AWAITING_DECISION',
'PUNISHMENT_LIFTED',
'PUNISHMENT_REDUCED',
'PUNISHMENT_MAINTAINED'
]);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('appeals');
}
}

View File

@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAbsencesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('absences', function (Blueprint $table) {
$table->id();
$table->bigInteger('requesterID')->unsigned();
$table->date('start');
$table->date('predicted_end');
$table->boolean('available_assist');
$table->string('reason');
$table->enum('status', ['PENDING', 'APPROVED', 'DECLINED', 'CANCELLED', 'ENDED']);
$table->bigInteger('reviewer')->unsigned()->nullable();
$table->date('reviewed_date')->nullable();
$table->timestamps();
$table->foreign('requesterID')
->references('id')
->on('users');
$table->foreign('reviewer')
->references('id')
->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('absences');
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class AbsenceSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//
}
}

View File

@ -59,6 +59,7 @@ class PermissionSeeder extends Seeder
]);
// Spatie wildcard permissions (same concept of MC permissions)
// TODO: Wildcard permissions are not suitable for the app, switch to simpler permission model, starting with permissions for new features
$permissions = [
'applications.submit',
@ -76,6 +77,12 @@ class PermissionSeeder extends Seeder
'profiles.view.others',
'profiles.edit.others',
'admin.viewAllAbsences',
'admin.manageAbsences',
'reviewer.viewAbsence',
'reviewer.requestAbsence',
'reviewer.withdrawAbsence',
'admin.userlist',
'admin.stafflist',
'admin.hiring.forms',
@ -104,7 +111,10 @@ class PermissionSeeder extends Seeder
// Able to view applications and vote on them once they reach the right stage, but not approve applications up to said stage
$reviewer->givePermissionTo([
'applications.view.all',
'applications.vote'
'applications.vote',
'reviewer.viewAbsence',
'reviewer.requestAbsence',
'reviewer.withdrawAbsence',
]);
$hiringManager->givePermissionTo('appointments.*', 'applications.*', 'admin.hiring.*');
@ -117,7 +127,8 @@ class PermissionSeeder extends Seeder
'admin.notificationsettings.*',
'profiles.view.others',
'profiles.edit.others',
'admin.maintenance.logs.view'
'admin.viewAllAbsences',
'admin.manageAbsences',
]);
}
}

10926
public/css/app.css vendored

File diff suppressed because one or more lines are too long

1927
public/css/mixed.css vendored

File diff suppressed because one or more lines are too long

77423
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,8 @@
<div class="alert alert-{{$alertType}} {{$extraStyling ?? ''}}">
<!-- Simplicity is the essence of happiness. - Cedric Bledsoe -->
@if (!empty($title))
<p class="text-bold">@if (!empty($icon))<i class="fas {{ $icon }}"></i> @endif {{ $title }}</p>
@endif
{{$slot}}
</div>

View File

@ -0,0 +1,19 @@
@if (!empty($link))
<a href="{{ $link }}" target="{{ $target ?? '' }}">
<button {{ ($disabled == true) ? 'disabled' : ''}} type="{{ $type }}" class="btn {{ !empty($size) ? 'btn-' . $size : '' }} btn-{{ $color }}" id="{{ $id }}">
@if (empty($icon))
{{ $slot }}
@else
<i class="{{ $icon }}"></i> {{ $slot }}
@endif
</button>
</a>
@else
<button {{ ($disabled == true) ? 'disabled' : ''}} type="{{ $type }}" class="btn {{ !empty($size) ? 'btn-' . $size : '' }} btn-{{ $color }}" id="{{ $id }}">
@if (empty($icon))
{{ $slot }}
@else
<i class="{{ $icon }}"></i> {{ $slot }}
@endif
</button>
@endif

View File

@ -0,0 +1,119 @@
@extends('adminlte::page')
@section('title', config('app.name') . ' | ' . __('Member absence request'))
@section('content_header')
<h4>{{__('Human Resources')}} / {{ __('Staff') }} / {{__('Absence request')}}</h4>
@stop
@section('js')
<x-global-errors></x-global-errors>
<script>
$('#startDate').flatpickr(
{
enableTime: false,
dateFormat: 'Y-m-d',
static: false
}
)
$('#predictedEnd').flatpickr(
{
enableTime: false,
dateFormat: 'Y-m-d',
static: false
}
)
</script>
@stop
@section('content')
<div class="row">
<div class="col">
<form name="submitAbsenceRequest" method="post" action="{{ route('absences.store') }}">
@csrf
<div class="card">
<div class="card-header">
<h4 class="card-title">{{ __('Leave of absence') }}</h4>
</div>
<div class="card-body">
@if ($activeRequest)
<x-alert alert-type="danger" icon="fa-exclamation-triangle" title="{{ __('Submissions locked!') }}">
{{ __('Sorry, but you already have an active leave of absence request. Please cancel (or let expire) your previous request before attempting to make a new one.') }}
</x-alert>
@endif
<div class="form-group">
<label for="reason"><i class="fas fa-clipboard"></i> {{ __('Request reason') }}</label>
<input {{ ($activeRequest) ? 'disabled' : '' }} id="reason" name="reason" type="text" class="form-control" required>
</div>
<div class="form-group">
<div class="row">
<div class="col">
<label for="start_date"><i class="far fa-clock"></i> {{ __('Absence start date') }}</label>
<input {{ ($activeRequest) ? 'disabled' : '' }} type="text" name="start_date" id="startDate" class="form-control" required>
</div>
<div class="col">
<label for="predicted_end"><i class="fas fa-history"></i> {{ __('Predicted end') }}</label>
<input {{ ($activeRequest) ? 'disabled' : '' }} type="text" name="predicted_end" id="predictedEnd" class="form-control" required>
</div>
</div>
</div>
<div>
<input type="hidden" name="available_assist" value="off">
<label><input {{ ($activeRequest) ? 'disabled' : '' }} type="checkbox" name="available_assist"> {{ __('Will you be available to assist occasionally during your absence?') }}</label>
<input type="hidden" name="invalidAbsenceAgreement" value="off">
<label><input {{ ($activeRequest) ? 'disabled' : '' }} type="checkbox" name="invalidAbsenceAgreement"> {{ __('I understand that inactivity/no-show after a declined/expired absence request will be treated according to standard procedure.') }}</label>
</div>
</div>
<div class="card-footer">
<x-button disabled="{{ (bool) $activeRequest }}" id="btnSubmitRequest" type="submit" color="success" icon="fas fa-paper-plane">
{{ __('Submit for approval') }}
</x-button>
</div>
</div>
</form>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<x-alert alert-type="info" title="{{ __('How to request a leave of absence') }}" icon="fa-info-circle">
<p>{{ __('A leave of absence allows you to step away from your duties for a period of time. To request one, simply fill the form to your left, and enter the reason for which you\'re stepping away. You will also need to specify when you will be unavailable, and when you predict to be back.') }}</p
<p>{{ __('You will also need to agree to the terms of a LOA. Additionally, you may also specify whether you\'ll be available to chat occasionally during your absence, but not perform your full duties.') }}</p>
<p>{{ __('You may only have one active request at the same time, which will have to be either approved or declined by the admins. Please keep in mind that you will not be able to delete any of your requests.') }}</p>
</x-alert>
</div>
<div class="card-footer">
<x-button id="btnCancelRequest" color="info" icon="fas fa-info-circle">
{{ __('Cancel request') }}
</x-button>
</div>
</div>
</div>
</div>
@stop

View File

@ -0,0 +1,115 @@
@extends('adminlte::page')
@section('title', config('app.name') . ' | ' . __('Member absence management'))
@section('content_header')
<h4>{{__('Human Resources')}} / {{ __('Admin') }} / {{__('Absence management')}}</h4>
@stop
@section('js')
<x-global-errors></x-global-errors>
@stop
@section('content')
<div class="row">
<div class="col">
<x-alert alert-type="info">
<p class="text-bold"><i class="fas fa-info-circle"></i> {{ __('What is a leave of absence?') }}</p>
<p>{{ __('A leave of absence is a time period in which an employee takes personal time off, for a multitude of reasons. It\'s a prolonged, authorized absence form work and/or other duties, communicated in advance, usually via letter or via an HR system.') }}</p>
<p>{{ __('Here, you\'ll be able to view and approve leave requests from staff members. Notifications are sent out to ensure the right people know about this leave in advance. Staff members may ignore declined leave requests, however, their time off will be considered as a period of inactivity (no-show).') }}</p>
</x-alert>
</div>
</div>
<div class="row">
<div class="col">
<div class="card bg-gray-dark">
<div class="card-header bg-indigo">
<div class="card-title"><h4 class="text-bold">{{__('Leave of absence requests')}}</h4></div>
</div>
<div class="card-body">
@if (true)
<table class="table table-borderless table-active">
<thead>
<tr>
<th>{{__('Requesting user')}}</th>
<th>{{__('Reviewing admin')}}</th>
<th>{{ __('Status') }}</th>
<th>{{ __('Request date') }}</th>
<th>{{ __('Actions') }}</th>
</tr>
</thead>
<tbody>
@foreach($absences as $absence)
<tr>
<td>{{ $absence->requester->name }}</td>
<td><span class="badge badge-warning"><i class="fas fa-exclamation-circle"></i> {{ __('None yet') }}</span></td>
<td>
@switch($absence->status)
@case('PENDING')
<span class="badge badge-warning"><i class="fas fa-clock"></i> {{ __('Pending') }}</span>
@break
@case('APPROVED')
<span class="badge badge-success"><i class="far fa-thumbs-up"></i> {{ __('Approved') }}</span>
@break
@case('DECLINED')
<span class="badge badge-danger"><i class="far fa-thumbs-down"></i> {{ __('Declined') }}</span>
@break
@case('CANCELLED')
<span class="badge badge-secondary"><i class="fas fa-ban"></i> {{ __('Cancelled') }}</span>
@break
@case('ENDED')
<span class="badge badge-info"><i class="fas fa-history"></i> {{ __('Ended') }}</span>
@break
@endswitch
</td>
<td>{{ $absence->created_at }}</td>
<td><button class="btn btn-warning btn-sm"><i class="fas fa-search"></i> Review</button></td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i><span> {{__('No requests')}}</span>
<p>
{{__('There are no registered requests, of any status (declined, approved, pending).')}}
</p>
</div>
@endif
</div>
<div class="card-footer">
<a href="{{ route('absences.create') }}"><button class="btn btn-success btn-sm"><i class="fas fa-plus-circle"></i> New request</button></a>
</div>
</div>
</div>
</div>
@stop

View File

@ -244,6 +244,8 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
Route::patch('staff-members/terminate/{user}', [UserController::class, 'terminate'])
->name('terminateStaffMember');
Route::resource('absences', \App\Http\Controllers\AbsenceController::class);
});
Route::group(['prefix' => 'admin', 'middleware' => ['passwordredirect']], function () {