diff --git a/docs/DEVELOPMENT_SESSION_2025-12-12_Appointment_Details_Modal.md b/docs/DEVELOPMENT_SESSION_2025-12-12_Appointment_Details_Modal.md new file mode 100644 index 0000000..b0d15fd --- /dev/null +++ b/docs/DEVELOPMENT_SESSION_2025-12-12_Appointment_Details_Modal.md @@ -0,0 +1,264 @@ +# Development Session - 2025-12-12 + +## Session Overview + +**Started**: 18:30 +**Ended**: 22:10 +**Branch**: `fix/automatic-groups-detection` +**Focus**: Appointment Details Modal Enhancement & Tag Filtering + +## Major Accomplishments + +### Phase 1: Bulk Operations Widget (18:30-19:00) + +- **Goal**: Add selection functionality to AdminTable and create reusable bulk operations widget +- **Result**: Complete bulk operations system implemented +- **Code Changes**: + - Created `BulkActionsWidget.vue` - Reusable component for bulk operations + - Added `useBulkAppointmentActions.ts` - Composable for appointment extension + - Enhanced `AdminTable.vue` with checkbox selection support + - Integrated bulk widget into `ExpiringAppointmentsAdmin.vue` + +**Key Features**: + +- Checkbox selection in AdminTable (selectable prop) +- Selection count display in floating widget +- Bulk appointment extension (3-24 months) +- Toast notifications for operation results + +### Phase 2: Appointment Details Modal Overhaul (19:00-21:00) + +- **Goal**: Display all appointment information in structured, readable format +- **Result**: Complete modal redesign with full API integration +- **Code Changes**: + - Added `fetchAppointmentSeries()` API function + - Restructured modal with card-based layout + - Added all missing fields (subtitle, address, link, calendar color) + - Fixed data structure issues (image, description, additionals, exceptions) + +**Key Improvements**: + +- **Card-based Layout**: Separate cards for details, series info, tags, description, image +- **Two-column Grid**: Responsive layout for appointment details +- **Series Information**: Proper display using `repeatId` (DAILY=1, WEEKLY=7, etc.) +- **Collapsible Exceptions**: Show 10, expand to show all (for appointments with many exceptions) +- **Image Display**: Fixed using `imageUrl` from Image object +- **Description**: Fixed using correct `description` field (not deprecated `note`) +- **Additional Dates**: Fixed structure (array of objects with `date` field) +- **Calendar Color**: Visual indicator with colored dot + +### Phase 3: Tag Filtering & Dropdown Issues (21:00-22:10) + +- **Goal**: Fix tag filtering to work client-side without API reloads +- **Result**: Tag filtering works locally, dropdown positioning fixed +- **Code Changes**: + - Modified `useExpiringAppointments` to load all appointments once + - Removed tag parameters from API query + - Fixed `applyFilters()` to work on cached data + - Improved dropdown positioning in `TagMultiSelect.vue` + - Fixed watch initialization order + +**Key Fixes**: + +- Tag filtering now client-side only (no API reload) +- Dropdown appears directly under widget +- Dropdown follows widget on scroll (instead of closing) +- Fixed "Cannot access before initialization" error +- Added debug logging for filter operations + +## Technical Decisions + +### Decision: Bulk Operations Widget Pattern (18:45) + +**Context**: Need reusable pattern for bulk operations across admin panels +**Decision**: Create generic `BulkActionsWidget` with slot-based actions +**Impact**: Can be reused in all admin panels with custom actions + +**Pattern**: + +```vue + + + +``` + +### Decision: API Integration for Modal (19:30) + +**Context**: Modal showed incomplete data from table preview +**Decision**: Use `GET /calendars/appointments/{id}/{startDate}` for full data +**Impact**: Modal shows complete appointment series with all details + +**Response Structure**: + +```json +{ + "data": { + "appointment": { + "base": { + /* all series data */ + }, + "calculated": { + /* computed data */ + } + } + } +} +``` + +### Decision: Client-Side Tag Filtering (21:30) + +**Context**: Tag changes triggered API reloads, causing flicker +**Decision**: Load all appointments once, filter locally +**Impact**: Instant filtering, no network requests, better UX + +**Implementation**: + +- Load 9999 days of appointments once +- Apply tag filter in `applyFilters()` function +- Watch for tag changes, re-filter locally + +### Decision: Dropdown Positioning Strategy (22:00) + +**Context**: Dropdown appeared above widget or disconnected +**Decision**: Update position on scroll instead of closing +**Impact**: Better UX, dropdown stays visible and follows widget + +## Challenges & Solutions + +### Challenge 1: Set Reactivity in AdminTable + +**Problem**: Checkbox state not updating when using `Set.add()` and `Set.delete()` +**Solution**: Create new Set instance on each change for Vue reactivity +**Code**: + +```javascript +// Before (not reactive) +selectedItems.value.add(itemId) + +// After (reactive) +const newSet = new Set(selectedItems.value) +newSet.add(itemId) +selectedItems.value = newSet +``` + +### Challenge 2: Appointment Data Structure + +**Problem**: Multiple issues with nested data (image, description, additionals) +**Solution**: Understand API response structure and access correct fields +**Fixes**: + +- Image: `appointment.base.image.imageUrl` (not `image` directly) +- Description: `appointment.base.description` (not deprecated `note`) +- Additionals: Array of `{id, date}` objects (not date strings) +- Series ID: `appointment.base.id` for row-key + +### Challenge 3: Watch Initialization Order + +**Problem**: "Cannot access 'applyFilters' before initialization" error +**Solution**: Remove `{ immediate: true }` from watch, call in `onMounted()` instead +**Reason**: Watch with immediate runs before function declaration + +### Challenge 4: Tag Filtering API Reloads + +**Problem**: Changing tags triggered API requests, causing data reload +**Solution**: Load all data once, filter client-side +**Implementation**: + +- Remove tag parameter from `useExpiringAppointments` +- Filter in `applyFilters()` based on `selectedTagIds` +- Watch only triggers local filtering + +## Performance Improvements + +1. **Reduced API Calls**: Tag filtering no longer triggers API requests +2. **Cached Data**: TanStack Query caches appointments for 30 minutes +3. **Local Filtering**: Instant response to filter changes +4. **Optimized Rendering**: Card-based layout with conditional rendering + +## Code Quality + +- Added comprehensive debug logging for troubleshooting +- Consistent error handling with try-catch blocks +- TypeScript interfaces for type safety +- Reusable components (BulkActionsWidget, TagMultiSelect) +- Performance logging for API calls + +## Testing Notes + +- Tested with "Mädchenjungschar" (29 exceptions, proper display) +- Tested with "Hauskreis Schaak" (21 additional dates, correct structure) +- Tag filtering tested with multiple tags (187, 190) +- Bulk extension tested with various month selections +- Dropdown positioning tested with scroll and resize + +## Next Steps + +- [ ] Consider removing debug logs for production +- [ ] Add bulk operations to other admin panels (Tags, Automatic Groups) +- [ ] Add more bulk actions (delete, duplicate, etc.) +- [ ] Improve exception display for very large lists (pagination?) +- [ ] Add keyboard shortcuts for bulk operations + +## Lessons Learned + +### 1. Vue Reactivity with Collections + +**Problem**: Mutating Sets/Maps doesn't trigger Vue reactivity +**Solution**: Always create new instances when modifying collections +**Application**: Use this pattern for all Set/Map operations in Vue 3 + +### 2. API Response Structure Understanding + +**Problem**: Assumed flat structure, but API returns nested objects +**Solution**: Log full response, understand actual structure before coding +**Application**: Always inspect API responses before implementing UI + +### 3. Watch Timing Issues + +**Problem**: Watches with `immediate: true` can run before dependencies are ready +**Solution**: Use `onMounted()` for initial setup, watches for changes only +**Application**: Separate initialization logic from reactive updates + +### 4. Client-Side vs Server-Side Filtering + +**Problem**: Server-side filtering causes unnecessary API calls +**Solution**: Load all data once, filter client-side for better UX +**Application**: Use client-side filtering for small-medium datasets (<1000 items) + +### 5. Dropdown Positioning in Portals + +**Problem**: Fixed positioning requires careful calculation with scroll offsets +**Solution**: Use `getBoundingClientRect()` + `window.scrollX/Y` for accurate positioning +**Application**: Always account for scroll position when using fixed positioning + +## Files Modified + +- `src/components/common/BulkActionsWidget.vue` (new) +- `src/components/common/AdminTable.vue` (selection support) +- `src/components/common/TagMultiSelect.vue` (positioning fixes) +- `src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue` (major refactor) +- `src/composables/useBulkAppointmentActions.ts` (new) +- `src/composables/useExpiringAppointments.ts` (simplified) +- `src/services/churchtools.ts` (added fetchAppointmentSeries) + +## Commits + +1. `c039afc` - feat: Add bulk operations widget with appointment extension +2. `f3333f6` - feat: Improve appointment details modal with full API integration +3. `b55cf03` - fix: Improve tag filtering and dropdown positioning + +## Session Statistics + +- **Duration**: ~3.5 hours +- **Files Changed**: 7 files +- **Lines Added**: ~850 +- **Lines Removed**: ~600 +- **Commits**: 3 +- **Issues Resolved**: 5 major issues diff --git a/package-lock.json b/package-lock.json index 78291ef..178f8f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "churchtools-dashboard", - "version": "1.0.9", + "version": "1.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "churchtools-dashboard", - "version": "1.0.9", + "version": "1.0.10", "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.0.1", "@fortawesome/free-solid-svg-icons": "^7.0.1", @@ -15,6 +15,7 @@ "@tanstack/query-sync-storage-persister": "^5.90.2", "@tanstack/vue-query": "^5.90.2", "@vitejs/plugin-vue": "^6.0.1", + "@vueuse/core": "^14.1.0", "vue": "^3.5.21", "vue-i18n": "^9.14.5" }, @@ -1012,6 +1013,12 @@ "undici-types": "~7.10.0" } }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", @@ -1134,6 +1141,44 @@ "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", "license": "MIT" }, + "node_modules/@vueuse/core": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz", + "integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.1.0", + "@vueuse/shared": "14.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz", + "integrity": "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.1.0.tgz", + "integrity": "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", diff --git a/package.json b/package.json index 93d62bd..dca6faa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "churchtools-dashboard", "private": true, - "version": "1.0.9", + "version": "1.0.10", "type": "module", "scripts": { "dev": "vite", @@ -40,6 +40,7 @@ "@tanstack/query-sync-storage-persister": "^5.90.2", "@tanstack/vue-query": "^5.90.2", "@vitejs/plugin-vue": "^6.0.1", + "@vueuse/core": "^14.1.0", "vue": "^3.5.21", "vue-i18n": "^9.14.5" } diff --git a/src/components/automatic-groups/AutomaticGroupsAdmin.vue b/src/components/automatic-groups/AutomaticGroupsAdmin.vue index 5c7dc1a..9a34ec7 100644 --- a/src/components/automatic-groups/AutomaticGroupsAdmin.vue +++ b/src/components/automatic-groups/AutomaticGroupsAdmin.vue @@ -1,66 +1,80 @@ @@ -437,7 +500,8 @@ defineExpose({ .table-container { width: 100%; max-width: 100%; - overflow: auto; + overflow-x: auto; + overflow-y: auto; border-radius: 8px; background: var(--ct-bg-primary, #ffffff); margin-bottom: 0; @@ -485,10 +549,19 @@ defineExpose({ .admin-data-table td { padding: 0.75rem 1rem; border-bottom: 1px solid var(--ct-border-color, #f0f2f5); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; vertical-align: middle; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; + max-width: 300px; /* Default max-width for all cells */ +} + +/* Specific style for description cells */ +.admin-data-table td.description-cell { + white-space: normal; + min-width: 200px; + max-width: 400px; + word-break: break-word; } .admin-data-table th.sortable { @@ -536,6 +609,20 @@ defineExpose({ background-color: var(--ct-bg-secondary, #f8f9fa); } +/* Checkbox column */ +.checkbox-column { + width: 40px; + text-align: center; + padding: 0.75rem 0.5rem; +} + +.ct-checkbox { + cursor: pointer; + width: 18px; + height: 18px; + accent-color: var(--ct-primary, #3498db); +} + /* Buttons */ .ct-btn { padding: 0.75rem 1.5rem; diff --git a/src/components/common/BulkActionsWidget.vue b/src/components/common/BulkActionsWidget.vue new file mode 100644 index 0000000..1e27557 --- /dev/null +++ b/src/components/common/BulkActionsWidget.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/src/components/common/TagMultiSelect.vue b/src/components/common/TagMultiSelect.vue new file mode 100644 index 0000000..0170a53 --- /dev/null +++ b/src/components/common/TagMultiSelect.vue @@ -0,0 +1,592 @@ + + + + + diff --git a/src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue b/src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue index ef2702c..c80c78a 100644 --- a/src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue +++ b/src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue @@ -1,21 +1,207 @@