diff --git a/.editorconfig b/.editorconfig
index 8f0de65..9c841a9 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -15,4 +15,4 @@ trim_trailing_whitespace = false
indent_size = 2
[docker-compose.yml]
-indent_size = 4
+indent_size = 2
diff --git a/.env.example b/.env.example
index 1840fdd..9d4ff87 100644
--- a/.env.example
+++ b/.env.example
@@ -1,41 +1,60 @@
-APP_NAME=Laravel
+APP_NAME=Helpdesk
APP_ENV=local
APP_KEY=
APP_DEBUG=true
+APP_TIMEZONE=Asia/Manila
APP_URL=http://localhost
-APP_SERVICE=ticketing
+APP_SERVICE=helpdesk
+
+APP_LOCALE=en
+APP_FALLBACK_LOCALE=en
+APP_FAKER_LOCALE=en_US
+
+APP_MAINTENANCE_DRIVER=file
+# APP_MAINTENANCE_STORE=database
+
+PHP_CLI_SERVER_WORKERS=4
+
+BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
+LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
-DB_CONNECTION=pgsql
-DB_HOST=pgsql
-DB_PORT=5432
-DB_DATABASE=ticket
-DB_USERNAME=tixer
-DB_PASSWORD=rexit
+DB_CONNECTION=mysql
+DB_HOST=127.0.0.1
+DB_DATABASE=helpdesk
+DB_USERNAME=root
+DB_PASSWORD=
-BROADCAST_DRIVER=log
-CACHE_DRIVER=file
-FILESYSTEM_DISK=local
-QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
+SESSION_ENCRYPT=false
+SESSION_PATH=/
+SESSION_DOMAIN=null
+
+BROADCAST_CONNECTION=log
+FILESYSTEM_DISK=local
+QUEUE_CONNECTION=database
+
+CACHE_STORE=database
+CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
+REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
+MAIL_SCHEME=null
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
-MAIL_ENCRYPTION=null
-MAIL_FROM_ADDRESS="hello@example.com"
+MAIL_FROM_ADDRESS="support@local.dev"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
@@ -44,17 +63,22 @@ AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
-PUSHER_APP_ID=
-PUSHER_APP_KEY=
-PUSHER_APP_SECRET=
-PUSHER_HOST=
-PUSHER_PORT=443
-PUSHER_SCHEME=https
-PUSHER_APP_CLUSTER=mt1
-
VITE_APP_NAME="${APP_NAME}"
-VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
-VITE_PUSHER_HOST="${PUSHER_HOST}"
-VITE_PUSHER_PORT="${PUSHER_PORT}"
-VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
-VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
+
+REVERB_APP_ID=
+REVERB_APP_KEY=
+REVERB_APP_SECRET=
+REVERB_HOST="localhost"
+REVERB_PORT=8080
+REVERB_SCHEME=http
+
+VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
+VITE_REVERB_HOST="${REVERB_HOST}"
+VITE_REVERB_PORT="${REVERB_PORT}"
+VITE_REVERB_SCHEME="${REVERB_SCHEME}"
+VITE_FORWARD_REVERB_PORT="${VITE_REVERB_PORT:-8080}"
+
+SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=${APP_PORT:-80}"
+
+CLAMAV_PREFERRED_SOCKET=tcp_socket
+CLAMAV_TCP_SOCKET=tcp://clamav:3310
diff --git a/.gitattributes b/.gitattributes
index fcb21d3..d74ac08 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -9,3 +9,9 @@
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
+
+*.sh text eol=lf
+
+*.log export-ignore
+*.tmp export-ignore
+*.swp export-ignore
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..3fb5582
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,461 @@
+
')->toHtmlString() : null)
+ ->hidden($this instanceof BulkAction),
+ ]);
+
+ switch (true) {
+ case $this instanceof BulkAction:
+ $this->action(function (Collection $selectedRecords, array $data) {
+ $selectedRecords->toQuery()->update([
+ 'role' => $data['role'],
+ 'organization_id' => $data['organization_id'],
+ 'approved_by' => Auth::id(),
+ 'approved_at' => now(),
+ ]);
+
+ $this->sendSuccessNotification();
+ });
+
+ $this->deselectRecordsAfterCompletion();
+
+ break;
+
+ default:
+ $this->action(function (User $user, array $data) {
+ $organization = match (Filament::getCurrentPanel()->getId()) {
+ 'root' => $data['organization_id'],
+ default => $user->organization_id,
+ };
+
+ $user->forceFill([
+ 'role' => $data['role'],
+ 'organization_id' => $organization,
+ 'approved_by' => Auth::id(),
+ 'approved_at' => now(),
+ ]);
+
+ $user->save();
+
+ $this->sendSuccessNotification();
+ });
+
+ if ($this instanceof Action) {
+ $this->accessSelectedRecords();
+ }
+
+ }
+ }
+}
diff --git a/app/Filament/Actions/Concerns/ChangePassword.php b/app/Filament/Actions/Concerns/ChangePassword.php
new file mode 100644
index 0000000..9cab6ae
--- /dev/null
+++ b/app/Filament/Actions/Concerns/ChangePassword.php
@@ -0,0 +1,81 @@
+process(fn (User $user) => $this->user = $user->exists ? $user : Auth::user());
+
+ $this->name('change-password');
+
+ $this->icon('gmdi-lock');
+
+ $this->visible(fn () => $this->user->hasVerifiedEmail() && $this->user->hasApprovedAccount() && $this->user->hasActiveAccess() && ! $this->user->trashed());
+
+ $this->modalIcon('gmdi-lock');
+
+ $this->modalSubmitActionLabel('Change Password');
+
+ $this->modalDescription($this->user->is(Auth::user()) ? 'You will be logged out after changing your password' : 'Change this account\'s password.');
+
+ $this->modalFooterActionsAlignment(Alignment::Justify);
+
+ $this->modalWidth('md');
+
+ $this->successNotificationTitle('Password changed');
+
+ $this->form([
+ TextInput::make('password')
+ ->password()
+ ->currentPassword()
+ ->rule('required')
+ ->markAsRequired()
+ ->helperText('Enter your current password.')
+ ->visible($this->user->is(Auth::user())),
+ TextInput::make('new_password')
+ ->password()
+ ->rule('required')
+ ->markAsRequired()
+ ->rule(Password::defaults())
+ ->helperText('Enter new password.')
+ ->same('passwordConfirmation'),
+ TextInput::make('passwordConfirmation')
+ ->password()
+ ->rule('required')
+ ->markAsRequired()
+ ->helperText('Confirm new password.')
+ ->dehydrated(false),
+ ]);
+
+ $this->action(function (array $data) {
+ $this->user->forceFill(['password' => $data['new_password']])->save();
+
+ $this->sendSuccessNotification();
+
+ if ($this->user->is(Auth::user())) {
+ Filament::auth()->logout();
+
+ session()->invalidate();
+ session()->regenerate();
+
+ return redirect()->route('filament.auth.auth.login');
+ }
+ });
+ }
+}
diff --git a/app/Filament/Actions/Concerns/DeactivateAccess.php b/app/Filament/Actions/Concerns/DeactivateAccess.php
new file mode 100644
index 0000000..4a6a9a0
--- /dev/null
+++ b/app/Filament/Actions/Concerns/DeactivateAccess.php
@@ -0,0 +1,71 @@
+name('deactivate-user');
+
+ $this->requiresConfirmation();
+
+ $this->visible(fn (User $user) => $user->hasVerifiedEmail() && $user->hasApprovedAccount() && ! $user->trashed());
+
+ $this->label(fn (User $user) => $user->deactivated_at ? 'Reactivate access' : 'Deactivate access');
+
+ $this->icon(fn (User $user) => $user->deactivated_at ? 'gmdi-verified-user-o' : 'gmdi-block-o');
+
+ $this->color(fn (User $user) => $user->deactivated_at ? 'success' : 'warning');
+
+ $this->modalIcon(fn (User $user) => $user->deactivated_at ? 'gmdi-verified-user-o' : 'gmdi-block-o');
+
+ $this->modalSubmitActionLabel(fn (User $user) => $user->deactivated_at ? 'Reactivate' : 'Confirm');
+
+ $this->successNotificationTitle(fn (User $user) => $user->deactivated_at ? 'User deactivated' : 'User reactivated');
+
+ $this->form([
+ TextInput::make('password')
+ ->rule('required')
+ ->markAsRequired()
+ ->password()
+ ->currentPassword()
+ ->helperText('Enter your password to confirm this action.'),
+ ]);
+
+ $this->action(function (User $user) {
+ match (true) {
+ isset($user->deactivated_at) => $user->forceFill([
+ 'deactivated_at' => null,
+ 'deactivated_by' => null,
+ ]),
+ default => $user->forceFill([
+ 'deactivated_at' => now(),
+ 'deactivated_by' => Auth::id(),
+ ]),
+ };
+
+ $user->save();
+
+ $this->sendSuccessNotification();
+ });
+
+ $this->modalDescription(function (User $user) {
+ if (is_null($user->deactivated_at)) {
+ return 'Deactivate this user to revoke their access.';
+ }
+
+ $label = <<Warning !
+ This user has been deactivated by
+
{$user->deactivatedBy?->name} ({$user->deactivatedBy?->email}) on {$user->deactivated_at->format('jS \o\f F \a\t H:i:s')}.
+ HTML;
+
+ return str($label)->toHtmlString();
+ });
+ }
+}
diff --git a/app/Filament/Actions/Concerns/DeleteRequest.php b/app/Filament/Actions/Concerns/DeleteRequest.php
new file mode 100644
index 0000000..ab4d6f5
--- /dev/null
+++ b/app/Filament/Actions/Concerns/DeleteRequest.php
@@ -0,0 +1,45 @@
+successNotificationTitle('Request trashed');
+
+ $this->action(function (): void {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $result = $this->process(static function (Request $request) {
+ return Auth::user()->root
+ ? $request->delete()
+ : $request->delete() && $request->action()->create([
+ 'status' => ActionStatus::TRASHED,
+ 'user_id' => Auth::id(),
+ ]);
+ });
+
+ if (! $result) {
+ $this->failure();
+
+ return;
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->success();
+ } catch (Exception) {
+ $this->rollBackDatabaseTransaction();
+
+ $this->failure();
+ }
+ });
+ }
+}
diff --git a/app/Filament/Actions/Concerns/InviteUser.php b/app/Filament/Actions/Concerns/InviteUser.php
new file mode 100644
index 0000000..6dab4a1
--- /dev/null
+++ b/app/Filament/Actions/Concerns/InviteUser.php
@@ -0,0 +1,137 @@
+name('invite-user');
+
+ $this->icon('gmdi-person-add-o');
+
+ $this->modalIcon('gmdi-person-add-o');
+
+ $this->modalWidth(MaxWidth::Large);
+
+ $this->modalFooterActionsAlignment(Alignment::Right);
+
+ $this->modalDescription('Invite a new user to your organization.');
+
+ $this->form([
+ Radio::make('role')
+ ->required()
+ ->options([
+ UserRole::ADMIN->value => UserRole::ADMIN->getLabel(),
+ UserRole::MODERATOR->value => UserRole::MODERATOR->getLabel(),
+ UserRole::AGENT->value => UserRole::AGENT->getLabel(),
+ UserRole::USER->value => UserRole::USER->getLabel(),
+ ])
+ ->descriptions([
+ UserRole::ADMIN->value => UserRole::ADMIN->getDescription(),
+ UserRole::MODERATOR->value => UserRole::MODERATOR->getDescription(),
+ UserRole::AGENT->value => UserRole::AGENT->getDescription(),
+ UserRole::USER->value => UserRole::USER->getDescription(),
+ ]),
+ TextInput::make('email')
+ ->markAsRequired()
+ ->rules(['required', 'email'])
+ ->helperText('User needs to be registered that is not already a member of your organization.')
+ ->rule(fn () => function ($attribute, $value, $fail) {
+ $user = User::query()
+ ->withoutGlobalScopes()
+ ->where('email', $value)
+ ->first();
+
+ if (is_null($user)) {
+ $fail('User cannot be found.');
+
+ return;
+ }
+
+ if ($user->organization_id === Auth::user()->organization_id) {
+ $fail('This user is already a member of your organization.');
+ }
+ }),
+ Select::make('organization')
+ ->options(Organization::pluck('name', 'id'))
+ ->required()
+ ->searchable()
+ ->visible(Filament::getCurrentPanel()->getId() === 'root')
+ ->placeholder('Select an organization'),
+ ]);
+
+ $this->action(function (array $data) {
+ $user = User::firstWhere('email', $data['email']);
+
+ $organization = Organization::find($data['organization'] ?? Auth::user()->organization_id);
+
+ try {
+ if (RateLimiter::tooManyAttempts("invitation-request:{$user->id}:{$organization->id}", 1)) {
+ throw new TooManyRequestsException(
+ 'invitation-request',
+ 'invite-user',
+ request()->ip(),
+ RateLimiter::availableIn("invitation-request:{$user->id}:{$organization->id}")
+ );
+ }
+ } catch (TooManyRequestsException $exception) {
+ $this->getRateLimitedNotification($exception)?->send();
+
+ return;
+ }
+
+ /** @var User $authenticated */
+ $authenticated = Auth::user();
+
+ $time = now();
+
+ $notification = new InvitationRequest($user, $authenticated, $organization, UserRole::from($data['role']), $time);
+
+ $user->notify($notification);
+
+ Notification::make()
+ ->title('User invited')
+ ->body('The user has been invited to your organization.')
+ ->success()
+ ->actions([AcceptInvitationAction::make()->url($notification->url)])
+ ->sendToDatabase($user, true);
+
+ Notification::make()
+ ->title('User invited')
+ ->success()
+ ->send();
+
+ RateLimiter::hit("invitation-request:{$user->id}:{$organization->id}", now()->addDay());
+ });
+ }
+
+ protected function getRateLimitedNotification(TooManyRequestsException $exception): ?Notification
+ {
+ $next = now()->addSeconds($exception->secondsUntilAvailable)->diffForHumans();
+
+ return Notification::make()
+ ->title('Invitation request limit reached')
+ ->body("A user can only recieve an invitation once a day per organization. Please try again within {$next}.")
+ ->danger();
+ }
+}
diff --git a/app/Filament/Actions/Concerns/NoteDossier.php b/app/Filament/Actions/Concerns/NoteDossier.php
new file mode 100644
index 0000000..458a76e
--- /dev/null
+++ b/app/Filament/Actions/Concerns/NoteDossier.php
@@ -0,0 +1,62 @@
+name('note-dossier');
+
+ $this->label('Note');
+
+ $this->icon('gmdi-edit-note-o');
+
+ $this->slideOver();
+
+ $this->form([
+ MarkdownEditor::make('content')
+ ->disableToolbarButtons(['attachFiles', 'codeBlock', 'blockquote'])
+ ->required(),
+ FileAttachment::make(),
+ ]);
+
+ $this->successNotificationTitle('Note added');
+
+ $this->failureNotificationTitle('Failed to add note');
+
+ $this->closeModalByClickingAway(false);
+
+ $this->action(function (Dossier $dossier, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $note = $dossier->notes()->create([
+ 'content' => $data['content'],
+ 'user_id' => Auth::id(),
+ ]);
+
+ if (count($data['files']) > 0) {
+ $note->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->success();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->failure();
+ }
+ });
+ }
+}
diff --git a/app/Filament/Actions/Concerns/Notifications/CanNotifyUsers.php b/app/Filament/Actions/Concerns/Notifications/CanNotifyUsers.php
new file mode 100644
index 0000000..f0a9ce6
--- /dev/null
+++ b/app/Filament/Actions/Concerns/Notifications/CanNotifyUsers.php
@@ -0,0 +1,105 @@
+hasProperty('requestAction') && $reflection->getProperty('requestAction')->isStatic()) || static::$requestAction === null) {
+ return;
+ }
+
+ $notify = function (User $user, string $heading, ?string $description, ?string $icon, ?string $color) {
+ Notification::make()
+ ->title($heading)
+ ->body($description)
+ ->color($color)
+ ->icon($icon)
+ ->sendToDatabase($user, true);
+ };
+
+ $this->process(function (?Request $record, Authenticatable $authenticated, array $data = []) use ($notify, $request) {
+ $request ??= $record;
+
+ $heading = match (static::$requestAction) {
+ ActionStatus::SUBMITTED => "New {$request->class?->value} received",
+ ActionStatus::ASSIGNED => "New {$request->class->value} request assignment",
+ ActionStatus::STARTED => "{$request->class->getLabel()} request #{$request->code} started",
+ ActionStatus::SUSPENDED => "{$request->class->getLabel()} request #{$request->code} is on hold",
+ ActionStatus::COMPLIED => "{$request->class->getLabel()} request #{$request->code} complied",
+ ActionStatus::COMPLETED => "{$request->class->getLabel()} request #{$request->code} completed",
+ ActionStatus::REJECTED => "{$request->class->getLabel()} request assignment rejected",
+ ActionStatus::REPLIED => "$authenticated->name has replied to ".($authenticated->id !== $request->user_id ? 'your' : 'their')." inquiry #{$request->code}.",
+ ActionStatus::REINSTATED => "{$request->class->getLabel()} request #{$request->code} reinstated",
+ ActionStatus::CLOSED => "Request #{$request->code} has been closed",
+ default => null,
+ };
+
+ if ($heading === null) {
+ return;
+ }
+
+ $description = match (static::$requestAction) {
+ ActionStatus::SUBMITTED => "A new {$request->class?->value} #{$request?->code} has been submitted by {$authenticated->name}.",
+ ActionStatus::ASSIGNED => "{$request->class->getLabel()} #{$request->code} has been assigned to you by {$authenticated->name}.",
+ ActionStatus::STARTED => "{$authenticated->name} is now processing your {$request->class->value} request.",
+ ActionStatus::SUSPENDED => "Please comply with the agent's instructions and or requirements to resume processing your {$request->class->value} request.",
+ ActionStatus::COMPLIED => "{$authenticated->name} has complied with your {$request->class->value} request.",
+ ActionStatus::REINSTATED => "{$authenticated->name} has reinstated the request after suspension.",
+ ActionStatus::COMPLETED => "The request has been completed successfully by {$authenticated->name}.",
+ ActionStatus::REJECTED => "The assignment has been rejected by {$authenticated->name} for {$request->class->value} request #{$request->code}.",
+ ActionStatus::CLOSED => match (static::$requestResolution ?? ActionResolution::tryFrom($data['resolution'])) {
+ ActionResolution::RESOLVED => "The request has been successfully resolved by {$authenticated->name}.",
+ ActionResolution::UNRESOLVED => "The request has been closed by {$authenticated->name} without resolution provided.",
+ ActionResolution::INVALIDATED => "The request has been found invalid by {$authenticated->name}.",
+ ActionResolution::ACKNOWLEDGED => "The request has been acknowledged by {$authenticated->name}.",
+ ActionResolution::CANCELLED => "The request has been canceled by {$authenticated->name}.",
+ default => null,
+ },
+ default => null,
+ };
+
+ $icon = match (static::$requestAction) {
+ ActionStatus::SUBMITTED => 'gmdi-move-to-inbox-o',
+ ActionStatus::STARTED => ActionStatus::IN_PROGRESS->getIcon(),
+ ActionStatus::SUSPENDED => ActionStatus::ON_HOLD->getIcon(),
+ ActionStatus::CLOSED => (static::$requestResolution ?? ActionResolution::tryFrom($data['resolution']))->getIcon(),
+ default => static::$requestAction->getIcon(),
+ };
+
+ $color = match (static::$requestAction) {
+ ActionStatus::CLOSED => (static::$requestResolution ?? ActionResolution::tryFrom($data['resolution']))->getColor(),
+ default => static::$requestAction->getColor(),
+ };
+
+ $users = match (static::$requestAction) {
+ ActionStatus::ASSIGNED => User::find($data['assignees']),
+ ActionStatus::SUBMITTED,
+ ActionStatus::REJECTED => User::where('organization_id', $request->organization_id)->moderator(admin: true)->get(),
+ default => $request->user->is($authenticated) ?
+ $request->assignees :
+ User::find([$request->user_id]),
+ };
+
+ $users->each(fn ($user) => $notify($user, $heading, $description, $icon, $color));
+ });
+ }
+}
diff --git a/app/Filament/Actions/Concerns/RecallRequest.php b/app/Filament/Actions/Concerns/RecallRequest.php
new file mode 100644
index 0000000..33cc0e9
--- /dev/null
+++ b/app/Filament/Actions/Concerns/RecallRequest.php
@@ -0,0 +1,48 @@
+name('recall-request');
+
+ $this->label('Recall');
+
+ $this->icon(ActionStatus::RECALLED->getIcon());
+
+ $this->requiresConfirmation();
+
+ $this->modalHeading('Recall request');
+
+ $this->modalDescription(
+ 'For '.static::$duration.' minutes since the last submission, you may recall this request.
+ This will allow you to make changes before resubmitting the request.
+ Subsequently, recalled requests after '.static::$cancellation.' hours will be automatically cancelled.
+ Are you sure you want to recall this request?'
+ );
+
+ $this->modalIcon(ActionStatus::RECALLED->getIcon());
+
+ $this->successNotificationTitle('Request recalled');
+
+ $this->action(function (Request $request) {
+ $request->actions()->create(['user_id' => Auth::id(), 'status' => ActionStatus::RECALLED]);
+
+ $this->sendSuccessNotification();
+ });
+
+ $this->disabled(fn (Request $request) => $request->action?->status !== ActionStatus::SUBMITTED || $request->action->created_at->addMinutes(static::$duration)->lessThan(now()));
+
+ $this->hidden(fn (Request $request) => $request->action?->status === ActionStatus::RECALLED);
+ }
+}
diff --git a/app/Filament/Actions/Concerns/RestoreRequest.php b/app/Filament/Actions/Concerns/RestoreRequest.php
new file mode 100644
index 0000000..7caed56
--- /dev/null
+++ b/app/Filament/Actions/Concerns/RestoreRequest.php
@@ -0,0 +1,40 @@
+successNotificationTitle('Request restored');
+
+ $this->action(function (Request $request): void {
+ if (! method_exists($request, 'restore')) {
+ $this->failure();
+
+ return;
+ }
+
+ $result = $this->process(static function () use ($request): bool {
+ return Auth::user()->root
+ ? $request->restore()
+ : $request->restore() && $request->actions()->create([
+ 'status' => ActionStatus::RESTORED,
+ 'user_id' => Auth::id(),
+ ]);
+ });
+
+ if (! $result) {
+ $this->failure();
+
+ return;
+ }
+
+ $this->success();
+ });
+ }
+}
diff --git a/app/Filament/Actions/Concerns/ResubmitRequest.php b/app/Filament/Actions/Concerns/ResubmitRequest.php
new file mode 100644
index 0000000..62c6dc4
--- /dev/null
+++ b/app/Filament/Actions/Concerns/ResubmitRequest.php
@@ -0,0 +1,60 @@
+name('resubmit-request');
+
+ $this->label('Resubmit');
+
+ $this->icon('gmdi-publish-o');
+
+ $this->requiresConfirmation();
+
+ $this->modalHeading('Resubmit request');
+
+ $this->modalIcon('gmdi-publish-o');
+
+ $this->successNotificationTitle('Request resubmitted');
+
+ $this->failureNotificationTitle('Failed to resubmit request');
+
+ $this->action(function (Request $request): void {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $request->actions()->create(['user_id' => Auth::id(), 'status' => ActionStatus::SUBMITTED]);
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->visible(fn (Request $request): bool => is_null($request->action) ?: in_array($request->action?->status, [
+ ActionStatus::RECALLED,
+ ActionStatus::RESTORED,
+ ]));
+
+ $this->hidden(fn (Request $request): bool => $request->trashed());
+ }
+}
diff --git a/app/Filament/Actions/Concerns/ShowRequest.php b/app/Filament/Actions/Concerns/ShowRequest.php
new file mode 100644
index 0000000..06abd80
--- /dev/null
+++ b/app/Filament/Actions/Concerns/ShowRequest.php
@@ -0,0 +1,82 @@
+icon('heroicon-o-eye');
+
+ $this->label('Show');
+
+ $this->color('gray');
+
+ $this->slideOver();
+
+ $this->modalIcon(fn (Request $request) => $request->class->getIcon());
+
+ $this->modalIconColor(fn (Request $request) => $request->class->getColor());
+
+ $this->modalHeading(fn (Request $request) => str("Show {$request->class->value} #{$request->code}")->toHtmlString());
+
+ $this->modalDescription(fn (Request $request) => $request->user->name);
+
+ $this->modalFooterActionsAlignment(Alignment::End);
+
+ $this->modalSubmitAction(false);
+
+ $this->modalCancelAction(false);
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->infolist(fn (Request $request) => [
+ TextEntry::make('tags')
+ ->hiddenLabel()
+ ->badge()
+ ->alignEnd()
+ ->hidden(fn (Request $request) => $request->tags->isEmpty())
+ ->color(fn (string $state) => $request->tags->first(fn ($tag) => $tag->name === $state)?->color ?? 'gray')
+ ->state(fn (Request $request) => $request->tags->pluck('name')->toArray()),
+ TextEntry::make('from.name')
+ ->hiddenLabel()
+ ->helperText(fn (Request $request) => $request->user->name),
+ TextEntry::make('action.status')
+ ->hiddenLabel()
+ ->size(TextEntry\TextEntrySize::ExtraSmall)
+ ->state(fn (Request $request) => $request->action->status === ActionStatus::CLOSED ? $request->action->resolution : $request->action->status),
+ TextEntry::make('subject')
+ ->hiddenLabel()
+ ->weight(FontWeight::Bold)
+ ->size(TextEntry\TextEntrySize::Large),
+ TextEntry::make('submitted.created_at')
+ ->hiddenLabel()
+ ->color('gray')
+ ->dateTime('F j, Y \a\t H:i'),
+ ViewEntry::make('body')
+ ->label('Inquiry')
+ ->hiddenLabel(false)
+ ->view('filament.requests.show', [
+ 'request' => $request,
+ ]),
+ ViewEntry::make('responses')
+ ->visible($request->class === RequestClass::INQUIRY)
+ ->view('filament.requests.history', [
+ 'request' => $request,
+ 'chat' => true,
+ 'descending' => false,
+ ]),
+ ]);
+
+ $this->hidden(fn (Request $request) => $request->trashed());
+ }
+}
diff --git a/app/Filament/Actions/Concerns/UpdateRequest.php b/app/Filament/Actions/Concerns/UpdateRequest.php
new file mode 100644
index 0000000..f77249d
--- /dev/null
+++ b/app/Filament/Actions/Concerns/UpdateRequest.php
@@ -0,0 +1,102 @@
+icon('heroicon-o-pencil-square');
+
+ $this->label('Update');
+
+ $this->slideOver();
+
+ $this->modalHeading('Update request');
+
+ $this->modalFooterActionsAlignment(Alignment::End);
+
+ $this->modalSubmitActionLabel('Update');
+
+ $this->modalCancelActionLabel('Cancel');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->successNotificationTitle('Request updated');
+
+ $this->failureNotificationTitle('Failed to update request');
+
+ $this->fillForm(fn (Request $request): array => [
+ 'subject' => $request->subject,
+ 'body' => $request->body,
+ ]);
+
+ $this->form([
+ TextInput::make('subject')
+ ->rule('required')
+ ->markAsRequired()
+ ->extraAttributes([
+ 'x-data' => '{}',
+ 'x-on:input' => 'event.target.value = event.target.value.charAt(0).toUpperCase() + event.target.value.slice(1)',
+ ])
+ ->helperText(fn (Request $request) => 'Be clear and concise about '.match ($request->class) {
+ RequestClass::TICKET => 'the issue you are facing.',
+ RequestClass::SUGGESTION => 'the idea or suggestion you would like to share.',
+ RequestClass::INQUIRY => 'the question you have.',
+ }),
+ MarkdownEditor::make('body')
+ ->required()
+ ->helperText(fn (Request $request) => 'Provide detailed information about '.match ($request->class) {
+ RequestClass::INQUIRY => 'your question, specifying any necessary context for clarity.',
+ RequestClass::SUGGESTION => 'your idea, explaining its benefits and potential impact.',
+ RequestClass::TICKET => 'the issue, including any steps to reproduce it and relevant details.',
+ }),
+ ]);
+
+ $this->action(function (Request $request, array $data): void {
+ if ($request->body === $data['body'] && $request->subject === $data['subject']) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $old = $request->only(['subject', 'body']);
+
+ $request->update($data);
+
+ $request->actions()->create([
+ 'user_id' => Auth::id(),
+ 'status' => ActionStatus::UPDATED,
+ 'remarks' => "#### From: \n\n#### Subject: \n".$old['subject']."\n\n #### Body: \n".$old['body'],
+ ]);
+
+ $this->commitDatabaseTransaction();
+
+ $this->success();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->failure();
+ }
+ });
+
+ $this->hidden(fn (Request $request): bool => $request->trashed());
+
+ $this->visible(fn (Request $request): bool => is_null($request->action) ?: in_array($request->action?->status, [
+ ActionStatus::RECALLED,
+ ActionStatus::SUSPENDED,
+ ActionStatus::RESTORED,
+ ]));
+ }
+}
diff --git a/app/Filament/Actions/Concerns/ViewRequest.php b/app/Filament/Actions/Concerns/ViewRequest.php
new file mode 100644
index 0000000..d9fbb6d
--- /dev/null
+++ b/app/Filament/Actions/Concerns/ViewRequest.php
@@ -0,0 +1,100 @@
+icon('heroicon-o-eye');
+
+ $this->label('View');
+
+ $this->color('gray');
+
+ $this->slideOver();
+
+ $this->modalIcon(fn (Request $request) => $request->class->getIcon());
+
+ $this->modalIconColor(fn (Request $request) => $request->class->getColor());
+
+ $this->modalHeading(fn (Request $request) => str("{$request->class->getLabel()} #{$request->code}")->toHtmlString());
+
+ $this->modalDescription(fn (Request $request) => $request->user->name);
+
+ $this->modalFooterActionsAlignment(Alignment::End);
+
+ $this->modalSubmitAction(false);
+
+ $this->modalCancelAction(false);
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->infolist(fn (Request $request) => [
+ TextEntry::make('tags')
+ ->hiddenLabel()
+ ->badge()
+ ->alignEnd()
+ ->hidden($request->tags->isEmpty())
+ ->color(fn (string $state) => $request->tags->first(fn ($tag) => $tag->name === $state)?->color ?? 'gray')
+ ->state($request->tags->pluck('name')->toArray()),
+ TextEntry::make('from.name')
+ ->hiddenLabel()
+ ->helperText("{$request->submission?->created_at->format('jS \of F \a\t H:i')}"),
+ TextEntry::make('action.status')
+ ->hiddenLabel()
+ ->state($request->action->status === ActionStatus::CLOSED ? $request->action->resolution : $request->action->status),
+ TextEntry::make('subject')
+ ->hiddenLabel()
+ ->weight(FontWeight::Bold),
+ Tabs::make()
+ ->contained(false)
+ ->tabs([
+ Tab::make($request->class === RequestClass::INQUIRY ? 'Replies' : 'Content')
+ ->schema([
+ ViewEntry::make('body')
+ ->view('filament.requests.show', [
+ 'request' => $request,
+ ]),
+ ViewEntry::make('responses')
+ ->visible($request->class === RequestClass::INQUIRY)
+ ->view('filament.requests.history', [
+ 'request' => $request,
+ 'chat' => true,
+ 'descending' => false,
+ ]),
+ ]),
+ Tab::make('Attachments')
+ ->schema([
+ ViewEntry::make('attachments')
+ ->view('filament.requests.attachment', [
+ 'attachment' => $request->attachment,
+ 'request' => $request,
+ ]),
+ ]),
+ Tab::make('History')
+ ->schema([
+ ViewEntry::make('history')
+ ->view('filament.requests.history', [
+ 'request' => $request,
+ 'chat' => false,
+ 'descending' => true,
+ ]),
+ ]),
+ ]),
+ ]);
+
+ $this->hidden(fn (Request $request) => $request->trashed());
+ }
+}
diff --git a/app/Filament/Actions/DeactivateAccessAction.php b/app/Filament/Actions/DeactivateAccessAction.php
new file mode 100644
index 0000000..ed0990c
--- /dev/null
+++ b/app/Filament/Actions/DeactivateAccessAction.php
@@ -0,0 +1,18 @@
+bootDeactivateUser();
+ }
+}
diff --git a/app/Filament/Actions/DeclineRequestAction.php b/app/Filament/Actions/DeclineRequestAction.php
deleted file mode 100644
index 4596777..0000000
--- a/app/Filament/Actions/DeclineRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-bootInviteUser();
+ }
+}
diff --git a/app/Filament/Actions/NewRequestPromptAction.php b/app/Filament/Actions/NewRequestPromptAction.php
new file mode 100644
index 0000000..bc44fde
--- /dev/null
+++ b/app/Filament/Actions/NewRequestPromptAction.php
@@ -0,0 +1,63 @@
+name('new-request-prompt');
+
+ $this->label(fn () => "New {$this->class->value}");
+
+ $this->modalIcon('heroicon-o-plus');
+
+ $this->modalWidth(MaxWidth::Large);
+
+ $this->modalDescription('Which organization do you want to create a new request for?');
+
+ $this->modalSubmitActionLabel('Proceed');
+
+ $this->form(function () {
+ $organizations = Organization::query()
+ ->whereHas('subcategories')
+ ->get(['organizations.name', 'organizations.code', 'organizations.id'])
+ ->mapWithKeys(fn ($organization) => [$organization->id => "{$organization->code} — {$organization->name}"])
+ ->toArray();
+
+ return [
+ Select::make('organization')
+ ->hiddenLabel()
+ ->options($organizations)
+ ->default(count($organizations) === 1 ? key($organizations) : null)
+ ->placeholder('Select an organization')
+ ->required()
+ ->validationMessages([
+ 'required' => 'Please select an organization.',
+ ]),
+ ];
+ });
+
+ $this->action(function (Component $livewire, array $data) {
+ $this->redirect($livewire->getResource()::getUrl('new', ['record' => $data['organization']]));
+ });
+ }
+
+ public function class(RequestClass $class): static
+ {
+ $this->class = $class;
+
+ return $this;
+ }
+}
diff --git a/app/Filament/Actions/NoteDossierAction.php b/app/Filament/Actions/NoteDossierAction.php
new file mode 100644
index 0000000..4ed99f0
--- /dev/null
+++ b/app/Filament/Actions/NoteDossierAction.php
@@ -0,0 +1,18 @@
+bootTraitDossierAction();
+ }
+}
diff --git a/app/Filament/Actions/Notifications/AcceptInvitationAction.php b/app/Filament/Actions/Notifications/AcceptInvitationAction.php
new file mode 100644
index 0000000..b70fb37
--- /dev/null
+++ b/app/Filament/Actions/Notifications/AcceptInvitationAction.php
@@ -0,0 +1,27 @@
+name('accept-invitation');
+
+ $this->markAsUnread();
+ }
+
+ public function for(User $user): static
+ {
+ $this->user = $user;
+
+ return $this;
+ }
+}
diff --git a/app/Filament/Actions/PublishRequestAction.php b/app/Filament/Actions/PublishRequestAction.php
deleted file mode 100644
index efef68d..0000000
--- a/app/Filament/Actions/PublishRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-name('acknowledge-request');
+
+ $this->label('Acknowledge');
+
+ $this->slideOver();
+
+ $this->icon(ActionResolution::ACKNOWLEDGED->getIcon());
+
+ $this->modalIcon(ActionResolution::ACKNOWLEDGED->getIcon());
+
+ $this->modalHeading('Acknowledge request');
+
+ $this->modalDescription('Close and acknowledge this request.');
+
+ $this->modalSubmitActionLabel('Confirm');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->successNotificationTitle('Request acknowledged');
+
+ $this->failureNotificationTitle('Request acknowledgement failed');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->required(),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'status' => ActionStatus::CLOSED,
+ 'user_id' => Auth::id(),
+ 'remarks' => $data['remarks'],
+ 'resolution' => ActionResolution::ACKNOWLEDGED,
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->notifyUsers();
+
+ $this->sendSuccessNotification();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->hidden(fn (Request $record) => $record->action->status === ActionStatus::CLOSED);
+
+ $this->visible(fn (Request $record) => $record->class === RequestClass::SUGGESTION);
+ }
+}
diff --git a/app/Filament/Actions/Tables/AdjustRequestAction.php b/app/Filament/Actions/Tables/AdjustRequestAction.php
deleted file mode 100644
index efb7ce6..0000000
--- a/app/Filament/Actions/Tables/AdjustRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-bootApproveUser();
+ }
+}
diff --git a/app/Filament/Actions/Tables/ApproveAccountBulkAction.php b/app/Filament/Actions/Tables/ApproveAccountBulkAction.php
new file mode 100644
index 0000000..3a7fa69
--- /dev/null
+++ b/app/Filament/Actions/Tables/ApproveAccountBulkAction.php
@@ -0,0 +1,18 @@
+bootApproveUser();
+ }
+}
diff --git a/app/Filament/Actions/Tables/ApproveRequestAction.php b/app/Filament/Actions/Tables/ApproveRequestAction.php
deleted file mode 100644
index 634d1dd..0000000
--- a/app/Filament/Actions/Tables/ApproveRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-name('assign-request');
+
+ $this->label(fn (Request $request) => $request->action->status === ActionStatus::ASSIGNED ? 'Reassign' : 'Assign');
+
+ $this->icon(ActionStatus::ASSIGNED->getIcon());
+
+ $this->slideOver();
+
+ $this->modalIcon(ActionStatus::ASSIGNED->getIcon());
+
+ $this->modalHeading(fn (Request $request) => ($request->action->status === ActionStatus::ASSIGNED ? 'Reassign' : 'Assign').' request');
+
+ $this->modalDescription('Please select support users to assign this request to.');
+
+ $this->modalContent(fn (Request $request) => $request->organization->users()->agent(moderators: true, admin: Auth::user()->role !== UserRole::MODERATOR)->doesntExist() ? str('No support users found')->toHtmlString() : null);
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->modalSubmitAction(fn (Request $request) => $request->organization->users()->agent(moderators: true, admin: Auth::user()->role !== UserRole::MODERATOR)->exists() ? null : false);
+
+ $this->modalSubmitActionLabel('Assign');
+
+ $this->successNotificationTitle(fn (Request $request) => $request->action->status === ActionStatus::ASSIGNED ? 'Request reassigned' : 'Request assigned');
+
+ $this->failureNotificationTitle('Failed to assign request');
+
+ $this->fillForm(fn (Request $request) => [
+ 'declination' => $request->declination,
+ 'assignees' => $request->assignees->pluck('id')->toArray(),
+ ]);
+
+ $this->form(fn (Request $request) => $request->organization->users()->agent(moderators: true, admin: Auth::user()->role !== UserRole::MODERATOR)->approvedAccount()->exists() ? [
+ Toggle::make('declination')
+ ->label('Allow declination')
+ ->helperText('Allow assignees to have the option to decline the assignment')
+ ->default(true),
+ // MarkdownEditor::make('remarks'),
+ CheckboxList::make('assignees')
+ ->required()
+ ->searchable()
+ ->exists('users', 'id')
+ ->options(
+ $request->organization->users()
+ ->agent(moderators: true, admin: Auth::user()->role !== UserRole::MODERATOR)
+ ->approvedAccount()
+ ->sortByRole(false)
+ ->get(['id', 'name', 'role'])
+ ->mapWithKeys(fn ($user) => [$user->id => "{$user->name} ({$user->role->getLabel()}) ".(Auth::id() === $user->id ? '(you)' : '')])
+ ->toArray()
+ )
+ ->descriptions($request->organization->users()->agent(moderators: true, admin: Auth::user()->role !== UserRole::MODERATOR)->approvedAccount()->pluck('designation', 'id')->toArray()),
+ ] : []);
+
+ $this->action(function (Request $request, array $data) {
+ if (
+ $request->assignees->pluck('id')->diff($data['assignees'])->isEmpty() &&
+ collect($data['assignees'])->diff($request->assignees->pluck('id')->toArray())->isEmpty()
+ ) {
+ Notification::make()
+ ->info()
+ ->title('No changes made')
+ ->body('As there are no changes to assignees, action was not performed.')
+ ->send();
+
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $request->update([
+ 'declination' => $data['declination'],
+ ]);
+
+ $request->actions()->create([
+ 'status' => ActionStatus::ASSIGNED,
+ 'user_id' => Auth::id(),
+ 'remarks' => User::select('id')
+ ->find($data['assignees'])
+ ->map(fn (User $user) => ['id' => "* {$user->id}"])
+ ->implode('id', "\n"),
+ ]);
+
+ $request->assignees()->sync(
+ collect($data['assignees'])->mapWithKeys(function (string $assigned) use ($request) {
+ $assignee = $request->assignees->first(fn (User $assignee) => $assignee->id === $assigned);
+
+ return [$assigned => [
+ 'assigner_id' => Auth::id(),
+ 'response' => $assignee?->pivot->response,
+ 'responded_at' => $assignee?->pivot->responded_at,
+ 'created_at' => $assignee?->pivot->created_at ?? now(),
+ ]];
+ }),
+ );
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollBackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->visible(function (Request $request) {
+ return match ($request->class) {
+ RequestClass::TICKET => in_array($request->action->status, [
+ ActionStatus::QUEUED,
+ ActionStatus::ASSIGNED,
+ ActionStatus::SUSPENDED,
+ ActionStatus::REINSTATED,
+ ActionStatus::COMPLIED,
+ ]),
+ RequestClass::INQUIRY => in_array($request->action->status, [
+ ActionStatus::REPLIED,
+ ActionStatus::SUBMITTED,
+ ActionStatus::ASSIGNED,
+ ]),
+ RequestClass::SUGGESTION => in_array($request->action->status, [
+ ActionStatus::SUBMITTED,
+ ActionStatus::ASSIGNED,
+ ]),
+ default => false,
+ };
+ });
+ }
}
diff --git a/app/Filament/Actions/Tables/CancelRequestAction.php b/app/Filament/Actions/Tables/CancelRequestAction.php
new file mode 100644
index 0000000..3af9afe
--- /dev/null
+++ b/app/Filament/Actions/Tables/CancelRequestAction.php
@@ -0,0 +1,106 @@
+name('cancel-request');
+
+ $this->label('Cancel');
+
+ $this->slideOver();
+
+ $this->icon(ActionResolution::CANCELLED->getIcon());
+
+ $this->modalIcon(ActionResolution::CANCELLED->getIcon());
+
+ $this->modalHeading('Cancel request');
+
+ $this->modalDescription('Cancel this request to prevent further processing.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->closeModalByClickingAway(false);
+
+ $this->successNotificationTitle('Request cancelled');
+
+ $this->failureNotificationTitle('Request closure failed');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->required()
+ ->helperText('Please provide a reason for cancelling this request.'),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'status' => ActionStatus::CLOSED,
+ 'resolution' => ActionResolution::CANCELLED,
+ 'remarks' => $data['remarks'],
+ 'user_id' => Auth::id(),
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollBackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->disabled(function (Request $request) {
+ return match ($request->class) {
+ RequestClass::TICKET => $request->action->created_at->addHours(24)->greaterThan(now()) &&
+ in_array($request->action->status, [
+ ActionStatus::COMPLETED,
+ ActionStatus::CLOSED,
+ ]),
+ RequestClass::INQUIRY => $request->action->created_at->addHours(24)->greaterThan(now()) &&
+ in_array($request->action->status, [
+ ActionStatus::CLOSED,
+ ]),
+ RequestClass::SUGGESTION => $request->action->created_at->addHours(24)->greaterThan(now()) &&
+ in_array($request->action->status, [
+ ActionStatus::CLOSED,
+ ]),
+ };
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/CloseRequestAction.php b/app/Filament/Actions/Tables/CloseRequestAction.php
new file mode 100644
index 0000000..bf07b3d
--- /dev/null
+++ b/app/Filament/Actions/Tables/CloseRequestAction.php
@@ -0,0 +1,106 @@
+name('close-request');
+
+ $this->label('Close');
+
+ $this->slideOver();
+
+ $this->icon(ActionStatus::CLOSED->getIcon());
+
+ $this->modalIcon(ActionStatus::CLOSED->getIcon());
+
+ $this->modalHeading('Close request');
+
+ $this->modalDescription('Close this request prematurely.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->closeModalByClickingAway(false);
+
+ $this->successNotificationTitle('Request closed');
+
+ $this->failureNotificationTitle('Request closure failed');
+
+ $this->form([
+ Radio::make('resolution')
+ ->options([
+ ActionResolution::UNRESOLVED->value => ActionResolution::UNRESOLVED->getLabel(),
+ ActionResolution::INVALIDATED->value => ActionResolution::INVALIDATED->getLabel(),
+ ])
+ ->descriptions([
+ ActionResolution::UNRESOLVED->value => ActionResolution::UNRESOLVED->getDescription(),
+ ActionResolution::INVALIDATED->value => ActionResolution::INVALIDATED->getDescription(),
+ ])
+ ->required()
+ ->live()
+ ->afterStateUpdated(function ($state, $old, $set) {
+ $set('remarks', ActionResolution::from($state)->remarks());
+ }),
+ MarkdownEditor::make('remarks')
+ ->helperText('Please provide a brief reason for closing this request.')
+ ->required(fn () => $this->remarksRequired),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $request->actions()->create([
+ 'status' => ActionStatus::CLOSED,
+ 'resolution' => $data['resolution'],
+ 'remarks' => $data['remarks'],
+ 'user_id' => Auth::id(),
+ ]);
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception $ex) {
+ $this->rollBackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+
+ throw $ex;
+ }
+ });
+
+ $this->hidden(fn (Request $request) => $request->action->status->finalized() ?: $request->action->status === ActionStatus::COMPLETED);
+ }
+
+ public function requireRemarks(bool $required = true)
+ {
+ $this->remarksRequired = $required;
+
+ return $this;
+ }
+}
diff --git a/app/Filament/Actions/Tables/CompileRequestAction.php b/app/Filament/Actions/Tables/CompileRequestAction.php
new file mode 100644
index 0000000..90e37e1
--- /dev/null
+++ b/app/Filament/Actions/Tables/CompileRequestAction.php
@@ -0,0 +1,78 @@
+name('compile-request');
+
+ $this->label('Compile');
+
+ $this->modalDescription('Compile this request into a dossier.');
+
+ $this->icon(Dossiers::getNavigationIcon());
+
+ $this->modalIcon(Dossiers::getNavigationIcon());
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->modalSubmitActionLabel('Compile');
+
+ $this->slideOver();
+
+ $this->form([
+ Select::make('dossier')
+ ->required()
+ ->relationship(
+ 'dossiers', 'name',
+ fn ($query, $record) => $query->whereDoesntHave('requests', function ($query) use ($record) {
+ $query->where('requests.id', $record->id);
+ })
+ )
+ ->preload()
+ ->searchable()
+ ->noSearchResultsMessage('No dossiers found.')
+ ->placeholder('Select a dossier')
+ ->createOptionForm(fn (HasForms $livewire) => [
+ ...AllDossierResource::form(new Form($livewire))->getComponents(),
+ Hidden::make('organization_id')
+ ->default(Auth::user()->organization_id),
+ ])
+ ->createOptionAction(function ($action) {
+ $action->label('New dossier')
+ ->modalHeading('Create new dossier')
+ ->slideOver()
+ ->modalWidth(MaxWidth::ExtraLarge);
+ })
+ ->rule(fn (Request $request) => function ($a, $v, $f) use ($request) {
+ if ($request->dossiers()->where('dossier_id', $v)->exists()) {
+ return 'This dossier already exists in this request.';
+ }
+ }),
+ ]);
+
+ $this->action(function (array $data) {
+ Notification::make()
+ ->title('Request compiled to '.Dossier::find($data['dossier'])->name)
+ ->success()
+ ->send();
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/CompleteRequestAction.php b/app/Filament/Actions/Tables/CompleteRequestAction.php
new file mode 100644
index 0000000..4d170ff
--- /dev/null
+++ b/app/Filament/Actions/Tables/CompleteRequestAction.php
@@ -0,0 +1,93 @@
+name('complete-request');
+
+ $this->label('Complete');
+
+ $this->slideOver();
+
+ $this->icon(ActionStatus::COMPLETED->getIcon());
+
+ $this->modalIcon(ActionStatus::COMPLETED->getIcon());
+
+ $this->modalHeading('Complete Request');
+
+ $this->modalDescription('Mark this request as completed. Requester may reopen this request if needed.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->modalSubmitActionLabel('Confirm');
+
+ $this->successNotificationTitle('Request completed');
+
+ $this->failureNotificationTitle('Request completion failed');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->helperText('Please describe the reason for suspending this request.')
+ ->required(),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'status' => ActionStatus::COMPLETED,
+ 'user_id' => Auth::id(),
+ 'remarks' => $data['remarks'],
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->notifyUsers();
+
+ $this->sendSuccessNotification();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->closeModalByClickingAway(false);
+
+ $this->visible(fn (Request $request) => in_array($request->action->status, [
+ ActionStatus::STARTED,
+ ActionStatus::REOPENED,
+ ActionStatus::REPLIED,
+ ActionStatus::REINSTATED,
+ ActionStatus::SUSPENDED,
+ ]));
+ }
+}
diff --git a/app/Filament/Actions/Tables/CompliedRequestAction.php b/app/Filament/Actions/Tables/CompliedRequestAction.php
deleted file mode 100644
index db8892b..0000000
--- a/app/Filament/Actions/Tables/CompliedRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-name('comply-request');
+
+ $this->label('Comply');
+
+ $this->icon(ActionStatus::COMPLIED->getIcon());
+
+ $this->modalIcon(ActionStatus::COMPLIED->getIcon());
+
+ $this->successNotificationTitle('Action success');
+
+ $this->failureNotificationTitle('Action failed');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->required(),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ if ($request->action->status !== ActionStatus::SUSPENDED) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'status' => ActionStatus::COMPLIED,
+ 'user_id' => Auth::id(),
+ 'remarks' => $data['remarks'],
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->visible(function (Request $request) {
+ return $request->action->status === ActionStatus::SUSPENDED;
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/DeactivateAccessAction.php b/app/Filament/Actions/Tables/DeactivateAccessAction.php
new file mode 100644
index 0000000..e5c7a3e
--- /dev/null
+++ b/app/Filament/Actions/Tables/DeactivateAccessAction.php
@@ -0,0 +1,18 @@
+bootDeactivateUser();
+ }
+}
diff --git a/app/Filament/Actions/Tables/DeclineRequestAction.php b/app/Filament/Actions/Tables/DeclineRequestAction.php
deleted file mode 100644
index b1a812f..0000000
--- a/app/Filament/Actions/Tables/DeclineRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-bootDeleteRequest();
+ }
+}
diff --git a/app/Filament/Actions/Tables/DenyCompletedAction.php b/app/Filament/Actions/Tables/DenyCompletedAction.php
deleted file mode 100644
index 1ef2f63..0000000
--- a/app/Filament/Actions/Tables/DenyCompletedAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-name('generate-feedback-report');
+
+ $this->label('Generate');
+
+ $this->icon('gmdi-picture-as-pdf');
+
+ $this->action(function ($records){
+ GenerateFeedbackForm::dispatch($records);
+ // try{
+ // }catch(\Exception $e){
+ // Notification::make()
+ // ->title('Failed to generate feedback report')
+ // ->body($e->getMessage())
+ // ->danger()
+ // ->send();
+ // }
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/InvalidateRequestAction.php b/app/Filament/Actions/Tables/InvalidateRequestAction.php
new file mode 100644
index 0000000..f667687
--- /dev/null
+++ b/app/Filament/Actions/Tables/InvalidateRequestAction.php
@@ -0,0 +1,91 @@
+name('invalidate-request');
+
+ $this->label('Invalidate');
+
+ $this->slideOver();
+
+ $this->icon(ActionResolution::INVALIDATED->getIcon());
+
+ $this->modalIcon(ActionResolution::INVALIDATED->getIcon());
+
+ $this->modalHeading('Invalidate request');
+
+ $this->modalDescription('Close and invalidate this request if you find it invalid so it will not be processed any further.');
+
+ $this->modalSubmitActionLabel('Confirm');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->successNotificationTitle('Request invalidated');
+
+ $this->failureNotificationTitle('Request invalidation failed');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->required(),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'status' => ActionStatus::CLOSED,
+ 'user_id' => Auth::id(),
+ 'remarks' => $data['remarks'],
+ 'resolution' => ActionResolution::INVALIDATED,
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->notifyUsers();
+
+ $this->sendSuccessNotification();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->hidden(fn (Request $record) => $record->action->status === ActionStatus::CLOSED);
+
+ $this->visible(fn (Request $record) => $record->class === RequestClass::SUGGESTION);
+ }
+}
diff --git a/app/Filament/Actions/Tables/NoteDossierAction.php b/app/Filament/Actions/Tables/NoteDossierAction.php
new file mode 100644
index 0000000..3a4ad8e
--- /dev/null
+++ b/app/Filament/Actions/Tables/NoteDossierAction.php
@@ -0,0 +1,18 @@
+bootTraitDossierAction();
+ }
+}
diff --git a/app/Filament/Actions/Tables/PublishRequestAction.php b/app/Filament/Actions/Tables/PublishRequestAction.php
deleted file mode 100644
index 2eab808..0000000
--- a/app/Filament/Actions/Tables/PublishRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-name('queue');
+
+ $this->label('Queue');
+
+ $this->color(ActionStatus::QUEUED->getColor());
+
+ $this->icon(ActionStatus::QUEUED->getIcon());
+
+ $this->visible(fn (Request $request) => $request->action->status === ActionStatus::SUBMITTED);
+
+ $this->action(function (Request $request) {
+ if ($request->action->status !== ActionStatus::SUBMITTED) {
+ return;
+ }
+
+ $request->actions()->create([
+ 'status' => ActionStatus::QUEUED,
+ 'user_id' => Auth::id(),
+ ]);
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/RecallRequestAction.php b/app/Filament/Actions/Tables/RecallRequestAction.php
new file mode 100644
index 0000000..40d28f4
--- /dev/null
+++ b/app/Filament/Actions/Tables/RecallRequestAction.php
@@ -0,0 +1,18 @@
+bootRecallRequest();
+ }
+}
diff --git a/app/Filament/Actions/Tables/RecategorizeRequestAction.php b/app/Filament/Actions/Tables/RecategorizeRequestAction.php
new file mode 100644
index 0000000..5ffb023
--- /dev/null
+++ b/app/Filament/Actions/Tables/RecategorizeRequestAction.php
@@ -0,0 +1,93 @@
+name('recategorize-request');
+
+ $this->label('Recategorize');
+
+ $this->icon(ActionStatus::RECATEGORIZED->getIcon());
+
+ $this->modalIcon(ActionStatus::RECATEGORIZED->getIcon());
+
+ $this->modalDescription('Move this request to a different category and/or subcategory.');
+
+ $this->modalWidth(MaxWidth::Large);
+
+ $this->modalAlignment(Alignment::Left);
+
+ $this->modalFooterActionsAlignment(Alignment::Right);
+
+ $this->modalSubmitActionLabel('Confirm');
+
+ $this->successNotificationTitle('Request recategorized');
+
+ $this->fillForm(fn (Request $request) => ['category' => $request->subcategory_id]);
+
+ $this->form(fn (Request $request) => [
+ Select::make('category')
+ ->hiddenLabel()
+ ->options(
+ $request->organization
+ ->subcategories
+ ->load('category')
+ ->groupBy('category.name')
+ ->mapWithKeys(fn ($subs, $cat) => [
+ $cat => $subs->pluck('name', 'id')
+ ->map(fn ($sub) => $cat !== $sub ? "$cat — $sub" : $sub)
+ ->toArray(),
+ ])
+ )
+ ->disableOptionWhen(fn (string $value) => $value === $request->subcategory_id)
+ ->required()
+ ->placeholder(null),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $subcategory = Subcategory::find($data['category']);
+
+ $category = $subcategory->category;
+
+ $request->update([
+ 'category_id' => $category->id,
+ 'subcategory_id' => $subcategory->id,
+ ]);
+
+ $request->action()->create([
+ 'status' => ActionStatus::RECATEGORIZED,
+ 'user_id' => Auth::id(),
+ 'remarks' => "From *{$request->category->id} — **{$request->subcategory->id} to *{$category->id} — **{$subcategory->id}",
+ ]);
+
+ $this->commitDatabaseTransaction();
+
+ $this->success();
+ } catch (Exception) {
+ $this->rollBackDatabaseTransaction();
+
+ $this->failure();
+ }
+ });
+
+ $this->hidden(fn (Request $request) => $request->action?->status->finalized());
+ }
+}
diff --git a/app/Filament/Actions/Tables/ReclassifyRequestAction.php b/app/Filament/Actions/Tables/ReclassifyRequestAction.php
new file mode 100644
index 0000000..2438d4f
--- /dev/null
+++ b/app/Filament/Actions/Tables/ReclassifyRequestAction.php
@@ -0,0 +1,86 @@
+name('reclassify-request');
+
+ $this->label('Reclassify');
+
+ $this->icon(ActionStatus::RECLASSIFIED->getIcon());
+
+ $this->slideOver();
+
+ $this->modalIcon(ActionStatus::RECLASSIFIED->getIcon());
+
+ $this->modalDescription('Reclassify this request to best fit its nature.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->successNotificationTitle('Request reclassified');
+
+ $this->form(fn (Request $request) => [
+ Radio::make('class')
+ ->options(RequestClass::class)
+ ->default($request->class)
+ ->disableOptionWhen(fn (string $value) => $value === $request->class->value)
+ ->rule('required')
+ ->markAsRequired()
+ ->rule(fn () => function ($attribute, $value, $fail) use ($request) {
+ if ($value === $request->class) {
+ $fail('The request is already classified as '.$request->class->getLabel().'.');
+ }
+ }),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $old = $request->class;
+
+ $new = $data['class'];
+
+ $request->update([
+ 'class' => $new,
+ ]);
+
+ $request->actions()->create([
+ 'status' => ActionStatus::RECLASSIFIED,
+ 'user_id' => Auth::id(),
+ 'remarks' => 'From *'.$old->getLabel().'* to *'.$new->getLabel().'*',
+ ]);
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+ } catch (Exception $e) {
+ $this->rollbackDatabaseTransaction();
+
+ throw $e;
+ }
+ });
+
+ $this->hidden(function (Request $request) {
+ return $request->action?->status->finalized() ?:
+ $request->actions->some(fn (\App\Models\Action $action) => in_array($action->status, [
+ ActionStatus::STARTED,
+ ActionStatus::REPLIED,
+ ], true));
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/ReinstateRequestAction.php b/app/Filament/Actions/Tables/ReinstateRequestAction.php
new file mode 100644
index 0000000..e52c408
--- /dev/null
+++ b/app/Filament/Actions/Tables/ReinstateRequestAction.php
@@ -0,0 +1,102 @@
+name('reinstate-request');
+
+ $this->label('Reinstate');
+
+ $this->icon(static::$requestAction->getIcon());
+
+ $this->modalIcon(static::$requestAction->getIcon());
+
+ $this->slideOver();
+
+ $this->modalHeading('Reinstate Request');
+
+ $this->modalDescription('Reinstating a request removes its suspension status and immediately resumes the process.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->closeModalByClickingAway(false);
+
+ $this->modalSubmitActionLabel('Confirm');
+
+ $this->successNotificationTitle('Request reinstated');
+
+ $this->failureNotificationTitle('Failed to reinstate request');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->label('Reason')
+ ->required()
+ ->helperText('Please provide a reason for reinstating this request.'),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ if ($request->class !== RequestClass::TICKET || $request->action->status === static::$requestAction) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'status' => static::$requestAction,
+ 'user_id' => Auth::id(),
+ 'remarks' => $data['remarks'],
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->visible(function (Request $request) {
+ if ($request->action->status->finalized()) {
+ return false;
+ }
+
+ return in_array($request->action->status, [ActionStatus::SUSPENDED]) && match ($request->class) {
+ RequestClass::TICKET => $request->assignees->contains(Auth::user()) || Auth::user()->admin,
+ default => false,
+ };
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/RejectAssignmentAction.php b/app/Filament/Actions/Tables/RejectAssignmentAction.php
deleted file mode 100644
index cf40d3b..0000000
--- a/app/Filament/Actions/Tables/RejectAssignmentAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-name('reject-request');
+
+ $this->label('Reject');
+
+ $this->slideOver();
+
+ $this->icon(ActionStatus::REJECTED->getIcon());
+
+ $this->modalIcon(ActionStatus::REJECTED->getIcon());
+
+ $this->modalDescription('Reject this request assignment.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->successNotificationTitle('Request assignment rejected');
+
+ $this->failureNotificationTitle('Request assignment rejection failed');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->label('Reason')
+ ->required()
+ ->helperText('Please provide a valid reason for rejecting this request assignment.'),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ if ($request->action->status !== ActionStatus::ASSIGNED) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $request->assignees()->detach(Auth::id());
+
+ $action = $request->actions()->create([
+ 'remarks' => $data['remarks'],
+ 'status' => ActionStatus::REJECTED,
+ 'user_id' => Auth::id(),
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->closeModalByClickingAway(false);
+
+ $this->visible(fn (Request $request) => $request->declination &&
+ $request->assignees->first(fn (User $user) => $user->id === Auth::id()) &&
+ $request->assignees->count() > 1 &&
+ $request->action->status === ActionStatus::ASSIGNED,
+ );
+ }
+}
diff --git a/app/Filament/Actions/Tables/ReopenRequestAction.php b/app/Filament/Actions/Tables/ReopenRequestAction.php
new file mode 100644
index 0000000..5c421be
--- /dev/null
+++ b/app/Filament/Actions/Tables/ReopenRequestAction.php
@@ -0,0 +1,85 @@
+name('reopen-request');
+
+ $this->label('Reopen');
+
+ $this->slideOver();
+
+ $this->icon(ActionStatus::REOPENED->getIcon());
+
+ $this->modalIcon(ActionStatus::REOPENED->getIcon());
+
+ $this->modalDescription('Reopen this request if you think it has not been resolved yet.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->successNotificationTitle('Request reopened');
+
+ $this->failureNotificationTitle('Request reopening failed');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->required()
+ ->helperText('Please provide a brief reason for reopening this request.'),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ if ($request->action->status !== ActionStatus::COMPLETED) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'remarks' => $data['remarks'],
+ 'status' => ActionStatus::REOPENED,
+ 'user_id' => Auth::id(),
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->visible(fn (Request $request) => $request->action->status === ActionStatus::COMPLETED);
+ }
+}
diff --git a/app/Filament/Actions/Tables/ReplyRequestAction.php b/app/Filament/Actions/Tables/ReplyRequestAction.php
new file mode 100644
index 0000000..d66f641
--- /dev/null
+++ b/app/Filament/Actions/Tables/ReplyRequestAction.php
@@ -0,0 +1,135 @@
+name('reply-request');
+
+ $this->label('Reply');
+
+ $this->icon(ActionStatus::REPLIED->getIcon());
+
+ $this->modalIcon(ActionStatus::REPLIED->getIcon());
+
+ $this->modalDescription(fn (Request $request) => str('Reply to user\'s inquiry #'.$request->code.'')->toHtmlString());
+
+ $this->slideOver();
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->successNotificationTitle(function (Request $request) {
+ $pronoun = match (Filament::getCurrentPanel()->getId()) {
+ 'user' => 'your',
+ default => 'the',
+ };
+
+ return 'You have replied to '.$pronoun.' inquiry #'.$request->code.'';
+ });
+
+ $this->failureNotificationTitle('An error occurred while replying to this inquiry');
+
+ $this->form(fn (Request $request) => [
+ Placeholder::make('tags')
+ ->content(function () use ($request) {
+ if ($request->tags->isEmpty()) {
+ return '(none)';
+ }
+
+ $tags = $request->tags->map(function ($tag) {
+ return <<
+ {$tag->name}
+
+ HTML;
+ })->join(PHP_EOL);
+
+ $html = Blade::render(<<
+ {$tags}
+
+ HTML);
+
+ return str($html)->toHtmlString();
+ }),
+ MarkdownEditor::make('response')
+ ->label('Message')
+ ->required(),
+ FileAttachment::make(),
+ Placeholder::make('responses')
+ ->hidden($request->actions()->where('status', ActionStatus::REPLIED)->doesntExist())
+ ->content(view('filament.requests.history', [
+ 'request' => $request,
+ 'chat' => true,
+ ])),
+ Placeholder::make('subject')
+ ->content($request->subject),
+ Placeholder::make('inquiry')
+ ->content(view('filament.requests.show', [
+ 'request' => $request,
+ ])),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'remarks' => $data['response'],
+ 'status' => ActionStatus::REPLIED,
+ 'user_id' => Auth::id(),
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->success();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollBackDatabaseTransaction();
+
+ $this->failure();
+ }
+ });
+
+ $this->visible(function (Request $request) {
+ $valid = $request->class === RequestClass::INQUIRY && ! $request->action->status->finalized();
+
+ return $valid && match (Filament::getCurrentPanel()->getId()) {
+ 'user' => $request->action->status === ActionStatus::REPLIED,
+ 'moderator', 'agent', 'admin' => in_array($request->action->status, [ActionStatus::REPLIED, ActionStatus::ASSIGNED]) &&
+ $request->assignees->contains(Auth::user()),
+ default => false,
+ };
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/RequeueRequestAction.php b/app/Filament/Actions/Tables/RequeueRequestAction.php
new file mode 100644
index 0000000..d73087e
--- /dev/null
+++ b/app/Filament/Actions/Tables/RequeueRequestAction.php
@@ -0,0 +1,98 @@
+name('requeue-request');
+
+ $this->label('Requeue');
+
+ $this->slideOver();
+
+ $this->icon(ActionStatus::QUEUED->getIcon());
+
+ $this->modalIcon(ActionStatus::QUEUED->getIcon());
+
+ $this->modalDescription('Please provide a valid reason for rejecting and requeueing this request.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->successNotificationTitle('Request rejected and requeued');
+
+ $this->failureNotificationTitle('Request rejection and requeue failed');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->label('Reason')
+ ->required(Filament::getCurrentPanel()->getId() === 'agent'),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ if ($request->action->status !== ActionStatus::ASSIGNED) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $request->assignees()->detach();
+
+ $action = $request->actions()->create([
+ 'remarks' => $data['remarks'],
+ 'status' => ActionStatus::QUEUED,
+ 'user_id' => Auth::id(),
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollBackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->closeModalByClickingAway(false);
+
+ $this->hidden(fn (Request $request) => $request->action->status->finalized() ?: $request->action->status === ActionStatus::QUEUED);
+
+ $this->visible(fn (Request $request) => $request->action->status === ActionStatus::ASSIGNED &&
+ in_array(Auth::user()->role, [UserRole::ADMIN, UserRole::MODERATOR]) ?:
+ $request->declination === true &&
+ $request->assignees()->count() === 1 &&
+ $request->action->status === ActionStatus::ASSIGNED,
+ );
+ }
+}
diff --git a/app/Filament/Actions/Tables/ResolveRequestAction.php b/app/Filament/Actions/Tables/ResolveRequestAction.php
index 0efeb4e..1d8e65d 100644
--- a/app/Filament/Actions/Tables/ResolveRequestAction.php
+++ b/app/Filament/Actions/Tables/ResolveRequestAction.php
@@ -2,10 +2,105 @@
namespace App\Filament\Actions\Tables;
-use App\Filament\Actions\Traits\ResolveRequestTrait;
+use App\Enums\ActionResolution;
+use App\Enums\ActionStatus;
+use App\Filament\Actions\Concerns\Notifications\CanNotifyUsers;
+use App\Filament\Forms\FileAttachment;
+use App\Models\Request;
+use Exception;
+use Filament\Forms\Components\MarkdownEditor;
+use Filament\Notifications\Actions\Action as NotificationAction;
+use Filament\Notifications\Notification;
+use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\Action;
+use Illuminate\Support\Facades\Auth;
+
class ResolveRequestAction extends Action
{
- use ResolveRequestTrait;
+ use CanNotifyUsers;
+
+ protected static ?ActionStatus $requestAction = ActionStatus::CLOSED;
+
+ protected static ?ActionResolution $requestResolution = ActionResolution::RESOLVED;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->name('resolve-request');
+
+ $this->label('Close');
+
+ $this->slideOver();
+
+ $this->icon(ActionResolution::RESOLVED->getIcon());
+
+ $this->modalIcon(ActionResolution::RESOLVED->getIcon());
+
+ $this->modalHeading('Close request');
+
+ $this->modalDescription('Permanently close this request and mark it as resolved.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->closeModalByClickingAway(false);
+
+ $this->successNotificationTitle('Request closed');
+
+ $this->failureNotificationTitle('Request closure failed');
+
+ $this->form([
+ MarkdownEditor::make('remarks'),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ if ($request->action->status !== ActionStatus::COMPLETED) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'status' => ActionStatus::CLOSED,
+ 'resolution' => ActionResolution::RESOLVED,
+ 'remarks' => $data['remarks'],
+ 'user_id' => Auth::id(),
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ Notification::make()
+ ->persistent()
+ ->title('Request closed')
+ ->body('Would you like to provide feedback?')
+ ->actions([
+ NotificationAction::make('feedback')
+ ->label('Give Feedback')
+ ->url(route('filament.feedback.feedback', [
+ 'organization' => $request->organization_id,
+ ])),
+ ])
+ ->success()
+ ->send();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollBackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->hidden(fn (Request $request) => $request->action->status->finalized() ?: $request->action->status !== ActionStatus::COMPLETED);
+ }
}
diff --git a/app/Filament/Actions/Tables/RespondRequestAction.php b/app/Filament/Actions/Tables/RespondRequestAction.php
new file mode 100644
index 0000000..a4c360f
--- /dev/null
+++ b/app/Filament/Actions/Tables/RespondRequestAction.php
@@ -0,0 +1,135 @@
+name('respond-request');
+
+ $this->label('Respond');
+
+ $this->icon(ActionStatus::REPLIED->getIcon());
+
+ $this->modalIcon(ActionStatus::REPLIED->getIcon());
+
+ $this->modalDescription(fn (Request $request) => str('Respond to user\'s inquiry #'.$request->code.'')->toHtmlString());
+
+ $this->slideOver();
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->successNotificationTitle(function (Request $request) {
+ $pronoun = match (Filament::getCurrentPanel()->getId()) {
+ 'user' => 'your',
+ default => 'the',
+ };
+
+ return 'You have responded to '.$pronoun.' inquiry #'.$request->code.'';
+ });
+
+ $this->failureNotificationTitle('An error occurred while responding to this inquiry');
+
+ $this->form(fn (Request $request) => [
+ Placeholder::make('tags')
+ ->content(function () use ($request) {
+ if ($request->tags->isEmpty()) {
+ return '(none)';
+ }
+
+ $tags = $request->tags->map(function ($tag) {
+ return <<
+ {$tag->name}
+
+ HTML;
+ })->join(PHP_EOL);
+
+ $html = Blade::render(<<
+ {$tags}
+
+ HTML);
+
+ return str($html)->toHtmlString();
+ }),
+ MarkdownEditor::make('response')
+ ->label('Message')
+ ->required(),
+ FileAttachment::make(),
+ Placeholder::make('responses')
+ ->hidden($request->actions()->where('status', ActionStatus::REPLIED)->doesntExist())
+ ->content(view('filament.requests.history', [
+ 'request' => $request,
+ 'chat' => true,
+ ])),
+ Placeholder::make('subject')
+ ->content($request->subject),
+ Placeholder::make('inquiry')
+ ->content(view('filament.requests.show', [
+ 'request' => $request,
+ ])),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'remarks' => $data['response'],
+ 'status' => ActionStatus::REPLIED,
+ 'user_id' => Auth::id(),
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->success();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollBackDatabaseTransaction();
+
+ $this->failure();
+ }
+ });
+
+ $this->visible(function (Request $request) {
+ $valid = $request->class === RequestClass::INQUIRY && ! $request->action->status->finalized();
+
+ return $valid && match (Filament::getCurrentPanel()->getId()) {
+ 'user' => $request->action->status === ActionStatus::REPLIED,
+ 'moderator', 'agent', 'admin' => in_array($request->action->status, [ActionStatus::REPLIED, ActionStatus::ASSIGNED]) &&
+ $request->assignees->contains(Auth::user()),
+ default => false,
+ };
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/RestoreRequestAction.php b/app/Filament/Actions/Tables/RestoreRequestAction.php
new file mode 100644
index 0000000..a728942
--- /dev/null
+++ b/app/Filament/Actions/Tables/RestoreRequestAction.php
@@ -0,0 +1,18 @@
+bootRestoreRequest();
+ }
+}
diff --git a/app/Filament/Actions/Tables/ResubmitRequestAction.php b/app/Filament/Actions/Tables/ResubmitRequestAction.php
new file mode 100644
index 0000000..564caa7
--- /dev/null
+++ b/app/Filament/Actions/Tables/ResubmitRequestAction.php
@@ -0,0 +1,18 @@
+bootResubmitRequest();
+ }
+}
diff --git a/app/Filament/Actions/Tables/RetractRequestAction.php b/app/Filament/Actions/Tables/RetractRequestAction.php
deleted file mode 100644
index 401ad35..0000000
--- a/app/Filament/Actions/Tables/RetractRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-bootShowRequest();
+ }
+}
diff --git a/app/Filament/Actions/Tables/StartRequestAction.php b/app/Filament/Actions/Tables/StartRequestAction.php
new file mode 100644
index 0000000..5fcfd37
--- /dev/null
+++ b/app/Filament/Actions/Tables/StartRequestAction.php
@@ -0,0 +1,84 @@
+name('start-request');
+
+ $this->label('Start');
+
+ $this->icon(ActionStatus::STARTED->getIcon());
+
+ $this->requiresConfirmation();
+
+ $this->modalHeading('Start processing request');
+
+ $this->modalDescription('Start this request to begin processing. Once started, the request will be marked as in progress.');
+
+ $this->successNotificationTitle(fn (Request $request) => "{$request->class->getLabel()} request #{$request->code} started");
+
+ $this->failureNotificationTitle(fn (Request $request) => "Failed to start {$request->class->getLabel()} request #{$request->code}");
+
+ $this->action(function (Request $request) {
+ if ($request->class !== RequestClass::TICKET || in_array($request->action->status, [ActionStatus::STARTED, ActionStatus::SUSPENDED])) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $request->actions()->create([
+ 'status' => ActionStatus::STARTED,
+ 'user_id' => Auth::id(),
+ ]);
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->visible(function (Request $request) {
+ if ($request->action->status->finalized() || in_array($request->action->status, [
+ ActionStatus::STARTED,
+ ActionStatus::SUSPENDED,
+ ActionStatus::COMPLETED,
+ ])) {
+ return false;
+ }
+
+ return in_array($request->action->status, [
+ ActionStatus::ASSIGNED,
+ ActionStatus::REINSTATED,
+ ActionStatus::COMPLIED,
+ ActionStatus::REOPENED,
+ ]) && match ($request->class) {
+ RequestClass::TICKET => $request->assignees->contains(Auth::user()),
+ default => false,
+ };
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/StartedRequestAction.php b/app/Filament/Actions/Tables/StartedRequestAction.php
deleted file mode 100644
index d47ab62..0000000
--- a/app/Filament/Actions/Tables/StartedRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-name('suspend-request');
+
+ $this->label('Suspend');
+
+ $this->icon(ActionStatus::SUSPENDED->getIcon());
+
+ $this->modalIcon(ActionStatus::SUSPENDED->getIcon());
+
+ $this->slideOver();
+
+ $this->modalHeading('Suspend Request');
+
+ $this->modalDescription('Suspend this on-going request. This will hold the request until requester complies with the requirements.');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->closeModalByClickingAway(false);
+
+ $this->modalSubmitActionLabel('Confirm');
+
+ $this->successNotificationTitle('Request suspended');
+
+ $this->failureNotificationTitle('Failed to suspend request');
+
+ $this->form([
+ MarkdownEditor::make('remarks')
+ ->label('Reason')
+ ->required()
+ ->helperText('Please describe the reason for suspending this request.'),
+ FileAttachment::make(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ if ($request->class !== RequestClass::TICKET || $request->action->status === ActionStatus::SUSPENDED) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $action = $request->actions()->create([
+ 'status' => ActionStatus::SUSPENDED,
+ 'user_id' => Auth::id(),
+ 'remarks' => $data['remarks'],
+ ]);
+
+ if (count($data['files']) > 0) {
+ $action->attachment()->create([
+ 'files' => $data['files'],
+ 'paths' => $data['paths'],
+ ]);
+ }
+
+ $this->commitDatabaseTransaction();
+
+ $this->sendSuccessNotification();
+
+ $this->notifyUsers();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+
+ $this->sendFailureNotification();
+ }
+ });
+
+ $this->visible(function (Request $request) {
+ if ($request->action->status->finalized() || $request->action->status === ActionStatus::SUSPENDED) {
+ return false;
+ }
+
+ return in_array($request->action->status, [ActionStatus::STARTED, ActionStatus::ASSIGNED, ActionStatus::COMPLIED]) && match ($request->class) {
+ RequestClass::TICKET => $request->assignees->contains(Auth::user()) || Auth::user()->admin,
+ default => false,
+ };
+ });
+ }
+}
diff --git a/app/Filament/Actions/Tables/TagRequestAction.php b/app/Filament/Actions/Tables/TagRequestAction.php
new file mode 100644
index 0000000..5e1aa58
--- /dev/null
+++ b/app/Filament/Actions/Tables/TagRequestAction.php
@@ -0,0 +1,83 @@
+name('tag-request');
+
+ $this->label('Tag');
+
+ $this->icon(ActionStatus::TAGGED->getIcon());
+
+ $this->modalDescription('Label this request with topics that apply.');
+
+ $this->modalIcon(ActionStatus::TAGGED->getIcon());
+
+ $this->modalWidth(MaxWidth::Small);
+
+ $this->modalAlignment(Alignment::Left);
+
+ $this->modalFooterActionsAlignment(Alignment::Right);
+
+ $this->fillForm(fn (Request $request) => [
+ 'labels' => $request->tags->pluck('id'),
+ ]);
+
+ $this->form(fn (Request $request) => [
+ Select::make('labels')
+ ->options($request->organization->tags->pluck('name', 'id'))
+ ->multiple(),
+ ]);
+
+ $this->action(function (Request $request, array $data) {
+ if ($request->tags->pluck('name')->toArray() === $data['labels']) {
+ return;
+ }
+
+ try {
+ $this->beginDatabaseTransaction();
+
+ $added = array_diff($data['labels'], $request->tags->pluck('id')->toArray());
+
+ $removed = array_diff($request->tags->pluck('id')->toArray(), $data['labels']);
+
+ $remarks = $removed ? '-'.Tag::find($removed)->pluck('id')->implode('-') : null;
+
+ $remarks .= $added ? '+'.Tag::find($added)->pluck('id')->implode('+') : null;
+
+ $request->actions()->create([
+ 'status' => ActionStatus::TAGGED,
+ 'remarks' => $remarks,
+ 'user_id' => Auth::id(),
+ ]);
+
+ $request->tags()->sync($data['labels']);
+
+ $this->commitDatabaseTransaction();
+ } catch (Exception) {
+ $this->rollbackDatabaseTransaction();
+ }
+
+ $request->tags()->sync($data['labels']);
+ });
+
+ $this->disabled(fn (Request $request) => $request->action->status->finalized() &&
+ $request->action->created_at->addDays(90)->lessThan(now())
+ );
+ }
+}
diff --git a/app/Filament/Actions/Tables/TakeFeedbackAction.php b/app/Filament/Actions/Tables/TakeFeedbackAction.php
new file mode 100644
index 0000000..074b8ce
--- /dev/null
+++ b/app/Filament/Actions/Tables/TakeFeedbackAction.php
@@ -0,0 +1,25 @@
+name('take-feedback');
+
+ $this->label('Take Feedback');
+
+ $this->openUrlInNewTab();
+
+ $this->icon('heroicon-o-chat-bubble-left-right');
+
+ $this->hidden(fn ($record) => $record->feedback()->exists());
+
+ $this->url(fn ($record) => route('filament.feedback.feedback', ['organization' => $record->organization_id, 'request' => $record->id]));
+ }
+}
diff --git a/app/Filament/Actions/Tables/TemplatesPreviewActionGroup.php b/app/Filament/Actions/Tables/TemplatesPreviewActionGroup.php
new file mode 100644
index 0000000..99f4d3b
--- /dev/null
+++ b/app/Filament/Actions/Tables/TemplatesPreviewActionGroup.php
@@ -0,0 +1,51 @@
+ $actions]);
+
+ $static->configure();
+
+ return $static;
+ }
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->label('Templates');
+
+ $this->link();
+
+ $this->icon('gmdi-notes');
+
+ $this->actions(array_map(fn (RequestClass $class) => Action::make($class->value)
+ ->icon($class->getIcon())
+ ->label($class->getLabel())
+ ->slideOver()
+ ->modalFooterActionsAlignment(Alignment::End)
+ ->modalSubmitAction(false)
+ ->modalCancelActionLabel('Close')
+ ->modalWidth(MaxWidth::ExtraLarge)->infolist(fn (Subcategory $subcategory) => [
+ TextEntry::make('preview')
+ ->hiddenLabel()
+ ->extraEntryWrapperAttributes(['class' => 'w-full'])
+ ->state(str($subcategory->{"{$class->value}Template"}?->content ?? '')->markdown()->toHtmlString())
+ ->markdown(),
+ ]),
+ RequestClass::cases()
+ ));
+ }
+}
diff --git a/app/Filament/Actions/Tables/UndoRecentAction.php b/app/Filament/Actions/Tables/UndoRecentAction.php
new file mode 100644
index 0000000..7226aaa
--- /dev/null
+++ b/app/Filament/Actions/Tables/UndoRecentAction.php
@@ -0,0 +1,58 @@
+name('undo-recent-action-request');
+
+ $this->label('Undo');
+
+ $this->icon('gmdi-change-circle-o');
+
+ $this->modalIcon('gmdi-change-circle-o');
+
+ $this->requiresConfirmation();
+
+ $this->modalHeading(fn (Request $request) => "Undo {$request->action->status->getLabel('nounForm', false)}");
+
+ $this->modalDescription(fn (Request $request) => 'For '.static::$duration." minutes, you are allowed to undo your recent action. Are you sure you want to undo the {$request->action->status->getLabel('nounForm', false)}?");
+
+ $this->successNotificationTitle(fn (Request $request) => "Request {$request->action->status->getLabel('nounForm', false)} undone");
+
+ $this->action(function (Request $request) {
+ if (in_array($request->action->status, static::$undoable) === false) {
+ return;
+ }
+
+ $request->action->delete();
+
+ $this->sendSuccessNotification();
+ });
+
+ $this->visible(fn (Request $request) => in_array($request->action->status, static::$undoable) &&
+ $request->action->created_at->addMinutes(static::$duration)->greaterThan(now()) &&
+ $request->action->user_id === Auth::id(),
+ );
+ }
+}
diff --git a/app/Filament/Actions/Tables/UpdateRequestAction.php b/app/Filament/Actions/Tables/UpdateRequestAction.php
index 46669c8..dc1c97f 100644
--- a/app/Filament/Actions/Tables/UpdateRequestAction.php
+++ b/app/Filament/Actions/Tables/UpdateRequestAction.php
@@ -2,10 +2,17 @@
namespace App\Filament\Actions\Tables;
-use App\Filament\Actions\Traits\UpdateRequestTraits;
-use Filament\Tables\Actions\Action;
+use App\Filament\Actions\Concerns\UpdateRequest;
+use Filament\Tables\Actions\EditAction;
-class UpdateRequestAction extends Action
+class UpdateRequestAction extends EditAction
{
- use UpdateRequestTraits;
+ use UpdateRequest;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->bootUpdateRequest();
+ }
}
diff --git a/app/Filament/Actions/Tables/ViewRequestAction.php b/app/Filament/Actions/Tables/ViewRequestAction.php
new file mode 100644
index 0000000..ade4cdd
--- /dev/null
+++ b/app/Filament/Actions/Tables/ViewRequestAction.php
@@ -0,0 +1,16 @@
+bootViewRequest();
+ }
+}
diff --git a/app/Filament/Actions/Tables/ViewRequestHistoryAction.php b/app/Filament/Actions/Tables/ViewRequestHistoryAction.php
index 28e1ac7..d84bdff 100644
--- a/app/Filament/Actions/Tables/ViewRequestHistoryAction.php
+++ b/app/Filament/Actions/Tables/ViewRequestHistoryAction.php
@@ -2,10 +2,38 @@
namespace App\Filament\Actions\Tables;
-use App\Filament\Actions\Traits\ViewRequestHistoryTrait;
+use App\Models\Request;
+use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\Action;
class ViewRequestHistoryAction extends Action
{
- use ViewRequestHistoryTrait;
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->name('view-request-history');
+
+ $this->label('History');
+
+ $this->icon('gmdi-history-o');
+
+ $this->slideOver();
+
+ $this->modalIcon('gmdi-history-o');
+
+ $this->modalWidth(MaxWidth::ExtraLarge);
+
+ $this->modalHeading('Request History');
+
+ $this->modalDescription('See the history of this request.');
+
+ $this->modalSubmitAction(false);
+
+ $this->modalCancelAction(false);
+
+ $this->modalContent(fn (Request $request) => view('filament.requests.history', ['request' => $request]));
+
+ $this->hidden(fn (Request $request) => $request->trashed());
+ }
}
diff --git a/app/Filament/Actions/Traits/AcceptAssignmentTrait.php b/app/Filament/Actions/Traits/AcceptAssignmentTrait.php
deleted file mode 100644
index 33447a5..0000000
--- a/app/Filament/Actions/Traits/AcceptAssignmentTrait.php
+++ /dev/null
@@ -1,70 +0,0 @@
-name ??= 'accept';
-
- $this->button();
-
- $this->icon('heroicon-c-check-circle');
-
- $this->color('success');
-
- $this->close();
-
- $this->hidden(function ($record) {
- return $record->currentUserAssignee->responded_at?->addMinutes(15)->lt(now());
- });
-
- $this->action(function ($record, $action) {
- if ($record->currentUserAssignee->responded_at?->addMinutes(15)->lt(now())) {
- Notification::make()
- ->title('No activity for 15 minutes')
- ->Warning()
- ->send();
-
- return;
- }
- $record->currentUserAssignee()->updateOrCreate([
- 'user_id' => Auth::id(),
- 'assignees.request_id' => $record->id,
- ], [
- 'response' => UserAssignmentResponse::ACCEPTED,
- 'responded_at' => $record->currentUserAssignee->responded->at ?? now(),
- ]);
-
- $record->action()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'response' => RequestStatus::ACCEPTED,
- 'status' => RequestStatus::ACCEPTED,
- 'time' => now(),
- ]);
-
- Notification::make()
- ->title('Accepted Successfully!')
- ->success()
- ->send();
-
- Notification::make()
- ->title('Request Accepted')
- ->body(str("Request of {$record->requestor->name} has been ACCEPTED by ".auth()->user()->name.'.')->toHtmlString())
- ->icon(RequestStatus::ACCEPTED->getIcon())
- ->iconColor(RequestStatus::ACCEPTED->getColor())
- ->sendToDatabase($record->currentUserAssignee->assigner);
-
- $action->sendSuccessNotification();
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/AdjustRequestTrait.php b/app/Filament/Actions/Traits/AdjustRequestTrait.php
deleted file mode 100644
index 4c9f0f6..0000000
--- a/app/Filament/Actions/Traits/AdjustRequestTrait.php
+++ /dev/null
@@ -1,64 +0,0 @@
-name ??= 'adjust';
-
- $this->label('Set Difficulty');
-
- $this->icon('heroicon-s-adjustments-vertical');
-
- $this->hidden(fn ($record) => in_array($record->action?->status, [
- RequestStatus::RESOLVED,
- RequestStatus::REJECTED,
- RequestStatus::COMPLETED,
- RequestStatus::DECLINED,
- ]) || in_array($record->currentUserAssignee->response, [
- UserAssignmentResponse::REJECTED,
- UserAssignmentResponse::COMPLETED,
- ]));
-
- $this->action(function ($record, $data, $action) {
- $from = $record->difficulty;
-
- $record->update(['difficulty' => $data['diff']]);
-
- $record->action()->create([
- 'user_id' => Auth::id(),
- 'actions.request_id' => $record->id,
- 'status' => RequestStatus::ADJUSTED->value,
- 'time' => now(),
- 'remarks' => 'Difficulty'.($from ? ' from '.$from : '').' to '.$data['diff'],
- ]);
-
- Notification::make()
- ->title('Difficulty Adjusted')
- ->body(str("Request difficulty of {$record->requestor->name} has adjusted ".($from ? ' from '.$from : '').' by '.auth()->user()->name.'.')->toHtmlString())
- ->icon(RequestStatus::ADJUSTED->getIcon())
- ->iconColor(RequestStatus::ADJUSTED->getColor())
- ->sendToDatabase($record->currentUserAssignee->assigner);
- $action->sendSuccessNotification();
-
- });
-
- $this->form([
- Select::make('diff')
- ->label('Difficulty Level')
- ->options(RequestDifficulty::options())
- ->default(fn ($record) => $record->difficulty),
- ]);
- }
-}
diff --git a/app/Filament/Actions/Traits/AmmendRecentActionTrait.php b/app/Filament/Actions/Traits/AmmendRecentActionTrait.php
deleted file mode 100644
index 6312530..0000000
--- a/app/Filament/Actions/Traits/AmmendRecentActionTrait.php
+++ /dev/null
@@ -1,168 +0,0 @@
-name ??= 'amend';
-
- $this->visible(function (Request $record) {
- return $record->action?->status->major() &&
- $record->action?->status !== RequestStatus::PUBLISHED &&
- $record->action?->user->is(Auth::user());
- });
- $this->hidden(fn (Request $record) => $record->action?->status == RequestStatus::RESOLVED);
-
- $this->color('primary');
-
- $this->icon('gmdi-draw-o');
-
- $this->modalAlignment(Alignment::Left);
-
- $this->modalIcon('gmdi-draw-o');
-
- $this->modalDescription(function (Request $record) {
- $html = <<“{$record->action->status->getLabel('nounForm', false)}” remarks and attachments here.
- HTML;
-
- return str($html)->toHtmlString();
- });
-
- $this->modalWidth('3xl');
-
- $this->successNotificationTitle('Ammended successfully');
-
- $this->fillForm(fn (Request $record) => [
- 'status' => $record->action->status,
- 'remarks' => $record->action->remarks,
- 'paths' => $record->action->attachment?->paths->toArray() ?? [],
- 'files' => $record->action->attachment?->files->toArray() ?? [],
- ]);
-
- $this->form([
- Select::make('status')
- ->hidden(fn () => empty($this->statuses))
- ->columnSpanFull()
- ->options(fn () => $this->statuses)
- ->required(),
- RichEditor::make('remarks')
- ->columnSpanFull()
- ->label('Remarks')
- ->placeholder('Please provide a reason for retracting this request...')
- ->required(),
- FileUpload::make('paths')
- ->label('Attachments')
- ->placeholder(fn (string $operation) => match ($operation) {
- 'view' => 'Click the icon at the left side of the filename to download',
- default => null,
- })
- ->directory(fn (Request $record) => "attachments/tmp/{$record->action->id}")
- ->preserveFilenames()
- ->storeFileNamesIn('files')
- ->multiple()
- ->maxFiles(5)
- ->downloadable()
- ->previewable(false)
- ->maxSize(1024 * 4)
- ->removeUploadedFileButtonPosition('right')
- ->rule(fn (Request $record, Get $get) => function ($attribute, $value, $fail) use ($record, $get) {
- $files = collect($get('paths'))->map(fn (TemporaryUploadedFile|string $file) => [
- 'file' => $file instanceof TemporaryUploadedFile
- ? $file->getClientOriginalName()
- : $record->action->attachment->files[$file],
- 'hash' => $file instanceof TemporaryUploadedFile
- ? hash_file('sha512', $file->getRealPath())
- : hash_file('sha512', storage_path("app/public/$file")),
- ]);
-
- if (($duplicates = $files->duplicates('hash'))->isNotEmpty()) {
- $dupes = $files->filter(fn ($file) => $duplicates->contains($file['hash']))->unique();
-
- $fail('Please do not upload the same files ('.$dupes->map->file->join(', ').') multiple times.');
- }
- }),
- ]);
-
- $this->action(function (Request $record, self $action, array $data) {
- $ammendment = $record->action;
-
- $ammendment->update(['remarks' => $data['remarks']]);
-
- $new = collect($data['files'])->filter(fn (string $file, string $path) => str($path)->startsWith('attachments/tmp'));
-
- if ($new->isNotEmpty() || count($data['files']) != $ammendment->attachment?->files->count()) {
- $files = $new->mapWithKeys(fn (string $file) => [
- str(str()->ulid())
- ->prepend("attachments/action-{$ammendment->id}-")
- ->append(($ext = pathinfo($file, PATHINFO_EXTENSION)) ? ".$ext" : '')
- ->lower()
- ->toString() => "attachments/tmp/{$ammendment->id}/$file",
- ]);
-
- $files->each(fn (string $file, string $path) => Storage::move("public/$file", "public/$path"));
-
- $files = collect($data['files'])->mapWithKeys(fn ($file, $path) => [$files->search(fn ($tmp) => $tmp === $path) ?: $path => $file]);
-
- Process::run(['rm', '-rf', Storage::path('public/attachments/tmp/'.$ammendment->id)]);
-
- $attachment = $ammendment->attachment;
-
- if ($attachment !== null) {
- $attachment->files = $files->map(fn ($file) => basename($file))->toArray();
-
- $attachment->paths = $files->keys()->toArray();
- } else {
- $attachment = $ammendment->attachment()->create([
- 'files' => $files->map(fn ($file) => basename($file))->toArray(),
- 'paths' => $files->keys()->toArray(),
- ]);
- }
-
- $attachment->sanitize();
-
- $ammendment->touch();
- }
-
- Notification::make()
- ->title('Request Ammenbded')
- ->icon(RequestStatus::AMMENDED->getIcon())
- ->iconColor(RequestStatus::AMMENDED->getColor())
- ->body($record->category->name.' ( '.$record->subcategory->name.' ) '.''.auth()->user()->name.' : '.''.$data['remarks'])
- ->sendToDatabase($record->assignees);
- $action->sendSuccessNotification();
-
- });
- }
-
- public function statuses(?array $statuses, ?string $type = null): static
- {
- $statuses = collect($statuses)->mapWithKeys(fn (RequestStatus|string $status) => $status instanceof RequestStatus
- ? [$status->value => $status->getLabel($type)]
- : [mb_strtolower($status) => RequestStatus::from(mb_strtolower($status))->getLabel($type)]
- );
-
- $this->statuses = $statuses->toArray();
-
- return $this;
- }
-}
diff --git a/app/Filament/Actions/Traits/ApproveRequestTrait.php b/app/Filament/Actions/Traits/ApproveRequestTrait.php
deleted file mode 100644
index 5699b5d..0000000
--- a/app/Filament/Actions/Traits/ApproveRequestTrait.php
+++ /dev/null
@@ -1,108 +0,0 @@
-name ??= 'approve';
-
- $this->color(RequestStatus::APPROVED->getColor());
-
- $this->icon(RequestStatus::APPROVED->getIcon());
-
- $this->visible(fn (Request $record) => $record->action?->status === RequestStatus::PUBLISHED);
-
- $this->form([
- Select::make('priority')
- ->placeholder('Provide an estimate on how time crucial the task is.')
- ->options(RequestPriority::options()
- )
- ->required(),
-
- RichEditor::make('remarks')
- ->label('Remarks')
- ->placeholder('Provide further details regarding this request'),
-
- Select::make('assignees')
- ->label('Assignees')
- ->placeholder('Select a support to assign this request to.')
- ->options(function ($record) {
- return
- User::query()
- ->where('role', 'support')
- ->where('office_id', $record->office->id)
- ->pluck('name', 'id');
- })
- ->multiple(),
- ]);
-
- $this->action(function ($data, $record, self $action) {
- $status = empty($data['assignees'])
- ? RequestStatus::APPROVED->value
- : RequestStatus::ASSIGNED->value;
- $record->action()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'status' => $status,
- 'remarks' => $data['remarks'],
- 'time' => now(),
- ]);
-
- $record->assignees()->attach(
- collect($data['assignees'])->mapWithKeys(function ($id) use ($record) {
- Notification::make()
- ->title('New Request Assigned')
- ->icon('heroicon-o-check-circle')
- ->iconColor(RequestStatus::APPROVED->getColor())
- ->body($record->office->acronym.' - '.$record->subject.'( '.$record->category->name)
- ->sendToDatabase(User::find($id));
-
- return [
- $id => [
- 'assigner_id' => Auth::id(),
- 'created_at' => now(),
- ],
- ];
- })->toArray()
- );
-
- $assigneeNames = User::whereIn('id', $data['assignees'])->pluck('name')->toArray();
- $assigneesString = implode(', ', $assigneeNames);
-
- if (empty($userIds)) {
- Notification::make()
- ->title('The request is being processed and is to be assigned soon')
- ->icon('heroicon-o-check-circle')
- ->iconColor(RequestStatus::APPROVED->getColor())
- ->sendToDatabase($record->requestor);
- } else {
- Notification::make()
- ->title('The request has been assigned to '.$assigneesString.' by '.auth()->user()->name.' and is awaiting further process')
- ->icon('heroicon-o-check-circle')
- ->iconColor(RequestStatus::APPROVED->getColor())
- ->sendToDatabase($record->requestor);
- }
-
- Notification::make()
- ->title('Request has been assigned')
- ->success()
- ->send();
- $record->update(['priority' => $data['priority']]);
-
- });
-
- }
-}
diff --git a/app/Filament/Actions/Traits/AssignRequestTrait.php b/app/Filament/Actions/Traits/AssignRequestTrait.php
deleted file mode 100644
index 01ce7b8..0000000
--- a/app/Filament/Actions/Traits/AssignRequestTrait.php
+++ /dev/null
@@ -1,88 +0,0 @@
-name ??= 'assign';
-
- $this->color(RequestStatus::ASSIGNED->getColor());
-
- $this->icon(RequestStatus::ASSIGNED->getIcon());
-
- $this->visible(fn (Request $record) => in_array($record->action?->status, [
- RequestStatus::ASSIGNED,
- RequestStatus::REJECTED,
- RequestStatus::APPROVED,
- ]));
-
- $this->form([
- CheckboxList::make('assignees')
- ->columns(2)
- ->label('Assignees')
- ->default(fn ($record) => $record ? $record->assignees()->pluck('user_id')->toArray() : [])
- ->searchable()
- ->hiddenLabel()
- ->options(function ($record) {
- return
- User::query()
- ->where('role', 'support')
- ->where('office_id', $record->office->id)
- ->pluck('name', 'id');
- }),
- ]);
-
- $this->action(function ($data, $record, self $action) {
- $from = implode(' and ', $record?->assignees()->pluck('name')->toArray());
- $record->assignees()->detach();
- $record->assignees()->attach(
- collect($data['assignees'])->mapWithKeys(function ($id) use ($record, $data) {
- Notification::make()
- ->title('New Request Assigned')
- ->icon('heroicon-o-check-circle')
- ->iconColor(RequestStatus::APPROVED->getColor())
- ->body(str('Assigned to : '.implode(' and ', User::whereIn('id', $data['assignees'])->pluck('name')->toArray()).''.$record->office->acronym.' - '.$record->subject.' ( '.$record->category->name.' )')->toHtmlString())
- ->sendToDatabase(User::find($id));
-
- return [
- $id => [
- 'assigner_id' => Auth::id(),
- 'created_at' => now(),
- ],
- ];
- })->toArray()
- );
- $record->action()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'status' => RequestStatus::ASSIGNED,
- 'remarks' => 'Assigned '.($from ? ' from '.$from : '').' to '.implode(' and ', User::whereIn('id', $data['assignees'])->pluck('name')->toArray()),
- 'time' => now(),
- ]);
-
- Notification::make()
- ->title(str("Your request {$record->subject} has been assigned")->toHtmlString())
- ->icon(RequestStatus::ASSIGNED->getIcon())
- ->iconColor(RequestStatus::ASSIGNED->getColor())
- ->body('Assigned '.($from ? ' from '.$from : '').' to '.implode(' and ', User::whereIn('id', $data['assignees'])->pluck('name')->toArray()))
- ->sendToDatabase($record->requestor);
-
- Notification::make()
- ->title('Request has been reassigned')
- ->success()
- ->send();
-
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/CompliedRequestTrait.php b/app/Filament/Actions/Traits/CompliedRequestTrait.php
deleted file mode 100644
index f6a9037..0000000
--- a/app/Filament/Actions/Traits/CompliedRequestTrait.php
+++ /dev/null
@@ -1,28 +0,0 @@
-name ??= 'complied';
-
- $this->color(RequestStatus::COMPLIED->getColor());
-
- $this->icon(RequestStatus::COMPLIED->getIcon());
-
- $this->visible(fn (Request $record) => $record->action?->status === RequestStatus::SUSPENDED);
-
- $this->action(function ($data, Request $record, self $action) {
- $action->sendSuccessNotification();
-
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/DeclineRequestTrait.php b/app/Filament/Actions/Traits/DeclineRequestTrait.php
deleted file mode 100644
index ee1371a..0000000
--- a/app/Filament/Actions/Traits/DeclineRequestTrait.php
+++ /dev/null
@@ -1,57 +0,0 @@
-name ??= 'decline';
-
- $this->color(RequestStatus::DECLINED->getColor());
-
- $this->icon(RequestStatus::DECLINED->getIcon());
-
- $this->visible(fn (Request $record) => $record->action?->status === RequestStatus::PUBLISHED);
-
- $this->requiresConfirmation();
-
- $this->modalWidth('3xl');
-
- $this->form([
- RichEditor::make('remarks')
- ->label('Declining Request')
- ->placeholder('Please provide a reason for declining this request'),
- ]);
-
- $this->action(function ($record, $data, self $action) {
- $record->action()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'status' => RequestStatus::DECLINED,
- 'remarks' => $data['remarks'],
- 'time' => now(),
- ]);
-
- Notification::make()
- ->title('The request has been declined by the officers')
- ->icon('heroicon-c-no-symbol')
- ->iconColor(RequestStatus::DECLINED->getColor())
- ->body(str(auth()->user()->name.' declined : '.' '.$record->subject.''.$data['remarks'])->toHtmlString())
- ->sendToDatabase($record->requestor);
- $action->sendSuccessNotification();
-
- });
-
- $this->successNotificationTitle('Request declined');
- }
-}
diff --git a/app/Filament/Actions/Traits/DenyCompletedTrait.php b/app/Filament/Actions/Traits/DenyCompletedTrait.php
deleted file mode 100644
index bbf2535..0000000
--- a/app/Filament/Actions/Traits/DenyCompletedTrait.php
+++ /dev/null
@@ -1,43 +0,0 @@
-name ??= 'deny';
-
- $this->color(RequestStatus::DENIED->getColor());
-
- $this->icon(RequestStatus::DENIED->getIcon());
-
- $this->visible(fn (Request $record) => $record->action?->status === RequestStatus::COMPLETED);
-
- $this->requiresConfirmation();
-
- $this->action(function ($record, $data, $action) {
- $record->action()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'status' => RequestStatus::DENIED,
- 'time' => now(),
- ]);
- Notification::make()
- ->title('Completion denied')
- ->icon(RequestStatus::DENIED->getIcon())
- ->iconColor(RequestStatus::DENIED->getColor())
- ->body($record->category->name.' ( '.$record->subcategory->name.' ) '.''.auth()->user()->name.' has denied the completion of this request')
- ->sendToDatabase($record->assignees);
- $this->successNotificationTitle('Denied request completion');
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/PublishRequestTrait.php b/app/Filament/Actions/Traits/PublishRequestTrait.php
deleted file mode 100644
index 2ec46fb..0000000
--- a/app/Filament/Actions/Traits/PublishRequestTrait.php
+++ /dev/null
@@ -1,55 +0,0 @@
-name ??= 'publish';
-
- $this->color(RequestStatus::PUBLISHED->getColor());
-
- $this->requiresConfirmation();
-
- $this->label(fn (Request $record) => $record->action?->status !== RequestStatus::RETRACTED ? 'Publish' : 'Republish');
-
- $this->visible(fn (Request $record) => is_null($status = $record->action?->status) || $status === RequestStatus::RETRACTED);
-
- $this->icon('heroicon-c-newspaper');
-
- $this->modalIcon('heroicon-c-newspaper');
-
- $this->modalDescription('Are you sure you want to publish this request? This will prevent this request from any alteration until it is retracted.');
-
- $this->successNotificationTitle('Request published successfully');
-
- $this->action(function (Request $record, self $action) {
-
- $record->actions()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'status' => RequestStatus::PUBLISHED,
- 'time' => now(),
- ]);
-
- Notification::make()
- ->title('Request published')
- ->icon(RequestStatus::PUBLISHED->getIcon())
- ->body('( '.$record->subject.' )'.' has been published for processing')
- ->iconColor(RequestStatus::PUBLISHED->getColor())
- ->sendToDatabase($record->requestor);
-
- $this->successNotificationTitle('Request successfully published');
-
- redirect(route('filament.user.resources.requests.index'));
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/RejectAssignmentTrait.php b/app/Filament/Actions/Traits/RejectAssignmentTrait.php
deleted file mode 100644
index 29aab74..0000000
--- a/app/Filament/Actions/Traits/RejectAssignmentTrait.php
+++ /dev/null
@@ -1,74 +0,0 @@
-name ??= 'reject';
-
- $this->button();
-
- $this->icon('heroicon-c-x-circle');
-
- $this->color('danger');
-
- $this->close();
-
- $this->action(function ($record) {
-
- if ($record->currentUserAssignee->responded_at?->addMinutes(15)->lt(now())) {
- Notification::make()
- ->title('No activity for 15 minutes')
- ->Warning()
- ->send();
-
- return;
- }
- $record->currentUserAssignee()->updateOrCreate([
- 'user_id' => Auth::id(),
- 'assignees.request_id' => $record->id,
- ], [
- 'response' => UserAssignmentResponse::REJECTED,
- 'responded_at' => $record->currentUserAssignee->responded->at ?? now(),
- ]);
- $record->action()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'response' => RequestStatus::REJECTED,
- 'status' => RequestStatus::REJECTED,
- 'time' => now(),
- ]);
- Notification::make()
- ->title('Rejected Successfully!')
- ->danger()
- ->send();
-
- Notification::make()
- ->title('Request Rejected')
- ->body(str("Request of {$record->requestor->name} has been REJECTED by ".auth()->user()->name.'.')->toHtmlString())
- ->icon(RequestStatus::DECLINED->getIcon())
- ->iconColor(RequestStatus::DECLINED->getColor())
- ->sendToDatabase($record->currentUserAssignee->assigner);
-
- $this->successNotificationTitle('Assignment rejected');
- });
-
- $this->hidden(function ($record) {
- if ($record->currentUserAssignee->responded_at == null) {
- return;
- }
-
- return $record->currentUserAssignee->responded_at->addMinutes(15)->lt(now());
- });
-
- }
-}
diff --git a/app/Filament/Actions/Traits/ResolveRequestTrait.php b/app/Filament/Actions/Traits/ResolveRequestTrait.php
deleted file mode 100644
index f66e85a..0000000
--- a/app/Filament/Actions/Traits/ResolveRequestTrait.php
+++ /dev/null
@@ -1,59 +0,0 @@
-name ??= 'resolve';
-
- $this->color(RequestStatus::RESOLVED->getColor());
-
- $this->icon(RequestStatus::RESOLVED->getIcon());
-
- $this->visible(fn (Request $record) => in_array($record->action?->status, [
- RequestStatus::COMPLETED,
- ]));
-
- $this->requiresConfirmation();
- $this->form([
- Select::make('quality')
- ->required()
- ->options(RequestQuality::options()),
- Select::make('timeliness')
- ->required()
- ->options(RequestTimeliness::options()),
- Textarea::make('remarks')
- ->placeholder('Provide further description of your experience regarding this request transaction'),
- ]);
- $this->modalDescription('This survey reflects how well the request has been managed by the support and how smooth the process was');
- $this->action(function ($data, Request $record, self $action) {
- $record->action()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'remarks' => str('Quality: '.' '.$data['quality'].' - '.RequestQuality::from($data['quality'])->getDescription().' ( '.''.RequestQuality::from($data['quality'])->getRating().''.' )'.''.'Timeliness: '.' '.$data['timeliness'].' - '.RequestTimeliness::from($data['timeliness'])->getDescription().''.'Comments: '.' '.$data['remarks'])->toHtmlString(),
- 'status' => RequestStatus::RESOLVED,
- 'time' => now(),
- ]);
- Notification::make()
- ->title('Your assigned request has been resolved')
- ->icon(RequestStatus::RESOLVED->getIcon())
- ->iconColor(RequestStatus::RESOLVED->getColor())
- ->body(str($record['subject'].'( '.$record->category->name.' - '.$record->subcategory->name.' )'.'
'.'This request will no longer recieve any updates')->toHtmlString())
- ->sendToDatabase($record->assignees);
- $this->successNotificationTitle('Request resolved and surveyed');
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/RetractRequestTrait.php b/app/Filament/Actions/Traits/RetractRequestTrait.php
deleted file mode 100644
index 863dbf2..0000000
--- a/app/Filament/Actions/Traits/RetractRequestTrait.php
+++ /dev/null
@@ -1,120 +0,0 @@
-name ??= 'retract';
-
- $this->color(RequestStatus::RETRACTED->getColor());
-
- $this->visible(fn (Request $record) => $record->action?->status === RequestStatus::PUBLISHED);
-
- $this->icon('heroicon-c-newspaper');
-
- $this->modalAlignment(Alignment::Left);
-
- $this->modalIcon('heroicon-c-newspaper');
-
- $this->modalDescription('Are you sure you want to retract this request?');
-
- $this->modalWidth('3xl');
-
- $this->successNotificationTitle('Request retracted successfully');
-
- $this->form([
- RichEditor::make('remarks')
- ->columnSpanFull()
- ->label('Remarks')
- ->placeholder('Please provide a reason for retracting this request...')
- ->required(),
- Repeater::make('attachments')
- ->columnSpanFull()
- ->label('Attachments')
- ->columnSpanFull()
- ->deletable(false)
- ->addable(false)
- ->reorderable(false)
- ->hint('Help')
- ->hintIcon('heroicon-o-question-mark-circle')
- ->hintIconTooltip('Please upload a maximum file count of 5 items and file size of 4096 kilobytes.')
- ->simple(
- FileUpload::make('paths')
- ->placeholder(fn (string $operation) => match ($operation) {
- 'view' => 'Click the icon at the left side of the filename to download',
- default => null,
- })
- ->directory(fn (Request $record) => "attachments/tmp/{$record->id}")
- ->preserveFilenames()
- ->multiple()
- ->maxFiles(5)
- ->downloadable()
- ->previewable(false)
- ->maxSize(1024 * 4)
- ->removeUploadedFileButtonPosition('right')
- )
- ->rule(fn () => function ($attribute, $value, $fail) {
- $files = collect(current($value)['paths'])->map(fn (TemporaryUploadedFile|string $file) => [
- 'file' => $file instanceof TemporaryUploadedFile
- ? $file->getClientOriginalName()
- : current($value)['files'][$file],
- 'hash' => $file instanceof TemporaryUploadedFile
- ? hash_file('sha512', $file->getRealPath())
- : hash_file('sha512', storage_path("app/public/$file")),
- ]);
-
- if (($duplicates = $files->duplicates('hash'))->isNotEmpty()) {
- $dupes = $files->filter(fn ($file) => $duplicates->contains($file['hash']))->unique();
-
- $fail('Please do not upload the same files ('.$dupes->map->file->join(', ').') multiple times.');
- }
- }
- ),
- ]);
-
- $this->action(function (Request $record, self $action, array $data) {
- $retraction = $record->actions()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'status' => RequestStatus::RETRACTED,
- 'time' => now(),
- 'remarks' => $data['remarks'],
- ]);
-
- if (($attachments = collect(current($data['attachments'])))->isNotEmpty()) {
- $files = $attachments
- ->mapWithKeys(fn (string $file) => [
- str(str()->ulid())
- ->prepend("attachments/action-{$retraction->id}-")
- ->append(($ext = pathinfo($file, PATHINFO_EXTENSION)) ? ".$ext" : '')
- ->lower()
- ->toString() => $file,
- ])
- ->each(fn (string $file, string $path) => Storage::move("public/$file", "public/$path"));
-
- $retraction->attachment()->create([
- 'files' => $files->map(fn ($file) => basename($file))->toArray(),
- 'paths' => $files->keys()->toArray(),
- ]);
-
- Process::run(['rm', '-rf', Storage::path('public/attachments/tmp/'.$record->id)]);
- }
- $this->successNotificationTitle('Request retracted');
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/ScheduleRequestTrait.php b/app/Filament/Actions/Traits/ScheduleRequestTrait.php
deleted file mode 100644
index 04a1cc2..0000000
--- a/app/Filament/Actions/Traits/ScheduleRequestTrait.php
+++ /dev/null
@@ -1,79 +0,0 @@
-name ??= 'schedule';
-
- $this->icon('heroicon-s-clock');
-
- $this->label('Set target Date and Time');
-
- $this->modalWidth(MaxWidth::Large);
-
- $this->hidden(fn ($record) => in_array($record->action?->status, [
- RequestStatus::RESOLVED,
- UserAssignmentResponse::REJECTED,
- RequestStatus::COMPLETED,
- RequestStatus::DECLINED,
- ]) || in_array($record->currentUserAssignee->response, [
- UserAssignmentResponse::REJECTED,
- UserAssignmentResponse::COMPLETED,
- ]) || in_array(RequestStatus::STARTED, $record->actions->pluck('status')->toArray()));
-
- $this->form([
- DatePicker::make('target_date')
- ->required()
- ->minDate(fn ($record) => $record->availability_from)
- ->default(fn ($record) => $record->availability_from)
- ->maxDate(fn ($record) => $record->availability_to),
- TimePicker::make('target_time')
- ->required()
- ->seconds(false)
- ->default(now())
- ->placeholder('12:00')
- ->rule(fn () => function ($a, $v, $f) {
- if ($v < '08:00' || $v > '17:00') {
- $f('Invalid time');
- }
- }),
- ]);
-
- $this->action(function ($record, $data) {
- $from = $record->target_date.' '.$record->target_time;
- $record->update($data);
- $record->action()->create([
- 'user_id' => Auth::id(),
- 'actions.request_id' => $record->id,
- 'status' => RequestStatus::SCHEDULED->value,
- 'time' => now(),
- 'remarks' => 'Scheduled'.($from ? ' from '.$from : '').' to '.$data['target_date'].' '.$data['target_time'],
- ]);
- Notification::make()
- ->title('Scheduled Successfully!')
- ->success()
- ->send();
-
- Notification::make()
- ->title('Request Scheduled')
- ->body(str("Your request has been scheduled to {$data['target_date']} at {$data['target_time']} by ".auth()->user()->name.'.')->toHtmlString())
- ->icon(RequestStatus::SCHEDULED->getIcon())
- ->iconColor(RequestStatus::SCHEDULED->getColor())
- ->sendToDatabase($record->requestor);
- $this->successNotificationTitle('Request scheduled');
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/StartedRequestTrait.php b/app/Filament/Actions/Traits/StartedRequestTrait.php
deleted file mode 100644
index 963cd8d..0000000
--- a/app/Filament/Actions/Traits/StartedRequestTrait.php
+++ /dev/null
@@ -1,53 +0,0 @@
-name ??= 'Start';
-
- $this->color(RequestStatus::STARTED->getColor());
-
- $this->icon(RequestStatus::STARTED->getIcon());
-
- $this->visible(fn ($record) => $record->assignees()->wherePivot('response', 'accepted')->count() === $record->assignees->count() && $record->action?->status === RequestStatus::VERIFIED || $record->action?->status === RequestStatus::DENIED);
-
- $this->hidden(fn ($record) => in_array($record->action?->status, [
- RequestStatus::STARTED,
- RequestStatus::COMPLIED,
- RequestStatus::SUSPENDED,
- RequestStatus::RESOLVED,
- RequestStatus::RESOLVED,
- ]));
-
- $this->requiresConfirmation();
-
- $this->action(function ($data, Request $record, self $action) {
- $record->action()->create([
- 'request_id' => $record->id,
- 'user_id' => Auth::id(),
- 'status' => RequestStatus::STARTED,
- 'time' => now(),
- ]);
-
- Notification::make()
- ->title('Support has started this ticket')
- ->body('Assigned support has started working on this ticket')
- ->icon(RequestStatus::STARTED->getIcon())
- ->iconColor(RequestStatus::STARTED->getColor())
- ->sendToDatabase($record->requestor);
- $this->successNotificationTitle('Request started');
-
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/UpdateRequestTraits.php b/app/Filament/Actions/Traits/UpdateRequestTraits.php
deleted file mode 100644
index 76fae38..0000000
--- a/app/Filament/Actions/Traits/UpdateRequestTraits.php
+++ /dev/null
@@ -1,133 +0,0 @@
-name ??= 'update';
-
- $this->color('info');
-
- $this->visible(fn ($record) => $record->action?->status === RequestStatus::STARTED);
-
- $this->button();
-
- $this->disabled(function ($record) {
- return $record->currentUserAssignee->response->name == 'REJECTED';
- });
-
- $this->form([
- Select::make('status')
- ->required()
- ->options([
- RequestStatus::COMPLETED->value => RequestStatus::COMPLETED->getLabel(),
- RequestStatus::SUSPENDED->value => RequestStatus::SUSPENDED->getLabel(),
- ])
- ->reactive()
- ->native(false),
- RichEditor::make('remarks')
- ->required(fn (Get $get): bool => $get('status') === RequestStatus::SUSPENDED->value),
- Repeater::make('attachments')
- ->columnSpanFull()
- ->label('Attachments')
- ->columnSpanFull()
- ->deletable(false)
- ->addable(false)
- ->reorderable(false)
- ->hint('Help')
- ->hintIcon('heroicon-o-question-mark-circle')
- ->hintIconTooltip('Please upload a maximum file count of 5 items and file size of 4096 kilobytes.')
- ->simple(
- FileUpload::make('paths')
- ->placeholder(fn (string $operation) => match ($operation) {
- 'view' => 'Click the icon at the left side of the filename to download',
- default => null,
- })
- ->directory(fn (Request $record) => "attachments/tmp/{$record->id}")
- ->preserveFilenames()
- ->multiple()
- ->maxFiles(5)
- ->downloadable()
- ->previewable(false)
- ->maxSize(1024 * 4)
- ->removeUploadedFileButtonPosition('right')
- )
- ->rule(fn () => function ($attribute, $value, $fail) {
- $files = collect(current($value)['paths'])->map(fn (TemporaryUploadedFile|string $file) => [
- 'file' => $file instanceof TemporaryUploadedFile
- ? $file->getClientOriginalName()
- : current($value)['files'][$file],
- 'hash' => $file instanceof TemporaryUploadedFile
- ? hash_file('sha512', $file->getRealPath())
- : hash_file('sha512', storage_path("app/public/$file")),
- ]);
-
- if (($duplicates = $files->duplicates('hash'))->isNotEmpty()) {
- $dupes = $files->filter(fn ($file) => $duplicates->contains($file['hash']))->unique();
-
- $fail('Please do not upload the same files ('.$dupes->map->file->join(', ').') multiple times.');
- }
- }
- ),
- ]);
-
- $this->action(function ($data, $record) {
- $update = $record->action()->create([
- 'user_id' => Auth::id(),
- 'actions.request_id' => $record->id,
- 'status' => $data['status'],
- 'remarks' => $data['remarks'],
- 'time' => now(),
- ]);
- if (($attachments = collect(current($data['attachments'])))->isNotEmpty()) {
- $files = $attachments
- ->mapWithKeys(fn (string $file) => [
- str(str()->ulid())
- ->prepend("attachments/action-{$update->id}-")
- ->append(($ext = pathinfo($file, PATHINFO_EXTENSION)) ? ".$ext" : '')
- ->lower()
- ->toString() => $file,
- ])
- ->each(fn (string $file, string $path) => Storage::move("public/$file", "public/$path"));
-
- $update->attachment()->create([
- 'files' => $files->map(fn ($file) => basename($file))->toArray(),
- 'paths' => $files->keys()->toArray(),
- ]);
-
- Process::run(['rm', '-rf', Storage::path('public/attachments/tmp/'.$record->id)]);
- }
-
- Notification::make()
- ->title('Submitted Successfully!')
- ->success()
- ->send();
-
- Notification::make()
- ->title('Request '.$data['status'])
- ->body(str("Request “{$record->subject}” has been {$data['status']} by ".auth()->user()->name.'.')->toHtmlString())
- ->icon(RequestStatus::tryFrom($data['status'])?->getIcon())
- ->iconColor(RequestStatus::tryFrom($data['status'])?->getColor())
- ->sendToDatabase($record->requestor);
-
- $this->successNotificationTitle('Request updated');
- });
- }
-}
diff --git a/app/Filament/Actions/Traits/ViewRequestHistoryTrait.php b/app/Filament/Actions/Traits/ViewRequestHistoryTrait.php
deleted file mode 100644
index c16a6c8..0000000
--- a/app/Filament/Actions/Traits/ViewRequestHistoryTrait.php
+++ /dev/null
@@ -1,37 +0,0 @@
-name ??= 'history';
-
- $this->modalSubmitAction(false);
-
- $this->color('primary');
-
- $this->icon('gmdi-timeline-o');
-
- $this->slideOver();
-
- $this->modalWidth('2xl');
-
- $this->modalContent(function (Request $record) {
- $record->load([
- 'actions' => fn ($q) => $q->orderBy('created_at', 'desc'),
- 'actions.attachment',
- 'attachment',
- ]);
-
- return view('filament.request.history', [
- 'request' => $record,
- ]);
- });
- }
-}
diff --git a/app/Filament/Actions/UpdateRequestAction.php b/app/Filament/Actions/UpdateRequestAction.php
deleted file mode 100644
index d3412bc..0000000
--- a/app/Filament/Actions/UpdateRequestAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-name('view-qr-code');
+
+ $this->label('View QR Code');
+
+ $this->icon('gmdi-qr-code-o');
+
+ $this->hidden(fn () => Filament::getCurrentPanel()->getId() !== 'admin');
+
+ $this->modalIcon('gmdi-qr-code-o');
+
+ $this->modalWidth(\Filament\Support\Enums\MaxWidth::Small);
+
+ $this->modalDescription('You can download this QR code and share it to access the feedback form for your office.');
+
+ $this->modalHeading(request()->user()->organization->code ?? '' . ' QR Code');
+
+ $this->modalSubmitActionLabel('Download');
+
+ $this->modalCancelAction(false);
+
+ $this->closeModalByClickingAway(false);
+
+ $this->modalFooterActionsAlignment(Alignment::Center);
+
+
+ $this->modalContent(function () {
+ return view('filament.panels.admin.clusters.organization.view-qrCode', [
+ 'qr' => QrCode::size(200)->generate(url('/') . '/feedback/' . request()->user()->organization_id . '/feedback'),
+ ]);
+ });
+
+ $this->action(function () {
+ $filename = 'QR-' . request()->user()->organization->code . '.png';
+ $qrCode = QrCode::format('png')
+ ->size(1024)
+ ->generate(url('/') . '/feedback/' . request()->user()->organization_id . '/feedback');
+
+ return response()->streamDownload(
+ fn () => print($qrCode),
+ $filename,
+ ['Content-Type' => 'image/png']
+ );
+ });
+ }
+}
diff --git a/app/Filament/Actions/ViewRequestHistoryAction.php b/app/Filament/Actions/ViewRequestHistoryAction.php
deleted file mode 100644
index 4f8a720..0000000
--- a/app/Filament/Actions/ViewRequestHistoryAction.php
+++ /dev/null
@@ -1,11 +0,0 @@
-visible($office)
- ->relationship('office', 'name')
- ->native(false)
- ->searchable()
- ->preload()
- ->required()
- ->editOptionAction(fn ($action) => $action->slideOver())
- ->editOptionForm([
- Forms\Components\TextInput::make('name')
- ->unique(ignoreRecord: true)
- ->markAsRequired()
- ->rule('required')
- ->maxLength(255),
- Forms\Components\TextInput::make('acronym')
- ->unique(ignoreRecord: true)
- ->maxLength(255),
- ])
- ->createOptionAction(fn ($action) => $action->slideOver())
- ->createOptionForm([
- Forms\Components\TextInput::make('name')
- ->unique(ignoreRecord: true)
- ->markAsRequired()
- ->rule('required')
- ->maxLength(255),
- Forms\Components\TextInput::make('acronym')
- ->unique(ignoreRecord: true)
- ->maxLength(255),
- ]),
- Forms\Components\TextInput::make('name')
- ->markAsRequired()
- ->rule('required')
- ->maxLength(255),
- Forms\Components\Fieldset::make('Tags')
- ->schema([
- Forms\Components\Repeater::make('tag')
- ->relationship('tags')
- ->columnSpanFull()
- ->hiddenLabel()
- ->grid(3)
- ->simple(
- Forms\Components\TextInput::make('name')
- ->distinct()
- ->markAsRequired()
- ->rule('required')
- ->maxLength(15),
- ),
- ]),
- ];
- }
-
- public static function form(Form $form): Form
- {
- return $form->schema([
- Forms\Components\Section::make('Category')
- ->schema(static::formSchema(true)),
- ]);
- }
-
- public static function table(Table $table): Table
- {
- return $table
- ->columns([
- Tables\Columns\TextColumn::make('name')
- ->searchable(),
- Tables\Columns\TextColumn::make('tags.name')
- ->badge()
- ->searchable(),
- Tables\Columns\TextColumn::make('office.name')
- ->searchable(),
- ])
- ->filters([
- Tables\Filters\SelectFilter::make('office_id')
- ->relationship('office', 'name')
- ->label('Office')
- ->searchable()
- ->preload()
- ->multiple(),
- ])
- ->actions([
- Tables\Actions\EditAction::make(),
- ])
- ->recordUrl(null);
- }
-
- public static function getRelations(): array
- {
- return [
- SubcategoriesRelationManager::class,
- ];
- }
-
- public static function getPages(): array
- {
- return [
- 'index' => Pages\ListCategories::route('/'),
- 'create' => Pages\CreateCategory::route('/create'),
- 'edit' => Pages\EditCategory::route('/{record}/edit'),
- ];
- }
-}
diff --git a/app/Filament/Admin/Resources/CategoryResource/Pages/CreateCategory.php b/app/Filament/Admin/Resources/CategoryResource/Pages/CreateCategory.php
deleted file mode 100644
index b6ad327..0000000
--- a/app/Filament/Admin/Resources/CategoryResource/Pages/CreateCategory.php
+++ /dev/null
@@ -1,11 +0,0 @@
-schema([
- Forms\Components\TextInput::make('name')
- ->required()
- ->columnSpanFull()
- ->maxLength(255),
- Forms\Components\Fieldset::make('Tags')
- ->schema([
- Forms\Components\Repeater::make('tag')
- ->relationship('tags')
- ->columnSpanFull()
- ->hiddenLabel()
- ->grid(3)
- ->simple(
- Forms\Components\TextInput::make('name')
- ->distinct()
- ->markAsRequired()
- ->rule('required')
- ->maxLength(15),
- ),
- ]),
- ]);
- }
-
- public function table(Table $table): Table
- {
- return $table
- ->recordTitleAttribute('name')
- ->columns([
- Tables\Columns\TextColumn::make('name'),
- Tables\Columns\TextColumn::make('tags.name')
- ->limit(20),
- ])
- ->filters([
- //
- ])
- ->headerActions([
- Tables\Actions\CreateAction::make()
- ->slideOver(),
- ])
- ->actions([
- Tables\Actions\EditAction::make()
- ->slideOver(),
- Tables\Actions\DeleteAction::make(),
- ])
- ->recordAction(null);
- }
-}
diff --git a/app/Filament/Admin/Resources/OfficeResource.php b/app/Filament/Admin/Resources/OfficeResource.php
deleted file mode 100644
index ea229ba..0000000
--- a/app/Filament/Admin/Resources/OfficeResource.php
+++ /dev/null
@@ -1,92 +0,0 @@
-schema([
- Forms\Components\Section::make('Office')
- ->columns(3)
- ->schema([
- Forms\Components\FileUpload::make('logo')
- ->avatar()
- ->directory('logos'),
- Forms\Components\Group::make()
- ->columnSpan(2)
- ->schema([
- Forms\Components\TextInput::make('name')
- ->unique(ignoreRecord: true)
- ->markAsRequired()
- ->rule('required')
- ->maxLength(255),
- Forms\Components\TextInput::make('acronym')
- ->required()
- ->unique(ignoreRecord: true)
- ->required()
- ->maxLength(255),
- ]),
- Forms\Components\TextInput::make('address')
- ->maxLength(255),
- Forms\Components\TextInput::make('building')
- ->maxLength(255),
- Forms\Components\TextInput::make('room')
- ->maxLength(255),
- ]),
- ]);
- }
-
- public static function table(Table $table): Table
- {
- return $table
- ->columns([
- Tables\Columns\ImageColumn::make('logo')
- ->circular()
- ->label('Logo'),
- Tables\Columns\TextColumn::make('name')
- ->searchable(),
- Tables\Columns\TextColumn::make('acronym')
- ->searchable(),
- Tables\Columns\TextColumn::make('building')
- ->searchable(),
- Tables\Columns\TextColumn::make('address')
- ->searchable(),
-
- ])
- ->actions([
- Tables\Actions\EditAction::make(),
- ])
- ->recordUrl(false);
- }
-
- public static function getRelations(): array
- {
- return [
- CategoriesRelationManager::class,
- SubcategoriesRelationManager::class,
- ];
- }
-
- public static function getPages(): array
- {
- return [
- 'index' => Pages\ListOffices::route('/'),
- 'create' => Pages\CreateOffice::route('/create'),
- 'edit' => Pages\EditOffice::route('/{record}/edit'),
- ];
- }
-}
diff --git a/app/Filament/Admin/Resources/OfficeResource/Pages/CreateOffice.php b/app/Filament/Admin/Resources/OfficeResource/Pages/CreateOffice.php
deleted file mode 100644
index 49392b0..0000000
--- a/app/Filament/Admin/Resources/OfficeResource/Pages/CreateOffice.php
+++ /dev/null
@@ -1,11 +0,0 @@
-columns(1)
- ->schema(CategoryResource::formSchema());
- }
-
- public function table(Table $table): Table
- {
- return $table
- ->recordTitleAttribute('name')
- ->columns([
- Tables\Columns\TextColumn::make('name')
- ->searchable(),
- Tables\Columns\TextColumn::make('tags.name')
- ->limit(20),
- ])
- ->headerActions([
- Tables\Actions\CreateAction::make()
- ->slideOver(),
- ])
- ->actions([
- Tables\Actions\EditAction::make()
- ->slideOver(),
- Tables\Actions\DeleteAction::make(),
- ])
- ->recordAction(null);
- }
-}
diff --git a/app/Filament/Admin/Resources/OfficeResource/RelationManagers/SubcategoriesRelationManager.php b/app/Filament/Admin/Resources/OfficeResource/RelationManagers/SubcategoriesRelationManager.php
deleted file mode 100644
index f7d6830..0000000
--- a/app/Filament/Admin/Resources/OfficeResource/RelationManagers/SubcategoriesRelationManager.php
+++ /dev/null
@@ -1,92 +0,0 @@
-columns(1)
- ->schema([
- Forms\Components\Select::make('category_id')
- ->relationship('category', 'name')
- ->native(false)
- ->searchable()
- ->preload()
- ->required()
- ->editOptionAction(fn ($action) => $action->slideOver())
- ->editOptionForm([
- Forms\Components\TextInput::make('name')
- ->markAsRequired()
- ->rule('required')
- ->maxLength(255),
- ])
- ->createOptionAction(fn ($action) => $action->slideOver())
- ->createOptionForm([
- Forms\Components\Hidden::make('office_id')
- ->default($this->ownerRecord->getKey()),
- Forms\Components\TextInput::make('name')
- ->markAsRequired()
- ->rule('required')
- ->maxLength(255),
- ]),
- Forms\Components\TextInput::make('name')
- ->markAsRequired()
- ->rule('required')
- ->maxLength(255),
- Forms\Components\Fieldset::make('Tags')
- ->schema([
- Forms\Components\Repeater::make('tag')
- ->relationship('tags')
- ->columnSpanFull()
- ->hiddenLabel()
- ->grid(3)
- ->simple(
- Forms\Components\TextInput::make('name')
- ->distinct()
- ->markAsRequired()
- ->rule('required')
- ->maxLength(255)
- ),
- ]),
- ]);
- }
-
- public function table(Table $table): Table
- {
- return $table
- ->recordTitleAttribute('name')
- ->columns([
- Tables\Columns\TextColumn::make('name')
- ->searchable(),
- Tables\Columns\TextColumn::make('tags.name')
- ->limit(20),
- ])
- ->filters([
- Tables\Filters\SelectFilter::make('category')
- ->relationship('category', 'name')
- ->searchable()
- ->preload()
- ->multiple(),
- ])
- ->headerActions([
- Tables\Actions\CreateAction::make()
- ->slideOver(),
- ])
- ->actions([
- Tables\Actions\EditAction::make()
- ->slideOver(),
- Tables\Actions\DeleteAction::make(),
- ])
- ->recordAction(null);
- }
-}
diff --git a/app/Filament/Admin/Resources/RequestResource.php b/app/Filament/Admin/Resources/RequestResource.php
deleted file mode 100644
index 6199850..0000000
--- a/app/Filament/Admin/Resources/RequestResource.php
+++ /dev/null
@@ -1,192 +0,0 @@
-modifyQueryUsing(function (Builder $query) {
- $query->whereHas('action');
- // filter only published requests
- })
- ->columns([
- Tables\Columns\TextColumn::make('requestor.name')
- ->label('Requestor Name')
- ->searchable()
- ->sortable()
- ->limit(13),
- Tables\Columns\TextColumn::make('office.acronym')
- ->searchable()
- ->sortable(),
- Tables\Columns\TextColumn::make('subject')
- ->searchable()
- ->sortable(),
- Tables\Columns\TextColumn::make('category.name')
- ->searchable()
- ->sortable(),
- Tables\Columns\TextColumn::make('action.status')
- ->badge(),
- ])
- ->filters([
- Tables\Filters\SelectFilter::make('office')
- ->relationship('office', 'acronym')
- ->searchable()
- ->preload(),
- ])
- ->actions([
- Tables\Actions\ViewAction::make()
- ->modalWidth('7xl')
- ->infolist([
- ComponentsGrid::make(12)
- ->schema([
- Group::make([
- Section::make('Personal Details')
- ->columnSpan(8)
- ->columns(3)
- ->schema([
- TextEntry::make('requestor.name')
- ->label('Name'),
- TextEntry::make('requestor.number')
- ->prefix('+63 0')
- ->label('Phone Number'),
- TextEntry::make('requestor.email')
- ->label('Email'),
- ]),
- Section::make('Office Details')
- ->columnSpan(8)
- ->columns(3)
- ->schema([
- TextEntry::make('office.acronym')
- ->label('Office'),
- TextEntry::make('office.room')
- ->label('Room Number'),
- TextEntry::make('office.address')
- ->label('Office address :'),
-
- ]),
-
- ])->columnSpan(8),
-
- Group::make([
- Section::make('Availability')
- ->columnSpan(4)
- ->columns(2)
- ->schema([
- TextEntry::make('availability_from')
- ->columnSpan(1)
- ->date()
- ->label('Availability from'),
- TextEntry::make('availability_to')
- ->columnSpan(1)
- ->date()
- ->label('Availability to'),
-
- ]),
- Section::make('Assignees')
- ->columnSpan(4)
- ->schema([
- TextEntry::make('')
- ->label(false)
- ->placeholder(fn ($record) => implode(', ', $record->assignees->pluck('name')->toArray()))
- ->inLinelabel(false),
- ]),
-
- ])->columnSpan(4),
- Section::make('Request Details')
- ->columns(2)
- ->schema([
- TextEntry::make('category.name')
- ->label('Category'),
- TextEntry::make('subcategory.name')
- ->label('Subcategory'),
- ])->columnSpan(4),
-
- Group::make([
-
- Section::make('Attachments')
- ->columns(2)
- ->schema(function ($record) {
- return [
- TextEntry::make('attachment.attachable_id')
- ->formatStateUsing(function ($record) {
- $attachments = json_decode($record->attachment->files, true);
-
- $html = collect($attachments)->map(function ($filename, $path) {
- $fileName = basename($path);
- $fileUrl = Storage::url($path);
-
- return "{$filename}";
- })->implode('
');
-
- return $html;
- })
- ->openUrlInNewTab()
- ->label(false)
- ->inLineLabel(false)
- ->html(),
- ];
- }),
- ])->columnSpan(4),
- Group::make([
- Section::make('Request Rating')
- ->columnSpan(4)
- ->visible(fn ($record) => in_array(RequestStatus::RESOLVED, $record->actions->pluck('status')->toArray()))
- ->schema([
- TextEntry::make('remarks')
- ->markdown()
- ->label(false),
- ]),
-
- ])->columnSpan(4),
- Section::make('Remarks')
- ->columnSpan(12)
- ->schema([
- TextEntry::make('remarks')
- ->columnSpan(2)
- ->formatStateUsing(fn ($record) => new HtmlString($record->remarks))
- ->label(false),
- ]),
- ]),
-
- ]),
- ViewRequestHistoryAction::make(),
- ]
- );
-
- }
-
- public static function getRelations(): array
- {
- return [
-
- ];
- }
-
- public static function getPages(): array
- {
- return [
- 'index' => Pages\ListRequests::route('/'),
- ];
- }
-}
diff --git a/app/Filament/Admin/Resources/RequestResource/Pages/ListRequests.php b/app/Filament/Admin/Resources/RequestResource/Pages/ListRequests.php
deleted file mode 100644
index f250c2b..0000000
--- a/app/Filament/Admin/Resources/RequestResource/Pages/ListRequests.php
+++ /dev/null
@@ -1,65 +0,0 @@
-label('All Requests')
- ->badgeColor('success')
- ->modifyQueryUsing(fn (Builder $query) => $query),
- Tab::make('publised')
- ->label('Published')
- ->modifyQueryUsing(fn (Builder $query) => $query->whereHas('action', function (Builder $query) {
- $query->where('status', RequestStatus::PUBLISHED);
- })),
- Tab::make('assigned')
- ->label('Assigned')
- ->modifyQueryUsing(fn (Builder $query) => $query->whereHas('action', function (Builder $query) {
- $query->where('status', RequestStatus::ASSIGNED);
- })),
- Tab::make('approved')
- ->label('Approved')
- ->modifyQueryUsing(fn (Builder $query) => $query->whereHas('action', function (Builder $query) {
- $query->where('status', RequestStatus::APPROVED);
- })),
- Tab::make('started')
- ->label('Started')
- ->modifyQueryUsing(fn (Builder $query) => $query->whereHas('action', function (Builder $query) {
- $query->where('status', RequestStatus::STARTED);
- })),
- Tab::make('resolved')
- ->label('Resolved')
- ->modifyQueryUsing(fn (Builder $query) => $query->whereHas('action', function (Builder $query) {
- $query->where('status', RequestStatus::RESOLVED);
- })),
- ];
- }
-}
diff --git a/app/Filament/Admin/Resources/UserResource.php b/app/Filament/Admin/Resources/UserResource.php
deleted file mode 100644
index 82e9eb7..0000000
--- a/app/Filament/Admin/Resources/UserResource.php
+++ /dev/null
@@ -1,164 +0,0 @@
-schema([
- Forms\Components\Section::make('Profile')
- ->columns(3)
- ->schema([
- Forms\Components\FileUpload::make('avatar')
- ->avatar()
- ->directory('avatars'),
- Forms\Components\Group::make()
- ->columnSpan(2)
- ->schema([
- Forms\Components\TextInput::make('name')
- ->markAsRequired()
- ->rule('required')
- ->maxLength(255),
- Forms\Components\Select::make('office_id')
- ->relationship('office', 'name')
- ->markAsRequired()
- ->rule('required')
- ->searchable()
- ->preload(),
- ]),
- Forms\Components\TextInput::make('email')
- ->markAsRequired()
- ->rules(['required', 'email'])
- ->maxLength(255),
- Forms\Components\Select::make('role')
- ->options(UserRole::class)
- ->searchable(),
- Forms\Components\TextInput::make('number')
- ->placeholder('9xx xxx xxxx')
- ->mask('999 999 9999')
- ->prefix('+63 ')
- ->rule(fn () => function ($a, $v, $f) {
- if (! preg_match('/^9.*/', $v)) {
- $f('Incorrect number format');
- }
- }),
- ]),
- Forms\Components\Section::make('Password')
- ->visible(fn ($operation) => $operation === 'create')
- ->columns(2)
- ->schema([
- Forms\Components\TextInput::make('password')
- ->password()
- ->revealable()
- ->dehydrated(fn (?string $state) => ! is_null($state))
- ->markAsRequired()
- ->rule('required'),
- Forms\Components\TextInput::make('confirm_password')
- ->password()
- ->markAsRequired()
- ->rule('required')
- ->same('password')
- ->revealable(),
- ]),
- ]);
- }
-
- public static function table(Table $table): Table
- {
- return $table
- ->columns([
- Tables\Columns\ImageColumn::make('avatar')
- ->circular(),
- Tables\Columns\TextColumn::make('name')
- ->searchable(),
- Tables\Columns\TextColumn::make('role')
- ->sortable(),
- Tables\Columns\TextColumn::make('number')
- ->prefix('+63 0'),
- Tables\Columns\TextColumn::make('email')
- ->searchable(),
- Tables\Columns\ToggleColumn::make('is_active')
- ->sortable()
- ->hidden(fn ($livewire) => $livewire->activeTab === 'pending')
- ->onColor('success'),
- ])
- ->filters([
- Tables\Filters\SelectFilter::make('role')
- ->options(UserRole::class)
- ->searchable()
- ->preload()
- ->multiple(),
- Tables\Filters\SelectFilter::make('office')
- ->relationship('office', 'name')
- ->searchable()
- ->preload()
- ->multiple(),
- ])
- ->actions([
- Tables\Actions\Action::make('approve')
- ->button()
- ->label('Approved')
- ->icon(RequestStatus::APPROVED->getIcon())
- ->color(RequestStatus::APPROVED->getColor())
- ->visible(fn ($record) => $record->email_verified_at === null)
- ->form([
- Select::make('office_id')
- ->relationship('office', 'name')
- ->default(fn ($record) => $record->office?->name)
- ->native(false)
- ->markAsRequired()
- ->rule('required')
- ->searchable()
- ->preload(),
- Select::make('role')
- ->options(UserRole::class)
- ->default(fn ($record) => $record->role),
- ])
- ->action(function ($data, $record) {
- $record->update([
- 'id' => $record->id,
- 'name' => $record->name,
- 'email' => $record->email,
- 'password' => $record->password,
- 'is_active' => 'TRUE',
- 'email_verified_at' => now(),
- 'role' => $data['role'],
- 'office_id' => $data['office_id'],
- ]);
- }),
- Tables\Actions\EditAction::make(),
- ])
- ->recordUrl(null);
- }
-
- public static function getRelations(): array
- {
- return [
- //
- ];
- }
-
- public static function getPages(): array
- {
- return [
- 'index' => Pages\ListUsers::route('/'),
- 'create' => Pages\CreateUser::route('/create'),
- 'edit' => Pages\EditUser::route('/{record}/edit'),
- ];
- }
-}
diff --git a/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php b/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php
deleted file mode 100644
index f67c2b2..0000000
--- a/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php
+++ /dev/null
@@ -1,11 +0,0 @@
-requiresConfirmation()
- ->modalDescription()
- ->icon('heroicon-s-lock-closed')
- ->form([
- TextInput::make('password')
- ->password()
- ->markAsRequired()
- ->rules('required')
- ->revealable(),
- TextInput::make('confirm_password')
- ->password()
- ->markAsRequired()
- ->rules('required')
- ->same('password')
- ->revealable(),
- ])
- ->closeModalByClickingAway(false)
- ->action(function (array $data, User $record) {
- $record->update($data);
-
- Notification::make()
- ->title('Pasword updated successfully')
- ->success()
- ->send();
- }),
- Actions\DeleteAction::make(),
- ];
- }
-}
diff --git a/app/Filament/Admin/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Admin/Resources/UserResource/Pages/ListUsers.php
deleted file mode 100644
index 918a12c..0000000
--- a/app/Filament/Admin/Resources/UserResource/Pages/ListUsers.php
+++ /dev/null
@@ -1,30 +0,0 @@
- Tab::make('Approved User')
- ->modifyQueryUsing(fn ($query) => $query->whereNotNull('email_verified_at')),
- 'pending' => Tab::make('Pending User')
- ->modifyQueryUsing(fn ($query) => $query->whereNull('email_verified_at')),
- ];
- }
-}
diff --git a/app/Filament/Auth/RegistrationPage.php b/app/Filament/Auth/RegistrationPage.php
deleted file mode 100644
index 6271ec7..0000000
--- a/app/Filament/Auth/RegistrationPage.php
+++ /dev/null
@@ -1,132 +0,0 @@
-rateLimit(2);
- } catch (TooManyRequestsException $exception) {
- $this->getRateLimitedNotification($exception)?->send();
-
- return null;
- }
-
- $user = $this->wrapInDatabaseTransaction(function () {
- $this->callHook('beforeValidate');
-
- $data = $this->form->getState();
-
- $this->callHook('afterValidate');
-
- $data = $this->mutateFormDataBeforeRegister($data);
-
- $this->callHook('beforeRegister');
-
- $user = $this->handleRegistration($data);
-
- $this->form->model($user)->saveRelationships();
-
- $this->notifyAdmin($data);
-
- $this->callHook('afterRegister');
-
- return $user;
- });
-
- event(new Registered($user));
-
- Notification::make()
- ->title('Registered Successfully')
- ->success()
- ->send();
-
- session()->regenerate();
-
- return app(RegistrationResponse::class);
- }
-
- public function form(Form $form): Form
- {
- return $this->makeForm()
- ->schema([
- $this->getAvatarFormComponent(),
- Group::make()
- ->columns(2)
- ->schema([
- Group::make()
- ->schema([
- $this->getNameFormComponent(),
- $this->getEmailFormComponent(),
- $this->getPasswordFormComponent(),
- ]),
- Group::make()
- ->schema([
- $this->getOfficeFormComponent(),
- $this->getNumberFormComponent(),
- $this->getPasswordConfirmationFormComponent(),
- ]),
- ]),
- ])
- ->statePath('data');
- }
-
- protected function getAvatarFormComponent(): Component
- {
- return FileUpload::make('avatar')
- ->alignCenter()
- ->avatar()
- ->directory('avatars');
- }
-
- protected function getOfficeFormComponent(): Component
- {
- return Select::make('office_id')
- ->native(false)
- ->searchable()
- ->options(Office::query()->pluck('acronym', 'id'))
- ->getSearchResultsUsing(fn ($search): array => Office::where('name', 'like', "{$search}")->pluck('acronym', 'id')->toArray())
- ->getOptionLabelUsing(fn ($value): ?string => Office::find($value)?->acronym);
- }
-
- protected function getNumberFormComponent(): Component
- {
- return TextInput::make('number')
- ->required()
- ->placeholder('9xx xxx xxxx')
- ->mask('999 999 9999')
- ->prefix('+63 ')
- ->rule(fn () => function ($a, $v, $f) {
- if (! preg_match('/^9.*/', $v)) {
- $f('Incorrect number format');
- }
- });
- }
-
- private function notifyAdmin(array $data): Notification
- {
- return Notification::make()
- ->title('Newly Registered User!')
- ->icon(RequestStatus::ACCEPTED->getIcon())
- ->iconColor(RequestStatus::ACCEPTED->getColor())
- ->body(str("{$data['name']} has been registered in the system")->toHtmlString())
- ->sendToDatabase(User::where('role', 'admin')->get());
- }
-}
diff --git a/app/Filament/AvatarProviders/UiAvatarsProvider.php b/app/Filament/AvatarProviders/UiAvatarsProvider.php
new file mode 100644
index 0000000..b55b7cd
--- /dev/null
+++ b/app/Filament/AvatarProviders/UiAvatarsProvider.php
@@ -0,0 +1,25 @@
+trim()
+ ->explode(' ')
+ ->map(fn (string $segment): string => filled($segment) ? mb_substr($segment, 0, 1) : '')
+ ->join(' ');
+
+ $backgroundColor = Rgb::fromString('rgb('.FilamentColor::getColors()['primary'][500].')')->toHex();
+
+ return 'https://ui-avatars.com/api/?name='.urlencode($name).'&color=FFFFFF&background='.str($backgroundColor)->after('#');
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers.php b/app/Filament/Clusters/Dossiers.php
new file mode 100644
index 0000000..4a5c227
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers.php
@@ -0,0 +1,12 @@
+schema([
+ Forms\Components\Hidden::make('user_id')
+ ->default(Auth::id()),
+ Forms\Components\Select::make('organization_id')
+ ->columnSpanFull()
+ ->visible(Auth::user()->root)
+ ->default(Auth::user()->organization_id)
+ ->dehydratedWhenHidden()
+ ->relationship('organization', 'name')
+ ->preload()
+ ->required(),
+ Forms\Components\TextInput::make('name')
+ ->columnSpanFull()
+ ->required(),
+ Forms\Components\MarkdownEditor::make('description')
+ ->columnSpanFull()
+ ->required(),
+ Forms\Components\Repeater::make('records')
+ ->visibleOn('create')
+ ->columnSpanFull()
+ ->relationship()
+ ->required()
+ ->addActionLabel('Add record')
+ ->defaultItems(1)
+ ->simple(
+ Forms\Components\Select::make('request_id')
+ ->relationship(
+ 'request',
+ 'code',
+ fn (Builder $query) => $query->where(function ($query) {
+ if (Auth::user()->root) {
+ return;
+ }
+
+ $query->where('organization_id', Auth::user()->organization_id);
+
+ $query->orWhere('from_id', Auth::user()->organization_id);
+ }),
+ )
+ ->searchable(['code', 'subject'])
+ ->distinct()
+ ->preload()
+ ->getOptionLabelFromRecordUsing(fn (Request $request) => "#{$request->code} — {$request->subject}")
+ ->required()
+ ->validationMessages(['distinct' => 'These fields must not have a duplicate value.']),
+ ),
+ ]);
+ }
+
+ public static function table(Table $table): Table
+ {
+ return $table
+ ->columns([
+ Tables\Columns\TextColumn::make('name')
+ ->searchable()
+ ->limit(36)
+ ->wrap()
+ ->tooltip(fn ($column) => strlen($column->getState()) > $column->getCharacterLimit() ? $column->getState() : null),
+ Tables\Columns\TextColumn::make('requests_count')
+ ->counts('requests')
+ ->label('Requests'),
+ Tables\Columns\TextColumn::make('user.name')
+ ->searchable(['name', 'email']),
+ ])
+ ->filters([
+ Tables\Filters\TrashedFilter::make(),
+ ])
+ ->actions([
+ NoteDossierAction::make(),
+ Tables\Actions\ViewAction::make()
+ ->url(fn (Dossier $dossier, Component $livewire) => $livewire::getResource()::getUrl('show', ['record' => $dossier->id])),
+ ])
+ ->emptyStateHeading('No dossiers found')
+ ->emptyStateDescription('Create a new dossier to make a collection of related requests.')
+ ->recordAction(null);
+ }
+
+ public static function infolist(Infolist $infolist): Infolist
+ {
+ return $infolist
+ ->columns(1)
+ ->schema([
+ Infolists\Components\TextEntry::make('name')
+ ->size(TextEntrySize::Large)
+ ->weight(FontWeight::Bold),
+ Infolists\Components\TextEntry::make('created')
+ ->weight(FontWeight::SemiBold)
+ ->state(fn (Dossier $record) => "By {$record->user->name} on {$record->created_at->format('jS \of F Y')} at {$record->created_at->format('H:i')}"
+ ),
+ Infolists\Components\TextEntry::make('description')
+ ->visible(fn (Dossier $record) => filled($record->description))
+ ->markdown(),
+ Infolists\Components\RepeatableEntry::make('notes')
+ // ->contained(false)
+ ->visible(fn (Dossier $record) => $record->notes->isNotEmpty())
+ ->schema([
+ Infolists\Components\TextEntry::make('user.name')
+ ->suffixAction(fn (Note $note) => Infolists\Components\Actions\Action::make('delete-'.$note->id)
+ ->requiresConfirmation()
+ ->icon('heroicon-o-trash')
+ ->color('danger')
+ ->modalHeading('Delete note')
+ ->visible(Auth::user()->is($note->user) || Auth::user()->admin)
+ ->action(function () use ($note) {
+ $note->delete();
+
+ Notification::make()
+ ->title('Note deleted')
+ ->success()
+ ->send();
+ }),
+ )
+ ->getStateUsing(function (Note $note) {
+ $username = $note->user?->name ?? '(non-existent user)';
+
+ return str("{$username} on {$note->created_at->format('jS \of F Y')} at {$note->created_at->format('H:i')}")
+ ->toHtmlString();
+ })
+ ->hiddenLabel(),
+ Infolists\Components\TextEntry::make('content')
+ ->hiddenLabel()
+ ->markdown(),
+ Infolists\Components\ViewEntry::make('attachment')
+ ->hiddenLabel()
+ ->visible(fn (Note $note) => $note->attachment?->exists)
+ ->view('filament.attachments.show'),
+ ]),
+ ]);
+ }
+
+ public static function getRelations(): array
+ {
+ return [
+ RequestsRelationManager::make(),
+ ];
+ }
+
+ public static function getPages(): array
+ {
+ return [
+ 'index' => Pages\ListDossiers::route('/'),
+ 'show' => Pages\ViewDossier::route('/{record}'),
+ ];
+ }
+
+ public static function getEloquentQuery(): Builder
+ {
+ $panel = Filament::getCurrentPanel()->getId();
+
+ $query = parent::getEloquentQuery()
+ ->withoutGlobalScopes([
+ SoftDeletingScope::class,
+ ]);
+
+ return match ($panel) {
+ 'root' => $query,
+ default => $query->where('organization_id', Auth::user()->organization_id),
+ };
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers/Resources/AllDossierResource/Pages/ListDossiers.php b/app/Filament/Clusters/Dossiers/Resources/AllDossierResource/Pages/ListDossiers.php
new file mode 100644
index 0000000..7f8e7e9
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers/Resources/AllDossierResource/Pages/ListDossiers.php
@@ -0,0 +1,31 @@
+label('New dossier')
+ ->modalHeading('Create new dossier')
+ ->createAnother(false)
+ ->slideOver()
+ ->modalWidth(MaxWidth::ExtraLarge),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers/Resources/AllDossierResource/Pages/ViewDossier.php b/app/Filament/Clusters/Dossiers/Resources/AllDossierResource/Pages/ViewDossier.php
new file mode 100644
index 0000000..27edd52
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers/Resources/AllDossierResource/Pages/ViewDossier.php
@@ -0,0 +1,55 @@
+record->name)->limit(36, '...', true);
+ }
+
+ public function getBreadcrumbs(): array
+ {
+ return array_merge(array_slice(parent::getBreadcrumbs(), 0, -1), [
+ str($this->record->name)->limit(36, '...', true),
+ ]);
+ }
+
+ public function getSubNavigation(): array
+ {
+ if (filled($cluster = static::getCluster())) {
+ return $this->generateNavigationItems($cluster::getClusteredComponents());
+ }
+
+ return [];
+ }
+
+ protected function getHeaderActions(): array
+ {
+ return [
+ Actions\RestoreAction::make()
+ ->label('Restore')
+ ->modalHeading('Restore dossier'),
+ NoteDossierAction::make()
+ ->icon(null),
+ Actions\EditAction::make()
+ ->label('Edit')
+ ->hidden($this->record->trashed())
+ ->slideOver(),
+ Actions\ActionGroup::make([
+ Actions\DeleteAction::make()
+ ->modalHeading('Delete dossier'),
+ Actions\ForceDeleteAction::make()
+ ->modalHeading('Force delete dossier'),
+ ]),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers/Resources/AllDossierResource/RelationManagers/RequestsRelationManager.php b/app/Filament/Clusters/Dossiers/Resources/AllDossierResource/RelationManagers/RequestsRelationManager.php
new file mode 100644
index 0000000..2727e0a
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers/Resources/AllDossierResource/RelationManagers/RequestsRelationManager.php
@@ -0,0 +1,67 @@
+recordTitleAttribute('code')
+ ->columns([
+ Tables\Columns\TextColumn::make('action.status')
+ ->label('Status')
+ ->badge()
+ ->state(fn (Request $request) => $request->action->status === ActionStatus::CLOSED ? $request->action->resolution : $request->action->status),
+ Tables\Columns\TextColumn::make('code')
+ ->extraCellAttributes(['class' => 'font-mono'])
+ ->getStateUsing(fn (Request $request) => "#{$request->code}")
+ ->searchable(),
+ Tables\Columns\TextColumn::make('subject')
+ ->searchable(),
+ ])
+ ->headerActions([
+ Tables\Actions\AttachAction::make()
+ ->label('Add request')
+ ->attachAnother(false)
+ ->preloadRecordSelect()
+ ->recordSelectSearchColumns(['code', 'subject'])
+ ->mutateFormDataUsing(fn (array $data) => [...$data, 'user_id' => Auth::id()]),
+ ])
+ ->actions([
+ ViewRequestAction::make(),
+ ViewRequestHistoryAction::make(),
+ Tables\Actions\ActionGroup::make([
+ Tables\Actions\DetachAction::make()
+ ->label('Remove')
+ ->modalHeading('Remove request from dossier')
+ ->modalDescription('Are you sure you want to remove this request from this dossier?'),
+ ]),
+ ])
+ ->bulkActions([
+ Tables\Actions\DetachBulkAction::make()
+ ->label('Remove')
+ ->modalHeading('Remove selected requests from dossier')
+ ->modalDescription('Are you sure you want to remove these selected requests from this dossier?'),
+ ])
+ ->defaultSort('requests.created_at', 'desc');
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers/Resources/ClosedDossierResource.php b/app/Filament/Clusters/Dossiers/Resources/ClosedDossierResource.php
new file mode 100644
index 0000000..51166e4
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers/Resources/ClosedDossierResource.php
@@ -0,0 +1,81 @@
+ Pages\ListClosedDossiers::route('/'),
+ 'show' => Pages\ViewDossier::route('/{record}'),
+ ];
+ }
+
+ public static function getEloquentQuery(): Builder
+ {
+ $panel = Filament::getCurrentPanel()->getId();
+
+ $query = parent::getEloquentQuery()
+ ->withoutGlobalScopes([
+ SoftDeletingScope::class,
+ ])
+ ->where(function ($query) {
+ $query->whereDoesntHave('requests', function (Builder $query) {
+ $query->whereRelation('action', 'status', '!=', ActionStatus::CLOSED);
+ });
+ });
+
+ return match ($panel) {
+ 'root' => $query,
+ default => $query->where('organization_id', Auth::user()->organization_id),
+ };
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers/Resources/ClosedDossierResource/Pages/ListClosedDossiers.php b/app/Filament/Clusters/Dossiers/Resources/ClosedDossierResource/Pages/ListClosedDossiers.php
new file mode 100644
index 0000000..1e60e0e
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers/Resources/ClosedDossierResource/Pages/ListClosedDossiers.php
@@ -0,0 +1,29 @@
+label('New dossier')
+ ->modalHeading('Create new dossier')
+ ->createAnother(false)
+ ->slideOver(),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers/Resources/ClosedDossierResource/Pages/ViewDossier.php b/app/Filament/Clusters/Dossiers/Resources/ClosedDossierResource/Pages/ViewDossier.php
new file mode 100644
index 0000000..84bbced
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers/Resources/ClosedDossierResource/Pages/ViewDossier.php
@@ -0,0 +1,55 @@
+record->name)->limit(36, '...', true);
+ }
+
+ public function getBreadcrumbs(): array
+ {
+ return array_merge(array_slice(parent::getBreadcrumbs(), 0, -1), [
+ str($this->record->name)->limit(36, '...', true),
+ ]);
+ }
+
+ public function getSubNavigation(): array
+ {
+ if (filled($cluster = static::getCluster())) {
+ return $this->generateNavigationItems($cluster::getClusteredComponents());
+ }
+
+ return [];
+ }
+
+ protected function getHeaderActions(): array
+ {
+ return [
+ Actions\RestoreAction::make()
+ ->label('Restore')
+ ->modalHeading('Restore dossier'),
+ NoteDossierAction::make()
+ ->icon(null),
+ Actions\EditAction::make()
+ ->label('Edit')
+ ->hidden($this->record->trashed())
+ ->slideOver(),
+ Actions\ActionGroup::make([
+ Actions\DeleteAction::make()
+ ->modalHeading('Delete dossier'),
+ Actions\ForceDeleteAction::make()
+ ->modalHeading('Force delete dossier'),
+ ]),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers/Resources/OpenDossierResource.php b/app/Filament/Clusters/Dossiers/Resources/OpenDossierResource.php
new file mode 100644
index 0000000..bd974ed
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers/Resources/OpenDossierResource.php
@@ -0,0 +1,83 @@
+ Pages\ListOpenDossiers::route('/'),
+ 'show' => Pages\ViewDossier::route('/{record}'),
+ ];
+ }
+
+ public static function getEloquentQuery(): Builder
+ {
+ $panel = Filament::getCurrentPanel()->getId();
+
+ $query = parent::getEloquentQuery()
+ ->withoutGlobalScopes([
+ SoftDeletingScope::class,
+ ])
+ ->where(function ($query) {
+ $query->whereHas('requests', function (Builder $query) {
+ $query->whereRelation('action', 'status', '!=', ActionStatus::CLOSED);
+ });
+ });
+
+ return match ($panel) {
+ 'root' => $query,
+ default => $query->where('organization_id', Auth::user()->organization_id),
+ };
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers/Resources/OpenDossierResource/Pages/ListOpenDossiers.php b/app/Filament/Clusters/Dossiers/Resources/OpenDossierResource/Pages/ListOpenDossiers.php
new file mode 100644
index 0000000..0720673
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers/Resources/OpenDossierResource/Pages/ListOpenDossiers.php
@@ -0,0 +1,29 @@
+label('New dossier')
+ ->modalHeading('Create new dossier')
+ ->createAnother(false)
+ ->slideOver(),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Dossiers/Resources/OpenDossierResource/Pages/ViewDossier.php b/app/Filament/Clusters/Dossiers/Resources/OpenDossierResource/Pages/ViewDossier.php
new file mode 100644
index 0000000..cc168c5
--- /dev/null
+++ b/app/Filament/Clusters/Dossiers/Resources/OpenDossierResource/Pages/ViewDossier.php
@@ -0,0 +1,55 @@
+record->name)->limit(36, '...', true);
+ }
+
+ public function getBreadcrumbs(): array
+ {
+ return array_merge(array_slice(parent::getBreadcrumbs(), 0, -1), [
+ str($this->record->name)->limit(36, '...', true),
+ ]);
+ }
+
+ public function getSubNavigation(): array
+ {
+ if (filled($cluster = static::getCluster())) {
+ return $this->generateNavigationItems($cluster::getClusteredComponents());
+ }
+
+ return [];
+ }
+
+ protected function getHeaderActions(): array
+ {
+ return [
+ Actions\RestoreAction::make()
+ ->label('Restore')
+ ->modalHeading('Restore dossier'),
+ NoteDossierAction::make()
+ ->icon(null),
+ Actions\EditAction::make()
+ ->label('Edit')
+ ->hidden($this->record->trashed())
+ ->slideOver(),
+ Actions\ActionGroup::make([
+ Actions\DeleteAction::make()
+ ->modalHeading('Delete dossier'),
+ Actions\ForceDeleteAction::make()
+ ->modalHeading('Force delete dossier'),
+ ]),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Feedbacks.php b/app/Filament/Clusters/Feedbacks.php
new file mode 100644
index 0000000..1378838
--- /dev/null
+++ b/app/Filament/Clusters/Feedbacks.php
@@ -0,0 +1,16 @@
+getId(), ['root', 'auditor']);
+ }
+}
diff --git a/app/Filament/Clusters/Feedbacks/Resources/FeedbacksResource.php b/app/Filament/Clusters/Feedbacks/Resources/FeedbacksResource.php
new file mode 100644
index 0000000..45e686c
--- /dev/null
+++ b/app/Filament/Clusters/Feedbacks/Resources/FeedbacksResource.php
@@ -0,0 +1,96 @@
+columns([
+ TextColumn::make('email')
+ ->label('Email')
+ ->searchable()
+ ->sortable(),
+ TextColumn::make('feedbacks.category_id')
+ ->label('Service Type')
+ ->searchable()
+ ->sortable()
+ ->getStateUsing(fn ($record) => $record->category?->name),
+ TextColumn::make('organization.code')
+ ->label('Organization')
+ ->searchable()
+ ->sortable(),
+ TextColumn::make('sqdAverage')
+ ->label('SQD Average')
+ ->formatStateUsing(fn ($state) => $state ? number_format($state, 2) : 'N/A')
+ ->sortable(),
+ TextColumn::make('created_at')
+ ->label('Date')
+ ->date()
+ ->searchable()
+ ->toggleable(isToggledHiddenByDefault:true)
+ ->sortable(),
+ ])
+ ->filters([
+
+ ])
+ ->recordUrl(fn ($record): string => static::getUrl('view', ['record' => $record]))
+ ->bulkActions([
+ GenerateFeedbackReport::make(),
+
+ // BulkAction::make('generated-pdf')
+ // ->label('Generate')
+ // ->icon('gmdi-picture-as-pdf')
+ // ->action(function($records){
+ // $pdf = Pdf::view('filament.panels.feedback.feedback-form', ['records' => $records,'preview' => false])
+ // ->margins(10, 10, 10, 10)
+ // ->paperSize(8.5, 13, Unit::Inch)
+ // ->withBrowsershot(function (Browsershot $browsershot) {
+ // return $browsershot
+ // ->noSandbox()
+ // ->emulateMedia('print')
+ // ->portrait()
+ // ->timeout(120)
+ // ->showBackground();
+ // })
+ // ->base64();
+
+ // return response()->streamDownload(
+ // function() use ($pdf) {
+ // echo base64_decode($pdf);
+ // },
+ // 'feedback_form_'.now()->format('Y_m_d_H_i_s').'.pdf',
+ // );
+ // })
+ ]);
+ }
+
+ public static function getPages(): array
+ {
+ return [
+ 'index' => Pages\ListFeedbacks::route('/'),
+ 'view' => Pages\FeedbackForm::route('/{record}'),
+ ];
+ }
+}
+
diff --git a/app/Filament/Clusters/Feedbacks/Resources/FeedbacksResource/Pages/FeedbackForm.php b/app/Filament/Clusters/Feedbacks/Resources/FeedbacksResource/Pages/FeedbackForm.php
new file mode 100644
index 0000000..aeedc60
--- /dev/null
+++ b/app/Filament/Clusters/Feedbacks/Resources/FeedbacksResource/Pages/FeedbackForm.php
@@ -0,0 +1,65 @@
+record->email . ' Feedback';
+ }
+
+ protected function getHeaderActions(): array
+ {
+ return [
+ Actions\Action::make('back')
+ ->label('Back')
+ ->url($this->getResource()::getUrl('index'))
+ ->color('gray')
+ ->icon('heroicon-o-arrow-left'),
+ Actions\Action::make('export')
+ ->label('Download PDF')
+ ->icon('gmdi-download')
+ ->action(function ($record) {
+
+ $filename = 'feedback_' . $record->id . now()->format('Y-m-d') . '.pdf';
+
+ $pdf = Pdf::view('filament.panels.feedback.feedback-form', ['record' => $record, 'preview' => true])
+ ->margins(10, 10, 10, 10)
+ ->paperSize(8.5, 13, Unit::Inch)
+ ->withBrowsershot(function (Browsershot $browsershot) {
+ return $browsershot
+ ->noSandbox()
+ ->emulateMedia('print')
+ ->portrait()
+ ->timeout(120)
+ ->showBackground();
+ })
+ ->base64();
+
+ return response()->streamDownload(function() use ($pdf) {
+ echo base64_decode($pdf);
+ }, $filename);
+ }),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Feedbacks/Resources/FeedbacksResource/Pages/ListFeedbacks.php b/app/Filament/Clusters/Feedbacks/Resources/FeedbacksResource/Pages/ListFeedbacks.php
new file mode 100644
index 0000000..0451f6d
--- /dev/null
+++ b/app/Filament/Clusters/Feedbacks/Resources/FeedbacksResource/Pages/ListFeedbacks.php
@@ -0,0 +1,14 @@
+getID(),['root']);
+ }
+
+ public static function table(Table $table): Table
+ {
+ return $table
+ ->columns([
+ Tables\Columns\TextColumn::make('code')
+ ->label('Code')
+ ->sortable()
+ ->searchable(),
+ Tables\Columns\TextColumn::make('name')
+ ->label('Name')
+ ->sortable()
+ ->searchable(),
+ Tables\Columns\TextColumn::make('barangays.name')
+ ->label('Barangays')
+ ->counts('barangays')
+ ->bulleted()
+ ->limitList(2)
+ ->expandableLimitedList()
+ ->listWithLineBreaks()
+ ->sortable(),
+ ]);
+ }
+
+ public static function getPages(): array
+ {
+ return [
+ 'index' => Pages\ListMunicipalities::route('/'),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Feedbacks/Resources/MunicipalityResource/Pages/ListMunicipalities.php b/app/Filament/Clusters/Feedbacks/Resources/MunicipalityResource/Pages/ListMunicipalities.php
new file mode 100644
index 0000000..eac8ff8
--- /dev/null
+++ b/app/Filament/Clusters/Feedbacks/Resources/MunicipalityResource/Pages/ListMunicipalities.php
@@ -0,0 +1,29 @@
+color('primary')
+ ->icon('gmdi-sync-s')
+ ->action(function () {
+ PSGCSync::dispatch();
+ }),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Management.php b/app/Filament/Clusters/Management.php
new file mode 100644
index 0000000..92fa28f
--- /dev/null
+++ b/app/Filament/Clusters/Management.php
@@ -0,0 +1,18 @@
+getId(), ['root', 'admin']);
+ }
+}
diff --git a/app/Filament/Clusters/Management/Pages/Settings.php b/app/Filament/Clusters/Management/Pages/Settings.php
new file mode 100644
index 0000000..7cbceb5
--- /dev/null
+++ b/app/Filament/Clusters/Management/Pages/Settings.php
@@ -0,0 +1,155 @@
+getId() !== 'root';
+ }
+
+ public function mount(): void
+ {
+ abort_unless(static::canAccess(), 403);
+
+ $this->fillForm();
+ }
+
+ public function getBreadcrumbs(): array
+ {
+ if (filled($cluster = static::getCluster())) {
+ return $cluster::unshiftClusterBreadcrumbs([
+ Settings::getUrl() => static::getTitle(),
+ ]);
+ }
+
+ return [];
+ }
+
+ public function form(Form $form): Form
+ {
+ return $form
+ ->schema([
+ Forms\Components\Tabs::make()
+ ->contained(false)
+ ->columns(3)
+ ->tabs([
+ Forms\Components\Tabs\Tab::make('Organization')
+ ->icon('gmdi-domain-o')
+ ->schema([
+ Forms\Components\FileUpload::make('logo')
+ ->avatar()
+ ->alignCenter()
+ ->directory('logos'),
+ Forms\Components\Group::make()
+ ->columnSpan([
+ 'md' => 2,
+ ])
+ ->schema([
+ Forms\Components\TextInput::make('name')
+ ->unique(ignoreRecord: true)
+ ->markAsRequired()
+ ->rule('required'),
+ Forms\Components\TextInput::make('code')
+ ->markAsRequired()
+ ->rule('required')
+ ->unique(ignoreRecord: true),
+ ]),
+ Forms\Components\TextInput::make('address')
+ ->maxLength(255)
+ ->columnSpan([
+ 'sm' => 1,
+ 'md' => 3,
+ ]),
+ Forms\Components\TextInput::make('building')
+ ->maxLength(255)
+ ->columnSpan([
+ 'sm' => 1,
+ 'md' => 2,
+ ]),
+ Forms\Components\TextInput::make('room')
+ ->columnSpan(1),
+ ]),
+ Forms\Components\Tabs\Tab::make('Configuration')
+ ->icon('gmdi-build-circle-o')
+ ->schema([
+ Forms\Components\TextInput::make('settings.auto_queue')
+ ->label('Request auto queue')
+ ->placeholder('Number of minutes')
+ ->helperText('Number of minutes to auto queue a request')
+ ->rules(['numeric']),
+ Forms\Components\TextInput::make('settings.auto_resolve')
+ ->label('Request auto resolve')
+ ->placeholder('Number of hours')
+ ->helperText('Number of hours to auto resolve a completed request')
+ ->minValue(48)
+ ->rules(['numeric']),
+ Forms\Components\TextInput::make('settings.auto_assign')
+ ->label('Request auto assign')
+ ->placeholder('Number of minutes')
+ ->helperText('Number of minutes to auto assign a request')
+ ->rules(['numeric']),
+ // Forms\Components\Toggle::make('settings.support_reassignment')
+ // ->inline(false)
+ // ->disabled(),
+ ]),
+ ]),
+ ])
+ ->statePath('data');
+ }
+
+ public function update(): void
+ {
+ $data = $this->form->getState();
+
+ $organization = Organization::find(Auth::user()->organization_id);
+
+ DB::transaction(function () use ($data, $organization) {
+ $organization->update($data);
+
+ Notification::make()
+ ->success()
+ ->title('Settings updated')
+ ->send();
+ });
+ }
+
+ protected function fillForm(): void
+ {
+ $this->form->fill(Organization::find(Auth::user()->organization_id)->toArray());
+ }
+
+ protected function getFormActions(): array
+ {
+ return [
+ Action::make('Update')
+ ->submit('update')
+ ->keyBindings(['mod+s']),
+ ];
+ }
+}
diff --git a/app/Filament/Clusters/Management/Resources/CategoryResource.php b/app/Filament/Clusters/Management/Resources/CategoryResource.php
new file mode 100644
index 0000000..700735a
--- /dev/null
+++ b/app/Filament/Clusters/Management/Resources/CategoryResource.php
@@ -0,0 +1,221 @@
+getId(), ['root', 'admin']);
+ }
+
+ public static function form(Form $form): Form
+ {
+ $panel = Filament::getCurrentPanel()->getId();
+
+ return $form
+ ->schema([
+ Forms\Components\Select::make('organization_id')
+ ->columnSpanFull()
+ ->relationship('organization', 'code')
+ ->searchable()
+ ->preload()
+ ->required()
+ ->default(fn () => $panel !== 'root' ? Auth::user()->organization_id : null)
+ ->visible(fn (string $operation) => $panel === 'root' && $operation === 'create')
+ ->dehydratedWhenHidden(),
+ Forms\Components\TextInput::make('name')
+ ->label('Name')
+ ->columnSpanFull()
+ ->dehydrateStateUsing(fn (?string $state) => mb_ucfirst($state ?? ''))
+ ->maxLength(48)
+ ->rule('required')
+ ->markAsRequired()
+ ->unique(
+ ignoreRecord: true,
+ modifyRuleUsing: fn ($rule, $get) => $rule->withoutTrashed()
+ ->where('organization_id', $get('organization'))
+ ),
+ Forms\Components\Select::make('standard_type')
+ ->label('Standard Type')
+ ->columnSpanFull()
+ ->options(Feedback::standardizationsLabel())
+ ->native(false),
+ Forms\Components\Select::make('service_type')
+ ->label('Service Type')
+ ->columnSpanFull()
+ ->allowHtml()
+ ->required()
+ ->options(function () {
+ return collect(Feedback::serviceTypesLabel())->mapWithKeys(
+ function ($label, $value){
+ $description = Feedback::serviceTypesDescription()[$value] ?? "No description available.";
+ return [$value => "
+ Deleting this category will affect all related records associated with it e.g. subcategories under this category. +
+ ++ Proceeding with this action will permanently delete the category and all related records associated with it. +
+ HTML; + + return str($description)->toHtmlString(); + }), + ]), + ]) + ->recordAction(null) + ->recordUrl(null); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListCategories::route('/'), + 'subcategories' => Pages\ListSubcategories::route('/{record}/subcategories'), + ]; + } + + public static function getEloquentQuery(): Builder + { + $query = parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + + return match (Filament::getCurrentPanel()->getId()) { + 'root' => $query, + 'admin' => $query->where('organization_id', Auth::user()->organization_id), + default => $query->whereRaw('1 = 0'), + }; + } + + public static function getNavigationBadge(): ?string + { + return static::getEloquentQuery() + ->withoutTrashed() + ->count(); + } +} diff --git a/app/Filament/Clusters/Management/Resources/CategoryResource/Pages/ListCategories.php b/app/Filament/Clusters/Management/Resources/CategoryResource/Pages/ListCategories.php new file mode 100644 index 0000000..1a7fc38 --- /dev/null +++ b/app/Filament/Clusters/Management/Resources/CategoryResource/Pages/ListCategories.php @@ -0,0 +1,44 @@ +createAnother(false) + ->slideOver() + ->modalWidth(MaxWidth::Large), + ViewQrCodeAction::make(), + ]; + } + + public function getTabs(): array + { + $query = fn () => static::$resource::getEloquentQuery(); + + return [ + 'all' => Tab::make('Active') + ->modifyQueryUsing(fn (Builder $query) => $query->withoutTrashed()) + ->icon('gmdi-verified-o') + ->badge(fn () => $query()->withoutTrashed()->count()), + 'trashed' => Tab::make('Trashed') + ->modifyQueryUsing(fn (Builder $query) => $query->onlyTrashed()) + ->icon('gmdi-delete-o') + ->badgeColor('danger') + ->badge(fn () => $query()->onlyTrashed()->count()), + ]; + } +} diff --git a/app/Filament/Clusters/Management/Resources/CategoryResource/Pages/ListSubcategories.php b/app/Filament/Clusters/Management/Resources/CategoryResource/Pages/ListSubcategories.php new file mode 100644 index 0000000..8f19445 --- /dev/null +++ b/app/Filament/Clusters/Management/Resources/CategoryResource/Pages/ListSubcategories.php @@ -0,0 +1,206 @@ + $this->record->subcategories(); + + return [ + 'all' => Tab::make('Active') + ->modifyQueryUsing(fn (Builder $query) => $query->withoutTrashed()) + ->icon('gmdi-verified-o') + ->badge(fn () => $query()->withoutTrashed()->count()), + 'trashed' => Tab::make('Trashed') + ->modifyQueryUsing(fn (Builder $query) => $query->onlyTrashed()) + ->icon('gmdi-delete-o') + ->badgeColor('danger') + ->badge(fn () => $query()->onlyTrashed()->count()), + ]; + } + + public function getBreadcrumbs(): array + { + return array_merge(array_slice(parent::getBreadcrumbs(), 0, -1), [ + $this->record->name, + 'Subcategories', + 'List', + ]); + } + + public function getSubNavigation(): array + { + if (filled($cluster = static::getCluster())) { + return $this->generateNavigationItems($cluster::getClusteredComponents()); + } + + return []; + } + + public function getHeaderActions(): array + { + return [ + Actions\Action::make('back') + ->color('gray') + ->icon('heroicon-o-arrow-left') + ->url(static::$resource::getUrl()), + Actions\CreateAction::make() + ->model(Subcategory::class) + ->createAnother(false) + ->slideOver() + ->modalWidth(MaxWidth::Large) + ->closeModalByClickingAway(false) + ->form(fn (Form $form) => [ + Forms\Components\Select::make('category_id') + ->columnSpanFull() + ->relationship('category', 'name') + ->searchable() + ->preload() + ->default($this->record->getKey()) + ->hidden() + ->dehydratedWhenHidden(), + ...$this->form($form)->getComponents(), + ]), + ]; + } + + public function getHeading(): string + { + return "{$this->record->name} → Subcategories"; + } + + public function form(Form $form): Form + { + return $form + ->columns(1) + ->schema([ + Forms\Components\TextInput::make('name') + ->dehydrateStateUsing(fn (?string $state) => mb_ucfirst($state ?? '')) + ->rule('required') + ->markAsRequired() + ->maxLength(48), + Forms\Components\Group::make() + ->relationship('inquiryTemplate') + ->mutateRelationshipDataBeforeCreateUsing(fn (array $data) => [...$data, 'class' => RequestClass::INQUIRY]) + ->schema([ + Forms\Components\MarkdownEditor::make('content') + ->label('Inquiry Template') + ->nullable(), + ]), + Forms\Components\Group::make() + ->relationship('suggestionTemplate') + ->mutateRelationshipDataBeforeCreateUsing(fn (array $data) => [...$data, 'class' => RequestClass::SUGGESTION]) + ->schema([ + Forms\Components\MarkdownEditor::make('content') + ->label('Suggestion Template') + ->nullable(), + ]), + Forms\Components\Group::make() + ->relationship('ticketTemplate') + ->mutateRelationshipDataBeforeCreateUsing(fn (array $data) => [...$data, 'class' => RequestClass::TICKET]) + ->schema([ + Forms\Components\MarkdownEditor::make('content') + ->label('Ticket Template') + ->nullable(), + ]), + ]); + } + + public function table(Table $table): Table + { + $panel = Filament::getCurrentPanel()->getId(); + + return $table + ->heading($panel === 'root' ? "{$this->record->organization->code} → {$this->record->name} → Subcategories" : null) + ->recordTitleAttribute('name') + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('requests_count') + ->label('Requests') + ->counts('requests'), + Tables\Columns\TextColumn::make('open_count') + ->label('Open') + ->counts('open'), + Tables\Columns\TextColumn::make('closed_count') + ->label('Closed') + ->counts('closed'), + Tables\Columns\TextColumn::make('deleted_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->actions([ + Tables\Actions\RestoreAction::make(), + TemplatesPreviewActionGroup::make(), + Tables\Actions\EditAction::make() + ->slideOver() + ->modalWidth(MaxWidth::Large) + ->closeModalByClickingAway(false), + Tables\Actions\ActionGroup::make([ + Tables\Actions\DeleteAction::make() + ->modalDescription('Deleting this subcategory will affect all related records associated with it.'), + Tables\Actions\ForceDeleteAction::make() + ->modalDescription(function () { + $description = <<<'HTML' ++ Deleting this subcategory will affect all related records associated with it. +
+ ++ Proceeding with this action will permanently delete the subcategory and all related records associated with it. +
+ HTML; + + return str($description)->toHtmlString(); + }), + ]), + ]) + ->modifyQueryUsing(fn (Builder $query) => $query->withoutGlobalScopes([ + SoftDeletingScope::class, + ])) + ->groups([ + Tables\Grouping\Group::make('category.name') + ->label('Organization') + ->getDescriptionFromRecordUsing(fn (Subcategory $subcategory) => "({$subcategory->category->organization->code}) {$subcategory->category->organization->name}") + ->titlePrefixedWithLabel(false), + ]) + ->groupingSettingsHidden() + ->recordAction(null) + ->recordUrl(null); + } +} diff --git a/app/Filament/Clusters/Management/Resources/OrganizationResource.php b/app/Filament/Clusters/Management/Resources/OrganizationResource.php new file mode 100644 index 0000000..692d2e5 --- /dev/null +++ b/app/Filament/Clusters/Management/Resources/OrganizationResource.php @@ -0,0 +1,154 @@ +getId() === 'root'; + } + + public static function form(Form $form): Form + { + return $form + ->columns(3) + ->schema([ + Forms\Components\FileUpload::make('logo') + ->avatar() + ->alignCenter() + ->directory('logos'), + Forms\Components\Group::make() + ->columnSpan([ + 'md' => 2, + ]) + ->schema([ + Forms\Components\TextInput::make('name') + ->autofocus() + ->unique(ignoreRecord: true) + ->markAsRequired() + ->rule('required'), + Forms\Components\TextInput::make('code') + ->unique(ignoreRecord: true) + ->markAsRequired() + ->rule('required'), + ]), + Forms\Components\TextInput::make('address') + ->maxLength(255) + ->columnSpan([ + 'sm' => 1, + 'md' => 3, + ]), + Forms\Components\TextInput::make('building') + ->maxLength(255) + ->columnSpan([ + 'sm' => 1, + 'md' => 2, + ]), + Forms\Components\TextInput::make('room') + ->columnSpan(1), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\ImageColumn::make('logo_url') + ->label('') + ->circular() + ->extraImgAttributes(['loading' => 'lazy']) + ->grow(0), + Tables\Columns\TextColumn::make('name') + ->searchable(isIndividual: true) + ->sortable(), + Tables\Columns\TextColumn::make('code') + ->searchable(isIndividual: true) + ->sortable(), + Tables\Columns\TextColumn::make('users_count') + ->label('Users') + ->counts('users') + ->sortable(), + Tables\Columns\TextColumn::make('deleted_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->actions([ + Tables\Actions\RestoreAction::make(), + Tables\Actions\EditAction::make(), + Tables\Actions\ActionGroup::make([ + Tables\Actions\DeleteAction::make() + ->modalDescription('Deleting this office will affect all related records associated with it e.g. categories and subcategories under this office.'), + Tables\Actions\ForceDeleteAction::make() + ->modalDescription(function () { + $description = <<<'HTML' ++ Deleting this office will affect all related records associated with it e.g. categories and subcategories under this office. +
+ ++ Proceeding will permanently delete the office and all related records associated with it. +
+ HTML; + + return str($description)->toHtmlString(); + }), + ]), + ]) + ->recordAction(null) + ->recordUrl(null); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListOrganizations::route('/'), + 'create' => Pages\CreateOrganization::route('/create'), + 'edit' => Pages\EditOrganization::route('/{record}/edit'), + ]; + } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } + + public static function getNavigationBadge(): ?string + { + return static::getEloquentQuery()->withoutTrashed()->count(); + } +} diff --git a/app/Filament/Clusters/Management/Resources/OrganizationResource/Pages/CreateOrganization.php b/app/Filament/Clusters/Management/Resources/OrganizationResource/Pages/CreateOrganization.php new file mode 100644 index 0000000..c82443e --- /dev/null +++ b/app/Filament/Clusters/Management/Resources/OrganizationResource/Pages/CreateOrganization.php @@ -0,0 +1,39 @@ +generateNavigationItems($cluster::getClusteredComponents()); + } + + return []; + } + + protected function getHeaderActions(): array + { + return [ + Action::make('back') + ->color('gray') + ->icon('heroicon-o-arrow-left') + ->url(static::$resource::getUrl()), + ]; + } + + protected function getCancelFormAction(): Action + { + return Action::make('cancel') + ->hidden(); + } +} diff --git a/app/Filament/Clusters/Management/Resources/OrganizationResource/Pages/EditOrganization.php b/app/Filament/Clusters/Management/Resources/OrganizationResource/Pages/EditOrganization.php new file mode 100644 index 0000000..82266f4 --- /dev/null +++ b/app/Filament/Clusters/Management/Resources/OrganizationResource/Pages/EditOrganization.php @@ -0,0 +1,49 @@ +color('gray') + ->icon('heroicon-o-arrow-left') + ->url(static::$resource::getUrl()), + Actions\DeleteAction::make(), + ]; + } + + public function getSubNavigation(): array + { + if (filled($cluster = static::getCluster())) { + return $this->generateNavigationItems($cluster::getClusteredComponents()); + } + + return []; + } + + public function hasCombinedRelationManagerTabsWithContent(): bool + { + return true; + } + + public function getContentTabLabel(): ?string + { + return 'Information'; + } + + protected function getCancelFormAction(): Action + { + return Action::make('cancel') + ->hidden(); + } +} diff --git a/app/Filament/Clusters/Management/Resources/OrganizationResource/Pages/ListOrganizations.php b/app/Filament/Clusters/Management/Resources/OrganizationResource/Pages/ListOrganizations.php new file mode 100644 index 0000000..d6be6c5 --- /dev/null +++ b/app/Filament/Clusters/Management/Resources/OrganizationResource/Pages/ListOrganizations.php @@ -0,0 +1,39 @@ + Organization::query(); + + return [ + 'all' => Tab::make('Active') + ->modifyQueryUsing(fn (Builder $query) => $query->withoutTrashed()) + ->icon('gmdi-verified-o') + ->badge(fn () => $query()->withoutTrashed()->count()), + 'trashed' => Tab::make('Trashed') + ->modifyQueryUsing(fn (Builder $query) => $query->onlyTrashed()) + ->icon('gmdi-delete-o') + ->badgeColor('danger') + ->badge(fn () => $query()->onlyTrashed()->count()), + ]; + } +} diff --git a/app/Filament/Clusters/Management/Resources/TagResource.php b/app/Filament/Clusters/Management/Resources/TagResource.php new file mode 100644 index 0000000..1888582 --- /dev/null +++ b/app/Filament/Clusters/Management/Resources/TagResource.php @@ -0,0 +1,154 @@ +getId(), ['root', 'admin']); + } + + public static function form(Form $form): Form + { + $panel = Filament::getCurrentPanel()->getId(); + + return $form + ->schema([ + Forms\Components\Select::make('organization_id') + ->columnSpanFull() + ->relationship('organization', 'code') + ->searchable() + ->preload() + ->required() + ->placeholder('Select organization') + ->visible(fn (string $operation) => $panel === 'root' && $operation === 'create'), + Forms\Components\TextInput::make('name') + ->maxLength(24) + ->columnSpanFull() + ->live(debounce: 250) + ->rules('required') + ->markAsRequired() + ->unique(ignoreRecord: true, modifyRuleUsing: fn ($get, $rule) => $rule->where('organization_id', $get('organization_id'))), + Forms\Components\Select::make('color') + ->columnSpanFull() + ->options(array_reverse(array_combine(array_keys(Color::all()), array_map('ucfirst', array_keys(Color::all()))))) + ->default('gray') + ->live(debounce: 250) + ->searchable() + ->required(), + Forms\Components\Placeholder::make('preview') + ->columnSpanFull() + ->extraAttributes(['class' => 'w-fit']) + ->content(fn ($get) => new HtmlString(Blade::render( + '