secutorpro commited on
Commit
17d6f8a
·
verified ·
1 Parent(s): e8845c2

member management: list + add/edit/delete

Browse files
Files changed (1) hide show
  1. web/src/components/MembersPanel.jsx +107 -0
web/src/components/MembersPanel.jsx ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { api } from '../api.js';
3
+
4
+ const EMPTY = { name: '', callsign: '', device_id: '', group_id: '' };
5
+
6
+ export default function MembersPanel({ members, groups }) {
7
+ const [form, setForm] = useState(EMPTY);
8
+ const [editingId, setEditingId] = useState(null);
9
+ const [error, setError] = useState('');
10
+
11
+ const save = async (e) => {
12
+ e.preventDefault();
13
+ setError('');
14
+ const body = { ...form, group_id: form.group_id ? Number(form.group_id) : null };
15
+ try {
16
+ if (editingId) await api.put(`/members/${editingId}`, body);
17
+ else await api.post('/members', body);
18
+ setForm(EMPTY);
19
+ setEditingId(null);
20
+ } catch (err) {
21
+ setError(err.message);
22
+ }
23
+ };
24
+
25
+ const edit = (m) => {
26
+ setEditingId(m.id);
27
+ setForm({ name: m.name, callsign: m.callsign || '', device_id: m.device_id || '', group_id: m.group_id || '' });
28
+ };
29
+
30
+ const remove = async (m) => {
31
+ if (!confirm(`Remove ${m.name}?`)) return;
32
+ try {
33
+ await api.del(`/members/${m.id}`);
34
+ } catch (err) {
35
+ setError(err.message);
36
+ }
37
+ };
38
+
39
+ return (
40
+ <section className="panel">
41
+ <h2>Members ({members.length})</h2>
42
+ {error && <p className="error">{error}</p>}
43
+ <table>
44
+ <thead>
45
+ <tr><th></th><th>Name</th><th>Group</th><th>Device</th><th>Batt</th><th></th></tr>
46
+ </thead>
47
+ <tbody>
48
+ {members.map((m) => (
49
+ <tr key={m.id}>
50
+ <td><span className={`dot ${m.status}`} /></td>
51
+ <td>
52
+ {m.name}
53
+ {m.callsign && <><br /><span className="muted small">{m.callsign}</span></>}
54
+ </td>
55
+ <td><span className="chip-label" style={{ '--c': m.group_color || '#64748b' }}>{m.group_name || '—'}</span></td>
56
+ <td className="small muted">{m.device_id}</td>
57
+ <td className="small">{m.battery != null ? `${m.battery}%` : '—'}</td>
58
+ <td>
59
+ <button className="secondary small-btn" onClick={() => edit(m)}>edit</button>{' '}
60
+ <button className="secondary small-btn" onClick={() => remove(m)}>✕</button>
61
+ </td>
62
+ </tr>
63
+ ))}
64
+ </tbody>
65
+ </table>
66
+
67
+ <h3>{editingId ? `Edit member #${editingId}` : 'Add member'}</h3>
68
+ <form onSubmit={save}>
69
+ <input
70
+ required
71
+ placeholder="Name"
72
+ value={form.name}
73
+ onChange={(e) => setForm({ ...form, name: e.target.value })}
74
+ />
75
+ <input
76
+ placeholder="Callsign"
77
+ value={form.callsign}
78
+ onChange={(e) => setForm({ ...form, callsign: e.target.value })}
79
+ />
80
+ <input
81
+ required
82
+ placeholder="Device ID (unique)"
83
+ value={form.device_id}
84
+ onChange={(e) => setForm({ ...form, device_id: e.target.value })}
85
+ />
86
+ <select value={form.group_id} onChange={(e) => setForm({ ...form, group_id: e.target.value })}>
87
+ <option value="">No group</option>
88
+ {groups.map((g) => (
89
+ <option key={g.id} value={g.id}>{g.name}</option>
90
+ ))}
91
+ </select>
92
+ <div className="row">
93
+ <button type="submit">{editingId ? 'Save' : 'Add'}</button>
94
+ {editingId && (
95
+ <button
96
+ type="button"
97
+ className="secondary"
98
+ onClick={() => { setEditingId(null); setForm(EMPTY); }}
99
+ >
100
+ Cancel
101
+ </button>
102
+ )}
103
+ </div>
104
+ </form>
105
+ </section>
106
+ );
107
+ }