```
**Step 3: Add the note count badge + inline form cell** after the status `| ` and before the actions ` | `. This new ` | ` contains:
1. A badge showing the note count (reactive via `x-text="noteCount"`)
2. A toggle button to open/close the inline form
3. The inline form itself (conditionally shown via `x-show="noteFormOpen"`)
```html
|
|
```
**Step 4: Update the empty state `` colspan** from `7` to `8` (since we added the 備忘錄 column).
**Step 5: Add `x-cloak` CSS support.** Add a `@push('styles')` block at the top of the template (after ``) with:
```html
@push('styles')
@endpush
```
Check if the layout already includes an x-cloak style. If it does, skip this step. If not, add it. This prevents flash of unstyled content on the inline form.
**Important implementation notes:**
- The `noteCount` initializes from `{{ $member->notes_count ?? 0 }}` — this comes from `withCount('notes')` in the controller (already done in Phase 1)
- The axios.post URL uses `{{ route('admin.members.notes.store', $member) }}` — this named route was registered in Phase 1
- CSRF token is automatically included by axios (configured in bootstrap.js)
- Each row has its own independent Alpine.js scope — pagination works because each page renders fresh x-data
- The `x-cloak` directive ensures the form is hidden until Alpine initializes
- Dark mode is covered by `dark:` prefixed Tailwind classes on every element
- The submit button is disabled when `isSubmitting` is true OR `noteContent` is empty (prevents submitting blank notes)
- The cancel button clears content, closes form, and resets errors
1. Run `php artisan route:list | grep notes` to confirm the note routes exist (from Phase 1)
2. Open the member list view file and verify:
- x-data with noteFormOpen, noteContent, isSubmitting, errors, noteCount on each ``
- submitNote() async method calls axios.post to the correct route
- Note count badge with `x-text="noteCount"`
- Form with `@submit.prevent="submitNote()"`
- Textarea with `x-model="noteContent"` and dark mode classes
- Error display with `x-show="errors.content"` and `x-text="errors.content?.[0]"`
- Submit button with `:disabled="isSubmitting || noteContent.trim() === ''"`
- Loading text toggle between 儲存 and 儲存中...
- Cancel button that resets state
- colspan updated to 8 on empty state row
3. Run `php artisan view:cache` and `php artisan view:clear` to verify the Blade template compiles without errors
Member list Blade template includes per-row Alpine.js inline note form with: badge showing reactive note count, toggle button to expand form, textarea with dark mode support, AJAX submission via axios with CSRF, loading state on submit button, validation error display in Chinese, cancel button that resets state, and correct colspan on empty row.
Task 2: Add feature tests verifying inline note UI renders correctly
tests/Feature/Admin/MemberNoteInlineUITest.php
Create a feature test class that verifies the member list Blade template includes the necessary Alpine.js note UI elements. These are server-side rendering tests — they verify the HTML output contains the right Alpine directives and data, not that JavaScript executes (that would be a browser test).
Create `tests/Feature/Admin/MemberNoteInlineUITest.php`:
```php
artisan('db:seed', ['--class' => 'RoleSeeder']);
}
/** @test */
public function member_list_renders_note_count_badge_for_each_member()
{
$admin = User::factory()->create();
$admin->assignRole('admin');
$member = Member::factory()->create();
// Create 3 notes for this member
Note::factory()->count(3)->forMember($member)->byAuthor($admin)->create();
$response = $this->actingAs($admin)->get(route('admin.members.index'));
$response->assertStatus(200);
// Verify the Alpine.js noteCount is initialized with 3
$response->assertSee('noteCount: 3', false);
// Verify badge rendering element exists
$response->assertSee('x-text="noteCount"', false);
}
/** @test */
public function member_list_renders_inline_note_form_with_alpine_directives()
{
$admin = User::factory()->create();
$admin->assignRole('admin');
Member::factory()->create();
$response = $this->actingAs($admin)->get(route('admin.members.index'));
$response->assertStatus(200);
// Verify Alpine.js form directives are present
$response->assertSee('noteFormOpen', false);
$response->assertSee('@submit.prevent="submitNote()"', false);
$response->assertSee('x-model="noteContent"', false);
$response->assertSee(':disabled="isSubmitting', false);
}
/** @test */
public function member_list_renders_note_column_header()
{
$admin = User::factory()->create();
$admin->assignRole('admin');
$response = $this->actingAs($admin)->get(route('admin.members.index'));
$response->assertStatus(200);
$response->assertSee('備忘錄', false);
}
/** @test */
public function member_with_zero_notes_shows_zero_count()
{
$admin = User::factory()->create();
$admin->assignRole('admin');
Member::factory()->create();
$response = $this->actingAs($admin)->get(route('admin.members.index'));
$response->assertStatus(200);
$response->assertSee('noteCount: 0', false);
}
/** @test */
public function member_list_includes_correct_note_store_route_for_each_member()
{
$admin = User::factory()->create();
$admin->assignRole('admin');
$member = Member::factory()->create();
$response = $this->actingAs($admin)->get(route('admin.members.index'));
$response->assertStatus(200);
// Verify the axios.post URL contains the correct member route
$expectedRoute = route('admin.members.notes.store', $member);
$response->assertSee($expectedRoute, false);
}
}
```
**Test coverage rationale:**
1. `member_list_renders_note_count_badge_for_each_member` — Verifies noteCount initialization from withCount and badge element exists (DISP-01)
2. `member_list_renders_inline_note_form_with_alpine_directives` — Verifies Alpine.js form directives are in the HTML (NOTE-01, NOTE-03, UI-03)
3. `member_list_renders_note_column_header` — Verifies 備忘錄 column header exists (UI-01)
4. `member_with_zero_notes_shows_zero_count` — Edge case: zero notes shows badge with 0
5. `member_list_includes_correct_note_store_route_for_each_member` — Verifies AJAX URL targets correct endpoint per member (NOTE-03, key link)
Use the same test patterns as existing `MemberNoteTest.php` (setUp with RoleSeeder, actingAs admin, assertSee).
Run: `php artisan test --filter=MemberNoteInlineUITest`
All 5 tests must pass. Expected output:
```
PASS Tests\Feature\Admin\MemberNoteInlineUITest
✓ member list renders note count badge for each member
✓ member list renders inline note form with alpine directives
✓ member list renders note column header
✓ member with zero notes shows zero count
✓ member list includes correct note store route for each member
```
Also run: `php artisan test --filter=MemberNoteTest` to verify no regressions in the existing Phase 1 tests (all 7 should still pass).
5 feature tests pass verifying: note count badge renders with correct count from withCount, Alpine.js directives (noteFormOpen, submitNote, x-model, :disabled) present in HTML, 備忘錄 column header renders, zero-note members show 0 count, and correct note store route URL embedded for each member. No regressions in Phase 1 tests.
1. `php artisan test --filter=MemberNoteInlineUITest` — All 5 new tests pass
2. `php artisan test --filter=MemberNoteTest` — All 7 Phase 1 tests still pass (no regressions)
3. `php artisan view:clear && php artisan view:cache` — Blade template compiles without errors
4. `php artisan route:list | grep notes` — Note routes exist and are accessible
5. Manual review of `resources/views/admin/members/index.blade.php`:
- Every `dark:` prefix class has a corresponding light-mode class
- x-cloak on the inline form div
- @submit.prevent (not @submit) on the form
- :disabled binding on submit button
- Error display with x-show and x-text
1. Member list page at /admin/members renders with a "備忘錄" column containing per-row note count badges
2. Each badge displays the correct note count (from withCount, no N+1 queries)
3. Each row has an expand button that reveals an inline note form (Alpine.js x-show)
4. The inline form includes textarea, cancel button, and submit button with loading state
5. Axios POST URL correctly targets /admin/members/{member}/notes for each row
6. Validation error display element exists with x-show binding to errors.content
7. All Tailwind classes include dark: equivalents for dark mode
8. 12 total tests pass (5 new UI + 7 existing API) with zero regressions
|