355 lines
22 KiB
TypeScript
355 lines
22 KiB
TypeScript
|
|
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
|
import { Tender, Expert, Customer, KanbanColumn } from '../types';
|
|
import { supabase, TABLES } from '../lib/supabase';
|
|
|
|
interface TendersProps {
|
|
tenders: Tender[];
|
|
setTenders: React.Dispatch<React.SetStateAction<Tender[]>>;
|
|
customers: Customer[];
|
|
experts: Expert[];
|
|
refreshData: () => Promise<void>;
|
|
}
|
|
|
|
type SortConfig = {
|
|
key: keyof Tender | 'none';
|
|
direction: 'asc' | 'desc';
|
|
};
|
|
|
|
type FilterStatus = 'Alle' | 'Aktiv' | 'Inaktiv';
|
|
|
|
export const Tenders: React.FC<TendersProps> = ({ tenders, setTenders, customers, experts, refreshData }) => {
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
const [tenderToDelete, setTenderToDelete] = useState<string | null>(null);
|
|
const [sortConfig, setSortConfig] = useState<SortConfig>({ key: 'none', direction: 'asc' });
|
|
const [filterStatus, setFilterStatus] = useState<FilterStatus>('Alle');
|
|
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
|
|
const filterMenuRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingTender, setEditingTender] = useState<Tender | null>(null);
|
|
const [formState, setFormState] = useState({ title: '', source: '', status: 'Aktiv' as const, link: '' });
|
|
|
|
const [isExpertModalOpen, setIsExpertModalOpen] = useState(false);
|
|
const [isCustomerModalOpen, setIsCustomerModalOpen] = useState(false);
|
|
const [activeTenderIdForSelection, setActiveTenderIdForSelection] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (filterMenuRef.current && !filterMenuRef.current.contains(event.target as Node)) {
|
|
setIsFilterMenuOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
const displayTenders = useMemo(() => {
|
|
let items = [...tenders];
|
|
if (filterStatus === 'Aktiv') {
|
|
items = items.filter(t => t.status === 'Aktiv');
|
|
} else if (filterStatus === 'Inaktiv') {
|
|
items = items.filter(t => t.status === 'Inaktiv');
|
|
}
|
|
if (sortConfig.key !== 'none') {
|
|
items.sort((a, b) => {
|
|
const aValue = a[sortConfig.key as keyof Tender];
|
|
const bValue = b[sortConfig.key as keyof Tender];
|
|
if (aValue === undefined || bValue === undefined) return 0;
|
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
return sortConfig.direction === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
return items;
|
|
}, [tenders, sortConfig, filterStatus]);
|
|
|
|
const requestSort = (key: keyof Tender) => {
|
|
let direction: 'asc' | 'desc' = 'asc';
|
|
if (sortConfig.key === key && sortConfig.direction === 'asc') direction = 'desc';
|
|
setSortConfig({ key, direction });
|
|
};
|
|
|
|
const getSortIndicator = (key: keyof Tender) => {
|
|
if (sortConfig.key !== key) return <span className="opacity-20 text-[10px]">↕</span>;
|
|
return sortConfig.direction === 'asc' ? <span className="text-brand-500 text-[10px]">↑</span> : <span className="text-brand-500 text-[10px]">↓</span>;
|
|
};
|
|
|
|
const openModal = (tender?: Tender) => {
|
|
if (tender) {
|
|
setEditingTender(tender);
|
|
setFormState({ title: tender.title, source: tender.source, status: tender.status as any, link: tender.link || '' });
|
|
} else {
|
|
setEditingTender(null);
|
|
setFormState({ title: '', source: '', status: 'Aktiv', link: '' });
|
|
}
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (editingTender) {
|
|
await supabase.from(TABLES.TENDERS).update({
|
|
title: formState.title,
|
|
source: formState.source,
|
|
status: formState.status,
|
|
link: formState.link
|
|
}).eq('id', editingTender.id);
|
|
} else {
|
|
await supabase.from(TABLES.TENDERS).insert([{
|
|
title: formState.title,
|
|
source: formState.source,
|
|
status: formState.status,
|
|
kanban_status: 'Ausschreibung',
|
|
link: formState.link || null,
|
|
notes: ''
|
|
}]);
|
|
}
|
|
setIsModalOpen(false);
|
|
refreshData();
|
|
};
|
|
|
|
const confirmDelete = async () => {
|
|
if (tenderToDelete) {
|
|
await supabase.from(TABLES.TENDERS).delete().eq('id', tenderToDelete);
|
|
if (expandedId === tenderToDelete) setExpandedId(null);
|
|
setTenderToDelete(null);
|
|
refreshData();
|
|
}
|
|
};
|
|
|
|
const handleUpdateNotes = async (id: string, newNotes: string) => {
|
|
setTenders(prev => prev.map(t => t.id === id ? { ...t, notes: newNotes } : t));
|
|
await supabase.from(TABLES.TENDERS).update({ notes: newNotes }).eq('id', id);
|
|
};
|
|
|
|
const assignCustomer = async (tenderId: string, customer: Customer) => {
|
|
await supabase.from(TABLES.TENDERS).update({ customer_id: customer.id }).eq('id', tenderId);
|
|
setIsCustomerModalOpen(false);
|
|
refreshData();
|
|
};
|
|
|
|
const toggleExpertSelection = async (tenderId: string, expertId: string) => {
|
|
const tender = tenders.find(t => t.id === tenderId);
|
|
const isAlreadySelected = tender?.selectedExperts?.some(e => e.id === expertId);
|
|
|
|
if (isAlreadySelected) {
|
|
await supabase.from(TABLES.TENDER_EXPERTS).delete().match({ tender_id: tenderId, expert_id: expertId });
|
|
} else {
|
|
await supabase.from(TABLES.TENDER_EXPERTS).insert([{ tender_id: tenderId, expert_id: expertId }]);
|
|
}
|
|
refreshData();
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Ausschreibungen</h2>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex bg-slate-100 dark:bg-slate-800 p-1 rounded-xl">
|
|
{(['Alle', 'Aktiv', 'Inaktiv'] as FilterStatus[]).map(s => (
|
|
<button
|
|
key={s}
|
|
onClick={() => setFilterStatus(s)}
|
|
className={`px-4 py-1.5 text-xs font-bold rounded-lg transition-all ${filterStatus === s ? 'bg-white dark:bg-slate-700 shadow-sm text-brand-600 dark:text-brand-400' : 'text-slate-500 hover:text-slate-700'}`}
|
|
>
|
|
{s === 'Inaktiv' ? 'Nicht aktiv' : s}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button type="button" onClick={() => openModal()} className="bg-brand-600 hover:bg-brand-700 text-white px-5 py-2.5 rounded-xl font-bold transition-all shadow-lg shadow-brand-500/20 flex items-center gap-2">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
|
|
Ausschreibung hinzufügen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-slate-900 rounded-3xl overflow-hidden border border-slate-100 dark:border-slate-800 shadow-sm">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left border-collapse min-w-[1050px]">
|
|
<thead>
|
|
<tr className="bg-slate-50 dark:bg-slate-800/50">
|
|
<th className="p-4 font-bold text-slate-500 uppercase text-xs tracking-wider cursor-pointer" onClick={() => requestSort('title')}>
|
|
<div className="flex items-center gap-2">Titel {getSortIndicator('title')}</div>
|
|
</th>
|
|
<th className="p-4 font-bold text-slate-500 uppercase text-xs tracking-wider">Kunde / Kontakt</th>
|
|
<th className="p-4 font-bold text-slate-500 uppercase text-xs tracking-wider cursor-pointer" onClick={() => requestSort('source')}>
|
|
<div className="flex items-center gap-2">Quelle {getSortIndicator('source')}</div>
|
|
</th>
|
|
<th className="p-4 font-bold text-slate-500 uppercase text-xs tracking-wider text-center">Experten</th>
|
|
<th className="p-4 font-bold text-slate-500 uppercase text-xs tracking-wider">Status</th>
|
|
<th className="p-4 w-32"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
|
{displayTenders.map((tender) => (
|
|
<React.Fragment key={tender.id}>
|
|
<tr className={`hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors cursor-pointer ${expandedId === tender.id ? 'bg-brand-50/50 dark:bg-brand-500/5' : ''}`} onClick={() => setExpandedId(expandedId === tender.id ? null : tender.id)}>
|
|
<td className="p-4 font-bold text-slate-800 dark:text-slate-100">{tender.title}</td>
|
|
<td className="p-4 font-medium text-slate-700 dark:text-slate-300">
|
|
{tender.selectedCustomer ? (
|
|
<div className="flex flex-col">
|
|
<span className="font-bold">{tender.selectedCustomer.name}</span>
|
|
<span className="text-[10px] text-slate-500 uppercase">{tender.selectedCustomer.company}</span>
|
|
</div>
|
|
) : <span className="text-slate-400 italic text-xs">Nicht zugewiesen</span>}
|
|
</td>
|
|
<td className="p-4 font-medium text-slate-600 dark:text-slate-400 text-sm">{tender.source}</td>
|
|
<td className="p-4 text-center">
|
|
<span className="font-bold bg-slate-100 dark:bg-slate-700 px-3 py-1 rounded-full text-sm text-slate-700 dark:text-white border border-slate-200 dark:border-slate-700">{tender.applicantsCount}</span>
|
|
</td>
|
|
<td className="p-4">
|
|
<span className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest ${
|
|
tender.status === 'Aktiv' ? 'bg-brand-500 text-white' :
|
|
tender.status === 'Inaktiv' ? 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400' :
|
|
'bg-slate-200 dark:bg-slate-700 text-slate-600'
|
|
}`}>
|
|
{tender.status === 'Inaktiv' ? 'Nicht aktiv' : tender.status}
|
|
</span>
|
|
</td>
|
|
<td className="p-4 flex items-center justify-end gap-1">
|
|
<button type="button" onClick={(e) => { e.stopPropagation(); openModal(tender); }} className="p-2 text-slate-400 hover:text-brand-500"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg></button>
|
|
<button type="button" onClick={(e) => { e.stopPropagation(); setTenderToDelete(tender.id); }} className="p-2 text-slate-400 hover:text-red-500"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg></button>
|
|
</td>
|
|
</tr>
|
|
{expandedId === tender.id && (
|
|
<tr>
|
|
<td colSpan={6} className="p-6 bg-slate-50/50 dark:bg-slate-900/50 border-t border-slate-100 dark:border-slate-800">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Experts */}
|
|
<div className="bg-white dark:bg-slate-800 rounded-3xl p-6 shadow-sm border border-slate-100 dark:border-slate-700 flex flex-col h-[400px]">
|
|
<h4 className="text-xl font-bold flex items-center gap-2 mb-6 text-slate-900 dark:text-white"><span className="w-2 h-8 bg-brand-500 rounded-full inline-block"></span>Experten</h4>
|
|
<div className="flex-1 overflow-y-auto space-y-3 mb-4 pr-2">
|
|
{tender.selectedExperts && tender.selectedExperts.length > 0 ? tender.selectedExperts.map(expert => (
|
|
<div key={expert.id} className="flex items-center justify-between p-3 bg-slate-50 dark:bg-slate-900/80 rounded-2xl border border-slate-100 dark:border-slate-800 group/expert">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-brand-500/10 text-brand-600 flex items-center justify-center font-bold text-sm uppercase">{expert.name.charAt(0)}</div>
|
|
<div>
|
|
<p className="font-bold text-sm text-slate-900 dark:text-slate-100">{expert.name}</p>
|
|
<p className="text-[11px] text-slate-500">{expert.role}</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" onClick={() => toggleExpertSelection(tender.id, expert.id)} className="p-2 text-slate-400 hover:text-red-500 opacity-0 group-hover/expert:opacity-100"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg></button>
|
|
</div>
|
|
)) : <div className="py-12 text-center bg-slate-50 dark:bg-slate-900/30 rounded-3xl border border-dashed border-slate-200 dark:border-slate-800"><p className="text-sm text-slate-500 italic">Noch keine Experten zugewiesen.</p></div>}
|
|
</div>
|
|
<button type="button" onClick={(e) => { e.stopPropagation(); setActiveTenderIdForSelection(tender.id); setIsExpertModalOpen(true); }} className="w-full py-3.5 bg-white dark:bg-slate-800 text-brand-600 border-2 border-brand-500 hover:bg-brand-500 hover:text-white rounded-2xl font-bold transition-all shadow-sm">Experten zuweisen</button>
|
|
</div>
|
|
|
|
{/* Customer */}
|
|
<div className="bg-white dark:bg-slate-800 rounded-3xl p-6 shadow-sm border border-slate-100 dark:border-slate-700 flex flex-col h-[400px]">
|
|
<h4 className="text-xl font-bold flex items-center gap-2 mb-6 text-slate-900 dark:text-white"><span className="w-2 h-8 bg-blue-500 rounded-full inline-block"></span>Kundenkontakt</h4>
|
|
<div className="flex-1 flex flex-col items-center justify-center text-center p-8 bg-slate-50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800 mb-6">
|
|
{tender.selectedCustomer ? (
|
|
<div>
|
|
<div className="w-20 h-20 rounded-3xl bg-blue-500/10 text-blue-600 flex items-center justify-center mb-6 mx-auto"><svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg></div>
|
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Zugeordneter Kontakt</p>
|
|
<p className="font-bold text-slate-900 dark:text-white text-xl leading-tight">{tender.selectedCustomer.name}</p>
|
|
<p className="text-sm text-slate-500 font-medium">{tender.selectedCustomer.company}</p>
|
|
</div>
|
|
) : <p className="text-slate-500 italic text-sm">Kein Kunde ausgewählt</p>}
|
|
</div>
|
|
<button type="button" onClick={(e) => { e.stopPropagation(); setActiveTenderIdForSelection(tender.id); setIsCustomerModalOpen(true); }} className="w-full py-4 bg-blue-500 hover:bg-blue-600 text-white rounded-2xl font-bold transition-all shadow-lg shadow-blue-500/20">Kunden auswählen</button>
|
|
</div>
|
|
|
|
{/* Notes */}
|
|
<div className="bg-white dark:bg-slate-800 rounded-3xl p-6 shadow-sm border border-slate-100 dark:border-slate-700 flex flex-col h-[400px]">
|
|
<h4 className="text-xl font-bold flex items-center gap-2 mb-6 text-slate-900 dark:text-white"><span className="w-2 h-8 bg-brand-500 rounded-full inline-block"></span>Notizen</h4>
|
|
<textarea
|
|
value={tender.notes || ''}
|
|
onChange={(e) => handleUpdateNotes(tender.id, e.target.value)}
|
|
placeholder="Interne Informationen..."
|
|
className="flex-1 w-full p-4 bg-slate-50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 rounded-2xl text-sm leading-relaxed text-slate-700 dark:text-slate-200 resize-none outline-none focus:ring-2 focus:ring-brand-500/20"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* MODALS */}
|
|
{isModalOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm">
|
|
<div className="bg-white dark:bg-slate-900 w-full max-w-md rounded-3xl shadow-2xl p-6">
|
|
<h3 className="text-xl font-bold mb-4">{editingTender ? 'Bearbeiten' : 'Neu'}</h3>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<input required type="text" value={formState.title} onChange={e => setFormState({ ...formState, title: e.target.value })} placeholder="Titel" className="w-full px-4 py-3 bg-slate-50 dark:bg-slate-800 rounded-xl outline-none" />
|
|
<input required type="text" value={formState.source} onChange={e => setFormState({ ...formState, source: e.target.value })} placeholder="Quelle" className="w-full px-4 py-3 bg-slate-50 dark:bg-slate-800 rounded-xl outline-none" />
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Status</label>
|
|
<select
|
|
value={formState.status}
|
|
onChange={e => setFormState({ ...formState, status: e.target.value as any })}
|
|
className="w-full px-4 py-3 bg-slate-50 dark:bg-slate-800 rounded-xl outline-none"
|
|
>
|
|
<option value="Aktiv">Aktiv</option>
|
|
<option value="Inaktiv">Nicht aktiv</option>
|
|
<option value="Ausgelaufen">Ausgelaufen</option>
|
|
</select>
|
|
</div>
|
|
<button type="submit" className="w-full py-3 bg-brand-600 text-white font-bold rounded-xl">Speichern</button>
|
|
<button type="button" onClick={() => setIsModalOpen(false)} className="w-full py-2 text-slate-500">Abbrechen</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isExpertModalOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/70 backdrop-blur-md">
|
|
<div className="bg-white dark:bg-slate-900 w-full max-w-lg rounded-[2.5rem] shadow-2xl p-8 max-h-[80vh] overflow-y-auto">
|
|
<h3 className="text-2xl font-bold mb-6">Experten auswählen</h3>
|
|
<div className="space-y-3">
|
|
{experts.map(expert => {
|
|
const isSelected = tenders.find(t => t.id === activeTenderIdForSelection)?.selectedExperts?.some(e => e.id === expert.id);
|
|
return (
|
|
<div key={expert.id} onClick={() => toggleExpertSelection(activeTenderIdForSelection!, expert.id)} className={`flex items-center justify-between p-4 rounded-3xl border-2 cursor-pointer ${isSelected ? 'border-brand-500 bg-brand-50' : 'border-transparent bg-slate-50 dark:bg-slate-800'}`}>
|
|
<span className="font-bold">{expert.name}</span>
|
|
{isSelected && <span className="text-brand-500 font-bold">✓</span>}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<button onClick={() => setIsExpertModalOpen(false)} className="w-full mt-8 py-4 bg-brand-600 text-white font-bold rounded-2xl">Fertig</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isCustomerModalOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/70 backdrop-blur-md">
|
|
<div className="bg-white dark:bg-slate-900 w-full max-w-lg rounded-[2.5rem] shadow-2xl p-8 max-h-[80vh] overflow-y-auto">
|
|
<h3 className="text-2xl font-bold mb-6">Kunden zuweisen</h3>
|
|
<div className="space-y-3">
|
|
{customers.map(customer => (
|
|
<div key={customer.id} onClick={() => assignCustomer(activeTenderIdForSelection!, customer)} className="p-5 rounded-3xl bg-slate-50 dark:bg-slate-800 border-2 border-transparent hover:border-blue-500 cursor-pointer">
|
|
<p className="font-bold text-lg">{customer.name}</p>
|
|
<p className="text-xs text-slate-500 uppercase font-bold">{customer.company}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<button onClick={() => setIsCustomerModalOpen(false)} className="w-full mt-8 py-4 bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-bold rounded-2xl">Schließen</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{tenderToDelete && (
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-md">
|
|
<div className="bg-white dark:bg-slate-900 w-full max-w-sm rounded-[2rem] p-8 text-center shadow-2xl border border-red-100">
|
|
<h3 className="text-2xl font-bold mb-2">Bist du sicher?</h3>
|
|
<p className="text-slate-500 mb-8">Dieser Tender wird unwiderruflich gelöscht.</p>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<button onClick={() => setTenderToDelete(null)} className="py-4 bg-slate-100 dark:bg-slate-800 rounded-2xl font-bold">Abbruch</button>
|
|
<button onClick={confirmDelete} className="py-4 bg-red-600 text-white rounded-2xl font-bold shadow-lg shadow-red-500/20">Löschen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}; |