From 0c3fc444409922503549ee3faae4a914d9b4254c Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 30 Mar 2026 00:57:30 +0000 Subject: [PATCH] Notification prefs per task + Odoo module management - notification_prefs table (remind_before, on_due, daily, channels) - GET/PUT /notification-prefs/:taskId - GET /odoo/modules, POST /odoo/modules/install, GET /odoo/status Co-Authored-By: Claude Opus 4.6 (1M context) --- api/src/index.js | 2 ++ api/src/routes/notification-prefs.js | 39 +++++++++++++++++++++++ api/src/routes/odoo-modules.js | 47 ++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 api/src/routes/notification-prefs.js create mode 100644 api/src/routes/odoo-modules.js diff --git a/api/src/index.js b/api/src/index.js index 1eb9264..aae5dae 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -125,6 +125,8 @@ const start = async () => { await app.register(require("./routes/email"), { prefix: "/api/v1" }); await app.register(require("./routes/errors"), { prefix: "/api/v1" }); await app.register(require("./routes/invitations"), { prefix: "/api/v1" }); + await app.register(require("./routes/notification-prefs"), { prefix: "/api/v1" }); + await app.register(require("./routes/odoo-modules"), { prefix: "/api/v1" }); await app.register(require("./routes/spaced-repetition"), { prefix: "/api/v1" }); await app.register(require("./routes/admin"), { prefix: "/api/v1" }); await app.register(require("./routes/activity"), { prefix: "/api/v1" }); diff --git a/api/src/routes/notification-prefs.js b/api/src/routes/notification-prefs.js new file mode 100644 index 0000000..8d804f9 --- /dev/null +++ b/api/src/routes/notification-prefs.js @@ -0,0 +1,39 @@ +// Task Team — Notification Preferences — 2026-03-30 +async function notifPrefRoutes(app) { + app.db.query(` + CREATE TABLE IF NOT EXISTS notification_prefs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + task_id UUID REFERENCES tasks(id) ON DELETE CASCADE, + remind_before_minutes INTEGER DEFAULT 15, + remind_on_due BOOLEAN DEFAULT true, + remind_daily BOOLEAN DEFAULT false, + channels JSONB DEFAULT '["push"]', + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, task_id) + ) + `).catch(() => {}); + + app.get("/notification-prefs/:taskId", async (req) => { + const { user_id } = req.query; + const { rows } = await app.db.query( + "SELECT * FROM notification_prefs WHERE task_id=$1 AND user_id=$2", [req.params.taskId, user_id]); + return { data: rows[0] || { remind_before_minutes: 15, remind_on_due: true, remind_daily: false, channels: ["push"] } }; + }); + + app.put("/notification-prefs/:taskId", async (req) => { + const { user_id, remind_before_minutes, remind_on_due, remind_daily, channels } = req.body; + const { rows } = await app.db.query( + `INSERT INTO notification_prefs (user_id, task_id, remind_before_minutes, remind_on_due, remind_daily, channels) + VALUES ($1,$2,$3,$4,$5,$6) + ON CONFLICT (user_id, task_id) DO UPDATE SET + remind_before_minutes=EXCLUDED.remind_before_minutes, + remind_on_due=EXCLUDED.remind_on_due, + remind_daily=EXCLUDED.remind_daily, + channels=EXCLUDED.channels + RETURNING *`, + [user_id, req.params.taskId, remind_before_minutes || 15, remind_on_due !== false, remind_daily || false, JSON.stringify(channels || ["push"])]); + return { data: rows[0] }; + }); +} +module.exports = notifPrefRoutes; diff --git a/api/src/routes/odoo-modules.js b/api/src/routes/odoo-modules.js new file mode 100644 index 0000000..417b7ac --- /dev/null +++ b/api/src/routes/odoo-modules.js @@ -0,0 +1,47 @@ +// Task Team — Odoo Module Management — 2026-03-30 +async function odooModuleRoutes(app) { + const ODOO_ENT = "http://10.10.10.20:8069"; + const ODOO_COM = "http://10.10.10.20:8070"; + + app.get("/odoo/modules", async (req) => { + return { data: { + available: ["task_team_connector"], + location: "/opt/task-team/odoo_modules/", + enterprise_url: ODOO_ENT, + community_url: ODOO_COM + }}; + }); + + app.post("/odoo/modules/install", async (req) => { + const { module_name, server } = req.body; + const url = server === "community" ? ODOO_COM : ODOO_ENT; + // Trigger module install via Odoo JSON-RPC + try { + const authRes = await fetch(url + "/jsonrpc", { + method: "POST", headers: {"Content-Type":"application/json"}, + body: JSON.stringify({jsonrpc:"2.0",method:"call",id:1, + params:{service:"common",method:"authenticate", + args:[server==="community"?"odoo_community":"odoo_enterprise","admin","admin",{}]}}) + }); + const uid = (await authRes.json()).result; + if (!uid) return { status: "error", message: "Odoo auth failed" }; + + const installRes = await fetch(url + "/jsonrpc", { + method: "POST", headers: {"Content-Type":"application/json"}, + body: JSON.stringify({jsonrpc:"2.0",method:"call",id:2, + params:{service:"object",method:"execute_kw", + args:[server==="community"?"odoo_community":"odoo_enterprise",uid,"admin", + "ir.module.module","button_immediate_install",[[["name","=",module_name]]],{}]}}) + }); + return { status: "ok", result: (await installRes.json()).result }; + } catch(e) { return { status: "error", message: e.message }; } + }); + + app.get("/odoo/status", async (req) => { + let ent = false, com = false; + try { const r = await fetch("http://10.10.10.20:8069/web/login"); ent = r.status === 200; } catch {} + try { const r = await fetch("http://10.10.10.20:8070/web/login"); com = r.status === 200; } catch {} + return { data: { enterprise: ent, community: com } }; + }); +} +module.exports = odooModuleRoutes;