first commit

This commit is contained in:
Stefan Hacker
2026-01-29 01:16:54 +01:00
commit 31f807fbd0
12106 changed files with 2480685 additions and 0 deletions
+888
View File
@@ -0,0 +1,888 @@
import { useState, useMemo } from 'react';
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
import { Link, useNavigate } from 'react-router-dom';
import { contractTaskApi, customerApi, contractApi, appSettingsApi } from '../../services/api';
import { useAuth } from '../../context/AuthContext';
import Card from '../../components/ui/Card';
import Button from '../../components/ui/Button';
import Select from '../../components/ui/Select';
import Badge from '../../components/ui/Badge';
import Modal from '../../components/ui/Modal';
import Input from '../../components/ui/Input';
import {
Eye,
CheckCircle,
Circle,
Clock,
User,
Users,
Plus,
ChevronDown,
ChevronRight,
FileText,
Send,
} from 'lucide-react';
import type { ContractTask, ContractTaskStatus, Contract, Customer, ContractTaskSubtask } from '../../types';
const statusLabels: Record<ContractTaskStatus, string> = {
OPEN: 'Offen',
COMPLETED: 'Erledigt',
};
const statusVariants: Record<ContractTaskStatus, 'warning' | 'success'> = {
OPEN: 'warning',
COMPLETED: 'success',
};
export default function TaskList() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { isCustomerPortal, user, hasPermission } = useAuth();
const [statusFilter, setStatusFilter] = useState<string>('OPEN');
const [expandedTasks, setExpandedTasks] = useState<Set<number>>(new Set());
const [showCreateModal, setShowCreateModal] = useState(false);
const [replyInputs, setReplyInputs] = useState<Record<number, string>>({});
// Labels abhängig von Benutzertyp
const pageTitle = isCustomerPortal ? 'Support-Anfragen' : 'Aufgaben';
const taskLabel = isCustomerPortal ? 'Anfrage' : 'Aufgabe';
// Lade öffentliche Einstellungen (für Kundenportal - Support-Tickets aktiviert?)
const { data: publicSettings, isLoading: isLoadingSettings } = useQuery({
queryKey: ['app-settings-public'],
queryFn: () => appSettingsApi.getPublic(),
enabled: isCustomerPortal,
staleTime: 0, // Immer neu laden, damit Einstellungsänderungen sofort wirken
});
// Wichtig: Nur true wenn explizit aktiviert UND geladen
const supportTicketsEnabled = !isLoadingSettings && publicSettings?.data?.customerSupportTicketsEnabled === 'true';
const { data: tasksData, isLoading } = useQuery({
queryKey: ['all-tasks', statusFilter],
queryFn: () => contractTaskApi.getAll({
status: statusFilter as ContractTaskStatus || undefined
}),
staleTime: 0, // Immer neu laden, damit Änderungen sofort sichtbar sind
});
// Mutations für Subtasks
const completeSubtaskMutation = useMutation({
mutationFn: (subtaskId: number) => contractTaskApi.completeSubtask(subtaskId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-stats'] });
},
});
const reopenSubtaskMutation = useMutation({
mutationFn: (subtaskId: number) => contractTaskApi.reopenSubtask(subtaskId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-stats'] });
},
});
const createSubtaskMutation = useMutation({
mutationFn: ({ taskId, title }: { taskId: number; title: string }) =>
isCustomerPortal
? contractTaskApi.createReply(taskId, title)
: contractTaskApi.createSubtask(taskId, title),
onSuccess: (_, { taskId }) => {
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
setReplyInputs(prev => ({ ...prev, [taskId]: '' }));
},
});
// Gruppiere Tasks nach Vertrag für Kundenportal
const groupedTasks = useMemo(() => {
if (!tasksData?.data) return { ownTasks: [], representedTasks: [], allTasks: [] };
const tasks = tasksData.data;
if (!isCustomerPortal) {
// Für Mitarbeiter: Alle Tasks in einer Liste
return { allTasks: tasks, ownTasks: [], representedTasks: [] };
}
// Für Kundenportal: Nach eigenen vs. freigegebenen Kunden gruppieren
const ownTasks: ContractTask[] = [];
const representedTasks: ContractTask[] = [];
for (const task of tasks) {
if (task.contract?.customerId === user?.customerId) {
ownTasks.push(task);
} else {
representedTasks.push(task);
}
}
return { ownTasks, representedTasks, allTasks: [] };
}, [tasksData?.data, isCustomerPortal, user?.customerId]);
const toggleExpanded = (taskId: number) => {
setExpandedTasks(prev => {
const next = new Set(prev);
if (next.has(taskId)) {
next.delete(taskId);
} else {
next.add(taskId);
}
return next;
});
};
const handleSubtaskToggle = (subtask: ContractTaskSubtask) => {
// Verhindere Doppelklicks während Mutation läuft
if (completeSubtaskMutation.isPending || reopenSubtaskMutation.isPending) return;
if (subtask.status === 'COMPLETED') {
reopenSubtaskMutation.mutate(subtask.id);
} else {
completeSubtaskMutation.mutate(subtask.id);
}
};
const handleAddReply = (taskId: number) => {
const title = replyInputs[taskId]?.trim();
if (!title) return;
createSubtaskMutation.mutate({ taskId, title });
};
// Prüfe ob Benutzer Subtasks bearbeiten kann
const canEditSubtasks = !isCustomerPortal && hasPermission('contracts:update');
const renderTask = (task: ContractTask, showCustomer = false) => {
const isExpanded = expandedTasks.has(task.id);
const hasSubtasks = task.subtasks && task.subtasks.length > 0;
const completedSubtasks = task.subtasks?.filter(s => s.status === 'COMPLETED').length || 0;
const totalSubtasks = task.subtasks?.length || 0;
const isTaskCompleted = task.status === 'COMPLETED';
const contractDisplay = task.contract
? `${task.contract.contractNumber} - ${task.contract.provider?.name || task.contract.providerName || 'Kein Anbieter'}`
: `Vertrag #${task.contractId}`;
const customerDisplay = task.contract?.customer
? (task.contract.customer.companyName || `${task.contract.customer.firstName} ${task.contract.customer.lastName}`)
: '';
return (
<div key={task.id} className="border rounded-lg mb-2">
{/* Task Header */}
<div
className="flex items-center p-4 hover:bg-gray-50 cursor-pointer"
onClick={() => toggleExpanded(task.id)}
>
{/* Expand Button */}
<div className="w-6 mr-2">
{isExpanded ? (
<ChevronDown className="w-5 h-5 text-gray-400" />
) : (
<ChevronRight className="w-5 h-5 text-gray-400" />
)}
</div>
{/* Status Icon */}
<div className="mr-3">
{task.status === 'COMPLETED' ? (
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<Clock className="w-5 h-5 text-yellow-500" />
)}
</div>
{/* Task Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium">{task.title}</span>
<Badge variant={statusVariants[task.status]}>
{statusLabels[task.status]}
</Badge>
{hasSubtasks && (
<span className="text-xs text-gray-500">
({completedSubtasks}/{totalSubtasks} erledigt)
</span>
)}
</div>
<div className="text-sm text-gray-500 mt-1 flex items-center gap-2">
<FileText className="w-4 h-4" />
<Link
to={`/contracts/${task.contractId}`}
className="text-blue-600 hover:underline"
onClick={(e) => e.stopPropagation()}
>
{contractDisplay}
</Link>
{showCustomer && customerDisplay && (
<>
<span className="text-gray-400">|</span>
<span>{customerDisplay}</span>
</>
)}
</div>
{task.description && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2">{task.description}</p>
)}
</div>
{/* Actions */}
<div className="ml-4 flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
navigate(`/contracts/${task.contractId}`);
}}
title="Zum Vertrag"
>
<Eye className="w-4 h-4" />
</Button>
</div>
</div>
{/* Expanded Content: Subtasks + Reply */}
{isExpanded && (
<div className="border-t bg-gray-50 px-4 py-3">
{/* Subtasks */}
{hasSubtasks && (
<div className="space-y-2 mb-4">
{task.subtasks?.map((subtask) => {
const createdDate = new Date(subtask.createdAt).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
return (
<div
key={subtask.id}
className={`flex items-start gap-2 text-sm ml-6 ${canEditSubtasks ? 'cursor-pointer hover:bg-gray-100 rounded px-2 py-1 -mx-2' : ''}`}
onClick={canEditSubtasks ? () => handleSubtaskToggle(subtask) : undefined}
>
{/* Radio-Button Style Icon */}
<span className="flex-shrink-0 mt-0.5">
{subtask.status === 'COMPLETED' ? (
<CheckCircle className="w-4 h-4 text-green-500" />
) : (
<Circle className="w-4 h-4 text-gray-400" />
)}
</span>
<span className={subtask.status === 'COMPLETED' ? 'text-gray-500 line-through' : ''}>
{subtask.title}
<span className="text-xs text-gray-400 ml-2">
{subtask.createdBy} {createdDate}
</span>
</span>
</div>
);
})}
</div>
)}
{/* Reply Input - nur anzeigen wenn Task noch offen */}
{!isTaskCompleted && (canEditSubtasks || isCustomerPortal) && (
<div className="flex gap-2 ml-6">
<Input
placeholder={isCustomerPortal ? 'Antwort schreiben...' : 'Neue Unteraufgabe...'}
value={replyInputs[task.id] || ''}
onChange={(e) => setReplyInputs(prev => ({ ...prev, [task.id]: e.target.value }))}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleAddReply(task.id);
}
}}
className="flex-1"
/>
<Button
size="sm"
onClick={() => handleAddReply(task.id)}
disabled={!replyInputs[task.id]?.trim() || createSubtaskMutation.isPending}
>
<Send className="w-4 h-4" />
</Button>
</div>
)}
{/* Keine Subtasks Nachricht */}
{!hasSubtasks && isTaskCompleted && (
<p className="text-gray-500 text-sm text-center py-2">
Keine Unteraufgaben vorhanden.
</p>
)}
</div>
)}
</div>
);
};
// Kann der Benutzer Aufgaben erstellen?
// Für Kundenportal: Nur wenn Setting aktiviert UND geladen ist
const canCreateTask = isCustomerPortal
? supportTicketsEnabled
: hasPermission('contracts:update');
return (
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">{pageTitle}</h1>
{canCreateTask && (
<Button onClick={() => setShowCreateModal(true)}>
<Plus className="w-4 h-4 mr-2" />
Neue {taskLabel}
</Button>
)}
</div>
{/* Filter */}
<Card className="mb-6">
<div className="flex gap-4 flex-wrap items-center">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Status:</span>
<Select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
options={[
{ value: '', label: 'Alle' },
...Object.entries(statusLabels).map(([value, label]) => ({ value, label }))
]}
className="w-40"
/>
</div>
</div>
</Card>
{isLoading ? (
<Card>
<div className="text-center py-8 text-gray-500">Laden...</div>
</Card>
) : (
<>
{/* Kundenportal: Gruppierte Ansicht */}
{isCustomerPortal ? (
<div className="space-y-6">
{/* Eigene Anfragen */}
<Card>
<div className="flex items-center gap-3 mb-4 pb-3 border-b">
<User className="w-5 h-5 text-blue-600" />
<h2 className="text-lg font-semibold text-gray-900">Meine {pageTitle}</h2>
<Badge variant="default">{groupedTasks.ownTasks.length}</Badge>
</div>
{groupedTasks.ownTasks.length > 0 ? (
<div>
{groupedTasks.ownTasks.map((task) => renderTask(task, false))}
</div>
) : (
<p className="text-gray-500 text-center py-4">
Keine eigenen {pageTitle.toLowerCase()} vorhanden.
</p>
)}
</Card>
{/* Fremd-Anfragen - nur anzeigen wenn vorhanden */}
{groupedTasks.representedTasks.length > 0 && (
<Card>
<div className="flex items-center gap-3 mb-4 pb-3 border-b">
<Users className="w-5 h-5 text-purple-600" />
<h2 className="text-lg font-semibold text-gray-900">
{pageTitle} freigegebener Kunden
</h2>
<Badge variant="default">{groupedTasks.representedTasks.length}</Badge>
</div>
<div>
{groupedTasks.representedTasks.map((task) => renderTask(task, true))}
</div>
</Card>
)}
</div>
) : (
/* Mitarbeiter-Ansicht */
<Card>
{groupedTasks.allTasks && groupedTasks.allTasks.length > 0 ? (
<div>
{groupedTasks.allTasks.map((task) => renderTask(task, true))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
Keine {pageTitle.toLowerCase()} gefunden.
</div>
)}
</Card>
)}
</>
)}
{/* Modal für neue Aufgabe/Support-Anfrage */}
{isCustomerPortal ? (
<CreateSupportTicketModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
/>
) : (
<CreateTaskModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
/>
)}
</div>
);
}
// Modal für neue Support-Anfrage (Kundenportal)
function CreateSupportTicketModal({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) {
const { user } = useAuth();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [customerFilter, setCustomerFilter] = useState<'own' | number>('own');
const [selectedContractId, setSelectedContractId] = useState<number | null>(null);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [contractSearch, setContractSearch] = useState('');
// Lade alle Verträge des Benutzers (eigene + freigegebene)
const { data: contractsData } = useQuery({
queryKey: ['contracts', user?.customerId],
queryFn: () => contractApi.getAll({ customerId: user?.customerId }),
enabled: isOpen,
});
// Gruppiere Verträge nach Kunde
const groupedContracts = useMemo(() => {
if (!contractsData?.data) return { own: [], represented: {} as Record<number, { name: string; contracts: Contract[] }> };
const own: Contract[] = [];
const represented: Record<number, { name: string; contracts: Contract[] }> = {};
for (const contract of contractsData.data) {
if (contract.customerId === user?.customerId) {
own.push(contract);
} else {
if (!represented[contract.customerId]) {
const name = contract.customer
? (contract.customer.companyName || `${contract.customer.firstName} ${contract.customer.lastName}`)
: `Kunde ${contract.customerId}`;
represented[contract.customerId] = { name, contracts: [] };
}
represented[contract.customerId].contracts.push(contract);
}
}
return { own, represented };
}, [contractsData?.data, user?.customerId]);
// Hat der Benutzer freigegebene Kunden?
const hasRepresentedCustomers = Object.keys(groupedContracts.represented).length > 0;
// Aktuelle Verträge basierend auf Kundenfilter
const currentContracts = useMemo(() => {
if (customerFilter === 'own') {
return groupedContracts.own;
}
return groupedContracts.represented[customerFilter]?.contracts || [];
}, [customerFilter, groupedContracts]);
// Gefilterte Verträge basierend auf Suche
const filteredContracts = useMemo(() => {
if (!contractSearch) return currentContracts;
const search = contractSearch.toLowerCase();
return currentContracts.filter(c =>
c.contractNumber.toLowerCase().includes(search) ||
(c.providerName || '').toLowerCase().includes(search) ||
(c.tariffName || '').toLowerCase().includes(search)
);
}, [currentContracts, contractSearch]);
const handleSubmit = async () => {
if (!selectedContractId || !title.trim()) return;
setIsSubmitting(true);
try {
await contractTaskApi.createSupportTicket(selectedContractId, {
title: title.trim(),
description: description.trim() || undefined,
});
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-stats'] });
onClose();
// Reset form
setTitle('');
setDescription('');
setSelectedContractId(null);
setCustomerFilter('own');
// Navigate to the contract
navigate(`/contracts/${selectedContractId}`);
} catch (error) {
console.error('Fehler beim Erstellen der Support-Anfrage:', error);
alert('Fehler beim Erstellen der Support-Anfrage. Bitte versuchen Sie es erneut.');
} finally {
setIsSubmitting(false);
}
};
const handleClose = () => {
setTitle('');
setDescription('');
setSelectedContractId(null);
setCustomerFilter('own');
setContractSearch('');
onClose();
};
return (
<Modal
isOpen={isOpen}
onClose={handleClose}
title="Neue Support-Anfrage"
>
<div className="space-y-4">
{/* Kundenauswahl (nur wenn freigegebene Kunden vorhanden) */}
{hasRepresentedCustomers && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kunde
</label>
<select
value={customerFilter}
onChange={(e) => {
const val = e.target.value;
setCustomerFilter(val === 'own' ? 'own' : parseInt(val));
setSelectedContractId(null);
setContractSearch('');
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="own">Eigene Verträge</option>
{Object.entries(groupedContracts.represented).map(([id, { name }]) => (
<option key={id} value={id}>{name}</option>
))}
</select>
</div>
)}
{/* Vertragsauswahl */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Vertrag *
</label>
<Input
placeholder="Vertrag suchen..."
value={contractSearch}
onChange={(e) => setContractSearch(e.target.value)}
className="mb-2"
/>
<div className="max-h-48 overflow-y-auto border rounded-lg">
{filteredContracts.length > 0 ? (
filteredContracts.map((contract) => (
<div
key={contract.id}
onClick={() => setSelectedContractId(contract.id)}
className={`p-3 cursor-pointer border-b last:border-b-0 hover:bg-gray-50 ${
selectedContractId === contract.id ? 'bg-blue-50 border-blue-200' : ''
}`}
>
<div className="font-medium">{contract.contractNumber}</div>
<div className="text-sm text-gray-500">
{contract.providerName || 'Kein Anbieter'}
{contract.tariffName && ` - ${contract.tariffName}`}
</div>
</div>
))
) : (
<div className="p-3 text-gray-500 text-center">
Keine Verträge gefunden.
</div>
)}
</div>
</div>
{/* Titel */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Titel *
</label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Kurze Beschreibung Ihres Anliegens"
/>
</div>
{/* Beschreibung */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Beschreibung
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Detaillierte Beschreibung (optional)"
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Buttons */}
<div className="flex justify-end gap-2 pt-4">
<Button variant="secondary" onClick={handleClose}>
Abbrechen
</Button>
<Button
onClick={handleSubmit}
disabled={!selectedContractId || !title.trim() || isSubmitting}
>
{isSubmitting ? 'Wird erstellt...' : 'Anfrage erstellen'}
</Button>
</div>
</div>
</Modal>
);
}
// Modal für neue Aufgabe (Mitarbeiter/Admin)
function CreateTaskModal({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [selectedCustomerId, setSelectedCustomerId] = useState<number | null>(null);
const [selectedContractId, setSelectedContractId] = useState<number | null>(null);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [visibleInPortal, setVisibleInPortal] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [customerSearch, setCustomerSearch] = useState('');
const [contractSearch, setContractSearch] = useState('');
// Lade Kunden
const { data: customersData } = useQuery({
queryKey: ['customers-for-task'],
queryFn: () => customerApi.getAll({ limit: 100 }),
enabled: isOpen,
});
// Lade Verträge des ausgewählten Kunden
const { data: contractsData } = useQuery({
queryKey: ['contracts-for-task', selectedCustomerId],
queryFn: () => contractApi.getAll({ customerId: selectedCustomerId! }),
enabled: isOpen && selectedCustomerId !== null,
});
// Gefilterte Kunden
const filteredCustomers = useMemo(() => {
if (!customersData?.data) return [];
if (!customerSearch) return customersData.data;
const search = customerSearch.toLowerCase();
return customersData.data.filter(c =>
c.customerNumber.toLowerCase().includes(search) ||
c.firstName.toLowerCase().includes(search) ||
c.lastName.toLowerCase().includes(search) ||
(c.companyName || '').toLowerCase().includes(search)
);
}, [customersData?.data, customerSearch]);
// Gefilterte Verträge
const filteredContracts = useMemo(() => {
if (!contractsData?.data) return [];
if (!contractSearch) return contractsData.data;
const search = contractSearch.toLowerCase();
return contractsData.data.filter(c =>
c.contractNumber.toLowerCase().includes(search) ||
(c.providerName || '').toLowerCase().includes(search) ||
(c.tariffName || '').toLowerCase().includes(search)
);
}, [contractsData?.data, contractSearch]);
const handleSubmit = async () => {
if (!selectedContractId || !title.trim()) return;
setIsSubmitting(true);
try {
await contractTaskApi.create(selectedContractId, {
title: title.trim(),
description: description.trim() || undefined,
visibleInPortal,
});
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-stats'] });
onClose();
// Reset form
setTitle('');
setDescription('');
setVisibleInPortal(false);
setSelectedContractId(null);
setSelectedCustomerId(null);
// Navigate to the contract
navigate(`/contracts/${selectedContractId}`);
} catch (error) {
console.error('Fehler beim Erstellen der Aufgabe:', error);
alert('Fehler beim Erstellen der Aufgabe. Bitte versuchen Sie es erneut.');
} finally {
setIsSubmitting(false);
}
};
const handleClose = () => {
setTitle('');
setDescription('');
setVisibleInPortal(false);
setSelectedContractId(null);
setSelectedCustomerId(null);
setCustomerSearch('');
setContractSearch('');
onClose();
};
const getCustomerDisplay = (customer: Customer) => {
const name = customer.companyName || `${customer.firstName} ${customer.lastName}`;
return `${customer.customerNumber} - ${name}`;
};
return (
<Modal
isOpen={isOpen}
onClose={handleClose}
title="Neue Aufgabe"
>
<div className="space-y-4">
{/* Kundenauswahl */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kunde *
</label>
<Input
placeholder="Kunde suchen..."
value={customerSearch}
onChange={(e) => setCustomerSearch(e.target.value)}
className="mb-2"
/>
<div className="max-h-40 overflow-y-auto border rounded-lg">
{filteredCustomers.length > 0 ? (
filteredCustomers.map((customer) => (
<div
key={customer.id}
onClick={() => {
setSelectedCustomerId(customer.id);
setSelectedContractId(null);
setContractSearch('');
}}
className={`p-3 cursor-pointer border-b last:border-b-0 hover:bg-gray-50 ${
selectedCustomerId === customer.id ? 'bg-blue-50 border-blue-200' : ''
}`}
>
<div className="font-medium">{getCustomerDisplay(customer)}</div>
</div>
))
) : (
<div className="p-3 text-gray-500 text-center">
Keine Kunden gefunden.
</div>
)}
</div>
</div>
{/* Vertragsauswahl (nur wenn Kunde ausgewählt) */}
{selectedCustomerId && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Vertrag *
</label>
<Input
placeholder="Vertrag suchen..."
value={contractSearch}
onChange={(e) => setContractSearch(e.target.value)}
className="mb-2"
/>
<div className="max-h-40 overflow-y-auto border rounded-lg">
{filteredContracts.length > 0 ? (
filteredContracts.map((contract) => (
<div
key={contract.id}
onClick={() => setSelectedContractId(contract.id)}
className={`p-3 cursor-pointer border-b last:border-b-0 hover:bg-gray-50 ${
selectedContractId === contract.id ? 'bg-blue-50 border-blue-200' : ''
}`}
>
<div className="font-medium">{contract.contractNumber}</div>
<div className="text-sm text-gray-500">
{contract.providerName || 'Kein Anbieter'}
{contract.tariffName && ` - ${contract.tariffName}`}
</div>
</div>
))
) : (
<div className="p-3 text-gray-500 text-center">
{contractsData ? 'Keine Verträge gefunden.' : 'Laden...'}
</div>
)}
</div>
</div>
)}
{/* Titel */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Titel *
</label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Aufgabentitel"
/>
</div>
{/* Beschreibung */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Beschreibung
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Detaillierte Beschreibung (optional)"
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Im Kundenportal sichtbar */}
<div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={visibleInPortal}
onChange={(e) => setVisibleInPortal(e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">Im Kundenportal sichtbar</span>
</label>
</div>
{/* Buttons */}
<div className="flex justify-end gap-2 pt-4">
<Button variant="secondary" onClick={handleClose}>
Abbrechen
</Button>
<Button
onClick={handleSubmit}
disabled={!selectedContractId || !title.trim() || isSubmitting}
>
{isSubmitting ? 'Wird erstellt...' : 'Aufgabe erstellen'}
</Button>
</div>
</div>
</Modal>
);
}