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);
|
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 => {
|
isTokenAvailable: (token: string): boolean => {
|
||||||
const result = db.prepare('SELECT id FROM participants WHERE token = ?').get(token);
|
const result = db.prepare('SELECT id FROM participants WHERE token = ?').get(token);
|
||||||
return !result;
|
return !result;
|
||||||
|
|||||||
@@ -225,6 +225,30 @@ router.post('/questionnaires/:id/participants', (req: Request, res: Response) =>
|
|||||||
res.json({ success: true, participant });
|
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
|
// Delete participant
|
||||||
router.delete('/participants/:id', (req: Request, res: Response) => {
|
router.delete('/participants/:id', (req: Request, res: Response) => {
|
||||||
participantOps.delete(parseInt(req.params.id));
|
participantOps.delete(parseInt(req.params.id));
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ export function QuestionnaireDetail() {
|
|||||||
const [editDescriptionInput, setEditDescriptionInput] = useState('')
|
const [editDescriptionInput, setEditDescriptionInput] = useState('')
|
||||||
const [editLoading, setEditLoading] = useState(false)
|
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(() => {
|
useEffect(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
}, [id])
|
}, [id])
|
||||||
@@ -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() {
|
async function handleDelete() {
|
||||||
if (!confirm('Weet je zeker dat je deze vragenlijst wilt verwijderen? Dit verwijdert ook alle activiteiten en stemmen.')) {
|
if (!confirm('Weet je zeker dat je deze vragenlijst wilt verwijderen? Dit verwijdert ook alle activiteiten en stemmen.')) {
|
||||||
return
|
return
|
||||||
@@ -332,6 +377,13 @@ export function QuestionnaireDetail() {
|
|||||||
)}
|
)}
|
||||||
<div className="text-xs font-mono text-text-faint truncate mt-1">{participantUrl}</div>
|
<div className="text-xs font-mono text-text-faint truncate mt-1">{participantUrl}</div>
|
||||||
</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
|
<button
|
||||||
onClick={() => handleDeleteParticipant(participant.id)}
|
onClick={() => handleDeleteParticipant(participant.id)}
|
||||||
className="px-2 py-1 text-xs font-medium text-danger hover:bg-danger-muted rounded transition-colors shrink-0"
|
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>
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user