--- 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 ` ``` Keep the existing pencil icon toggle button for the add form unchanged. **Step 7: Add expansion panel as a separate `` after the main row** Immediately after the closing `` of the main member row (before the `@empty` directive), add a new `` for the expansion panel. This `` must be OUTSIDE the main row's `` but needs to share Alpine.js state. IMPORTANT: The expansion panel `` cannot access the main row's `x-data` if it's on a sibling ``. To solve this, wrap BOTH the main `` and the expansion `` in a `