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:
@@ -6,39 +6,26 @@ Alpine.plugin(sort);
|
||||
window.Alpine = Alpine;
|
||||
Alpine.start();
|
||||
|
||||
// Meow sound toggle (stored in localStorage)
|
||||
// ── PWA Service Worker ────────────────────────────────────────
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => {/* silently fail */});
|
||||
});
|
||||
}
|
||||
|
||||
// ── KolManager helpers (toast notifications only — no meow) ──
|
||||
window.KolManager = {
|
||||
meowEnabled: localStorage.getItem('meow') !== 'false',
|
||||
|
||||
toggleMeow() {
|
||||
this.meowEnabled = !this.meowEnabled;
|
||||
localStorage.setItem('meow', this.meowEnabled);
|
||||
},
|
||||
|
||||
playMeow() {
|
||||
if (!this.meowEnabled) return;
|
||||
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const o = ctx.createOscillator();
|
||||
const g = ctx.createGain();
|
||||
o.connect(g);
|
||||
g.connect(ctx.destination);
|
||||
o.type = 'sine';
|
||||
o.frequency.setValueAtTime(880, ctx.currentTime);
|
||||
o.frequency.exponentialRampToValueAtTime(1200, ctx.currentTime + 0.05);
|
||||
o.frequency.exponentialRampToValueAtTime(900, ctx.currentTime + 0.15);
|
||||
g.gain.setValueAtTime(0.3, ctx.currentTime);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
|
||||
o.start(ctx.currentTime);
|
||||
o.stop(ctx.currentTime + 0.3);
|
||||
},
|
||||
|
||||
notify(msg) {
|
||||
this.playMeow();
|
||||
const el = document.createElement('div');
|
||||
el.className = 'fixed top-4 right-4 z-50 cat-card px-5 py-3 flex items-center gap-2 shadow-lg border border-pink-200 animate-bounce';
|
||||
el.className = 'fixed top-4 right-4 z-50 cat-card px-5 py-3 flex items-center gap-2 shadow-lg border border-pink-200';
|
||||
el.style.cssText = 'animation: slideIn .3s ease';
|
||||
el.innerHTML = `<span class="text-xl">🐱</span><span class="text-sm font-medium text-pink-700">${msg}</span>`;
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => el.remove(), 3500);
|
||||
setTimeout(() => {
|
||||
el.style.opacity = '0';
|
||||
el.style.transition = 'opacity .3s';
|
||||
setTimeout(() => el.remove(), 300);
|
||||
}, 3200);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,7 +33,6 @@ window.KolManager = {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const flash = document.getElementById('flash-message');
|
||||
if (flash) {
|
||||
window.KolManager.playMeow();
|
||||
setTimeout(() => {
|
||||
flash.style.opacity = '0';
|
||||
flash.style.transform = 'translateY(-10px)';
|
||||
|
||||
@@ -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' : '' }}>
|
||||
↳ {{ $child->icon }} {{ $child->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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' : '' }}>
|
||||
↳ {{ $child->icon }} {{ $child->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -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' : '' }}>
|
||||
↳ {{ $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>
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta name="theme-color" content="#FFB7C5">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="{{ config('app.name') }}">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="apple-touch-icon" href="/icons/icon-192.png">
|
||||
<title>{{ config('app.name') }} — @yield('title', 'Dashboard')</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="bg-cream font-sans antialiased" x-data="{ sidebarOpen: false }">
|
||||
<body class="bg-cream font-sans antialiased" x-data="appShell()">
|
||||
|
||||
{{-- ── Backdrop (mobile only) ─────────────────────────────────── --}}
|
||||
<div
|
||||
@@ -33,20 +39,16 @@
|
||||
transition-transform duration-300 ease-in-out
|
||||
lg:translate-x-0"
|
||||
>
|
||||
{{-- Logo / Close trigger --}}
|
||||
{{-- Logo --}}
|
||||
<div class="flex items-center justify-between gap-3 mb-8 px-2">
|
||||
<button
|
||||
@click="sidebarOpen = false"
|
||||
class="flex items-center gap-3 group focus:outline-none"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<a href="{{ route('dashboard') }}" @click="sidebarOpen = false"
|
||||
class="flex items-center gap-3 group focus:outline-none">
|
||||
<span class="text-3xl transition-transform duration-200 group-hover:scale-110 group-active:scale-95 select-none">🐱</span>
|
||||
<div class="text-left">
|
||||
<div class="text-lg font-extrabold text-pink-500 leading-tight">KOL Manager</div>
|
||||
<div class="text-xs text-gray-400">Your Purr-fect Assistant</div>
|
||||
</div>
|
||||
</button>
|
||||
{{-- X button — mobile only --}}
|
||||
</a>
|
||||
<button @click="sidebarOpen = false" class="lg:hidden text-gray-400 hover:text-pink-500 text-xl leading-none" aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
@@ -65,9 +67,6 @@
|
||||
class="nav-item {{ request()->routeIs('schedule.*') ? 'active' : '' }}">
|
||||
<span class="text-lg shrink-0">📅</span>
|
||||
<span class="flex-1">Schedule</span>
|
||||
@if($alertCount > 0)
|
||||
<span class="badge-sakura shrink-0">{{ $alertCount }}</span>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
<a href="{{ route('clients.index') }}"
|
||||
@@ -109,17 +108,15 @@
|
||||
class="nav-item {{ request()->routeIs('files.*') ? 'active' : '' }}">
|
||||
<span class="text-lg shrink-0">📁</span> Files
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
{{-- Meow Toggle --}}
|
||||
<div class="mt-4 px-2 border-t border-pink-50 pt-4">
|
||||
<button onclick="window.KolManager.toggleMeow()"
|
||||
class="flex items-center gap-2 text-sm text-gray-500 hover:text-pink-500 transition-colors w-full">
|
||||
<span class="text-base">🔔</span>
|
||||
<span>Meow Sounds</span>
|
||||
</button>
|
||||
<div class="text-xs text-gray-400 mt-1">Toggle notification sounds</div>
|
||||
</div>
|
||||
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mt-4 mb-1 px-2">Settings</div>
|
||||
|
||||
<a href="{{ route('settings.notifications') }}"
|
||||
@click="sidebarOpen = false"
|
||||
class="nav-item {{ request()->routeIs('settings.*') ? 'active' : '' }}">
|
||||
<span class="text-lg shrink-0">🔔</span> Notifications
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{{-- ── Main wrapper ─────────────────────────────────────────────── --}}
|
||||
@@ -127,33 +124,91 @@
|
||||
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
|
||||
{{-- ── Mobile / Desktop Topbar ─────────────────────────── --}}
|
||||
{{-- ── Topbar ───────────────────────────────────────────── --}}
|
||||
<header class="sticky top-0 z-20 bg-white border-b border-pink-100 px-4 lg:px-8 py-3 lg:py-4 flex items-center gap-3 shadow-sm">
|
||||
|
||||
{{-- Cat icon — hamburger for mobile --}}
|
||||
{{-- Hamburger (mobile) --}}
|
||||
<button
|
||||
@click="sidebarOpen = true"
|
||||
class="lg:hidden text-2xl transition-transform duration-150 active:scale-90 focus:outline-none select-none"
|
||||
aria-label="Open menu"
|
||||
>🐱</button>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
{{-- Page title (hidden when search open) --}}
|
||||
<div class="flex-1 min-w-0" x-show="!searchOpen" x-transition>
|
||||
<h1 class="text-base lg:text-xl font-bold text-gray-800 truncate">@yield('page-title', 'Dashboard')</h1>
|
||||
<p class="text-xs lg:text-sm text-gray-400 hidden sm:block">@yield('page-subtitle', today()->format('l, d F Y'))</p>
|
||||
</div>
|
||||
|
||||
{{-- ── Global Search bar ─────────────────────────── --}}
|
||||
<div class="flex-1 min-w-0" x-show="searchOpen" x-transition style="display:none">
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none">🔍</span>
|
||||
<input
|
||||
id="global-search-input"
|
||||
type="text"
|
||||
placeholder="Search clients, events, invoices…"
|
||||
class="input-soft pl-9 pr-4 py-2 text-sm w-full"
|
||||
x-model="searchQuery"
|
||||
@input.debounce.300ms="doSearch()"
|
||||
@keydown.escape="closeSearch()"
|
||||
x-ref="searchInput"
|
||||
>
|
||||
{{-- Search results dropdown --}}
|
||||
<div
|
||||
x-show="searchResults.length > 0 || (searchQuery.length >= 2 && !searchLoading)"
|
||||
class="absolute top-full left-0 right-0 mt-2 bg-white rounded-2xl shadow-xl border border-pink-100 z-50 max-h-80 overflow-y-auto"
|
||||
@click.outside="searchResults = []"
|
||||
>
|
||||
<template x-if="searchLoading">
|
||||
<div class="px-4 py-3 text-sm text-gray-400 flex items-center gap-2">
|
||||
<span class="animate-spin">🐾</span> Searching…
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!searchLoading && searchResults.length === 0 && searchQuery.length >= 2">
|
||||
<div class="px-4 py-3 text-sm text-gray-400">No results for "<span x-text="searchQuery"></span>"</div>
|
||||
</template>
|
||||
<template x-for="group in searchResults" :key="group.type">
|
||||
<div>
|
||||
<div class="px-4 py-1.5 text-xs font-bold text-gray-400 uppercase tracking-wide bg-pink-50/60 sticky top-0"
|
||||
x-text="group.label"></div>
|
||||
<template x-for="item in group.items" :key="item.url">
|
||||
<a :href="item.url"
|
||||
class="flex items-center gap-3 px-4 py-2.5 hover:bg-pink-50 transition-colors"
|
||||
@click="closeSearch()">
|
||||
<span class="text-lg shrink-0" x-text="item.icon"></span>
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold text-gray-700 truncate" x-text="item.title"></div>
|
||||
<div class="text-xs text-gray-400 truncate" x-text="item.subtitle"></div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="border-t border-pink-50 px-4 py-2">
|
||||
<a :href="'/search?q=' + encodeURIComponent(searchQuery)"
|
||||
class="text-xs text-pink-500 hover:text-pink-700 font-medium flex items-center gap-1"
|
||||
@click="closeSearch()">
|
||||
🔍 See all results for "<span x-text="searchQuery"></span>"
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Right actions --}}
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
@if($alertCount > 0)
|
||||
<a href="{{ route('schedule.index') }}"
|
||||
class="hidden sm:flex items-center gap-1.5 bg-pink-50 text-pink-600 text-xs px-3 py-1.5 rounded-full border border-pink-200">
|
||||
<span>🔔</span>
|
||||
<span class="font-semibold">{{ $alertCount }} alert{{ $alertCount > 1 ? 's' : '' }}</span>
|
||||
</a>
|
||||
{{-- Mobile: just the badge --}}
|
||||
<a href="{{ route('schedule.index') }}" class="sm:hidden badge-sakura flex items-center gap-1">
|
||||
🔔 {{ $alertCount }}
|
||||
</a>
|
||||
@endif
|
||||
{{-- Search toggle --}}
|
||||
<button
|
||||
@click="toggleSearch()"
|
||||
class="w-8 h-8 lg:w-9 lg:h-9 rounded-full flex items-center justify-center hover:bg-pink-50 transition-colors text-gray-500 hover:text-pink-500"
|
||||
:title="searchOpen ? 'Close search' : 'Search'"
|
||||
aria-label="Toggle search"
|
||||
>
|
||||
<span x-text="searchOpen ? '✕' : '🔍'" class="text-sm lg:text-base"></span>
|
||||
</button>
|
||||
|
||||
{{-- Avatar --}}
|
||||
<div class="w-8 h-8 lg:w-9 lg:h-9 rounded-full flex items-center justify-center text-white font-bold text-xs lg:text-sm shadow-sm"
|
||||
style="background: linear-gradient(135deg, #FFB7C5, #E8D5F5);">
|
||||
KOL
|
||||
@@ -185,7 +240,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@stack('scripts')
|
||||
|
||||
<script>
|
||||
function appShell() {
|
||||
return {
|
||||
sidebarOpen: false,
|
||||
searchOpen: false,
|
||||
searchQuery: '',
|
||||
searchResults: [],
|
||||
searchLoading: false,
|
||||
_searchTimer: null,
|
||||
|
||||
toggleSearch() {
|
||||
this.searchOpen = !this.searchOpen;
|
||||
if (this.searchOpen) {
|
||||
this.$nextTick(() => this.$refs.searchInput?.focus());
|
||||
} else {
|
||||
this.searchQuery = '';
|
||||
this.searchResults = [];
|
||||
}
|
||||
},
|
||||
|
||||
closeSearch() {
|
||||
this.searchOpen = false;
|
||||
this.searchQuery = '';
|
||||
this.searchResults = [];
|
||||
},
|
||||
|
||||
async doSearch() {
|
||||
if (this.searchQuery.length < 2) { this.searchResults = []; return; }
|
||||
this.searchLoading = true;
|
||||
try {
|
||||
const res = await fetch(`/search/quick?q=${encodeURIComponent(this.searchQuery)}`, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
this.searchResults = await res.json();
|
||||
} catch { this.searchResults = []; }
|
||||
this.searchLoading = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
175
resources/views/schedule/edit.blade.php
Normal file
175
resources/views/schedule/edit.blade.php
Normal file
@@ -0,0 +1,175 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Edit Event')
|
||||
@section('page-title', 'Edit Event 📅')
|
||||
@section('page-subtitle', $event->title)
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="cat-card p-4 lg:p-8">
|
||||
<form action="{{ route('schedule.update', $event) }}" method="POST" id="edit-event-form"
|
||||
class="space-y-5">
|
||||
@csrf @method('PUT')
|
||||
|
||||
{{-- Hidden combined datetime fields --}}
|
||||
<input type="hidden" name="start_time" id="start_time_hidden">
|
||||
<input type="hidden" name="end_time" id="end_time_hidden">
|
||||
|
||||
{{-- Title --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">
|
||||
Title <span class="text-pink-400">*</span>
|
||||
</label>
|
||||
<input type="text" name="title" value="{{ old('title', $event->title) }}"
|
||||
class="input-soft" placeholder="Event title" required>
|
||||
@error('title') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{{-- Type --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Type</label>
|
||||
<select name="event_type" class="input-soft">
|
||||
@foreach(['task' => '📋 Task', 'meeting' => '🤝 Meeting', 'deadline' => '⏰ Deadline', 'posting' => '📸 Posting'] as $val => $label)
|
||||
<option value="{{ $val }}" {{ old('event_type', $event->event_type) === $val ? 'selected' : '' }}>
|
||||
{{ $label }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Status --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Status</label>
|
||||
<select name="status" class="input-soft">
|
||||
@foreach($statuses as $s)
|
||||
<option value="{{ $s->value }}"
|
||||
{{ old('status', $event->status->value) === $s->value ? 'selected' : '' }}>
|
||||
{{ $s->label() }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Client --}}
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Client</label>
|
||||
<select name="client_id" class="input-soft">
|
||||
<option value="">— None —</option>
|
||||
@foreach($clients as $client)
|
||||
<option value="{{ $client->id }}"
|
||||
{{ old('client_id', $event->client_id) == $client->id ? 'selected' : '' }}>
|
||||
{{ $client->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Start Date --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">
|
||||
Start Date <span class="text-pink-400">*</span>
|
||||
</label>
|
||||
<input type="date" id="start_date" class="input-soft"
|
||||
value="{{ old('_start_date', $event->start_time->format('Y-m-d')) }}" required>
|
||||
</div>
|
||||
|
||||
{{-- Start Time --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Start Time</label>
|
||||
<input type="time" id="start_time_input" class="input-soft"
|
||||
value="{{ old('_start_time', $event->is_all_day ? '00:00' : $event->start_time->format('H:i')) }}">
|
||||
</div>
|
||||
|
||||
{{-- End Date --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">End Date</label>
|
||||
<input type="date" id="end_date" class="input-soft"
|
||||
value="{{ old('_end_date', $event->end_time?->format('Y-m-d') ?? '') }}">
|
||||
</div>
|
||||
|
||||
{{-- End Time --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">End Time</label>
|
||||
<input type="time" id="end_time_input" class="input-soft"
|
||||
value="{{ old('_end_time', $event->end_time?->format('H:i') ?? '18:00') }}">
|
||||
</div>
|
||||
|
||||
{{-- Location --}}
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Location</label>
|
||||
<input type="text" name="location" value="{{ old('location', $event->location) }}"
|
||||
class="input-soft" placeholder="Online / venue address">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Description --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Description</label>
|
||||
<textarea name="description" rows="2" class="input-soft resize-none"
|
||||
placeholder="Brief description…">{{ old('description', $event->description) }}</textarea>
|
||||
</div>
|
||||
|
||||
{{-- Color --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Color</label>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
@foreach(['#FFB7C5','#B5EAD7','#FDFD96','#E8D5F5','#FFD5B8','#AEC6CF'] as $hex)
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="color" value="{{ $hex }}" class="sr-only peer"
|
||||
{{ old('color', $event->color) === $hex ? 'checked' : '' }}>
|
||||
<span class="w-8 h-8 rounded-full block ring-2 ring-transparent peer-checked:ring-gray-500 peer-checked:ring-offset-1 transition-all"
|
||||
style="background:{{ $hex }};"></span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Priority checkbox --}}
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" name="is_priority" value="1" id="priority" class="w-4 h-4 accent-pink-400"
|
||||
{{ old('is_priority', $event->is_priority) ? 'checked' : '' }}>
|
||||
<label for="priority" class="text-sm text-gray-600 cursor-pointer">⭐ Mark as Priority</label>
|
||||
</div>
|
||||
|
||||
{{-- Notes --}}
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Notes</label>
|
||||
<textarea name="notes" rows="3" class="input-soft resize-none"
|
||||
placeholder="Additional notes…">{{ old('notes', $event->notes) }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-3 pt-1">
|
||||
<button type="submit" class="btn-sakura flex-1 py-3">💾 Save Changes</button>
|
||||
<a href="{{ route('schedule.index') }}"
|
||||
class="flex-1 py-3 text-center rounded-full border border-pink-200 text-gray-600 hover:bg-pink-50 font-semibold text-sm">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('edit-event-form').addEventListener('submit', function (e) {
|
||||
const startDate = document.getElementById('start_date').value;
|
||||
const startTime = document.getElementById('start_time_input').value || '00:00';
|
||||
const endDate = document.getElementById('end_date').value;
|
||||
const endTime = document.getElementById('end_time_input').value || '00:00';
|
||||
|
||||
if (!startDate) {
|
||||
e.preventDefault();
|
||||
alert('Please select a start date.');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('start_time_hidden').value = startDate + 'T' + startTime;
|
||||
|
||||
if (endDate) {
|
||||
document.getElementById('end_time_hidden').value = endDate + 'T' + endTime;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
62
resources/views/search/index.blade.php
Normal file
62
resources/views/search/index.blade.php
Normal file
@@ -0,0 +1,62 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Search')
|
||||
@section('page-title', '🔍 Search Results')
|
||||
@section('page-subtitle', $q ? "Results for \"" . e($q) . "\"" : 'Enter a search term')
|
||||
|
||||
@section('content')
|
||||
|
||||
<form method="GET" action="{{ route('search.index') }}" class="mb-6">
|
||||
<div class="relative max-w-xl">
|
||||
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">🔍</span>
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
value="{{ e($q) }}"
|
||||
placeholder="Search clients, events, invoices, content…"
|
||||
class="input-soft pl-11 py-3 text-base w-full"
|
||||
autofocus
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if(! $q)
|
||||
<div class="text-center py-20 text-gray-400">
|
||||
<span class="text-5xl block mb-3">🔍</span>
|
||||
<p class="text-base font-medium">Start typing to search across everything</p>
|
||||
<p class="text-sm mt-1">Clients, events, invoices, content briefs, rate cards</p>
|
||||
</div>
|
||||
@elseif(empty($results))
|
||||
<div class="text-center py-20 text-gray-400">
|
||||
<span class="text-5xl block mb-3">😿</span>
|
||||
<p class="text-base font-medium">No results for "{{ e($q) }}"</p>
|
||||
<p class="text-sm mt-1">Try a different keyword</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-6">
|
||||
@foreach($results as $group)
|
||||
<div class="cat-card overflow-hidden">
|
||||
<div class="px-5 py-3 bg-pink-50/60 border-b border-pink-100">
|
||||
<h2 class="text-sm font-bold text-gray-600">{{ $group['label'] }}</h2>
|
||||
</div>
|
||||
<div class="divide-y divide-pink-50">
|
||||
@foreach($group['items'] as $item)
|
||||
<a href="{{ $item['url'] }}"
|
||||
class="flex items-center gap-4 px-5 py-3 hover:bg-pink-50/40 transition-colors">
|
||||
<span class="text-2xl shrink-0">{{ $item['icon'] }}</span>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-gray-800 text-sm truncate">{{ $item['title'] }}</div>
|
||||
@if($item['subtitle'])
|
||||
<div class="text-xs text-gray-400 truncate mt-0.5">{{ $item['subtitle'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<span class="ml-auto text-gray-300 text-sm shrink-0">→</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
263
resources/views/settings/notifications.blade.php
Normal file
263
resources/views/settings/notifications.blade.php
Normal 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
|
||||
Reference in New Issue
Block a user