From afb9794db498b1e3eaccfcf2fa8210ec2f397c5e Mon Sep 17 00:00:00 2001 From: Pete Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Mon, 28 Apr 2025 12:34:36 +0100 Subject: [PATCH 01/18] Add Support Page --- resources/views/support/index.blade.php | 3 +++ routes/web.php | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 resources/views/support/index.blade.php diff --git a/resources/views/support/index.blade.php b/resources/views/support/index.blade.php new file mode 100644 index 00000000..fc170a76 --- /dev/null +++ b/resources/views/support/index.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/routes/web.php b/routes/web.php index 0471a388..1f62a203 100644 --- a/routes/web.php +++ b/routes/web.php @@ -54,3 +54,9 @@ })->name('docs')->where('page', '.*'); Route::get('/order/{checkoutSessionId}', App\Livewire\OrderSuccess::class)->name('order.success'); + +Route::prefix('/support')->group(function () { + Route::get('/', function () { + return view('support.index'); + })->name('support.index'); +}); From 14d709e96e91fbf32f37eee775c5fa2ccdc397b6 Mon Sep 17 00:00:00 2001 From: Pete Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Mon, 28 Apr 2025 12:42:35 +0100 Subject: [PATCH 02/18] Update support view --- resources/views/support/index.blade.php | 76 ++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/resources/views/support/index.blade.php b/resources/views/support/index.blade.php index fc170a76..8a3120f1 100644 --- a/resources/views/support/index.blade.php +++ b/resources/views/support/index.blade.php @@ -1,3 +1,77 @@ - + + {{-- Support Grid Section --}} +
+ {{-- Header --}} +
+

Support

+

+ Get help with NativePHP through our various support channels. +

+
+ {{-- Support Grid --}} + + + {{-- Additional Support Information --}} +
+

Need more help?

+

+ Check out our documentation for comprehensive guides and tutorials to help you get the most out of NativePHP. +

+
+
From db5a593820606a3bb9e7c074c4ef2f8c1d501695 Mon Sep 17 00:00:00 2001 From: Pete Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:06:10 +0100 Subject: [PATCH 03/18] Login for support --- resources/views/support/auth/login.blade.php | 84 ++++++++++++++++++++ resources/views/support/index.blade.php | 2 +- routes/web.php | 18 +++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 resources/views/support/auth/login.blade.php diff --git a/resources/views/support/auth/login.blade.php b/resources/views/support/auth/login.blade.php new file mode 100644 index 00000000..ae81cccf --- /dev/null +++ b/resources/views/support/auth/login.blade.php @@ -0,0 +1,84 @@ + +
+
+ +
+
+

+ Sign in to your account +

+

+ Or + + create a new account + +

+
+
+ @csrf + +
+
+ + + @error('email') +

{{ $message }}

+ @enderror +
+
+ + + @error('password') +

{{ $message }}

+ @enderror +
+
+ +
+
+ + +
+ + +
+ +
+ +
+
+
+
+
diff --git a/resources/views/support/index.blade.php b/resources/views/support/index.blade.php index 8a3120f1..ec8d87ca 100644 --- a/resources/views/support/index.blade.php +++ b/resources/views/support/index.blade.php @@ -50,7 +50,7 @@ class="group flex w-full flex-col items-center rounded-xl bg-gray-100/80 p-8 tex {{-- Support Tickets Box --}} - name('support.index'); + + Route::prefix('/tickets')->group(function () { + Route::get('/', function () { + if (!Auth::check()) { + return redirect()->route('support.auth.login'); + } + + return view('support.tickets.index'); + })->name('support.tickets'); + + Route::get('/login', function () { + if (Auth::check()) { + return redirect()->route('support.tickets'); + } + return view('support.auth.login'); + })->name('support.auth.login'); + }); }); From 50d33deb95c4b12e8a47c51d5720289f0a05e03f Mon Sep 17 00:00:00 2001 From: Pete Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:14:46 +0100 Subject: [PATCH 04/18] Basic Account Functionality --- .../Controllers/Account/AuthController.php | 40 ++++++++++++++ app/Http/Requests/LoginRequest.php | 29 +++++++++++ app/Providers/AppServiceProvider.php | 17 +++++- app/Providers/RouteServiceProvider.php | 2 +- database/seeders/DatabaseSeeder.php | 12 ++--- .../{support => account}/auth/login.blade.php | 34 ++++++------ resources/views/account/index.blade.php | 52 +++++++++++++++++++ routes/web.php | 45 ++++++++++------ 8 files changed, 192 insertions(+), 39 deletions(-) create mode 100644 app/Http/Controllers/Account/AuthController.php create mode 100644 app/Http/Requests/LoginRequest.php rename resources/views/{support => account}/auth/login.blade.php (82%) create mode 100644 resources/views/account/index.blade.php diff --git a/app/Http/Controllers/Account/AuthController.php b/app/Http/Controllers/Account/AuthController.php new file mode 100644 index 00000000..5df75ee5 --- /dev/null +++ b/app/Http/Controllers/Account/AuthController.php @@ -0,0 +1,40 @@ +logout(); + session()->regenerateToken(); + + return redirect()->route('account.login'); + } + + public function processLogin(LoginRequest $request) + { + $credentials = $request->only('email', 'password'); + + if (auth()->attempt($credentials, $request->boolean('remember'))) { + session()->regenerate(); + + return redirect()->intended('/account'); + } + + return back() + ->withInput($request->only('email')) + ->withErrors([ + 'email' => 'The provided credentials do not match our records.', + ]); + } +} diff --git a/app/Http/Requests/LoginRequest.php b/app/Http/Requests/LoginRequest.php new file mode 100644 index 00000000..58c936e9 --- /dev/null +++ b/app/Http/Requests/LoginRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => 'required|email', + 'password' => 'required|string', + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f58138ee..3ab7fbb8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,9 @@ namespace App\Providers; use App\Support\GitHub; +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; @@ -21,10 +24,11 @@ public function register(): void */ public function boot(): void { - $this->registerSharedViewVariables(); + $this->registerSharedViewVariables() + ->registerRateLimiters(); } - private function registerSharedViewVariables(): void + private function registerSharedViewVariables(): static { View::share('electronGitHubVersion', app()->environment('production') ? GitHub::electron()->latestVersion() @@ -34,5 +38,14 @@ private function registerSharedViewVariables(): void View::share('bskyLink', 'https://bsky.app/profile/nativephp.bsky.social'); View::share('openCollectiveLink', 'https://opencollective.com/nativephp'); View::share('githubLink', 'https://github.com/NativePHP'); + + return $this; + } + + private function registerRateLimiters() + { + RateLimiter::for('login', function (Request $request) { + return Limit::perMinute(5)->by($request->input('email') . '|' . $request->ip()); + }); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 1cf5f15c..07ca748c 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider * * @var string */ - public const HOME = '/home'; + public const HOME = '/account'; /** * Define your route model bindings, pattern filters, and other route configuration. diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a9f4519f..d67c9ba5 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -4,6 +4,7 @@ // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { @@ -12,11 +13,10 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // \App\Models\User::factory(10)->create(); - - // \App\Models\User::factory()->create([ - // 'name' => 'Test User', - // 'email' => 'test@example.com', - // ]); + \App\Models\User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => Hash::make('password'), + ]); } } diff --git a/resources/views/support/auth/login.blade.php b/resources/views/account/auth/login.blade.php similarity index 82% rename from resources/views/support/auth/login.blade.php rename to resources/views/account/auth/login.blade.php index ae81cccf..aa8ea0fc 100644 --- a/resources/views/support/auth/login.blade.php +++ b/resources/views/account/auth/login.blade.php @@ -14,40 +14,44 @@

-
+ @csrf + @error('email') +
+
+ + + +

{{ $message }}

+
+
+ @enderror
- @error('email') -

{{ $message }}

- @enderror
- @error('password') -

{{ $message }}

- @enderror
-
- +
+
- @forelse(auth()->user()->supportTickets as $ticket) + @forelse($supportTickets as $ticket) @@ -65,6 +64,10 @@ @endforelse
@@ -37,7 +36,7 @@
#{{ $ticket->mask }} @@ -51,7 +50,7 @@ - + View
+ +
+ {{ $supportTickets->links() }} +
{{-- Additional Support Information --}}
diff --git a/resources/views/support/tickets/show.blade.php b/resources/views/support/tickets/show.blade.php new file mode 100644 index 00000000..b5235d5d --- /dev/null +++ b/resources/views/support/tickets/show.blade.php @@ -0,0 +1,47 @@ + + + {{ $supportTicket->subject }} + + + +

+ {{ $supportTicket->subject }} +

+
+ +
+
+
+

Ticket Details

+

+ Ticket ID: #{{ $supportTicket->mask }}
+ Status: {{ $supportTicket->status }}
+ Created At: {{ $supportTicket->created_at->format('d M Y, H:i') }}
+ Updated At: {{ $supportTicket->updated_at->format('d M Y, H:i') }} +

+
+
+ + {{-- Ticket Messages --}} +
+
+

Messages

+ @foreach([] as $message) +
+

{{ $message->user->name }}:

+

{{ $message->content }}

+

{{ $message->created_at->format('d M Y, H:i') }}

+
+ @endforeach +
+
+ + {{-- Additional Support Information --}} +
+

Need more help?

+

+ Check out our documentation for comprehensive guides and tutorials to help you get the most out of NativePHP. +

+
+
+
diff --git a/routes/web.php b/routes/web.php index 38326123..dbafe198 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ group(function () { - Route::get('/', function () { - return view('support.tickets.index'); - })->name('support.tickets'); + Route::get('/', [TicketController::class, 'index'])->name('support.tickets'); - Route::get('/{ticketMask}', function ($ticketMask) { - return view('support.tickets.show', ['ticket' => $ticketMask]); - })->name('support.tickets.show'); + Route::get('/{supportTicket}', [TicketController::class, 'show']) + ->name('support.tickets.show'); }); }); From f750abdaed06b974fb7eaea686f8f673fbe81d82 Mon Sep 17 00:00:00 2001 From: Pete Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:19:28 +0100 Subject: [PATCH 08/18] More work on Support Ticket Single View --- app/Models/SupportTicket.php | 11 +++++- app/Models/SupportTicket/Reply.php | 37 +++++++++++++++++++ .../factories/SupportTicket/ReplyFactory.php | 28 ++++++++++++++ ...2025_04_28_160102_create_replies_table.php | 28 ++++++++++++++ database/seeders/SupportTicketSeeder.php | 5 +++ .../views/support/tickets/show.blade.php | 16 +++++--- 6 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 app/Models/SupportTicket/Reply.php create mode 100644 database/factories/SupportTicket/ReplyFactory.php create mode 100644 database/migrations/2025_04_28_160102_create_replies_table.php diff --git a/app/Models/SupportTicket.php b/app/Models/SupportTicket.php index b0ad0663..91f08e63 100644 --- a/app/Models/SupportTicket.php +++ b/app/Models/SupportTicket.php @@ -2,9 +2,11 @@ namespace App\Models; +use App\Models\SupportTicket\Reply; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; class SupportTicket extends Model @@ -23,7 +25,8 @@ protected static function booted() { static::creating(function ($ticket) { if (is_null($ticket->mask)) { - + // @TODO Generate a unique mask for the ticket + $ticket->mask = uniqid('ticket_'); } }); } @@ -33,6 +36,12 @@ public function getRouteKeyName(): string return 'mask'; } + public function replies(): HasMany + { + return $this->hasMany(Reply::class) + ->orderBy('created_at', 'desc'); + } + public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/app/Models/SupportTicket/Reply.php b/app/Models/SupportTicket/Reply.php new file mode 100644 index 00000000..02268e94 --- /dev/null +++ b/app/Models/SupportTicket/Reply.php @@ -0,0 +1,37 @@ + 'array', + 'note' => 'boolean', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function supportTicket(): BelongsTo + { + return $this->belongsTo(SupportTicket::class); + } +} diff --git a/database/factories/SupportTicket/ReplyFactory.php b/database/factories/SupportTicket/ReplyFactory.php new file mode 100644 index 00000000..3873abcc --- /dev/null +++ b/database/factories/SupportTicket/ReplyFactory.php @@ -0,0 +1,28 @@ + $this->faker->paragraphs(2, true), + 'attachments' => null, + 'note' => false, + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + + 'support_ticket_id' => SupportTicket::factory(), + 'user_id' => User::factory(), + ]; + } +} diff --git a/database/migrations/2025_04_28_160102_create_replies_table.php b/database/migrations/2025_04_28_160102_create_replies_table.php new file mode 100644 index 00000000..f12e6186 --- /dev/null +++ b/database/migrations/2025_04_28_160102_create_replies_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignIdFor(SupportTicket::class); + $table->foreignIdFor(User::class ); + $table->text('message'); + $table->json('attachments')->nullable(); + $table->boolean('note'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('replies'); + } +}; diff --git a/database/seeders/SupportTicketSeeder.php b/database/seeders/SupportTicketSeeder.php index 73e8f9dd..34edb2c3 100644 --- a/database/seeders/SupportTicketSeeder.php +++ b/database/seeders/SupportTicketSeeder.php @@ -15,6 +15,11 @@ public function run(): void { SupportTicket::factory() ->count(10) + ->has( + SupportTicket\Reply::factory() + ->state(['user_id' => 1]) + ->count(5) + ) ->create([ 'user_id' => 1, 'status' => 'open', diff --git a/resources/views/support/tickets/show.blade.php b/resources/views/support/tickets/show.blade.php index b5235d5d..84b1ad10 100644 --- a/resources/views/support/tickets/show.blade.php +++ b/resources/views/support/tickets/show.blade.php @@ -26,11 +26,17 @@

Messages

- @foreach([] as $message) -
-

{{ $message->user->name }}:

-

{{ $message->content }}

-

{{ $message->created_at->format('d M Y, H:i') }}

+ @foreach($supportTicket->replies as $reply) +
+
+
+

{{ $reply->user->name }}

+

{{ $reply->message }}

+
+
+
+ {{ $reply->created_at->format('d M Y, H:i') }} +
@endforeach
From b0b098c00650c49050a5f5c998d7563bea142a72 Mon Sep 17 00:00:00 2001 From: Pete Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:39:07 +0100 Subject: [PATCH 09/18] More show ticket work --- app/Models/SupportTicket/Reply.php | 6 ++++ .../views/support/tickets/show.blade.php | 28 +++++++++++-------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/Models/SupportTicket/Reply.php b/app/Models/SupportTicket/Reply.php index 02268e94..c063cff7 100644 --- a/app/Models/SupportTicket/Reply.php +++ b/app/Models/SupportTicket/Reply.php @@ -4,6 +4,7 @@ use App\Models\SupportTicket; use App\Models\User; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -25,6 +26,11 @@ class Reply extends Model 'note' => 'boolean', ]; + public function isFromUser(): Attribute + { + return Attribute::get(fn () => $this->user_id === auth()->user()->id); + } + public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/resources/views/support/tickets/show.blade.php b/resources/views/support/tickets/show.blade.php index 84b1ad10..d01827fb 100644 --- a/resources/views/support/tickets/show.blade.php +++ b/resources/views/support/tickets/show.blade.php @@ -1,18 +1,24 @@ - - {{ $supportTicket->subject }} - - - -

- {{ $supportTicket->subject }} -

-
-
-

Ticket Details

+
+

#{{ $supportTicket->mask }} » {{ $supportTicket->subject }}

+
+ + +
+

Ticket ID: #{{ $supportTicket->mask }}
Status: {{ $supportTicket->status }}
From 533a827ce0af9553afc8944dfecd3917d159223d Mon Sep 17 00:00:00 2001 From: Pete Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:55:16 +0100 Subject: [PATCH 10/18] Ticket statuses! --- app/Models/SupportTicket.php | 6 + app/SupportTicket/Status.php | 17 ++ lang/en/account.php | 13 ++ lang/en/auth.php | 20 ++ lang/en/pagination.php | 19 ++ lang/en/passwords.php | 22 ++ lang/en/validation.php | 191 ++++++++++++++++++ .../views/support/tickets/index.blade.php | 14 +- .../views/support/tickets/show.blade.php | 2 +- 9 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 app/SupportTicket/Status.php create mode 100644 lang/en/account.php create mode 100644 lang/en/auth.php create mode 100644 lang/en/pagination.php create mode 100644 lang/en/passwords.php create mode 100644 lang/en/validation.php diff --git a/app/Models/SupportTicket.php b/app/Models/SupportTicket.php index 91f08e63..a2f1458e 100644 --- a/app/Models/SupportTicket.php +++ b/app/Models/SupportTicket.php @@ -3,6 +3,8 @@ namespace App\Models; use App\Models\SupportTicket\Reply; +use App\SupportTicket\Status; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -21,6 +23,10 @@ class SupportTicket extends Model 'status', ]; + protected $casts = [ + 'status' => Status::class, + ]; + protected static function booted() { static::creating(function ($ticket) { diff --git a/app/SupportTicket/Status.php b/app/SupportTicket/Status.php new file mode 100644 index 00000000..7d9297b1 --- /dev/null +++ b/app/SupportTicket/Status.php @@ -0,0 +1,17 @@ +value); + } +} diff --git a/lang/en/account.php b/lang/en/account.php new file mode 100644 index 00000000..889ffad0 --- /dev/null +++ b/lang/en/account.php @@ -0,0 +1,13 @@ + [ + 'status' => [ + 'open' => 'Open', + 'in_progress' => 'In Progress', + 'on_hold' => 'On Hold', + 'responded' => 'Responded', + 'closed' => 'Closed', + ], + ], +]; diff --git a/lang/en/auth.php b/lang/en/auth.php new file mode 100644 index 00000000..6598e2c0 --- /dev/null +++ b/lang/en/auth.php @@ -0,0 +1,20 @@ + 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + +]; diff --git a/lang/en/pagination.php b/lang/en/pagination.php new file mode 100644 index 00000000..d4814118 --- /dev/null +++ b/lang/en/pagination.php @@ -0,0 +1,19 @@ + '« Previous', + 'next' => 'Next »', + +]; diff --git a/lang/en/passwords.php b/lang/en/passwords.php new file mode 100644 index 00000000..f1223bd7 --- /dev/null +++ b/lang/en/passwords.php @@ -0,0 +1,22 @@ + 'Your password has been reset.', + 'sent' => 'We have emailed your password reset link.', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => "We can't find a user with that email address.", + +]; diff --git a/lang/en/validation.php b/lang/en/validation.php new file mode 100644 index 00000000..8dbe37f1 --- /dev/null +++ b/lang/en/validation.php @@ -0,0 +1,191 @@ + 'The :attribute field must be accepted.', + 'accepted_if' => 'The :attribute field must be accepted when :other is :value.', + 'active_url' => 'The :attribute field must be a valid URL.', + 'after' => 'The :attribute field must be a date after :date.', + 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.', + 'alpha' => 'The :attribute field must only contain letters.', + 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.', + 'alpha_num' => 'The :attribute field must only contain letters and numbers.', + 'array' => 'The :attribute field must be an array.', + 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', + 'before' => 'The :attribute field must be a date before :date.', + 'before_or_equal' => 'The :attribute field must be a date before or equal to :date.', + 'between' => [ + 'array' => 'The :attribute field must have between :min and :max items.', + 'file' => 'The :attribute field must be between :min and :max kilobytes.', + 'numeric' => 'The :attribute field must be between :min and :max.', + 'string' => 'The :attribute field must be between :min and :max characters.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'can' => 'The :attribute field contains an unauthorized value.', + 'confirmed' => 'The :attribute field confirmation does not match.', + 'current_password' => 'The password is incorrect.', + 'date' => 'The :attribute field must be a valid date.', + 'date_equals' => 'The :attribute field must be a date equal to :date.', + 'date_format' => 'The :attribute field must match the format :format.', + 'decimal' => 'The :attribute field must have :decimal decimal places.', + 'declined' => 'The :attribute field must be declined.', + 'declined_if' => 'The :attribute field must be declined when :other is :value.', + 'different' => 'The :attribute field and :other must be different.', + 'digits' => 'The :attribute field must be :digits digits.', + 'digits_between' => 'The :attribute field must be between :min and :max digits.', + 'dimensions' => 'The :attribute field has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.', + 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.', + 'email' => 'The :attribute field must be a valid email address.', + 'ends_with' => 'The :attribute field must end with one of the following: :values.', + 'enum' => 'The selected :attribute is invalid.', + 'exists' => 'The selected :attribute is invalid.', + 'extensions' => 'The :attribute field must have one of the following extensions: :values.', + 'file' => 'The :attribute field must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'array' => 'The :attribute field must have more than :value items.', + 'file' => 'The :attribute field must be greater than :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than :value.', + 'string' => 'The :attribute field must be greater than :value characters.', + ], + 'gte' => [ + 'array' => 'The :attribute field must have :value items or more.', + 'file' => 'The :attribute field must be greater than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than or equal to :value.', + 'string' => 'The :attribute field must be greater than or equal to :value characters.', + ], + 'hex_color' => 'The :attribute field must be a valid hexadecimal color.', + 'image' => 'The :attribute field must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field must exist in :other.', + 'integer' => 'The :attribute field must be an integer.', + 'ip' => 'The :attribute field must be a valid IP address.', + 'ipv4' => 'The :attribute field must be a valid IPv4 address.', + 'ipv6' => 'The :attribute field must be a valid IPv6 address.', + 'json' => 'The :attribute field must be a valid JSON string.', + 'lowercase' => 'The :attribute field must be lowercase.', + 'lt' => [ + 'array' => 'The :attribute field must have less than :value items.', + 'file' => 'The :attribute field must be less than :value kilobytes.', + 'numeric' => 'The :attribute field must be less than :value.', + 'string' => 'The :attribute field must be less than :value characters.', + ], + 'lte' => [ + 'array' => 'The :attribute field must not have more than :value items.', + 'file' => 'The :attribute field must be less than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be less than or equal to :value.', + 'string' => 'The :attribute field must be less than or equal to :value characters.', + ], + 'mac_address' => 'The :attribute field must be a valid MAC address.', + 'max' => [ + 'array' => 'The :attribute field must not have more than :max items.', + 'file' => 'The :attribute field must not be greater than :max kilobytes.', + 'numeric' => 'The :attribute field must not be greater than :max.', + 'string' => 'The :attribute field must not be greater than :max characters.', + ], + 'max_digits' => 'The :attribute field must not have more than :max digits.', + 'mimes' => 'The :attribute field must be a file of type: :values.', + 'mimetypes' => 'The :attribute field must be a file of type: :values.', + 'min' => [ + 'array' => 'The :attribute field must have at least :min items.', + 'file' => 'The :attribute field must be at least :min kilobytes.', + 'numeric' => 'The :attribute field must be at least :min.', + 'string' => 'The :attribute field must be at least :min characters.', + ], + 'min_digits' => 'The :attribute field must have at least :min digits.', + 'missing' => 'The :attribute field must be missing.', + 'missing_if' => 'The :attribute field must be missing when :other is :value.', + 'missing_unless' => 'The :attribute field must be missing unless :other is :value.', + 'missing_with' => 'The :attribute field must be missing when :values is present.', + 'missing_with_all' => 'The :attribute field must be missing when :values are present.', + 'multiple_of' => 'The :attribute field must be a multiple of :value.', + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute field format is invalid.', + 'numeric' => 'The :attribute field must be a number.', + 'password' => [ + 'letters' => 'The :attribute field must contain at least one letter.', + 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', + 'numbers' => 'The :attribute field must contain at least one number.', + 'symbols' => 'The :attribute field must contain at least one symbol.', + 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.', + ], + 'present' => 'The :attribute field must be present.', + 'present_if' => 'The :attribute field must be present when :other is :value.', + 'present_unless' => 'The :attribute field must be present unless :other is :value.', + 'present_with' => 'The :attribute field must be present when :values is present.', + 'present_with_all' => 'The :attribute field must be present when :values are present.', + 'prohibited' => 'The :attribute field is prohibited.', + 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', + 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', + 'prohibits' => 'The :attribute field prohibits :other from being present.', + 'regex' => 'The :attribute field format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_array_keys' => 'The :attribute field must contain entries for: :values.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_if_accepted' => 'The :attribute field is required when :other is accepted.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute field must match :other.', + 'size' => [ + 'array' => 'The :attribute field must contain :size items.', + 'file' => 'The :attribute field must be :size kilobytes.', + 'numeric' => 'The :attribute field must be :size.', + 'string' => 'The :attribute field must be :size characters.', + ], + 'starts_with' => 'The :attribute field must start with one of the following: :values.', + 'string' => 'The :attribute field must be a string.', + 'timezone' => 'The :attribute field must be a valid timezone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'uppercase' => 'The :attribute field must be uppercase.', + 'url' => 'The :attribute field must be a valid URL.', + 'ulid' => 'The :attribute field must be a valid ULID.', + 'uuid' => 'The :attribute field must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + +]; diff --git a/resources/views/support/tickets/index.blade.php b/resources/views/support/tickets/index.blade.php index 7a04f54b..6e2578b1 100644 --- a/resources/views/support/tickets/index.blade.php +++ b/resources/views/support/tickets/index.blade.php @@ -17,8 +17,8 @@ Submit a new request

-
- +
+
@@ -46,7 +46,7 @@ - {{ $ticket->status }} + {{ $ticket->status->translated() }} @@ -65,9 +65,11 @@
-
- {{ $supportTickets->links() }} -
+ @if ($supportTickets->hasPages()) +
+ {{ $supportTickets->links() }} +
+ @endif
{{-- Additional Support Information --}}
diff --git a/resources/views/support/tickets/show.blade.php b/resources/views/support/tickets/show.blade.php index d01827fb..69d3d0ab 100644 --- a/resources/views/support/tickets/show.blade.php +++ b/resources/views/support/tickets/show.blade.php @@ -21,7 +21,7 @@

Ticket ID: #{{ $supportTicket->mask }}
- Status: {{ $supportTicket->status }}
+ Status: {{ $supportTicket->status->translated() }}
Created At: {{ $supportTicket->created_at->format('d M Y, H:i') }}
Updated At: {{ $supportTicket->updated_at->format('d M Y, H:i') }}

From 1085a28949de77b522585f4f1b4b4e7671e7199c Mon Sep 17 00:00:00 2001 From: Pete Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:02:00 +0100 Subject: [PATCH 11/18] Add back to tickets button --- resources/views/support/tickets/show.blade.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/views/support/tickets/show.blade.php b/resources/views/support/tickets/show.blade.php index 69d3d0ab..c65a984a 100644 --- a/resources/views/support/tickets/show.blade.php +++ b/resources/views/support/tickets/show.blade.php @@ -5,6 +5,12 @@

#{{ $supportTicket->mask }} » {{ $supportTicket->subject }}

+ + + + + Back to Tickets + + +

#{{ $supportTicket->mask }} » {{ $supportTicket->subject }}

-
- - - - - Back to Tickets - - - -

Ticket ID: #{{ $supportTicket->mask }}
@@ -62,4 +63,29 @@

+ + {{-- Mobile Footer - Visible only on Mobile --}} +
+ + + + + Back + + + +
+ + {{-- Add padding at the bottom to prevent content from being hidden behind the mobile footer --}} +
From 1f6f22afaa897010790fb1ef3f34ead9e12b40cb Mon Sep 17 00:00:00 2001 From: Pete Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:13:30 +0100 Subject: [PATCH 13/18] Add close ticket functionality --- .../Account/Support/TicketController.php | 14 +++++++ app/Policies/SupportTicketPolicy.php | 5 +++ .../views/support/tickets/show.blade.php | 40 ++++++++++++------- routes/web.php | 3 ++ 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/Account/Support/TicketController.php b/app/Http/Controllers/Account/Support/TicketController.php index eb8f858f..3a911dbf 100644 --- a/app/Http/Controllers/Account/Support/TicketController.php +++ b/app/Http/Controllers/Account/Support/TicketController.php @@ -4,12 +4,26 @@ use App\Http\Controllers\Controller; use App\Models\SupportTicket; +use App\SupportTicket\Status; use Illuminate\Http\Request; class TicketController extends Controller { public static string $paginationLimit = '10'; + public function closeTicket(SupportTicket $supportTicket) + { + $this->authorize('closeTicket', $supportTicket); + + $supportTicket->update([ + 'status' => Status::CLOSED, + ]); + + return redirect() + ->route('support.tickets.show', $supportTicket) + ->with('success', __('account.support_ticket.close_ticket.success')); + } + public function index() { $supportTickets = SupportTicket::whereUserId(auth()->user()->id) diff --git a/app/Policies/SupportTicketPolicy.php b/app/Policies/SupportTicketPolicy.php index 5069c809..848c1e49 100644 --- a/app/Policies/SupportTicketPolicy.php +++ b/app/Policies/SupportTicketPolicy.php @@ -8,6 +8,11 @@ class SupportTicketPolicy { + public function closeTicket(User $user, SupportTicket $supportTicket): bool + { + return $supportTicket->user_id == $user->id; + } + /** * Determine whether the user can view any models. */ diff --git a/resources/views/support/tickets/show.blade.php b/resources/views/support/tickets/show.blade.php index ccb1c111..5fad8a5e 100644 --- a/resources/views/support/tickets/show.blade.php +++ b/resources/views/support/tickets/show.blade.php @@ -1,24 +1,29 @@ {{-- Desktop Buttons - Hidden on Mobile --}} -
diff --git a/resources/views/support/tickets/show.blade.php b/resources/views/support/tickets/show.blade.php index 0ad70e6b..f028f42f 100644 --- a/resources/views/support/tickets/show.blade.php +++ b/resources/views/support/tickets/show.blade.php @@ -1,4 +1,5 @@ +
{{-- Desktop Buttons - Hidden on Mobile --}} +
+
+ + {{-- Add Alpine.js x-cloak style to hide elements with x-cloak before Alpine initializes --}} + +