Skip to content

Commit

Permalink
Merge pull request #206 from liberu-crm/sweep/Implement-in-app-notifi…
Browse files Browse the repository at this point in the history
…cations-for-CRM-events

Implement in-app notifications for CRM events
  • Loading branch information
curtisdelicata authored Oct 16, 2024
2 parents c47e455 + c9a7878 commit 7f00fe6
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 0 deletions.
28 changes: 28 additions & 0 deletions app/Http/Controllers/NotificationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class NotificationController extends Controller
{
public function index()
{
$user = auth()->user();
$notifications = $user->inAppNotifications()->paginate(10);
return view('notifications.index', compact('notifications'));
}

public function markAsRead($id)
{
$notification = auth()->user()->inAppNotifications()->findOrFail($id);
$notification->markAsRead();
return response()->json(['success' => true]);
}

public function markAllAsRead()
{
auth()->user()->unreadNotifications->markAsRead();
return response()->json(['success' => true]);
}
}
33 changes: 33 additions & 0 deletions app/Listeners/SendCRMEventNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\Listeners;

use App\Notifications\CRMEventNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SendCRMEventNotification implements ShouldQueue
{
use InteractsWithQueue;

public function handle($event)
{
$eventName = class_basename($event);
$notificationConfig = config("crm.notifications.events.{$eventName}");

if ($notificationConfig) {
$users = $this->getUsersToNotify($event);
foreach ($users as $user) {
$user->notify(new CRMEventNotification($eventName, $event->toArray()));
}
}
}

protected function getUsersToNotify($event)
{
// Implement logic to determine which users should be notified
// This could be based on user roles, team membership, or other criteria
// For now, we'll just notify all users (you should refine this)
return \App\Models\User::all();
}
}
6 changes: 6 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Laravel\Jetstream\HasTeams;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
use Illuminate\Notifications\DatabaseNotification;

class User extends Authenticatable implements HasDefaultTenant, HasTenants, FilamentUser
{
Expand Down Expand Up @@ -53,6 +54,11 @@ public function dashboardWidgets()
return $this->hasMany(DashboardWidget::class);
}

public function inAppNotifications()
{
return $this->morphMany(DatabaseNotification::class, 'notifiable')->orderBy('created_at', 'desc');
}

/**
* The attributes that should be hidden for arrays.
*
Expand Down
51 changes: 51 additions & 0 deletions app/Notifications/CRMEventNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class CRMEventNotification extends Notification implements ShouldQueue
{
use Queueable;

protected $event;
protected $data;

public function __construct($event, $data)
{
$this->event = $event;
$this->data = $data;
}

public function via($notifiable)
{
return ['mail', 'database'];
}

public function toMail($notifiable)
{
return (new MailMessage)
->subject("CRM Event: {$this->event}")
->line("A new CRM event has occurred: {$this->event}")
->line("Details: " . $this->getEventDetails())
->action('View in CRM', url('/dashboard'))
->line('Thank you for using our application!');
}

public function toArray($notifiable)
{
return [
'event' => $this->event,
'data' => $this->data,
];
}

protected function getEventDetails()
{
// Customize this method based on the event type and data
return json_encode($this->data);
}
}
17 changes: 17 additions & 0 deletions app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Services\AuditLogService;
use Illuminate\Auth\Events\Login;
use App\Listeners\LogSuccessfulLogin;
use App\Listeners\SendCRMEventNotification;

class EventServiceProvider extends ServiceProvider
{
Expand All @@ -32,6 +33,22 @@ class EventServiceProvider extends ServiceProvider
'Illuminate\Auth\Events\Logout' => [
'App\Listeners\LogSuccessfulLogout',
],
// Add CRM event listeners
'App\Events\NewLead' => [
SendCRMEventNotification::class,
],
'App\Events\DealClosed' => [
SendCRMEventNotification::class,
],
'App\Events\TaskOverdue' => [
SendCRMEventNotification::class,
],
'App\Events\NewComment' => [
SendCRMEventNotification::class,
],
'App\Events\MeetingScheduled' => [
SendCRMEventNotification::class,
],
];

/**
Expand Down
13 changes: 13 additions & 0 deletions config/crm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

return [
'notifications' => [
'events' => [
'new_lead' => true,
'deal_closed' => true,
'task_overdue' => true,
'new_comment' => true,
'meeting_scheduled' => true,
],
],
];
31 changes: 31 additions & 0 deletions resources/views/layouts/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,37 @@
@include('components.footer')
</div>

<!-- Notification Bell -->
<div class="fixed top-4 right-4">
<div class="dropdown relative">
<button class="dropdown-toggle text-gray-500 hover:text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span class="badge bg-red-500 text-white text-xs rounded-full px-1 absolute -top-1 -right-1">
{{ auth()->user()->unreadNotifications->count() }}
</span>
</button>
<div class="dropdown-menu hidden absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg overflow-hidden z-20">
<div class="py-2">
<h3 class="text-lg font-semibold px-4 py-2 border-b">Notifications</h3>
<div class="max-h-64 overflow-y-auto">
@forelse(auth()->user()->inAppNotifications()->take(5)->get() as $notification)
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 {{ $notification->read_at ? 'opacity-50' : '' }}">
{{ $notification->data['event'] }}
</a>
@empty
<p class="px-4 py-2 text-sm text-gray-500">No new notifications</p>
@endforelse
</div>
<div class="border-t px-4 py-2">
<a href="{{ route('notifications.index') }}" class="text-sm text-blue-500 hover:underline">View all notifications</a>
</div>
</div>
</div>
</div>
</div>

<!-- Scripts -->
@vite('resources/js/app.js')
@livewireScripts
Expand Down
66 changes: 66 additions & 0 deletions tests/Feature/NotificationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\User;
use App\Notifications\CRMEventNotification;
use Illuminate\Support\Facades\Notification;

class NotificationTest extends TestCase
{
use RefreshDatabase;

public function test_notifications_are_created_for_crm_events()
{
Notification::fake();

$user = User::factory()->create();
$event = 'NewLead';
$data = ['name' => 'John Doe', 'email' => '[email protected]'];

$user->notify(new CRMEventNotification($event, $data));

Notification::assertSentTo(
$user,
CRMEventNotification::class,
function ($notification) use ($event, $data) {
return $notification->event === $event && $notification->data === $data;
}
);
}

public function test_users_can_view_in_app_notifications()
{
$user = User::factory()->create();
$this->actingAs($user);

$event = 'NewLead';
$data = ['name' => 'John Doe', 'email' => '[email protected]'];

$user->notify(new CRMEventNotification($event, $data));

$response = $this->get('/notifications');

$response->assertStatus(200);
$response->assertSee($event);
}

public function test_users_can_mark_notifications_as_read()
{
$user = User::factory()->create();
$this->actingAs($user);

$notification = $user->notifications()->create([
'type' => CRMEventNotification::class,
'data' => ['event' => 'NewLead', 'data' => ['name' => 'John Doe']],
]);

$response = $this->post("/notifications/{$notification->id}/mark-as-read");

$response->assertStatus(200);
$this->assertNotNull($notification->fresh()->read_at);
}
}
40 changes: 40 additions & 0 deletions tests/Unit/Notifications/CRMEventNotificationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Tests\Unit\Notifications;

use App\Notifications\CRMEventNotification;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;

class CRMEventNotificationTest extends TestCase
{
use RefreshDatabase;

public function test_email_notification_content()
{
$user = User::factory()->create();
$event = 'NewLead';
$data = ['name' => 'John Doe', 'email' => '[email protected]'];

$notification = new CRMEventNotification($event, $data);
$mailMessage = $notification->toMail($user);

$this->assertEquals("CRM Event: {$event}", $mailMessage->subject);
$this->assertStringContainsString("A new CRM event has occurred: {$event}", $mailMessage->introLines[0]);
$this->assertStringContainsString("Details: " . json_encode($data), $mailMessage->introLines[1]);
}

public function test_in_app_notification_data_structure()
{
$user = User::factory()->create();
$event = 'DealClosed';
$data = ['deal_id' => 123, 'amount' => 10000];

$notification = new CRMEventNotification($event, $data);
$arrayData = $notification->toArray($user);

$this->assertEquals($event, $arrayData['event']);
$this->assertEquals($data, $arrayData['data']);
}
}

0 comments on commit 7f00fe6

Please sign in to comment.