feat: Add notification services for Email, Telegram, and WhatsApp

- Implement EmailNotifier for sending batch and test notifications via email.
- Implement TelegramNotifier for sending batch and test notifications via Telegram.
- Implement WahaNotifier for sending batch and test notifications via WhatsApp using WAHA API.
- Create migration for client_categories table and update clients table to include category_id.
- Create migration for notification_settings table to manage notification configurations.
- Add ClientCategorySeeder to populate default client categories.
- Create manifest.json for PWA support.
- Implement service worker (sw.js) for caching static assets and offline support.
- Add edit event view for modifying scheduled events.
- Create search results view for displaying search queries.
- Develop notification settings view for configuring notification channels and triggers.
This commit is contained in:
I-Fun
2026-04-01 12:46:58 +07:00
parent 1126877ef5
commit d3c35ad730
29 changed files with 1849 additions and 96 deletions

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\ContentBrief;
use App\Models\Event;
use App\Models\Invoice;
use App\Models\NotificationSetting;
use App\Services\Notifications\EmailNotifier;
use App\Services\Notifications\TelegramNotifier;
use App\Services\Notifications\WahaNotifier;
use Illuminate\Support\Facades\Log;
class NotificationDispatcher
{
/** Dispatch all due notifications for enabled channels */
public function dispatchAll(): void
{
$settings = NotificationSetting::where('is_enabled', true)->get();
foreach ($settings as $setting) {
try {
$this->dispatchForSetting($setting);
} catch (\Throwable $e) {
Log::error("Notification dispatch failed for [{$setting->name}]: " . $e->getMessage());
}
}
}
public function dispatchForSetting(NotificationSetting $setting): void
{
$triggers = $setting->triggers ?? [];
$hours = $setting->notify_before_hours;
$messages = collect();
if (in_array('upcoming_event', $triggers)) {
$events = Event::with('client')
->where('start_time', '>=', now())
->where('start_time', '<=', now()->addHours($hours))
->whereNotIn('status', ['completed', 'cancelled'])
->get();
foreach ($events as $event) {
$messages->push(
"📅 *Upcoming Event*\n"
. "*{$event->title}*\n"
. "⏰ " . $event->start_time->format('d M Y, H:i') . "\n"
. ($event->client ? "👤 {$event->client->name}\n" : '')
. ($event->location ? "📍 {$event->location}" : '')
);
}
}
if (in_array('invoice_due', $triggers)) {
$invoices = Invoice::with('client')
->whereBetween('due_date', [now(), now()->addHours($hours)])
->whereNotIn('status', ['paid', 'cancelled'])
->get();
foreach ($invoices as $inv) {
$messages->push(
"🧾 *Invoice Due Soon*\n"
. "*{$inv->invoice_number}*"
. ($inv->title ? " — {$inv->title}" : '') . "\n"
. "📅 Due: " . $inv->due_date->format('d M Y') . "\n"
. ($inv->client ? "👤 {$inv->client->name}\n" : '')
. ($inv->formatted_total ?? '')
);
}
}
if (in_array('content_due', $triggers)) {
$content = ContentBrief::with('client')
->whereBetween('target_date', [now(), now()->addHours($hours)])
->whereNotIn('status', ['posted', 'archived'])
->get();
foreach ($content as $c) {
$messages->push(
"🎬 *Content Target*\n"
. "*{$c->title}*\n"
. "📅 " . $c->target_date->format('d M Y') . "\n"
. ucfirst($c->platform)
. ($c->client ? " · {$c->client->name}" : '')
);
}
}
if ($messages->isEmpty()) return;
$notifier = $this->resolveNotifier($setting);
$notifier->sendBatch($setting, $messages->all());
}
/** Send a simple test message */
public function sendTest(NotificationSetting $setting): void
{
$this->resolveNotifier($setting)->sendTest($setting);
}
private function resolveNotifier(NotificationSetting $setting): EmailNotifier|TelegramNotifier|WahaNotifier
{
return match ($setting->channel) {
'email' => new EmailNotifier,
'telegram' => new TelegramNotifier,
'whatsapp' => new WahaNotifier,
default => throw new \InvalidArgumentException("Unknown channel: {$setting->channel}"),
};
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Services\Notifications;
use App\Models\NotificationSetting;
use Illuminate\Support\Facades\Mail;
class EmailNotifier
{
public function sendBatch(NotificationSetting $setting, array $messages): void
{
$toEmail = $setting->config['to_email'] ?? null;
if (! $toEmail) {
throw new \RuntimeException('Email to_email address is required.');
}
$combined = implode("\n\n---\n\n", $messages);
Mail::raw($combined, function ($mail) use ($toEmail, $setting): void {
$mail->to($toEmail)
->subject("🐱 KOL Manager — {$setting->name} Notifications");
});
}
public function sendTest(NotificationSetting $setting): void
{
$toEmail = $setting->config['to_email'] ?? null;
if (! $toEmail) {
throw new \RuntimeException('Email to_email address is required.');
}
Mail::raw(
"🐱 KOL Manager — Test Notification\n\nYour Email channel is configured correctly! 🎉",
function ($mail) use ($toEmail): void {
$mail->to($toEmail)->subject('🐱 KOL Manager — Test Notification');
}
);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Services\Notifications;
use App\Models\NotificationSetting;
use Illuminate\Support\Facades\Http;
class TelegramNotifier
{
public function sendBatch(NotificationSetting $setting, array $messages): void
{
foreach ($messages as $message) {
$this->send($setting, $message);
}
}
public function sendTest(NotificationSetting $setting): void
{
$this->send(
$setting,
"🐱 *KOL Manager — Test Notification*\n\nYour Telegram channel is configured correctly! 🎉"
);
}
private function send(NotificationSetting $setting, string $text): void
{
$config = $setting->config ?? [];
$botToken = $config['bot_token'] ?? null;
$chatId = $config['chat_id'] ?? null;
if (! $botToken || ! $chatId) {
throw new \RuntimeException('Telegram bot_token and chat_id are required.');
}
$response = Http::timeout(10)->post(
"https://api.telegram.org/bot{$botToken}/sendMessage",
[
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'Markdown',
]
);
if (! $response->successful()) {
throw new \RuntimeException('Telegram API error: ' . $response->body());
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Services\Notifications;
use App\Models\NotificationSetting;
use Illuminate\Support\Facades\Http;
/**
* WAHA (WhatsApp HTTP API) — self-hosted WhatsApp bridge.
* Docs: https://waha.devlike.pro/docs/how-to/send-messages/
*/
class WahaNotifier
{
public function sendBatch(NotificationSetting $setting, array $messages): void
{
foreach ($messages as $message) {
$this->send($setting, $message);
}
}
public function sendTest(NotificationSetting $setting): void
{
$this->send(
$setting,
"🐱 *KOL Manager — Test Notification*\n\nYour WhatsApp channel is configured correctly! 🎉"
);
}
private function send(NotificationSetting $setting, string $text): void
{
$config = $setting->config ?? [];
$url = rtrim($config['waha_url'] ?? '', '/');
$session = $config['waha_session'] ?? 'default';
$phone = $config['phone'] ?? null;
if (! $url || ! $phone) {
throw new \RuntimeException('WAHA waha_url and phone are required.');
}
// Normalise phone: strip non-digits, ensure no leading +
$chatId = preg_replace('/[^0-9]/', '', $phone) . '@c.us';
$response = Http::timeout(15)->post("{$url}/api/sendText", [
'session' => $session,
'chatId' => $chatId,
'text' => $text,
]);
if (! $response->successful()) {
throw new \RuntimeException('WAHA API error: ' . $response->body());
}
}
}