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,263 @@
@extends('layouts.app')
@section('title', 'Notification Settings')
@section('page-title', '🔔 Notifications')
@section('page-subtitle', 'Configure Email, Telegram & WhatsApp alerts')
@section('content')
<div class="max-w-3xl mx-auto space-y-6">
{{-- ── How it works ─────────────────────────────────────── --}}
<div class="cat-card p-4 flex gap-3 items-start bg-blue-50/50 border-blue-100">
<span class="text-2xl shrink-0">💡</span>
<div class="text-sm text-gray-600 space-y-1">
<p class="font-semibold text-gray-700">How notifications work</p>
<p>Add one or more channels below. Notifications fire automatically every hour via the scheduler.</p>
<p class="text-xs text-gray-400">To enable the scheduler on your server: add <code class="bg-white px-1 rounded text-xs">* * * * * php /path/to/artisan schedule:run</code> to crontab.</p>
</div>
</div>
{{-- ── Existing channels ────────────────────────────────── --}}
@if($settings->isNotEmpty())
<div class="space-y-3">
@foreach($settings as $setting)
<div class="cat-card p-4" x-data="{ editing: false }">
<div class="flex items-center gap-3">
<span class="text-2xl shrink-0">{{ $setting->channelIcon }}</span>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<span class="font-semibold text-gray-800 text-sm">{{ $setting->name }}</span>
<span class="badge badge-ghost text-xs">{{ $setting->channelLabel }}</span>
@if(! $setting->is_enabled)
<span class="badge badge-error text-xs">Disabled</span>
@endif
</div>
<div class="text-xs text-gray-400 mt-0.5 flex flex-wrap gap-2">
@foreach($setting->triggers ?? [] as $trigger)
<span>{{ \App\Models\NotificationSetting::$triggerMeta[$trigger]['icon'] ?? '🔔' }}
{{ \App\Models\NotificationSetting::$triggerMeta[$trigger]['label'] ?? $trigger }}</span>
@endforeach
<span>⏰ {{ $setting->notify_before_hours }}h before</span>
</div>
</div>
<div class="flex items-center gap-1 shrink-0 flex-wrap justify-end">
{{-- Test --}}
<form action="{{ route('settings.notifications.test', $setting) }}" method="POST">
@csrf
<button type="submit" class="text-xs px-3 py-1.5 rounded-full border border-blue-200 text-blue-500 hover:bg-blue-50">
Test
</button>
</form>
{{-- Toggle --}}
<form action="{{ route('settings.notifications.toggle', $setting) }}" method="POST">
@csrf
<button type="submit"
class="text-xs px-3 py-1.5 rounded-full border {{ $setting->is_enabled ? 'border-amber-200 text-amber-500 hover:bg-amber-50' : 'border-green-200 text-green-500 hover:bg-green-50' }}">
{{ $setting->is_enabled ? 'Disable' : 'Enable' }}
</button>
</form>
{{-- Edit --}}
<button @click="editing = !editing"
class="text-xs px-3 py-1.5 rounded-full border border-pink-200 text-pink-500 hover:bg-pink-50">
Edit
</button>
{{-- Delete --}}
<form action="{{ route('settings.notifications.destroy', $setting) }}" method="POST"
onsubmit="return confirm('Remove this channel?')">
@csrf @method('DELETE')
<button type="submit"
class="text-xs px-3 py-1.5 rounded-full border border-red-100 text-red-400 hover:bg-red-50">
Del
</button>
</form>
</div>
</div>
{{-- Inline edit form --}}
<div x-show="editing" x-transition class="mt-4 pt-4 border-t border-pink-50">
<form action="{{ route('settings.notifications.update', $setting) }}" method="POST"
class="space-y-3">
@csrf @method('PUT')
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Name</label>
<input type="text" name="name" value="{{ $setting->name }}"
class="input-soft text-sm" required>
</div>
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Notify before (hours)</label>
<input type="number" name="notify_before_hours"
value="{{ $setting->notify_before_hours }}"
min="1" max="720" class="input-soft text-sm">
</div>
</div>
{{-- Triggers --}}
<div>
<label class="text-xs font-semibold text-gray-600 block mb-2">Triggers</label>
<div class="flex flex-wrap gap-3">
@foreach(\App\Models\NotificationSetting::$triggerMeta as $key => $meta)
<label class="flex items-center gap-1.5 cursor-pointer text-sm">
<input type="checkbox" name="triggers[]" value="{{ $key }}"
class="accent-pink-400"
{{ in_array($key, $setting->triggers ?? []) ? 'checked' : '' }}>
{{ $meta['icon'] }} {{ $meta['label'] }}
</label>
@endforeach
</div>
</div>
{{-- Channel-specific config --}}
@if($setting->channel === 'email')
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">To Email</label>
<input type="email" name="config[to_email]"
value="{{ $setting->config['to_email'] ?? '' }}"
class="input-soft text-sm" placeholder="you@example.com">
</div>
@elseif($setting->channel === 'telegram')
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Bot Token</label>
<input type="text" name="config[bot_token]"
value="{{ $setting->config['bot_token'] ?? '' }}"
class="input-soft text-sm" placeholder="123456:ABC-DEF...">
</div>
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Chat ID</label>
<input type="text" name="config[chat_id]"
value="{{ $setting->config['chat_id'] ?? '' }}"
class="input-soft text-sm" placeholder="-100123456789">
</div>
</div>
@elseif($setting->channel === 'whatsapp')
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">WAHA URL</label>
<input type="url" name="config[waha_url]"
value="{{ $setting->config['waha_url'] ?? '' }}"
class="input-soft text-sm" placeholder="http://localhost:3000">
</div>
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Session Name</label>
<input type="text" name="config[waha_session]"
value="{{ $setting->config['waha_session'] ?? 'default' }}"
class="input-soft text-sm" placeholder="default">
</div>
<div class="sm:col-span-2">
<label class="text-xs font-semibold text-gray-600 block mb-1">Phone Number</label>
<input type="text" name="config[phone]"
value="{{ $setting->config['phone'] ?? '' }}"
class="input-soft text-sm" placeholder="628123456789">
</div>
</div>
@endif
<button type="submit" class="btn-sakura text-sm px-6 py-2">💾 Save</button>
</form>
</div>
</div>
@endforeach
</div>
@else
<div class="cat-card p-8 text-center text-gray-400">
<span class="text-4xl block mb-2">🔕</span>
<p class="text-sm font-medium">No notification channels yet</p>
<p class="text-xs mt-1">Add one below to get started</p>
</div>
@endif
{{-- ── Add new channel ─────────────────────────────────── --}}
<div class="cat-card p-5" x-data="{ channel: 'email' }">
<h2 class="font-bold text-gray-700 mb-4 flex items-center gap-2">
<span>➕</span> Add Notification Channel
</h2>
<form action="{{ route('settings.notifications.store') }}" method="POST" class="space-y-4">
@csrf
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Name <span class="text-pink-400">*</span></label>
<input type="text" name="name" class="input-soft text-sm"
placeholder="My Telegram Bot" required>
</div>
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Channel <span class="text-pink-400">*</span></label>
<select name="channel" class="input-soft text-sm" x-model="channel">
<option value="email">✉️ Email</option>
<option value="telegram">✈️ Telegram</option>
<option value="whatsapp">💬 WhatsApp (WAHA)</option>
</select>
</div>
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Notify before (hours)</label>
<input type="number" name="notify_before_hours" value="24"
min="1" max="720" class="input-soft text-sm">
</div>
</div>
{{-- Triggers --}}
<div>
<label class="text-xs font-semibold text-gray-600 block mb-2">Send when…</label>
<div class="flex flex-wrap gap-4">
@foreach(\App\Models\NotificationSetting::$triggerMeta as $key => $meta)
<label class="flex items-center gap-1.5 cursor-pointer text-sm">
<input type="checkbox" name="triggers[]" value="{{ $key }}"
class="accent-pink-400" checked>
{{ $meta['icon'] }} {{ $meta['label'] }}
</label>
@endforeach
</div>
</div>
{{-- Email --}}
<div x-show="channel === 'email'" x-transition>
<label class="text-xs font-semibold text-gray-600 block mb-1">To Email <span class="text-pink-400">*</span></label>
<input type="email" name="config[to_email]" class="input-soft text-sm"
placeholder="you@example.com">
</div>
{{-- Telegram --}}
<div x-show="channel === 'telegram'" x-transition class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Bot Token <span class="text-pink-400">*</span></label>
<input type="text" name="config[bot_token]" class="input-soft text-sm"
placeholder="123456:ABC-DEF...">
<p class="text-xs text-gray-400 mt-1">Get from <a href="https://t.me/BotFather" target="_blank" class="text-pink-500">@BotFather</a></p>
</div>
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Chat ID <span class="text-pink-400">*</span></label>
<input type="text" name="config[chat_id]" class="input-soft text-sm"
placeholder="-100123456789 or @username">
<p class="text-xs text-gray-400 mt-1">Use <a href="https://t.me/userinfobot" target="_blank" class="text-pink-500">@userinfobot</a> to find your ID</p>
</div>
</div>
{{-- WhatsApp / WAHA --}}
<div x-show="channel === 'whatsapp'" x-transition class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">WAHA API URL <span class="text-pink-400">*</span></label>
<input type="url" name="config[waha_url]" class="input-soft text-sm"
placeholder="http://localhost:3000">
<p class="text-xs text-gray-400 mt-1">Your self-hosted WAHA instance</p>
</div>
<div>
<label class="text-xs font-semibold text-gray-600 block mb-1">Session Name</label>
<input type="text" name="config[waha_session]" value="default" class="input-soft text-sm">
</div>
<div class="sm:col-span-2">
<label class="text-xs font-semibold text-gray-600 block mb-1">Phone Number <span class="text-pink-400">*</span></label>
<input type="text" name="config[phone]" class="input-soft text-sm"
placeholder="628123456789 (country code, no +)">
</div>
</div>
<button type="submit" class="btn-sakura">🔔 Add Channel</button>
</form>
</div>
</div>
@endsection