import express from 'express'; import cors from 'cors'; import sqlite3 from 'sqlite3'; import path from 'path'; import { fileURLToPath } from 'url'; import moment from 'moment-timezone'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const port = 3000; // Load webhook URLs from environment const ADMIN_WEBHOOK_URL = process.env.ADMIN_WEBHOOK_URL; const USER_WEBHOOK_URL = process.env.USER_WEBHOOK_URL; const timestamp = moment().utc().format('YYYY-MM-DD HH:mm:ss'); app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname, 'public'))); app.use('/assets', express.static(path.join(__dirname, 'assets'))); const dbPath = path.resolve(__dirname, 'tickets.db'); const db = new sqlite3.Database(dbPath, err => { if (err) { console.error('Failed to connect to DB:', err.message); } else { console.log('Connected to SQLite DB.'); } }); // Create tables db.serialize(() => { db.run(` CREATE TABLE IF NOT EXISTS tickets ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, requester TEXT, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT, priority TEXT, category TEXT, total_time INTEGER DEFAULT 0 ) `); db.run(` CREATE TABLE IF NOT EXISTS time_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, ticket_id INTEGER NOT NULL, minutes INTEGER NOT NULL, comment TEXT, author TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(ticket_id) REFERENCES tickets(id) ) `); db.run(` CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, ticket_id INTEGER NOT NULL, action TEXT NOT NULL, message TEXT, author TEXT, role TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(ticket_id) REFERENCES tickets(id) ) `); db.run(`CREATE INDEX IF NOT EXISTS idx_time_logs_ticket ON time_logs(ticket_id)`); db.run(`CREATE INDEX IF NOT EXISTS idx_history_ticket ON history(ticket_id)`); }); // Log history function logHistory(ticketId, action, message, author = 'system', role = 'user') { const timestamp = moment().utc().format('YYYY-MM-DD HH:mm:ss'); console.log(`Logging history for ticket: ${ticketId}, timestamp: ${timestamp}`); // Debugging line db.run( 'INSERT INTO history (ticket_id, action, message, author, role, timestamp) VALUES (?, ?, ?, ?, ?, ?)', [ticketId, action, message, author, role, timestamp] ); } // GET webhook configuration for frontend app.get('/api/webhooks', (req, res) => { res.json({ admin: ADMIN_WEBHOOK_URL || null, user: USER_WEBHOOK_URL || null, configured: { admin: !!ADMIN_WEBHOOK_URL, user: !!USER_WEBHOOK_URL } }); }); // GET all tickets app.get('/api/tickets', (req, res) => { db.all( `SELECT id, title, requester, description, status, priority, category, total_time, strftime('%Y-%m-%dT%H:%M:%S', created_at) AS created_at, strftime('%Y-%m-%dT%H:%M:%S', updated_at) AS updated_at FROM tickets ORDER BY created_at DESC`, (err, rows) => { if (err) return res.status(500).json({ error: err.message }); res.json(rows); } ); }); // GET single ticket app.get('/api/tickets/:id', (req, res) => { db.get('SELECT * FROM tickets WHERE id = ?', [req.params.id], (err, row) => { if (err) return res.status(500).json({ error: err.message }); if (!row) return res.status(404).json({ error: 'Ticket not found' }); res.json(row); }); }); // GET ticket history (ISO formatted date) app.get('/api/tickets/:id/history', (req, res) => { db.all( `SELECT id, ticket_id, action, message, author, role, strftime('%Y-%m-%dT%H:%M:%S', timestamp) AS date FROM history WHERE ticket_id = ? ORDER BY timestamp DESC`, [req.params.id], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); res.json(rows); } ); }); // POST new ticket app.post('/api/tickets', (req, res) => { const { title, requester, description, status, priority, category } = req.body; const statusValue = status && status.trim() ? status : 'open'; // Get current Berlin time and convert it to UTC const berlinTime = moment.tz('Europe/Berlin'); const utcTime = berlinTime.clone().utc().format('YYYY-MM-DD HH:mm:ss'); // Convert Berlin time to UTC db.run( 'INSERT INTO tickets (title, requester, description, status, priority, category, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [title, requester, description, statusValue, priority, category, utcTime, utcTime], function (err) { if (err) return res.status(500).json({ error: err.message }); logHistory(this.lastID, 'Ticket Created', `Ticket "${title}" created`, requester); res.status(201).json({ id: this.lastID }); } ); }); // PUT update ticket app.put('/api/tickets/:id', (req, res) => { const { title, requester, description, status, priority, category, author } = req.body; // Get current Berlin time and convert it to UTC const berlinTime = moment.tz('Europe/Berlin'); const utcTime = berlinTime.clone().utc().format('YYYY-MM-DD HH:mm:ss'); // Convert Berlin time to UTC db.run( `UPDATE tickets SET title = ?, requester = ?, description = ?, status = ?, priority = ?, category = ?, updated_at = ? WHERE id = ?`, [title, requester, description, status, priority, category, utcTime, req.params.id], function (err) { if (err) return res.status(500).json({ error: err.message }); if (this.changes === 0) return res.status(404).json({ error: 'Ticket not found' }); logHistory(req.params.id, 'Ticket Updated', 'Ticket details updated', author || 'system'); res.json({ message: 'Ticket updated' }); } ); }); // DELETE ticket app.delete('/api/tickets/:id', (req, res) => { db.run('DELETE FROM tickets WHERE id = ?', [req.params.id], function (err) { if (err) return res.status(500).json({ error: err.message }); if (this.changes === 0) return res.status(404).json({ error: 'Ticket not found' }); logHistory(req.params.id, 'Ticket Deleted', 'Ticket deleted'); res.json({ message: 'Ticket deleted' }); }); }); // POST comment with role app.post('/api/tickets/:id/comments', (req, res) => { const { author, role, content } = req.body; if (!author || !content) return res.status(400).json({ error: 'Author and content required' }); const timestamp = moment().utc().format('YYYY-MM-DD HH:mm:ss'); // Explicit UTC timestamp db.run( 'INSERT INTO history (ticket_id, action, message, author, role, timestamp) VALUES (?, "Comment", ?, ?, ?, ?)', [req.params.id, content, author, role || 'user', timestamp], function (err) { if (err) return res.status(500).json({ error: err.message }); res.status(201).json({ id: this.lastID }); } ); }); // GET comments (ISO formatted date) app.get('/api/tickets/:id/comments', (req, res) => { db.all( `SELECT id, ticket_id, message AS content, author, role, strftime('%Y-%m-%dT%H:%M:%S', timestamp) AS date FROM history WHERE ticket_id = ? AND action = "Comment" ORDER BY timestamp DESC`, [req.params.id], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); res.json(rows); } ); }); // POST time log app.post('/api/tickets/:id/time', (req, res) => { const { minutes, comment, author } = req.body; if (typeof minutes !== 'number' || minutes <= 0) return res.status(400).json({ error: 'Minutes must be a positive number.' }); if (!author) return res.status(400).json({ error: 'Author is required.' }); db.get('SELECT * FROM tickets WHERE id = ?', [req.params.id], (err, ticket) => { if (err) return res.status(500).json({ error: err.message }); if (!ticket) return res.status(404).json({ error: 'Ticket not found' }); db.run( 'INSERT INTO time_logs (ticket_id, minutes, comment, author) VALUES (?, ?, ?, ?)', [req.params.id, minutes, comment || '', author], function (err) { if (err) return res.status(500).json({ error: err.message }); db.get( 'SELECT SUM(minutes) as total FROM time_logs WHERE ticket_id = ?', [req.params.id], (err, row) => { if (err) return res.status(500).json({ error: err.message }); const totalTime = row.total || 0; db.run( 'UPDATE tickets SET total_time = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [totalTime, req.params.id], err => { if (err) return res.status(500).json({ error: err.message }); logHistory( req.params.id, 'Time Logged', `Added ${minutes} minutes by ${author}. Comment: ${comment || 'none'}`, author ); // Send back the updated total_time to the frontend res.status(201).json({ message: 'Time logged', total_time: totalTime }); } ); } ); } ); }); }); // GET time logs app.get('/api/tickets/:id/time', (req, res) => { db.all( 'SELECT * FROM time_logs WHERE ticket_id = ? ORDER BY created_at DESC', [req.params.id], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); res.json(rows); } ); }); app.listen(port, () => { console.log(`Server läuft auf http://localhost:${port}`); console.log('Webhook URLs loaded:', { admin: ADMIN_WEBHOOK_URL ? 'Configured' : 'Not configured', user: USER_WEBHOOK_URL ? 'Configured' : 'Not configured' }); });