This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/sdk/dsr/DSRCommunicationLog.tsx
BreakPilot Dev 660295e218 fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit
restores all missing components:

- Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education,
  infrastructure, communication, development, onboarding, rbac
- SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen,
  vendor-compliance, tom-generator, dsr, and more
- Developer portal (25 pages): API docs, SDK guides, frameworks
- All components, lib files, hooks, and types
- Updated package.json with all dependencies

The issue was caused by incomplete initial repository state - the full
admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2
but was never fully synced to the main admin-v2 directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 23:40:15 -08:00

289 lines
12 KiB
TypeScript

'use client'
import React, { useState } from 'react'
import {
DSRCommunication,
CommunicationType,
CommunicationChannel,
DSRSendCommunicationRequest
} from '@/lib/sdk/dsr/types'
interface DSRCommunicationLogProps {
communications: DSRCommunication[]
onSendMessage?: (message: DSRSendCommunicationRequest) => Promise<void>
isLoading?: boolean
}
const CHANNEL_ICONS: Record<CommunicationChannel, string> = {
email: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
letter: 'M3 19v-8.93a2 2 0 01.89-1.664l7-4.666a2 2 0 012.22 0l7 4.666A2 2 0 0121 10.07V19M3 19a2 2 0 002 2h14a2 2 0 002-2M3 19l6.75-4.5M21 19l-6.75-4.5M3 10l6.75 4.5M21 10l-6.75 4.5',
phone: 'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z',
portal: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9',
internal_note: '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'
}
const TYPE_COLORS: Record<CommunicationType, { bg: string; border: string; icon: string }> = {
incoming: { bg: 'bg-blue-50', border: 'border-blue-200', icon: 'text-blue-600' },
outgoing: { bg: 'bg-green-50', border: 'border-green-200', icon: 'text-green-600' },
internal: { bg: 'bg-gray-50', border: 'border-gray-200', icon: 'text-gray-600' }
}
const CHANNEL_LABELS: Record<CommunicationChannel, string> = {
email: 'E-Mail',
letter: 'Brief',
phone: 'Telefon',
portal: 'Portal',
internal_note: 'Interne Notiz'
}
function formatDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
export function DSRCommunicationLog({
communications,
onSendMessage,
isLoading = false
}: DSRCommunicationLogProps) {
const [showComposeForm, setShowComposeForm] = useState(false)
const [newMessage, setNewMessage] = useState<DSRSendCommunicationRequest>({
type: 'outgoing',
channel: 'email',
subject: '',
content: ''
})
const [isSending, setIsSending] = useState(false)
const sortedCommunications = [...communications].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
const handleSendMessage = async () => {
if (!onSendMessage || !newMessage.content.trim()) return
setIsSending(true)
try {
await onSendMessage(newMessage)
setNewMessage({ type: 'outgoing', channel: 'email', subject: '', content: '' })
setShowComposeForm(false)
} catch (error) {
console.error('Failed to send message:', error)
} finally {
setIsSending(false)
}
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">Kommunikation</h3>
{onSendMessage && (
<button
onClick={() => setShowComposeForm(!showComposeForm)}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Nachricht
</button>
)}
</div>
{/* Compose Form */}
{showComposeForm && (
<div className="bg-white rounded-xl border border-gray-200 p-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Typ</label>
<select
value={newMessage.type}
onChange={(e) => setNewMessage({ ...newMessage, type: e.target.value as CommunicationType })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="outgoing">Ausgehend</option>
<option value="incoming">Eingehend</option>
<option value="internal">Interne Notiz</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kanal</label>
<select
value={newMessage.channel}
onChange={(e) => setNewMessage({ ...newMessage, channel: e.target.value as CommunicationChannel })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="email">E-Mail</option>
<option value="letter">Brief</option>
<option value="phone">Telefon</option>
<option value="portal">Portal</option>
<option value="internal_note">Interne Notiz</option>
</select>
</div>
</div>
{newMessage.type !== 'internal' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Betreff</label>
<input
type="text"
value={newMessage.subject || ''}
onChange={(e) => setNewMessage({ ...newMessage, subject: e.target.value })}
placeholder="Betreff eingeben..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Inhalt</label>
<textarea
value={newMessage.content}
onChange={(e) => setNewMessage({ ...newMessage, content: e.target.value })}
placeholder="Nachricht eingeben..."
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-none"
/>
</div>
<div className="flex items-center justify-end gap-3">
<button
onClick={() => setShowComposeForm(false)}
className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
>
Abbrechen
</button>
<button
onClick={handleSendMessage}
disabled={!newMessage.content.trim() || isSending}
className={`
px-4 py-2 rounded-lg font-medium transition-colors
${newMessage.content.trim() && !isSending
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}
`}
>
{isSending ? 'Sende...' : 'Speichern'}
</button>
</div>
</div>
)}
{/* Communication Timeline */}
{isLoading ? (
<div className="flex items-center justify-center py-8">
<svg className="animate-spin w-6 h-6 text-purple-600" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
</div>
) : sortedCommunications.length === 0 ? (
<div className="bg-gray-50 rounded-xl p-8 text-center">
<div className="w-12 h-12 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
</div>
<p className="text-gray-500">Noch keine Kommunikation vorhanden</p>
</div>
) : (
<div className="relative">
{/* Timeline Line */}
<div className="absolute left-5 top-0 bottom-0 w-0.5 bg-gray-200" />
{/* Timeline Items */}
<div className="space-y-4">
{sortedCommunications.map((comm) => {
const colors = TYPE_COLORS[comm.type]
return (
<div key={comm.id} className="relative pl-12">
{/* Timeline Dot */}
<div className={`
absolute left-3 w-5 h-5 rounded-full border-2 ${colors.border} ${colors.bg}
flex items-center justify-center
`}>
<svg className={`w-3 h-3 ${colors.icon}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={CHANNEL_ICONS[comm.channel]} />
</svg>
</div>
{/* Content Card */}
<div className={`${colors.bg} border ${colors.border} rounded-xl p-4`}>
{/* Header */}
<div className="flex items-start justify-between mb-2">
<div>
<div className="flex items-center gap-2">
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${colors.bg} ${colors.icon} border ${colors.border}`}>
{comm.type === 'incoming' ? 'Eingehend' :
comm.type === 'outgoing' ? 'Ausgehend' : 'Intern'}
</span>
<span className="text-xs text-gray-500">
{CHANNEL_LABELS[comm.channel]}
</span>
</div>
{comm.subject && (
<div className="font-medium text-gray-900 mt-1">
{comm.subject}
</div>
)}
</div>
<div className="text-xs text-gray-500">
{formatDate(comm.createdAt)}
</div>
</div>
{/* Content */}
<div className="text-sm text-gray-700 whitespace-pre-wrap">
{comm.content}
</div>
{/* Attachments */}
{comm.attachments && comm.attachments.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="text-xs text-gray-500 mb-2">Anhaenge:</div>
<div className="flex flex-wrap gap-2">
{comm.attachments.map((attachment, idx) => (
<a
key={idx}
href={attachment.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 px-2 py-1 bg-white rounded border border-gray-200 text-xs text-gray-600 hover:bg-gray-50"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
</svg>
{attachment.name}
</a>
))}
</div>
</div>
)}
{/* Footer */}
{comm.sentBy && (
<div className="mt-2 text-xs text-gray-500">
Von: {comm.sentBy}
</div>
)}
</div>
</div>
)
})}
</div>
</div>
)}
</div>
)
}