Bündelt die Neuentwicklung in AssigmentMonitorV2 (eigene Ports/DB), die Umstellung auf numerische Bewertungen, Meeting-Checklisten, KI-Tagging und die geplante Feedback-Kaskade — als Basis für Versionsverwaltung in Gitea. Co-authored-by: Cursor <cursoragent@cursor.com>
496 lines
24 KiB
TypeScript
496 lines
24 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import { useNavigate, useParams } from 'react-router-dom'
|
||
import {
|
||
type Assignment, type AssignmentType, type PhaseTemplate, type MeetingInstance, type Consultant, type Assessment, type RatingScore, type FeedbackCategory, type FeedbackCriterionItem,
|
||
getAssignment, getAssignmentType,
|
||
listPhaseTemplatesForType,
|
||
listMeetingInstancesForAssignment, addMeetingInstance, softDeleteMeetingInstance, restoreMeetingInstance, hardDeleteMeetingInstance,
|
||
getConsultantsByIds,
|
||
listAssessmentsForMeetings,
|
||
listMeetingCategoryRatingsForMeetings,
|
||
loadFeedbackStructure, resolveVisibleStructure,
|
||
isAssignmentFeedbackComplete, softDeleteAssignment, archiveAssignment,
|
||
} from '../db'
|
||
import { exportMeetingAsMarkdown, exportMeetingAsJson, downloadFile } from '../utils/meetingExport'
|
||
import { exportAssignmentReportPdf, exportInstituteeFeedbackPdf } from '../utils/pdfExport'
|
||
import { resolveScoreStyle } from '../config/constants'
|
||
import { resolveCategoryChipScore } from '../utils/meetingCategoryScore'
|
||
import { useRatingColorBands } from '../hooks/useRatingColorBands'
|
||
import type { MeetingCategoryRating } from '../db'
|
||
|
||
interface PhaseRow {
|
||
template: PhaseTemplate
|
||
instances: MeetingInstance[]
|
||
}
|
||
|
||
export default function AssignmentDetail() {
|
||
const { id } = useParams()
|
||
const navigate = useNavigate()
|
||
const assignmentId = Number(id)
|
||
|
||
const [assignment, setAssignment] = useState<Assignment | null>(null)
|
||
const [assignmentType, setAssignmentType] = useState<AssignmentType | null>(null)
|
||
const [phaseRows, setPhaseRows] = useState<PhaseRow[]>([])
|
||
const [allTemplates, setAllTemplates] = useState<PhaseTemplate[]>([])
|
||
const [institutees, setInstitutees] = useState<Consultant[]>([])
|
||
const [deletedInstances, setDeletedInstances] = useState<MeetingInstance[]>([])
|
||
const [showAdHoc, setShowAdHoc] = useState(false)
|
||
const [showTrash, setShowTrash] = useState(false)
|
||
const [exportMenuId, setExportMenuId] = useState<number | null>(null)
|
||
const [confirmSoftDelete, setConfirmSoftDelete] = useState<number | null>(null)
|
||
const [confirmHardDelete, setConfirmHardDelete] = useState<number | null>(null)
|
||
const [allAssessments, setAllAssessments] = useState<Assessment[]>([])
|
||
const [categoryRatings, setCategoryRatings] = useState<MeetingCategoryRating[]>([])
|
||
const [fbCategories, setFbCategories] = useState<FeedbackCategory[]>([])
|
||
const [fbItems, setFbItems] = useState<FeedbackCriterionItem[]>([])
|
||
const colorBands = useRatingColorBands()
|
||
const [feedbackComplete, setFeedbackComplete] = useState(false)
|
||
const [confirmAssignmentTrash, setConfirmAssignmentTrash] = useState(false)
|
||
const [confirmAssignmentArchive, setConfirmAssignmentArchive] = useState(false)
|
||
const [pdfExportingKey, setPdfExportingKey] = useState<string | null>(null)
|
||
|
||
const load = async () => {
|
||
const a = await getAssignment(assignmentId)
|
||
if (!a) return
|
||
setAssignment(a)
|
||
setFeedbackComplete(a.status === 'done' ? await isAssignmentFeedbackComplete(assignmentId) : false)
|
||
|
||
const [aType, templates, instances, cons] = await Promise.all([
|
||
getAssignmentType(a.assignmentTypeId),
|
||
listPhaseTemplatesForType(a.assignmentTypeId),
|
||
listMeetingInstancesForAssignment(assignmentId),
|
||
getConsultantsByIds(a.instituteeIds),
|
||
])
|
||
|
||
setAssignmentType(aType ?? null)
|
||
setAllTemplates(templates)
|
||
setInstitutees(a.instituteeIds.map(cid => cons.find(c => c.id === cid)!).filter(Boolean))
|
||
const active = instances.filter(i => !i.deletedAt)
|
||
const deleted = instances.filter(i => !!i.deletedAt)
|
||
setDeletedInstances(deleted)
|
||
setPhaseRows(templates.map(t => ({
|
||
template: t,
|
||
instances: active.filter(i => i.phaseTemplateId === t.id).sort((a, b) => a.phaseIndex - b.phaseIndex),
|
||
})))
|
||
const instIds = active.map(i => i.id!)
|
||
const [asmts, catRatings, structure, aType2] = await Promise.all([
|
||
listAssessmentsForMeetings(instIds),
|
||
listMeetingCategoryRatingsForMeetings(instIds),
|
||
loadFeedbackStructure(),
|
||
getAssignmentType(a.assignmentTypeId),
|
||
])
|
||
setAllAssessments(asmts)
|
||
setCategoryRatings(catRatings)
|
||
const visible = resolveVisibleStructure(structure.dimensions, structure.categories, structure.items, aType2?.criteriaIds)
|
||
setFbItems(visible.items)
|
||
setFbCategories(visible.categories)
|
||
}
|
||
|
||
useEffect(() => { load() }, [assignmentId])
|
||
|
||
const startMeeting = async (templateId: number, existingCount: number) => {
|
||
const mid = await addMeetingInstance({
|
||
assignmentId,
|
||
phaseTemplateId: templateId,
|
||
phaseIndex: existingCount + 1,
|
||
date: new Date().toISOString().slice(0, 10),
|
||
status: 'active',
|
||
generalNotes: '',
|
||
})
|
||
navigate(`/assignment/${assignmentId}/meeting/${mid}`)
|
||
}
|
||
|
||
const buildFilename = (inst: MeetingInstance, ext: string) => {
|
||
const tmpl = allTemplates.find(t => t.id === inst.phaseTemplateId)
|
||
const date = new Date(inst.date).toLocaleDateString('de-DE').replace(/\./g, '-')
|
||
const label = tmpl?.label ?? 'Meeting'
|
||
const idx = inst.phaseIndex > 1 ? `-${inst.phaseIndex}` : ''
|
||
const id = assignment?.assignmentNumber ?? assignment?.title ?? 'export'
|
||
return `${id}_${date}_${label}${idx}.${ext}`
|
||
}
|
||
|
||
const handleExportMd = async (inst: MeetingInstance) => {
|
||
const md = await exportMeetingAsMarkdown(inst.id!)
|
||
downloadFile(md, buildFilename(inst, 'md'), 'text/markdown')
|
||
setExportMenuId(null)
|
||
}
|
||
|
||
const handleExportJson = async (inst: MeetingInstance) => {
|
||
const json = await exportMeetingAsJson(inst.id!)
|
||
downloadFile(json, buildFilename(inst, 'json'), 'application/json')
|
||
setExportMenuId(null)
|
||
}
|
||
|
||
const softDeleteMeeting = async (instId: number) => {
|
||
await softDeleteMeetingInstance(instId)
|
||
setConfirmSoftDelete(null)
|
||
await load()
|
||
}
|
||
|
||
const restoreMeeting = async (instId: number) => {
|
||
await restoreMeetingInstance(instId)
|
||
await load()
|
||
}
|
||
|
||
const hardDeleteMeeting = async (instId: number) => {
|
||
await hardDeleteMeetingInstance(instId)
|
||
setConfirmHardDelete(null)
|
||
await load()
|
||
}
|
||
|
||
const trashAssignment = async () => {
|
||
await softDeleteAssignment(assignmentId)
|
||
navigate('/')
|
||
}
|
||
|
||
const archiveThisAssignment = async () => {
|
||
await archiveAssignment(assignmentId)
|
||
navigate('/')
|
||
}
|
||
|
||
const handleExportReportPdf = async () => {
|
||
setPdfExportingKey('report')
|
||
try {
|
||
await exportAssignmentReportPdf(assignmentId)
|
||
} finally {
|
||
setPdfExportingKey(null)
|
||
}
|
||
}
|
||
|
||
const handleExportInstituteeFeedbackPdf = async (instituteeId: number) => {
|
||
const key = `inst-${instituteeId}`
|
||
setPdfExportingKey(key)
|
||
try {
|
||
await exportInstituteeFeedbackPdf(assignmentId, instituteeId)
|
||
} finally {
|
||
setPdfExportingKey(null)
|
||
}
|
||
}
|
||
|
||
const statusBadge = (status: MeetingInstance['status']) => {
|
||
const map = {
|
||
planned: 'bg-yellow-100 text-yellow-700',
|
||
active: 'bg-green-100 text-green-700',
|
||
done: 'bg-gray-100 text-gray-500',
|
||
}
|
||
const label = { planned: 'Geplant', active: 'Aktiv', done: 'Fertig' }
|
||
return <span className={`text-xs px-2 py-0.5 rounded-full font-medium ${map[status]}`}>{label[status]}</span>
|
||
}
|
||
|
||
if (!assignment) return <div className="text-center py-10 text-gray-400">Lädt…</div>
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div>
|
||
<button onClick={() => navigate('/')} className="text-sm text-brand mb-2 block">← Zurück</button>
|
||
<div className="flex items-start justify-between gap-2">
|
||
<div>
|
||
{assignment.assignmentNumber && (
|
||
<div className="text-xs font-semibold text-brand uppercase tracking-wide">{assignment.assignmentNumber}</div>
|
||
)}
|
||
<h1 className="text-xl font-bold text-gray-800 leading-tight">{assignment.title}</h1>
|
||
</div>
|
||
<div className="flex-shrink-0 flex gap-1.5">
|
||
<button
|
||
onClick={() => navigate(`/assignment/${assignmentId}/history`)}
|
||
className="text-xs px-3 py-1.5 border border-gray-200 rounded-lg text-gray-500 hover:border-brand hover:text-brand transition">
|
||
📜 Gesamtprotokoll
|
||
</button>
|
||
<button
|
||
onClick={handleExportReportPdf}
|
||
disabled={pdfExportingKey === 'report'}
|
||
title="Gesamtprotokoll + Feedback als PDF"
|
||
className="text-xs px-3 py-1.5 border border-gray-200 rounded-lg text-gray-500 hover:border-brand hover:text-brand transition disabled:opacity-50">
|
||
{pdfExportingKey === 'report' ? '⏳' : '⬇ PDF'}
|
||
</button>
|
||
<button
|
||
onClick={() => navigate(`/assignment/${assignmentId}/edit`)}
|
||
className="text-xs px-3 py-1.5 border border-gray-200 rounded-lg text-gray-500 hover:border-brand hover:text-brand transition">
|
||
✏️ Bearbeiten
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="text-sm text-gray-500">{assignment.client} · {assignmentType?.name}</div>
|
||
<div className="text-xs text-gray-400 mt-1">
|
||
{institutees.map(c => `${c.firstName} ${c.lastName}`).join(', ')}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Assignment-Aktionen (nur für abgeschlossene Assignments) */}
|
||
{assignment.status === 'done' && (
|
||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 space-y-2">
|
||
<span className="text-sm font-semibold text-gray-700">Assignment-Aktionen</span>
|
||
<div className="flex gap-2 flex-wrap">
|
||
<button
|
||
onClick={() => setConfirmAssignmentTrash(p => !p)}
|
||
disabled={!feedbackComplete}
|
||
title={!feedbackComplete ? 'Erst möglich, wenn für alle Institutees ein finales Feedback vorliegt' : undefined}
|
||
className={`text-xs px-3 py-1.5 rounded-lg font-medium ${
|
||
feedbackComplete ? 'bg-red-50 text-red-600 border border-red-200 hover:bg-red-100' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||
}`}>
|
||
🗑 In Papierkorb verschieben
|
||
</button>
|
||
<button
|
||
onClick={() => setConfirmAssignmentArchive(p => !p)}
|
||
className="text-xs px-3 py-1.5 rounded-lg font-medium bg-gray-50 text-gray-600 border border-gray-200 hover:bg-gray-100">
|
||
🗄 Archivieren
|
||
</button>
|
||
</div>
|
||
{!feedbackComplete && (
|
||
<div className="text-xs text-gray-400">
|
||
Löschen erst möglich, sobald für alle Institutees ein finales Feedback vorliegt.
|
||
</div>
|
||
)}
|
||
{confirmAssignmentTrash && (
|
||
<div className="bg-red-50 border border-red-200 rounded-lg px-3 py-2.5 flex items-center justify-between gap-2">
|
||
<span className="text-xs text-red-700">Assignment in den Papierkorb verschieben?</span>
|
||
<div className="flex gap-1.5">
|
||
<button onClick={trashAssignment} className="text-xs bg-red-600 text-white px-2.5 py-1 rounded-lg font-medium">Ja</button>
|
||
<button onClick={() => setConfirmAssignmentTrash(false)} className="text-xs border border-gray-200 text-gray-500 px-2.5 py-1 rounded-lg">Nein</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{confirmAssignmentArchive && (
|
||
<div className="bg-gray-50 border border-gray-200 rounded-lg px-3 py-2.5 flex items-center justify-between gap-2">
|
||
<span className="text-xs text-gray-600">Assignment archivieren?</span>
|
||
<div className="flex gap-1.5">
|
||
<button onClick={archiveThisAssignment} className="text-xs bg-gray-700 text-white px-2.5 py-1 rounded-lg font-medium">Ja</button>
|
||
<button onClick={() => setConfirmAssignmentArchive(false)} className="text-xs border border-gray-200 text-gray-500 px-2.5 py-1 rounded-lg">Nein</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Ad-hoc Meeting */}
|
||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm font-semibold text-gray-700">Ad-hoc Meeting starten</span>
|
||
<button onClick={() => setShowAdHoc(p => !p)}
|
||
className="text-xs bg-brand text-white px-3 py-1.5 rounded-lg font-medium">
|
||
{showAdHoc ? 'Abbrechen' : '+ Ad-hoc'}
|
||
</button>
|
||
</div>
|
||
{showAdHoc && (
|
||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||
{allTemplates.map(t => (
|
||
<button key={t.id}
|
||
onClick={() => {
|
||
const count = phaseRows.find(r => r.template.id === t.id)?.instances.length ?? 0
|
||
startMeeting(t.id!, count)
|
||
}}
|
||
className="py-2.5 px-3 text-left rounded-lg border border-gray-200 hover:border-brand hover:bg-blue-50 transition">
|
||
<div className="text-sm font-medium text-gray-700">{t.label}</div>
|
||
<div className={`text-xs mt-0.5 ${t.withWhom === 'client' ? 'text-blue-500' : 'text-purple-500'}`}>
|
||
{t.withWhom === 'client' ? 'Kundentermin' : 'Lead-intern'}
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Feedback-Entwürfe */}
|
||
{institutees.length > 0 && (
|
||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 flex items-center justify-between">
|
||
<span className="text-sm text-amber-800 font-medium">Feedback-Entwürfe</span>
|
||
<div className="flex gap-2 flex-wrap">
|
||
{institutees.map(c => (
|
||
<div key={c.id} className="flex items-center gap-1">
|
||
<button
|
||
onClick={() => navigate(`/assignment/${assignmentId}/feedback/${c.id}`)}
|
||
className="text-xs bg-amber-600 text-white px-3 py-1 rounded-lg">
|
||
{c.firstName}
|
||
</button>
|
||
<button
|
||
onClick={() => handleExportInstituteeFeedbackPdf(c.id!)}
|
||
disabled={pdfExportingKey === `inst-${c.id}`}
|
||
title={`Feedback für ${c.firstName} als PDF`}
|
||
className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded-lg disabled:opacity-50">
|
||
{pdfExportingKey === `inst-${c.id}` ? '⏳' : '⬇'}
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Phasen-Flow */}
|
||
<div className="space-y-3" onClick={() => setExportMenuId(null)}>
|
||
{phaseRows.map(({ template, instances }) => {
|
||
const canStart = instances.length === 0 || template.isRepeatable
|
||
return (
|
||
<div key={template.id} className="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex-1 min-w-0">
|
||
<span className="font-semibold text-gray-800 text-sm">{template.label}</span>
|
||
<span className={`ml-2 text-xs px-2 py-0.5 rounded-full ${
|
||
template.withWhom === 'client' ? 'bg-blue-100 text-blue-600' : 'bg-purple-100 text-purple-600'
|
||
}`}>
|
||
{template.withWhom === 'client' ? 'Kunde' : 'Lead'}
|
||
</span>
|
||
{template.hasConversation && (
|
||
<span className="ml-1 text-xs bg-orange-100 text-orange-600 px-2 py-0.5 rounded-full">
|
||
Protokoll
|
||
</span>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={() => canStart && startMeeting(template.id!, instances.length)}
|
||
className={`ml-2 flex-shrink-0 text-xs px-3 py-1.5 rounded-lg font-medium ${
|
||
canStart ? 'bg-brand text-white' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||
}`}
|
||
disabled={!canStart}>
|
||
{instances.length === 0 ? 'Starten' : template.isRepeatable ? '+ Wiederholen' : 'Erledigt'}
|
||
</button>
|
||
</div>
|
||
|
||
{instances.map(inst => (
|
||
<div key={inst.id} className="mt-2 relative">
|
||
<div
|
||
onClick={() => { if (confirmSoftDelete !== inst.id) navigate(`/assignment/${assignmentId}/meeting/${inst.id}`) }}
|
||
className="flex items-start justify-between py-2 px-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 gap-2">
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-sm text-gray-700">
|
||
{template.isRepeatable ? `#${inst.phaseIndex} · ` : ''}
|
||
{new Date(inst.date).toLocaleDateString('de-DE')}
|
||
</div>
|
||
{/* Per-institutee per-category chips */}
|
||
{institutees.map(c => {
|
||
const chips = fbCategories.map(cat => {
|
||
const catItems = fbItems.filter(i => i.categoryId === cat.id)
|
||
const mode = resolveCategoryChipScore({
|
||
meetingId: inst.id!,
|
||
instituteeId: c.id!,
|
||
categoryId: cat.id!,
|
||
categoryRatings,
|
||
assessments: allAssessments,
|
||
catItems,
|
||
})
|
||
return mode ? { cat, mode } : null
|
||
}).filter(Boolean) as { cat: FeedbackCategory; mode: RatingScore }[]
|
||
|
||
if (chips.length === 0) return null
|
||
return (
|
||
<div key={c.id} className="mt-1">
|
||
<span className="text-xs text-gray-400 mr-1">{c.firstName}:</span>
|
||
{chips.map(({ cat, mode }) => (
|
||
<span key={cat.id} className={`inline-block text-xs px-2 py-0.5 rounded font-medium mr-1 tabular-nums ${resolveScoreStyle(mode, colorBands).active}`}>
|
||
{cat.name} {mode}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
<div className="flex items-center gap-1 flex-shrink-0">
|
||
{statusBadge(inst.status)}
|
||
<button
|
||
onClick={e => { e.stopPropagation(); setExportMenuId(exportMenuId === inst.id ? null : inst.id!); setConfirmSoftDelete(null) }}
|
||
className="text-xs text-gray-400 hover:text-gray-600 px-1.5 py-1">
|
||
↓
|
||
</button>
|
||
<button
|
||
onClick={e => { e.stopPropagation(); setConfirmSoftDelete(confirmSoftDelete === inst.id ? null : inst.id!); setExportMenuId(null) }}
|
||
className="text-xs text-red-300 hover:text-red-500 px-1.5 py-1">
|
||
🗑
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{exportMenuId === inst.id && (
|
||
<div className="absolute right-8 top-full mt-0.5 bg-white border border-gray-200 rounded-xl shadow-lg z-10 min-w-[150px] overflow-hidden">
|
||
<button onClick={() => handleExportMd(inst)}
|
||
className="w-full text-left px-4 py-2.5 text-sm hover:bg-gray-50 border-b border-gray-100">
|
||
📄 Markdown
|
||
</button>
|
||
<button onClick={() => handleExportJson(inst)}
|
||
className="w-full text-left px-4 py-2.5 text-sm hover:bg-gray-50">
|
||
🗂 JSON-Backup
|
||
</button>
|
||
</div>
|
||
)}
|
||
{confirmSoftDelete === inst.id && (
|
||
<div className="mt-1 bg-red-50 border border-red-200 rounded-lg px-3 py-2.5 flex items-center justify-between gap-2">
|
||
<span className="text-xs text-red-700">In Papierkorb verschieben?</span>
|
||
<div className="flex gap-1.5">
|
||
<button onClick={() => softDeleteMeeting(inst.id!)}
|
||
className="text-xs bg-red-600 text-white px-2.5 py-1 rounded-lg font-medium">
|
||
Ja
|
||
</button>
|
||
<button onClick={() => setConfirmSoftDelete(null)}
|
||
className="text-xs border border-gray-200 text-gray-500 px-2.5 py-1 rounded-lg">
|
||
Nein
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* Papierkorb */}
|
||
{deletedInstances.length > 0 && (
|
||
<div className="border border-dashed border-gray-200 rounded-xl overflow-hidden">
|
||
<button
|
||
onClick={() => setShowTrash(p => !p)}
|
||
className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-gray-50 transition">
|
||
<span className="text-sm text-gray-400 font-medium">
|
||
🗑 Papierkorb ({deletedInstances.length})
|
||
</span>
|
||
<span className="text-gray-300 text-xs">{showTrash ? '▲' : '▼'}</span>
|
||
</button>
|
||
|
||
{showTrash && (
|
||
<div className="border-t border-dashed border-gray-200 divide-y divide-gray-100">
|
||
{deletedInstances.map(inst => {
|
||
const tmpl = allTemplates.find(t => t.id === inst.phaseTemplateId)
|
||
return (
|
||
<div key={inst.id} className="px-4 py-3 bg-gray-50">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="text-sm text-gray-500">{tmpl?.label ?? '—'}</div>
|
||
<div className="text-xs text-gray-400">
|
||
{new Date(inst.date).toLocaleDateString('de-DE')}
|
||
{inst.deletedAt && ` · gelöscht ${new Date(inst.deletedAt).toLocaleDateString('de-DE')}`}
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button onClick={() => restoreMeeting(inst.id!)}
|
||
className="text-xs bg-white border border-gray-200 text-gray-600 px-2.5 py-1 rounded-lg hover:bg-gray-100">
|
||
↺ Wiederherstellen
|
||
</button>
|
||
<button onClick={() => setConfirmHardDelete(inst.id!)}
|
||
className="text-xs text-red-400 hover:text-red-600 px-2 py-1">
|
||
✕
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{confirmHardDelete === inst.id && (
|
||
<div className="mt-2 bg-red-50 border border-red-200 rounded-lg p-3 space-y-2">
|
||
<div className="text-xs text-red-700 font-medium">Endgültig löschen? Alle Daten dieses Meetings werden unwiderruflich entfernt.</div>
|
||
<div className="flex gap-2">
|
||
<button onClick={() => hardDeleteMeeting(inst.id!)}
|
||
className="flex-1 py-1.5 rounded-lg text-xs font-medium bg-red-600 text-white">
|
||
Endgültig löschen
|
||
</button>
|
||
<button onClick={() => setConfirmHardDelete(null)}
|
||
className="px-3 py-1.5 rounded-lg text-xs border border-gray-200 text-gray-500">
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|