- Refactoring: Error Boundaries, apiFetch Wrapper, Socket Status-Tracking - Refactoring: UI Kontrast (theme-aware colors), unused imports bereinigt - Symbol-Verwaltung: Neues Split-Panel (Meine Symbole + Bibliothek) - Symbol-Verwaltung: Umbenennen (TLF rot/blau), Duplikate erlaubt - Symbol-Verwaltung: Karten-Sidebar zeigt eigene Symbole bevorzugt - Schlauch-Labels: Groessere Schrift (13px/10px), verschiebbar (Drag) - Schema: TenantSymbol customName, sortOrder, unique constraint entfernt - Open Source Referenz entfernt (kostenloses Projekt)
151 lines
5.7 KiB
TypeScript
151 lines
5.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { useToast } from '@/components/ui/use-toast'
|
|
import { BookOpen, Plus, X, Download, Upload } from 'lucide-react'
|
|
|
|
interface SuggestionsTabProps {
|
|
tenantId: string | undefined
|
|
}
|
|
|
|
export function SuggestionsTab({ tenantId }: SuggestionsTabProps) {
|
|
const { toast } = useToast()
|
|
const [journalSuggestions, setJournalSuggestions] = useState<string[]>([])
|
|
const [newSuggestion, setNewSuggestion] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (!tenantId) return
|
|
fetch(`/api/tenants/${tenantId}/suggestions`)
|
|
.then(r => r.ok ? r.json() : null)
|
|
.then(data => { if (data?.suggestions) setJournalSuggestions(data.suggestions) })
|
|
.catch(() => {})
|
|
}, [tenantId])
|
|
|
|
const saveSuggestions = (updated: string[]) => {
|
|
if (!tenantId) return
|
|
fetch(`/api/tenants/${tenantId}/suggestions`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ suggestions: updated }),
|
|
}).then(r => { if (r.ok) toast({ title: 'Gespeichert' }) })
|
|
}
|
|
|
|
const handleAdd = () => {
|
|
const trimmed = newSuggestion.trim()
|
|
if (!trimmed || journalSuggestions.includes(trimmed)) return
|
|
const updated = [...journalSuggestions, trimmed].sort((a, b) => a.localeCompare(b, 'de'))
|
|
setJournalSuggestions(updated)
|
|
setNewSuggestion('')
|
|
saveSuggestions(updated)
|
|
}
|
|
|
|
const handleRemove = (index: number) => {
|
|
const updated = journalSuggestions.filter((_, idx) => idx !== index)
|
|
setJournalSuggestions(updated)
|
|
saveSuggestions(updated)
|
|
toast({ title: 'Entfernt' })
|
|
}
|
|
|
|
return (
|
|
<div className="border rounded-lg p-6">
|
|
<h3 className="font-semibold text-lg mb-2 flex items-center gap-2">
|
|
<BookOpen className="w-5 h-5" />
|
|
Journal-Wörterliste
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
Häufige Begriffe und Textbausteine, die beim Erfassen von Journal-Einträgen als Vorschläge erscheinen.
|
|
Wenn der Benutzer im "Was..."-Feld tippt, werden passende Begriffe vorgeschlagen.
|
|
</p>
|
|
|
|
{/* Add new suggestion */}
|
|
<div className="flex gap-2 mb-4">
|
|
<Input
|
|
placeholder="Neuer Begriff, z.B. 'Leitung aufbauen'..."
|
|
value={newSuggestion}
|
|
onChange={(e) => setNewSuggestion(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' && newSuggestion.trim()) handleAdd()
|
|
}}
|
|
className="flex-1"
|
|
/>
|
|
<Button onClick={handleAdd} disabled={!newSuggestion.trim()}>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Hinzufügen
|
|
</Button>
|
|
</div>
|
|
|
|
{/* List of suggestions */}
|
|
{journalSuggestions.length === 0 ? (
|
|
<div className="text-center text-muted-foreground py-8 text-sm border-2 border-dashed rounded-lg">
|
|
Noch keine Begriffe hinterlegt. Fügen Sie häufig verwendete Textbausteine hinzu,<br />
|
|
z.B. "Leitung aufbauen", "Leitung abbauen", "Lüfter in Stellung", etc.
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-wrap gap-2">
|
|
{journalSuggestions.map((s, i) => (
|
|
<span key={i} className="inline-flex items-center gap-1 px-3 py-1.5 bg-blue-50 dark:bg-blue-950/30 text-blue-700 dark:text-blue-300 rounded-full text-sm border border-blue-200 dark:border-blue-800">
|
|
{s}
|
|
<button
|
|
onClick={() => handleRemove(i)}
|
|
className="ml-1 hover:text-red-500 transition-colors"
|
|
>
|
|
<X className="w-3.5 h-3.5" />
|
|
</button>
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2 mt-4 pt-4 border-t">
|
|
<p className="text-xs text-muted-foreground flex-1">
|
|
{journalSuggestions.length} Begriff(e) hinterlegt. Änderungen werden automatisch gespeichert.
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const blob = new Blob([journalSuggestions.join('\n')], { type: 'text/plain' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = 'woerterliste.txt'
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
toast({ title: 'Exportiert', description: `${journalSuggestions.length} Begriffe exportiert` })
|
|
}}
|
|
disabled={journalSuggestions.length === 0}
|
|
>
|
|
<Download className="w-4 h-4 mr-1" />
|
|
Export
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const input = document.createElement('input')
|
|
input.type = 'file'
|
|
input.accept = '.txt,.csv'
|
|
input.onchange = async (e) => {
|
|
const file = (e.target as HTMLInputElement).files?.[0]
|
|
if (!file) return
|
|
const text = await file.text()
|
|
const words = text.split(/[\n\r,;]+/).map(w => w.trim()).filter(Boolean)
|
|
if (words.length === 0) { toast({ title: 'Keine Begriffe gefunden', variant: 'destructive' }); return }
|
|
const merged = Array.from(new Set([...journalSuggestions, ...words])).sort((a, b) => a.localeCompare(b, 'de'))
|
|
setJournalSuggestions(merged)
|
|
saveSuggestions(merged)
|
|
toast({ title: 'Importiert', description: `${words.length} Begriffe importiert (${merged.length} total)` })
|
|
}
|
|
input.click()
|
|
}}
|
|
>
|
|
<Upload className="w-4 h-4 mr-1" />
|
|
Import
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|