Add: Lizenzpreise, Rollen-Picker, Audit-Log Fix, Ticket Benutzerfilter
This commit is contained in:
@@ -29,6 +29,14 @@ const COMMON_ICONS = ['📋', '💻', '🖥️', '🌐', '📊', '🖨️', '
|
||||
export default function SettingsPage() {
|
||||
const [tab, setTab] = useState('categories');
|
||||
|
||||
// ── License prices state ──────────────────────────────────────────────────
|
||||
const [licPrices, setLicPrices] = useState([]);
|
||||
const [licLoading, setLicLoading] = useState(false);
|
||||
const [licEditId, setLicEditId] = useState(null);
|
||||
const [licEditRow, setLicEditRow] = useState({});
|
||||
const [licNewForm, setLicNewForm] = useState(null); // null | { sku_part_number, display_name, price_per_month }
|
||||
const [licSaving, setLicSaving] = useState(false);
|
||||
|
||||
// ── Categories state ──────────────────────────────────────────────────────
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [catModal, setCatModal] = useState(null); // null | { id?, name, icon }
|
||||
@@ -78,8 +86,24 @@ export default function SettingsPage() {
|
||||
setDesignDraft(res.data);
|
||||
}, []);
|
||||
|
||||
const loadLicPrices = useCallback(async () => {
|
||||
setLicLoading(true);
|
||||
try {
|
||||
const apiUrl = process.env.REACT_APP_API_URL || '/api';
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch(`${apiUrl}/license-prices`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const json = await res.json();
|
||||
setLicPrices(json.data || []);
|
||||
} catch (_) {}
|
||||
setLicLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadCategories(); loadTemplates(); loadEmailTpls(); loadEmailDesign(); }, [loadCategories, loadTemplates, loadEmailTpls, loadEmailDesign]);
|
||||
|
||||
useEffect(() => { if (tab === 'license-prices') loadLicPrices(); }, [tab, loadLicPrices]);
|
||||
|
||||
// Fetch preview when designDraft or previewType changes
|
||||
useEffect(() => {
|
||||
if (!designDraft) return;
|
||||
@@ -184,6 +208,49 @@ export default function SettingsPage() {
|
||||
|
||||
const setDraft = (key, val) => setDesignDraft(d => ({ ...d, [key]: val }));
|
||||
|
||||
// ── License price CRUD ────────────────────────────────────────────────────
|
||||
const licAuthFetch = (url, opts = {}) => {
|
||||
const token = localStorage.getItem('token');
|
||||
const apiUrl = process.env.REACT_APP_API_URL || '/api';
|
||||
return fetch(`${apiUrl}${url}`, {
|
||||
...opts,
|
||||
headers: { ...(opts.headers || {}), Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
});
|
||||
};
|
||||
|
||||
const saveLicEdit = async () => {
|
||||
setLicSaving(true);
|
||||
try {
|
||||
await licAuthFetch(`/license-prices/${licEditId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(licEditRow),
|
||||
});
|
||||
setLicEditId(null);
|
||||
await loadLicPrices();
|
||||
} catch (_) { alert('Fehler beim Speichern'); }
|
||||
setLicSaving(false);
|
||||
};
|
||||
|
||||
const saveLicNew = async () => {
|
||||
if (!licNewForm?.sku_part_number?.trim() || !licNewForm?.display_name?.trim()) return;
|
||||
setLicSaving(true);
|
||||
try {
|
||||
await licAuthFetch('/license-prices', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(licNewForm),
|
||||
});
|
||||
setLicNewForm(null);
|
||||
await loadLicPrices();
|
||||
} catch (_) { alert('Fehler beim Erstellen'); }
|
||||
setLicSaving(false);
|
||||
};
|
||||
|
||||
const deleteLicPrice = async (id, name) => {
|
||||
if (!window.confirm(`Lizenzpreis „${name}" wirklich löschen?`)) return;
|
||||
await licAuthFetch(`/license-prices/${id}`, { method: 'DELETE' });
|
||||
await loadLicPrices();
|
||||
};
|
||||
|
||||
// ── Email template CRUD ───────────────────────────────────────────────────
|
||||
|
||||
const saveEmailTpl = async () => {
|
||||
@@ -313,6 +380,7 @@ export default function SettingsPage() {
|
||||
{ key: 'email-templates', label: '📧 E-Mail-Vorlagen' },
|
||||
{ key: 'email-design', label: '🎨 E-Mail-Design' },
|
||||
{ key: 'cronjobs', label: '⏰ Cronjobs' },
|
||||
{ key: 'license-prices', label: '💰 Lizenzpreise' },
|
||||
].map(t => (
|
||||
<button key={t.key} onClick={() => setTab(t.key)} style={{
|
||||
background: 'none',
|
||||
@@ -435,6 +503,148 @@ export default function SettingsPage() {
|
||||
{/* ── CRONJOBS TAB ──────────────────────────────────────────────── */}
|
||||
{tab === 'cronjobs' && <CronJobsTab />}
|
||||
|
||||
{/* ── LICENSE PRICES TAB ───────────────────────────────────────── */}
|
||||
{tab === 'license-prices' && (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '13px' }}>
|
||||
{licPrices.length} Einträge · Preise werden in der Benutzerverwaltung bei Lizenzen angezeigt
|
||||
</p>
|
||||
<button style={btnPrimary} onClick={() => setLicNewForm({ sku_part_number: '', display_name: '', price_per_month: '' })}>
|
||||
+ Neue Lizenz
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Neues Lizenz-Formular */}
|
||||
{licNewForm && (
|
||||
<div style={{ ...card, background: 'var(--bg-secondary)', marginBottom: '16px', border: '1px solid var(--cereda-primary)' }}>
|
||||
<div style={{ fontWeight: 700, fontSize: '14px', color: 'var(--text-primary)', marginBottom: '12px' }}>➕ Neue Lizenz</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr 120px auto', gap: '10px', alignItems: 'end' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '12px', color: 'var(--text-muted)', display: 'block', marginBottom: '4px' }}>SKU Part Number*</label>
|
||||
<input
|
||||
className="form-input"
|
||||
value={licNewForm.sku_part_number}
|
||||
onChange={e => setLicNewForm(f => ({ ...f, sku_part_number: e.target.value }))}
|
||||
placeholder="z.B. SPE_E3"
|
||||
style={{ fontSize: '13px' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '12px', color: 'var(--text-muted)', display: 'block', marginBottom: '4px' }}>Anzeigename*</label>
|
||||
<input
|
||||
className="form-input"
|
||||
value={licNewForm.display_name}
|
||||
onChange={e => setLicNewForm(f => ({ ...f, display_name: e.target.value }))}
|
||||
placeholder="z.B. Microsoft 365 E3"
|
||||
style={{ fontSize: '13px' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '12px', color: 'var(--text-muted)', display: 'block', marginBottom: '4px' }}>€/Monat</label>
|
||||
<input
|
||||
className="form-input"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={licNewForm.price_per_month}
|
||||
onChange={e => setLicNewForm(f => ({ ...f, price_per_month: e.target.value }))}
|
||||
placeholder="0.00"
|
||||
style={{ fontSize: '13px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button style={btnPrimary} onClick={saveLicNew} disabled={licSaving || !licNewForm.sku_part_number?.trim() || !licNewForm.display_name?.trim()}>
|
||||
{licSaving ? '…' : 'Speichern'}
|
||||
</button>
|
||||
<button style={btnOutline} onClick={() => setLicNewForm(null)}>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={card}>
|
||||
{licLoading ? (
|
||||
<p style={{ color: 'var(--text-muted)', textAlign: 'center', margin: '32px 0' }}>Lädt…</p>
|
||||
) : licPrices.length === 0 ? (
|
||||
<p style={{ color: 'var(--text-muted)', textAlign: 'center', margin: '32px 0' }}>Keine Lizenzpreise vorhanden</p>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid var(--border-color)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '8px 10px', color: 'var(--text-muted)', fontWeight: 600, fontSize: '12px' }}>SKU</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px 10px', color: 'var(--text-muted)', fontWeight: 600, fontSize: '12px' }}>Name</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px 10px', color: 'var(--text-muted)', fontWeight: 600, fontSize: '12px' }}>Preis/Monat</th>
|
||||
<th style={{ padding: '8px 10px' }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{licPrices.map(p => (
|
||||
<tr key={p.id} style={{ borderBottom: '1px solid var(--border-color)' }}>
|
||||
{licEditId === p.id ? (
|
||||
<>
|
||||
<td style={{ padding: '6px 10px' }}>
|
||||
<input
|
||||
className="form-input"
|
||||
value={licEditRow.sku_part_number ?? p.sku_part_number}
|
||||
onChange={e => setLicEditRow(r => ({ ...r, sku_part_number: e.target.value }))}
|
||||
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||||
/>
|
||||
</td>
|
||||
<td style={{ padding: '6px 10px' }}>
|
||||
<input
|
||||
className="form-input"
|
||||
value={licEditRow.display_name ?? p.display_name}
|
||||
onChange={e => setLicEditRow(r => ({ ...r, display_name: e.target.value }))}
|
||||
style={{ fontSize: '12px', padding: '4px 8px', width: '100%' }}
|
||||
/>
|
||||
</td>
|
||||
<td style={{ padding: '6px 10px' }}>
|
||||
<input
|
||||
className="form-input"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={licEditRow.price_per_month ?? p.price_per_month}
|
||||
onChange={e => setLicEditRow(r => ({ ...r, price_per_month: e.target.value }))}
|
||||
style={{ fontSize: '12px', padding: '4px 8px', textAlign: 'right' }}
|
||||
/>
|
||||
</td>
|
||||
<td style={{ padding: '6px 10px', textAlign: 'right', whiteSpace: 'nowrap' }}>
|
||||
<button style={{ ...btnPrimary, padding: '4px 10px', fontSize: '12px', marginRight: '6px' }} onClick={saveLicEdit} disabled={licSaving}>
|
||||
{licSaving ? '…' : 'Speichern'}
|
||||
</button>
|
||||
<button style={{ ...btnOutline, padding: '4px 10px', fontSize: '12px' }} onClick={() => setLicEditId(null)}>Abbrechen</button>
|
||||
</td>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<td style={{ padding: '8px 10px', color: 'var(--text-muted)', fontFamily: 'monospace', fontSize: '12px' }}>{p.sku_part_number}</td>
|
||||
<td style={{ padding: '8px 10px', color: 'var(--text-primary)', fontWeight: 500 }}>{p.display_name}</td>
|
||||
<td style={{ padding: '8px 10px', textAlign: 'right', color: p.price_per_month > 0 ? 'var(--success, #10b981)' : 'var(--text-muted)', fontWeight: 600 }}>
|
||||
{p.price_per_month > 0
|
||||
? new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(p.price_per_month)
|
||||
: 'Kostenlos'}
|
||||
</td>
|
||||
<td style={{ padding: '8px 10px', textAlign: 'right', whiteSpace: 'nowrap' }}>
|
||||
<button style={{ ...btnOutline, padding: '4px 10px', fontSize: '12px', marginRight: '6px' }} onClick={() => { setLicEditId(p.id); setLicEditRow({ display_name: p.display_name, price_per_month: p.price_per_month, sku_part_number: p.sku_part_number }); }}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button style={{ ...btnDanger, padding: '4px 10px', fontSize: '12px' }} onClick={() => deleteLicPrice(p.id, p.display_name)}>
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>{/* end constrained tab content */}
|
||||
|
||||
{/* ── EMAIL DESIGN TAB – full width ─────────────────────────────── */}
|
||||
|
||||
Reference in New Issue
Block a user