import React, { useState, useEffect } from 'react'; import { useAuth } from '../../contexts/AuthContext'; import { departmentsAPI, kanbanAPI } from '../../services/api'; import ConfirmDialog from '../Common/ConfirmDialog'; import type { KanbanCard, User, Department, KanbanChecklistWithItems } from '../../types'; const AddChecklistItemForm: React.FC<{ checklistId: number; onAdd: (checklistId: number, title: string) => void }> = ({ checklistId, onAdd }) => { const [title, setTitle] = useState(''); const [isAdding, setIsAdding] = useState(false); const handleSubmit = () => { if (title.trim()) { onAdd(checklistId, title); setTitle(''); setIsAdding(false); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSubmit(); } else if (e.key === 'Escape') { setTitle(''); setIsAdding(false); } }; if (!isAdding) { return ( ); } return (
setTitle(e.target.value)} onKeyPress={handleKeyPress} placeholder="Aufgaben-Titel eingeben..." className="flex-1 p-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white" autoFocus />
); }; interface KanbanCardModalProps { card: KanbanCard; onClose: () => void; onUpdate: (cardId: number, updates: Partial) => void; } const KanbanCardModal: React.FC = ({ card, onClose, onUpdate }) => { const { user } = useAuth(); const [title, setTitle] = useState(card.title); const [description, setDescription] = useState(card.description || ''); const [assigneeId, setAssigneeId] = useState(card.assignee_id); const [dueDate, setDueDate] = useState(card.due_date ? card.due_date.split('T')[0] : ''); const [priority, setPriority] = useState<'low' | 'medium' | 'high'>(card.priority || 'medium'); const [labels, setLabels] = useState(card.labels || ''); const [availableUsers, setAvailableUsers] = useState([]); const [checklists, setChecklists] = useState([]); const [showChecklistForm, setShowChecklistForm] = useState(false); const [newChecklistTitle, setNewChecklistTitle] = useState(''); const [activeTab, setActiveTab] = useState<'overview' | 'attachments' | 'comments' | 'activity'>('overview'); const [comments, setComments] = useState([]); const [newComment, setNewComment] = useState(''); const [attachments, setAttachments] = useState([]); const [uploading, setUploading] = useState(false); const [postingComment, setPostingComment] = useState(false); const [activity, setActivity] = useState([]); const fileInputRef = React.useRef(null); // Confirm Dialog States const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [deleteItemType, setDeleteItemType] = useState<'comment' | 'attachment' | null>(null); const [deleteItemId, setDeleteItemId] = useState(null); useEffect(() => { loadAvailableUsers(); }, []); const loadAvailableUsers = async () => { if (!user) return; try { // Get all departments the user has access to const departments: Department[] = await departmentsAPI.getMy(); // Collect all users from these departments const userSet = new Map(); for (const dept of departments) { try { const deptUsers: User[] = await departmentsAPI.getUsers(dept.id); for (const deptUser of deptUsers) { userSet.set(deptUser.id, deptUser); } } catch (error) { console.error(`Failed to load users for department ${dept.id}:`, error); } } setAvailableUsers(Array.from(userSet.values())); } catch (error) { console.error('Failed to load available users:', error); } }; useEffect(() => { if (activeTab === 'activity') { loadActivity(); } }, [activeTab, card.id]); const loadActivity = async () => { try { const activityData = await kanbanAPI.getCardActivity(card.id); setActivity(activityData); } catch (error) { console.error('Failed to load activity:', error); } }; // Auto-save functions for individual fields const autoSaveTitle = () => { if (title.trim() !== card.title) { onUpdate(card.id, { title: title.trim() }); } }; const autoSaveDescription = () => { const desc = description.trim() || undefined; if (desc !== (card.description || undefined)) { onUpdate(card.id, { description: desc }); } }; const autoSavePriority = () => { if (priority !== (card.priority || 'medium')) { onUpdate(card.id, { priority }); } }; const autoSaveDueDate = () => { const date = dueDate ? new Date(dueDate).toISOString() : undefined; if (date !== card.due_date) { onUpdate(card.id, { due_date: date }); } }; const autoSaveLabels = () => { const lbls = labels.trim() || undefined; if (lbls !== (card.labels || undefined)) { onUpdate(card.id, { labels: lbls }); } }; // Checklist functions const loadChecklists = async () => { try { const cardChecklists = await kanbanAPI.getCardChecklists(card.id); setChecklists(cardChecklists); } catch (error) { console.error('Failed to load checklists:', error); } }; const handleCreateChecklist = async () => { if (!newChecklistTitle.trim()) return; try { const newChecklist = await kanbanAPI.createChecklist({ card_id: card.id, title: newChecklistTitle.trim(), position: checklists.length }); setChecklists(prev => [...prev, { ...newChecklist, items: [] }]); setNewChecklistTitle(''); setShowChecklistForm(false); } catch (error) { console.error('Failed to create checklist:', error); } }; const handleDeleteChecklist = async (checklistId: number) => { if (!confirm('Checkliste wirklich löschen?')) return; try { await kanbanAPI.deleteChecklist(checklistId); setChecklists(prev => prev.filter(c => c.id !== checklistId)); } catch (error) { console.error('Failed to delete checklist:', error); } }; const handleCreateChecklistItem = async (checklistId: number, title: string) => { try { const checklist = checklists.find(c => c.id === checklistId); if (!checklist) return; const newItem = await kanbanAPI.createChecklistItem({ checklist_id: checklistId, title: title.trim(), position: checklist.items.length }); setChecklists(prev => prev.map(c => c.id === checklistId ? { ...c, items: [...c.items, newItem] } : c )); } catch (error) { console.error('Failed to create checklist item:', error); } }; const handleToggleChecklistItem = async (itemId: number, completed: boolean) => { try { await kanbanAPI.updateChecklistItem(itemId, { is_completed: completed }); setChecklists(prev => prev.map(checklist => ({ ...checklist, items: checklist.items.map(item => item.id === itemId ? { ...item, is_completed: completed } : item ) }))); } catch (error) { console.error('Failed to update checklist item:', error); } }; const handleDeleteChecklistItem = async (itemId: number) => { try { await kanbanAPI.deleteChecklistItem(itemId); setChecklists(prev => prev.map(checklist => ({ ...checklist, items: checklist.items.filter(item => item.id !== itemId) }))); } catch (error) { console.error('Failed to delete checklist item:', error); } }; useEffect(() => { loadAvailableUsers(); loadChecklists(); loadAttachments(); loadComments(); }, []); const loadComments = async () => { try { const data = await kanbanAPI.getCardComments(card.id); setComments(Array.isArray(data) ? data : []); } catch (error) { console.error('Failed to load comments:', error); setComments([]); } }; const handlePostComment = async () => { if (!newComment.trim()) return; setPostingComment(true); try { const comment = await kanbanAPI.createComment({ card_id: card.id, content: newComment.trim() }); setComments(prev => [...prev, comment]); setNewComment(''); } catch (error) { console.error('Failed to post comment:', error); alert('Fehler beim Posten des Kommentars'); } finally { setPostingComment(false); } }; const handleDeleteComment = (commentId: number) => { setDeleteItemType('comment'); setDeleteItemId(commentId); setShowConfirmDialog(true); }; const loadAttachments = async () => { try { const data = await kanbanAPI.getCardAttachments(card.id); setAttachments(Array.isArray(data) ? data : []); } catch (error) { console.error('Failed to load attachments:', error); setAttachments([]); } }; const handleUploadAttachment = async (file: File) => { if (!file) return; setUploading(true); try { const newAttachment = await kanbanAPI.uploadAttachment(card.id, file); setAttachments(prev => [...prev, newAttachment]); if (fileInputRef.current) { fileInputRef.current.value = ''; } } catch (error) { console.error('Failed to upload attachment:', error); alert('Fehler beim Hochladen der Datei'); } finally { setUploading(false); } }; const handleDeleteAttachment = (attachmentId: number) => { setDeleteItemType('attachment'); setDeleteItemId(attachmentId); setShowConfirmDialog(true); }; const handleConfirmDelete = async () => { if (!deleteItemId || !deleteItemType) return; try { if (deleteItemType === 'attachment') { await kanbanAPI.deleteAttachment(deleteItemId); setAttachments(prev => prev.filter(a => a.id !== deleteItemId)); } else if (deleteItemType === 'comment') { await kanbanAPI.deleteComment(deleteItemId); setComments(prev => prev.filter(c => c.id !== deleteItemId)); } } catch (error) { console.error('Failed to delete item:', error); } finally { setShowConfirmDialog(false); setDeleteItemType(null); setDeleteItemId(null); } }; const handleDownloadAttachment = async (attachment: any) => { try { const blob = await kanbanAPI.downloadAttachment(attachment.id); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = attachment.original_filename || attachment.filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); } catch (error) { console.error('Failed to download attachment:', error); } }; return (
{ if (e.target === e.currentTarget) { onClose(); } }} >
{/* Header */}
setTitle(e.target.value)} onBlur={autoSaveTitle} className="flex-1 text-xl font-bold text-gray-900 dark:text-white bg-transparent border-b border-transparent hover:border-gray-300 dark:hover:border-gray-600 focus:border-blue-500 focus:outline-none resize-none overflow-y-auto max-h-20" placeholder="Kartentitel eingeben..." style={{ minHeight: '2rem' }} />
{card.is_archived && ( )}
{/* Tabs Navigation */}
{/* Content */}
{/* Overview Tab - Details & Einstellungen */} {activeTab === 'overview' && (
{/* Description */}