Kontakt-Modul
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
623bbdf5dd
Commit
7d67557be4
556
frontend/js/reminders.js
Normale Datei
556
frontend/js/reminders.js
Normale Datei
@ -0,0 +1,556 @@
|
||||
/**
|
||||
* TASKMATE - Reminders Module
|
||||
* ===========================
|
||||
* Erinnerungsmanagement
|
||||
*/
|
||||
|
||||
import store from './store.js';
|
||||
import api from './api.js';
|
||||
import {
|
||||
$, $$, createElement, clearElement, formatDate
|
||||
} from './utils.js';
|
||||
|
||||
class ReminderManager {
|
||||
constructor() {
|
||||
this.modal = null;
|
||||
this.form = null;
|
||||
this.mode = 'create';
|
||||
this.reminderId = null;
|
||||
this.selectedColor = '#F59E0B';
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.modal = $('#reminder-modal');
|
||||
this.form = $('#reminder-form');
|
||||
|
||||
// Wait for DOM to be ready, then bind events
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => this.bindEvents());
|
||||
} else {
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
// Listen for modal events
|
||||
window.addEventListener('modal:open', (e) => {
|
||||
if (e.detail.modalId === 'reminder-modal') {
|
||||
this.open(e.detail.mode, e.detail.data);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('modal:close', (e) => {
|
||||
if (e.detail.modalId === 'reminder-modal') {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Also bind button when calendar view is loaded
|
||||
window.addEventListener('app:view-changed', (e) => {
|
||||
if (e.detail.view === 'calendar') {
|
||||
setTimeout(() => this.bindCalendarButton(), 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Form submission
|
||||
this.form?.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
|
||||
// Close buttons
|
||||
$$('.modal-close', this.modal)?.forEach(btn => {
|
||||
btn.addEventListener('click', () => this.close());
|
||||
});
|
||||
|
||||
$('#btn-cancel-reminder')?.addEventListener('click', () => this.close());
|
||||
|
||||
// Delete button
|
||||
$('#btn-delete-reminder')?.addEventListener('click', () => this.handleDelete());
|
||||
|
||||
// Color picker trigger
|
||||
const colorTrigger = $('#color-picker-trigger');
|
||||
const colorDropdown = $('#color-picker-dropdown');
|
||||
|
||||
if (colorTrigger) {
|
||||
colorTrigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
colorDropdown.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Color picker options
|
||||
$$('.color-picker-dropdown .color-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const color = e.target.dataset.color;
|
||||
|
||||
// Update selected state
|
||||
$$('.color-picker-dropdown .color-option').forEach(opt => opt.classList.remove('selected'));
|
||||
e.target.classList.add('selected');
|
||||
|
||||
// Update trigger color and form value
|
||||
colorTrigger.style.backgroundColor = color;
|
||||
this.selectedColor = color;
|
||||
$('#reminder-color').value = color;
|
||||
|
||||
// Close dropdown
|
||||
colorDropdown.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.color-picker-wrapper')) {
|
||||
colorDropdown?.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Setup custom select for user dropdown
|
||||
this.setupCustomSelect();
|
||||
|
||||
// Calendar button will be bound separately
|
||||
this.bindCalendarButton();
|
||||
}
|
||||
|
||||
bindCalendarButton() {
|
||||
const reminderBtn = $('#btn-new-reminder');
|
||||
if (reminderBtn) {
|
||||
console.log('[Reminder] Button found, binding event');
|
||||
|
||||
// Remove existing event listeners
|
||||
const newBtn = reminderBtn.cloneNode(true);
|
||||
reminderBtn.parentNode.replaceChild(newBtn, reminderBtn);
|
||||
|
||||
newBtn.addEventListener('click', (e) => {
|
||||
console.log('[Reminder] Button clicked!');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
console.log('[Reminder] Opening modal...');
|
||||
console.log('[Reminder] Modal element:', this.modal);
|
||||
|
||||
this.open('create', {});
|
||||
});
|
||||
} else {
|
||||
console.warn('[Reminder] Button #btn-new-reminder not found!');
|
||||
}
|
||||
}
|
||||
|
||||
async open(mode = 'create', data = {}) {
|
||||
console.log('[Reminder] open() called with mode:', mode, 'data:', data);
|
||||
console.log('[Reminder] Modal exists:', !!this.modal);
|
||||
|
||||
this.mode = mode;
|
||||
this.reminderId = data.reminderId || null;
|
||||
|
||||
// Find modal if not already set
|
||||
if (!this.modal) {
|
||||
this.modal = $('#reminder-modal');
|
||||
console.log('[Reminder] Modal found on second attempt:', !!this.modal);
|
||||
}
|
||||
|
||||
if (!this.modal) {
|
||||
console.error('[Reminder] Modal element not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Modal title
|
||||
const titleEl = $('#reminder-modal-title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = mode === 'edit' ? 'Erinnerung bearbeiten' : 'Neue Erinnerung';
|
||||
console.log('[Reminder] Title set to:', titleEl.textContent);
|
||||
}
|
||||
|
||||
// Button text
|
||||
const saveBtn = $('#btn-save-reminder .btn-text');
|
||||
if (saveBtn) {
|
||||
saveBtn.textContent = mode === 'edit' ? 'Aktualisieren' : 'Speichern';
|
||||
}
|
||||
|
||||
// Show/hide delete button
|
||||
const deleteBtn = $('#btn-delete-reminder');
|
||||
if (deleteBtn) {
|
||||
if (mode === 'edit' && this.reminderId) {
|
||||
deleteBtn.classList.remove('hidden');
|
||||
} else {
|
||||
deleteBtn.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
this.resetForm();
|
||||
await this.loadUsers(); // Benutzer laden
|
||||
|
||||
if (mode === 'edit' && this.reminderId) {
|
||||
this.loadReminder();
|
||||
} else if (data.prefill) {
|
||||
this.prefillForm(data.prefill);
|
||||
}
|
||||
|
||||
// Show modal
|
||||
console.log('[Reminder] Showing modal...');
|
||||
|
||||
// Create overlay if it doesn't exist
|
||||
let overlay = document.querySelector('.modal-overlay');
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
// Show overlay first
|
||||
overlay.classList.add('visible');
|
||||
|
||||
// Show modal
|
||||
this.modal.classList.remove('hidden');
|
||||
this.modal.classList.add('visible');
|
||||
|
||||
console.log('[Reminder] Modal and overlay should now be visible');
|
||||
}
|
||||
|
||||
close() {
|
||||
// Hide modal
|
||||
this.modal.classList.remove('visible');
|
||||
setTimeout(() => this.modal.classList.add('hidden'), 200);
|
||||
|
||||
// Hide overlay
|
||||
const overlay = document.querySelector('.modal-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('visible');
|
||||
}
|
||||
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
if (this.form) {
|
||||
this.form.reset();
|
||||
$('#reminder-time').value = '09:00';
|
||||
|
||||
// Reset color selection
|
||||
$$('.color-picker-dropdown .color-option').forEach(opt => opt.classList.remove('selected'));
|
||||
$$('.color-picker-dropdown .color-option[data-color="#F59E0B"]').forEach(opt => opt.classList.add('selected'));
|
||||
const colorTrigger = $('#color-picker-trigger');
|
||||
if (colorTrigger) {
|
||||
colorTrigger.style.backgroundColor = '#F59E0B';
|
||||
}
|
||||
this.selectedColor = '#F59E0B';
|
||||
$('#reminder-color').value = '#F59E0B';
|
||||
|
||||
// Reset advance days
|
||||
$$('input[name="advance-days"]').forEach(cb => {
|
||||
cb.checked = cb.value === '1';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
prefillForm(prefill) {
|
||||
if (prefill.date) {
|
||||
$('#reminder-date').value = prefill.date;
|
||||
}
|
||||
}
|
||||
|
||||
async loadUsers() {
|
||||
console.log('[Reminder] Loading users...');
|
||||
let users = store.get('users') || [];
|
||||
console.log('[Reminder] Users from store:', users);
|
||||
|
||||
// If no users in store, try to load them
|
||||
if (users.length === 0) {
|
||||
try {
|
||||
console.log('[Reminder] No users in store, loading from API...');
|
||||
const response = await api.request('/auth/users');
|
||||
users = response.data || response;
|
||||
console.log('[Reminder] Users from API:', users);
|
||||
|
||||
// Update store
|
||||
if (users.length > 0) {
|
||||
store.setUsers(users);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Reminder] Failed to load users:', error);
|
||||
users = [];
|
||||
}
|
||||
}
|
||||
|
||||
const optionsContainer = $('#reminder-assignee-options');
|
||||
|
||||
if (!optionsContainer) {
|
||||
console.warn('[Reminder] Assignee options container not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing options except first one
|
||||
optionsContainer.innerHTML = `
|
||||
<div class="custom-select-option" data-value="">
|
||||
<span class="option-text">Alle Benutzer</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add user options with avatars
|
||||
users.forEach(user => {
|
||||
console.log('[Reminder] Adding user:', user);
|
||||
|
||||
// Get user info
|
||||
const displayName = user.displayName || user.display_name || user.username || user.email;
|
||||
const initials = user.initials || this.getInitials(displayName);
|
||||
const color = user.color || '#6366F1';
|
||||
|
||||
// Create option element
|
||||
const option = document.createElement('div');
|
||||
option.className = 'custom-select-option';
|
||||
option.dataset.value = user.id;
|
||||
option.dataset.initials = initials;
|
||||
option.dataset.color = color;
|
||||
option.dataset.displayName = displayName;
|
||||
|
||||
option.innerHTML = `
|
||||
<div class="option-avatar" style="background-color: ${color}">${initials}</div>
|
||||
<span class="option-text">${displayName}</span>
|
||||
`;
|
||||
|
||||
optionsContainer.appendChild(option);
|
||||
});
|
||||
|
||||
// Setup custom select behavior
|
||||
this.setupCustomSelect();
|
||||
|
||||
console.log('[Reminder] Users loaded, total options:', optionsContainer.children.length);
|
||||
}
|
||||
|
||||
setupCustomSelect() {
|
||||
const wrapper = $('#reminder-assignee-wrapper');
|
||||
const trigger = $('#reminder-assignee-trigger');
|
||||
const options = $('#reminder-assignee-options');
|
||||
const hiddenInput = $('#reminder-assignee');
|
||||
|
||||
if (!wrapper || !trigger || !options || !hiddenInput) {
|
||||
console.log('[Reminder] Custom select elements not found, skipping setup');
|
||||
return;
|
||||
}
|
||||
|
||||
const valueDisplay = trigger.querySelector('.custom-select-value');
|
||||
|
||||
// Remove existing listeners by cloning elements
|
||||
const newTrigger = trigger.cloneNode(true);
|
||||
trigger.parentNode.replaceChild(newTrigger, trigger);
|
||||
|
||||
const newOptions = options.cloneNode(true);
|
||||
options.parentNode.replaceChild(newOptions, options);
|
||||
|
||||
// Re-get elements after cloning
|
||||
const freshTrigger = $('#reminder-assignee-trigger');
|
||||
const freshOptions = $('#reminder-assignee-options');
|
||||
const freshValueDisplay = freshTrigger.querySelector('.custom-select-value');
|
||||
|
||||
// Toggle dropdown
|
||||
freshTrigger.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
wrapper.classList.toggle('open');
|
||||
console.log('[Reminder] Dropdown toggled, open:', wrapper.classList.contains('open'));
|
||||
});
|
||||
|
||||
// Handle option selection
|
||||
freshOptions.addEventListener('click', (e) => {
|
||||
const option = e.target.closest('.custom-select-option');
|
||||
if (!option) return;
|
||||
|
||||
const value = option.dataset.value;
|
||||
const displayName = option.dataset.displayName;
|
||||
const initials = option.dataset.initials;
|
||||
const color = option.dataset.color;
|
||||
|
||||
// Update hidden input
|
||||
hiddenInput.value = value;
|
||||
|
||||
// Update display
|
||||
if (value === '') {
|
||||
freshValueDisplay.innerHTML = 'Alle Benutzer';
|
||||
} else {
|
||||
freshValueDisplay.innerHTML = `
|
||||
<div class="selected-user-avatar" style="background-color: ${color}">${initials}</div>
|
||||
<span>${displayName}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Update selected state
|
||||
freshOptions.querySelectorAll('.custom-select-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
});
|
||||
option.classList.add('selected');
|
||||
|
||||
// Close dropdown
|
||||
wrapper.classList.remove('open');
|
||||
});
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!wrapper.contains(e.target)) {
|
||||
wrapper.classList.remove('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadReminder() {
|
||||
try {
|
||||
const response = await api.request(`/reminders/${this.reminderId}`);
|
||||
const reminder = response.data;
|
||||
|
||||
$('#reminder-title').value = reminder.title || '';
|
||||
$('#reminder-description').value = reminder.description || '';
|
||||
$('#reminder-date').value = reminder.reminder_date || '';
|
||||
$('#reminder-time').value = reminder.reminder_time || '09:00';
|
||||
$('#reminder-assignee').value = reminder.assigned_to || '';
|
||||
|
||||
// Set color
|
||||
this.selectedColor = reminder.color || '#F59E0B';
|
||||
$('#reminder-color').value = this.selectedColor;
|
||||
$$('.color-option').forEach(opt => opt.classList.remove('selected'));
|
||||
$$(`[data-color="${this.selectedColor}"]`).forEach(opt => opt.classList.add('selected'));
|
||||
|
||||
// Set advance days
|
||||
const advanceDays = reminder.advance_days || ['1'];
|
||||
$$('input[name="advance-days"]').forEach(cb => {
|
||||
cb.checked = advanceDays.includes(cb.value);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading reminder:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const saveBtn = $('#btn-save-reminder');
|
||||
const btnText = saveBtn.querySelector('.btn-text');
|
||||
const btnLoading = saveBtn.querySelector('.btn-loading');
|
||||
|
||||
// Show loading
|
||||
btnText.classList.add('hidden');
|
||||
btnLoading.classList.remove('hidden');
|
||||
saveBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData(this.form);
|
||||
|
||||
// Get advance days
|
||||
const advanceDays = [];
|
||||
$$('input[name="advance-days"]:checked').forEach(cb => {
|
||||
advanceDays.push(cb.value);
|
||||
});
|
||||
|
||||
if (advanceDays.length === 0) {
|
||||
throw new Error('Bitte wählen Sie mindestens eine Erinnerungszeit aus');
|
||||
}
|
||||
|
||||
const data = {
|
||||
project_id: store.get('currentProjectId'),
|
||||
title: formData.get('reminder-title') || $('#reminder-title').value,
|
||||
description: $('#reminder-description').value || null,
|
||||
reminder_date: $('#reminder-date').value,
|
||||
reminder_time: $('#reminder-time').value,
|
||||
assigned_to: $('#reminder-assignee').value || null,
|
||||
color: this.selectedColor,
|
||||
advance_days: advanceDays
|
||||
};
|
||||
|
||||
if (this.mode === 'edit') {
|
||||
await api.request(`/reminders/${this.reminderId}`, {
|
||||
method: 'PUT',
|
||||
body: data
|
||||
});
|
||||
} else {
|
||||
await api.request('/reminders', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
});
|
||||
}
|
||||
|
||||
// Update store and refresh calendar
|
||||
if (this.mode === 'edit') {
|
||||
const updatedData = await api.request(`/reminders/${this.reminderId}`);
|
||||
store.updateReminder(this.reminderId, updatedData.data);
|
||||
} else {
|
||||
// Load all reminders to get the new one
|
||||
const projectId = store.get('currentProjectId');
|
||||
const allReminders = await api.getReminders(projectId);
|
||||
store.setReminders(allReminders);
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('app:refresh'));
|
||||
|
||||
this.close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving reminder:', error);
|
||||
alert(`Fehler beim Speichern: ${error.message || 'Unbekannter Fehler'}`);
|
||||
} finally {
|
||||
// Hide loading
|
||||
btnText.classList.remove('hidden');
|
||||
btnLoading.classList.add('hidden');
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper Methods
|
||||
getInitials(name) {
|
||||
if (!name) return '?';
|
||||
|
||||
return name
|
||||
.split(' ')
|
||||
.map(part => part.charAt(0).toUpperCase())
|
||||
.slice(0, 2)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// API Methods
|
||||
async getRemindersByProject(projectId) {
|
||||
try {
|
||||
const response = await api.request(`/reminders?project_id=${projectId}`);
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching reminders:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async handleDelete() {
|
||||
if (!this.reminderId) return;
|
||||
|
||||
const reminderTitle = $('#reminder-title').value || 'diese Erinnerung';
|
||||
|
||||
if (!confirm(`Möchten Sie "${reminderTitle}" wirklich löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.deleteReminder(this.reminderId);
|
||||
this.close();
|
||||
|
||||
// Reload reminders
|
||||
const projectId = store.get('currentProjectId');
|
||||
const allReminders = await api.getReminders(projectId);
|
||||
store.setReminders(allReminders);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('app:refresh'));
|
||||
} catch (error) {
|
||||
console.error('Error deleting reminder:', error);
|
||||
alert('Fehler beim Löschen der Erinnerung. Bitte versuche es erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteReminder(id) {
|
||||
try {
|
||||
await api.request(`/reminders/${id}`, { method: 'DELETE' });
|
||||
window.dispatchEvent(new CustomEvent('app:refresh'));
|
||||
} catch (error) {
|
||||
console.error('Error deleting reminder:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
const reminderManager = new ReminderManager();
|
||||
export default reminderManager;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren