From 427da6452add5596bb5b765389f89e9a6db67cdd Mon Sep 17 00:00:00 2001 From: Bert Hausmans Date: Tue, 6 Jan 2026 02:56:35 +0100 Subject: [PATCH] Add phone number field and WhatsApp/copy message buttons for participants --- server/database.ts | 14 +++- server/routes/admin.ts | 7 +- src/pages/QuestionnaireDetail.tsx | 109 ++++++++++++++++++++++++------ 3 files changed, 105 insertions(+), 25 deletions(-) diff --git a/server/database.ts b/server/database.ts index 548a269..2aa6b7b 100644 --- a/server/database.ts +++ b/server/database.ts @@ -46,6 +46,7 @@ export interface Participant { id: number; questionnaire_id: number; name: string; + phone: string | null; token: string; created_at: string; } @@ -141,6 +142,13 @@ export function initializeDatabase(): void { ) `); + // Migration: Add phone column to participants if it doesn't exist + try { + db.exec(`ALTER TABLE participants ADD COLUMN phone TEXT`); + } catch (e) { + // Column already exists, ignore + } + db.exec(` CREATE INDEX IF NOT EXISTS idx_participants_questionnaire ON participants(questionnaire_id); CREATE INDEX IF NOT EXISTS idx_participants_token ON participants(token); @@ -308,10 +316,10 @@ export const questionnaireOps = { // Participant operations export const participantOps = { - create: (questionnaireId: number, name: string, token: string): number => { + create: (questionnaireId: number, name: string, phone: string | null, token: string): number => { const result = db.prepare( - 'INSERT INTO participants (questionnaire_id, name, token) VALUES (?, ?, ?)' - ).run(questionnaireId, name, token); + 'INSERT INTO participants (questionnaire_id, name, phone, token) VALUES (?, ?, ?, ?)' + ).run(questionnaireId, name, phone, token); return result.lastInsertRowid as number; }, diff --git a/server/routes/admin.ts b/server/routes/admin.ts index 6398a3c..3baa552 100644 --- a/server/routes/admin.ts +++ b/server/routes/admin.ts @@ -197,7 +197,7 @@ function generateToken(): string { // Add participant to questionnaire router.post('/questionnaires/:id/participants', (req: Request, res: Response) => { - const { name } = req.body; + const { name, phone } = req.body; const questionnaire = questionnaireOps.findById(parseInt(req.params.id)); if (!questionnaire) { @@ -210,13 +210,16 @@ router.post('/questionnaires/:id/participants', (req: Request, res: Response) => return; } + // Clean phone number (remove spaces, keep + and digits) + const cleanPhone = phone?.trim() ? phone.trim().replace(/[^\d+]/g, '') : null; + // Generate unique token let token = generateToken(); while (!participantOps.isTokenAvailable(token)) { token = generateToken(); } - const id = participantOps.create(questionnaire.id, name.trim(), token); + const id = participantOps.create(questionnaire.id, name.trim(), cleanPhone, token); const participant = participantOps.findById(id); res.json({ success: true, participant }); diff --git a/src/pages/QuestionnaireDetail.tsx b/src/pages/QuestionnaireDetail.tsx index 56f91c3..8f2269f 100644 --- a/src/pages/QuestionnaireDetail.tsx +++ b/src/pages/QuestionnaireDetail.tsx @@ -15,6 +15,7 @@ interface Activity { interface Participant { id: number name: string + phone: string | null token: string created_at: string } @@ -38,6 +39,7 @@ export function QuestionnaireDetail() { const [loading, setLoading] = useState(true) const [copied, setCopied] = useState('') const [newParticipantName, setNewParticipantName] = useState('') + const [newParticipantPhone, setNewParticipantPhone] = useState('') const [addingParticipant, setAddingParticipant] = useState(false) // Edit activity modal state @@ -89,11 +91,15 @@ export function QuestionnaireDetail() { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', - body: JSON.stringify({ name: newParticipantName.trim() }), + body: JSON.stringify({ + name: newParticipantName.trim(), + phone: newParticipantPhone.trim() || null + }), }) const data = await res.json() if (data.success) { setNewParticipantName('') + setNewParticipantPhone('') fetchParticipants() } } catch (error) { @@ -103,6 +109,36 @@ export function QuestionnaireDetail() { } } + // Get first name from full name + function getFirstName(fullName: string): string { + const parts = fullName.trim().split(/\s+/) + return parts[0] || fullName + } + + // Generate invitation message for a participant + function getInvitationMessage(participant: Participant, url: string): string { + const firstName = getFirstName(participant.name) + const title = questionnaire?.title || 'de vragenlijst' + return `Hoi ${firstName}, we zijn benieuwd naar je ideeën voor "${title}". Je kunt nieuwe ideeën aandragen en/of stemmen op reeds toegevoegde ideeën via deze link: ${url}. Alvast bedankt voor je hulp!` + } + + // Copy invitation message to clipboard + function copyInvitation(participant: Participant, url: string) { + const message = getInvitationMessage(participant, url) + navigator.clipboard.writeText(message) + setCopied(`msg-${participant.id}`) + setTimeout(() => setCopied(''), 2000) + } + + // Generate WhatsApp link + function getWhatsAppLink(participant: Participant, url: string): string { + const message = getInvitationMessage(participant, url) + const encodedMessage = encodeURIComponent(message) + // Remove leading + from phone number for wa.me link + const phone = participant.phone?.replace(/^\+/, '') || '' + return `https://wa.me/${phone}?text=${encodedMessage}` + } + async function handleDeleteParticipant(participantId: number) { if (!confirm('Deze deelnemer verwijderen?')) return @@ -256,15 +292,22 @@ export function QuestionnaireDetail() {

{/* Add Participant Form */} -
+ setNewParticipantName(e.target.value)} placeholder="Naam deelnemer" - className="flex-1 px-3 py-2 bg-bg-input border border-border rounded-lg text-sm text-text placeholder-text-faint focus:outline-none focus:border-accent" + className="flex-1 min-w-[150px] px-3 py-2 bg-bg-input border border-border rounded-lg text-sm text-text placeholder-text-faint focus:outline-none focus:border-accent" required /> + setNewParticipantPhone(e.target.value)} + placeholder="Telefoon (optioneel, bijv. +31612345678)" + className="flex-1 min-w-[200px] px-3 py-2 bg-bg-input border border-border rounded-lg text-sm text-text placeholder-text-faint focus:outline-none focus:border-accent" + /> + + + {/* Action buttons */} +
+ + + {participant.phone && ( + + 💬 WhatsApp + + )}
- - ) })}