/** * TASKMATE - Proposals Manager * ============================ * Genehmigungen (projektbezogen) */ import api from './api.js'; import { $, $$ } from './utils.js'; import authManager from './auth.js'; import store from './store.js'; class ProposalsManager { constructor() { this.proposals = []; this.currentSort = 'date'; this.showArchived = false; this.searchQuery = ''; this.allTasks = []; this.initialized = false; } async init() { console.log('[Proposals] init() called, initialized:', this.initialized); if (this.initialized) { await this.loadProposals(); return; } // DOM Elements - erst bei init() laden this.proposalsView = $('#view-proposals'); this.proposalsList = $('#proposals-list'); this.proposalsEmpty = $('#proposals-empty'); this.sortSelect = $('#proposals-sort'); this.newProposalBtn = $('#btn-new-proposal'); this.archiveBtn = $('#btn-show-proposals-archive'); this.proposalsTitle = $('#proposals-title'); console.log('[Proposals] newProposalBtn found:', !!this.newProposalBtn); // Modal Elements this.proposalModal = $('#proposal-modal'); this.proposalForm = $('#proposal-form'); this.proposalTitle = $('#proposal-title'); this.proposalDescription = $('#proposal-description'); this.proposalTask = $('#proposal-task'); this.bindEvents(); this.initialized = true; console.log('[Proposals] Initialization complete'); await this.loadProposals(); } bindEvents() { console.log('[Proposals] bindEvents() called'); // Sort Change this.sortSelect?.addEventListener('change', () => { this.currentSort = this.sortSelect.value; this.loadProposals(); }); // Archive Toggle Button this.archiveBtn?.addEventListener('click', () => { this.showArchived = !this.showArchived; this.updateArchiveButton(); this.loadProposals(); }); // New Proposal Button if (this.newProposalBtn) { console.log('[Proposals] Binding click event to newProposalBtn'); this.newProposalBtn.addEventListener('click', () => { console.log('[Proposals] Button clicked!'); this.openNewProposalModal(); }); } else { console.error('[Proposals] newProposalBtn not found!'); } // Proposal Form Submit this.proposalForm?.addEventListener('submit', (e) => this.handleProposalSubmit(e)); // Modal close buttons this.proposalModal?.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', () => this.closeModal()); }); } updateArchiveButton() { if (this.archiveBtn) { this.archiveBtn.textContent = this.showArchived ? 'Aktive anzeigen' : 'Archiv anzeigen'; } if (this.proposalsTitle) { this.proposalsTitle.textContent = this.showArchived ? 'Archiv' : 'Genehmigungen'; } } resetToActiveView() { this.showArchived = false; this.searchQuery = ''; this.updateArchiveButton(); this.loadProposals(); } setSearchQuery(query) { this.searchQuery = query.toLowerCase().trim(); this.render(); } getFilteredProposals() { if (!this.searchQuery) { return this.proposals; } return this.proposals.filter(proposal => { const titleMatch = proposal.title?.toLowerCase().includes(this.searchQuery); const descriptionMatch = proposal.description?.toLowerCase().includes(this.searchQuery); return titleMatch || descriptionMatch; }); } getCurrentProjectId() { // Direkt die currentProjectId aus dem Store holen return store.get('currentProjectId') || null; } async loadProposals() { try { const projectId = this.getCurrentProjectId(); this.proposals = await api.getProposals(this.currentSort, this.showArchived, projectId); this.render(); } catch (error) { console.error('Error loading proposals:', error); this.showToast('Fehler beim Laden der Genehmigungen', 'error'); } } async loadTasks() { try { this.allTasks = await api.getAllTasks(); this.populateTaskDropdown(); } catch (error) { console.error('Error loading tasks:', error); } } populateTaskDropdown() { if (!this.proposalTask) return; const currentProjectId = this.getCurrentProjectId(); // Reset dropdown this.proposalTask.innerHTML = ''; // Nur Aufgaben des aktuellen Projekts anzeigen const projectTasks = this.allTasks.filter(task => task.project_id === currentProjectId); projectTasks.forEach(task => { const option = document.createElement('option'); option.value = task.id; option.textContent = task.title; this.proposalTask.appendChild(option); }); } render() { if (!this.proposalsList) return; const filteredProposals = this.getFilteredProposals(); if (filteredProposals.length === 0) { this.proposalsList.classList.add('hidden'); this.proposalsEmpty?.classList.remove('hidden'); // Update empty message based on search if (this.proposalsEmpty) { const h3 = this.proposalsEmpty.querySelector('h3'); const p = this.proposalsEmpty.querySelector('p'); if (this.searchQuery) { if (h3) h3.textContent = 'Keine Treffer'; if (p) p.textContent = 'Keine Genehmigungen entsprechen der Suche.'; } else { if (h3) h3.textContent = 'Keine Genehmigungen vorhanden'; if (p) p.textContent = 'Erstellen Sie die erste Genehmigung!'; } } return; } this.proposalsEmpty?.classList.add('hidden'); this.proposalsList.classList.remove('hidden'); const currentUserId = authManager.getUser()?.id; const canApprove = authManager.hasPermission('genehmigung'); this.proposalsList.innerHTML = filteredProposals.map(proposal => this.renderProposalCard(proposal, currentUserId, canApprove) ).join(''); // Bind event listeners this.bindProposalEvents(); } renderProposalCard(proposal, currentUserId, canApprove) { const isOwn = proposal.created_by === currentUserId; const initial = (proposal.created_by_name || 'U').charAt(0).toUpperCase(); const dateStr = this.formatDate(proposal.created_at); const isArchived = proposal.archived === 1; return `

${this.escapeHtml(proposal.title)}

${isArchived ? ` Archiviert ` : ''} ${proposal.approved ? ` Genehmigt ` : ''}
${proposal.description ? `

${this.escapeHtml(proposal.description)}

` : ''} ${proposal.task_title ? `
Verknüpft mit: ${this.escapeHtml(proposal.task_title)}
` : ''}
${initial} ${this.escapeHtml(proposal.created_by_name)} ${dateStr}
${canApprove && !isArchived ? ` ` : ''} ${canApprove ? ` ` : ''} ${(isOwn || canApprove) && !isArchived ? ` ` : ''}
${proposal.approved && proposal.approved_by_name ? `
Genehmigt von ${this.escapeHtml(proposal.approved_by_name)} am ${this.formatDate(proposal.approved_at)}
` : ''}
`; } bindProposalEvents() { this.proposalsList?.querySelectorAll('.proposal-card').forEach(card => { const proposalId = parseInt(card.dataset.proposalId); const proposal = this.proposals.find(p => p.id === proposalId); // Approve checkbox const approveCheckbox = card.querySelector('[data-action="approve"]'); approveCheckbox?.addEventListener('change', (e) => this.handleApprove(proposalId, e.target.checked)); // Archive button const archiveBtn = card.querySelector('[data-action="archive"]'); archiveBtn?.addEventListener('click', () => this.handleArchive(proposalId, proposal?.archived !== 1)); // Delete button const deleteBtn = card.querySelector('[data-action="delete"]'); deleteBtn?.addEventListener('click', () => this.handleDelete(proposalId)); }); } async handleApprove(proposalId, approved) { try { await api.approveProposal(proposalId, approved); await this.loadProposals(); this.showToast(approved ? 'Genehmigung erteilt' : 'Genehmigung zurückgezogen', 'success'); // Board aktualisieren, damit die Genehmigung auf der Task-Karte aktualisiert wird window.dispatchEvent(new CustomEvent('app:refresh')); } catch (error) { this.showToast(error.message || 'Fehler beim Genehmigen', 'error'); // Revert checkbox await this.loadProposals(); } } async handleArchive(proposalId, archive) { try { await api.archiveProposal(proposalId, archive); await this.loadProposals(); this.showToast(archive ? 'Genehmigung archiviert' : 'Genehmigung wiederhergestellt', 'success'); // Board aktualisieren window.dispatchEvent(new CustomEvent('app:refresh')); } catch (error) { this.showToast(error.message || 'Fehler beim Archivieren', 'error'); } } async handleDelete(proposalId) { const proposal = this.proposals.find(p => p.id === proposalId); if (!proposal) return; const confirmDelete = confirm(`Genehmigung "${proposal.title}" wirklich löschen?`); if (!confirmDelete) return; try { await api.deleteProposal(proposalId); this.showToast('Genehmigung gelöscht', 'success'); await this.loadProposals(); // Board aktualisieren window.dispatchEvent(new CustomEvent('app:refresh')); } catch (error) { this.showToast(error.message || 'Fehler beim Löschen', 'error'); } } async openNewProposalModal() { console.log('[Proposals] Opening new proposal modal'); this.proposalForm?.reset(); await this.loadTasks(); this.openModal(); this.proposalTitle?.focus(); } async handleProposalSubmit(e) { e.preventDefault(); const title = this.proposalTitle.value.trim(); const description = this.proposalDescription.value.trim(); const taskId = this.proposalTask?.value ? parseInt(this.proposalTask.value) : null; const projectId = this.getCurrentProjectId(); if (!title) { this.showToast('Bitte einen Titel eingeben', 'error'); return; } if (!projectId) { this.showToast('Kein Projekt ausgewählt', 'error'); return; } try { await api.createProposal({ title, description, taskId, projectId }); this.showToast('Genehmigung erstellt', 'success'); this.closeModal(); await this.loadProposals(); // Board aktualisieren, damit die Genehmigung auf der Task-Karte erscheint window.dispatchEvent(new CustomEvent('app:refresh')); } catch (error) { this.showToast(error.message || 'Fehler beim Erstellen', 'error'); } } openModal() { if (this.proposalModal) { this.proposalModal.classList.remove('hidden'); this.proposalModal.classList.add('visible'); } const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('hidden'); overlay.classList.add('visible'); } store.openModal('proposal-modal'); } closeModal() { if (this.proposalModal) { this.proposalModal.classList.remove('visible'); this.proposalModal.classList.add('hidden'); } // Only hide overlay if no other modals are open const openModals = store.get('openModals').filter(id => id !== 'proposal-modal'); if (openModals.length === 0) { const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('visible'); overlay.classList.add('hidden'); } } store.closeModal('proposal-modal'); } formatDate(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } showToast(message, type = 'info') { window.dispatchEvent(new CustomEvent('toast:show', { detail: { message, type } })); } escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } show() { this.proposalsView?.classList.remove('hidden'); this.proposalsView?.classList.add('active'); } hide() { this.proposalsView?.classList.add('hidden'); this.proposalsView?.classList.remove('active'); } /** * Scrollt zu einer Genehmigung und hebt sie hervor */ scrollToAndHighlight(proposalId) { const card = this.proposalsList?.querySelector(`[data-proposal-id="${proposalId}"]`); if (!card) { console.warn('[Proposals] Proposal card not found:', proposalId); return; } // Scroll to the card card.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Add highlight class for animation card.classList.add('highlight-pulse'); // Remove highlight class after animation setTimeout(() => { card.classList.remove('highlight-pulse'); }, 2500); } } // Create singleton instance const proposalsManager = new ProposalsManager(); export { proposalsManager }; export default proposalsManager;