diff --git a/backend/package.json b/backend/package.json index 1fc447a..f179e64 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.11.13", + "version": "0.11.14", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/index.js b/backend/src/index.js index 09f48fc..9dd5f77 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -35,7 +35,7 @@ app.use('/api/users', require('./routes/users')); app.use('/api/groups', require('./routes/groups')(io)); app.use('/api/messages', require('./routes/messages')(io)); app.use('/api/usergroups', require('./routes/usergroups')(io)); -app.use('/api/schedule', require('./routes/schedule')); +app.use('/api/schedule', require('./routes/schedule')(io)); app.use('/api/settings', require('./routes/settings')); app.use('/api/about', require('./routes/about')); app.use('/api/help', require('./routes/help')); diff --git a/backend/src/routes/schedule.js b/backend/src/routes/schedule.js index b1e3d5e..0d52461 100644 --- a/backend/src/routes/schedule.js +++ b/backend/src/routes/schedule.js @@ -1,11 +1,56 @@ const express = require('express'); -const router = express.Router(); 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) { @@ -212,6 +257,8 @@ router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => { const eventId = r.rows[0].id; for (const ugId of (Array.isArray(userGroupIds) ? userGroupIds : [])) 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) + 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 }); } @@ -255,6 +302,9 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => } 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 }); } }); @@ -375,4 +425,5 @@ router.post('/import/confirm', authMiddleware, teamManagerMiddleware, async (req } catch (e) { res.status(500).json({ error: e.message }); } }); -module.exports = router; +return router; +}; // end module.exports diff --git a/build.sh b/build.sh index 9e1e9f5..65ad60a 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.11.13}" +VERSION="${1:-0.11.14}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index e5ffc13..e458ccf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.11.13", + "version": "0.11.14", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index bf1fa54..3f7a56c 100644 Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ diff --git a/frontend/public/icons/icon-192-maskable.png b/frontend/public/icons/icon-192-maskable.png index 1742ca5..c62db49 100644 Binary files a/frontend/public/icons/icon-192-maskable.png and b/frontend/public/icons/icon-192-maskable.png differ diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png index 3ad9432..20e4a9b 100644 Binary files a/frontend/public/icons/icon-192.png and b/frontend/public/icons/icon-192.png differ diff --git a/frontend/public/icons/icon-512-maskable.png b/frontend/public/icons/icon-512-maskable.png index 2f30105..f8710bd 100644 Binary files a/frontend/public/icons/icon-512-maskable.png and b/frontend/public/icons/icon-512-maskable.png differ diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png index f785653..650f2a6 100644 Binary files a/frontend/public/icons/icon-512.png and b/frontend/public/icons/icon-512.png differ diff --git a/frontend/public/icons/jama.png b/frontend/public/icons/jama.png index f08c18c..650f2a6 100644 Binary files a/frontend/public/icons/jama.png and b/frontend/public/icons/jama.png differ diff --git a/frontend/public/icons/logo-64.png b/frontend/public/icons/logo-64.png index 5529d61..37d4051 100644 Binary files a/frontend/public/icons/logo-64.png and b/frontend/public/icons/logo-64.png differ diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index 70c2af0..0b70959 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -22,10 +22,16 @@ "purpose": "maskable" }, { - "purpose": "any maskable", + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { "src": "/icons/icon-512-maskable.png", "sizes": "512x512", - "type": "image/png" + "type": "image/png", + "purpose": "maskable" } ], "min_width": "320px"