Add ability to edit participant name and phone number
This commit is contained in:
@@ -339,6 +339,10 @@ export const participantOps = {
|
||||
db.prepare('DELETE FROM participants WHERE id = ?').run(id);
|
||||
},
|
||||
|
||||
update: (id: number, name: string, phone: string | null): void => {
|
||||
db.prepare('UPDATE participants SET name = ?, phone = ? WHERE id = ?').run(name, phone, id);
|
||||
},
|
||||
|
||||
isTokenAvailable: (token: string): boolean => {
|
||||
const result = db.prepare('SELECT id FROM participants WHERE token = ?').get(token);
|
||||
return !result;
|
||||
|
||||
@@ -225,6 +225,30 @@ router.post('/questionnaires/:id/participants', (req: Request, res: Response) =>
|
||||
res.json({ success: true, participant });
|
||||
});
|
||||
|
||||
// Update participant
|
||||
router.put('/participants/:id', (req: Request, res: Response) => {
|
||||
const { name, phone } = req.body;
|
||||
const participant = participantOps.findById(parseInt(req.params.id));
|
||||
|
||||
if (!participant) {
|
||||
res.status(404).json({ error: 'Deelnemer niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name?.trim()) {
|
||||
res.status(400).json({ error: 'Naam is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean phone number (remove spaces, keep + and digits) or set to null if empty
|
||||
const cleanPhone = phone?.trim() ? phone.trim().replace(/[^\d+]/g, '') : null;
|
||||
|
||||
participantOps.update(participant.id, name.trim(), cleanPhone);
|
||||
const updated = participantOps.findById(participant.id);
|
||||
|
||||
res.json({ success: true, participant: updated });
|
||||
});
|
||||
|
||||
// Delete participant
|
||||
router.delete('/participants/:id', (req: Request, res: Response) => {
|
||||
participantOps.delete(parseInt(req.params.id));
|
||||
|
||||
@@ -48,6 +48,13 @@ export function QuestionnaireDetail() {
|
||||
const [editNameInput, setEditNameInput] = useState('')
|
||||
const [editDescriptionInput, setEditDescriptionInput] = useState('')
|
||||
const [editLoading, setEditLoading] = useState(false)
|
||||
|
||||
// Edit participant modal state
|
||||
const [showEditParticipantModal, setShowEditParticipantModal] = useState(false)
|
||||
const [editingParticipant, setEditingParticipant] = useState<Participant | null>(null)
|
||||
const [editParticipantName, setEditParticipantName] = useState('')
|
||||
const [editParticipantPhone, setEditParticipantPhone] = useState('')
|
||||
const [editParticipantLoading, setEditParticipantLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
@@ -153,6 +160,44 @@ export function QuestionnaireDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
function openEditParticipantModal(participant: Participant) {
|
||||
setEditingParticipant(participant)
|
||||
setEditParticipantName(participant.name)
|
||||
setEditParticipantPhone(participant.phone || '')
|
||||
setShowEditParticipantModal(true)
|
||||
}
|
||||
|
||||
async function handleEditParticipant(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!editingParticipant || !editParticipantName.trim()) return
|
||||
|
||||
setEditParticipantLoading(true)
|
||||
try {
|
||||
const res = await fetch(`/api/admin/participants/${editingParticipant.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
name: editParticipantName.trim(),
|
||||
phone: editParticipantPhone.trim() || null
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.success && data.participant) {
|
||||
setShowEditParticipantModal(false)
|
||||
setEditingParticipant(null)
|
||||
// Update the participant in the list
|
||||
setParticipants(participants.map(p =>
|
||||
p.id === editingParticipant.id ? data.participant : p
|
||||
))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update participant:', error)
|
||||
} finally {
|
||||
setEditParticipantLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('Weet je zeker dat je deze vragenlijst wilt verwijderen? Dit verwijdert ook alle activiteiten en stemmen.')) {
|
||||
return
|
||||
@@ -332,6 +377,13 @@ export function QuestionnaireDetail() {
|
||||
)}
|
||||
<div className="text-xs font-mono text-text-faint truncate mt-1">{participantUrl}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => openEditParticipantModal(participant)}
|
||||
className="px-2 py-1 text-xs font-medium text-text-muted hover:text-accent hover:bg-bg-card rounded transition-colors shrink-0"
|
||||
title="Bewerken"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteParticipant(participant.id)}
|
||||
className="px-2 py-1 text-xs font-medium text-danger hover:bg-danger-muted rounded transition-colors shrink-0"
|
||||
@@ -516,6 +568,80 @@ export function QuestionnaireDetail() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Participant Modal */}
|
||||
{showEditParticipantModal && editingParticipant && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||
onClick={() => setShowEditParticipantModal(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-bg-card border border-border rounded-2xl w-full max-w-md"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<h3 className="font-semibold text-text">Deelnemer Bewerken</h3>
|
||||
<button
|
||||
onClick={() => setShowEditParticipantModal(false)}
|
||||
className="text-2xl text-text-muted hover:text-text leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleEditParticipant} className="p-4 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">
|
||||
Naam <span className="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editParticipantName}
|
||||
onChange={(e) => setEditParticipantName(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-bg-input border border-border rounded-lg text-text placeholder-text-faint focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-colors"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">
|
||||
Telefoonnummer
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={editParticipantPhone}
|
||||
onChange={(e) => setEditParticipantPhone(e.target.value)}
|
||||
placeholder="+31612345678 (optioneel, leeg laten om te verwijderen)"
|
||||
className="w-full px-4 py-2 bg-bg-input border border-border rounded-lg text-text placeholder-text-faint focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-colors"
|
||||
/>
|
||||
<p className="text-xs text-text-faint mt-1">
|
||||
Laat leeg om het telefoonnummer te verwijderen
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-end pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEditParticipantModal(false)}
|
||||
className="px-4 py-2 border border-border rounded-lg text-text-muted hover:text-text hover:border-text-muted transition-colors"
|
||||
>
|
||||
Annuleren
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={editParticipantLoading}
|
||||
className="px-4 py-2 bg-accent hover:bg-accent-hover text-white font-semibold rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{editParticipantLoading ? 'Opslaan...' : 'Opslaan'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user