const express = require('express'); const { query, queryOne, queryResult, exec } = require('../models/db'); const { authMiddleware, teamManagerMiddleware } = require('../middleware/auth'); const multer = require('multer'); const { parse: csvParse } = require('csv-parse/sync'); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 2 * 1024 * 1024 } }); const R = (schema, type, id) => `${schema}:${type}:${id}`; module.exports = function(io) { const router = express.Router(); // ── Event notification helper ───────────────────────────────────────────────── // Posts a plain system message to each assigned user group's DM channel // when an event is created or updated. async function postEventNotification(schema, eventId, actorId, isUpdate) { try { const event = await queryOne(schema, 'SELECT * FROM events WHERE id=$1', [eventId]); if (!event) return; const dateStr = new Date(event.start_at).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', }); const verb = isUpdate ? 'updated' : 'added'; const content = `📅 Event ${verb}: "${event.title}" on ${dateStr}`; const groups = await query(schema, ` SELECT ug.dm_group_id FROM event_user_groups eug JOIN user_groups ug ON ug.id = eug.user_group_id WHERE eug.event_id = $1 AND ug.dm_group_id IS NOT NULL `, [eventId]); for (const { dm_group_id } of groups) { const r = await queryResult(schema, "INSERT INTO messages (group_id,user_id,content,type) VALUES ($1,$2,$3,'system') RETURNING id", [dm_group_id, actorId, content] ); const msg = await queryOne(schema, ` SELECT m.*, u.name AS user_name, u.display_name AS user_display_name, u.avatar AS user_avatar, u.role AS user_role, u.status AS user_status, u.hide_admin_tag AS user_hide_admin_tag, u.about_me AS user_about_me, u.allow_dm AS user_allow_dm FROM messages m JOIN users u ON m.user_id = u.id WHERE m.id = $1 `, [r.rows[0].id]); if (msg) { msg.reactions = []; io.to(R(schema, 'group', dm_group_id)).emit('message:new', msg); } } } catch (e) { console.error('[Schedule] postEventNotification error:', e.message); } } // ── Helpers ─────────────────────────────────────────────────────────────────── async function isToolManagerFn(schema, user) { if (user.role === 'admin' || user.role === 'manager') return true; const tm = await queryOne(schema, "SELECT value FROM settings WHERE key='team_tool_managers'"); const gm = await queryOne(schema, "SELECT value FROM settings WHERE key='team_group_managers'"); const groupIds = [...new Set([...JSON.parse(tm?.value||'[]'), ...JSON.parse(gm?.value||'[]')])]; if (!groupIds.length) return false; const ph = groupIds.map((_,i) => `$${i+2}`).join(','); return !!(await queryOne(schema, `SELECT 1 FROM user_group_members WHERE user_id=$1 AND user_group_id IN (${ph})`, [user.id, ...groupIds])); } async function canViewEvent(schema, event, userId, isToolManager) { if (isToolManager || event.is_public) return true; const assigned = await queryOne(schema, ` SELECT 1 FROM event_user_groups eug JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id WHERE eug.event_id=$1 AND ugm.user_id=$2 `, [event.id, userId]); return !!assigned; } async function enrichEvent(schema, event) { event.event_type = event.event_type_id ? await queryOne(schema, 'SELECT * FROM event_types WHERE id=$1', [event.event_type_id]) : null; // recurrence_rule is JSONB in Postgres — already parsed, no need to JSON.parse event.user_groups = await query(schema, ` SELECT ug.id, ug.name FROM event_user_groups eug JOIN user_groups ug ON ug.id=eug.user_group_id WHERE eug.event_id=$1 `, [event.id]); return event; } async function applyEventUpdate(schema, eventId, fields, userGroupIds) { const { title, eventTypeId, startAt, endAt, allDay, location, description, isPublic, trackAvailability, recurrenceRule, origEvent } = fields; await exec(schema, ` UPDATE events SET title = COALESCE($1, title), event_type_id = $2, start_at = COALESCE($3, start_at), end_at = COALESCE($4, end_at), all_day = COALESCE($5, all_day), location = $6, description = $7, is_public = COALESCE($8, is_public), track_availability = COALESCE($9, track_availability), recurrence_rule = $10, updated_at = NOW() WHERE id = $11 `, [ title?.trim() || null, eventTypeId !== undefined ? (eventTypeId || null) : origEvent.event_type_id, startAt || null, endAt || null, allDay !== undefined ? allDay : null, location !== undefined ? (location || null) : origEvent.location, description !== undefined ? (description || null) : origEvent.description, isPublic !== undefined ? isPublic : null, trackAvailability !== undefined ? trackAvailability : null, recurrenceRule !== undefined ? recurrenceRule : origEvent.recurrence_rule, eventId, ]); if (Array.isArray(userGroupIds)) { await exec(schema, 'DELETE FROM event_user_groups WHERE event_id=$1', [eventId]); for (const ugId of userGroupIds) await exec(schema, 'INSERT INTO event_user_groups (event_id,user_group_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [eventId, ugId]); } } // ── Event Types ─────────────────────────────────────────────────────────────── router.get('/event-types', authMiddleware, async (req, res) => { try { const eventTypes = await query(req.schema, 'SELECT * FROM event_types ORDER BY is_default DESC, name ASC'); res.json({ eventTypes }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.post('/event-types', authMiddleware, teamManagerMiddleware, async (req, res) => { const { name, colour, defaultUserGroupId, defaultDurationHrs } = req.body; if (!name?.trim()) return res.status(400).json({ error: 'Name required' }); try { if (await queryOne(req.schema, 'SELECT id FROM event_types WHERE LOWER(name)=LOWER($1)', [name.trim()])) return res.status(400).json({ error: 'Event type with that name already exists' }); const r = await queryResult(req.schema, 'INSERT INTO event_types (name,colour,default_user_group_id,default_duration_hrs) VALUES ($1,$2,$3,$4) RETURNING id', [name.trim(), colour||'#6366f1', defaultUserGroupId||null, defaultDurationHrs||1.0] ); res.json({ eventType: await queryOne(req.schema, 'SELECT * FROM event_types WHERE id=$1', [r.rows[0].id]) }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.patch('/event-types/:id', authMiddleware, teamManagerMiddleware, async (req, res) => { try { const et = await queryOne(req.schema, 'SELECT * FROM event_types WHERE id=$1', [req.params.id]); if (!et) return res.status(404).json({ error: 'Not found' }); if (et.is_protected) return res.status(403).json({ error: 'Cannot edit a protected event type' }); const { name, colour, defaultUserGroupId, defaultDurationHrs } = req.body; if (name && name.trim() !== et.name) { if (await queryOne(req.schema, 'SELECT id FROM event_types WHERE LOWER(name)=LOWER($1) AND id!=$2', [name.trim(), et.id])) return res.status(400).json({ error: 'Name already in use' }); } await exec(req.schema, ` UPDATE event_types SET name = COALESCE($1, name), colour = COALESCE($2, colour), default_user_group_id = $3, default_duration_hrs = COALESCE($4, default_duration_hrs) WHERE id=$5 `, [name?.trim()||null, colour||null, defaultUserGroupId??et.default_user_group_id, defaultDurationHrs||null, et.id]); res.json({ eventType: await queryOne(req.schema, 'SELECT * FROM event_types WHERE id=$1', [et.id]) }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.delete('/event-types/:id', authMiddleware, teamManagerMiddleware, async (req, res) => { try { const et = await queryOne(req.schema, 'SELECT * FROM event_types WHERE id=$1', [req.params.id]); if (!et) return res.status(404).json({ error: 'Not found' }); if (et.is_default || et.is_protected) return res.status(403).json({ error: 'Cannot delete a protected event type' }); await exec(req.schema, 'UPDATE events SET event_type_id=NULL WHERE event_type_id=$1', [et.id]); await exec(req.schema, 'DELETE FROM event_types WHERE id=$1', [et.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); // ── User's own groups (for regular users creating events) ───────────────────── router.get('/my-groups', authMiddleware, async (req, res) => { try { const groups = await query(req.schema, ` SELECT ug.id, ug.name FROM user_groups ug JOIN user_group_members ugm ON ugm.user_group_id = ug.id WHERE ugm.user_id = $1 ORDER BY ug.name ASC `, [req.user.id]); res.json({ groups }); } catch (e) { res.status(500).json({ error: e.message }); } }); // ── Events ──────────────────────────────────────────────────────────────────── router.get('/', authMiddleware, async (req, res) => { try { const itm = await isToolManagerFn(req.schema, req.user); const { from, to } = req.query; let sql = 'SELECT * FROM events WHERE 1=1'; const params = []; let pi = 1; if (from) { sql += ` AND end_at >= $${pi++}`; params.push(from); } if (to) { sql += ` AND start_at <= $${pi++}`; params.push(to); } sql += ' ORDER BY start_at ASC'; const rawEvents = await query(req.schema, sql, params); const events = []; for (const e of rawEvents) { if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue; await enrichEvent(req.schema, e); const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id, req.user.id]); e.my_response = mine?.response || null; events.push(e); } res.json({ events }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.get('/me/pending', authMiddleware, async (req, res) => { try { const pending = await query(req.schema, ` SELECT DISTINCT e.* FROM events e JOIN event_user_groups eug ON eug.event_id=e.id JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id WHERE ugm.user_id=$1 AND e.track_availability=TRUE AND e.end_at >= NOW() AND NOT EXISTS (SELECT 1 FROM event_availability ea WHERE ea.event_id=e.id AND ea.user_id=$1) ORDER BY e.start_at ASC `, [req.user.id]); const result = []; for (const e of pending) result.push(await enrichEvent(req.schema, e)); res.json({ events: result }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.get('/:id', authMiddleware, async (req, res) => { try { const event = await queryOne(req.schema, 'SELECT * FROM events WHERE id=$1', [req.params.id]); if (!event) return res.status(404).json({ error: 'Not found' }); const itm = await isToolManagerFn(req.schema, req.user); if (!(await canViewEvent(req.schema, event, req.user.id, itm))) return res.status(403).json({ error: 'Access denied' }); await enrichEvent(req.schema, event); const isMember = !itm && !!(await queryOne(req.schema, ` SELECT 1 FROM event_user_groups eug JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id WHERE eug.event_id=$1 AND ugm.user_id=$2 `, [event.id, req.user.id])); if (event.track_availability && (itm || isMember)) { event.availability = await query(req.schema, ` SELECT ea.response, ea.note, ea.updated_at, u.id AS user_id, u.name, u.display_name, u.avatar FROM event_availability ea JOIN users u ON u.id=ea.user_id WHERE ea.event_id=$1 `, [req.params.id]); if (itm) { const assignedIds = (await query(req.schema, ` SELECT DISTINCT ugm.user_id FROM event_user_groups eug JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id WHERE eug.event_id=$1 `, [req.params.id])).map(r => r.user_id); const respondedIds = new Set(event.availability.map(r => r.user_id)); event.no_response_count = assignedIds.filter(id => !respondedIds.has(id)).length; } } const mine = await queryOne(req.schema, 'SELECT response, note FROM event_availability WHERE event_id=$1 AND user_id=$2', [req.params.id, req.user.id]); event.my_response = mine?.response || null; event.my_note = mine?.note || null; res.json({ event }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.post('/', authMiddleware, async (req, res) => { const { title, eventTypeId, startAt, endAt, allDay, location, description, isPublic, trackAvailability, userGroupIds=[], recurrenceRule } = req.body; if (!title?.trim()) return res.status(400).json({ error: 'Title required' }); if (!startAt || !endAt) return res.status(400).json({ error: 'Start and end time required' }); try { const itm = await isToolManagerFn(req.schema, req.user); const groupIds = Array.isArray(userGroupIds) ? userGroupIds : []; if (!itm) { // Regular users: must select at least one group they belong to; event always private if (!groupIds.length) return res.status(400).json({ error: 'Select at least one group' }); for (const ugId of groupIds) { const member = await queryOne(req.schema, 'SELECT 1 FROM user_group_members WHERE user_id=$1 AND user_group_id=$2', [req.user.id, ugId]); if (!member) return res.status(403).json({ error: 'You can only assign groups you belong to' }); } } const effectiveIsPublic = itm ? (isPublic !== false) : false; const r = await queryResult(req.schema, ` INSERT INTO events (title,event_type_id,start_at,end_at,all_day,location,description,is_public,track_availability,recurrence_rule,created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING id `, [title.trim(), eventTypeId||null, startAt, endAt, !!allDay, location||null, description||null, effectiveIsPublic, !!trackAvailability, recurrenceRule||null, req.user.id]); const eventId = r.rows[0].id; for (const ugId of groupIds) await exec(req.schema, 'INSERT INTO event_user_groups (event_id,user_group_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [eventId, ugId]); if (groupIds.length > 0) await postEventNotification(req.schema, eventId, req.user.id, false); const event = await queryOne(req.schema, 'SELECT * FROM events WHERE id=$1', [eventId]); res.json({ event: await enrichEvent(req.schema, event) }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.patch('/:id', authMiddleware, async (req, res) => { try { const event = await queryOne(req.schema, 'SELECT * FROM events WHERE id=$1', [req.params.id]); if (!event) return res.status(404).json({ error: 'Not found' }); const itm = await isToolManagerFn(req.schema, req.user); if (!itm && event.created_by !== req.user.id) return res.status(403).json({ error: 'Access denied' }); let { title, eventTypeId, startAt, endAt, allDay, location, description, isPublic, trackAvailability, userGroupIds, recurrenceRule, recurringScope } = req.body; if (!itm) { // Regular users editing their own event: force private, validate group membership isPublic = false; if (Array.isArray(userGroupIds)) { if (!userGroupIds.length) return res.status(400).json({ error: 'Select at least one group' }); for (const ugId of userGroupIds) { const member = await queryOne(req.schema, 'SELECT 1 FROM user_group_members WHERE user_id=$1 AND user_group_id=$2', [req.user.id, ugId]); if (!member) return res.status(403).json({ error: 'You can only assign groups you belong to' }); } } } const fields = { title, eventTypeId, startAt, endAt, allDay, location, description, isPublic, trackAvailability, recurrenceRule, origEvent: event }; await applyEventUpdate(req.schema, req.params.id, fields, userGroupIds); // Recurring future scope — update this and all future occurrences if (recurringScope === 'future' && event.recurrence_rule) { const futureEvents = await query(req.schema, ` SELECT id FROM events WHERE id!=$1 AND created_by=$2 AND recurrence_rule IS NOT NULL AND start_at >= $3 AND title=$4 `, [req.params.id, event.created_by, event.start_at, event.title]); for (const fe of futureEvents) await applyEventUpdate(req.schema, fe.id, fields, userGroupIds); } // Recurring all scope — update every occurrence if (recurringScope === 'all' && event.recurrence_rule) { const allEvents = await query(req.schema, ` SELECT id FROM events WHERE id!=$1 AND created_by=$2 AND recurrence_rule IS NOT NULL AND title=$3 `, [req.params.id, event.created_by, event.title]); for (const ae of allEvents) await applyEventUpdate(req.schema, ae.id, fields, userGroupIds); } // Clean up availability for users removed from groups if (Array.isArray(userGroupIds)) { const prevGroupIds = (await query(req.schema, 'SELECT user_group_id FROM event_user_groups WHERE event_id=$1', [req.params.id])).map(r => r.user_group_id); const newGroupSet = new Set(userGroupIds.map(Number)); const removedGroupIds = prevGroupIds.filter(id => !newGroupSet.has(id)); for (const removedGid of removedGroupIds) { const removedUids = (await query(req.schema, 'SELECT user_id FROM user_group_members WHERE user_group_id=$1', [removedGid])).map(r => r.user_id); for (const uid of removedUids) { if (newGroupSet.size > 0) { const ph = [...newGroupSet].map((_,i) => `$${i+2}`).join(','); const stillAssigned = await queryOne(req.schema, `SELECT 1 FROM user_group_members WHERE user_id=$1 AND user_group_id IN (${ph})`, [uid, ...[...newGroupSet]]); if (stillAssigned) continue; } await exec(req.schema, 'DELETE FROM event_availability WHERE event_id=$1 AND user_id=$2', [req.params.id, uid]); } } } const updated = await queryOne(req.schema, 'SELECT * FROM events WHERE id=$1', [req.params.id]); const finalGroups = await query(req.schema, 'SELECT user_group_id FROM event_user_groups WHERE event_id=$1', [req.params.id]); if (finalGroups.length > 0) await postEventNotification(req.schema, req.params.id, req.user.id, true); res.json({ event: await enrichEvent(req.schema, updated) }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.delete('/:id', authMiddleware, async (req, res) => { try { const event = await queryOne(req.schema, 'SELECT * FROM events WHERE id=$1', [req.params.id]); if (!event) return res.status(404).json({ error: 'Not found' }); const itm = await isToolManagerFn(req.schema, req.user); if (!itm && event.created_by !== req.user.id) return res.status(403).json({ error: 'Access denied' }); const { recurringScope } = req.body || {}; if (recurringScope === 'future' && event.recurrence_rule) { // Delete this event and all future occurrences with same creator/title await exec(req.schema, ` DELETE FROM events WHERE created_by=$1 AND recurrence_rule IS NOT NULL AND title=$2 AND start_at >= $3 `, [event.created_by, event.title, event.start_at]); } else if (recurringScope === 'all' && event.recurrence_rule) { // Delete every occurrence await exec(req.schema, ` DELETE FROM events WHERE created_by=$1 AND recurrence_rule IS NOT NULL AND title=$2 `, [event.created_by, event.title]); } else { await exec(req.schema, 'DELETE FROM events WHERE id=$1', [req.params.id]); } res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); // ── Availability ────────────────────────────────────────────────────────────── router.put('/:id/availability', authMiddleware, async (req, res) => { try { const event = await queryOne(req.schema, 'SELECT * FROM events WHERE id=$1', [req.params.id]); if (!event) return res.status(404).json({ error: 'Not found' }); if (!event.track_availability) return res.status(400).json({ error: 'Availability tracking not enabled' }); const { response, note } = req.body; if (!['going','maybe','not_going'].includes(response)) return res.status(400).json({ error: 'Invalid response' }); const trimmedNote = note ? String(note).trim().slice(0, 20) : null; const itm = await isToolManagerFn(req.schema, req.user); const inGroup = await queryOne(req.schema, ` SELECT 1 FROM event_user_groups eug JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id WHERE eug.event_id=$1 AND ugm.user_id=$2 `, [event.id, req.user.id]); if (!inGroup && !itm) return res.status(403).json({ error: 'You are not assigned to this event' }); await exec(req.schema, ` INSERT INTO event_availability (event_id,user_id,response,note,updated_at) VALUES ($1,$2,$3,$4,NOW()) ON CONFLICT (event_id,user_id) DO UPDATE SET response=$3, note=$4, updated_at=NOW() `, [event.id, req.user.id, response, trimmedNote]); res.json({ success: true, response, note: trimmedNote }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.patch('/:id/availability/note', authMiddleware, async (req, res) => { try { const existing = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [req.params.id, req.user.id]); if (!existing) return res.status(404).json({ error: 'No availability response found' }); const trimmedNote = req.body.note ? String(req.body.note).trim().slice(0, 20) : null; await exec(req.schema, 'UPDATE event_availability SET note=$1, updated_at=NOW() WHERE event_id=$2 AND user_id=$3', [trimmedNote, req.params.id, req.user.id]); res.json({ success: true, note: trimmedNote }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.delete('/:id/availability', authMiddleware, async (req, res) => { try { await exec(req.schema, 'DELETE FROM event_availability WHERE event_id=$1 AND user_id=$2', [req.params.id, req.user.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.post('/me/bulk-availability', authMiddleware, async (req, res) => { const { responses } = req.body; if (!Array.isArray(responses)) return res.status(400).json({ error: 'responses array required' }); try { let saved = 0; const itm = await isToolManagerFn(req.schema, req.user); for (const { eventId, response } of responses) { if (!['going','maybe','not_going'].includes(response)) continue; const event = await queryOne(req.schema, 'SELECT * FROM events WHERE id=$1', [eventId]); if (!event || !event.track_availability) continue; const inGroup = await queryOne(req.schema, ` SELECT 1 FROM event_user_groups eug JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id WHERE eug.event_id=$1 AND ugm.user_id=$2 `, [eventId, req.user.id]); if (!inGroup && !itm) continue; await exec(req.schema, ` INSERT INTO event_availability (event_id,user_id,response,updated_at) VALUES ($1,$2,$3,NOW()) ON CONFLICT (event_id,user_id) DO UPDATE SET response=$3, updated_at=NOW() `, [eventId, req.user.id, response]); saved++; } res.json({ success: true, saved }); } catch (e) { res.status(500).json({ error: e.message }); } }); // ── CSV Import ──────────────────────────────────────────────────────────────── router.post('/import/preview', authMiddleware, teamManagerMiddleware, upload.single('file'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); try { const rows = csvParse(req.file.buffer.toString('utf8'), { columns:true, skip_empty_lines:true, trim:true }); const results = await Promise.all(rows.map(async (row, i) => { const title = row['Event Title'] || row['event_title'] || row['title'] || ''; const startDate = row['start_date'] || row['Start Date'] || ''; const startTime = row['start_time'] || row['Start Time'] || '09:00'; const location = row['event_location'] || row['location'] || ''; const typeName = row['event_type'] || row['Event Type'] || 'Default'; const durHrs = parseFloat(row['default_duration'] || row['duration'] || '1') || 1; if (!title || !startDate) return { row:i+1, title, error:'Missing title or start date', duplicate:false }; const startAt = `${startDate}T${startTime.padStart(5,'0')}:00`; const endMs = new Date(startAt).getTime() + durHrs * 3600000; const endAt = isNaN(endMs) ? startAt : new Date(endMs).toISOString().slice(0,19); const dup = await queryOne(req.schema, 'SELECT id,title FROM events WHERE title=$1 AND start_at=$2', [title, startAt]); return { row:i+1, title, startAt, endAt, location, typeName, durHrs, duplicate:!!dup, duplicateId:dup?.id, error:null }; })); res.json({ rows: results }); } catch (e) { res.status(400).json({ error: 'CSV parse error: ' + e.message }); } }); router.post('/import/confirm', authMiddleware, teamManagerMiddleware, async (req, res) => { const { rows } = req.body; if (!Array.isArray(rows)) return res.status(400).json({ error: 'rows array required' }); try { let imported = 0; const colours = ['#ef4444','#f97316','#eab308','#22c55e','#06b6d4','#3b82f6','#8b5cf6','#ec4899']; for (const row of rows) { if (row.error || row.skip) continue; let typeId = null; if (row.typeName) { let et = await queryOne(req.schema, 'SELECT id FROM event_types WHERE LOWER(name)=LOWER($1)', [row.typeName]); if (!et) { const usedColours = (await query(req.schema, 'SELECT colour FROM event_types')).map(r => r.colour); const colour = colours.find(c => !usedColours.includes(c)) || '#' + Math.floor(Math.random()*0xffffff).toString(16).padStart(6,'0'); const cr = await queryResult(req.schema, 'INSERT INTO event_types (name,colour) VALUES ($1,$2) RETURNING id', [row.typeName, colour]); typeId = cr.rows[0].id; } else { typeId = et.id; } } await exec(req.schema, 'INSERT INTO events (title,event_type_id,start_at,end_at,location,is_public,track_availability,created_by) VALUES ($1,$2,$3,$4,$5,TRUE,FALSE,$6)', [row.title, typeId, row.startAt, row.endAt, row.location||null, req.user.id] ); imported++; } res.json({ success: true, imported }); } catch (e) { res.status(500).json({ error: e.message }); } }); return router; }; // end module.exports