cd backend npm install cp .env.example .env # Fyll i SMTP (och ev. Twilio) i .env npm run dev
cd backend npm install cp .env.example .env # Fyll i SMTP (och ev. Twilio) i .env npm run dev

Ny uppgift

Uppgifter

Uppgift Område Frekvens Nästa förfall Mottagare Åtgärd
cd backend npm install cp .env.example .env # Fyll i SMTP (och ev. Twilio) i .env npm run dev async function loadHist(){ const res = await fetch('/api/admin/completions'); const rows = await res.json(); hist.innerHTML = rows.map(r => { const cl = r.checklist_checked_json ? JSON.parse(r.checklist_checked_json) : []; const files = r.attachments_json ? JSON.parse(r.attachments_json) : []; return ` ${fmt(r.completed_at)} ${r.title} ${r.area_name || '-'} ${r.frequency} ${r.reporter || ''} ${cl.map(t => `${t}`).join('')} ${r.comment || ''} ${files.map(f => `${f.split('__')[1]||f}`).join('
')} `; }).join(''); } async function load(){ const u = await ensureAdmin(); if(!u) return; await loadUsers(); await loadHist(); } load();{ "name": "ugc-reminder-backend", "version": "3.0.0", "type": "module", "scripts": { "start": "node server.js", "dev": "NODE_ENV=development node server.js", "seed-admin": "node seed-admin.js" }, "dependencies": { "better-sqlite3": "^9.4.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "date-fns": "^3.6.0", "date-fns-tz": "^3.2.0", "dotenv": "^16.4.5", "express": "^4.19.2", "node-cron": "^3.0.3", "nodemailer": "^6.9.13", "twilio": "^4.23.0", "bcryptjs": "^2.4.3", "csv-stringify": "^6.5.0", "multer": "^1.4.5-lts.1" } }

Uppgifter

UppgiftOmrådeFrekvensNästa förfallÅtgärd

Uppgifter

UppgiftFrekvensNästa förfallÅtgärd
import express from 'express'; import cors from 'cors'; import Database from 'better-sqlite3'; import path from 'path'; import { fileURLToPath } from 'url'; import { addDays, addWeeks, addMonths, parseISO } from 'date-fns'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); app.use(express.json()); app.use(cors()); // DB const db = new Database(path.join(__dirname, 'data.db')); db.pragma('journal_mode = WAL'); db.exec(` CREATE TABLE IF NOT EXISTS areas ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, slug TEXT UNIQUE, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, area_id INTEGER NOT NULL, frequency TEXT CHECK(frequency IN ('daily','weekly','monthly','once')) NOT NULL DEFAULT 'weekly', notes TEXT, next_due_at TEXT, last_completed_at TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(area_id) REFERENCES areas(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS completions ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, completed_at TEXT NOT NULL, reporter TEXT, comment TEXT, FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE ); `); // helpers function slugify(s){ return s.toLowerCase().trim().replace(/[^a-z0-9åäöüé]+/g,'-').replace(/^-+|-+$/g,''); } function computeNextDue(fromISO, freq){ const d = parseISO(fromISO); if (freq === 'daily') return addDays(d,1).toISOString(); if (freq === 'weekly') return addWeeks(d,1).toISOString(); if (freq === 'monthly') return addMonths(d,1).toISOString(); return null; } // API app.get('/api/areas', (req,res)=>{ const rows = db.prepare('SELECT * FROM areas ORDER BY name').all(); res.json(rows); }); app.post('/api/areas', (req,res)=>{ const { name, slug } = req.body || {}; if(!name) return res.status(400).json({ error: 'name krävs' }); const s = slug || slugify(name); const info = db.prepare('INSERT INTO areas (name, slug) VALUES (?, ?)').run(name, s); res.json({ id: info.lastInsertRowid, name, slug: s }); }); app.patch('/api/areas/:id', (req,res)=>{ const id = Number(req.params.id); const { name, slug } = req.body || {}; const s = slug || (name ? slugify(name) : undefined); const sets = []; const params = { id }; if (name){ sets.push('name=@name'); params.name = name; } if (s){ sets.push('slug=@slug'); params.slug = s; } if (!sets.length) return res.json({ ok: true }); db.prepare(`UPDATE areas SET ${sets.join(', ')} WHERE id=@id`).run(params); res.json({ ok: true }); }); app.delete('/api/areas/:id', (req,res)=>{ const id = Number(req.params.id); db.prepare('DELETE FROM areas WHERE id=?').run(id); res.json({ ok: true }); }); app.get('/api/tasks', (req,res)=>{ const rows = db.prepare(` SELECT t.*, a.name AS area_name, a.slug AS area_slug FROM tasks t JOIN areas a ON a.id = t.area_id ORDER BY COALESCE(t.next_due_at, t.created_at) ASC `).all(); res.json(rows); }); app.post('/api/tasks', (req,res)=>{ const { title, area_id, frequency='weekly', notes, first_due_at } = req.body || {}; if (!title || !area_id) return res.status(400).json({ error: 'title och area_id krävs' }); const next_due_at = first_due_at || new Date().toISOString(); const info = db.prepare(` INSERT INTO tasks (title, area_id, frequency, notes, next_due_at) VALUES (?,?,?,?,?) `).run(title, area_id, frequency, notes || null, next_due_at); res.json({ id: info.lastInsertRowid }); }); app.patch('/api/tasks/:id', (req,res)=>{ const id = Number(req.params.id); const fields = ['title','area_id','frequency','notes','next_due_at']; const sets = []; const params = { id }; for (const f of fields) if (f in req.body){ sets.push(`${f}=@${f}`); params[f] = req.body[f]; } if (!sets.length) return res.json({ ok: true }); db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id=@id`).run(params); res.json({ ok: true }); }); app.delete('/api/tasks/:id', (req,res)=>{ const id = Number(req.params.id); db.prepare('DELETE FROM tasks WHERE id=?').run(id); res.json({ ok: true }); }); // staff filter by area app.get('/api/staff', (req,res)=>{ const { area, slug } = req.query; let rows = []; if (area){ rows = db.prepare(` SELECT t.*, a.name AS area_name FROM tasks t JOIN areas a ON a.id=t.area_id WHERE a.id=? ORDER BY COALESCE(t.next_due_at, t.created_at) ASC `).all(Number(area)); } else if (slug){ rows = db.prepare(` SELECT t.*, a.name AS area_name FROM tasks t JOIN areas a ON a.id=t.area_id WHERE a.slug=? ORDER BY COALESCE(t.next_due_at, t.created_at) ASC `).all(String(slug)); } else { rows = db.prepare(` SELECT t.*, a.name AS area_name FROM tasks t JOIN areas a ON a.id=t.area_id ORDER BY COALESCE(t.next_due_at, t.created_at) ASC `).all(); } res.json(rows); }); // complete app.post('/api/tasks/:id/complete', (req,res)=>{ const id = Number(req.params.id); const { reporter, comment } = req.body || {}; const t = db.prepare('SELECT * FROM tasks WHERE id=?').get(id); if (!t) return res.status(404).json({ error:'task finns ej' }); const completed_at = new Date().toISOString(); db.prepare('INSERT INTO completions (task_id, completed_at, reporter, comment) VALUES (?,?,?,?)') .run(id, completed_at, reporter || null, comment || null); let next_due = null; if (t.frequency === 'daily') next_due = addDays(parseISO(completed_at),1).toISOString(); else if (t.frequency === 'weekly') next_due = addWeeks(parseISO(completed_at),1).toISOString(); else if (t.frequency === 'monthly') next_due = addMonths(parseISO(completed_at),1).toISOString(); db.prepare('UPDATE tasks SET last_completed_at=?, next_due_at=? WHERE id=?').run(completed_at, next_due, id); res.json({ ok:true, next_due_at: next_due }); }); // history app.get('/api/tasks/:id/history', (req,res)=>{ const id = Number(req.params.id); const rows = db.prepare('SELECT * FROM completions WHERE task_id=? ORDER BY completed_at DESC').all(id); res.json(rows); }); // static app.use(express.static(path.join(__dirname, 'public'))); const port = Number(process.env.PORT || 4000); app.listen(port, ()=> console.log('UGC MVP lyssnar på http://localhost:'+port));

AVVIKANDEÖPPETTIDER 8 Juni 15:00-22:00 9 Juni STÄNGT 20 Juni (midsommarafton) STÄNGT VÄLKOMNA ALLA ANDRA DAGAR!