<template x-data> inside <tbody> is inert — browsers don't render its children. Replace with per-member <tbody x-data> (multiple tbody is valid HTML). Also replace x-collapse on <tr> with x-transition since table rows don't support max-height/overflow-hidden. UAT: all 7 tests passed via Playwright automation. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
498 lines
34 KiB
PHP
498 lines
34 KiB
PHP
<x-app-layout>
|
||
<x-slot name="header">
|
||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||
會員管理
|
||
</h2>
|
||
</x-slot>
|
||
|
||
@push('styles')
|
||
<style>[x-cloak] { display: none !important; }</style>
|
||
@endpush
|
||
|
||
<div class="py-12">
|
||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||
<div class="px-4 py-5 sm:p-6">
|
||
<form method="GET" action="{{ route('admin.members.index') }}" class="mb-4 space-y-4" role="search" aria-label="搜尋和篩選會員">
|
||
<div>
|
||
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
依姓名、電子郵件、電話、Line ID 或身分證號搜尋
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="search"
|
||
id="search"
|
||
value="{{ $filters['search'] ?? '' }}"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||
placeholder="輸入搜尋關鍵字..."
|
||
>
|
||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||
在姓名、電子郵件、電話號碼、Line ID 和身分證號中搜尋
|
||
</p>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||
<div>
|
||
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
會員資格狀態
|
||
</label>
|
||
<select
|
||
id="status"
|
||
name="status"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||
>
|
||
<option value="">所有</option>
|
||
<option value="active" @selected(($filters['status'] ?? '') === 'active')>使用中</option>
|
||
<option value="expired" @selected(($filters['status'] ?? '') === 'expired')>已過期</option>
|
||
<option value="expiring_soon" @selected(($filters['status'] ?? '') === 'expiring_soon')>即將到期(30天內)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="payment_status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
付款狀態
|
||
</label>
|
||
<select
|
||
id="payment_status"
|
||
name="payment_status"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||
>
|
||
<option value="">所有</option>
|
||
<option value="has_payments" @selected(($filters['payment_status'] ?? '') === 'has_payments')>有付款記錄</option>
|
||
<option value="no_payments" @selected(($filters['payment_status'] ?? '') === 'no_payments')>無付款記錄</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
日期範圍
|
||
</label>
|
||
<button
|
||
type="button"
|
||
onclick="document.getElementById('dateFilters').classList.toggle('hidden')"
|
||
class="mt-1 inline-flex w-full items-center justify-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
|
||
>
|
||
切換日期篩選
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="dateFilters" class="{{ (($filters['started_from'] ?? '') || ($filters['started_to'] ?? '')) ? '' : 'hidden' }} grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
<div>
|
||
<label for="started_from" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
加入起始
|
||
</label>
|
||
<input
|
||
type="date"
|
||
name="started_from"
|
||
id="started_from"
|
||
value="{{ $filters['started_from'] ?? '' }}"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||
>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="started_to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
加入結束
|
||
</label>
|
||
<input
|
||
type="date"
|
||
name="started_to"
|
||
id="started_to"
|
||
value="{{ $filters['started_to'] ?? '' }}"
|
||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap items-center gap-2 justify-between">
|
||
<div class="flex gap-2">
|
||
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||
套用篩選
|
||
</button>
|
||
<a href="{{ route('admin.members.export', request()->only('search','status')) }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||
匯出CSV
|
||
</a>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<a href="{{ route('admin.members.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-green-600 dark:bg-green-500 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 dark:hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 dark:focus:ring-green-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||
新增會員
|
||
</a>
|
||
<a href="{{ route('admin.members.import-form') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||
匯入CSV
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- Batch Actions Toolbar (Hidden by default) -->
|
||
<div id="batchActions" class="hidden mb-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600 flex items-center justify-between">
|
||
<div class="flex items-center space-x-4">
|
||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||
<span id="selectedCount">0</span> 已選擇
|
||
</span>
|
||
|
||
<!-- Batch Delete -->
|
||
<form id="batchDeleteForm" action="{{ route('admin.members.batch-destroy') }}" method="POST" onsubmit="return confirm('您確定要刪除選取的會員嗎?');">
|
||
@csrf
|
||
<div id="batchDeleteInputs"></div>
|
||
<button type="submit" class="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 text-sm font-medium">
|
||
刪除所選
|
||
</button>
|
||
</form>
|
||
|
||
<span class="text-gray-300 dark:text-gray-500">|</span>
|
||
|
||
<!-- Batch Update Status -->
|
||
<form id="batchStatusForm" action="{{ route('admin.members.batch-update-status') }}" method="POST" class="flex items-center space-x-2">
|
||
@csrf
|
||
<div id="batchStatusInputs"></div>
|
||
<select name="status" required class="text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-md shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600">
|
||
<option value="">設定狀態...</option>
|
||
<option value="active">活躍</option>
|
||
<option value="pending">待審核</option>
|
||
<option value="expired">已過期</option>
|
||
<option value="suspended">已停權</option>
|
||
</select>
|
||
<button type="submit" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 text-sm font-medium">
|
||
更新
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-4 overflow-x-auto">
|
||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700" role="table">
|
||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||
<tr>
|
||
<th scope="col" class="px-4 py-3 text-left">
|
||
<input type="checkbox" id="selectAll" class="rounded border-gray-300 dark:border-gray-600 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-800">
|
||
</th>
|
||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||
名稱
|
||
</th>
|
||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||
電子郵件
|
||
</th>
|
||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||
角色
|
||
</th>
|
||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||
會員資格到期
|
||
</th>
|
||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||
狀態
|
||
</th>
|
||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||
備忘錄
|
||
</th>
|
||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||
<span class="sr-only">操作</span>
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
@forelse ($members as $member)
|
||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800" x-data="{
|
||
noteFormOpen: false,
|
||
noteContent: '',
|
||
isSubmitting: false,
|
||
errors: {},
|
||
noteCount: {{ $member->notes_count ?? 0 }},
|
||
historyOpen: false,
|
||
notes: [],
|
||
notesLoaded: false,
|
||
isLoadingNotes: false,
|
||
searchQuery: '',
|
||
async submitNote() {
|
||
this.isSubmitting = true;
|
||
this.errors = {};
|
||
try {
|
||
const response = await axios.post('{{ route('admin.members.notes.store', $member) }}', {
|
||
content: this.noteContent
|
||
});
|
||
this.noteCount++;
|
||
this.noteContent = '';
|
||
this.noteFormOpen = false;
|
||
if (this.notesLoaded) {
|
||
this.notes.unshift(response.data.note);
|
||
}
|
||
} catch (error) {
|
||
if (error.response && error.response.status === 422) {
|
||
this.errors = error.response.data.errors || {};
|
||
}
|
||
} finally {
|
||
this.isSubmitting = false;
|
||
}
|
||
},
|
||
toggleHistory() {
|
||
this.historyOpen = !this.historyOpen;
|
||
if (!this.historyOpen) {
|
||
this.searchQuery = '';
|
||
}
|
||
if (this.historyOpen && !this.notesLoaded) {
|
||
this.loadNotes();
|
||
}
|
||
},
|
||
async loadNotes() {
|
||
this.isLoadingNotes = true;
|
||
try {
|
||
const response = await axios.get('{{ route("admin.members.notes.index", $member) }}');
|
||
this.notes = response.data.notes;
|
||
this.notesLoaded = true;
|
||
} catch (error) {
|
||
console.error('Failed to load notes:', error);
|
||
} finally {
|
||
this.isLoadingNotes = false;
|
||
}
|
||
},
|
||
get filteredNotes() {
|
||
if (!this.searchQuery.trim()) return this.notes;
|
||
const query = this.searchQuery.toLowerCase();
|
||
return this.notes.filter(note =>
|
||
note.content.toLowerCase().includes(query) ||
|
||
note.author.name.toLowerCase().includes(query)
|
||
);
|
||
},
|
||
formatDateTime(dateString) {
|
||
const date = new Date(dateString);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
return year + '年' + month + '月' + day + '日 ' + hours + ':' + minutes;
|
||
}
|
||
}">
|
||
<tr>
|
||
<td class="px-4 py-3">
|
||
<input type="checkbox" name="selected_ids[]" value="{{ $member->id }}" class="member-checkbox rounded border-gray-300 dark:border-gray-600 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-700">
|
||
</td>
|
||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||
{{ $member->full_name }}
|
||
</td>
|
||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||
{{ $member->email }}
|
||
</td>
|
||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||
@if($member->user && $member->user->roles->isNotEmpty())
|
||
<div class="flex flex-wrap gap-1">
|
||
@foreach($member->user->roles as $role)
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200">
|
||
{{ $role->name }}
|
||
</span>
|
||
@endforeach
|
||
</div>
|
||
@else
|
||
<span class="text-gray-500 dark:text-gray-400">-</span>
|
||
@endif
|
||
</td>
|
||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||
@if ($member->membership_expires_at)
|
||
{{ $member->membership_expires_at->toDateString() }}
|
||
@else
|
||
<span class="text-gray-500 dark:text-gray-400">未設定</span>
|
||
@endif
|
||
</td>
|
||
<td class="whitespace-nowrap px-4 py-3 text-sm">
|
||
<span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 {{ $member->membership_status_badge }}">
|
||
{{ $member->membership_status_label }}
|
||
</span>
|
||
</td>
|
||
<td class="px-4 py-3 text-sm">
|
||
<div class="flex items-center gap-2">
|
||
<!-- Note count badge -->
|
||
<button @click="toggleHistory()"
|
||
type="button"
|
||
:aria-expanded="historyOpen.toString()"
|
||
:aria-controls="'notes-panel-{{ $member->id }}'"
|
||
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 hover:bg-blue-200 dark:hover:bg-blue-800 cursor-pointer transition-colors">
|
||
<span x-text="noteCount"></span>
|
||
</button>
|
||
<!-- Toggle button -->
|
||
<button
|
||
@click="noteFormOpen = !noteFormOpen"
|
||
type="button"
|
||
class="text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400"
|
||
:title="noteFormOpen ? '收合' : '新增備忘錄'"
|
||
>
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<!-- Inline note form -->
|
||
<div x-show="noteFormOpen" x-cloak
|
||
x-transition:enter="transition ease-out duration-200"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100"
|
||
class="mt-2">
|
||
<form @submit.prevent="submitNote()">
|
||
<textarea
|
||
x-model="noteContent"
|
||
rows="2"
|
||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-500 focus:ring-indigo-500 dark:focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-gray-200"
|
||
:class="{ 'border-red-500 dark:border-red-400': errors.content }"
|
||
placeholder="輸入備忘錄..."
|
||
></textarea>
|
||
<p x-show="errors.content" x-text="errors.content?.[0]"
|
||
class="mt-1 text-xs text-red-600 dark:text-red-400"></p>
|
||
<div class="mt-1 flex justify-end gap-2">
|
||
<button
|
||
type="button"
|
||
@click="noteFormOpen = false; noteContent = ''; errors = {};"
|
||
class="inline-flex items-center rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
:disabled="isSubmitting || noteContent.trim() === ''"
|
||
class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-2.5 py-1.5 text-xs font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<span x-show="!isSubmitting">儲存</span>
|
||
<span x-show="isSubmitting">儲存中...</span>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
<td class="whitespace-nowrap px-4 py-3 text-right text-sm font-medium">
|
||
<a href="{{ route('admin.members.show', $member) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
|
||
檢視
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
<!-- Expansion panel row -->
|
||
<tr x-show="historyOpen"
|
||
x-transition:enter="transition ease-out duration-200"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100"
|
||
x-transition:leave="transition ease-in duration-150"
|
||
x-transition:leave-start="opacity-100"
|
||
x-transition:leave-end="opacity-0"
|
||
:id="'notes-panel-{{ $member->id }}'"
|
||
class="bg-gray-50 dark:bg-gray-900">
|
||
<td colspan="8" class="px-6 py-4">
|
||
<!-- Loading state -->
|
||
<div x-show="isLoadingNotes" class="flex justify-center py-4">
|
||
<svg class="animate-spin h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">載入中...</span>
|
||
</div>
|
||
|
||
<!-- Loaded content -->
|
||
<div x-show="!isLoadingNotes" x-cloak>
|
||
<!-- Search input (only show if notes exist) -->
|
||
<template x-if="notes.length > 0">
|
||
<div class="mb-3">
|
||
<input type="text"
|
||
x-model="searchQuery"
|
||
placeholder="搜尋備忘錄內容或作者..."
|
||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-500 focus:ring-indigo-500 dark:focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-gray-200">
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Notes list -->
|
||
<template x-if="filteredNotes.length > 0">
|
||
<div class="space-y-3 max-h-64 overflow-y-auto">
|
||
<template x-for="note in filteredNotes" :key="note.id">
|
||
<div class="border-l-4 border-blue-500 dark:border-blue-400 pl-3 py-2">
|
||
<p class="text-sm text-gray-900 dark:text-gray-100 whitespace-pre-line" x-text="note.content"></p>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
<span x-text="note.author.name"></span>
|
||
<span class="mx-1">·</span>
|
||
<span x-text="formatDateTime(note.created_at)"></span>
|
||
</p>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Empty state: no notes at all -->
|
||
<template x-if="notesLoaded && notes.length === 0">
|
||
<p class="text-sm text-gray-500 dark:text-gray-400 py-2">尚無備註</p>
|
||
</template>
|
||
|
||
<!-- No search results -->
|
||
<template x-if="notes.length > 0 && filteredNotes.length === 0">
|
||
<p class="text-sm text-gray-500 dark:text-gray-400 py-2">找不到符合的備忘錄</p>
|
||
</template>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
@empty
|
||
<tbody class="bg-white dark:bg-gray-800">
|
||
<tr>
|
||
<td colspan="8" class="px-4 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||
找不到會員。
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
@endforelse
|
||
</table>
|
||
</div>
|
||
|
||
<div class="mt-4">
|
||
{{ $members->links() }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const selectAll = document.getElementById('selectAll');
|
||
const checkboxes = document.querySelectorAll('.member-checkbox');
|
||
const batchActions = document.getElementById('batchActions');
|
||
const selectedCount = document.getElementById('selectedCount');
|
||
const batchDeleteInputs = document.getElementById('batchDeleteInputs');
|
||
const batchStatusInputs = document.getElementById('batchStatusInputs');
|
||
|
||
function updateBatchUI() {
|
||
const selected = Array.from(checkboxes).filter(cb => cb.checked);
|
||
selectedCount.textContent = selected.length;
|
||
|
||
if (selected.length > 0) {
|
||
batchActions.classList.remove('hidden');
|
||
} else {
|
||
batchActions.classList.add('hidden');
|
||
}
|
||
|
||
// Update hidden inputs for forms
|
||
batchDeleteInputs.innerHTML = '';
|
||
batchStatusInputs.innerHTML = '';
|
||
|
||
selected.forEach(cb => {
|
||
const input1 = document.createElement('input');
|
||
input1.type = 'hidden';
|
||
input1.name = 'ids[]';
|
||
input1.value = cb.value;
|
||
batchDeleteInputs.appendChild(input1);
|
||
|
||
const input2 = document.createElement('input');
|
||
input2.type = 'hidden';
|
||
input2.name = 'ids[]';
|
||
input2.value = cb.value;
|
||
batchStatusInputs.appendChild(input2);
|
||
});
|
||
}
|
||
|
||
selectAll.addEventListener('change', function() {
|
||
checkboxes.forEach(cb => cb.checked = selectAll.checked);
|
||
updateBatchUI();
|
||
});
|
||
|
||
checkboxes.forEach(cb => {
|
||
cb.addEventListener('change', function() {
|
||
updateBatchUI();
|
||
selectAll.checked = Array.from(checkboxes).every(c => c.checked);
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
</x-app-layout>
|