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

@@ -42,6 +42,22 @@
@endforeach
</select>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-semibold text-gray-700 mb-1">🏷️ Category</label>
<select name="category_id" class="input-soft">
<option value="">— No Category —</option>
@foreach($categoryTree as $parent)
<option value="{{ $parent->id }}" {{ old('category_id') == $parent->id ? 'selected' : '' }}>
{{ $parent->icon }} {{ $parent->name }}
</option>
@foreach($parent->children as $child)
<option value="{{ $child->id }}" {{ old('category_id') == $child->id ? 'selected' : '' }}>
&nbsp;&nbsp;↳ {{ $child->icon }} {{ $child->name }}
</option>
@endforeach
@endforeach
</select>
</div>
</div>
</div>

View File

@@ -37,6 +37,22 @@
@endforeach
</select>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-semibold text-gray-700 mb-1">🏷️ Category</label>
<select name="category_id" class="input-soft">
<option value="">— No Category —</option>
@foreach($categoryTree as $parent)
<option value="{{ $parent->id }}" {{ old('category_id', $client->category_id) == $parent->id ? 'selected' : '' }}>
{{ $parent->icon }} {{ $parent->name }}
</option>
@foreach($parent->children as $child)
<option value="{{ $child->id }}" {{ old('category_id', $client->category_id) == $child->id ? 'selected' : '' }}>
&nbsp;&nbsp;↳ {{ $child->icon }} {{ $child->name }}
</option>
@endforeach
@endforeach
</select>
</div>
</div>
<div>

View File

@@ -5,33 +5,102 @@
@section('page-subtitle', 'Manage your brand partners and contacts')
@section('content')
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
<div class="relative">
<input type="text" placeholder="Search clients..." class="input-soft pl-9 text-sm"
oninput="filterClients(this.value)" id="search-input">
<span class="absolute left-3 top-2.5 text-gray-400 text-sm">🔍</span>
{{-- ── Filters bar ─────────────────────────────────────────── --}}
<div class="flex flex-col gap-3 mb-5">
<div class="flex flex-col sm:flex-row sm:items-center gap-3">
{{-- Local text filter --}}
<div class="relative flex-1 max-w-xs">
<span class="absolute left-3 top-2.5 text-gray-400 text-sm">🔍</span>
<input type="text" placeholder="Filter by name…" class="input-soft pl-9 text-sm"
oninput="filterClients(this.value)" id="search-input">
</div>
{{-- Category filter --}}
<form method="GET" action="{{ route('clients.index') }}" class="flex flex-wrap gap-2 items-center" id="filter-form">
<select name="category" class="input-soft text-sm py-2"
onchange="document.getElementById('filter-form').submit()">
<option value="">All Categories</option>
@foreach($categories as $parent)
<option value="{{ $parent->id }}"
{{ request('category') == $parent->id ? 'selected' : '' }}>
{{ $parent->icon }} {{ $parent->name }}
</option>
@foreach($parent->children as $child)
<option value="{{ $child->id }}"
{{ request('category') == $child->id ? 'selected' : '' }}>
&nbsp;&nbsp;↳ {{ $child->icon }} {{ $child->name }}
</option>
@endforeach
@endforeach
</select>
<select name="status" class="input-soft text-sm py-2"
onchange="document.getElementById('filter-form').submit()">
<option value="">All Statuses</option>
@foreach($statuses as $s)
<option value="{{ $s->value }}" {{ request('status') === $s->value ? 'selected' : '' }}>
{{ $s->label() }}
</option>
@endforeach
</select>
@if(request('category') || request('status'))
<a href="{{ route('clients.index') }}" class="text-xs text-gray-400 hover:text-gray-600">✕ Clear</a>
@endif
</form>
<a href="{{ route('clients.create') }}" class="btn-sakura text-sm text-center shrink-0">+ Add Client</a>
</div>
{{-- Category pills (top-level only) --}}
<div class="flex gap-2 overflow-x-auto pb-1 -mx-4 px-4 lg:mx-0 lg:px-0">
<a href="{{ route('clients.index') }}"
class="shrink-0 text-xs px-3 py-1.5 rounded-full font-semibold transition-colors
{{ ! request('category') ? 'bg-pink-400 text-white' : 'bg-white border border-pink-200 text-gray-600 hover:bg-pink-50' }}">
All
</a>
@foreach($categories as $cat)
<a href="{{ route('clients.index', ['category' => $cat->id, 'status' => request('status')]) }}"
class="shrink-0 text-xs px-3 py-1.5 rounded-full font-semibold transition-colors whitespace-nowrap
{{ request('category') == $cat->id ? 'bg-pink-400 text-white' : 'bg-white border border-pink-200 text-gray-600 hover:bg-pink-50' }}"
style="{{ request('category') == $cat->id ? '' : 'border-color:' . $cat->color }}">
{{ $cat->icon }} {{ $cat->name }}
</a>
@endforeach
</div>
<a href="{{ route('clients.create') }}" class="btn-sakura text-sm text-center">+ Add Client</a>
</div>
{{-- ── Client grid ─────────────────────────────────────────── --}}
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4" id="clients-grid">
@forelse($clients as $client)
<div class="cat-card p-4 flex flex-col gap-3 hover:shadow-md transition-shadow duration-200"
data-name="{{ strtolower($client->name) }}">
data-name="{{ strtolower($client->name) }} {{ strtolower($client->company ?? '') }}">
<div class="flex items-start gap-3">
<div class="w-11 h-11 rounded-2xl flex items-center justify-center text-white font-bold text-base shadow-sm shrink-0"
style="background: linear-gradient(135deg, #FFB7C5, #E8D5F5);">
style="background: linear-gradient(135deg, {{ $client->category?->color ?? '#FFB7C5' }}, #E8D5F5);">
{{ $client->initials }}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<h3 class="font-bold text-gray-800 text-sm truncate">{{ $client->name }}</h3>
<span class="badge {{ $client->status->badgeClass() }} text-xs shrink-0">{{ $client->status->label() }}</span>
<span class="badge {{ $client->status->badgeClass() }} text-xs shrink-0">
{{ $client->status->label() }}
</span>
</div>
@if($client->company)
<div class="text-xs text-gray-400 truncate">{{ $client->company }}</div>
@endif
@if($client->category)
<div class="text-xs mt-0.5" style="color: {{ $client->category->color === '#FDFD96' ? '#8B8000' : $client->category->color }}">
{{ $client->category->icon }} {{ $client->category->name }}
@if($client->category->parent)
<span class="text-gray-300 mx-0.5">·</span>
{{ $client->category->parent->name }}
@endif
</div>
@endif
</div>
</div>
@@ -62,18 +131,28 @@
<div class="flex gap-2 pt-2 border-t border-pink-50">
<a href="{{ route('clients.show', $client) }}" class="btn-sakura text-xs py-1.5 flex-1 text-center">View</a>
<a href="{{ route('clients.edit', $client) }}" class="flex-1 text-center text-xs py-1.5 px-3 rounded-full border border-pink-200 text-gray-600 hover:bg-pink-50">Edit</a>
<form action="{{ route('clients.destroy', $client) }}" method="POST" onsubmit="return confirm('Remove this client?')">
<a href="{{ route('clients.edit', $client) }}"
class="flex-1 text-center text-xs py-1.5 px-3 rounded-full border border-pink-200 text-gray-600 hover:bg-pink-50">
Edit
</a>
<form action="{{ route('clients.destroy', $client) }}" method="POST"
onsubmit="return confirm('Remove this client?')">
@csrf @method('DELETE')
<button class="text-xs py-1.5 px-3 rounded-full border border-red-100 text-red-400 hover:bg-red-50">Del</button>
<button class="text-xs py-1.5 px-3 rounded-full border border-red-100 text-red-400 hover:bg-red-50">
Del
</button>
</form>
</div>
</div>
@empty
<div class="col-span-full text-center py-16 text-gray-400">
<span class="text-5xl block mb-3">🐱</span>
<p class="text-base font-medium">No clients yet!</p>
<a href="{{ route('clients.create') }}" class="btn-sakura mt-4 inline-block">Add your first client</a>
<p class="text-base font-medium">No clients found!</p>
@if(request('category') || request('status'))
<a href="{{ route('clients.index') }}" class="text-sm text-pink-500 mt-2 block">Clear filters</a>
@else
<a href="{{ route('clients.create') }}" class="btn-sakura mt-4 inline-block">Add your first client</a>
@endif
</div>
@endforelse
</div>