task(E6-frontend): guest panel with ribbon entry and api client
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* GuestPanel (Task E6) — Gäste-Verwaltung für Event-Bestuhlung.
|
||||
* Liste + Suche + Hinzufügen + Löschen. Zuweisung: seat_element_id
|
||||
* wird per API gesetzt (Klick auf Stuhl im Canvas folgt in E6b).
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { listGuests, createGuest, deleteGuest, type Guest } from '../services/api';
|
||||
|
||||
interface GuestPanelProps {
|
||||
token: string;
|
||||
drawingId: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const GuestPanel: React.FC<GuestPanelProps> = ({ token, drawingId, onClose }) => {
|
||||
const [guests, setGuests] = useState<Guest[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newEmail, setNewEmail] = useState('');
|
||||
const [newCategory, setNewCategory] = useState('standard');
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setGuests(await listGuests(token, drawingId));
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Laden');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token, drawingId]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (newName.trim() === '') return;
|
||||
try {
|
||||
await createGuest(token, drawingId, { name: newName.trim(), email: newEmail.trim() || undefined, category: newCategory });
|
||||
setNewName('');
|
||||
setNewEmail('');
|
||||
setNewCategory('standard');
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Hinzufügen');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteGuest(token, id);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Löschen');
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = guests.filter((g) =>
|
||||
g.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(g.email ?? '').toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
const assigned = guests.filter((g) => g.seat_element_id).length;
|
||||
|
||||
return (
|
||||
<div className="guest-panel" style={{ padding: 8, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<strong>Gäste ({guests.length})</strong>
|
||||
<span style={{ fontSize: 12, opacity: 0.7 }}>{assigned} zugewiesen</span>
|
||||
{onClose && <button onClick={onClose} aria-label="Schließen">×</button>}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Suchen…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{ padding: '4px 8px' }}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input
|
||||
placeholder="Name"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') void handleAdd(); }}
|
||||
style={{ flex: 1, padding: '4px 8px' }}
|
||||
/>
|
||||
<input
|
||||
placeholder="E-Mail (optional)"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
style={{ flex: 1, padding: '4px 8px' }}
|
||||
/>
|
||||
<select value={newCategory} onChange={(e) => setNewCategory(e.target.value)} style={{ width: 90 }}>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="vip">VIP</option>
|
||||
<option value="staff">Staff</option>
|
||||
</select>
|
||||
<button onClick={() => void handleAdd()} disabled={newName.trim() === ''}>+</button>
|
||||
</div>
|
||||
|
||||
{error && <div style={{ color: '#e5484d', fontSize: 12 }}>{error}</div>}
|
||||
{loading && <div style={{ opacity: 0.6 }}>Lädt…</div>}
|
||||
|
||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, overflowY: 'auto', flex: 1 }}>
|
||||
{filtered.map((g) => (
|
||||
<li
|
||||
key={g.id}
|
||||
style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
padding: '6px 8px', borderBottom: '1px solid rgba(128,128,128,0.2)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: g.category === 'vip' ? 700 : 400 }}>
|
||||
{g.category === 'vip' ? '★ ' : ''}{g.name}
|
||||
</div>
|
||||
{g.email && <div style={{ fontSize: 11, opacity: 0.6 }}>{g.email}</div>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
{g.seat_element_id && (
|
||||
<span style={{ fontSize: 11, opacity: 0.7 }} title="Sitzplatz zugewiesen">💺</span>
|
||||
)}
|
||||
<button onClick={() => void handleDelete(g.id)} aria-label={`Lösche ${g.name}`}>🗑</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{!loading && filtered.length === 0 && (
|
||||
<li style={{ opacity: 0.5, padding: 8 }}>Keine Gäste gefunden.</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuestPanel;
|
||||
@@ -324,6 +324,10 @@ const RibbonBar: React.FC<RibbonBarProps> = ({
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><path d="M12 7v5l4 2"/></svg>
|
||||
<span>Historie</span>
|
||||
</button>
|
||||
<button className="ribbon-btn" title="Gäste-Verwaltung" onClick={() => onAction('guests')}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<span>Gäste</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="ribbon-group-label">Extras</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user