diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 3d3ce4e..840f0a9 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -2,7 +2,7 @@
## Overview
-This roadmap delivers inline note-taking capabilities for the Taiwan NPO admin member list, enabling quick annotation without page navigation. The implementation follows a foundation-first approach: database schema and backend API (Phase 1), followed by inline quick-add UI delivering core value (Phase 2), and concluding with full note history and display features (Phase 3). All work leverages the existing Laravel 10 + Alpine.js + Tailwind stack with zero new dependencies.
+This roadmap delivers inline note-taking capabilities for the Taiwan NPO admin member list, enabling quick annotation without page navigation. The implementation follows a foundation-first approach: database schema and backend API (Phase 1), followed by inline quick-add UI delivering core value (Phase 2), and concluding with full note history and display features (Phase 3). All work leverages the existing Laravel 10 + Alpine.js + Tailwind stack (Phase 3 adds @alpinejs/collapse, the official Alpine.js collapse plugin).
## Phases
@@ -73,10 +73,10 @@ Plans:
4. Admin can filter/search notes by text content within a member's note history
5. Expanded panel collapses cleanly without affecting other member rows
-**Plans**: TBD
+**Plans:** 1 plan
Plans:
-- [ ] TBD (will be defined in plan-phase)
+- [ ] 03-01-PLAN.md — Expandable note history panel with search, collapse plugin, controller ordering fix, and feature tests
## Progress
@@ -87,7 +87,7 @@ Phases execute in numeric order: 1 → 2 → 3
|-------|----------------|--------|-----------|
| 1. Database Schema & Backend API | 2/2 | ✓ Complete | 2026-02-13 |
| 2. Inline Quick-Add UI | 1/1 | ✓ Complete | 2026-02-13 |
-| 3. Note History & Display | 0/TBD | Not started | - |
+| 3. Note History & Display | 0/1 | Not started | - |
---
*Roadmap created: 2026-02-13*
diff --git a/.planning/phases/03-note-history-display/03-01-PLAN.md b/.planning/phases/03-note-history-display/03-01-PLAN.md
new file mode 100644
index 0000000..552d481
--- /dev/null
+++ b/.planning/phases/03-note-history-display/03-01-PLAN.md
@@ -0,0 +1,469 @@
+---
+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 `