207 lines
6.5 KiB
JavaScript
207 lines
6.5 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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;
|