---
phase: 03-note-history-display
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- resources/js/app.js
- resources/views/admin/members/index.blade.php
- app/Http/Controllers/Admin/MemberNoteController.php
- tests/Feature/Admin/MemberNoteTest.php
- package.json
autonomous: true
must_haves:
truths:
- "Admin clicks note count badge and an inline panel expands below the row showing all notes for that member"
- "Notes display newest first with author name and formatted datetime (YYYY年MM月DD日 HH:mm)"
- "Panel shows '尚無備註' when member has no notes"
- "Admin can type in a search field to filter notes by text content or author name"
- "Panel collapses cleanly when badge is clicked again, search query resets, other rows are unaffected"
- "After adding a note via inline form, the history panel (if previously opened) shows the new note immediately without re-fetching"
artifacts:
- path: "resources/js/app.js"
provides: "Alpine.js collapse plugin registration"
contains: "Alpine.plugin(collapse)"
- path: "resources/views/admin/members/index.blade.php"
provides: "Expandable note history panel with search"
contains: "toggleHistory"
- path: "app/Http/Controllers/Admin/MemberNoteController.php"
provides: "Notes endpoint with newest-first ordering and eager-loaded author"
contains: "latest"
- path: "tests/Feature/Admin/MemberNoteTest.php"
provides: "Tests verifying ordering, empty state, and search-related data"
key_links:
- from: "resources/views/admin/members/index.blade.php"
to: "GET /admin/members/{member}/notes"
via: "axios.get in Alpine.js toggleHistory() method"
pattern: "admin.members.notes.index"
- from: "resources/views/admin/members/index.blade.php"
to: "resources/js/app.js"
via: "Alpine.plugin(collapse) enables x-collapse directive"
pattern: "x-collapse"
- from: "resources/views/admin/members/index.blade.php"
to: "submitNote → notes.unshift"
via: "After note creation, new note injected into cached notes array"
pattern: "this.notes.unshift"
---
Add expandable note history panel to member list with search filtering, complete the note feature.
Purpose: Admins can view full note history for any member by clicking the note count badge, filter notes by content, and see notes displayed with author attribution and timestamps — all inline without leaving the member list page.
Output: Working expandable panel with lazy-loaded notes, client-side search, empty state, and proper cache sync with the inline add form.
@/Users/gbanyan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/gbanyan/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-note-history-display/03-RESEARCH.md
@.planning/phases/02-inline-quick-add-ui/02-01-SUMMARY.md
@resources/views/admin/members/index.blade.php
@resources/js/app.js
@app/Http/Controllers/Admin/MemberNoteController.php
@tests/Feature/Admin/MemberNoteTest.php
Task 1: Install collapse plugin, fix controller ordering, build expandable history panel with search
package.json
resources/js/app.js
app/Http/Controllers/Admin/MemberNoteController.php
resources/views/admin/members/index.blade.php
**Step 1: Install @alpinejs/collapse**
Run `npm install @alpinejs/collapse` to add the plugin.
**Step 2: Register collapse plugin in app.js**
In `resources/js/app.js`, import and register the collapse plugin BEFORE `Alpine.start()`:
```javascript
import './bootstrap';
import Alpine from 'alpinejs';
import collapse from '@alpinejs/collapse';
Alpine.plugin(collapse);
window.Alpine = Alpine;
Alpine.start();
```
Run `npm run build` to verify the build succeeds.
**Step 3: Fix controller ordering**
In `app/Http/Controllers/Admin/MemberNoteController.php`, update the `index` method to order notes newest first:
Change:
```php
$notes = $member->notes()->with('author')->get();
```
To:
```php
$notes = $member->notes()->with('author:id,name')->latest('created_at')->get();
```
This fixes a latent bug where ordering was only working by coincidence (SQLite insertion order). Also narrows author eager load to only `id` and `name` fields.
**Step 4: Extend Alpine.js x-data scope in member row**
In `resources/views/admin/members/index.blade.php`, extend the existing per-row `x-data` object to add history panel state. The existing `x-data` on the `
` (line ~196) currently has `noteFormOpen`, `noteContent`, `isSubmitting`, `errors`, `noteCount`, and `submitNote()`. Add these new properties and methods:
New state properties (add after `noteCount`):
- `historyOpen: false` — controls panel visibility
- `notes: []` — cached note data array
- `notesLoaded: false` — tracks if notes have been fetched
- `isLoadingNotes: false` — loading spinner state
- `searchQuery: ''` — search input value
New methods:
```javascript
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;
}
```
**Step 5: Update submitNote() for cache sync**
Inside the existing `submitNote()` method's success block (after `this.noteCount++`), add cache sync logic:
```javascript
// After noteCount++, noteContent = '', noteFormOpen = false:
if (this.notesLoaded) {
this.notes.unshift(response.data.note);
}
```
This ensures the history panel (if already opened) shows the new note immediately. The `response.data.note` already includes `author` from the store endpoint (Phase 1 returns `$note->load('author')`).
**Step 6: Make the note count badge clickable**
Replace the existing static `` badge in the 備忘錄 column with a clickable `