Skip to content

Commit dce84d7

Browse files
authored
Merge pull request #315 from NativePHP/ultra-improvements
Admin UX improvements: impersonate, mobile viewport, ticket UI
2 parents c9f63df + 3ec10cd commit dce84d7

Some content is hidden

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

48 files changed

+1338
-619
lines changed

app/Enums/PriceTier.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public function label(): string
1212
{
1313
return match ($this) {
1414
self::Regular => 'Regular',
15-
self::Subscriber => 'Pro/Max Subscriber',
15+
self::Subscriber => 'Subscriber',
1616
self::Eap => 'Early Access',
1717
};
1818
}
@@ -21,7 +21,7 @@ public function description(): string
2121
{
2222
return match ($this) {
2323
self::Regular => 'Standard pricing for all customers',
24-
self::Subscriber => 'Discounted pricing for Pro and Max license holders',
24+
self::Subscriber => 'Discounted pricing for subscribers',
2525
self::Eap => 'Special pricing for Early Access Program customers',
2626
};
2727
}

app/Filament/Resources/SupportTicketResource.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ public static function infolist(Schema $schema): Schema
5151
Infolists\Components\TextEntry::make('issue_type')
5252
->label('Issue Type')
5353
->placeholder('N/A'),
54-
Infolists\Components\TextEntry::make('user.email')
54+
Infolists\Components\TextEntry::make('user.name')
5555
->label('User')
56+
->formatStateUsing(fn (SupportTicket $record): string => ($record->user->name ?? '').' ('.$record->user->email.')')
5657
->url(fn (SupportTicket $record): string => UserResource::getUrl('edit', ['record' => $record->user_id])),
5758
Infolists\Components\TextEntry::make('created_at')
5859
->label('Created')
@@ -96,9 +97,10 @@ public static function table(Table $table): Table
9697
->sortable()
9798
->limit(50),
9899

99-
Tables\Columns\TextColumn::make('user.email')
100+
Tables\Columns\TextColumn::make('user.name')
100101
->label('User')
101-
->searchable()
102+
->formatStateUsing(fn (SupportTicket $record): string => ($record->user->name ?? '').' ('.$record->user->email.')')
103+
->searchable(query: fn ($query, string $search) => $query->whereHas('user', fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('email', 'like', "%{$search}%")))
102104
->sortable(),
103105

104106
Tables\Columns\TextColumn::make('product')

app/Filament/Resources/SupportTicketResource/Pages/ViewSupportTicket.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Filament\Actions;
99
use Filament\Forms\Components\Select;
1010
use Filament\Resources\Pages\ViewRecord;
11+
use STS\FilamentImpersonate\Actions\Impersonate;
1112

1213
class ViewSupportTicket extends ViewRecord
1314
{
@@ -16,6 +17,8 @@ class ViewSupportTicket extends ViewRecord
1617
protected function getHeaderActions(): array
1718
{
1819
return [
20+
Impersonate::make()->impersonateRecord(fn () => $this->getRecord()->user),
21+
1922
Actions\Action::make('updateStatus')
2023
->label('Update Status')
2124
->icon('heroicon-o-arrow-path')

app/Filament/Resources/SupportTicketResource/Widgets/TicketRepliesWidget.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Filament\Resources\SupportTicketResource\Widgets;
44

5+
use App\Models\SupportTicket\Reply;
56
use App\Notifications\SupportTicketReplied;
67
use Filament\Widgets\Widget;
78
use Illuminate\Database\Eloquent\Model;
@@ -42,4 +43,22 @@ public function sendReply(): void
4243
$this->newMessage = '';
4344
$this->isNote = false;
4445
}
46+
47+
public function togglePin(int $replyId): void
48+
{
49+
$reply = Reply::where('support_ticket_id', $this->record->id)
50+
->where('id', $replyId)
51+
->where('note', true)
52+
->firstOrFail();
53+
54+
if ($reply->pinned) {
55+
$reply->update(['pinned' => false]);
56+
} else {
57+
Reply::where('support_ticket_id', $this->record->id)
58+
->where('pinned', true)
59+
->update(['pinned' => false]);
60+
61+
$reply->update(['pinned' => true]);
62+
}
63+
}
4564
}

app/Filament/Resources/UserResource.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Filament\Schemas\Schema;
1313
use Filament\Tables;
1414
use Filament\Tables\Table;
15+
use STS\FilamentImpersonate\Actions\Impersonate;
1516

1617
class UserResource extends Resource
1718
{
@@ -89,6 +90,7 @@ public static function table(Table $table): Table
8990
//
9091
])
9192
->actions([
93+
Impersonate::make(),
9294
Actions\ActionGroup::make([
9395
Actions\EditAction::make(),
9496
Actions\Action::make('view_on_stripe')

app/Filament/Resources/UserResource/Pages/EditUser.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Filament\Notifications\Notification;
1414
use Filament\Resources\Pages\EditRecord;
1515
use Illuminate\Support\Facades\Password;
16+
use STS\FilamentImpersonate\Actions\Impersonate;
1617

1718
class EditUser extends EditRecord
1819
{
@@ -21,6 +22,8 @@ class EditUser extends EditRecord
2122
protected function getHeaderActions(): array
2223
{
2324
return [
25+
Impersonate::make()->record($this->getRecord()),
26+
2427
Actions\ActionGroup::make([
2528
Actions\Action::make('createStripeCustomer')
2629
->label('Create Stripe Customer')
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Enums\PluginStatus;
6+
use App\Enums\PluginType;
7+
use App\Models\Plugin;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Support\Facades\Auth;
10+
use Illuminate\View\View;
11+
12+
class UltraController extends Controller
13+
{
14+
public function index(): View|RedirectResponse
15+
{
16+
$user = Auth::user();
17+
18+
if (! $user->hasActiveUltraSubscription()) {
19+
return to_route('pricing');
20+
}
21+
22+
$subscription = $user->subscription();
23+
24+
$plugins = Plugin::query()
25+
->where('is_official', true)
26+
->where('is_active', true)
27+
->where('status', PluginStatus::Approved)
28+
->where('type', PluginType::Paid)
29+
->orderBy('name')
30+
->get();
31+
32+
return view('customer.ultra.index', [
33+
'subscription' => $subscription,
34+
'plugins' => $plugins,
35+
]);
36+
}
37+
}

app/Livewire/Customer/Support/Show.php

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\SupportTicket\Status;
88
use Illuminate\Contracts\View\View;
99
use Illuminate\Support\Facades\Notification;
10+
use Illuminate\Support\Facades\RateLimiter;
1011
use Livewire\Attributes\Layout;
1112
use Livewire\Attributes\Title;
1213
use Livewire\Component;
@@ -33,10 +34,22 @@ public function reply(): void
3334
{
3435
$this->authorize('reply', $this->supportTicket);
3536

37+
$key = 'support-reply:'.auth()->id();
38+
39+
if (RateLimiter::tooManyAttempts($key, 10)) {
40+
$seconds = RateLimiter::availableIn($key);
41+
42+
$this->addError('replyMessage', "You're sending messages too quickly. Please wait {$seconds} seconds.");
43+
44+
return;
45+
}
46+
3647
$this->validate([
3748
'replyMessage' => ['required', 'string', 'max:5000'],
3849
]);
3950

51+
RateLimiter::hit($key, 60);
52+
4053
$reply = $this->supportTicket->replies()->create([
4154
'user_id' => auth()->id(),
4255
'message' => $this->replyMessage,
@@ -48,8 +61,6 @@ public function reply(): void
4861

4962
$this->replyMessage = '';
5063
$this->supportTicket->load(['user', 'replies.user']);
51-
52-
session()->flash('success', 'Your reply has been sent.');
5364
}
5465

5566
public function closeTicket(): void
@@ -60,7 +71,30 @@ public function closeTicket(): void
6071
'status' => Status::CLOSED,
6172
]);
6273

63-
session()->flash('success', __('account.support_ticket.close_ticket.success'));
74+
$this->supportTicket->replies()->create([
75+
'user_id' => null,
76+
'message' => auth()->user()->name.' closed this ticket.',
77+
'note' => false,
78+
]);
79+
80+
$this->supportTicket->load(['user', 'replies.user']);
81+
}
82+
83+
public function reopenTicket(): void
84+
{
85+
$this->authorize('reopenTicket', $this->supportTicket);
86+
87+
$this->supportTicket->update([
88+
'status' => Status::OPEN,
89+
]);
90+
91+
$this->supportTicket->replies()->create([
92+
'user_id' => null,
93+
'message' => auth()->user()->name.' reopened this ticket.',
94+
'note' => false,
95+
]);
96+
97+
$this->supportTicket->load(['user', 'replies.user']);
6498
}
6599

66100
public function render(): View

app/Models/Plugin.php

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ public function routeParams(): array
6464

6565
protected static function booted(): void
6666
{
67+
static::saving(function (Plugin $plugin): void {
68+
if ($plugin->isDirty('name') && $plugin->name && ! $plugin->isDirty('is_official')) {
69+
$vendor = explode('/', $plugin->name)[0] ?? null;
70+
$plugin->is_official = $vendor === 'nativephp';
71+
}
72+
});
73+
6774
static::created(function (Plugin $plugin): void {
6875
$plugin->recordActivity(
6976
PluginActivityType::Submitted,
@@ -185,16 +192,6 @@ public function getBestPriceForUser(?User $user): ?PluginPrice
185192
->orderBy('amount', 'asc')
186193
->first();
187194

188-
// Ultra subscribers get official plugins for free
189-
if ($bestPrice && $user && $this->isOfficial() && $user->hasUltraAccess()) {
190-
$freePrice = $bestPrice->replicate();
191-
$freePrice->amount = 0;
192-
$freePrice->id = $bestPrice->id;
193-
$freePrice->exists = true;
194-
195-
return $freePrice;
196-
}
197-
198195
return $bestPrice;
199196
}
200197

app/Models/SupportTicket.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Database\Eloquent\Model;
99
use Illuminate\Database\Eloquent\Relations\BelongsTo;
1010
use Illuminate\Database\Eloquent\Relations\HasMany;
11+
use Illuminate\Database\Eloquent\Relations\HasOne;
1112
use Illuminate\Database\Eloquent\SoftDeletes;
1213

1314
class SupportTicket extends Model
@@ -54,6 +55,14 @@ public function replies(): HasMany
5455
->orderBy('created_at', 'desc');
5556
}
5657

58+
public function pinnedNote(): HasOne
59+
{
60+
return $this->hasOne(Reply::class)
61+
->where('note', true)
62+
->where('pinned', true)
63+
->latestOfMany();
64+
}
65+
5766
public function user(): BelongsTo
5867
{
5968
return $this->belongsTo(User::class);

0 commit comments

Comments
 (0)