refactor: update schedule filtering logic, adjust client page title, and reformat client index view components.
This commit is contained in:
@@ -1,187 +1,189 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Clients')
|
||||
@section('page-title', 'Clients / CRM 👥')
|
||||
@section('page-title', 'Clients👥')
|
||||
@section('page-subtitle', 'Manage your brand partners and contacts')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- ── Toolbar + collapsible filter ───────────────────────── --}}
|
||||
<div x-data="{ open: {{ request('category') || request('status') ? 'true' : 'false' }} }">
|
||||
{{-- ── Toolbar + collapsible filter ───────────────────────── --}}
|
||||
<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 gap-2">
|
||||
<a href="{{ route('clients.create') }}" class="btn-sakura text-sm">+ Add</a>
|
||||
@if(request('category') || request('status'))
|
||||
<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">
|
||||
✕ Clear filters
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<button @click="open = !open"
|
||||
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'">
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 4h18M7 10h10M11 16h2"/>
|
||||
</svg>
|
||||
Filter
|
||||
@if(request('category') || request('status'))
|
||||
<span class="w-2 h-2 rounded-full bg-pink-400 inline-block ml-0.5"></span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- ── 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 class="flex items-center justify-between gap-2 mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('clients.create') }}" class="btn-sakura text-sm">+ Add</a>
|
||||
@if(request('category') || request('status'))
|
||||
<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">
|
||||
✕ Clear filters
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<button @click="open = !open"
|
||||
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'">
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 4h18M7 10h10M11 16h2" />
|
||||
</svg>
|
||||
Filter
|
||||
@if(request('category') || request('status'))
|
||||
<span class="w-2 h-2 rounded-full bg-pink-400 inline-block ml-0.5"></span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Category dropdown --}}
|
||||
<select name="category" class="input-soft text-sm py-2 sm:w-48 shrink-0"
|
||||
onchange="this.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->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">
|
||||
{{-- ── 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>
|
||||
@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>
|
||||
@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
|
||||
|
||||
{{-- Category dropdown --}}
|
||||
<select name="category" class="input-soft text-sm py-2 sm:w-48 shrink-0" onchange="this.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->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>
|
||||
@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>
|
||||
@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 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>
|
||||
@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>
|
||||
|
||||
<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>
|
||||
{{ $clients->links() }}
|
||||
|
||||
@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>
|
||||
@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
|
||||
@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
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<a href="{{ route('clients.index') }}" @click="sidebarOpen = false"
|
||||
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 href="{{ route('client-categories.index') }}" @click="sidebarOpen = false"
|
||||
|
||||
@@ -13,11 +13,17 @@
|
||||
|
||||
{{-- ── Upcoming Alert Strip ─────────────────────────────────── --}}
|
||||
@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
|
||||
@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">
|
||||
@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"
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user