|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Usamamuneerchaudhary\Notifier\Filament\Pages; |
|
4
|
|
|
|
|
5
|
|
|
use Filament\Forms\Components\Select; |
|
6
|
|
|
use Filament\Forms\Components\TextInput; |
|
7
|
|
|
use Filament\Forms\Components\Toggle; |
|
8
|
|
|
use Filament\Forms\Concerns\InteractsWithForms; |
|
9
|
|
|
use Filament\Notifications\Notification; |
|
10
|
|
|
use Filament\Pages\Page; |
|
11
|
|
|
use Filament\Schemas\Components\Section; |
|
12
|
|
|
use Filament\Schemas\Components\Tabs; |
|
13
|
|
|
use Filament\Schemas\Components\Tabs\Tab; |
|
14
|
|
|
use Filament\Schemas\Components\Utilities\Get; |
|
15
|
|
|
use Filament\Schemas\Schema; |
|
16
|
|
|
use Usamamuneerchaudhary\Notifier\Filament\Widgets\NotificationStatsOverview; |
|
17
|
|
|
use Usamamuneerchaudhary\Notifier\Models\NotificationSetting; |
|
18
|
|
|
use Usamamuneerchaudhary\Notifier\Models\NotificationChannel; |
|
19
|
|
|
|
|
20
|
|
|
class NotificationSettings extends Page |
|
21
|
|
|
{ |
|
22
|
|
|
use InteractsWithForms; |
|
|
|
|
|
|
23
|
|
|
|
|
24
|
|
|
protected static string|null|\BackedEnum $navigationIcon = 'heroicon-o-cog-6-tooth'; |
|
|
|
|
|
|
25
|
|
|
protected static string|null|\UnitEnum $navigationGroup = 'Notifier'; |
|
|
|
|
|
|
26
|
|
|
protected static ?string $title = 'Notification Settings'; |
|
27
|
|
|
protected static ?string $navigationLabel = 'Settings'; |
|
28
|
|
|
protected static ?int $navigationSort = 1; |
|
29
|
|
|
protected string $view = 'notifier::pages.settings'; |
|
30
|
|
|
|
|
31
|
|
|
public array $data; |
|
32
|
|
|
|
|
33
|
|
|
public function mount(): void |
|
34
|
|
|
{ |
|
35
|
|
|
$channels = NotificationChannel::all(); |
|
36
|
|
|
$channelsData = []; |
|
37
|
|
|
|
|
38
|
|
|
foreach ($channels as $channel) { |
|
39
|
|
|
$settings = $channel->settings ?? []; |
|
40
|
|
|
$channelsData[$channel->type] = array_merge([ |
|
41
|
|
|
'enabled' => $channel->is_active, |
|
42
|
|
|
], $settings); |
|
43
|
|
|
} |
|
44
|
|
|
|
|
45
|
|
|
// Load preferences settings |
|
46
|
|
|
$preferences = NotificationSetting::get('preferences', config('notifier.settings.preferences', [])); |
|
47
|
|
|
// Load analytics settings |
|
48
|
|
|
$analytics = NotificationSetting::get('analytics', config('notifier.settings.analytics', [])); |
|
49
|
|
|
// Load rate limiting settings |
|
50
|
|
|
$rateLimiting = NotificationSetting::get('rate_limiting', config('notifier.settings.rate_limiting', [])); |
|
51
|
|
|
|
|
52
|
|
|
$this->form->fill([ |
|
|
|
|
|
|
53
|
|
|
'enabled' => NotificationSetting::get('enabled', true), |
|
54
|
|
|
'queue_name' => NotificationSetting::get('queue_name', 'default'), |
|
55
|
|
|
'default_channel' => NotificationSetting::get('default_channel', 'email'), |
|
56
|
|
|
'channels' => $channelsData, |
|
57
|
|
|
'preferences' => [ |
|
58
|
|
|
'enabled' => $preferences['enabled'] ?? true, |
|
59
|
|
|
'default_channels' => $preferences['default_channels'] ?? ['email'], |
|
60
|
|
|
'allow_override' => $preferences['allow_override'] ?? true, |
|
61
|
|
|
], |
|
62
|
|
|
'analytics' => [ |
|
63
|
|
|
'enabled' => $analytics['enabled'] ?? true, |
|
64
|
|
|
'track_opens' => $analytics['track_opens'] ?? true, |
|
65
|
|
|
'track_clicks' => $analytics['track_clicks'] ?? true, |
|
66
|
|
|
'retention_days' => $analytics['retention_days'] ?? 90, |
|
67
|
|
|
], |
|
68
|
|
|
'rate_limiting' => [ |
|
69
|
|
|
'enabled' => $rateLimiting['enabled'] ?? true, |
|
70
|
|
|
'max_per_minute' => $rateLimiting['max_per_minute'] ?? 60, |
|
71
|
|
|
'max_per_hour' => $rateLimiting['max_per_hour'] ?? 1000, |
|
72
|
|
|
'max_per_day' => $rateLimiting['max_per_day'] ?? 10000, |
|
73
|
|
|
], |
|
74
|
|
|
]); |
|
75
|
|
|
} |
|
76
|
|
|
|
|
77
|
|
|
public function form(Schema $schema): Schema |
|
78
|
|
|
{ |
|
79
|
|
|
$channels = NotificationChannel::all(); |
|
80
|
|
|
$tabs = [ |
|
81
|
|
|
Tab::make('General') |
|
82
|
|
|
->icon('heroicon-o-cog') |
|
83
|
|
|
->schema([ |
|
84
|
|
|
Toggle::make('enabled') |
|
85
|
|
|
->label('Enable Notifications') |
|
86
|
|
|
->default(true), |
|
87
|
|
|
TextInput::make('queue_name') |
|
88
|
|
|
->label('Queue Name') |
|
89
|
|
|
->default('default') |
|
90
|
|
|
->required(), |
|
91
|
|
|
Select::make('default_channel') |
|
92
|
|
|
->label('Default Channel') |
|
93
|
|
|
->options(NotificationChannel::pluck('title', 'type')->toArray()) |
|
94
|
|
|
->required(), |
|
95
|
|
|
]), |
|
96
|
|
|
Tab::make('Preferences') |
|
97
|
|
|
->icon('heroicon-o-user-circle') |
|
98
|
|
|
->schema([ |
|
99
|
|
|
Section::make('User Preferences') |
|
100
|
|
|
->description('Configure default user notification preferences') |
|
101
|
|
|
->schema([ |
|
102
|
|
|
Toggle::make('preferences.enabled') |
|
103
|
|
|
->label('Enable User Preferences') |
|
104
|
|
|
->default(true), |
|
105
|
|
|
Select::make('preferences.default_channels') |
|
106
|
|
|
->label('Default Channels') |
|
107
|
|
|
->multiple() |
|
108
|
|
|
->options(NotificationChannel::pluck('title', 'type')->toArray()) |
|
109
|
|
|
->default(['email']) |
|
110
|
|
|
->required(), |
|
111
|
|
|
Toggle::make('preferences.allow_override') |
|
112
|
|
|
->label('Allow Users to Override Preferences') |
|
113
|
|
|
->default(true) |
|
114
|
|
|
->helperText('If enabled, users can customize their notification preferences'), |
|
115
|
|
|
]), |
|
116
|
|
|
]), |
|
117
|
|
|
Tab::make('Analytics') |
|
118
|
|
|
->icon('heroicon-o-chart-bar') |
|
119
|
|
|
->schema([ |
|
120
|
|
|
Section::make('Analytics Settings') |
|
121
|
|
|
->description('Configure notification analytics and tracking') |
|
122
|
|
|
->schema([ |
|
123
|
|
|
Toggle::make('analytics.enabled') |
|
124
|
|
|
->label('Enable Analytics') |
|
125
|
|
|
->default(true), |
|
126
|
|
|
Toggle::make('analytics.track_opens') |
|
127
|
|
|
->label('Track Email Opens') |
|
128
|
|
|
->default(true) |
|
129
|
|
|
->visible(fn(Get $get): bool => $get('analytics.enabled')), |
|
130
|
|
|
Toggle::make('analytics.track_clicks') |
|
131
|
|
|
->label('Track Link Clicks') |
|
132
|
|
|
->default(true) |
|
133
|
|
|
->visible(fn(Get $get): bool => $get('analytics.enabled')), |
|
134
|
|
|
TextInput::make('analytics.retention_days') |
|
135
|
|
|
->label('Data Retention (Days)') |
|
136
|
|
|
->numeric() |
|
137
|
|
|
->default(90) |
|
138
|
|
|
->required() |
|
139
|
|
|
->visible(fn(Get $get): bool => $get('analytics.enabled')), |
|
140
|
|
|
]), |
|
141
|
|
|
]), |
|
142
|
|
|
Tab::make('Rate Limiting') |
|
143
|
|
|
->icon('heroicon-o-clock') |
|
144
|
|
|
->schema([ |
|
145
|
|
|
Section::make('Rate Limiting Settings') |
|
146
|
|
|
->description('Configure rate limits for notifications to prevent abuse') |
|
147
|
|
|
->schema([ |
|
148
|
|
|
Toggle::make('rate_limiting.enabled') |
|
149
|
|
|
->label('Enable Rate Limiting') |
|
150
|
|
|
->default(true), |
|
151
|
|
|
TextInput::make('rate_limiting.max_per_minute') |
|
152
|
|
|
->label('Max Per Minute') |
|
153
|
|
|
->numeric() |
|
154
|
|
|
->default(60) |
|
155
|
|
|
->required() |
|
156
|
|
|
->visible(fn(Get $get): bool => $get('rate_limiting.enabled')), |
|
157
|
|
|
TextInput::make('rate_limiting.max_per_hour') |
|
158
|
|
|
->label('Max Per Hour') |
|
159
|
|
|
->numeric() |
|
160
|
|
|
->default(1000) |
|
161
|
|
|
->required() |
|
162
|
|
|
->visible(fn(Get $get): bool => $get('rate_limiting.enabled')), |
|
163
|
|
|
TextInput::make('rate_limiting.max_per_day') |
|
164
|
|
|
->label('Max Per Day') |
|
165
|
|
|
->numeric() |
|
166
|
|
|
->default(10000) |
|
167
|
|
|
->required() |
|
168
|
|
|
->visible(fn(Get $get): bool => $get('rate_limiting.enabled')), |
|
169
|
|
|
]), |
|
170
|
|
|
]), |
|
171
|
|
|
]; |
|
172
|
|
|
|
|
173
|
|
|
// Dynamically create tabs for each channel |
|
174
|
|
|
foreach ($channels as $channel) { |
|
175
|
|
|
$tabs[] = Tab::make($channel->title) |
|
176
|
|
|
->icon($channel->icon ?? 'heroicon-o-bell') |
|
177
|
|
|
->schema(self::getChannelSchema($channel)); |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
return $schema |
|
181
|
|
|
->schema([ |
|
182
|
|
|
Tabs::make('Settings') |
|
183
|
|
|
->tabs($tabs) |
|
184
|
|
|
->columnSpanFull() |
|
185
|
|
|
]) |
|
186
|
|
|
->statePath('data'); |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
protected static function getChannelSchema(NotificationChannel $channel): array |
|
190
|
|
|
{ |
|
191
|
|
|
$schema = [ |
|
192
|
|
|
Toggle::make("channels.{$channel->type}.enabled") |
|
193
|
|
|
->label("Enable {$channel->title}"), |
|
194
|
|
|
]; |
|
195
|
|
|
|
|
196
|
|
|
$fields = match ($channel->type) { |
|
197
|
|
|
'email' => [ |
|
198
|
|
|
TextInput::make("channels.{$channel->type}.from_address") |
|
199
|
|
|
->label('From Address') |
|
200
|
|
|
->email() |
|
201
|
|
|
->required() |
|
202
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
203
|
|
|
TextInput::make("channels.{$channel->type}.from_name") |
|
204
|
|
|
->label('From Name') |
|
205
|
|
|
->required() |
|
206
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
207
|
|
|
], |
|
208
|
|
|
'slack' => [ |
|
209
|
|
|
TextInput::make("channels.{$channel->type}.webhook_url") |
|
210
|
|
|
->label('Webhook URL') |
|
211
|
|
|
->url() |
|
212
|
|
|
->required() |
|
213
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
214
|
|
|
TextInput::make("channels.{$channel->type}.channel") |
|
215
|
|
|
->label('Slack Channel') |
|
216
|
|
|
->default('#notifications') |
|
217
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
218
|
|
|
TextInput::make("channels.{$channel->type}.username") |
|
219
|
|
|
->label('Bot Username') |
|
220
|
|
|
->default('Notification Bot') |
|
221
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
222
|
|
|
], |
|
223
|
|
|
'sms' => [ |
|
224
|
|
|
Select::make("channels.{$channel->type}.provider") |
|
225
|
|
|
->label('SMS Provider') |
|
226
|
|
|
->options([ |
|
227
|
|
|
'twilio' => 'Twilio', |
|
228
|
|
|
'vonage' => 'Vonage (Nexmo)', |
|
229
|
|
|
'generic' => 'Generic API', |
|
230
|
|
|
]) |
|
231
|
|
|
->required() |
|
232
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
233
|
|
|
TextInput::make("channels.{$channel->type}.twilio_account_sid") |
|
234
|
|
|
->label('Twilio Account SID') |
|
235
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && $get("channels.{$channel->type}.provider") === 'twilio'), |
|
236
|
|
|
TextInput::make("channels.{$channel->type}.twilio_auth_token") |
|
237
|
|
|
->label('Twilio Auth Token') |
|
238
|
|
|
->password() |
|
239
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && $get("channels.{$channel->type}.provider") === 'twilio'), |
|
240
|
|
|
TextInput::make("channels.{$channel->type}.twilio_phone_number") |
|
241
|
|
|
->label('Twilio Phone Number') |
|
242
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && $get("channels.{$channel->type}.provider") === 'twilio'), |
|
243
|
|
|
TextInput::make("channels.{$channel->type}.api_url") |
|
244
|
|
|
->label('API URL') |
|
245
|
|
|
->url() |
|
246
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && $get("channels.{$channel->type}.provider") === 'generic'), |
|
247
|
|
|
TextInput::make("channels.{$channel->type}.api_key") |
|
248
|
|
|
->label('API Key') |
|
249
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && ($get("channels.{$channel->type}.provider") === 'generic' || $get("channels.{$channel->type}.provider") === 'vonage')), |
|
250
|
|
|
TextInput::make("channels.{$channel->type}.api_secret") |
|
251
|
|
|
->label('API Secret') |
|
252
|
|
|
->password() |
|
253
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && ($get("channels.{$channel->type}.provider") === 'generic' || $get("channels.{$channel->type}.provider") === 'vonage')), |
|
254
|
|
|
], |
|
255
|
|
|
'push' => [ |
|
256
|
|
|
TextInput::make("channels.{$channel->type}.firebase_server_key") |
|
257
|
|
|
->label('Firebase Server Key') |
|
258
|
|
|
->password() |
|
259
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
260
|
|
|
TextInput::make("channels.{$channel->type}.firebase_project_id") |
|
261
|
|
|
->label('Firebase Project ID') |
|
262
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
263
|
|
|
], |
|
264
|
|
|
'discord' => [ |
|
265
|
|
|
TextInput::make("channels.{$channel->type}.webhook_url") |
|
266
|
|
|
->label('Discord Webhook URL') |
|
267
|
|
|
->url() |
|
268
|
|
|
->required() |
|
269
|
|
|
->helperText('Get this from your Discord server settings > Integrations > Webhooks') |
|
270
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
271
|
|
|
TextInput::make("channels.{$channel->type}.username") |
|
272
|
|
|
->label('Bot Username') |
|
273
|
|
|
->helperText('Optional: Custom username for the webhook') |
|
274
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
275
|
|
|
TextInput::make("channels.{$channel->type}.avatar_url") |
|
276
|
|
|
->label('Avatar URL') |
|
277
|
|
|
->url() |
|
278
|
|
|
->helperText('Optional: URL for the webhook avatar') |
|
279
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
280
|
|
|
TextInput::make("channels.{$channel->type}.color") |
|
281
|
|
|
->label('Embed Color') |
|
282
|
|
|
->helperText('Optional: Decimal color code for embed (e.g., 3447003 for blue)') |
|
283
|
|
|
->numeric() |
|
284
|
|
|
->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")), |
|
285
|
|
|
], |
|
286
|
|
|
default => [], |
|
287
|
|
|
}; |
|
288
|
|
|
|
|
289
|
|
|
return array_merge($schema, $fields); |
|
290
|
|
|
} |
|
291
|
|
|
|
|
292
|
|
|
public function save(): void |
|
293
|
|
|
{ |
|
294
|
|
|
$settings = $this->data; |
|
295
|
|
|
|
|
296
|
|
|
NotificationSetting::set('enabled', $settings['enabled'] ?? true, 'general'); |
|
297
|
|
|
NotificationSetting::set('queue_name', $settings['queue_name'] ?? 'default', 'general'); |
|
298
|
|
|
NotificationSetting::set('default_channel', $settings['default_channel'] ?? 'email', 'general'); |
|
299
|
|
|
|
|
300
|
|
|
// Save preferences settings |
|
301
|
|
|
if (isset($settings['preferences'])) { |
|
302
|
|
|
NotificationSetting::set('preferences', $settings['preferences'], 'preferences'); |
|
303
|
|
|
} |
|
304
|
|
|
|
|
305
|
|
|
// Save analytics settings |
|
306
|
|
|
if (isset($settings['analytics'])) { |
|
307
|
|
|
NotificationSetting::set('analytics', $settings['analytics'], 'analytics'); |
|
308
|
|
|
} |
|
309
|
|
|
|
|
310
|
|
|
// Save rate limiting settings |
|
311
|
|
|
if (isset($settings['rate_limiting'])) { |
|
312
|
|
|
NotificationSetting::set('rate_limiting', $settings['rate_limiting'], 'rate_limiting'); |
|
313
|
|
|
} |
|
314
|
|
|
|
|
315
|
|
|
// Save channel settings to channel records |
|
316
|
|
|
if (isset($settings['channels']) && is_array($settings['channels'])) { |
|
317
|
|
|
foreach ($settings['channels'] as $channelType => $channelData) { |
|
318
|
|
|
$channel = NotificationChannel::where('type', $channelType)->first(); |
|
319
|
|
|
|
|
320
|
|
|
if ($channel) { |
|
321
|
|
|
$isActive = $channelData['enabled'] ?? false; |
|
322
|
|
|
unset($channelData['enabled']); |
|
323
|
|
|
|
|
324
|
|
|
$channel->is_active = $isActive; |
|
325
|
|
|
|
|
326
|
|
|
$existingSettings = $channel->settings ?? []; |
|
327
|
|
|
$channel->settings = array_merge($existingSettings, $channelData); |
|
328
|
|
|
|
|
329
|
|
|
$channel->save(); |
|
330
|
|
|
} |
|
331
|
|
|
} |
|
332
|
|
|
} |
|
333
|
|
|
|
|
334
|
|
|
Notification::make() |
|
335
|
|
|
->title('Settings saved successfully') |
|
336
|
|
|
->success() |
|
337
|
|
|
->send(); |
|
338
|
|
|
} |
|
339
|
|
|
|
|
340
|
|
|
protected function getHeaderWidgets(): array |
|
341
|
|
|
{ |
|
342
|
|
|
return [ |
|
343
|
|
|
NotificationStatsOverview::class, |
|
344
|
|
|
]; |
|
345
|
|
|
} |
|
346
|
|
|
} |
|
347
|
|
|
|