first commit
This commit is contained in:
@@ -0,0 +1,626 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { customerApi, contractApi, contractTaskApi, appSettingsApi } from '../services/api';
|
||||
import Card from '../components/ui/Card';
|
||||
import Button from '../components/ui/Button';
|
||||
import Input from '../components/ui/Input';
|
||||
import Modal from '../components/ui/Modal';
|
||||
import {
|
||||
Users,
|
||||
FileText,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
User,
|
||||
ClipboardList,
|
||||
MessageSquare,
|
||||
Plus,
|
||||
Clock,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import type { Contract } from '../types';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { user, isCustomer, isCustomerPortal } = useAuth();
|
||||
const [showCreateTicketModal, setShowCreateTicketModal] = useState(false);
|
||||
|
||||
// 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: customersData } = useQuery({
|
||||
queryKey: ['customers-count'],
|
||||
queryFn: () => customerApi.getAll({ limit: 1 }),
|
||||
enabled: !isCustomer,
|
||||
});
|
||||
|
||||
const { data: contractsData } = useQuery({
|
||||
queryKey: ['contracts', isCustomer ? user?.customerId : undefined],
|
||||
queryFn: () => contractApi.getAll(isCustomer ? { customerId: user?.customerId } : { limit: 1 }),
|
||||
});
|
||||
|
||||
const { data: activeContractsData } = useQuery({
|
||||
queryKey: ['contracts-active', isCustomer ? user?.customerId : undefined],
|
||||
queryFn: () => contractApi.getAll({
|
||||
status: 'ACTIVE',
|
||||
...(isCustomer ? { customerId: user?.customerId } : { limit: 1 }),
|
||||
}),
|
||||
});
|
||||
|
||||
const { data: pendingContractsData } = useQuery({
|
||||
queryKey: ['contracts-pending', isCustomer ? user?.customerId : undefined],
|
||||
queryFn: () => contractApi.getAll({
|
||||
status: 'PENDING',
|
||||
...(isCustomer ? { customerId: user?.customerId } : { limit: 1 }),
|
||||
}),
|
||||
});
|
||||
|
||||
// Task-Statistik
|
||||
const { data: taskStatsData } = useQuery({
|
||||
queryKey: ['task-stats'],
|
||||
queryFn: () => contractTaskApi.getStats(),
|
||||
});
|
||||
|
||||
// Vertrags-Cockpit für Mitarbeiter/Admins
|
||||
const { data: cockpitData } = useQuery({
|
||||
queryKey: ['contract-cockpit'],
|
||||
queryFn: () => contractApi.getCockpit(),
|
||||
enabled: !isCustomer,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
// Für Kundenportal: Verträge nach eigene/fremd gruppieren
|
||||
const { ownContracts, representedContracts } = useMemo(() => {
|
||||
if (!isCustomerPortal || !contractsData?.data) {
|
||||
return { ownContracts: [], representedContracts: [] };
|
||||
}
|
||||
|
||||
const own: Contract[] = [];
|
||||
const represented: Record<number, { customerName: string; contracts: Contract[] }> = {};
|
||||
|
||||
for (const contract of contractsData.data) {
|
||||
if (contract.customerId === user?.customerId) {
|
||||
own.push(contract);
|
||||
} else {
|
||||
const customerId = contract.customerId;
|
||||
if (!represented[customerId]) {
|
||||
const customerName = contract.customer
|
||||
? (contract.customer.companyName || `${contract.customer.firstName} ${contract.customer.lastName}`)
|
||||
: `Kunde ${customerId}`;
|
||||
represented[customerId] = { customerName, contracts: [] };
|
||||
}
|
||||
represented[customerId].contracts.push(contract);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ownContracts: own,
|
||||
representedContracts: Object.values(represented).sort((a, b) =>
|
||||
a.customerName.localeCompare(b.customerName)
|
||||
),
|
||||
};
|
||||
}, [contractsData?.data, isCustomerPortal, user?.customerId]);
|
||||
|
||||
// Zähle Verträge für eigene vs. fremd
|
||||
const ownActiveCount = useMemo(() =>
|
||||
ownContracts.filter(c => c.status === 'ACTIVE').length,
|
||||
[ownContracts]
|
||||
);
|
||||
const ownPendingCount = useMemo(() =>
|
||||
ownContracts.filter(c => c.status === 'PENDING').length,
|
||||
[ownContracts]
|
||||
);
|
||||
const ownExpiredCount = useMemo(() =>
|
||||
ownContracts.filter(c => c.status === 'EXPIRED').length,
|
||||
[ownContracts]
|
||||
);
|
||||
const representedTotalCount = useMemo(() =>
|
||||
representedContracts.reduce((sum, g) => sum + g.contracts.length, 0),
|
||||
[representedContracts]
|
||||
);
|
||||
const representedActiveCount = useMemo(() =>
|
||||
representedContracts.reduce((sum, g) => sum + g.contracts.filter(c => c.status === 'ACTIVE').length, 0),
|
||||
[representedContracts]
|
||||
);
|
||||
const representedExpiredCount = useMemo(() =>
|
||||
representedContracts.reduce((sum, g) => sum + g.contracts.filter(c => c.status === 'EXPIRED').length, 0),
|
||||
[representedContracts]
|
||||
);
|
||||
|
||||
const openTasksCount = taskStatsData?.data?.openCount || 0;
|
||||
|
||||
// Helper zum Rendern einer klickbaren Stat-Karte
|
||||
const renderStatCard = (stat: {
|
||||
label: string;
|
||||
value: number;
|
||||
icon: typeof FileText;
|
||||
color: string;
|
||||
link?: string;
|
||||
}) => (
|
||||
<Card key={stat.label} className={stat.link ? 'cursor-pointer hover:shadow-md transition-shadow' : ''}>
|
||||
{stat.link ? (
|
||||
<Link to={stat.link} className="block">
|
||||
<div className="flex items-center">
|
||||
<div className={`p-3 rounded-lg ${stat.color}`}>
|
||||
<stat.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm text-gray-500">{stat.label}</p>
|
||||
<p className="text-2xl font-bold">{stat.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<div className={`p-3 rounded-lg ${stat.color}`}>
|
||||
<stat.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm text-gray-500">{stat.label}</p>
|
||||
<p className="text-2xl font-bold">{stat.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold">
|
||||
Willkommen, {user?.firstName}!
|
||||
</h1>
|
||||
{/* Support-Ticket erstellen Button für Kundenportal */}
|
||||
{isCustomerPortal && supportTicketsEnabled && (
|
||||
<Button onClick={() => setShowCreateTicketModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Support-Anfrage
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Kundenportal: Getrennte Statistiken */}
|
||||
{isCustomerPortal ? (
|
||||
<>
|
||||
{/* Eigene Verträge Stats */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<User className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-lg font-semibold">Meine Verträge</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{renderStatCard({
|
||||
label: 'Eigene Verträge',
|
||||
value: ownContracts.length,
|
||||
icon: FileText,
|
||||
color: 'bg-blue-500',
|
||||
link: '/contracts',
|
||||
})}
|
||||
{renderStatCard({
|
||||
label: 'Davon aktiv',
|
||||
value: ownActiveCount,
|
||||
icon: CheckCircle,
|
||||
color: 'bg-green-500',
|
||||
})}
|
||||
{renderStatCard({
|
||||
label: 'Davon ausstehend',
|
||||
value: ownPendingCount,
|
||||
icon: Clock,
|
||||
color: 'bg-yellow-500',
|
||||
})}
|
||||
{renderStatCard({
|
||||
label: 'Davon abgelaufen',
|
||||
value: ownExpiredCount,
|
||||
icon: XCircle,
|
||||
color: 'bg-red-500',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fremdverträge Stats - nur anzeigen wenn vorhanden */}
|
||||
{representedTotalCount > 0 && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Users className="w-5 h-5 text-purple-600" />
|
||||
<h2 className="text-lg font-semibold">Fremdverträge</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{renderStatCard({
|
||||
label: 'Fremdverträge',
|
||||
value: representedTotalCount,
|
||||
icon: Users,
|
||||
color: 'bg-purple-500',
|
||||
link: '/contracts',
|
||||
})}
|
||||
{renderStatCard({
|
||||
label: 'Davon aktiv',
|
||||
value: representedActiveCount,
|
||||
icon: CheckCircle,
|
||||
color: 'bg-green-500',
|
||||
})}
|
||||
{/* Leere Karte für Symmetrie */}
|
||||
<div className="hidden lg:block"></div>
|
||||
{renderStatCard({
|
||||
label: 'Davon abgelaufen',
|
||||
value: representedExpiredCount,
|
||||
icon: XCircle,
|
||||
color: 'bg-red-500',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Support-Anfragen Stats */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<MessageSquare className="w-5 h-5 text-orange-600" />
|
||||
<h2 className="text-lg font-semibold">Support-Anfragen</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{renderStatCard({
|
||||
label: 'Offene Anfragen',
|
||||
value: openTasksCount,
|
||||
icon: MessageSquare,
|
||||
color: 'bg-orange-500',
|
||||
link: '/tasks',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Mitarbeiter/Admin: Standard Stats */
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||
{renderStatCard({
|
||||
label: 'Kunden',
|
||||
value: customersData?.pagination?.total || 0,
|
||||
icon: Users,
|
||||
color: 'bg-blue-500',
|
||||
link: '/customers',
|
||||
})}
|
||||
{renderStatCard({
|
||||
label: 'Verträge gesamt',
|
||||
value: contractsData?.pagination?.total || 0,
|
||||
icon: FileText,
|
||||
color: 'bg-purple-500',
|
||||
link: '/contracts',
|
||||
})}
|
||||
{renderStatCard({
|
||||
label: 'Aktive Verträge',
|
||||
value: activeContractsData?.pagination?.total || 0,
|
||||
icon: CheckCircle,
|
||||
color: 'bg-green-500',
|
||||
})}
|
||||
{renderStatCard({
|
||||
label: 'Ausstehende Verträge',
|
||||
value: pendingContractsData?.pagination?.total || 0,
|
||||
icon: AlertCircle,
|
||||
color: 'bg-yellow-500',
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Vertrags-Cockpit Übersicht */}
|
||||
{cockpitData?.data && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||
<h2 className="text-lg font-semibold">Vertrags-Cockpit</h2>
|
||||
</div>
|
||||
<Link to="/contracts/cockpit" className="text-sm text-blue-600 hover:underline">
|
||||
Alle anzeigen
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="cursor-pointer hover:shadow-md transition-shadow">
|
||||
<Link to="/contracts/cockpit?filter=critical" className="block">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-lg bg-red-100">
|
||||
<AlertCircle className="w-6 h-6 text-red-500" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm text-gray-500">Kritisch (<{cockpitData.data.thresholds.criticalDays} Tage)</p>
|
||||
<p className="text-2xl font-bold text-red-600">{cockpitData.data.summary.criticalCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Card>
|
||||
<Card className="cursor-pointer hover:shadow-md transition-shadow">
|
||||
<Link to="/contracts/cockpit?filter=warning" className="block">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-lg bg-yellow-100">
|
||||
<AlertTriangle className="w-6 h-6 text-yellow-500" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm text-gray-500">Warnung (<{cockpitData.data.thresholds.warningDays} Tage)</p>
|
||||
<p className="text-2xl font-bold text-yellow-600">{cockpitData.data.summary.warningCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Card>
|
||||
<Card className="cursor-pointer hover:shadow-md transition-shadow">
|
||||
<Link to="/contracts/cockpit?filter=ok" className="block">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-lg bg-green-100">
|
||||
<CheckCircle className="w-6 h-6 text-green-500" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm text-gray-500">OK (<{cockpitData.data.thresholds.okDays} Tage)</p>
|
||||
<p className="text-2xl font-bold text-green-600">{cockpitData.data.summary.okCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Card>
|
||||
<Card className="cursor-pointer hover:shadow-md transition-shadow">
|
||||
<Link to="/contracts/cockpit" className="block">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-lg bg-gray-100">
|
||||
<FileText className="w-6 h-6 text-gray-500" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm text-gray-500">Handlungsbedarf</p>
|
||||
<p className="text-2xl font-bold text-gray-600">{cockpitData.data.summary.totalContracts}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aufgaben Stats für Mitarbeiter */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<ClipboardList className="w-5 h-5 text-orange-600" />
|
||||
<h2 className="text-lg font-semibold">Aufgaben</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{renderStatCard({
|
||||
label: 'Offene Aufgaben',
|
||||
value: openTasksCount,
|
||||
icon: ClipboardList,
|
||||
color: 'bg-orange-500',
|
||||
link: '/tasks',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Support-Ticket erstellen Modal (für Kundenportal) */}
|
||||
{isCustomerPortal && (
|
||||
<CreateSupportTicketModal
|
||||
isOpen={showCreateTicketModal}
|
||||
onClose={() => setShowCreateTicketModal(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,
|
||||
});
|
||||
// Invalidate task stats
|
||||
queryClient.invalidateQueries({ queryKey: ['task-stats'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user