Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/rules/laravel-boost.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.

- php - 8.5.0
- php - 8.4.19
- filament/filament (FILAMENT) - v5
- laravel/cashier (CASHIER) - v15
- laravel/framework (LARAVEL) - v12
Expand Down
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.

- php - 8.5.0
- php - 8.4.19
- filament/filament (FILAMENT) - v5
- laravel/cashier (CASHIER) - v15
- laravel/framework (LARAVEL) - v12
Expand Down
2 changes: 1 addition & 1 deletion .junie/guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.

- php - 8.5.0
- php - 8.4.19
- filament/filament (FILAMENT) - v5
- laravel/cashier (CASHIER) - v15
- laravel/framework (LARAVEL) - v12
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.

- php - 8.5.0
- php - 8.4.19
- filament/filament (FILAMENT) - v5
- laravel/cashier (CASHIER) - v15
- laravel/framework (LARAVEL) - v12
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.

- php - 8.5.0
- php - 8.4.19
- filament/filament (FILAMENT) - v5
- laravel/cashier (CASHIER) - v15
- laravel/framework (LARAVEL) - v12
Expand Down
19 changes: 19 additions & 0 deletions app/Filament/Resources/SupportTicketResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\HtmlString;

class SupportTicketResource extends Resource
{
Expand Down Expand Up @@ -77,6 +78,24 @@ public static function infolist(Schema $schema): Schema
Infolists\Components\TextEntry::make('message')
->label('Message')
->markdown(),
Infolists\Components\TextEntry::make('attachments')
->label('Attachments')
->formatStateUsing(function (SupportTicket $record): HtmlString {
$attachments = $record->attachments;

if (empty($attachments)) {
return new HtmlString('<span style="color: #9ca3af;">None</span>');
}

$links = collect($attachments)->map(function (array $attachment, int $index) use ($record): string {
$url = route('customer.support.tickets.attachment', [$record, $index]);

return '<a href="'.e($url).'" target="_blank" style="color: #2563eb; text-decoration: underline;">'.e($attachment['name']).'</a>';
});

return new HtmlString($links->implode('<br>'));
})
->html(),
])
->collapsible()
->persistCollapsed(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
use App\Notifications\SupportTicketReplied;
use Filament\Widgets\Widget;
use Illuminate\Database\Eloquent\Model;
use Livewire\WithFileUploads;

class TicketRepliesWidget extends Widget
{
use WithFileUploads;

protected string $view = 'filament.resources.support-ticket-resource.widgets.ticket-replies';

public ?Model $record = null;
Expand All @@ -17,6 +20,8 @@ class TicketRepliesWidget extends Widget

public bool $isNote = false;

public array $replyAttachments = [];

protected int|string|array $columnSpan = 'full';

protected function getListeners(): array
Expand All @@ -28,12 +33,32 @@ public function sendReply(): void
{
$this->validate([
'newMessage' => ['required', 'string', 'max:5000'],
'replyAttachments' => ['array', 'max:5'],
'replyAttachments.*' => ['file', 'max:10240'],
]);

$attachments = null;

if (! empty($this->replyAttachments)) {
$attachments = [];

foreach ($this->replyAttachments as $file) {
$path = $file->store("{$this->record->mask}/replies", 'support-tickets');

$attachments[] = [
'name' => $file->getClientOriginalName(),
'path' => $path,
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
];
}
}

$reply = $this->record->replies()->create([
'user_id' => auth()->id(),
'message' => $this->newMessage,
'note' => $this->isNote,
'attachments' => $attachments,
]);

if (! $this->isNote && $this->record->user_id !== auth()->id()) {
Expand All @@ -42,6 +67,14 @@ public function sendReply(): void

$this->newMessage = '';
$this->isNote = false;
$this->replyAttachments = [];
}

public function removeReplyAttachment(int $index): void
{
$attachments = $this->replyAttachments;
array_splice($attachments, $index, 1);
$this->replyAttachments = $attachments;
}

public function togglePin(int $replyId): void
Expand Down
46 changes: 46 additions & 0 deletions app/Http/Controllers/SupportTicketAttachmentController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Http\Controllers;

use App\Models\SupportTicket;
use App\Models\SupportTicket\Reply;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;

class SupportTicketAttachmentController extends Controller
{
public function downloadTicketAttachment(SupportTicket $supportTicket, int $index): RedirectResponse
{
$user = auth()->user();

abort_unless($user->id === $supportTicket->user_id || $user->isAdmin(), 403);

$attachments = $supportTicket->attachments ?? [];

abort_unless(isset($attachments[$index]), 404);

$attachment = $attachments[$index];

$url = Storage::disk('support-tickets')->temporaryUrl($attachment['path'], now()->addMinutes(5));

return redirect($url);
}

public function downloadReplyAttachment(SupportTicket $supportTicket, Reply $reply, int $index): RedirectResponse
{
$user = auth()->user();

abort_unless($user->id === $supportTicket->user_id || $user->isAdmin(), 403);
abort_unless($reply->support_ticket_id === $supportTicket->id, 404);

$attachments = $reply->attachments ?? [];

abort_unless(isset($attachments[$index]), 404);

$attachment = $attachments[$index];

$url = Storage::disk('support-tickets')->temporaryUrl($attachment['path'], now()->addMinutes(5));

return redirect($url);
}
}
44 changes: 42 additions & 2 deletions app/Livewire/Customer/Support/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Livewire\Customer\Support;

use App\Enums\PluginStatus;
use App\Models\Plugin;
use App\Models\SupportTicket;
use App\Notifications\SupportTicketSubmitted;
Expand All @@ -12,11 +13,14 @@
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\WithFileUploads;

#[Layout('components.layouts.dashboard')]
#[Title('Submit a Request')]
class Create extends Component
{
use WithFileUploads;

public function boot(): void
{
abort_unless(auth()->user()->hasUltraAccess(), 403);
Expand All @@ -43,6 +47,9 @@ public function boot(): void

public string $environment = '';

/** File uploads */
public array $uploads = [];

/** Step 3: Subject + Message */
public string $subject = '';

Expand All @@ -57,6 +64,7 @@ public function updatedSelectedProduct(): void
$this->whatHappened = '';
$this->reproductionSteps = '';
$this->environment = '';
$this->uploads = [];
$this->subject = '';
$this->message = '';
$this->resetValidation();
Expand Down Expand Up @@ -139,6 +147,23 @@ public function submit(): void
'metadata' => $metadata ?: null,
]);

if (! empty($this->uploads)) {
$attachments = [];

foreach ($this->uploads as $file) {
$path = $file->store($ticket->mask, 'support-tickets');

$attachments[] = [
'name' => $file->getClientOriginalName(),
'path' => $path,
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
];
}

$ticket->update(['attachments' => $attachments]);
}

Notification::route('mail', 'support@nativephp.com')
->notify(new SupportTicketSubmitted($ticket));

Expand Down Expand Up @@ -185,7 +210,7 @@ protected function validateStep2(): void
$rules['tryingToDo'] = ['required', 'string', 'max:5000'];
$rules['whatHappened'] = ['required', 'string', 'max:5000'];
$rules['reproductionSteps'] = ['required', 'string', 'max:5000'];
$rules['environment'] = ['required', 'string', 'max:1000'];
$rules['environment'] = ['required', 'string', 'max:5000'];

$messages['tryingToDo.required'] = 'Please describe what you were trying to do.';
$messages['whatHappened.required'] = 'Please describe what happened instead.';
Expand All @@ -199,15 +224,30 @@ protected function validateStep2(): void
$messages['issueType.required'] = 'Please select an issue type.';
}

$rules['uploads'] = ['array', 'max:5'];
$rules['uploads.*'] = ['file', 'max:10240'];

$messages['uploads.max'] = 'You may attach up to 5 files.';
$messages['uploads.*.max'] = 'Each file must not exceed 10MB.';

$this->validate($rules, $messages);
}

public function removeUpload(int $index): void
{
$uploads = $this->uploads;
array_splice($uploads, $index, 1);
$this->uploads = $uploads;
}

public function render()
{
$officialPlugins = collect();

if ($this->selectedProduct === 'mobile') {
$officialPlugins = Plugin::where('is_official', true)
$officialPlugins = Plugin::query()
->where('status', PluginStatus::Approved)
->where('is_official', true)
->orderBy('name')
->pluck('name', 'id');
}
Expand Down
33 changes: 33 additions & 0 deletions app/Livewire/Customer/Support/Show.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\WithFileUploads;

#[Layout('components.layouts.dashboard')]
#[Title('Support Ticket')]
class Show extends Component
{
use WithFileUploads;

public SupportTicket $supportTicket;

public string $replyMessage = '';

public array $replyAttachments = [];

public function mount(SupportTicket $supportTicket): void
{
abort_unless(auth()->user()->hasUltraAccess(), 403);
Expand All @@ -46,20 +51,41 @@ public function reply(): void

$this->validate([
'replyMessage' => ['required', 'string', 'max:5000'],
'replyAttachments' => ['array', 'max:5'],
'replyAttachments.*' => ['file', 'max:10240'],
]);

RateLimiter::hit($key, 60);

$attachments = null;

if (! empty($this->replyAttachments)) {
$attachments = [];

foreach ($this->replyAttachments as $file) {
$path = $file->store("{$this->supportTicket->mask}/replies", 'support-tickets');

$attachments[] = [
'name' => $file->getClientOriginalName(),
'path' => $path,
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
];
}
}

$reply = $this->supportTicket->replies()->create([
'user_id' => auth()->id(),
'message' => $this->replyMessage,
'note' => false,
'attachments' => $attachments,
]);

Notification::route('mail', 'support@nativephp.com')
->notify(new SupportTicketUserReplied($this->supportTicket, $reply));

$this->replyMessage = '';
$this->replyAttachments = [];
$this->supportTicket->load(['user', 'replies.user']);
}

Expand All @@ -80,6 +106,13 @@ public function closeTicket(): void
$this->supportTicket->load(['user', 'replies.user']);
}

public function removeReplyAttachment(int $index): void
{
$attachments = $this->replyAttachments;
array_splice($attachments, $index, 1);
$this->replyAttachments = $attachments;
}

public function reopenTicket(): void
{
$this->authorize('reopenTicket', $this->supportTicket);
Expand Down
2 changes: 2 additions & 0 deletions app/Models/SupportTicket.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ class SupportTicket extends Model
'product',
'issue_type',
'metadata',
'attachments',
];

protected function casts(): array
{
return [
'status' => Status::class,
'metadata' => 'array',
'attachments' => 'array',
];
}

Expand Down
Loading
Loading