refactor: update schedule filtering logic, adjust client page title, and reformat client index view components.

This commit is contained in:
I-Fun
2026-04-05 08:25:13 +07:00
parent 1e7a35ad4f
commit 5656dc1f3a
3 changed files with 179 additions and 171 deletions

View File

@@ -1,187 +1,189 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Clients') @section('title', 'Clients')
@section('page-title', 'Clients / CRM 👥') @section('page-title', 'Clients👥')
@section('page-subtitle', 'Manage your brand partners and contacts') @section('page-subtitle', 'Manage your brand partners and contacts')
@section('content') @section('content')
{{-- ── Toolbar + collapsible filter ───────────────────────── --}} {{-- ── Toolbar + collapsible filter ───────────────────────── --}}
<div x-data="{ open: {{ request('category') || request('status') ? 'true' : 'false' }} }"> <div x-data="{ open: {{ request('category') || request('status') ? 'true' : 'false' }} }">
<div class="flex items-center justify-between gap-2 mb-3"> <div class="flex items-center justify-between gap-2 mb-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="{{ route('clients.create') }}" class="btn-sakura text-sm">+ Add</a> <a href="{{ route('clients.create') }}" class="btn-sakura text-sm">+ Add</a>
@if(request('category') || request('status')) @if(request('category') || request('status'))
<a href="{{ route('clients.index') }}" <a href="{{ route('clients.index') }}"
class="text-xs px-3 py-2 rounded-full border border-gray-200 text-gray-500 hover:bg-gray-50 flex items-center gap-1"> class="text-xs px-3 py-2 rounded-full border border-gray-200 text-gray-500 hover:bg-gray-50 flex items-center gap-1">
✕ Clear filters ✕ Clear filters
</a> </a>
@endif @endif
</div> </div>
<button @click="open = !open" <button @click="open = !open"
class="flex items-center gap-1.5 text-xs px-3 py-2 rounded-full border transition-colors" class="flex items-center gap-1.5 text-xs px-3 py-2 rounded-full border transition-colors"
:class="open ? 'border-pink-300 bg-pink-50 text-pink-600' : 'border-gray-200 text-gray-500 hover:bg-gray-50'"> :class="open ? 'border-pink-300 bg-pink-50 text-pink-600' : 'border-gray-200 text-gray-500 hover:bg-gray-50'">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" d="M3 4h18M7 10h10M11 16h2"/> stroke="currentColor" stroke-width="2">
</svg> <path stroke-linecap="round" stroke-linejoin="round" d="M3 4h18M7 10h10M11 16h2" />
Filter </svg>
@if(request('category') || request('status')) Filter
<span class="w-2 h-2 rounded-full bg-pink-400 inline-block ml-0.5"></span> @if(request('category') || request('status'))
@endif <span class="w-2 h-2 rounded-full bg-pink-400 inline-block ml-0.5"></span>
</button> @endif
</div> </button>
{{-- ── Filters panel (collapsible) ────────────────────────── --}}
<div x-show="open" x-transition.duration.150ms>
<form method="GET" action="{{ route('clients.index') }}" id="filter-form" class="cat-card p-3 mb-4">
{{-- Row 1: search + dropdowns --}}
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
{{-- Search (fills remaining space on desktop) --}}
<div class="relative flex-1 min-w-0">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none">🔍</span>
<input type="text" placeholder="Filter by name…" class="input-soft pl-9 text-sm py-2 w-full"
oninput="filterClients(this.value)" id="search-input">
</div> </div>
{{-- Category dropdown --}} {{-- ── Filters panel (collapsible) ────────────────────────── --}}
<select name="category" class="input-soft text-sm py-2 sm:w-48 shrink-0" <div x-show="open" x-transition.duration.150ms>
onchange="this.form.submit()"> <form method="GET" action="{{ route('clients.index') }}" id="filter-form" class="cat-card p-3 mb-4">
<option value="">All Categories</option> {{-- Row 1: search + dropdowns --}}
@foreach($categories as $parent) <div class="flex flex-col sm:flex-row sm:items-center gap-2">
<option value="{{ $parent->id }}" {{-- Search (fills remaining space on desktop) --}}
{{ request('category') == $parent->id ? 'selected' : '' }}> <div class="relative flex-1 min-w-0">
{{ $parent->icon }} {{ $parent->name }} <span
</option> class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none">🔍</span>
@foreach($parent->children as $child) <input type="text" placeholder="Filter by name…" class="input-soft pl-9 text-sm py-2 w-full"
<option value="{{ $child->id }}" oninput="filterClients(this.value)" id="search-input">
{{ request('category') == $child->id ? 'selected' : '' }}>
&nbsp;&nbsp;↳ {{ $child->name }}
</option>
@endforeach
@endforeach
</select>
{{-- Status dropdown --}}
<select name="status" class="input-soft text-sm py-2 sm:w-36 shrink-0"
onchange="this.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>
</div>
</form>
</div>
</div>{{-- end x-data --}}
{{-- ── 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) }} {{ strtolower($client->company ?? '') }}">
<div class="flex items-start gap-3">
@if($client->avatar)
<div class="w-11 h-11 rounded-2xl overflow-hidden shrink-0 shadow-sm ring-2 ring-pink-100">
<img src="{{ \Illuminate\Support\Facades\Storage::url($client->avatar) }}"
alt="{{ $client->name }}"
class="w-full h-full object-cover">
</div> </div>
@else
<div class="w-11 h-11 rounded-2xl flex items-center justify-center text-white font-bold text-base shadow-sm shrink-0" {{-- Category dropdown --}}
style="background: linear-gradient(135deg, {{ $client->category?->color ?? '#FFB7C5' }}, #E8D5F5);"> <select name="category" class="input-soft text-sm py-2 sm:w-48 shrink-0" onchange="this.form.submit()">
{{ $client->initials }} <option value="">All Categories</option>
</div> @foreach($categories as $parent)
@endif <option value="{{ $parent->id }}" {{ request('category') == $parent->id ? 'selected' : '' }}>
<div class="flex-1 min-w-0"> {{ $parent->icon }} {{ $parent->name }}
<div class="flex items-center gap-2 flex-wrap"> </option>
<h3 class="font-bold text-gray-800 text-sm truncate">{{ $client->name }}</h3> @foreach($parent->children as $child)
<span class="badge {{ $client->status->badgeClass() }} text-xs shrink-0"> <option value="{{ $child->id }}" {{ request('category') == $child->id ? 'selected' : '' }}>
{{ $client->status->label() }} &nbsp;&nbsp;↳ {{ $child->name }}
</span> </option>
</div> @endforeach
@if($client->company) @endforeach
<div class="text-xs text-gray-400 truncate">{{ $client->company }}</div> </select>
@endif
@if($client->category) {{-- Status dropdown --}}
<div class="text-xs mt-0.5" style="color: {{ $client->category->color === '#FDFD96' ? '#8B8000' : $client->category->color }}"> <select name="status" class="input-soft text-sm py-2 sm:w-36 shrink-0" onchange="this.form.submit()">
{{ $client->category->icon }} {{ $client->category->name }} <option value="">All Statuses</option>
@if($client->category->parent) @foreach($statuses as $s)
<span class="text-gray-300 mx-0.5">·</span> <option value="{{ $s->value }}" {{ request('status') === $s->value ? 'selected' : '' }}>
{{ $client->category->parent->name }} {{ $s->label() }}
@endif </option>
@endforeach
</select>
</div>
</form>
</div>
</div>{{-- end x-data --}}
{{-- ── 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) }} {{ strtolower($client->company ?? '') }}">
<div class="flex items-start gap-3">
@if($client->avatar)
<div class="w-11 h-11 rounded-2xl overflow-hidden shrink-0 shadow-sm ring-2 ring-pink-100">
<img src="{{ \Illuminate\Support\Facades\Storage::url($client->avatar) }}" alt="{{ $client->name }}"
class="w-full h-full object-cover">
</div>
@else
<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, {{ $client->category?->color ?? '#FFB7C5' }}, #E8D5F5);">
{{ $client->initials }}
</div> </div>
@endif @endif
<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>
</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>
<div class="flex flex-wrap gap-1.5">
@if($client->ig_handle)
<span class="text-xs bg-pink-50 text-pink-600 px-2 py-0.5 rounded-full">📸 {{ $client->ig_handle }}</span>
@endif
@if($client->tiktok_handle)
<span class="text-xs bg-purple-50 text-purple-600 px-2 py-0.5 rounded-full">🎵
{{ $client->tiktok_handle }}</span>
@endif
@if($client->youtube_handle)
<span class="text-xs bg-red-50 text-red-600 px-2 py-0.5 rounded-full">▶️
{{ $client->youtube_handle }}</span>
@endif
</div>
<div class="flex gap-3 text-xs text-gray-500">
<span>📅 {{ $client->events_count }}</span>
<span>🧾 {{ $client->invoices_count }}</span>
<span>🎬 {{ $client->content_briefs_count }}</span>
</div>
@if($client->email || $client->city)
<div class="text-xs text-gray-400 space-y-0.5">
@if($client->email)
<div class="truncate">✉️ {{ $client->email }}</div> @endif
@if($client->city)
<div>📍 {{ $client->city }}{{ $client->country ? ', ' . $client->country : '' }}</div> @endif
</div>
@endif
<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>
{{-- Share button disabled --}}
<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">
✕
</button>
</form>
</div> </div>
</div> </div>
@empty
<div class="flex flex-wrap gap-1.5"> <div class="col-span-full text-center py-16 text-gray-400">
@if($client->ig_handle) <span class="text-5xl block mb-3">🐱</span>
<span class="text-xs bg-pink-50 text-pink-600 px-2 py-0.5 rounded-full">📸 {{ $client->ig_handle }}</span> <p class="text-base font-medium">No clients found!</p>
@endif @if(request('category') || request('status'))
@if($client->tiktok_handle) <a href="{{ route('clients.index') }}" class="text-sm text-pink-500 mt-2 block">Clear filters</a>
<span class="text-xs bg-purple-50 text-purple-600 px-2 py-0.5 rounded-full">🎵 {{ $client->tiktok_handle }}</span> @else
@endif <a href="{{ route('clients.create') }}" class="btn-sakura mt-4 inline-block">Add your first client</a>
@if($client->youtube_handle)
<span class="text-xs bg-red-50 text-red-600 px-2 py-0.5 rounded-full">▶️ {{ $client->youtube_handle }}</span>
@endif @endif
</div> </div>
@endforelse
</div>
<div class="flex gap-3 text-xs text-gray-500"> {{ $clients->links() }}
<span>📅 {{ $client->events_count }}</span>
<span>🧾 {{ $client->invoices_count }}</span>
<span>🎬 {{ $client->content_briefs_count }}</span>
</div>
@if($client->email || $client->city) @push('scripts')
<div class="text-xs text-gray-400 space-y-0.5"> <script>
@if($client->email) <div class="truncate">✉️ {{ $client->email }}</div> @endif function filterClients(query) {
@if($client->city) <div>📍 {{ $client->city }}{{ $client->country ? ', '.$client->country : '' }}</div> @endif document.querySelectorAll('#clients-grid > div[data-name]').forEach(card => {
</div> card.style.display = card.dataset.name.includes(query.toLowerCase()) ? '' : 'none';
@endif });
}
<div class="flex gap-2 pt-2 border-t border-pink-50"> </script>
<a href="{{ route('clients.show', $client) }}" class="btn-sakura text-xs py-1.5 flex-1 text-center">View</a> @endpush
<a href="{{ route('clients.edit', $client) }}" @endsection
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>
{{-- Share button disabled --}}
<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">
✕
</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 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>
{{ $clients->links() }}
@push('scripts')
<script>
function filterClients(query) {
document.querySelectorAll('#clients-grid > div[data-name]').forEach(card => {
card.style.display = card.dataset.name.includes(query.toLowerCase()) ? '' : 'none';
});
}
</script>
@endpush
@endsection

View File

@@ -62,7 +62,7 @@
<a href="{{ route('clients.index') }}" @click="sidebarOpen = false" <a href="{{ route('clients.index') }}" @click="sidebarOpen = false"
class="nav-item {{ request()->routeIs('clients.*') ? 'active' : '' }}"> class="nav-item {{ request()->routeIs('clients.*') ? 'active' : '' }}">
<span class="text-lg shrink-0">👥</span> Clients / CRM <span class="text-lg shrink-0">👥</span> Clients
</a> </a>
<a href="{{ route('client-categories.index') }}" @click="sidebarOpen = false" <a href="{{ route('client-categories.index') }}" @click="sidebarOpen = false"

View File

@@ -13,11 +13,17 @@
{{-- ── Upcoming Alert Strip ─────────────────────────────────── --}} {{-- ── Upcoming Alert Strip ─────────────────────────────────── --}}
@php @php
$upcomingList = $events->filter(fn($e) => $e->start_time->isFuture() && $e->start_time->diffInHours(now()) <= 24); $startOfMonth = now()->startOfMonth();
$endOfMonth = now()->endOfMonth();
$upcomingList = $events->filter(function($e) use ($startOfMonth, $endOfMonth) {
$start = $e->start_time;
$end = $e->end_time ?: $e->start_time;
return $start->lte($endOfMonth) && $end->gte($startOfMonth);
});
@endphp @endphp
@if($upcomingList->isNotEmpty()) @if($upcomingList->isNotEmpty())
<div class="flex gap-2 overflow-x-auto pb-2 mb-4 -mx-4 px-4 lg:mx-0 lg:px-0"> <div class="flex gap-2 overflow-x-auto pb-2 mb-4 -mx-4 px-4 lg:mx-0 lg:px-0">
@foreach($upcomingList->take(6) as $event) @foreach($upcomingList as $event)
<div class="shrink-0 flex items-center gap-2 bg-white rounded-2xl border border-pink-100 px-3 py-2 shadow-sm cursor-pointer hover:border-pink-300 transition-colors" <div class="shrink-0 flex items-center gap-2 bg-white rounded-2xl border border-pink-100 px-3 py-2 shadow-sm cursor-pointer hover:border-pink-300 transition-colors"
onclick="openAlertEventModal({{ json_encode(['id' => $event->id, 'title' => $event->title, 'type' => $event->event_type, 'status' => $event->status->value, 'client' => $event->client?->name, 'start' => $event->start_time->format('d M Y H:i'), 'end' => $event->end_time?->format('d M Y H:i'), 'location' => $event->location, 'description' => $event->description, 'notes' => $event->notes, 'has_contract' => $event->has_contract, 'has_content' => $event->has_content]) }})"> onclick="openAlertEventModal({{ json_encode(['id' => $event->id, 'title' => $event->title, 'type' => $event->event_type, 'status' => $event->status->value, 'client' => $event->client?->name, 'start' => $event->start_time->format('d M Y H:i'), 'end' => $event->end_time?->format('d M Y H:i'), 'location' => $event->location, 'description' => $event->description, 'notes' => $event->notes, 'has_contract' => $event->has_contract, 'has_content' => $event->has_content]) }})">
<div class="w-1.5 h-8 rounded-full shrink-0" style="background:{{ $event->color }}"></div> <div class="w-1.5 h-8 rounded-full shrink-0" style="background:{{ $event->color }}"></div>