Skip to content

Commit c9f63df

Browse files
authored
Merge pull request #242 from NativePHP/feature/support-tickets
2 parents 0dd5379 + 77b05ef commit c9f63df

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3678
-235
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
namespace App\Filament\Resources;
4+
5+
use App\Filament\Resources\SupportTicketResource\Pages;
6+
use App\Models\SupportTicket;
7+
use App\SupportTicket\Status;
8+
use Filament\Actions;
9+
use Filament\Infolists;
10+
use Filament\Resources\Resource;
11+
use Filament\Schemas\Components\Section;
12+
use Filament\Schemas\Schema;
13+
use Filament\Tables;
14+
use Filament\Tables\Table;
15+
16+
class SupportTicketResource extends Resource
17+
{
18+
protected static ?string $model = SupportTicket::class;
19+
20+
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-ticket';
21+
22+
protected static ?string $navigationLabel = 'Support Tickets';
23+
24+
protected static ?string $pluralModelLabel = 'Support Tickets';
25+
26+
public static function canCreate(): bool
27+
{
28+
return false;
29+
}
30+
31+
public static function infolist(Schema $schema): Schema
32+
{
33+
return $schema
34+
->columns(2)
35+
->schema([
36+
Section::make('Ticket Details')
37+
->schema([
38+
Infolists\Components\TextEntry::make('mask')
39+
->label('Ticket ID'),
40+
Infolists\Components\TextEntry::make('status')
41+
->badge()
42+
->color(fn (Status $state): string => match ($state) {
43+
Status::OPEN => 'warning',
44+
Status::IN_PROGRESS => 'info',
45+
Status::ON_HOLD => 'gray',
46+
Status::RESPONDED => 'success',
47+
Status::CLOSED => 'danger',
48+
}),
49+
Infolists\Components\TextEntry::make('product')
50+
->label('Product'),
51+
Infolists\Components\TextEntry::make('issue_type')
52+
->label('Issue Type')
53+
->placeholder('N/A'),
54+
Infolists\Components\TextEntry::make('user.email')
55+
->label('User')
56+
->url(fn (SupportTicket $record): string => UserResource::getUrl('edit', ['record' => $record->user_id])),
57+
Infolists\Components\TextEntry::make('created_at')
58+
->label('Created')
59+
->dateTime(),
60+
Infolists\Components\TextEntry::make('updated_at')
61+
->label('Updated')
62+
->dateTime(),
63+
])
64+
->columns(2)
65+
->collapsible()
66+
->persistCollapsed()
67+
->columnSpan(1),
68+
69+
Section::make('Context')
70+
->schema([
71+
Infolists\Components\TextEntry::make('subject')
72+
->label('Subject')
73+
->columnSpanFull(),
74+
Infolists\Components\TextEntry::make('message')
75+
->label('Message')
76+
->markdown()
77+
->columnSpanFull(),
78+
])
79+
->collapsible()
80+
->persistCollapsed()
81+
->columnSpan(1),
82+
]);
83+
}
84+
85+
public static function table(Table $table): Table
86+
{
87+
return $table
88+
->columns([
89+
Tables\Columns\TextColumn::make('mask')
90+
->label('ID')
91+
->searchable()
92+
->sortable(),
93+
94+
Tables\Columns\TextColumn::make('subject')
95+
->searchable()
96+
->sortable()
97+
->limit(50),
98+
99+
Tables\Columns\TextColumn::make('user.email')
100+
->label('User')
101+
->searchable()
102+
->sortable(),
103+
104+
Tables\Columns\TextColumn::make('product')
105+
->sortable(),
106+
107+
Tables\Columns\TextColumn::make('status')
108+
->badge()
109+
->color(fn (Status $state): string => match ($state) {
110+
Status::OPEN => 'warning',
111+
Status::IN_PROGRESS => 'info',
112+
Status::ON_HOLD => 'gray',
113+
Status::RESPONDED => 'success',
114+
Status::CLOSED => 'danger',
115+
})
116+
->sortable(),
117+
118+
Tables\Columns\TextColumn::make('created_at')
119+
->label('Created')
120+
->dateTime()
121+
->sortable(),
122+
])
123+
->filters([
124+
Tables\Filters\SelectFilter::make('status')
125+
->options(collect(Status::cases())->mapWithKeys(fn (Status $s) => [$s->value => $s->name])),
126+
Tables\Filters\SelectFilter::make('product')
127+
->options([
128+
'mobile' => 'Mobile',
129+
'desktop' => 'Desktop',
130+
'bifrost' => 'Bifrost',
131+
'nativephp.com' => 'NativePHP.com',
132+
]),
133+
])
134+
->actions([
135+
Actions\ViewAction::make(),
136+
])
137+
->bulkActions([
138+
Actions\BulkActionGroup::make([
139+
Actions\DeleteBulkAction::make(),
140+
]),
141+
])
142+
->defaultSort('created_at', 'desc');
143+
}
144+
145+
public static function getRelations(): array
146+
{
147+
return [];
148+
}
149+
150+
public static function getPages(): array
151+
{
152+
return [
153+
'index' => Pages\ListSupportTickets::route('/'),
154+
'view' => Pages\ViewSupportTicket::route('/{record}'),
155+
];
156+
}
157+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\SupportTicketResource\Pages;
4+
5+
use App\Filament\Resources\SupportTicketResource;
6+
use Filament\Resources\Pages\ListRecords;
7+
8+
class ListSupportTickets extends ListRecords
9+
{
10+
protected static string $resource = SupportTicketResource::class;
11+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\SupportTicketResource\Pages;
4+
5+
use App\Filament\Resources\SupportTicketResource;
6+
use App\Filament\Resources\SupportTicketResource\Widgets\TicketRepliesWidget;
7+
use App\SupportTicket\Status;
8+
use Filament\Actions;
9+
use Filament\Forms\Components\Select;
10+
use Filament\Resources\Pages\ViewRecord;
11+
12+
class ViewSupportTicket extends ViewRecord
13+
{
14+
protected static string $resource = SupportTicketResource::class;
15+
16+
protected function getHeaderActions(): array
17+
{
18+
return [
19+
Actions\Action::make('updateStatus')
20+
->label('Update Status')
21+
->icon('heroicon-o-arrow-path')
22+
->form([
23+
Select::make('status')
24+
->label('Status')
25+
->options(collect(Status::cases())->mapWithKeys(fn (Status $s) => [$s->value => ucwords(str_replace('_', ' ', $s->value))]))
26+
->required(),
27+
])
28+
->fillForm(fn () => ['status' => $this->record->status->value])
29+
->action(function (array $data): void {
30+
$this->record->update(['status' => $data['status']]);
31+
$this->refreshFormData(['status']);
32+
}),
33+
];
34+
}
35+
36+
protected function getFooterWidgets(): array
37+
{
38+
return [
39+
TicketRepliesWidget::class,
40+
];
41+
}
42+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\SupportTicketResource\RelationManagers;
4+
5+
use App\Models\SupportTicket\Reply;
6+
use App\Notifications\SupportTicketReplied;
7+
use Filament\Forms;
8+
use Filament\Resources\RelationManagers\RelationManager;
9+
use Filament\Schemas\Schema;
10+
use Filament\Tables;
11+
use Filament\Tables\Table;
12+
13+
class RepliesRelationManager extends RelationManager
14+
{
15+
protected static string $relationship = 'replies';
16+
17+
protected static ?string $title = 'Replies';
18+
19+
protected static bool $shouldSkipAuthorization = true;
20+
21+
public function isReadOnly(): bool
22+
{
23+
return false;
24+
}
25+
26+
public function form(Schema $schema): Schema
27+
{
28+
return $schema
29+
->schema([
30+
Forms\Components\Textarea::make('message')
31+
->required()
32+
->maxLength(5000)
33+
->columnSpanFull(),
34+
Forms\Components\Toggle::make('note')
35+
->label('Internal note (not visible to user)')
36+
->default(false),
37+
]);
38+
}
39+
40+
public function table(Table $table): Table
41+
{
42+
return $table
43+
->columns([
44+
Tables\Columns\TextColumn::make('user.name')
45+
->label('Author')
46+
->sortable(),
47+
48+
Tables\Columns\TextColumn::make('message')
49+
->limit(80)
50+
->tooltip(fn ($record) => $record->message),
51+
52+
Tables\Columns\IconColumn::make('note')
53+
->label('Note')
54+
->boolean(),
55+
56+
Tables\Columns\TextColumn::make('created_at')
57+
->label('Date')
58+
->dateTime()
59+
->sortable(),
60+
])
61+
->defaultSort('created_at', 'desc')
62+
->headerActions([
63+
Tables\Actions\CreateAction::make()
64+
->label('Add Reply')
65+
->mutateFormDataUsing(function (array $data): array {
66+
$data['user_id'] = auth()->id();
67+
68+
return $data;
69+
})
70+
->after(function (Reply $record): void {
71+
if ($record->note) {
72+
return;
73+
}
74+
75+
$ticket = $this->getOwnerRecord();
76+
$ticket->user->notify(new SupportTicketReplied($ticket, $record));
77+
}),
78+
])
79+
->actions([
80+
//
81+
]);
82+
}
83+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\SupportTicketResource\Widgets;
4+
5+
use App\Notifications\SupportTicketReplied;
6+
use Filament\Widgets\Widget;
7+
use Illuminate\Database\Eloquent\Model;
8+
9+
class TicketRepliesWidget extends Widget
10+
{
11+
protected string $view = 'filament.resources.support-ticket-resource.widgets.ticket-replies';
12+
13+
public ?Model $record = null;
14+
15+
public string $newMessage = '';
16+
17+
public bool $isNote = false;
18+
19+
protected int|string|array $columnSpan = 'full';
20+
21+
protected function getListeners(): array
22+
{
23+
return [];
24+
}
25+
26+
public function sendReply(): void
27+
{
28+
$this->validate([
29+
'newMessage' => ['required', 'string', 'max:5000'],
30+
]);
31+
32+
$reply = $this->record->replies()->create([
33+
'user_id' => auth()->id(),
34+
'message' => $this->newMessage,
35+
'note' => $this->isNote,
36+
]);
37+
38+
if (! $this->isNote) {
39+
$this->record->user->notify(new SupportTicketReplied($this->record, $reply));
40+
}
41+
42+
$this->newMessage = '';
43+
$this->isNote = false;
44+
}
45+
}

0 commit comments

Comments
 (0)