v0.12.31 multiple UI changes

This commit is contained in:
2026-03-27 10:19:52 -04:00
parent d6a37d5948
commit 97f1dace4f
10 changed files with 174 additions and 69 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "rosterchirp-backend",
"version": "0.12.30",
"version": "0.12.31",
"description": "RosterChirp backend server",
"main": "src/index.js",
"scripts": {

View File

@@ -177,6 +177,20 @@ router.delete('/event-types/:id', authMiddleware, teamManagerMiddleware, async (
} 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) => {
@@ -244,31 +258,55 @@ router.get('/:id', authMiddleware, async (req, res) => {
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
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,
isPublic!==false, !!trackAvailability, recurrenceRule||null, req.user.id]);
effectiveIsPublic, !!trackAvailability, recurrenceRule||null, req.user.id]);
const eventId = r.rows[0].id;
for (const ugId of (Array.isArray(userGroupIds) ? userGroupIds : []))
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 (Array.isArray(userGroupIds) && userGroupIds.length > 0)
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, teamManagerMiddleware, async (req, res) => {
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 { title, eventTypeId, startAt, endAt, allDay, location, description, isPublic, trackAvailability, userGroupIds, recurrenceRule, recurringScope } = req.body;
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);
@@ -317,10 +355,12 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) =>
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.delete('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => {
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