Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
afb9794
Add Support Page
PeteBishwhip Apr 28, 2025
14d709e
Update support view
PeteBishwhip Apr 28, 2025
db5a593
Login for support
PeteBishwhip Apr 28, 2025
50d33de
Basic Account Functionality
PeteBishwhip Apr 28, 2025
3526a8c
Initial Support Ticket work
PeteBishwhip Apr 28, 2025
3faf133
Support Ticket Listings
PeteBishwhip Apr 28, 2025
cd5feca
Single Support Ticket View
PeteBishwhip Apr 28, 2025
f750abd
More work on Support Ticket Single View
PeteBishwhip Apr 28, 2025
b0b098c
More show ticket work
PeteBishwhip Apr 28, 2025
533a827
Ticket statuses!
PeteBishwhip Apr 28, 2025
1085a28
Add back to tickets button
PeteBishwhip Apr 29, 2025
e009e7e
Better styling of buttons
PeteBishwhip Apr 29, 2025
1f6f22a
Add close ticket functionality
PeteBishwhip Apr 29, 2025
b4d256a
Add link
PeteBishwhip Apr 29, 2025
6bac8ac
Ratelimiting auth + small styling changes
PeteBishwhip Apr 29, 2025
f78aef4
Ratelimiter messaging
PeteBishwhip Apr 29, 2025
af54154
Styling for show view
PeteBishwhip Apr 29, 2025
647c542
WIP Account Work
PeteBishwhip May 7, 2025
aa92cfa
Merge remote-tracking branch 'origin/main' into feature/support-tickets
simonhamp Mar 19, 2026
1d12e1c
Add support ticket system with admin panel, notifications, and chat UI
simonhamp Mar 23, 2026
233faff
Merge remote-tracking branch 'origin/main' into feature/support-tickets
simonhamp Mar 23, 2026
0815e0a
Apply Pint formatting fixes
simonhamp Mar 23, 2026
2f8d4fc
Move support tickets to customer dashboard with Livewire components
simonhamp Mar 24, 2026
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
157 changes: 157 additions & 0 deletions app/Filament/Resources/SupportTicketResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

namespace App\Filament\Resources;

use App\Filament\Resources\SupportTicketResource\Pages;
use App\Models\SupportTicket;
use App\SupportTicket\Status;
use Filament\Actions;
use Filament\Infolists;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;

class SupportTicketResource extends Resource
{
protected static ?string $model = SupportTicket::class;

protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-ticket';

protected static ?string $navigationLabel = 'Support Tickets';

protected static ?string $pluralModelLabel = 'Support Tickets';

public static function canCreate(): bool
{
return false;
}

public static function infolist(Schema $schema): Schema
{
return $schema
->columns(2)
->schema([
Section::make('Ticket Details')
->schema([
Infolists\Components\TextEntry::make('mask')
->label('Ticket ID'),
Infolists\Components\TextEntry::make('status')
->badge()
->color(fn (Status $state): string => match ($state) {
Status::OPEN => 'warning',
Status::IN_PROGRESS => 'info',
Status::ON_HOLD => 'gray',
Status::RESPONDED => 'success',
Status::CLOSED => 'danger',
}),
Infolists\Components\TextEntry::make('product')
->label('Product'),
Infolists\Components\TextEntry::make('issue_type')
->label('Issue Type')
->placeholder('N/A'),
Infolists\Components\TextEntry::make('user.email')
->label('User')
->url(fn (SupportTicket $record): string => UserResource::getUrl('edit', ['record' => $record->user_id])),
Infolists\Components\TextEntry::make('created_at')
->label('Created')
->dateTime(),
Infolists\Components\TextEntry::make('updated_at')
->label('Updated')
->dateTime(),
])
->columns(2)
->collapsible()
->persistCollapsed()
->columnSpan(1),

Section::make('Context')
->schema([
Infolists\Components\TextEntry::make('subject')
->label('Subject')
->columnSpanFull(),
Infolists\Components\TextEntry::make('message')
->label('Message')
->markdown()
->columnSpanFull(),
])
->collapsible()
->persistCollapsed()
->columnSpan(1),
]);
}

public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('mask')
->label('ID')
->searchable()
->sortable(),

Tables\Columns\TextColumn::make('subject')
->searchable()
->sortable()
->limit(50),

Tables\Columns\TextColumn::make('user.email')
->label('User')
->searchable()
->sortable(),

Tables\Columns\TextColumn::make('product')
->sortable(),

Tables\Columns\TextColumn::make('status')
->badge()
->color(fn (Status $state): string => match ($state) {
Status::OPEN => 'warning',
Status::IN_PROGRESS => 'info',
Status::ON_HOLD => 'gray',
Status::RESPONDED => 'success',
Status::CLOSED => 'danger',
})
->sortable(),

Tables\Columns\TextColumn::make('created_at')
->label('Created')
->dateTime()
->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('status')
->options(collect(Status::cases())->mapWithKeys(fn (Status $s) => [$s->value => $s->name])),
Tables\Filters\SelectFilter::make('product')
->options([
'mobile' => 'Mobile',
'desktop' => 'Desktop',
'bifrost' => 'Bifrost',
'nativephp.com' => 'NativePHP.com',
]),
])
->actions([
Actions\ViewAction::make(),
])
->bulkActions([
Actions\BulkActionGroup::make([
Actions\DeleteBulkAction::make(),
]),
])
->defaultSort('created_at', 'desc');
}

public static function getRelations(): array
{
return [];
}

public static function getPages(): array
{
return [
'index' => Pages\ListSupportTickets::route('/'),
'view' => Pages\ViewSupportTicket::route('/{record}'),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace App\Filament\Resources\SupportTicketResource\Pages;

use App\Filament\Resources\SupportTicketResource;
use Filament\Resources\Pages\ListRecords;

class ListSupportTickets extends ListRecords
{
protected static string $resource = SupportTicketResource::class;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace App\Filament\Resources\SupportTicketResource\Pages;

use App\Filament\Resources\SupportTicketResource;
use App\Filament\Resources\SupportTicketResource\Widgets\TicketRepliesWidget;
use App\SupportTicket\Status;
use Filament\Actions;
use Filament\Resources\Pages\ViewRecord;

class ViewSupportTicket extends ViewRecord
{
protected static string $resource = SupportTicketResource::class;

protected function getHeaderActions(): array
{
return [
Actions\Action::make('updateStatus')
->label('Update Status')
->icon('heroicon-o-arrow-path')
->form([
\Filament\Forms\Components\Select::make('status')
->label('Status')
->options(collect(Status::cases())->mapWithKeys(fn (Status $s) => [$s->value => ucwords(str_replace('_', ' ', $s->value))]))
->required(),
])
->fillForm(fn () => ['status' => $this->record->status->value])
->action(function (array $data): void {
$this->record->update(['status' => $data['status']]);
$this->refreshFormData(['status']);
}),
];
}

protected function getFooterWidgets(): array
{
return [
TicketRepliesWidget::class,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace App\Filament\Resources\SupportTicketResource\RelationManagers;

use App\Models\SupportTicket\Reply;
use App\Notifications\SupportTicketReplied;
use Filament\Forms;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;

class RepliesRelationManager extends RelationManager
{
protected static string $relationship = 'replies';

protected static ?string $title = 'Replies';

protected static bool $shouldSkipAuthorization = true;

public function isReadOnly(): bool
{
return false;
}

public function form(Schema $schema): Schema
{
return $schema
->schema([
Forms\Components\Textarea::make('message')
->required()
->maxLength(5000)
->columnSpanFull(),
Forms\Components\Toggle::make('note')
->label('Internal note (not visible to user)')
->default(false),
]);
}

public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('user.name')
->label('Author')
->sortable(),

Tables\Columns\TextColumn::make('message')
->limit(80)
->tooltip(fn ($record) => $record->message),

Tables\Columns\IconColumn::make('note')
->label('Note')
->boolean(),

Tables\Columns\TextColumn::make('created_at')
->label('Date')
->dateTime()
->sortable(),
])
->defaultSort('created_at', 'desc')
->headerActions([
Tables\Actions\CreateAction::make()
->label('Add Reply')
->mutateFormDataUsing(function (array $data): array {
$data['user_id'] = auth()->id();

return $data;
})
->after(function (Reply $record): void {
if ($record->note) {
return;
}

$ticket = $this->getOwnerRecord();
$ticket->user->notify(new SupportTicketReplied($ticket, $record));
}),
])
->actions([
//
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Filament\Resources\SupportTicketResource\Widgets;

use App\Notifications\SupportTicketReplied;
use Filament\Widgets\Widget;
use Illuminate\Database\Eloquent\Model;

class TicketRepliesWidget extends Widget
{
protected string $view = 'filament.resources.support-ticket-resource.widgets.ticket-replies';

public ?Model $record = null;

public string $newMessage = '';

public bool $isNote = false;

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

protected function getListeners(): array
{
return [];
}

public function sendReply(): void
{
$this->validate([
'newMessage' => ['required', 'string', 'max:5000'],
]);

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

if (! $this->isNote) {
$this->record->user->notify(new SupportTicketReplied($this->record, $reply));
}

$this->newMessage = '';
$this->isNote = false;
}
}
Loading
Loading