From aa606930d2585dbed567e4d6c5f0a74446347ac6 Mon Sep 17 00:00:00 2001 From: Wallace Martins Date: Sun, 7 Dec 2025 12:47:06 -0300 Subject: [PATCH] feat: add WhatsappMessageResource and WhatsappWebhookResource - Add WhatsappMessageResource to view message history - List view with filters by instance, direction, type, status - View page with message details and raw payload - Add WhatsappWebhookResource to view webhook logs - List view with filters by instance, event, processed status, errors - View page with webhook details, error info and payload - Update FilamentEvolutionPlugin with optional resource loading: - viewMessageHistory() - enable message history resource (default: false) - viewWebhookLogs() - enable webhook logs resource (default: false) - whatsappInstanceResource() - show/hide instances (default: true) - Add translations for new resources (en/pt_BR) - Update README with plugin options documentation - Fix Filament v4 infolist signature (Schema instead of Infolist) --- README.md | 25 +++ resources/lang/en/message.php | 29 ++++ resources/lang/en/webhook.php | 29 ++++ resources/lang/pt_BR/message.php | 29 ++++ resources/lang/pt_BR/webhook.php | 29 ++++ .../Pages/CreateWhatsappInstance.php | 2 +- .../Pages/ViewWhatsappInstance.php | 2 +- .../Resources/WhatsappMessageResource.php | 149 ++++++++++++++++++ .../Pages/ListWhatsappMessages.php | 18 +++ .../Pages/ViewWhatsappMessage.php | 94 +++++++++++ .../Resources/WhatsappWebhookResource.php | 149 ++++++++++++++++++ .../Pages/ListWhatsappWebhooks.php | 18 +++ .../Pages/ViewWhatsappWebhook.php | 73 +++++++++ src/FilamentEvolutionPlugin.php | 47 +++++- src/FilamentEvolutionServiceProvider.php | 2 +- src/Models/WhatsappWebhook.php | 4 +- src/Services/EvolutionClient.php | 2 +- src/Services/WhatsappService.php | 7 +- tests/Unit/EvolutionClientTest.php | 6 +- 19 files changed, 699 insertions(+), 15 deletions(-) create mode 100644 resources/lang/en/message.php create mode 100644 resources/lang/en/webhook.php create mode 100644 resources/lang/pt_BR/message.php create mode 100644 resources/lang/pt_BR/webhook.php create mode 100644 src/Filament/Resources/WhatsappMessageResource.php create mode 100644 src/Filament/Resources/WhatsappMessageResource/Pages/ListWhatsappMessages.php create mode 100644 src/Filament/Resources/WhatsappMessageResource/Pages/ViewWhatsappMessage.php create mode 100644 src/Filament/Resources/WhatsappWebhookResource.php create mode 100644 src/Filament/Resources/WhatsappWebhookResource/Pages/ListWhatsappWebhooks.php create mode 100644 src/Filament/Resources/WhatsappWebhookResource/Pages/ViewWhatsappWebhook.php diff --git a/README.md b/README.md index e99f777..52962a5 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,31 @@ public function panel(Panel $panel): Panel } ``` +### Plugin Options + +You can customize which resources are available in the panel: + +```php +FilamentEvolutionPlugin::make() + ->viewMessageHistory() // Enable message history resource + ->viewWebhookLogs() // Enable webhook logs resource +``` + +| Method | Default | Description | +|--------|---------|-------------| +| `whatsappInstanceResource(bool)` | `true` | Show/hide the WhatsApp Instances resource | +| `viewMessageHistory(bool)` | `false` | Show/hide the Message History resource | +| `viewWebhookLogs(bool)` | `false` | Show/hide the Webhook Logs resource | + +#### Example: Full Configuration + +```php +FilamentEvolutionPlugin::make() + ->whatsappInstanceResource() // Show instances (default: true) + ->viewMessageHistory() // Show message history + ->viewWebhookLogs() // Show webhook logs +``` + --- ## Configuration diff --git a/resources/lang/en/message.php b/resources/lang/en/message.php new file mode 100644 index 0000000..8b70732 --- /dev/null +++ b/resources/lang/en/message.php @@ -0,0 +1,29 @@ + 'Messages', + 'model_label' => 'Message', + 'plural_model_label' => 'Messages', + + 'sections' => [ + 'message_info' => 'Message Information', + 'content' => 'Content', + 'timestamps' => 'Timestamps', + 'raw_payload' => 'Raw Payload', + ], + + 'fields' => [ + 'instance' => 'Instance', + 'direction' => 'Direction', + 'phone' => 'Phone', + 'type' => 'Type', + 'content' => 'Content', + 'status' => 'Status', + 'message_id' => 'Message ID', + 'media' => 'Media', + 'sent_at' => 'Sent At', + 'delivered_at' => 'Delivered At', + 'read_at' => 'Read At', + 'created_at' => 'Created At', + ], +]; diff --git a/resources/lang/en/webhook.php b/resources/lang/en/webhook.php new file mode 100644 index 0000000..b8929cf --- /dev/null +++ b/resources/lang/en/webhook.php @@ -0,0 +1,29 @@ + 'Webhook Logs', + 'model_label' => 'Webhook Log', + 'plural_model_label' => 'Webhook Logs', + + 'sections' => [ + 'webhook_info' => 'Webhook Information', + 'payload' => 'Payload', + 'error' => 'Error', + ], + + 'fields' => [ + 'instance' => 'Instance', + 'event' => 'Event', + 'processed' => 'Processed', + 'has_error' => 'Has Error', + 'error' => 'Error', + 'processing_time' => 'Processing Time', + 'created_at' => 'Created At', + 'updated_at' => 'Updated At', + ], + + 'status' => [ + 'yes' => 'Yes', + 'no' => 'No', + ], +]; diff --git a/resources/lang/pt_BR/message.php b/resources/lang/pt_BR/message.php new file mode 100644 index 0000000..d8e1c70 --- /dev/null +++ b/resources/lang/pt_BR/message.php @@ -0,0 +1,29 @@ + 'Mensagens', + 'model_label' => 'Mensagem', + 'plural_model_label' => 'Mensagens', + + 'sections' => [ + 'message_info' => 'Informações da Mensagem', + 'content' => 'Conteúdo', + 'timestamps' => 'Datas', + 'raw_payload' => 'Payload Original', + ], + + 'fields' => [ + 'instance' => 'Instância', + 'direction' => 'Direção', + 'phone' => 'Telefone', + 'type' => 'Tipo', + 'content' => 'Conteúdo', + 'status' => 'Status', + 'message_id' => 'ID da Mensagem', + 'media' => 'Mídia', + 'sent_at' => 'Enviado em', + 'delivered_at' => 'Entregue em', + 'read_at' => 'Lido em', + 'created_at' => 'Criado em', + ], +]; diff --git a/resources/lang/pt_BR/webhook.php b/resources/lang/pt_BR/webhook.php new file mode 100644 index 0000000..1cc687a --- /dev/null +++ b/resources/lang/pt_BR/webhook.php @@ -0,0 +1,29 @@ + 'Logs de Webhook', + 'model_label' => 'Log de Webhook', + 'plural_model_label' => 'Logs de Webhook', + + 'sections' => [ + 'webhook_info' => 'Informações do Webhook', + 'payload' => 'Payload', + 'error' => 'Erro', + ], + + 'fields' => [ + 'instance' => 'Instância', + 'event' => 'Evento', + 'processed' => 'Processado', + 'has_error' => 'Tem Erro', + 'error' => 'Erro', + 'processing_time' => 'Tempo de Processamento', + 'created_at' => 'Criado em', + 'updated_at' => 'Atualizado em', + ], + + 'status' => [ + 'yes' => 'Sim', + 'no' => 'Não', + ], +]; diff --git a/src/Filament/Resources/WhatsappInstanceResource/Pages/CreateWhatsappInstance.php b/src/Filament/Resources/WhatsappInstanceResource/Pages/CreateWhatsappInstance.php index 69640e3..ada13aa 100644 --- a/src/Filament/Resources/WhatsappInstanceResource/Pages/CreateWhatsappInstance.php +++ b/src/Filament/Resources/WhatsappInstanceResource/Pages/CreateWhatsappInstance.php @@ -52,7 +52,7 @@ class CreateWhatsappInstance extends CreateRecord Notification::make() ->warning() ->title(__('filament-evolution::resource.messages.created')) - ->body('Instance saved locally. API sync failed: ' . $e->getMessage()) + ->body('Instance saved locally. API sync failed: '.$e->getMessage()) ->send(); } } diff --git a/src/Filament/Resources/WhatsappInstanceResource/Pages/ViewWhatsappInstance.php b/src/Filament/Resources/WhatsappInstanceResource/Pages/ViewWhatsappInstance.php index 98a114b..f103699 100644 --- a/src/Filament/Resources/WhatsappInstanceResource/Pages/ViewWhatsappInstance.php +++ b/src/Filament/Resources/WhatsappInstanceResource/Pages/ViewWhatsappInstance.php @@ -105,7 +105,7 @@ class ViewWhatsappInstance extends ViewRecord Notification::make() ->success() - ->title(__('filament-evolution::resource.fields.status') . ': ' . $status->getLabel()) + ->title(__('filament-evolution::resource.fields.status').': '.$status->getLabel()) ->send(); } catch (EvolutionApiException $e) { diff --git a/src/Filament/Resources/WhatsappMessageResource.php b/src/Filament/Resources/WhatsappMessageResource.php new file mode 100644 index 0000000..6cdb65b --- /dev/null +++ b/src/Filament/Resources/WhatsappMessageResource.php @@ -0,0 +1,149 @@ +schema([]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('instance.name') + ->label(__('filament-evolution::message.fields.instance')) + ->searchable() + ->sortable(), + + TextColumn::make('direction') + ->label(__('filament-evolution::message.fields.direction')) + ->badge() + ->sortable(), + + TextColumn::make('phone') + ->label(__('filament-evolution::message.fields.phone')) + ->searchable() + ->copyable(), + + TextColumn::make('type') + ->label(__('filament-evolution::message.fields.type')) + ->badge() + ->sortable(), + + TextColumn::make('content') + ->label(__('filament-evolution::message.fields.content')) + ->limit(50) + ->wrap() + ->searchable(), + + TextColumn::make('status') + ->label(__('filament-evolution::message.fields.status')) + ->badge() + ->sortable(), + + TextColumn::make('sent_at') + ->label(__('filament-evolution::message.fields.sent_at')) + ->dateTime() + ->sortable(), + + TextColumn::make('created_at') + ->label(__('filament-evolution::message.fields.created_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->defaultSort('created_at', 'desc') + ->filters([ + SelectFilter::make('instance') + ->relationship('instance', 'name') + ->label(__('filament-evolution::message.fields.instance')) + ->preload(), + + SelectFilter::make('direction') + ->options(MessageDirectionEnum::class) + ->label(__('filament-evolution::message.fields.direction')), + + SelectFilter::make('type') + ->options(MessageTypeEnum::class) + ->label(__('filament-evolution::message.fields.type')), + + SelectFilter::make('status') + ->options(MessageStatusEnum::class) + ->label(__('filament-evolution::message.fields.status')), + ]) + ->actions([]) + ->bulkActions([]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListWhatsappMessages::route('/'), + 'view' => Pages\ViewWhatsappMessage::route('/{record}'), + ]; + } +} diff --git a/src/Filament/Resources/WhatsappMessageResource/Pages/ListWhatsappMessages.php b/src/Filament/Resources/WhatsappMessageResource/Pages/ListWhatsappMessages.php new file mode 100644 index 0000000..ff4666c --- /dev/null +++ b/src/Filament/Resources/WhatsappMessageResource/Pages/ListWhatsappMessages.php @@ -0,0 +1,18 @@ +schema([ + Section::make(__('filament-evolution::message.sections.message_info')) + ->schema([ + TextEntry::make('instance.name') + ->label(__('filament-evolution::message.fields.instance')), + + TextEntry::make('direction') + ->label(__('filament-evolution::message.fields.direction')) + ->badge(), + + TextEntry::make('phone') + ->label(__('filament-evolution::message.fields.phone')) + ->copyable(), + + TextEntry::make('type') + ->label(__('filament-evolution::message.fields.type')) + ->badge(), + + TextEntry::make('status') + ->label(__('filament-evolution::message.fields.status')) + ->badge(), + + TextEntry::make('message_id') + ->label(__('filament-evolution::message.fields.message_id')) + ->copyable(), + ]) + ->columns(3), + + Section::make(__('filament-evolution::message.sections.content')) + ->schema([ + TextEntry::make('content') + ->label(__('filament-evolution::message.fields.content')) + ->columnSpanFull() + ->prose(), + + TextEntry::make('media') + ->label(__('filament-evolution::message.fields.media')) + ->columnSpanFull() + ->formatStateUsing(fn ($state) => $state ? json_encode($state, JSON_PRETTY_PRINT) : '-') + ->visible(fn ($record) => ! empty($record->media)), + ]), + + Section::make(__('filament-evolution::message.sections.timestamps')) + ->schema([ + TextEntry::make('sent_at') + ->label(__('filament-evolution::message.fields.sent_at')) + ->dateTime(), + + TextEntry::make('delivered_at') + ->label(__('filament-evolution::message.fields.delivered_at')) + ->dateTime(), + + TextEntry::make('read_at') + ->label(__('filament-evolution::message.fields.read_at')) + ->dateTime(), + + TextEntry::make('created_at') + ->label(__('filament-evolution::message.fields.created_at')) + ->dateTime(), + ]) + ->columns(4), + + Section::make(__('filament-evolution::message.sections.raw_payload')) + ->schema([ + TextEntry::make('raw_payload') + ->label('') + ->columnSpanFull() + ->formatStateUsing(fn ($state) => $state ? json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '-') + ->prose(), + ]) + ->collapsible() + ->collapsed(), + ]); + } +} diff --git a/src/Filament/Resources/WhatsappWebhookResource.php b/src/Filament/Resources/WhatsappWebhookResource.php new file mode 100644 index 0000000..a034aa8 --- /dev/null +++ b/src/Filament/Resources/WhatsappWebhookResource.php @@ -0,0 +1,149 @@ +schema([]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('instance.name') + ->label(__('filament-evolution::webhook.fields.instance')) + ->searchable() + ->sortable() + ->placeholder('-'), + + TextColumn::make('event') + ->label(__('filament-evolution::webhook.fields.event')) + ->badge() + ->sortable(), + + IconColumn::make('processed') + ->label(__('filament-evolution::webhook.fields.processed')) + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-clock') + ->trueColor('success') + ->falseColor('warning') + ->sortable(), + + IconColumn::make('error') + ->label(__('filament-evolution::webhook.fields.has_error')) + ->boolean() + ->getStateUsing(fn ($record) => ! empty($record->error)) + ->trueIcon('heroicon-o-x-circle') + ->falseIcon('heroicon-o-check') + ->trueColor('danger') + ->falseColor('success'), + + TextColumn::make('processing_time_ms') + ->label(__('filament-evolution::webhook.fields.processing_time')) + ->suffix(' ms') + ->sortable() + ->placeholder('-'), + + TextColumn::make('created_at') + ->label(__('filament-evolution::webhook.fields.created_at')) + ->dateTime() + ->sortable(), + ]) + ->defaultSort('created_at', 'desc') + ->filters([ + SelectFilter::make('instance') + ->relationship('instance', 'name') + ->label(__('filament-evolution::webhook.fields.instance')) + ->preload(), + + SelectFilter::make('event') + ->options(WebhookEventEnum::class) + ->label(__('filament-evolution::webhook.fields.event')), + + TernaryFilter::make('processed') + ->label(__('filament-evolution::webhook.fields.processed')), + + TernaryFilter::make('has_error') + ->label(__('filament-evolution::webhook.fields.has_error')) + ->queries( + true: fn ($query) => $query->whereNotNull('error'), + false: fn ($query) => $query->whereNull('error'), + ), + ]) + ->actions([]) + ->bulkActions([]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListWhatsappWebhooks::route('/'), + 'view' => Pages\ViewWhatsappWebhook::route('/{record}'), + ]; + } +} diff --git a/src/Filament/Resources/WhatsappWebhookResource/Pages/ListWhatsappWebhooks.php b/src/Filament/Resources/WhatsappWebhookResource/Pages/ListWhatsappWebhooks.php new file mode 100644 index 0000000..35f3d15 --- /dev/null +++ b/src/Filament/Resources/WhatsappWebhookResource/Pages/ListWhatsappWebhooks.php @@ -0,0 +1,18 @@ +schema([ + Section::make(__('filament-evolution::webhook.sections.webhook_info')) + ->schema([ + TextEntry::make('instance.name') + ->label(__('filament-evolution::webhook.fields.instance')) + ->placeholder('-'), + + TextEntry::make('event') + ->label(__('filament-evolution::webhook.fields.event')) + ->badge(), + + TextEntry::make('processed') + ->label(__('filament-evolution::webhook.fields.processed')) + ->badge() + ->formatStateUsing(fn ($state) => $state ? __('filament-evolution::webhook.status.yes') : __('filament-evolution::webhook.status.no')) + ->color(fn ($state) => $state ? 'success' : 'warning'), + + TextEntry::make('processing_time_ms') + ->label(__('filament-evolution::webhook.fields.processing_time')) + ->suffix(' ms') + ->placeholder('-'), + + TextEntry::make('created_at') + ->label(__('filament-evolution::webhook.fields.created_at')) + ->dateTime(), + + TextEntry::make('updated_at') + ->label(__('filament-evolution::webhook.fields.updated_at')) + ->dateTime(), + ]) + ->columns(3), + + Section::make(__('filament-evolution::webhook.sections.error')) + ->schema([ + TextEntry::make('error') + ->label('') + ->columnSpanFull() + ->prose() + ->color('danger'), + ]) + ->visible(fn ($record) => ! empty($record->error)), + + Section::make(__('filament-evolution::webhook.sections.payload')) + ->schema([ + TextEntry::make('payload') + ->label('') + ->columnSpanFull() + ->formatStateUsing(fn ($state) => $state ? json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '-') + ->prose(), + ]) + ->collapsible(), + ]); + } +} diff --git a/src/FilamentEvolutionPlugin.php b/src/FilamentEvolutionPlugin.php index 5dc156c..ce89b21 100644 --- a/src/FilamentEvolutionPlugin.php +++ b/src/FilamentEvolutionPlugin.php @@ -7,11 +7,17 @@ namespace WallaceMartinss\FilamentEvolution; use Filament\Contracts\Plugin; use Filament\Panel; use WallaceMartinss\FilamentEvolution\Filament\Resources\WhatsappInstanceResource; +use WallaceMartinss\FilamentEvolution\Filament\Resources\WhatsappMessageResource; +use WallaceMartinss\FilamentEvolution\Filament\Resources\WhatsappWebhookResource; class FilamentEvolutionPlugin implements Plugin { protected bool $hasWhatsappInstanceResource = true; + protected bool $hasWhatsappMessageResource = false; + + protected bool $hasWhatsappWebhookResource = false; + public static function make(): static { return app(static::class); @@ -32,10 +38,22 @@ class FilamentEvolutionPlugin implements Plugin public function register(Panel $panel): void { + $resources = []; + if ($this->hasWhatsappInstanceResource) { - $panel->resources([ - WhatsappInstanceResource::class, - ]); + $resources[] = WhatsappInstanceResource::class; + } + + if ($this->hasWhatsappMessageResource) { + $resources[] = WhatsappMessageResource::class; + } + + if ($this->hasWhatsappWebhookResource) { + $resources[] = WhatsappWebhookResource::class; + } + + if (! empty($resources)) { + $panel->resources($resources); } } @@ -44,10 +62,33 @@ class FilamentEvolutionPlugin implements Plugin // } + /** + * Enable or disable the WhatsApp Instance resource. + */ public function whatsappInstanceResource(bool $condition = true): static { $this->hasWhatsappInstanceResource = $condition; return $this; } + + /** + * Enable the Message History resource to view all messages. + */ + public function viewMessageHistory(bool $condition = true): static + { + $this->hasWhatsappMessageResource = $condition; + + return $this; + } + + /** + * Enable the Webhook Logs resource to view all webhook events. + */ + public function viewWebhookLogs(bool $condition = true): static + { + $this->hasWhatsappWebhookResource = $condition; + + return $this; + } } diff --git a/src/FilamentEvolutionServiceProvider.php b/src/FilamentEvolutionServiceProvider.php index 221c62e..53cb125 100644 --- a/src/FilamentEvolutionServiceProvider.php +++ b/src/FilamentEvolutionServiceProvider.php @@ -41,7 +41,7 @@ class FilamentEvolutionServiceProvider extends PackageServiceProvider public function packageRegistered(): void { $this->app->singleton(EvolutionClient::class, function () { - return new EvolutionClient(); + return new EvolutionClient; }); $this->app->singleton(WhatsappService::class, function ($app) { diff --git a/src/Models/WhatsappWebhook.php b/src/Models/WhatsappWebhook.php index 94ae624..7c7395c 100644 --- a/src/Models/WhatsappWebhook.php +++ b/src/Models/WhatsappWebhook.php @@ -51,7 +51,7 @@ class WhatsappWebhook extends Model return ! empty($this->error); } - public function markAsProcessed(int $processingTimeMs = null): void + public function markAsProcessed(?int $processingTimeMs = null): void { $this->update([ 'processed' => true, @@ -59,7 +59,7 @@ class WhatsappWebhook extends Model ]); } - public function markAsFailed(string $error, int $processingTimeMs = null): void + public function markAsFailed(string $error, ?int $processingTimeMs = null): void { $this->update([ 'processed' => false, diff --git a/src/Services/EvolutionClient.php b/src/Services/EvolutionClient.php index 6eff338..dc4f245 100644 --- a/src/Services/EvolutionClient.php +++ b/src/Services/EvolutionClient.php @@ -184,7 +184,7 @@ class EvolutionClient */ public function fetchInstance(string $instanceName): array { - return $this->request('GET', "/instance/fetchInstances", [ + return $this->request('GET', '/instance/fetchInstances', [ 'instanceName' => $instanceName, ]); } diff --git a/src/Services/WhatsappService.php b/src/Services/WhatsappService.php index ba05479..a9e735b 100644 --- a/src/Services/WhatsappService.php +++ b/src/Services/WhatsappService.php @@ -47,7 +47,7 @@ class WhatsappService // If Brazilian number without country code, add it if (strlen($number) === 10 || strlen($number) === 11) { - $number = '55' . $number; + $number = '55'.$number; } return $number; @@ -68,18 +68,19 @@ class WhatsappService // For local files, convert to base64 (Evolution API expects raw base64 without data: prefix) $storage = Storage::disk($disk); - + if (! $storage->exists($path)) { throw new EvolutionApiException("File not found: {$path}"); } $contents = $storage->get($path); - + return base64_encode($contents); } /** * Get public URL for a file (supports local and S3). + * * @deprecated Use getMediaContent() instead for Evolution API */ public function getFileUrl(string $path, ?string $disk = null): string diff --git a/tests/Unit/EvolutionClientTest.php b/tests/Unit/EvolutionClientTest.php index 527562a..9e12afc 100644 --- a/tests/Unit/EvolutionClientTest.php +++ b/tests/Unit/EvolutionClientTest.php @@ -11,21 +11,21 @@ class EvolutionClientTest extends TestCase { public function test_client_can_be_instantiated(): void { - $client = new EvolutionClient(); + $client = new EvolutionClient; $this->assertInstanceOf(EvolutionClient::class, $client); } public function test_client_is_configured_when_has_url_and_key(): void { - $client = new EvolutionClient(); + $client = new EvolutionClient; $this->assertTrue($client->isConfigured()); } public function test_client_returns_configured_base_url(): void { - $client = new EvolutionClient(); + $client = new EvolutionClient; $this->assertSame('https://api.evolution.test', $client->getBaseUrl()); }