627 lines
23 KiB
TypeScript
627 lines
23 KiB
TypeScript
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>
|
|
);
|
|
}
|