/** * Activity-List Component (Alpine.js) * * Used in: activities.html * * Features: * - Paginated table of activities * - Filter by type, owner, overdue, completed * - "+ New Activity" modal that POSTs to /api/v1/activities/ * - Row click opens a detail modal with a "Complete" button * * Backend: GET /api/v1/activities/?skip=&limit=&type=&owner_id=&overdue=&completed= * POST /api/v1/activities/ * PATCH /api/v1/activities/{id}/complete * * R-5: x-text only. Even activity bodies go through x-text, never x-html. */ import { api } from '/js/api.js'; export const ACTIVITY_TYPES = ['call', 'email', 'meeting', 'task', 'note']; export const TYPE_BADGE = { call: 'badge-blue', email: 'badge-yellow', meeting: 'badge-green', task: 'badge-gray', note: 'badge-gray', }; export function activityList() { return { // State activities: [], total: 0, skip: 0, limit: 20, type: '', ownerId: '', overdue: '', // '', 'true', 'false' completed: '', // '', 'true', 'false' loading: false, error: null, // Create-modal state showCreate: false, createForm: this._initForm(), createError: '', creating: false, // Detail-modal state showDetail: false, selected: null, completing: false, // Lookups types: ACTIVITY_TYPES.slice(), async init() { await this.load(); }, async load() { this.loading = true; this.error = null; try { const params = new URLSearchParams(); params.set('skip', String(this.skip)); params.set('limit', String(this.limit)); if (this.type) params.set('type', this.type); if (this.ownerId) params.set('owner_id', this.ownerId); if (this.overdue) params.set('overdue', this.overdue); if (this.completed) params.set('completed', this.completed); const res = await api.get(`/activities/?${params.toString()}`); this.activities = Array.isArray(res) ? res : (res && res.items) || []; this.total = (res && res.total) || this.activities.length; } catch (err) { this.error = err.message || 'Fehler beim Laden der Activities.'; this._notify('error', this.error); } finally { this.loading = false; } }, async applyFilters() { this.skip = 0; await this.load(); }, async nextPage() { if (this.skip + this.limit < this.total) { this.skip += this.limit; await this.load(); } }, async prevPage() { if (this.skip - this.limit >= 0) { this.skip -= this.limit; await this.load(); } }, get pageInfo() { const from = this.total === 0 ? 0 : this.skip + 1; const to = Math.min(this.skip + this.limit, this.total); return `${from}–${to} von ${this.total}`; }, // === Row helpers === isOverdue(a) { if (a.completed_at) return false; if (!a.due_date) return false; return new Date(a.due_date) < new Date(); }, isCompleted(a) { return !!a.completed_at; }, typeBadge(t) { return TYPE_BADGE[t] || 'badge-gray'; }, formatDateTime(iso) { if (!iso) return ''; try { return new Date(iso).toLocaleString('de-DE'); } catch { return iso; } }, // === Create modal === openCreate() { this.createForm = this._initForm(); this.createError = ''; this.showCreate = true; }, closeCreate() { this.showCreate = false; }, async submitCreate() { this.creating = true; this.createError = ''; try { const payload = { type: this.createForm.type, subject: this.createForm.subject, }; if (this.createForm.body) payload.body = this.createForm.body; if (this.createForm.due_date) payload.due_date = this.createForm.due_date; if (this.createForm.account_id) payload.account_id = Number(this.createForm.account_id); if (this.createForm.contact_id) payload.contact_id = Number(this.createForm.contact_id); if (this.createForm.deal_id) payload.deal_id = Number(this.createForm.deal_id); await api.post('/activities/', payload); this._notify('success', 'Activity angelegt.'); this.showCreate = false; await this.load(); } catch (err) { this.createError = err.message || 'Fehler beim Anlegen.'; } finally { this.creating = false; } }, // === Detail / Complete === openDetail(a) { this.selected = a; this.showDetail = true; }, closeDetail() { this.showDetail = false; this.selected = null; }, async completeSelected() { if (!this.selected) return; this.completing = true; try { await api.patch(`/activities/${this.selected.id}/complete`, { outcome: '' }); this._notify('success', 'Activity abgeschlossen.'); this.closeDetail(); await this.load(); } catch (err) { this._notify('error', err.message || 'Fehler beim Abschließen.'); } finally { this.completing = false; } }, // === Internals === _initForm() { return { type: 'task', subject: '', body: '', due_date: '', account_id: '', contact_id: '', deal_id: '', }; }, _notify(type, message) { if (window.Alpine) Alpine.store('notifications').push(message, type); }, }; } if (typeof window !== 'undefined') { document.addEventListener('alpine:init', () => { if (window.Alpine) { window.Alpine.data('activityList', activityList); } }); } export default activityList;