Initial commit

Next.js + Express event management app for Hope Family Church.
This commit is contained in:
2026-07-23 15:26:47 +02:00
commit 3d381944d2
246 changed files with 57565 additions and 0 deletions
@@ -0,0 +1,109 @@
const { listJobs, getJob, updateJob, deleteJob } = require('../utils/scheduledEmails');
// Normalize job for client UI
function toClient(job) {
const kind = job.broadcast ? 'broadcast' : (job.eventId ? 'attendees' : 'unknown');
const subject = job?.payload?.subject || '';
const html = job?.payload?.html || '';
const text = job?.payload?.text || '';
return {
id: job.id,
kind,
eventId: job.eventId || null,
broadcast: !!job.broadcast,
scheduledAt: job.scheduledAt,
createdAt: job.createdAt,
status: job.status,
attempts: job.attempts || 0,
sentAt: job.sentAt || null,
lastError: job.lastError || null,
subject,
hasHtml: !!html,
hasText: !!text,
};
}
// GET /api/scheduled-emails
// Returns jobs excluding emails sent more than a week ago
const listScheduledEmails = async (req, res) => {
try {
const raw = listJobs();
const now = new Date();
const weekMs = 7 * 24 * 60 * 60 * 1000;
const filtered = raw.filter(j => {
if (j.status === 'sent' && j.sentAt) {
const sentAt = new Date(j.sentAt).getTime();
return (now.getTime() - sentAt) <= weekMs;
}
// Include queued, sending, error by default
return true;
})
// Provide most-relevant first: queued -> sending -> error -> recent sent
.sort((a, b) => {
const order = { queued: 0, sending: 1, error: 2, sent: 3 };
const oa = order[a.status] ?? 99;
const ob = order[b.status] ?? 99;
if (oa !== ob) return oa - ob;
// Then by scheduledAt asc
return new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime();
})
.map(toClient);
return res.json({ jobs: filtered });
} catch (e) {
return res.status(400).json({ message: e?.message || 'Failed to list scheduled emails' });
}
};
// PATCH /api/scheduled-emails/:id
// Allows editing scheduledAt, subject, html/text on queued jobs only
const updateScheduledEmail = async (req, res) => {
try {
const { id } = req.params;
const job = getJob(id);
if (!job) return res.status(404).json({ message: 'Job not found' });
if (job.status !== 'queued') return res.status(400).json({ message: 'Only queued jobs can be edited' });
const { scheduledAt, subject, html, text } = req.body || {};
const patch = {};
if (scheduledAt) {
const when = new Date(scheduledAt);
if (isNaN(when.getTime())) return res.status(400).json({ message: 'scheduledAt must be a valid ISO date-time' });
patch.scheduledAt = when.toISOString();
}
if (subject != null || html != null || text != null) {
const payload = { ...(job.payload || {}) };
if (subject != null) payload.subject = subject;
if (html != null || text != null) {
// If html provided explicitly, set html; if text provided, set text
if (html != null) payload.html = html;
if (text != null) payload.text = text;
}
patch.payload = payload;
}
const updated = updateJob(id, patch);
return res.json({ message: 'Updated', job: toClient(updated) });
} catch (e) {
return res.status(400).json({ message: e?.message || 'Failed to update job' });
}
};
// DELETE /api/scheduled-emails/:id
// Only queued jobs can be removed
const deleteScheduledEmail = async (req, res) => {
try {
const { id } = req.params;
const job = getJob(id);
if (!job) return res.status(404).json({ message: 'Job not found' });
if (job.status !== 'queued') return res.status(400).json({ message: 'Only queued jobs can be deleted' });
const ok = deleteJob(id);
if (!ok) return res.status(404).json({ message: 'Job not found' });
return res.json({ message: 'Deleted' });
} catch (e) {
return res.status(400).json({ message: e?.message || 'Failed to delete job' });
}
};
module.exports = { listScheduledEmails, updateScheduledEmail, deleteScheduledEmail };