Files
IT-Nexus/frontend/src/pages/SettingsPage.jsx

902 lines
50 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback, useRef } from 'react';
import api from '../services/api';
import assetService from '../services/assetService';
const EMAIL_TEMPLATE_LABELS = {
comment_added: { label: '💬 Neuer Kommentar', desc: 'Gesendet an den Ersteller wenn Support antwortet' },
escalation: { label: '⚠️ Eskalation', desc: 'An Admins bei überfälligen kritischen/hohen Tickets' },
satisfaction: { label: '⭐ Zufriedenheits-Feedback', desc: 'Gesendet nach Ticketschließung' },
status_changed: { label: '🔄 Statusänderung', desc: 'An den Ersteller bei Statuswechsel' },
ticket_assigned: { label: '👤 Ticket-Zuweisung', desc: 'An den Bearbeiter bei Ticket-Zuweisung' },
ticket_created: { label: '✅ Ticket-Bestätigung', desc: 'An den Ersteller nach Ticket-Erstellung' },
};
const EMAIL_TEMPLATE_VARS = {
ticket_created: ['ticket_number', 'ticket_title', 'requester_name', 'category', 'priority'],
ticket_assigned: ['ticket_number', 'ticket_title', 'assignee_name', 'requester_name', 'requester_email', 'category', 'priority'],
comment_added: ['ticket_number', 'ticket_title', 'author_name'],
status_changed: ['ticket_number', 'ticket_title', 'old_status', 'new_status'],
escalation: ['ticket_number', 'ticket_title', 'priority', 'requester_name', 'age_hours'],
satisfaction: ['ticket_number', 'ticket_title', 'requester_name'],
};
const PRIORITIES = ['niedrig', 'mittel', 'hoch', 'kritisch'];
const PRIORITY_ICONS = { niedrig: '🟢', mittel: '🔵', hoch: '🟠', kritisch: '🔴' };
const COMMON_ICONS = ['📋', '💻', '🖥️', '🌐', '📊', '🖨️', '📧', '🔐', '📱', '🔧', '⚡', '🏢', '👤', '🗂️', '🔑'];
export default function SettingsPage() {
const [tab, setTab] = useState('categories');
// ── Categories state ──────────────────────────────────────────────────────
const [categories, setCategories] = useState([]);
const [catModal, setCatModal] = useState(null); // null | { id?, name, icon }
const [catLoading, setCatLoading] = useState(false);
// ── Templates state ───────────────────────────────────────────────────────
const [templates, setTemplates] = useState([]);
const [tplModal, setTplModal] = useState(null); // null | template object
const [tplLoading, setTplLoading] = useState(false);
// ── Email templates state ─────────────────────────────────────────────────
const [emailTpls, setEmailTpls] = useState([]);
const [emailModal, setEmailModal] = useState(null); // null | { type, label, subject, intro }
const [emailLoading, setEmailLoading] = useState(false);
const [lastFocused, setLastFocused] = useState('intro');
const emailSubjectRef = useRef(null);
const emailIntroRef = useRef(null);
// ── Email design state ────────────────────────────────────────────────────
const [emailDesign, setEmailDesign] = useState(null);
const [designDraft, setDesignDraft] = useState(null);
const [designSaving, setDesignSaving] = useState(false);
const [previewType, setPreviewType] = useState('ticket_created');
const [previewDark, setPreviewDark] = useState(false);
const [previewHtml, setPreviewHtml] = useState('');
const [previewLoading, setPreviewLoading] = useState(false);
const previewDebounce = useRef(null);
const loadCategories = useCallback(async () => {
const res = await api.get('/settings/categories');
setCategories(res.data);
}, []);
const loadTemplates = useCallback(async () => {
const res = await api.get('/settings/templates');
setTemplates(res.data);
}, []);
const loadEmailTpls = useCallback(async () => {
const res = await api.get('/settings/email-templates');
setEmailTpls(res.data);
}, []);
const loadEmailDesign = useCallback(async () => {
const res = await api.get('/settings/email-design');
setEmailDesign(res.data);
setDesignDraft(res.data);
}, []);
useEffect(() => { loadCategories(); loadTemplates(); loadEmailTpls(); loadEmailDesign(); }, [loadCategories, loadTemplates, loadEmailTpls, loadEmailDesign]);
// Fetch preview when designDraft or previewType changes
useEffect(() => {
if (!designDraft) return;
clearTimeout(previewDebounce.current);
previewDebounce.current = setTimeout(async () => {
setPreviewLoading(true);
try {
const apiUrl = process.env.REACT_APP_API_URL || '/api';
const token = localStorage.getItem('token');
const res = await fetch(`${apiUrl}/settings/email-preview/${previewType}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ design: designDraft, darkMode: previewDark }),
});
const html = await res.text();
setPreviewHtml(html);
} catch (_) {}
setPreviewLoading(false);
}, 600);
}, [designDraft, previewType, previewDark]); // eslint-disable-line
// ── Category CRUD ──────────────────────────────────────────────────────────
const saveCategory = async () => {
if (!catModal?.name?.trim()) return;
setCatLoading(true);
try {
if (catModal.id) {
await api.put(`/settings/categories/${catModal.id}`, { name: catModal.name, icon: catModal.icon });
} else {
await api.post('/settings/categories', { name: catModal.name, icon: catModal.icon });
}
await loadCategories();
setCatModal(null);
} catch (e) {
alert(e.response?.data?.message || 'Fehler beim Speichern');
} finally {
setCatLoading(false);
}
};
const deleteCategory = async (cat) => {
if (!window.confirm(`Kategorie „${cat.name}" wirklich löschen?`)) return;
await api.delete(`/settings/categories/${cat.id}`);
await loadCategories();
};
// ── Template CRUD ──────────────────────────────────────────────────────────
const saveTemplate = async () => {
if (!tplModal?.label?.trim()) return;
setTplLoading(true);
try {
if (tplModal.id) {
await api.put(`/settings/templates/${tplModal.id}`, tplModal);
} else {
await api.post('/settings/templates', tplModal);
}
await loadTemplates();
setTplModal(null);
} catch (e) {
alert(e.response?.data?.message || 'Fehler beim Speichern');
} finally {
setTplLoading(false);
}
};
const deleteTemplate = async (tpl) => {
if (!window.confirm(`Vorlage „${tpl.label}" wirklich löschen?`)) return;
await api.delete(`/settings/templates/${tpl.id}`);
await loadTemplates();
};
// ── Email design CRUD ─────────────────────────────────────────────────────
const saveDesign = async () => {
setDesignSaving(true);
try {
const res = await api.put('/settings/email-design', designDraft);
setEmailDesign(res.data);
setDesignDraft(res.data);
} catch (e) {
alert(e.response?.data?.message || 'Fehler beim Speichern');
} finally {
setDesignSaving(false);
}
};
const resetDesign = async () => {
if (!window.confirm('Design auf Standardwerte zurücksetzen?')) return;
try {
const res = await api.post('/settings/email-design/reset');
setEmailDesign(res.data);
setDesignDraft(res.data);
} catch (e) {
alert(e.response?.data?.message || 'Fehler');
}
};
const setDraft = (key, val) => setDesignDraft(d => ({ ...d, [key]: val }));
// ── Email template CRUD ───────────────────────────────────────────────────
const saveEmailTpl = async () => {
if (!emailModal?.subject?.trim() || !emailModal?.intro?.trim()) return;
setEmailLoading(true);
try {
await api.put(`/settings/email-templates/${emailModal.type}`, {
subject: emailModal.subject,
intro: emailModal.intro,
});
await loadEmailTpls();
setEmailModal(null);
} catch (e) {
alert(e.response?.data?.message || 'Fehler beim Speichern');
} finally {
setEmailLoading(false);
}
};
const resetEmailTpl = async (type) => {
if (!window.confirm('Vorlage auf Standardwerte zurücksetzen?')) return;
try {
await api.post(`/settings/email-templates/${type}/reset`);
await loadEmailTpls();
if (emailModal?.type === type) {
const res = await api.get('/settings/email-templates');
const updated = res.data.find(t => t.type === type);
if (updated) setEmailModal(m => ({ ...m, subject: updated.subject, intro: updated.intro }));
}
} catch (e) {
alert(e.response?.data?.message || 'Fehler beim Zurücksetzen');
}
};
const insertVar = (varName) => {
const isSubject = lastFocused === 'subject';
const el = isSubject ? emailSubjectRef.current : emailIntroRef.current;
const field = isSubject ? 'subject' : 'intro';
const token = `{{${varName}}}`;
if (el) {
const start = el.selectionStart ?? el.value.length;
const end = el.selectionEnd ?? el.value.length;
const newVal = el.value.substring(0, start) + token + el.value.substring(end);
setEmailModal(m => ({ ...m, [field]: newVal }));
setTimeout(() => {
el.focus();
el.selectionStart = el.selectionEnd = start + token.length;
}, 0);
} else {
setEmailModal(m => ({ ...m, [field]: (m[field] || '') + token }));
}
};
// ── Styles ─────────────────────────────────────────────────────────────────
const card = {
background: 'var(--bg-card)',
border: '1px solid var(--border-color)',
borderRadius: 'var(--radius-xl)',
padding: '24px',
marginBottom: '16px',
};
const rowStyle = {
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '10px 14px',
borderRadius: 'var(--radius-md)',
background: 'var(--bg-secondary)',
marginBottom: '8px',
};
const btnPrimary = {
background: 'var(--cereda-primary)',
color: '#fff',
border: 'none',
borderRadius: 'var(--radius-md)',
padding: '8px 16px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 600,
};
const btnOutline = {
background: 'transparent',
color: 'var(--text-secondary)',
border: '1px solid var(--border-color)',
borderRadius: 'var(--radius-md)',
padding: '6px 12px',
cursor: 'pointer',
fontSize: '13px',
};
const btnDanger = {
background: 'transparent',
color: 'var(--danger)',
border: '1px solid var(--danger)',
borderRadius: 'var(--radius-md)',
padding: '6px 12px',
cursor: 'pointer',
fontSize: '13px',
};
return (
<div className="main-content" style={{ padding: '40px 24px' }}>
{/* ── Constrained header + tabs ─────────────────────────────────── */}
<div style={{ maxWidth: '860px', margin: '0 auto' }}>
{/* Header */}
<div style={{ marginBottom: '28px' }}>
<h1 style={{ fontSize: '22px', fontWeight: 700, color: 'var(--text-primary)', margin: 0, display: 'flex', alignItems: 'center', gap: '10px' }}>
Einstellungen
</h1>
<p style={{ color: 'var(--text-muted)', fontSize: '14px', marginTop: '6px' }}>
Ticket-Kategorien, Vorlagen, E-Mail-Texte und E-Mail-Design verwalten
</p>
</div>
{/* Tabs */}
<div style={{ display: 'flex', gap: '4px', marginBottom: '24px', borderBottom: '1px solid var(--border-color)', paddingBottom: '0' }}>
{[
{ key: 'categories', label: '📂 Kategorien' },
{ key: 'templates', label: '📝 Vorlagen' },
{ key: 'asset-types', label: '🖥️ Asset-Typen' },
{ key: 'email-templates', label: '📧 E-Mail-Vorlagen' },
{ key: 'email-design', label: '🎨 E-Mail-Design' },
].map(t => (
<button key={t.key} onClick={() => setTab(t.key)} style={{
background: 'none',
border: 'none',
borderBottom: tab === t.key ? '2px solid var(--cereda-primary)' : '2px solid transparent',
color: tab === t.key ? 'var(--cereda-primary)' : 'var(--text-secondary)',
padding: '10px 18px',
cursor: 'pointer',
fontWeight: tab === t.key ? 700 : 400,
fontSize: '14px',
marginBottom: '-1px',
transition: 'var(--transition)',
}}>
{t.label}
</button>
))}
</div>
</div>{/* end constrained header/tabs */}
{/* ── Constrained tab content (all except email-design) ─────────── */}
<div style={{ maxWidth: '860px', margin: '0 auto', display: tab === 'email-design' ? 'none' : 'block' }}>
{/* ── CATEGORIES TAB ────────────────────────────────────────────── */}
{tab === 'categories' && (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '13px' }}>
{categories.length} Kategorien · werden beim Ticket-Erstellen angezeigt
</p>
<button style={btnPrimary} onClick={() => setCatModal({ name: '', icon: '📁' })}>
+ Kategorie hinzufügen
</button>
</div>
<div style={card}>
{categories.length === 0 && (
<p style={{ color: 'var(--text-muted)', textAlign: 'center', margin: '32px 0' }}>Keine Kategorien vorhanden</p>
)}
{categories.map(cat => (
<div key={cat.id} style={rowStyle}>
<span style={{ fontSize: '20px', minWidth: '28px', textAlign: 'center' }}>{cat.icon}</span>
<span style={{ flex: 1, fontWeight: 600, color: 'var(--text-primary)' }}>{cat.name}</span>
<button style={btnOutline} onClick={() => setCatModal({ id: cat.id, name: cat.name, icon: cat.icon })}>
Bearbeiten
</button>
<button style={btnDanger} onClick={() => deleteCategory(cat)}>
Löschen
</button>
</div>
))}
</div>
</div>
)}
{/* ── TEMPLATES TAB ─────────────────────────────────────────────── */}
{tab === 'templates' && (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '13px' }}>
{templates.length} Vorlagen · erscheinen im Ticket-Erstellungs-Dropdown
</p>
<button style={btnPrimary} onClick={() => setTplModal({ label: '', title: '', description: '', category: categories[0]?.name || 'Allgemein', priority: 'mittel' })}>
+ Vorlage hinzufügen
</button>
</div>
<div style={card}>
{templates.length === 0 && (
<p style={{ color: 'var(--text-muted)', textAlign: 'center', margin: '32px 0' }}>Keine Vorlagen vorhanden</p>
)}
{templates.map(tpl => (
<div key={tpl.id} style={rowStyle}>
<span style={{ fontSize: '16px' }}>{PRIORITY_ICONS[tpl.priority]}</span>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, color: 'var(--text-primary)', fontSize: '14px' }}>{tpl.label}</div>
<div style={{ color: 'var(--text-muted)', fontSize: '12px', marginTop: '2px' }}>
{tpl.category} · {tpl.title || <em>Kein Titel</em>}
</div>
</div>
<button style={btnOutline} onClick={() => setTplModal({ ...tpl })}>
Bearbeiten
</button>
<button style={btnDanger} onClick={() => deleteTemplate(tpl)}>
Löschen
</button>
</div>
))}
</div>
</div>
)}
{/* ── ASSET TYPES TAB ──────────────────────────────────────────── */}
{tab === 'asset-types' && <AssetTypesTab />}
{/* ── EMAIL TEMPLATES TAB ───────────────────────────────────────── */}
{tab === 'email-templates' && (
<div>
<p style={{ margin: '0 0 16px', color: 'var(--text-muted)', fontSize: '13px' }}>
Betreff und Einleitungstext der automatischen E-Mails anpassen. Variablen wie <code>{'{{ticket_number}}'}</code> werden beim Versand ersetzt.
</p>
<div style={card}>
{emailTpls.map(tpl => {
const meta = EMAIL_TEMPLATE_LABELS[tpl.type] || { label: tpl.type, desc: '' };
return (
<div key={tpl.type} style={rowStyle}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, color: 'var(--text-primary)', fontSize: '14px' }}>{meta.label}</div>
<div style={{ color: 'var(--text-muted)', fontSize: '12px', marginTop: '2px' }}>{meta.desc}</div>
</div>
<button style={btnOutline} onClick={() => setEmailModal({ ...tpl })}>Bearbeiten</button>
<button style={btnDanger} onClick={() => resetEmailTpl(tpl.type)}>Zurücksetzen</button>
</div>
);
})}
</div>
</div>
)}
</div>{/* end constrained tab content */}
{/* ── EMAIL DESIGN TAB full width ─────────────────────────────── */}
{tab === 'email-design' && designDraft && (
<div style={{ display: 'grid', gridTemplateColumns: '400px 1fr', gap: '28px', alignItems: 'start' }}>
{/* Left: settings (sticky) */}
<div style={{ position: 'sticky', top: '16px', maxHeight: 'calc(100vh - 80px)', overflowY: 'auto' }}>
<div style={card}>
<h3 style={{ margin: '0 0 16px', fontSize: '14px', fontWeight: 700, color: 'var(--text-primary)' }}>🏷 Marke</h3>
<div className="form-group">
<label className="form-label">Logo-URL</label>
<input className="form-input" value={designDraft.logo_url} onChange={e => setDraft('logo_url', e.target.value)} placeholder="https://... (leer = Emoji-Icon)" />
<p style={{ color: 'var(--text-muted)', fontSize: '11px', margin: '3px 0 0' }}>Bild-URL (PNG/SVG/JPG). Wenn leer wird das Emoji-Icon verwendet.</p>
</div>
<div className="form-group">
<label className="form-label">Emoji-Icon (Fallback)</label>
<input className="form-input" value={designDraft.brand_icon} onChange={e => setDraft('brand_icon', e.target.value)} placeholder="💻" style={{ fontSize: '18px', width: '80px' }} />
</div>
<div className="form-group">
<label className="form-label">Markenname</label>
<input className="form-input" value={designDraft.brand_name} onChange={e => setDraft('brand_name', e.target.value)} placeholder="CEREDA SYSTEMS" />
<p style={{ color: 'var(--text-muted)', fontSize: '11px', margin: '3px 0 0' }}>Erstes Wort dunkel, Rest in Primärfarbe.</p>
</div>
<div className="form-group">
<label className="form-label">Untertitel</label>
<input className="form-input" value={designDraft.brand_subtitle} onChange={e => setDraft('brand_subtitle', e.target.value)} placeholder="IT Support" />
</div>
<h3 style={{ margin: '20px 0 16px', fontSize: '14px', fontWeight: 700, color: 'var(--text-primary)' }}>🎨 Farben</h3>
{[
{ key: 'primary_color', label: 'Primärfarbe', desc: 'Akzentleiste, Links, Badge' },
{ key: 'button_color', label: 'Button-Farbe', desc: 'Hintergrund des Haupt-Buttons' },
{ key: 'bg_color', label: 'Hintergrundfarbe', desc: 'Äußerer E-Mail-Hintergrund' },
].map(({ key, label, desc }) => (
<div key={key} className="form-group">
<label className="form-label">{label}</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<input
type="color"
value={designDraft[key]}
onChange={e => setDraft(key, e.target.value)}
style={{ width: '44px', height: '36px', border: '1px solid var(--border-color)', borderRadius: '8px', cursor: 'pointer', padding: '2px' }}
/>
<input
className="form-input"
value={designDraft[key]}
onChange={e => setDraft(key, e.target.value)}
placeholder="#0d9488"
style={{ flex: 1, fontFamily: 'monospace', fontSize: '13px' }}
/>
</div>
<p style={{ color: 'var(--text-muted)', fontSize: '11px', margin: '3px 0 0' }}>{desc}</p>
</div>
))}
<h3 style={{ margin: '20px 0 16px', fontSize: '14px', fontWeight: 700, color: 'var(--text-primary)' }}>📝 Footer</h3>
<div className="form-group">
<label className="form-label">Firmenname</label>
<input className="form-input" value={designDraft.company_name} onChange={e => setDraft('company_name', e.target.value)} placeholder="Cereda Systems GmbH" />
</div>
<div className="form-group">
<label className="form-label">Footer-Text</label>
<textarea className="form-input" value={designDraft.footer_text} onChange={e => setDraft('footer_text', e.target.value)} rows={2} style={{ resize: 'vertical', fontSize: '13px' }} />
</div>
<div style={{ display: 'flex', gap: '10px', justifyContent: 'space-between', marginTop: '8px' }}>
<button style={btnDanger} onClick={resetDesign}>Zurücksetzen</button>
<button style={btnPrimary} onClick={saveDesign} disabled={designSaving}>
{designSaving ? 'Speichern...' : 'Design speichern'}
</button>
</div>
</div>
</div>
{/* Right: live preview */}
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-primary)' }}>Vorschau:</span>
<select
className="form-select"
value={previewType}
onChange={e => setPreviewType(e.target.value)}
style={{ flex: 1, fontSize: '13px' }}
>
{Object.entries(EMAIL_TEMPLATE_LABELS).map(([k, v]) => (
<option key={k} value={k}>{v.label}</option>
))}
</select>
{/* Dark/Light mode toggle */}
<button
onClick={() => setPreviewDark(v => !v)}
title={previewDark ? 'Zu Hell-Modus wechseln' : 'Zu Dunkel-Modus wechseln'}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '6px 12px',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--border-color)',
background: previewDark ? '#1e293b' : 'var(--bg-secondary)',
color: previewDark ? '#e2e8f0' : 'var(--text-secondary)',
cursor: 'pointer',
fontSize: '13px',
fontWeight: 500,
whiteSpace: 'nowrap',
transition: 'var(--transition)',
}}
>
{previewDark ? '🌙 Dark' : '☀️ Light'}
</button>
{previewLoading && <span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>Lädt...</span>}
</div>
<div style={{ border: '1px solid var(--border-color)', borderRadius: 'var(--radius-xl)', overflow: 'hidden', background: previewDark ? '#0f172a' : '#f1f5f9' }}>
<iframe
srcDoc={previewHtml}
style={{ width: '100%', height: 'calc(100vh - 260px)', minHeight: '600px', border: 'none', display: 'block' }}
title="E-Mail Vorschau"
sandbox="allow-same-origin"
/>
</div>
</div>
</div>
)}
{/* ── CATEGORY MODAL ────────────────────────────────────────────── */}
{catModal && (
<div className="modal-overlay" onClick={() => setCatModal(null)}>
<div className="modal-content" style={{ maxWidth: 460 }} onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">{catModal.id ? '✏️ Kategorie bearbeiten' : ' Neue Kategorie'}</h2>
<button className="modal-close" onClick={() => setCatModal(null)}>×</button>
</div>
<div className="form-group">
<label className="form-label">Name*</label>
<input
className="form-input"
value={catModal.name}
onChange={e => setCatModal(p => ({ ...p, name: e.target.value }))}
placeholder="z.B. Microsoft 365"
autoFocus
/>
</div>
<div className="form-group">
<label className="form-label">Icon (Emoji)</label>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '10px' }}>
{COMMON_ICONS.map(ic => (
<button
key={ic}
onClick={() => setCatModal(p => ({ ...p, icon: ic }))}
style={{
fontSize: '22px',
background: catModal.icon === ic ? 'var(--cereda-primary)20' : 'var(--bg-secondary)',
border: catModal.icon === ic ? '2px solid var(--cereda-primary)' : '2px solid transparent',
borderRadius: '8px',
width: '40px',
height: '40px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{ic}
</button>
))}
</div>
<input
className="form-input"
value={catModal.icon}
onChange={e => setCatModal(p => ({ ...p, icon: e.target.value }))}
placeholder="Oder eigenes Emoji eingeben"
style={{ fontSize: '18px' }}
/>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button style={btnOutline} onClick={() => setCatModal(null)}>Abbrechen</button>
<button style={btnPrimary} onClick={saveCategory} disabled={catLoading || !catModal.name?.trim()}>
{catLoading ? 'Speichern...' : 'Speichern'}
</button>
</div>
</div>
</div>
)}
{/* ── TEMPLATE MODAL ────────────────────────────────────────────── */}
{tplModal && (
<div className="modal-overlay" onClick={() => setTplModal(null)}>
<div className="modal-content modal-large" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">{tplModal.id ? '✏️ Vorlage bearbeiten' : ' Neue Vorlage'}</h2>
<button className="modal-close" onClick={() => setTplModal(null)}>×</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div className="form-group">
<label className="form-label">Anzeige-Name (Dropdown)*</label>
<input
className="form-input"
value={tplModal.label}
onChange={e => setTplModal(p => ({ ...p, label: e.target.value }))}
placeholder="z.B. Passwort-Reset"
autoFocus
/>
</div>
<div className="form-group">
<label className="form-label">Ticket-Titel</label>
<input
className="form-input"
value={tplModal.title}
onChange={e => setTplModal(p => ({ ...p, title: e.target.value }))}
placeholder="z.B. Passwort zurücksetzen"
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div className="form-group">
<label className="form-label">Kategorie</label>
<select
className="form-select"
value={tplModal.category}
onChange={e => setTplModal(p => ({ ...p, category: e.target.value }))}
>
{categories.map(c => (
<option key={c.id} value={c.name}>{c.icon} {c.name}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Priorität</label>
<select
className="form-select"
value={tplModal.priority}
onChange={e => setTplModal(p => ({ ...p, priority: e.target.value }))}
>
{PRIORITIES.map(p => (
<option key={p} value={p}>{PRIORITY_ICONS[p]} {p.charAt(0).toUpperCase() + p.slice(1)}</option>
))}
</select>
</div>
</div>
<div className="form-group">
<label className="form-label">Beschreibungs-Vorlage</label>
<textarea
className="form-input"
value={tplModal.description}
onChange={e => setTplModal(p => ({ ...p, description: e.target.value }))}
rows={6}
placeholder="Vorlage-Text... (Leerzeilen als Platzhalter für den Nutzer)"
style={{ resize: 'vertical', fontFamily: 'monospace', fontSize: '13px' }}
/>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '8px' }}>
<button style={btnOutline} onClick={() => setTplModal(null)}>Abbrechen</button>
<button style={btnPrimary} onClick={saveTemplate} disabled={tplLoading || !tplModal.label?.trim()}>
{tplLoading ? 'Speichern...' : 'Speichern'}
</button>
</div>
</div>
</div>
)}
{/* ── EMAIL TEMPLATE MODAL ──────────────────────────────────────── */}
{emailModal && (() => {
const vars = EMAIL_TEMPLATE_VARS[emailModal.type] || [];
const meta = EMAIL_TEMPLATE_LABELS[emailModal.type] || { label: emailModal.type };
return (
<div className="modal-overlay" onClick={() => setEmailModal(null)}>
<div className="modal-content modal-large" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">{meta.label} bearbeiten</h2>
<button className="modal-close" onClick={() => setEmailModal(null)}>×</button>
</div>
{/* Variable chips */}
{vars.length > 0 && (
<div style={{ marginBottom: '16px' }}>
<label className="form-label" style={{ marginBottom: '6px', display: 'block' }}>
Verfügbare Variablen <span style={{ color: 'var(--text-muted)', fontWeight: 400 }}>(klicken zum Einfügen)</span>
</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{vars.map(v => (
<button
key={v}
onClick={() => insertVar(v)}
style={{
background: 'var(--cereda-primary)15',
border: '1px solid var(--cereda-primary)40',
borderRadius: '6px',
padding: '3px 10px',
fontSize: '12px',
color: 'var(--cereda-primary)',
cursor: 'pointer',
fontFamily: 'monospace',
}}
>
{`{{${v}}}`}
</button>
))}
</div>
</div>
)}
<div className="form-group">
<label className="form-label">Betreff</label>
<input
ref={emailSubjectRef}
className="form-input"
value={emailModal.subject}
onChange={e => setEmailModal(m => ({ ...m, subject: e.target.value }))}
onFocus={() => setLastFocused('subject')}
placeholder="E-Mail Betreff"
/>
</div>
<div className="form-group">
<label className="form-label">Einleitungstext</label>
<textarea
ref={emailIntroRef}
className="form-input"
value={emailModal.intro}
onChange={e => setEmailModal(m => ({ ...m, intro: e.target.value }))}
onFocus={() => setLastFocused('intro')}
rows={5}
placeholder="Einleitungstext der E-Mail..."
style={{ resize: 'vertical', fontSize: '13px', lineHeight: 1.6 }}
/>
<p style={{ color: 'var(--text-muted)', fontSize: '12px', margin: '4px 0 0' }}>
Zeilenumbrüche werden in der E-Mail übernommen. Kein HTML nötig.
</p>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'space-between', marginTop: '8px' }}>
<button style={btnDanger} onClick={() => resetEmailTpl(emailModal.type)}>
Auf Standard zurücksetzen
</button>
<div style={{ display: 'flex', gap: '12px' }}>
<button style={btnOutline} onClick={() => setEmailModal(null)}>Abbrechen</button>
<button
style={btnPrimary}
onClick={saveEmailTpl}
disabled={emailLoading || !emailModal.subject?.trim() || !emailModal.intro?.trim()}
>
{emailLoading ? 'Speichern...' : 'Speichern'}
</button>
</div>
</div>
</div>
</div>
);
})()}
</div>
);
}
// ── Asset Types Tab ───────────────────────────────────────────────────────────
function AssetTypesTab() {
const [types, setTypes] = useState([]);
const [modal, setModal] = useState(null); // null | { id?, name, icon }
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(null);
const load = useCallback(async () => {
const data = await assetService.getTypes();
setTypes(data);
}, []);
useEffect(() => { load(); }, [load]);
const openNew = () => setModal({ name: '', icon: 'devices_other' });
const openEdit = (t) => setModal({ ...t });
const save = async () => {
if (!modal?.name?.trim()) return;
setSaving(true);
try {
if (modal.id) {
await assetService.updateType(modal.id, { name: modal.name.trim(), icon: modal.icon });
} else {
await assetService.createType({ name: modal.name.trim(), icon: modal.icon });
}
setModal(null);
await load();
} catch (e) {
alert(e.response?.data?.error || 'Fehler beim Speichern');
} finally {
setSaving(false);
}
};
const remove = async (id) => {
if (!window.confirm('Typ wirklich löschen?')) return;
setDeleting(id);
try {
await assetService.deleteType(id);
await load();
} finally {
setDeleting(null);
}
};
const card = { background: 'var(--bg-card)', borderRadius: '12px', border: '1px solid var(--border-color)', overflow: 'hidden' };
const row = { display: 'flex', alignItems: 'center', padding: '12px 16px', borderBottom: '1px solid var(--border-color)', gap: '12px' };
const btnPrimary = { background: 'var(--cereda-primary)', color: '#fff', border: 'none', borderRadius: '8px', padding: '8px 16px', cursor: 'pointer', fontSize: '13px', fontWeight: 600 };
const btnDanger = { background: 'rgba(239,68,68,0.12)', color: '#ef4444', border: '1px solid rgba(239,68,68,0.3)', borderRadius: '8px', padding: '6px 12px', cursor: 'pointer', fontSize: '12px' };
const btnOutline = { background: 'transparent', color: 'var(--cereda-primary)', border: '1px solid var(--cereda-primary)', borderRadius: '8px', padding: '6px 12px', cursor: 'pointer', fontSize: '12px' };
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '13px' }}>
Asset-Typen verwalten werden in allen Dropdowns angezeigt.
</p>
<button style={btnPrimary} onClick={openNew}>+ Neuer Typ</button>
</div>
<div style={card}>
{types.length === 0 && (
<div style={{ padding: '24px', textAlign: 'center', color: 'var(--text-muted)' }}>Keine Typen vorhanden</div>
)}
{types.map((t, i) => (
<div key={t.id} style={{ ...row, borderBottom: i < types.length - 1 ? '1px solid var(--border-color)' : 'none' }}>
<span style={{ fontSize: '20px', width: '28px', textAlign: 'center' }}>🖥</span>
<span style={{ flex: 1, fontWeight: 600, color: 'var(--text-primary)', fontSize: '14px' }}>{t.name}</span>
<button style={btnOutline} onClick={() => openEdit(t)}>Bearbeiten</button>
<button style={btnDanger} disabled={deleting === t.id} onClick={() => remove(t.id)}>
{deleting === t.id ? '...' : 'Löschen'}
</button>
</div>
))}
</div>
{modal && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div style={{ background: 'var(--bg-modal)', borderRadius: '16px', padding: '24px', width: '360px', border: '1px solid var(--border-color)' }}>
<h3 style={{ margin: '0 0 20px', color: 'var(--text-primary)' }}>{modal.id ? 'Typ bearbeiten' : 'Neuer Asset-Typ'}</h3>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', color: 'var(--text-secondary)', fontWeight: 600 }}>Name *</label>
<input
className="form-input"
value={modal.name}
onChange={e => setModal(m => ({ ...m, name: e.target.value }))}
placeholder="z.B. Notebook"
autoFocus
style={{ marginBottom: '20px' }}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
<button style={btnOutline} onClick={() => setModal(null)}>Abbrechen</button>
<button style={btnPrimary} disabled={saving || !modal.name?.trim()} onClick={save}>
{saving ? 'Speichern...' : 'Speichern'}
</button>
</div>
</div>
</div>
)}
</div>
);
}