Kontakt-Modul
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
623bbdf5dd
Commit
7d67557be4
@ -645,6 +645,31 @@ class ApiClient {
|
||||
return token ? `${url}?token=${encodeURIComponent(token)}` : url;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// REMINDER ENDPOINTS
|
||||
// =====================
|
||||
|
||||
async getReminders(projectId) {
|
||||
const response = await this.get(`/reminders?project_id=${projectId}`);
|
||||
return response.data || response; // Extract data property or fallback to response
|
||||
}
|
||||
|
||||
async createReminder(data) {
|
||||
return this.post('/reminders', data);
|
||||
}
|
||||
|
||||
async updateReminder(reminderId, data) {
|
||||
return this.put(`/reminders/${reminderId}`, data);
|
||||
}
|
||||
|
||||
async deleteReminder(reminderId) {
|
||||
return this.delete(`/reminders/${reminderId}`);
|
||||
}
|
||||
|
||||
async getDueReminders() {
|
||||
return this.get('/reminders/due/check');
|
||||
}
|
||||
|
||||
// =====================
|
||||
// LINK ENDPOINTS
|
||||
// =====================
|
||||
@ -1258,6 +1283,35 @@ class ApiClient {
|
||||
async validateCodingPath(path) {
|
||||
return this.post('/coding/validate-path', { path });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTACTS
|
||||
// =============================================================================
|
||||
|
||||
async getContacts(params = {}) {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
return this.get(`/contacts${queryString ? '?' + queryString : ''}`);
|
||||
}
|
||||
|
||||
async getContact(id) {
|
||||
return this.get(`/contacts/${id}`);
|
||||
}
|
||||
|
||||
async createContact(data) {
|
||||
return this.post('/contacts', data);
|
||||
}
|
||||
|
||||
async updateContact(id, data) {
|
||||
return this.put(`/contacts/${id}`, data);
|
||||
}
|
||||
|
||||
async deleteContact(id) {
|
||||
return this.delete(`/contacts/${id}`);
|
||||
}
|
||||
|
||||
async getContactTags() {
|
||||
return this.get('/contacts/tags/all');
|
||||
}
|
||||
}
|
||||
|
||||
// Custom API Error Class
|
||||
|
||||
@ -23,6 +23,7 @@ import giteaManager from './gitea.js';
|
||||
import knowledgeManager from './knowledge.js';
|
||||
import codingManager from './coding.js';
|
||||
import mobileManager from './mobile.js';
|
||||
import reminderManager from './reminders.js';
|
||||
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
|
||||
|
||||
class App {
|
||||
@ -152,15 +153,17 @@ class App {
|
||||
store.setCurrentProject(projectId);
|
||||
|
||||
try {
|
||||
const [columns, tasks, labels] = await Promise.all([
|
||||
const [columns, tasks, labels, reminders] = await Promise.all([
|
||||
api.getColumns(projectId),
|
||||
api.getTasks(projectId),
|
||||
api.getLabels(projectId)
|
||||
api.getLabels(projectId),
|
||||
api.getReminders(projectId)
|
||||
]);
|
||||
|
||||
store.setColumns(columns);
|
||||
store.setTasks(tasks);
|
||||
store.setLabels(labels);
|
||||
store.setReminders(reminders);
|
||||
|
||||
// Update project selector
|
||||
const projectSelect = $('#project-select');
|
||||
@ -667,6 +670,28 @@ class App {
|
||||
} else {
|
||||
knowledgeManager.hide();
|
||||
}
|
||||
|
||||
// Initialize contacts view when switching to it
|
||||
if (view === 'contacts') {
|
||||
window.initContactsPromise = window.initContactsPromise || import('./contacts.js').then(module => {
|
||||
if (module.initContacts) {
|
||||
return module.initContacts();
|
||||
}
|
||||
});
|
||||
window.initContactsPromise.catch(console.error);
|
||||
}
|
||||
|
||||
// Render list view when switching to it (delayed to ensure store is updated)
|
||||
if (view === 'list') {
|
||||
setTimeout(() => listViewManager.render(), 10);
|
||||
}
|
||||
|
||||
// Render calendar view when switching to it
|
||||
if (view === 'calendar') {
|
||||
setTimeout(() => {
|
||||
calendarViewManager.render();
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
@ -886,6 +911,14 @@ class App {
|
||||
// Clear view-specific search
|
||||
proposalsManager.setSearchQuery('');
|
||||
knowledgeManager.setSearchQuery('');
|
||||
|
||||
// Clear contacts search
|
||||
import('./contacts.js').then(module => {
|
||||
if (module.contactsManager) {
|
||||
module.contactsManager.searchQuery = '';
|
||||
module.contactsManager.filterContacts();
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel any pending server search
|
||||
if (searchAbortController) {
|
||||
@ -961,6 +994,14 @@ class App {
|
||||
} else if (currentView === 'knowledge') {
|
||||
// Search knowledge base
|
||||
knowledgeManager.setSearchQuery(value);
|
||||
} else if (currentView === 'contacts') {
|
||||
// Search contacts
|
||||
import('./contacts.js').then(module => {
|
||||
if (module.contactsManager) {
|
||||
module.contactsManager.searchQuery = value;
|
||||
module.contactsManager.filterContacts();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Immediate client-side filtering for tasks
|
||||
store.setFilter('search', value);
|
||||
|
||||
@ -372,7 +372,14 @@ class BoardManager {
|
||||
const currentUser = users.find(u => u.id === assignee.id);
|
||||
const color = currentUser?.color || assignee.color || '#888';
|
||||
const name = currentUser?.display_name || assignee.display_name || assignee.username || 'Benutzer';
|
||||
const initials = currentUser?.initials || assignee.initials || getInitials(name);
|
||||
|
||||
// Initialen berechnen - currentUser hat immer die korrekten initials
|
||||
let initials = currentUser?.initials || getInitials(name) || '?';
|
||||
|
||||
// Sicherheit: Falls initials undefined ist, Fallback verwenden
|
||||
if (!initials || initials === 'undefined') {
|
||||
initials = getInitials(currentUser?.email || name) || '?';
|
||||
}
|
||||
|
||||
const avatar = createElement('span', {
|
||||
className: 'avatar task-assignee-avatar stacked',
|
||||
@ -401,14 +408,20 @@ class BoardManager {
|
||||
const users = store.get('users');
|
||||
const assignedUser = users.find(u => u.id === task.assignedTo);
|
||||
const currentColor = assignedUser?.color || task.assignedColor || '#888';
|
||||
const currentName = assignedUser?.username || task.assignedName || 'Benutzer';
|
||||
const currentName = assignedUser?.display_name || assignedUser?.username || task.assignedName || 'Benutzer';
|
||||
|
||||
// Initialen berechnen
|
||||
let initials = assignedUser?.initials || getInitials(currentName) || '?';
|
||||
if (!initials || initials === 'undefined') {
|
||||
initials = getInitials(assignedUser?.email || currentName) || '?';
|
||||
}
|
||||
|
||||
const assignee = createElement('div', { className: 'task-assignees' }, [
|
||||
createElement('span', {
|
||||
className: 'avatar task-assignee-avatar',
|
||||
style: { backgroundColor: currentColor },
|
||||
title: currentName
|
||||
}, [getInitials(currentName)])
|
||||
}, [initials])
|
||||
]);
|
||||
footer.appendChild(assignee);
|
||||
} else {
|
||||
|
||||
@ -32,10 +32,17 @@ class CalendarViewManager {
|
||||
init() {
|
||||
this.container = $('#view-calendar');
|
||||
|
||||
// Set initial view mode on calendar grid
|
||||
const grid = $('#calendar-grid');
|
||||
if (grid) {
|
||||
grid.classList.add('calendar-month-view');
|
||||
}
|
||||
|
||||
this.bindEvents();
|
||||
|
||||
// Subscribe to store changes
|
||||
store.subscribe('tasks', () => this.render());
|
||||
store.subscribe('reminders', () => this.render());
|
||||
store.subscribe('filters', (filters) => {
|
||||
// Calendar-specific search behavior
|
||||
if (store.get('currentView') === 'calendar') {
|
||||
@ -95,7 +102,7 @@ class CalendarViewManager {
|
||||
// Close popup on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (this.dayDetailPopup && !this.dayDetailPopup.contains(e.target) &&
|
||||
!e.target.closest('.calendar-day')) {
|
||||
!e.target.closest('.calendar-day') && !e.target.closest('.calendar-week-day')) {
|
||||
this.closeDayDetail();
|
||||
}
|
||||
});
|
||||
@ -120,6 +127,12 @@ class CalendarViewManager {
|
||||
grid.classList.add(`calendar-${mode}-view`);
|
||||
}
|
||||
|
||||
// Show/hide weekday headers based on view mode
|
||||
const weekdaysHeader = $('#calendar-weekdays');
|
||||
if (weekdaysHeader) {
|
||||
weekdaysHeader.style.display = mode === 'month' ? 'grid' : 'none';
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
@ -230,13 +243,29 @@ class CalendarViewManager {
|
||||
render() {
|
||||
if (store.get('currentView') !== 'calendar') return;
|
||||
|
||||
// Ensure calendar grid exists and has correct class
|
||||
const grid = $('#calendar-grid');
|
||||
if (!grid) return;
|
||||
|
||||
grid.classList.remove('calendar-month-view', 'calendar-week-view');
|
||||
grid.classList.add(`calendar-${this.viewMode}-view`);
|
||||
|
||||
// Ensure weekday headers visibility matches current view mode
|
||||
const weekdaysHeader = $('#calendar-weekdays');
|
||||
if (weekdaysHeader) {
|
||||
weekdaysHeader.style.display = this.viewMode === 'month' ? 'grid' : 'none';
|
||||
}
|
||||
|
||||
this.updateHeader();
|
||||
|
||||
if (this.viewMode === 'month') {
|
||||
this.renderMonthView();
|
||||
} else {
|
||||
this.renderWeekView();
|
||||
}
|
||||
// Force a small delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
if (this.viewMode === 'month') {
|
||||
this.renderMonthView();
|
||||
} else {
|
||||
this.renderWeekView();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
updateHeader() {
|
||||
@ -264,7 +293,14 @@ class CalendarViewManager {
|
||||
|
||||
renderMonthView() {
|
||||
const daysContainer = $('#calendar-grid');
|
||||
if (!daysContainer) return;
|
||||
if (!daysContainer) {
|
||||
console.warn('[Calendar] calendar-grid not found for month view');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure correct classes are set
|
||||
daysContainer.classList.remove('calendar-week-view');
|
||||
daysContainer.classList.add('calendar-month-view');
|
||||
|
||||
clearElement(daysContainer);
|
||||
|
||||
@ -334,18 +370,20 @@ class CalendarViewManager {
|
||||
const dateString = this.getDateString(date);
|
||||
const isToday = dateString === todayString;
|
||||
|
||||
daysContainer.appendChild(this.createWeekDayElement(date, tasksByDate, isToday));
|
||||
daysContainer.appendChild(this.createWeekDayElement(date, tasksByDate, isToday, weekStart));
|
||||
}
|
||||
}
|
||||
|
||||
createWeekDayElement(date, tasksByDate, isToday = false) {
|
||||
createWeekDayElement(date, tasksByDate, isToday = false, weekStart = null) {
|
||||
const dateString = this.getDateString(date);
|
||||
const dayTasks = tasksByDate[dateString] || [];
|
||||
const remindersByDate = this.getRemindersByDate();
|
||||
const dayReminders = remindersByDate[dateString] || [];
|
||||
const hasOverdue = dayTasks.some(t => this.isTaskOverdue(t));
|
||||
|
||||
const classes = ['calendar-week-day'];
|
||||
if (isToday) classes.push('today');
|
||||
if (dayTasks.length > 0) classes.push('has-tasks');
|
||||
if (dayTasks.length > 0 || dayReminders.length > 0) classes.push('has-tasks');
|
||||
if (hasOverdue) classes.push('has-overdue');
|
||||
|
||||
const dayEl = createElement('div', {
|
||||
@ -367,12 +405,58 @@ class CalendarViewManager {
|
||||
// Tasks container
|
||||
const tasksContainer = createElement('div', { className: 'calendar-week-day-tasks' });
|
||||
|
||||
if (dayTasks.length === 0) {
|
||||
// Combined array of reminders and tasks
|
||||
const allItems = [];
|
||||
|
||||
// Add tasks first (wichtig für kontinuierliche Balken)
|
||||
dayTasks.forEach(task => {
|
||||
allItems.push({
|
||||
type: 'task',
|
||||
...task
|
||||
});
|
||||
});
|
||||
|
||||
// Add reminders after tasks
|
||||
dayReminders.forEach(reminder => {
|
||||
allItems.push({
|
||||
type: 'reminder',
|
||||
id: reminder.id,
|
||||
title: reminder.title,
|
||||
color: reminder.color || '#F59E0B',
|
||||
time: reminder.reminder_time
|
||||
});
|
||||
});
|
||||
|
||||
if (allItems.length === 0) {
|
||||
tasksContainer.appendChild(createElement('div', {
|
||||
className: 'calendar-week-empty'
|
||||
}, ['Keine Aufgaben']));
|
||||
} else {
|
||||
dayTasks.forEach(task => {
|
||||
allItems.forEach(item => {
|
||||
if (item.type === 'reminder') {
|
||||
// Create reminder element
|
||||
const reminderEl = createElement('div', {
|
||||
className: 'calendar-week-task calendar-reminder-item',
|
||||
dataset: { reminderId: item.id },
|
||||
title: `Erinnerung: ${item.title}${item.time ? ' um ' + item.time : ''}`,
|
||||
onclick: (e) => {
|
||||
e.stopPropagation();
|
||||
this.editReminder(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Add title and bell icon
|
||||
reminderEl.appendChild(createElement('span', { className: 'calendar-week-task-title' }, [item.title]));
|
||||
reminderEl.appendChild(createElement('span', { className: 'calendar-reminder-bell' }, ['🔔']));
|
||||
|
||||
// Apply color
|
||||
reminderEl.style.backgroundColor = `${item.color}25`;
|
||||
reminderEl.style.borderLeftColor = item.color;
|
||||
|
||||
tasksContainer.appendChild(reminderEl);
|
||||
} else {
|
||||
// Existing task rendering code
|
||||
const task = item;
|
||||
// Get column color and user badge info
|
||||
const columnColor = this.getColumnColor(task);
|
||||
const userBadge = this.getUserBadgeInfo(task);
|
||||
@ -391,13 +475,20 @@ class CalendarViewManager {
|
||||
taskClasses.push('search-highlight');
|
||||
}
|
||||
|
||||
// Build task element children
|
||||
const children = [
|
||||
createElement('span', { className: 'calendar-week-task-title' }, [task.title])
|
||||
];
|
||||
// Check if this is the first day of the week or the start of the task
|
||||
const isFirstDayOfWeek = weekStart && this.getDateString(date) === this.getDateString(weekStart);
|
||||
const showContent = task.isRangeStart || !task.hasRange || isFirstDayOfWeek;
|
||||
|
||||
// Add user badges if assigned (supports multiple)
|
||||
if (userBadge && userBadge.length > 0) {
|
||||
// Build task element children
|
||||
const children = [];
|
||||
|
||||
// Add title (only at start of task or first day of week)
|
||||
if (showContent) {
|
||||
children.push(createElement('span', { className: 'calendar-week-task-title' }, [task.title]));
|
||||
}
|
||||
|
||||
// Add user badges if assigned (only at start of task or first day of week, supports multiple)
|
||||
if (userBadge && userBadge.length > 0 && showContent) {
|
||||
const badgeContainer = createElement('span', { className: 'calendar-task-badges' });
|
||||
userBadge.forEach(badge => {
|
||||
const badgeEl = createElement('span', { className: 'calendar-task-user-badge' }, [badge.initials]);
|
||||
@ -420,6 +511,7 @@ class CalendarViewManager {
|
||||
}
|
||||
|
||||
tasksContainer.appendChild(taskEl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -452,12 +544,14 @@ class CalendarViewManager {
|
||||
createDayElement(date, dayNumber, isOtherMonth, tasksByDate, isToday = false) {
|
||||
const dateString = this.getDateString(date);
|
||||
const dayTasks = tasksByDate[dateString] || [];
|
||||
const remindersByDate = this.getRemindersByDate();
|
||||
const dayReminders = remindersByDate[dateString] || [];
|
||||
const hasOverdue = dayTasks.some(t => this.isTaskOverdue(t));
|
||||
|
||||
const classes = ['calendar-day'];
|
||||
if (isOtherMonth) classes.push('other-month');
|
||||
if (isToday) classes.push('today');
|
||||
if (dayTasks.length > 0) classes.push('has-tasks');
|
||||
if (dayTasks.length > 0 || dayReminders.length > 0) classes.push('has-tasks');
|
||||
if (hasOverdue) classes.push('has-overdue');
|
||||
|
||||
const dayEl = createElement('div', {
|
||||
@ -470,11 +564,57 @@ class CalendarViewManager {
|
||||
className: 'calendar-day-number'
|
||||
}, [dayNumber.toString()]));
|
||||
|
||||
// Tasks preview (show max 3)
|
||||
if (dayTasks.length > 0) {
|
||||
// Combined container for reminders and tasks
|
||||
const allItems = [];
|
||||
|
||||
// Add tasks first (wichtig für kontinuierliche Balken)
|
||||
dayTasks.forEach(task => {
|
||||
allItems.push({
|
||||
type: 'task',
|
||||
...task
|
||||
});
|
||||
});
|
||||
|
||||
// Add reminders after tasks
|
||||
dayReminders.forEach(reminder => {
|
||||
allItems.push({
|
||||
type: 'reminder',
|
||||
id: reminder.id,
|
||||
title: reminder.title,
|
||||
color: reminder.color || '#F59E0B',
|
||||
time: reminder.reminder_time
|
||||
});
|
||||
});
|
||||
|
||||
// Show max 3 items
|
||||
if (allItems.length > 0) {
|
||||
const tasksContainer = createElement('div', { className: 'calendar-day-tasks' });
|
||||
|
||||
dayTasks.slice(0, 3).forEach(task => {
|
||||
allItems.slice(0, 3).forEach(item => {
|
||||
if (item.type === 'reminder') {
|
||||
// Create reminder element
|
||||
const reminderEl = createElement('div', {
|
||||
className: 'calendar-task calendar-reminder-item',
|
||||
dataset: { reminderId: item.id },
|
||||
title: `Erinnerung: ${item.title}${item.time ? ' um ' + item.time : ''}`,
|
||||
onclick: (e) => {
|
||||
e.stopPropagation();
|
||||
this.editReminder(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Add title and bell icon
|
||||
reminderEl.appendChild(createElement('span', { className: 'calendar-task-title' }, [item.title]));
|
||||
reminderEl.appendChild(createElement('span', { className: 'calendar-reminder-bell' }, ['🔔']));
|
||||
|
||||
// Apply color
|
||||
reminderEl.style.backgroundColor = `${item.color}40`;
|
||||
reminderEl.style.borderLeftColor = item.color;
|
||||
|
||||
tasksContainer.appendChild(reminderEl);
|
||||
} else {
|
||||
// Existing task rendering code
|
||||
const task = item;
|
||||
// Get column color and user badge info
|
||||
const columnColor = this.getColumnColor(task);
|
||||
const userBadge = this.getUserBadgeInfo(task);
|
||||
@ -523,12 +663,13 @@ class CalendarViewManager {
|
||||
}
|
||||
|
||||
tasksContainer.appendChild(taskEl);
|
||||
}
|
||||
});
|
||||
|
||||
if (dayTasks.length > 3) {
|
||||
if (allItems.length > 3) {
|
||||
tasksContainer.appendChild(createElement('div', {
|
||||
className: 'calendar-more'
|
||||
}, [`+${dayTasks.length - 3} weitere`]));
|
||||
}, [`+${allItems.length - 3} weitere`]));
|
||||
}
|
||||
|
||||
dayEl.appendChild(tasksContainer);
|
||||
@ -570,18 +711,32 @@ class CalendarViewManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Sort tasks: earliest start date first (so they stay on top throughout their duration),
|
||||
// then alphabetically by title for same start date
|
||||
// Sort tasks: Range tasks first (to ensure continuous bars), then single-day tasks
|
||||
filteredTasks.sort((a, b) => {
|
||||
const startA = a.startDate || a.dueDate || '';
|
||||
const startB = b.startDate || b.dueDate || '';
|
||||
|
||||
// Compare start dates (earliest first = ascending)
|
||||
const endA = a.dueDate || '';
|
||||
const endB = b.dueDate || '';
|
||||
|
||||
// Check if task has range (both start and end date, and they're different)
|
||||
const hasRangeA = a.startDate && a.dueDate && a.startDate !== a.dueDate;
|
||||
const hasRangeB = b.startDate && b.dueDate && b.startDate !== b.dueDate;
|
||||
|
||||
// Range tasks come first
|
||||
if (hasRangeA && !hasRangeB) return -1;
|
||||
if (!hasRangeA && hasRangeB) return 1;
|
||||
|
||||
// If both have ranges or both are single-day, sort by start date
|
||||
if (startA !== startB) {
|
||||
return startA.localeCompare(startB);
|
||||
}
|
||||
|
||||
// Same start date: sort by end date (longer ranges first)
|
||||
if (endA !== endB) {
|
||||
return endB.localeCompare(endA); // Descending to put longer ranges first
|
||||
}
|
||||
|
||||
// Same start date: sort alphabetically by title
|
||||
// Same dates: sort alphabetically by title
|
||||
return (a.title || '').localeCompare(b.title || '', 'de');
|
||||
});
|
||||
|
||||
@ -629,6 +784,32 @@ class CalendarViewManager {
|
||||
return tasksByDate;
|
||||
}
|
||||
|
||||
getRemindersByDate() {
|
||||
const reminders = store.get('reminders') || [];
|
||||
if (!Array.isArray(reminders)) {
|
||||
console.warn('[Calendar] Reminders not available or not an array');
|
||||
return {};
|
||||
}
|
||||
|
||||
const activeReminders = reminders.filter(r => r.is_active);
|
||||
const remindersByDate = {};
|
||||
|
||||
activeReminders.forEach(reminder => {
|
||||
const date = reminder.reminder_date;
|
||||
if (!remindersByDate[date]) {
|
||||
remindersByDate[date] = [];
|
||||
}
|
||||
remindersByDate[date].push(reminder);
|
||||
});
|
||||
|
||||
// Sort reminders by time within each date
|
||||
Object.keys(remindersByDate).forEach(date => {
|
||||
remindersByDate[date].sort((a, b) => (a.reminder_time || '09:00').localeCompare(b.reminder_time || '09:00'));
|
||||
});
|
||||
|
||||
return remindersByDate;
|
||||
}
|
||||
|
||||
getDateString(date) {
|
||||
// Use local date components instead of toISOString() which converts to UTC
|
||||
// This fixes the issue where the date is off by one day due to timezone differences
|
||||
@ -643,9 +824,12 @@ class CalendarViewManager {
|
||||
// =====================
|
||||
|
||||
handleDayClick(e) {
|
||||
console.log('[Calendar] Day click event:', e.target, 'View mode:', this.viewMode);
|
||||
|
||||
// Check if clicked on a specific task
|
||||
const taskEl = e.target.closest('.calendar-task, .calendar-week-task');
|
||||
if (taskEl) {
|
||||
console.log('[Calendar] Task clicked:', taskEl.dataset.taskId);
|
||||
const taskId = parseInt(taskEl.dataset.taskId);
|
||||
this.openTaskModal(taskId);
|
||||
return;
|
||||
@ -653,24 +837,36 @@ class CalendarViewManager {
|
||||
|
||||
// Check if clicked on add button (week view)
|
||||
if (e.target.closest('.calendar-week-add-task')) {
|
||||
console.log('[Calendar] Add task button clicked');
|
||||
return; // Already handled by onclick
|
||||
}
|
||||
|
||||
// For month view, show day detail popup
|
||||
// Show day detail popup for both month and week view
|
||||
if (this.viewMode === 'month') {
|
||||
const dayEl = e.target.closest('.calendar-day');
|
||||
if (!dayEl) return;
|
||||
|
||||
console.log('[Calendar] Month day clicked:', dayEl.dataset.date);
|
||||
const dateString = dayEl.dataset.date;
|
||||
this.showDayDetail(dateString, dayEl);
|
||||
} else if (this.viewMode === 'week') {
|
||||
const dayEl = e.target.closest('.calendar-week-day');
|
||||
console.log('[Calendar] Week day element found:', !!dayEl);
|
||||
if (!dayEl) return;
|
||||
|
||||
console.log('[Calendar] Week day clicked:', dayEl.dataset.date);
|
||||
const dateString = dayEl.dataset.date;
|
||||
this.showDayDetail(dateString, dayEl);
|
||||
}
|
||||
}
|
||||
|
||||
showDayDetail(dateString, anchorEl) {
|
||||
console.log('[Calendar] showDayDetail called:', dateString, anchorEl);
|
||||
this.closeDayDetail();
|
||||
|
||||
const tasksByDate = this.getTasksByDate();
|
||||
const dayTasks = tasksByDate[dateString] || [];
|
||||
console.log('[Calendar] Tasks for date:', dayTasks.length);
|
||||
|
||||
const date = new Date(dateString);
|
||||
const dateDisplay = date.toLocaleDateString('de-DE', {
|
||||
@ -726,13 +922,35 @@ class CalendarViewManager {
|
||||
onclick: () => this.createTaskForDate(dateString)
|
||||
}, ['+ Aufgabe hinzufügen']));
|
||||
|
||||
// Position popup
|
||||
// Add reminder button
|
||||
popup.appendChild(createElement('button', {
|
||||
className: 'btn btn-secondary btn-block',
|
||||
style: { marginTop: 'var(--spacing-sm)' },
|
||||
onclick: () => this.createReminderForDate(dateString)
|
||||
}, ['🔔 Erinnerung hinzufügen']));
|
||||
|
||||
// Position popup - different logic for week vs month view
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
popup.style.top = `${rect.bottom + 8}px`;
|
||||
popup.style.left = `${Math.min(rect.left, window.innerWidth - 350)}px`;
|
||||
|
||||
let popupTop, popupLeft;
|
||||
|
||||
if (this.viewMode === 'week') {
|
||||
// For week view, position at the top of the day element
|
||||
popupTop = Math.max(150, rect.top + 50); // Ensure it's visible, minimum 150px from top
|
||||
popupLeft = Math.min(rect.left, window.innerWidth - 350);
|
||||
} else {
|
||||
// For month view, position below the day element
|
||||
popupTop = rect.bottom + 8;
|
||||
popupLeft = Math.min(rect.left, window.innerWidth - 350);
|
||||
}
|
||||
|
||||
popup.style.top = `${popupTop}px`;
|
||||
popup.style.left = `${popupLeft}px`;
|
||||
console.log('[Calendar] Popup positioning for', this.viewMode, '- Top:', popup.style.top, 'Left:', popup.style.left);
|
||||
|
||||
document.body.appendChild(popup);
|
||||
this.dayDetailPopup = popup;
|
||||
console.log('[Calendar] Popup created and appended to body');
|
||||
}
|
||||
|
||||
closeDayDetail() {
|
||||
@ -778,6 +996,155 @@ class CalendarViewManager {
|
||||
}));
|
||||
}
|
||||
|
||||
createReminderForDate(dateString) {
|
||||
this.closeDayDetail();
|
||||
|
||||
window.dispatchEvent(new CustomEvent('modal:open', {
|
||||
detail: {
|
||||
modalId: 'reminder-modal',
|
||||
mode: 'create',
|
||||
data: {
|
||||
prefill: {
|
||||
date: dateString
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
showRemindersForDate(dateString, anchorEl) {
|
||||
const reminders = this.getRemindersByDate();
|
||||
const dayReminders = reminders[dateString] || [];
|
||||
|
||||
if (dayReminders.length === 0) return;
|
||||
|
||||
// Close existing popup
|
||||
this.closeDayDetail();
|
||||
|
||||
// Create popup
|
||||
const popup = createElement('div', {
|
||||
className: 'calendar-day-detail calendar-reminder-detail',
|
||||
onclick: (e) => e.stopPropagation()
|
||||
});
|
||||
|
||||
this.dayDetailPopup = popup;
|
||||
|
||||
// Header
|
||||
const header = createElement('h3', {}, [`Erinnerungen für ${formatDate(dateString)}`]);
|
||||
popup.appendChild(header);
|
||||
|
||||
// Reminders list
|
||||
const remindersList = createElement('div', { className: 'calendar-detail-reminders' });
|
||||
|
||||
dayReminders.forEach(reminder => {
|
||||
// Create reminder content (clickable for edit)
|
||||
const reminderContent = createElement('div', {
|
||||
className: 'reminder-content',
|
||||
onclick: () => this.editReminder(reminder.id)
|
||||
}, [
|
||||
createElement('div', {
|
||||
className: 'reminder-time',
|
||||
style: { color: reminder.color || '#F59E0B' }
|
||||
}, [reminder.reminder_time || '09:00']),
|
||||
createElement('div', { className: 'reminder-title' }, [reminder.title]),
|
||||
reminder.description ? createElement('div', { className: 'reminder-description' }, [reminder.description]) : null
|
||||
].filter(Boolean));
|
||||
|
||||
// Create delete button
|
||||
const deleteBtn = createElement('button', {
|
||||
className: 'reminder-delete-btn',
|
||||
title: 'Erinnerung löschen',
|
||||
onclick: (e) => {
|
||||
e.stopPropagation();
|
||||
this.deleteReminder(reminder.id, reminder.title);
|
||||
}
|
||||
}, [
|
||||
createElement('svg', {
|
||||
viewBox: '0 0 24 24',
|
||||
width: '16',
|
||||
height: '16'
|
||||
}, [
|
||||
createElement('path', {
|
||||
d: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '2',
|
||||
fill: 'none',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round'
|
||||
})
|
||||
])
|
||||
]);
|
||||
|
||||
// Container with content and delete button
|
||||
const reminderItem = createElement('div', {
|
||||
className: 'calendar-detail-reminder'
|
||||
}, [reminderContent, deleteBtn]);
|
||||
|
||||
remindersList.appendChild(reminderItem);
|
||||
});
|
||||
|
||||
popup.appendChild(remindersList);
|
||||
|
||||
// Add reminder button
|
||||
popup.appendChild(createElement('button', {
|
||||
className: 'btn btn-secondary btn-block',
|
||||
style: { marginTop: 'var(--spacing-md)' },
|
||||
onclick: () => this.createReminderForDate(dateString)
|
||||
}, ['+ Weitere Erinnerung']));
|
||||
|
||||
// Position popup
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
let popupTop = rect.bottom + 8;
|
||||
let popupLeft = Math.min(rect.left, window.innerWidth - 350);
|
||||
|
||||
popup.style.top = `${popupTop}px`;
|
||||
popup.style.left = `${popupLeft}px`;
|
||||
|
||||
document.body.appendChild(popup);
|
||||
}
|
||||
|
||||
editReminder(reminderId) {
|
||||
this.closeDayDetail();
|
||||
|
||||
window.dispatchEvent(new CustomEvent('modal:open', {
|
||||
detail: {
|
||||
modalId: 'reminder-modal',
|
||||
mode: 'edit',
|
||||
data: { reminderId }
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async deleteReminder(reminderId, reminderTitle) {
|
||||
if (!confirm(`Erinnerung "${reminderTitle}" wirklich löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Import reminder manager if needed
|
||||
if (typeof reminderManager !== 'undefined') {
|
||||
await reminderManager.deleteReminder(reminderId);
|
||||
} else {
|
||||
// Fallback direct API call
|
||||
await api.request(`/reminders/${reminderId}`, { method: 'DELETE' });
|
||||
|
||||
// Update store
|
||||
const reminders = store.get('reminders') || [];
|
||||
const updatedReminders = reminders.filter(r => r.id !== reminderId);
|
||||
store.setReminders(updatedReminders);
|
||||
}
|
||||
|
||||
// Close popup and refresh calendar
|
||||
this.closeDayDetail();
|
||||
this.render();
|
||||
|
||||
console.log(`[Calendar] Reminder ${reminderId} deleted`);
|
||||
} catch (error) {
|
||||
console.error('[Calendar] Failed to delete reminder:', error);
|
||||
alert('Fehler beim Löschen der Erinnerung. Bitte versuche es erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// HELPERS
|
||||
// =====================
|
||||
|
||||
543
frontend/js/contacts.js
Normale Datei
543
frontend/js/contacts.js
Normale Datei
@ -0,0 +1,543 @@
|
||||
/**
|
||||
* TASKMATE - Contacts Manager
|
||||
* ===========================
|
||||
* Kontaktverwaltung mit Kartenansicht
|
||||
*/
|
||||
|
||||
import api from './api.js';
|
||||
import { $, $$, formatDate, debounce } from './utils.js';
|
||||
import store from './store.js';
|
||||
|
||||
class ContactsManager {
|
||||
constructor() {
|
||||
this.contacts = [];
|
||||
this.filteredContacts = [];
|
||||
this.allTags = new Set();
|
||||
this.searchQuery = '';
|
||||
this.filterTag = '';
|
||||
this.sortBy = 'created_at';
|
||||
this.sortOrder = 'desc';
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
console.log('[Contacts] init() called, initialized:', this.initialized);
|
||||
|
||||
if (this.initialized) {
|
||||
await this.loadContacts();
|
||||
return;
|
||||
}
|
||||
|
||||
// DOM Elements
|
||||
this.contactsView = $('#view-contacts');
|
||||
this.contactsGrid = $('#contacts-grid');
|
||||
this.contactsEmpty = $('#contacts-empty');
|
||||
this.tagFilter = $('#contacts-tag-filter');
|
||||
this.sortSelect = $('#contacts-sort');
|
||||
this.newContactBtn = $('#btn-new-contact');
|
||||
|
||||
console.log('[Contacts] DOM Elements check:');
|
||||
console.log(' contactsView:', this.contactsView);
|
||||
console.log(' newContactBtn:', this.newContactBtn);
|
||||
console.log(' contactsGrid:', this.contactsGrid);
|
||||
|
||||
// Modal Elements
|
||||
this.contactModal = $('#contact-modal');
|
||||
this.modalTitle = $('#contact-modal-title');
|
||||
this.contactForm = $('#contact-form');
|
||||
|
||||
console.log('[Contacts] Modal Elements check:');
|
||||
console.log(' contactModal:', this.contactModal);
|
||||
console.log(' contactForm:', this.contactForm);
|
||||
this.contactIdInput = $('#contact-id');
|
||||
this.firstNameInput = $('#contact-first-name');
|
||||
this.lastNameInput = $('#contact-last-name');
|
||||
this.companyInput = $('#contact-company');
|
||||
this.positionInput = $('#contact-position');
|
||||
this.emailInput = $('#contact-email');
|
||||
this.phoneInput = $('#contact-phone');
|
||||
this.mobileInput = $('#contact-mobile');
|
||||
this.addressInput = $('#contact-address');
|
||||
this.postalCodeInput = $('#contact-postal-code');
|
||||
this.cityInput = $('#contact-city');
|
||||
this.countryInput = $('#contact-country');
|
||||
this.websiteInput = $('#contact-website');
|
||||
this.notesInput = $('#contact-notes');
|
||||
this.tagsInput = $('#contact-tags');
|
||||
this.deleteContactBtn = $('#btn-delete-contact');
|
||||
|
||||
this.bindEvents();
|
||||
this.initialized = true;
|
||||
console.log('[Contacts] Initialization complete');
|
||||
|
||||
await this.loadContacts();
|
||||
|
||||
// Store subscriptions
|
||||
store.subscribe('contacts', this.handleContactsUpdate.bind(this));
|
||||
|
||||
// Window events
|
||||
window.addEventListener('app:refresh', () => this.loadContacts());
|
||||
window.addEventListener('modal:close', () => this.loadContacts());
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
console.log('[Contacts] bindEvents() called');
|
||||
|
||||
// Tag Filter
|
||||
if (this.tagFilter) {
|
||||
this.tagFilter.addEventListener('change', (e) => {
|
||||
this.filterTag = e.target.value;
|
||||
this.filterContacts();
|
||||
});
|
||||
}
|
||||
|
||||
// Sort
|
||||
if (this.sortSelect) {
|
||||
this.sortSelect.addEventListener('change', (e) => {
|
||||
const [sortBy, sortOrder] = e.target.value.split('-');
|
||||
this.sortBy = sortBy;
|
||||
this.sortOrder = sortOrder;
|
||||
this.sortContacts();
|
||||
this.renderContacts();
|
||||
});
|
||||
}
|
||||
|
||||
// New Contact Button
|
||||
console.log('[Contacts] newContactBtn element:', this.newContactBtn);
|
||||
if (this.newContactBtn) {
|
||||
console.log('[Contacts] Binding click event to newContactBtn');
|
||||
this.newContactBtn.addEventListener('click', () => {
|
||||
console.log('[Contacts] New contact button clicked!');
|
||||
this.showContactModal();
|
||||
});
|
||||
} else {
|
||||
console.warn('[Contacts] newContactBtn not found!');
|
||||
}
|
||||
|
||||
// Modal Form
|
||||
if (this.contactForm) {
|
||||
this.contactForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.saveContact();
|
||||
});
|
||||
}
|
||||
|
||||
// Delete Button
|
||||
if (this.deleteContactBtn) {
|
||||
this.deleteContactBtn.addEventListener('click', () => this.deleteContact());
|
||||
}
|
||||
|
||||
// Modal Close
|
||||
const modalCloseBtn = this.contactModal?.querySelector('.modal-close');
|
||||
if (modalCloseBtn) {
|
||||
modalCloseBtn.addEventListener('click', () => this.hideContactModal());
|
||||
}
|
||||
|
||||
const modalCancelBtn = this.contactModal?.querySelector('.modal-cancel');
|
||||
if (modalCancelBtn) {
|
||||
modalCancelBtn.addEventListener('click', () => this.hideContactModal());
|
||||
}
|
||||
|
||||
// Socket Events
|
||||
const socket = window.socket;
|
||||
if (socket) {
|
||||
socket.on('contact:created', (data) => {
|
||||
console.log('[Contacts] Socket: contact created', data);
|
||||
this.contacts.unshift(data.contact);
|
||||
this.updateTagsList();
|
||||
this.filterContacts();
|
||||
});
|
||||
|
||||
socket.on('contact:updated', (data) => {
|
||||
console.log('[Contacts] Socket: contact updated', data);
|
||||
const index = this.contacts.findIndex(c => c.id === data.contact.id);
|
||||
if (index !== -1) {
|
||||
this.contacts[index] = data.contact;
|
||||
this.updateTagsList();
|
||||
this.filterContacts();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('contact:deleted', (data) => {
|
||||
console.log('[Contacts] Socket: contact deleted', data);
|
||||
this.contacts = this.contacts.filter(c => c.id !== data.contactId);
|
||||
this.updateTagsList();
|
||||
this.filterContacts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadContacts() {
|
||||
try {
|
||||
console.log('[Contacts] Loading contacts...');
|
||||
const response = await api.getContacts();
|
||||
this.contacts = response.data || response || [];
|
||||
|
||||
this.updateTagsList();
|
||||
this.filterContacts();
|
||||
|
||||
console.log(`[Contacts] Loaded ${this.contacts.length} contacts`);
|
||||
} catch (error) {
|
||||
console.error('[Contacts] Error loading contacts:', error);
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: 'Fehler beim Laden der Kontakte', type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
filterContacts() {
|
||||
this.filteredContacts = this.contacts.filter(contact => {
|
||||
// Search filter
|
||||
if (this.searchQuery) {
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
const searchFields = [
|
||||
contact.firstName,
|
||||
contact.lastName,
|
||||
contact.company,
|
||||
contact.email,
|
||||
contact.position,
|
||||
contact.phone,
|
||||
contact.mobile
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
|
||||
if (!searchFields.includes(query)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Tag filter
|
||||
if (this.filterTag && contact.tags) {
|
||||
if (!contact.tags.includes(this.filterTag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.sortContacts();
|
||||
this.renderContacts();
|
||||
}
|
||||
|
||||
sortContacts() {
|
||||
this.filteredContacts.sort((a, b) => {
|
||||
let aVal = a[this.sortBy] || '';
|
||||
let bVal = b[this.sortBy] || '';
|
||||
|
||||
// Handle name sorting
|
||||
if (this.sortBy === 'name') {
|
||||
aVal = `${a.lastName || ''} ${a.firstName || ''}`.trim();
|
||||
bVal = `${b.lastName || ''} ${b.firstName || ''}`.trim();
|
||||
}
|
||||
|
||||
if (typeof aVal === 'string') {
|
||||
aVal = aVal.toLowerCase();
|
||||
bVal = bVal.toLowerCase();
|
||||
}
|
||||
|
||||
if (this.sortOrder === 'asc') {
|
||||
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
||||
} else {
|
||||
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderContacts() {
|
||||
if (!this.contactsGrid) return;
|
||||
|
||||
if (this.filteredContacts.length === 0) {
|
||||
this.contactsGrid.classList.add('hidden');
|
||||
this.contactsEmpty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.contactsGrid.classList.remove('hidden');
|
||||
this.contactsEmpty.classList.add('hidden');
|
||||
|
||||
const html = this.filteredContacts.map(contact => this.createContactCard(contact)).join('');
|
||||
this.contactsGrid.innerHTML = html;
|
||||
|
||||
// Bind card events
|
||||
this.bindCardEvents();
|
||||
}
|
||||
|
||||
createContactCard(contact) {
|
||||
const displayName = this.getContactDisplayName(contact);
|
||||
const initials = this.getContactInitials(contact);
|
||||
const tags = contact.tags || [];
|
||||
|
||||
return `
|
||||
<div class="contact-card" data-contact-id="${contact.id}">
|
||||
<div class="contact-card-header">
|
||||
<div class="contact-avatar">
|
||||
${initials}
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
<button class="btn-icon btn-edit-contact" title="Bearbeiten">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-card-body">
|
||||
<h3 class="contact-name">${displayName}</h3>
|
||||
${contact.company ? `<div class="contact-company">${contact.company}</div>` : ''}
|
||||
${contact.position ? `<div class="contact-position">${contact.position}</div>` : ''}
|
||||
${contact.email ? `<div class="contact-email"><i class="fas fa-envelope"></i> ${contact.email}</div>` : ''}
|
||||
${contact.phone ? `<div class="contact-phone"><i class="fas fa-phone"></i> ${contact.phone}</div>` : ''}
|
||||
${contact.mobile ? `<div class="contact-mobile"><i class="fas fa-mobile"></i> ${contact.mobile}</div>` : ''}
|
||||
</div>
|
||||
${tags.length > 0 ? `
|
||||
<div class="contact-tags">
|
||||
${tags.map(tag => `<span class="contact-tag">${tag}</span>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getContactDisplayName(contact) {
|
||||
const parts = [];
|
||||
if (contact.firstName) parts.push(contact.firstName);
|
||||
if (contact.lastName) parts.push(contact.lastName);
|
||||
|
||||
if (parts.length > 0) {
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
return contact.company || 'Unbenannt';
|
||||
}
|
||||
|
||||
getContactInitials(contact) {
|
||||
let initials = '';
|
||||
|
||||
if (contact.firstName) {
|
||||
initials += contact.firstName.charAt(0).toUpperCase();
|
||||
}
|
||||
if (contact.lastName) {
|
||||
initials += contact.lastName.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
if (!initials && contact.company) {
|
||||
initials = contact.company.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
return initials || '?';
|
||||
}
|
||||
|
||||
bindCardEvents() {
|
||||
// Edit buttons
|
||||
$$('.btn-edit-contact').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const card = btn.closest('.contact-card');
|
||||
const contactId = parseInt(card.dataset.contactId);
|
||||
this.editContact(contactId);
|
||||
});
|
||||
});
|
||||
|
||||
// Card click
|
||||
$$('.contact-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const contactId = parseInt(card.dataset.contactId);
|
||||
this.editContact(contactId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateTagsList() {
|
||||
// Collect all unique tags
|
||||
this.allTags.clear();
|
||||
this.contacts.forEach(contact => {
|
||||
if (contact.tags) {
|
||||
contact.tags.forEach(tag => this.allTags.add(tag));
|
||||
}
|
||||
});
|
||||
|
||||
// Update tag filter dropdown
|
||||
if (this.tagFilter) {
|
||||
const currentValue = this.tagFilter.value;
|
||||
this.tagFilter.innerHTML = '<option value="">Alle Tags</option>';
|
||||
|
||||
Array.from(this.allTags).sort().forEach(tag => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tag;
|
||||
option.textContent = tag;
|
||||
this.tagFilter.appendChild(option);
|
||||
});
|
||||
|
||||
this.tagFilter.value = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
showContactModal(contact = null) {
|
||||
console.log('[Contacts] showContactModal called with:', contact);
|
||||
console.log('[Contacts] contactModal element:', this.contactModal);
|
||||
|
||||
if (!this.contactModal) {
|
||||
console.error('[Contacts] Contact modal not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Contacts] Resetting form and showing modal');
|
||||
|
||||
// Reset form
|
||||
this.contactForm.reset();
|
||||
this.contactIdInput.value = '';
|
||||
|
||||
if (contact) {
|
||||
// Edit mode
|
||||
this.modalTitle.textContent = 'Kontakt bearbeiten';
|
||||
this.deleteContactBtn.classList.remove('hidden');
|
||||
|
||||
// Fill form
|
||||
this.contactIdInput.value = contact.id;
|
||||
this.firstNameInput.value = contact.firstName || '';
|
||||
this.lastNameInput.value = contact.lastName || '';
|
||||
this.companyInput.value = contact.company || '';
|
||||
this.positionInput.value = contact.position || '';
|
||||
this.emailInput.value = contact.email || '';
|
||||
this.phoneInput.value = contact.phone || '';
|
||||
this.mobileInput.value = contact.mobile || '';
|
||||
this.addressInput.value = contact.address || '';
|
||||
this.postalCodeInput.value = contact.postalCode || '';
|
||||
this.cityInput.value = contact.city || '';
|
||||
this.countryInput.value = contact.country || '';
|
||||
this.websiteInput.value = contact.website || '';
|
||||
this.notesInput.value = contact.notes || '';
|
||||
this.tagsInput.value = (contact.tags || []).join(', ');
|
||||
} else {
|
||||
// Create mode
|
||||
this.modalTitle.textContent = 'Neuer Kontakt';
|
||||
this.deleteContactBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Show modal overlay
|
||||
const overlay = $('#modal-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('hidden');
|
||||
overlay.classList.add('visible');
|
||||
}
|
||||
|
||||
this.contactModal.classList.remove('hidden');
|
||||
this.contactModal.classList.add('visible');
|
||||
}
|
||||
|
||||
hideContactModal() {
|
||||
if (this.contactModal) {
|
||||
this.contactModal.classList.remove('visible');
|
||||
this.contactModal.classList.add('hidden');
|
||||
|
||||
// Hide modal overlay
|
||||
const overlay = $('#modal-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('visible');
|
||||
overlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('modal:close'));
|
||||
}
|
||||
}
|
||||
|
||||
async editContact(contactId) {
|
||||
const contact = this.contacts.find(c => c.id === contactId);
|
||||
if (contact) {
|
||||
this.showContactModal(contact);
|
||||
}
|
||||
}
|
||||
|
||||
async saveContact() {
|
||||
const contactId = this.contactIdInput.value;
|
||||
|
||||
const contactData = {
|
||||
firstName: this.firstNameInput.value.trim(),
|
||||
lastName: this.lastNameInput.value.trim(),
|
||||
company: this.companyInput.value.trim(),
|
||||
position: this.positionInput.value.trim(),
|
||||
email: this.emailInput.value.trim(),
|
||||
phone: this.phoneInput.value.trim(),
|
||||
mobile: this.mobileInput.value.trim(),
|
||||
address: this.addressInput.value.trim(),
|
||||
postalCode: this.postalCodeInput.value.trim(),
|
||||
city: this.cityInput.value.trim(),
|
||||
country: this.countryInput.value.trim(),
|
||||
website: this.websiteInput.value.trim(),
|
||||
notes: this.notesInput.value.trim(),
|
||||
tags: this.tagsInput.value.split(',').map(t => t.trim()).filter(Boolean)
|
||||
};
|
||||
|
||||
try {
|
||||
if (contactId) {
|
||||
// Update
|
||||
await api.updateContact(contactId, contactData);
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: 'Kontakt aktualisiert', type: 'success' }
|
||||
}));
|
||||
} else {
|
||||
// Create
|
||||
await api.createContact(contactData);
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: 'Kontakt erstellt', type: 'success' }
|
||||
}));
|
||||
}
|
||||
|
||||
this.hideContactModal();
|
||||
await this.loadContacts();
|
||||
} catch (error) {
|
||||
console.error('[Contacts] Error saving contact:', error);
|
||||
const errorMsg = error.response?.data?.errors?.[0] || 'Fehler beim Speichern';
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: errorMsg, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async deleteContact() {
|
||||
const contactId = this.contactIdInput.value;
|
||||
if (!contactId) return;
|
||||
|
||||
const contact = this.contacts.find(c => c.id === parseInt(contactId));
|
||||
if (!contact) return;
|
||||
|
||||
const displayName = this.getContactDisplayName(contact);
|
||||
|
||||
if (!confirm(`Möchten Sie den Kontakt "${displayName}" wirklich löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.deleteContact(contactId);
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: 'Kontakt gelöscht', type: 'success' }
|
||||
}));
|
||||
this.hideContactModal();
|
||||
await this.loadContacts();
|
||||
} catch (error) {
|
||||
console.error('[Contacts] Error deleting contact:', error);
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: 'Fehler beim Löschen', type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleContactsUpdate(contacts) {
|
||||
this.contacts = contacts;
|
||||
this.updateTagsList();
|
||||
this.filterContacts();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const contactsManager = new ContactsManager();
|
||||
|
||||
// Export instance for external access
|
||||
export { contactsManager };
|
||||
|
||||
// Export functions
|
||||
export async function initContacts() {
|
||||
await contactsManager.init();
|
||||
}
|
||||
|
||||
export function refreshContacts() {
|
||||
return contactsManager.loadContacts();
|
||||
}
|
||||
@ -167,6 +167,9 @@ class KnowledgeManager {
|
||||
|
||||
// Drag & Drop for entries
|
||||
this.bindEntryDragEvents();
|
||||
|
||||
// Sidebar resize functionality
|
||||
this.bindResizeEvents();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@ -1143,6 +1146,79 @@ class KnowledgeManager {
|
||||
store.closeModal(modalId);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SIDEBAR RESIZE
|
||||
// ==========================================
|
||||
|
||||
bindResizeEvents() {
|
||||
// Use native DOM methods instead of $ utility
|
||||
this.resizeHandle = document.getElementById('knowledge-resize-handle');
|
||||
this.knowledgeLayoutContainer = document.querySelector('.knowledge-layout');
|
||||
|
||||
if (!this.resizeHandle || !this.knowledgeLayoutContainer) return;
|
||||
|
||||
// Load saved width from localStorage
|
||||
const savedWidth = localStorage.getItem('knowledge-sidebar-width');
|
||||
if (savedWidth) {
|
||||
this.setSidebarWidth(parseInt(savedWidth));
|
||||
}
|
||||
|
||||
let isResizing = false;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
|
||||
this.resizeHandle.addEventListener('mousedown', (e) => {
|
||||
isResizing = true;
|
||||
startX = e.clientX;
|
||||
startWidth = this.getCurrentSidebarWidth();
|
||||
|
||||
this.resizeHandle.classList.add('dragging');
|
||||
this.knowledgeLayoutContainer.classList.add('resizing');
|
||||
|
||||
document.addEventListener('mousemove', this.handleResize);
|
||||
document.addEventListener('mouseup', this.handleResizeEnd);
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
this.handleResize = (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const deltaX = e.clientX - startX;
|
||||
const newWidth = Math.max(220, Math.min(800, startWidth + deltaX));
|
||||
|
||||
this.setSidebarWidth(newWidth);
|
||||
};
|
||||
|
||||
this.handleResizeEnd = () => {
|
||||
if (!isResizing) return;
|
||||
|
||||
isResizing = false;
|
||||
this.resizeHandle.classList.remove('dragging');
|
||||
this.knowledgeLayoutContainer.classList.remove('resizing');
|
||||
|
||||
document.removeEventListener('mousemove', this.handleResize);
|
||||
document.removeEventListener('mouseup', this.handleResizeEnd);
|
||||
|
||||
// Save current width to localStorage
|
||||
const currentWidth = this.getCurrentSidebarWidth();
|
||||
localStorage.setItem('knowledge-sidebar-width', currentWidth.toString());
|
||||
};
|
||||
}
|
||||
|
||||
getCurrentSidebarWidth() {
|
||||
const computedStyle = getComputedStyle(this.knowledgeLayoutContainer);
|
||||
const gridColumns = computedStyle.gridTemplateColumns;
|
||||
const match = gridColumns.match(/(\d+)px/);
|
||||
return match ? parseInt(match[1]) : 450;
|
||||
}
|
||||
|
||||
setSidebarWidth(width) {
|
||||
if (this.knowledgeLayoutContainer) {
|
||||
this.knowledgeLayoutContainer.style.gridTemplateColumns = `${width}px 1fr`;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// UTILITIES
|
||||
// ==========================================
|
||||
|
||||
@ -318,16 +318,19 @@ class MobileManager {
|
||||
// Don't swipe if modal is open
|
||||
if ($('.modal-overlay:not(.hidden)')) return;
|
||||
|
||||
// Don't swipe on scrollable elements
|
||||
// Don't swipe on specific interactive elements, but allow swipe in column-body
|
||||
const target = e.target;
|
||||
if (target.closest('.column-body') ||
|
||||
target.closest('.modal') ||
|
||||
if (target.closest('.modal') ||
|
||||
target.closest('.calendar-grid') ||
|
||||
target.closest('.knowledge-entry-list') ||
|
||||
target.closest('.list-table') ||
|
||||
target.closest('input') ||
|
||||
target.closest('textarea') ||
|
||||
target.closest('select')) {
|
||||
target.closest('select') ||
|
||||
target.closest('button') ||
|
||||
target.closest('a[href]') ||
|
||||
target.closest('.task-card .priority-stars') ||
|
||||
target.closest('.task-card .task-counts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -372,18 +375,36 @@ class MobileManager {
|
||||
// Prevent scroll
|
||||
e.preventDefault();
|
||||
|
||||
// Show indicators
|
||||
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
||||
|
||||
if (deltaX > this.SWIPE_THRESHOLD && currentIndex > 0) {
|
||||
this.swipeIndicatorLeft?.classList.add('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
} else if (deltaX < -this.SWIPE_THRESHOLD && currentIndex < this.viewOrder.length - 1) {
|
||||
this.swipeIndicatorRight?.classList.add('visible');
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
// Show indicators based on current view
|
||||
if (this.currentView === 'board') {
|
||||
// In board view: show column navigation indicators
|
||||
const columns = $$('.column');
|
||||
const currentColumnIndex = this.getCurrentColumnIndex();
|
||||
|
||||
if (deltaX > this.SWIPE_THRESHOLD && currentColumnIndex > 0) {
|
||||
this.swipeIndicatorLeft?.classList.add('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
} else if (deltaX < -this.SWIPE_THRESHOLD && currentColumnIndex < columns.length - 1) {
|
||||
this.swipeIndicatorRight?.classList.add('visible');
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
} else {
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
}
|
||||
} else {
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
// In other views: show view navigation indicators
|
||||
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
||||
|
||||
if (deltaX > this.SWIPE_THRESHOLD && currentIndex > 0) {
|
||||
this.swipeIndicatorLeft?.classList.add('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
} else if (deltaX < -this.SWIPE_THRESHOLD && currentIndex < this.viewOrder.length - 1) {
|
||||
this.swipeIndicatorRight?.classList.add('visible');
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
} else {
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,14 +425,29 @@ class MobileManager {
|
||||
const isValidSwipe = Math.abs(deltaX) > this.SWIPE_THRESHOLD || velocity > this.SWIPE_VELOCITY_THRESHOLD;
|
||||
|
||||
if (isValidSwipe) {
|
||||
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
||||
if (this.currentView === 'board') {
|
||||
// In board view: navigate between columns
|
||||
const columns = $$('.column');
|
||||
const currentColumnIndex = this.getCurrentColumnIndex();
|
||||
|
||||
if (deltaX > 0 && currentIndex > 0) {
|
||||
// Swipe right - previous view
|
||||
this.switchView(this.viewOrder[currentIndex - 1]);
|
||||
} else if (deltaX < 0 && currentIndex < this.viewOrder.length - 1) {
|
||||
// Swipe left - next view
|
||||
this.switchView(this.viewOrder[currentIndex + 1]);
|
||||
if (deltaX > 0 && currentColumnIndex > 0) {
|
||||
// Swipe right - previous column
|
||||
this.scrollToColumn(currentColumnIndex - 1);
|
||||
} else if (deltaX < 0 && currentColumnIndex < columns.length - 1) {
|
||||
// Swipe left - next column
|
||||
this.scrollToColumn(currentColumnIndex + 1);
|
||||
}
|
||||
} else {
|
||||
// In other views: navigate between views
|
||||
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
||||
|
||||
if (deltaX > 0 && currentIndex > 0) {
|
||||
// Swipe right - previous view
|
||||
this.switchView(this.viewOrder[currentIndex - 1]);
|
||||
} else if (deltaX < 0 && currentIndex < this.viewOrder.length - 1) {
|
||||
// Swipe left - next view
|
||||
this.switchView(this.viewOrder[currentIndex + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -434,6 +470,36 @@ class MobileManager {
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current visible column index in mobile board view
|
||||
*/
|
||||
getCurrentColumnIndex() {
|
||||
const boardContainer = $('.board-container');
|
||||
if (!boardContainer) return 0;
|
||||
|
||||
const containerWidth = boardContainer.offsetWidth;
|
||||
const scrollLeft = boardContainer.scrollLeft;
|
||||
const columnWidth = 300; // Approximate column width in mobile
|
||||
|
||||
return Math.round(scrollLeft / columnWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to specific column in mobile board view
|
||||
*/
|
||||
scrollToColumn(columnIndex) {
|
||||
const boardContainer = $('.board-container');
|
||||
if (!boardContainer) return;
|
||||
|
||||
const columnWidth = 300; // Approximate column width in mobile
|
||||
const targetScrollLeft = columnIndex * columnWidth;
|
||||
|
||||
boardContainer.scrollTo({
|
||||
left: targetScrollLeft,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
// =====================
|
||||
// TOUCH DRAG & DROP
|
||||
// =====================
|
||||
|
||||
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;
|
||||
@ -27,6 +27,7 @@ class Store {
|
||||
columns: [],
|
||||
tasks: [],
|
||||
labels: [],
|
||||
reminders: [],
|
||||
|
||||
// Filters
|
||||
filters: {
|
||||
@ -363,6 +364,34 @@ class Store {
|
||||
}, 'REMOVE_LABEL');
|
||||
}
|
||||
|
||||
// =====================
|
||||
// REMINDER ACTIONS
|
||||
// =====================
|
||||
|
||||
setReminders(reminders) {
|
||||
this.setState({ reminders }, 'SET_REMINDERS');
|
||||
}
|
||||
|
||||
addReminder(reminder) {
|
||||
this.setState({
|
||||
reminders: [...this.state.reminders, reminder]
|
||||
}, 'ADD_REMINDER');
|
||||
}
|
||||
|
||||
updateReminder(reminderId, updates) {
|
||||
this.setState({
|
||||
reminders: this.state.reminders.map(r =>
|
||||
r.id === reminderId ? { ...r, ...updates } : r
|
||||
)
|
||||
}, 'UPDATE_REMINDER');
|
||||
}
|
||||
|
||||
removeReminder(reminderId) {
|
||||
this.setState({
|
||||
reminders: this.state.reminders.filter(r => r.id !== reminderId)
|
||||
}, 'REMOVE_REMINDER');
|
||||
}
|
||||
|
||||
// =====================
|
||||
// FILTER ACTIONS
|
||||
// =====================
|
||||
|
||||
@ -252,6 +252,15 @@ class TaskModalManager {
|
||||
if (mode === 'edit' && this.taskId) {
|
||||
await this.loadTaskData();
|
||||
} else {
|
||||
// Create mode - clear all UI elements and set defaults
|
||||
|
||||
// Clear UI elements (render empty arrays)
|
||||
this.renderSubtasks();
|
||||
this.renderLinks();
|
||||
this.renderFiles();
|
||||
this.renderComments();
|
||||
this.renderHistory();
|
||||
|
||||
// Set default column
|
||||
if (this.columnId) {
|
||||
const columnSelect = $('#task-status');
|
||||
@ -800,6 +809,8 @@ class TaskModalManager {
|
||||
optionsContainer.innerHTML = '';
|
||||
|
||||
users.forEach(user => {
|
||||
// DEBUG: User-Objekt anschauen
|
||||
console.log('[DEBUG] User in Dropdown:', user);
|
||||
const option = createElement('div', { class: 'multi-select-option' });
|
||||
|
||||
const checkbox = createElement('input', {
|
||||
@ -818,9 +829,12 @@ class TaskModalManager {
|
||||
const avatar = createElement('div', {
|
||||
class: 'multi-select-option-avatar',
|
||||
style: `background-color: ${user.color || '#6366F1'}`
|
||||
}, [user.initials || getInitials(user.display_name || user.username)]);
|
||||
}, [user.initials || getInitials(user.display_name || user.email || 'XX')]);
|
||||
|
||||
const name = createElement('span', { class: 'multi-select-option-name' }, [user.display_name || user.username]);
|
||||
// Probiere verschiedene Felder für den Namen
|
||||
const displayName = user.displayName || user.display_name || user.name || user.username || 'Benutzer';
|
||||
console.log('[DEBUG] Display name für user', user.id, ':', displayName);
|
||||
const name = createElement('span', { class: 'multi-select-option-name' }, [displayName]);
|
||||
|
||||
option.appendChild(checkbox);
|
||||
option.appendChild(avatar);
|
||||
@ -876,10 +890,9 @@ class TaskModalManager {
|
||||
} else {
|
||||
const tags = selectedUsers.map(user => `
|
||||
<span class="multi-select-tag">
|
||||
<span class="multi-select-tag-avatar" style="background-color: ${user.color || '#6366F1'}">
|
||||
${getInitials(user.display_name || user.username)}
|
||||
<span class="multi-select-tag-avatar" style="background-color: ${user.color || '#6366F1'}" title="${user.display_name || user.email}">
|
||||
${user.initials || getInitials(user.display_name || user.email || 'XX')}
|
||||
</span>
|
||||
${user.display_name || user.username}
|
||||
</span>
|
||||
`).join('');
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren