496 lines
30 KiB
TypeScript
496 lines
30 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
import { Project, Customer, Expert } from '../types';
|
|
import { supabase, TABLES } from '../lib/supabase';
|
|
|
|
interface ProjectsProps {
|
|
projects: Project[];
|
|
setProjects: React.Dispatch<React.SetStateAction<Project[]>>;
|
|
customers: Customer[];
|
|
experts: Expert[];
|
|
refreshData: () => Promise<void>;
|
|
}
|
|
|
|
export const Projects: React.FC<ProjectsProps> = ({ projects, setProjects, customers, experts, refreshData }) => {
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
const [editModeId, setEditModeId] = useState<string | null>(null);
|
|
const [projectToDelete, setProjectToDelete] = useState<string | null>(null);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
// Pickers
|
|
const [isCustomerPickerOpen, setIsCustomerPickerOpen] = useState(false);
|
|
const [isExpertPickerOpen, setIsExpertPickerOpen] = useState(false);
|
|
|
|
// Local state for editing
|
|
const [editForm, setEditForm] = useState<Partial<Project>>({});
|
|
|
|
const startEditing = (project: Project) => {
|
|
const customer = getDisplayInfo(project, 'KUNDE');
|
|
const expert = getDisplayInfo(project, 'EXPERTE');
|
|
const start = getDisplayInfo(project, 'START');
|
|
const end = getDisplayInfo(project, 'ENDE');
|
|
|
|
const formatDateForInput = (val: string) => {
|
|
if (!val || val === 'N/A' || val === 'TBD') return '';
|
|
if (val.includes('.')) {
|
|
const parts = val.split('.');
|
|
if (parts.length === 3) return `${parts[2]}-${parts[1].padStart(2, '0')}-${parts[0].padStart(2, '0')}`;
|
|
}
|
|
return val;
|
|
};
|
|
|
|
setEditForm({
|
|
...project,
|
|
customer: customer === 'N/A' ? '' : customer,
|
|
expert: expert === 'N/A' ? '' : expert,
|
|
start: formatDateForInput(start),
|
|
end: formatDateForInput(end),
|
|
});
|
|
setEditModeId(project.id);
|
|
};
|
|
|
|
const handleSaveProject = async () => {
|
|
if (!editModeId || isSaving) return;
|
|
setIsSaving(true);
|
|
|
|
try {
|
|
const baseNotes = editForm.notes?.split('---').pop()?.trim() || '';
|
|
const structuredNotes = [
|
|
`KUNDE: ${editForm.customer || 'Nicht angegeben'}`,
|
|
`KONTAKT: ${getDisplayInfo(projects.find(p => p.id === editModeId)!, 'KONTAKT')}`,
|
|
`EXPERTE: ${editForm.expert || 'Kein Experten zugewiesen'}`,
|
|
`START: ${editForm.start || 'N/A'}`,
|
|
`ENDE: ${editForm.end || 'TBD'}`,
|
|
`---`,
|
|
baseNotes
|
|
].join('\n');
|
|
|
|
const updatePayload = {
|
|
title: editForm.title,
|
|
customer: editForm.customer,
|
|
expert: editForm.expert,
|
|
start: editForm.start,
|
|
end: editForm.end,
|
|
status: editForm.status,
|
|
notes: structuredNotes
|
|
};
|
|
|
|
const { error } = await supabase
|
|
.from(TABLES.PROJECTS)
|
|
.update(updatePayload)
|
|
.eq('id', editModeId);
|
|
|
|
if (error) {
|
|
console.warn('Full update failed, trying safe mode (notes only):', error.message);
|
|
await supabase
|
|
.from(TABLES.PROJECTS)
|
|
.update({
|
|
title: editForm.title,
|
|
status: editForm.status,
|
|
notes: structuredNotes
|
|
})
|
|
.eq('id', editModeId);
|
|
}
|
|
|
|
setEditModeId(null);
|
|
await refreshData();
|
|
} catch (err) {
|
|
console.error('Save error:', err);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleUpdateNotesOnly = async (id: string, newNotes: string) => {
|
|
setProjects(prev => prev.map(p => p.id === id ? { ...p, notes: newNotes } : p));
|
|
await supabase.from(TABLES.PROJECTS).update({ notes: newNotes }).eq('id', id);
|
|
};
|
|
|
|
const confirmDelete = async () => {
|
|
if (projectToDelete) {
|
|
await supabase.from(TABLES.PROJECTS).delete().eq('id', projectToDelete);
|
|
if (expandedId === projectToDelete) setExpandedId(null);
|
|
setProjectToDelete(null);
|
|
refreshData();
|
|
}
|
|
};
|
|
|
|
const getDisplayInfo = (project: Project, type: 'KUNDE' | 'EXPERTE' | 'KONTAKT' | 'START' | 'ENDE') => {
|
|
const key = type.toLowerCase();
|
|
const value = (project as any)[key];
|
|
if (value && value !== 'Nicht angegeben' && value !== 'Kein Experte zugewiesen' && value !== 'TBD' && value !== 'N/A') return value;
|
|
|
|
if (project.notes) {
|
|
const lines = project.notes.split('\n');
|
|
const prefix = `${type}: `;
|
|
const line = lines.find(l => l.startsWith(prefix));
|
|
if (line) return line.replace(prefix, '').trim();
|
|
}
|
|
|
|
if (type === 'START') return project.start || 'N/A';
|
|
if (type === 'ENDE') return project.end || 'TBD';
|
|
return 'N/A';
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Laufende Projekte</h2>
|
|
<p className="text-slate-500">Übersicht aller aktiven Beauftragungen</p>
|
|
</div>
|
|
<button 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>
|
|
Projekt anlegen
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-slate-900 rounded-[2.5rem] 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-[900px]">
|
|
<thead>
|
|
<tr className="bg-slate-50 dark:bg-slate-800/50">
|
|
<th className="p-5 font-bold text-slate-500 uppercase text-xs tracking-wider">Projekt</th>
|
|
<th className="p-5 font-bold text-slate-500 uppercase text-xs tracking-wider">Kunde</th>
|
|
<th className="p-5 font-bold text-slate-500 uppercase text-xs tracking-wider">Experten</th>
|
|
<th className="p-5 font-bold text-slate-500 uppercase text-xs tracking-wider">Laufzeit</th>
|
|
<th className="p-5 font-bold text-slate-500 uppercase text-xs tracking-wider">Status</th>
|
|
<th className="p-5 w-24"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
|
{projects.map((project) => {
|
|
const customerName = getDisplayInfo(project, 'KUNDE');
|
|
const expertNamesString = getDisplayInfo(project, 'EXPERTE');
|
|
const expertNames = expertNamesString.split(', ').filter(n => n && n !== 'N/A' && n !== 'Kein Experte zugewiesen');
|
|
const start = getDisplayInfo(project, 'START');
|
|
const end = getDisplayInfo(project, 'ENDE');
|
|
|
|
return (
|
|
<React.Fragment key={project.id}>
|
|
<tr
|
|
onClick={() => {
|
|
if (editModeId !== project.id) {
|
|
setExpandedId(expandedId === project.id ? null : project.id);
|
|
}
|
|
}}
|
|
className={`hover:bg-slate-50 dark:hover:bg-slate-800 transition-all cursor-pointer group ${expandedId === project.id ? 'bg-brand-50/30 dark:bg-brand-500/5' : ''}`}
|
|
>
|
|
<td className="p-5 font-bold text-slate-900 dark:text-white group-hover:text-brand-600 transition-colors">
|
|
{project.title}
|
|
</td>
|
|
<td className="p-5 text-slate-700 dark:text-slate-300 font-medium">
|
|
{customerName}
|
|
</td>
|
|
<td className="p-5">
|
|
<div className="flex items-center -space-x-3">
|
|
{expertNames.length > 0 ? (
|
|
expertNames.slice(0, 3).map((name, i) => (
|
|
<div
|
|
key={i}
|
|
title={name}
|
|
className="w-10 h-10 rounded-full bg-brand-500 text-white border-2 border-white dark:border-slate-800 flex items-center justify-center text-xs font-bold shadow-sm"
|
|
>
|
|
{name.charAt(0)}
|
|
</div>
|
|
))
|
|
) : (
|
|
<span className="text-xs text-slate-400 italic">Keine Experten</span>
|
|
)}
|
|
{expertNames.length > 3 && (
|
|
<div className="w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-500 border-2 border-white dark:border-slate-800 flex items-center justify-center text-[10px] font-bold shadow-sm">
|
|
+{expertNames.length - 3}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="p-5 text-xs font-mono text-slate-600 dark:text-slate-400">
|
|
<div className="bg-slate-100 dark:bg-slate-800 px-3 py-1.5 rounded-lg inline-block">
|
|
{start} - {end}
|
|
</div>
|
|
</td>
|
|
<td className="p-5">
|
|
<span className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest ${
|
|
project.status === 'Aktiv'
|
|
? 'bg-brand-500 text-white shadow-sm shadow-brand-500/20'
|
|
: 'bg-slate-200 dark:bg-slate-700 text-slate-500 dark:text-slate-400'
|
|
}`}>
|
|
{project.status}
|
|
</span>
|
|
</td>
|
|
<td className="p-5 flex items-center justify-end gap-1">
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); setProjectToDelete(project.id); }}
|
|
className="p-2 text-slate-300 hover:text-red-500 transition-colors opacity-0 group-hover: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="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 === project.id && (
|
|
<tr className="animate-in fade-in slide-in-from-top-2 duration-300">
|
|
<td colSpan={6} className="p-8 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-8">
|
|
<div className="lg:col-span-1 space-y-6">
|
|
<div className="bg-white dark:bg-slate-800 p-6 rounded-[2rem] border border-slate-100 dark:border-slate-700 shadow-sm relative overflow-hidden">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h4 className="text-sm font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
|
<span className="w-1.5 h-4 bg-brand-500 rounded-full"></span>
|
|
{editModeId === project.id ? 'Bearbeiten' : 'Details'}
|
|
</h4>
|
|
{editModeId !== project.id ? (
|
|
<button
|
|
onClick={() => startEditing(project)}
|
|
className="text-xs font-bold text-brand-600 hover:text-brand-700 underline"
|
|
>
|
|
Bearbeiten
|
|
</button>
|
|
) : (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setEditModeId(null)}
|
|
className="text-[10px] font-bold text-slate-400 uppercase tracking-widest hover:text-slate-600"
|
|
>
|
|
Abbruch
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{editModeId === project.id ? (
|
|
<div className="space-y-4 animate-in fade-in duration-200">
|
|
<div>
|
|
<label className="text-[10px] font-bold text-slate-400 uppercase mb-1 block">Titel</label>
|
|
<input
|
|
type="text"
|
|
value={editForm.title || ''}
|
|
onChange={e => setEditForm({...editForm, title: e.target.value})}
|
|
className="w-full p-2.5 text-sm bg-slate-50 dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-lg outline-none focus:ring-2 focus:ring-brand-500/20 text-slate-900 dark:text-white"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="text-[10px] font-bold text-slate-400 uppercase mb-1 block">Beginn</label>
|
|
<input
|
|
type="date"
|
|
value={editForm.start || ''}
|
|
onChange={e => setEditForm({...editForm, start: e.target.value})}
|
|
className="w-full p-2.5 text-sm bg-slate-50 dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-lg outline-none focus:ring-2 focus:ring-brand-500/20 text-slate-900 dark:text-white"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-bold text-slate-400 uppercase mb-1 block">Ende</label>
|
|
<input
|
|
type="date"
|
|
value={editForm.end || ''}
|
|
onChange={e => setEditForm({...editForm, end: e.target.value})}
|
|
className="w-full p-2.5 text-sm bg-slate-50 dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-lg outline-none focus:ring-2 focus:ring-brand-500/20 text-slate-900 dark:text-white"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-bold text-slate-400 uppercase mb-1 block">Kunde</label>
|
|
<div className="relative group/field">
|
|
<input
|
|
type="text"
|
|
readOnly
|
|
value={editForm.customer || ''}
|
|
onClick={() => setIsCustomerPickerOpen(true)}
|
|
placeholder="Kunden auswählen..."
|
|
className="w-full p-2.5 text-sm bg-slate-50 dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-lg outline-none focus:ring-2 focus:ring-brand-500/20 text-slate-900 dark:text-white cursor-pointer hover:border-brand-500 transition-colors"
|
|
/>
|
|
<svg className="w-4 h-4 absolute right-3 top-3 text-slate-400 group-hover/field:text-brand-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-bold text-slate-400 uppercase mb-1 block">Experten ({editForm.expert ? editForm.expert.split(', ').length : 0})</label>
|
|
<div className="relative group/field">
|
|
<input
|
|
type="text"
|
|
readOnly
|
|
value={editForm.expert || ''}
|
|
onClick={() => setIsExpertPickerOpen(true)}
|
|
placeholder="Experten auswählen..."
|
|
className="w-full p-2.5 text-sm bg-slate-50 dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-lg outline-none focus:ring-2 focus:ring-brand-500/20 text-slate-900 dark:text-white cursor-pointer hover:border-brand-500 transition-colors truncate"
|
|
/>
|
|
<svg className="w-4 h-4 absolute right-3 top-3 text-slate-400 group-hover/field:text-brand-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={handleSaveProject}
|
|
disabled={isSaving}
|
|
className="w-full py-3 bg-brand-600 text-white rounded-xl font-bold shadow-lg shadow-brand-500/20 hover:bg-brand-700 transition-all flex items-center justify-center gap-2 mt-2"
|
|
>
|
|
{isSaving ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div> : 'Änderungen speichern'}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center py-2 border-b border-slate-50 dark:border-slate-700/50">
|
|
<span className="text-slate-500 text-[10px] font-bold uppercase">Beginn</span>
|
|
<span className="font-mono text-xs dark:text-white">{start}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2 border-b border-slate-50 dark:border-slate-700/50">
|
|
<span className="text-slate-500 text-[10px] font-bold uppercase">Geplantes Ende</span>
|
|
<span className="font-mono text-xs dark:text-white">{end}</span>
|
|
</div>
|
|
<div className="space-y-2 py-2 border-b border-slate-50 dark:border-slate-700/50">
|
|
<span className="text-slate-500 text-[10px] font-bold uppercase block mb-2">Eingesetzte Experten</span>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{expertNames.length > 0 ? expertNames.map((name, i) => (
|
|
<span key={i} className="px-2 py-1 bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400 text-[10px] font-extrabold uppercase rounded-lg border border-brand-100 dark:border-brand-500/20">
|
|
{name}
|
|
</span>
|
|
)) : <span className="text-xs text-slate-400 italic">Keine Experten</span>}
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2">
|
|
<span className="text-slate-500 text-[10px] font-bold uppercase">Kunde</span>
|
|
<span className="text-xs font-bold dark:text-white">{customerName}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button className="w-full py-4 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-bold rounded-2xl hover:bg-slate-200 transition-all border border-transparent hover:border-slate-200 text-xs">
|
|
Vertragsdokumente verwalten
|
|
</button>
|
|
</div>
|
|
|
|
<div className="lg:col-span-2">
|
|
<div className="bg-white dark:bg-slate-800 p-6 rounded-[2rem] border border-slate-100 dark:border-slate-700 shadow-sm h-full flex flex-col">
|
|
<h4 className="text-sm font-bold text-slate-400 uppercase tracking-widest mb-4 flex items-center gap-2">
|
|
<span className="w-1.5 h-4 bg-brand-500 rounded-full"></span>
|
|
Projektjournal & Protokoll
|
|
</h4>
|
|
<textarea
|
|
value={project.notes || ''}
|
|
onChange={(e) => handleUpdateNotesOnly(project.id, e.target.value)}
|
|
placeholder="Hier Meilensteine, Feedback oder Projektfortschritte festhalten..."
|
|
className="flex-1 w-full p-5 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 transition-all min-h-[250px]"
|
|
/>
|
|
<div className="mt-3 flex items-center gap-2 text-[10px] text-slate-400 uppercase font-bold tracking-widest">
|
|
<div className="w-2 h-2 rounded-full bg-brand-500 animate-pulse"></div>
|
|
Änderungen werden live synchronisiert
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Customer Picker Modal */}
|
|
{isCustomerPickerOpen && (
|
|
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-md animate-in fade-in duration-200">
|
|
<div className="bg-white dark:bg-slate-900 w-full max-w-lg rounded-[2.5rem] shadow-2xl p-8 max-h-[80vh] flex flex-col border border-slate-100 dark:border-slate-800">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h3 className="text-2xl font-bold dark:text-white">Kunden auswählen</h3>
|
|
<button onClick={() => setIsCustomerPickerOpen(false)} className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-white">
|
|
<svg className="w-6 h-6" 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="overflow-y-auto space-y-3 pr-2">
|
|
{customers.map(c => (
|
|
<div
|
|
key={c.id}
|
|
onClick={() => { setEditForm({...editForm, customer: c.company}); setIsCustomerPickerOpen(false); }}
|
|
className="p-5 rounded-3xl bg-slate-50 dark:bg-slate-800 border-2 border-transparent hover:border-brand-500 cursor-pointer transition-all group"
|
|
>
|
|
<p className="font-bold text-lg dark:text-white group-hover:text-brand-500 transition-colors">{c.company}</p>
|
|
<p className="text-xs text-slate-500 uppercase font-bold tracking-widest">{c.name}</p>
|
|
</div>
|
|
))}
|
|
{customers.length === 0 && <p className="text-center py-10 text-slate-500 italic">Keine Kunden gefunden.</p>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Multi-Expert Picker Modal */}
|
|
{isExpertPickerOpen && (
|
|
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-md animate-in fade-in duration-200">
|
|
<div className="bg-white dark:bg-slate-900 w-full max-w-lg rounded-[2.5rem] shadow-2xl p-8 max-h-[80vh] flex flex-col border border-slate-100 dark:border-slate-800">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h3 className="text-2xl font-bold dark:text-white">Experten auswählen</h3>
|
|
<button onClick={() => setIsExpertPickerOpen(false)} className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-white">
|
|
<svg className="w-6 h-6" 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="overflow-y-auto space-y-3 pr-2 flex-1">
|
|
{experts.map(e => {
|
|
const currentExpertList = (editForm.expert || '').split(', ').filter(Boolean);
|
|
const isSelected = currentExpertList.includes(e.name);
|
|
|
|
return (
|
|
<div
|
|
key={e.id}
|
|
onClick={() => {
|
|
const newList = isSelected
|
|
? currentExpertList.filter(name => name !== e.name)
|
|
: [...currentExpertList, e.name];
|
|
setEditForm({...editForm, expert: newList.join(', ')});
|
|
}}
|
|
className={`p-5 rounded-3xl border-2 transition-all group flex items-center gap-4 cursor-pointer ${
|
|
isSelected ? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10' : 'border-transparent bg-slate-50 dark:bg-slate-800 hover:border-slate-300'
|
|
}`}
|
|
>
|
|
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center font-bold text-lg uppercase transition-all ${
|
|
isSelected ? 'bg-brand-500 text-white' : 'bg-slate-200 dark:bg-slate-700 text-slate-500'
|
|
}`}>
|
|
{e.name.charAt(0)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className={`font-bold text-lg transition-colors ${isSelected ? 'text-brand-700 dark:text-brand-400' : 'dark:text-white'}`}>{e.name}</p>
|
|
<p className="text-xs text-slate-500 uppercase font-bold tracking-widest">{e.role}</p>
|
|
</div>
|
|
{isSelected && (
|
|
<div className="w-6 h-6 bg-brand-500 text-white rounded-full flex items-center justify-center shadow-lg">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /></svg>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
{experts.length === 0 && <p className="text-center py-10 text-slate-500 italic">Keine Experten gefunden.</p>}
|
|
</div>
|
|
<div className="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800 flex justify-between items-center gap-4">
|
|
<p className="text-xs font-bold text-slate-400">{(editForm.expert || '').split(', ').filter(Boolean).length} Experten gewählt</p>
|
|
<button
|
|
onClick={() => setIsExpertPickerOpen(false)}
|
|
className="px-8 py-4 bg-brand-600 text-white font-bold rounded-2xl shadow-xl shadow-brand-500/20 hover:bg-brand-700 transition-all"
|
|
>
|
|
Fertig
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Confirmation Modal */}
|
|
{projectToDelete && (
|
|
<div className="fixed inset-0 z-[120] 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 dark:border-red-900/30">
|
|
<div className="w-16 h-16 bg-red-100 dark:bg-red-900/20 text-red-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
|
</div>
|
|
<h3 className="text-2xl font-bold mb-2 dark:text-white">Projekt löschen?</h3>
|
|
<p className="text-slate-500 mb-8 text-sm">Diese Aktion kann nicht rückgängig gemacht werden. Alle verknüpften Journal-Einträge gehen verloren.</p>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<button onClick={() => setProjectToDelete(null)} className="py-4 bg-slate-100 dark:bg-slate-800 rounded-2xl font-bold text-slate-600 dark:text-slate-300">Abbruch</button>
|
|
<button onClick={confirmDelete} className="py-4 bg-red-600 text-white rounded-2xl font-bold shadow-lg shadow-red-500/20 hover:bg-red-700 transition-all">Löschen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|