• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

witseie-elen4010 / 2026-group-lab-002 / 25969776284

16 May 2026 06:39PM UTC coverage: 86.751%. Remained the same
25969776284

push

github

Aditya-Raghunandan
Merge branch 'main' of github.com:witseie-elen4010/2026-group-lab-002

562 of 694 branches covered (80.98%)

Branch coverage included in aggregate %.

80 of 96 new or added lines in 8 files covered. (83.33%)

2 existing lines in 1 file now uncovered.

1219 of 1359 relevant lines covered (89.7%)

11.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.08
/src/controllers/admin-controller.js
1
const db = require('../../database/db')
12✔
2
const { logActivity } = require('../services/logging-service')
12✔
3
const ActionTypes = require('../services/action-types')
12✔
4
const { FK_DISPLAY, getInputType, friendlyError, buildSearchQuery } = require('../services/admin-helpers')
12✔
5
const { logAdminAudit } = require('../services/admin-audit-service')
12✔
6

7
const PAGE_SIZE = 20
12✔
8
// tables the admin UI can view but not modify
9
const READ_ONLY_TABLES = ['admin_audit_log', 'activity_log', 'affected_records', 'actions', 'failed_login_log']
12✔
10

11
const getAllTables = () =>
12✔
12
  db.prepare('SELECT name FROM sqlite_master WHERE type=\'table\' ORDER BY name').all().map(r => r.name)
229✔
13

14
const getColumns = (tableName) =>
12✔
15
  db.prepare(`PRAGMA table_info("${tableName}")`).all()
11✔
16

17
const getForeignKeys = (tableName) =>
12✔
18
  db.prepare(`PRAGMA foreign_key_list("${tableName}")`).all()
5✔
19

20
const getForeignKeyOptions = (fkList) => {
12✔
21
  const options = {}
5✔
22
  fkList.forEach(fk => {
5✔
23
    if (!options[fk.from]) {
1!
24
      const display = FK_DISPLAY[fk.table]
1✔
25
      if (display) {
1!
26
        const [valCol, labelCol] = display
×
27
        const rows = db.prepare(`SELECT "${valCol}" as val, "${labelCol}" as label FROM "${fk.table}" ORDER BY 2`).all()
×
28
        options[fk.from] = rows.map(r => ({ value: r.val, label: `${r.val} — ${r.label}` }))
×
29
      } else {
30
        const rows = db.prepare(`SELECT "${fk.to}" as val FROM "${fk.table}" ORDER BY 1`).all()
1✔
31
        options[fk.from] = rows.map(r => ({ value: r.val, label: String(r.val) }))
18✔
32
      }
33
    }
34
  })
35
  return options
5✔
36
}
37

38
const getInputTypes = (columns) => {
12✔
39
  const types = {}
5✔
40
  columns.forEach(col => { types[col.name] = getInputType(col.name) })
12✔
41
  return types
5✔
42
}
43

44
const showAdminDashboard = (req, res) => {
12✔
45
  const user = { id: req.session.userId, name: req.session.userName }
1✔
46
  const tables = getAllTables()
1✔
47
  const stats = {
1✔
48
    students: db.prepare('SELECT COUNT(*) as n FROM students').get().n,
49
    staff: db.prepare('SELECT COUNT(*) as n FROM staff').get().n,
50
    consultations: db.prepare('SELECT COUNT(*) as n FROM consultations').get().n,
51
    availability: db.prepare('SELECT COUNT(*) as n FROM lecturer_availability').get().n
52
  }
53
  res.render('admin-dashboard', {
1✔
54
    user,
55
    tables,
56
    stats,
57
    activeTable: null,
58
    isReadOnly: false,
59
    columns: [],
60
    rows: [],
61
    page: 1,
62
    totalPages: 1,
63
    totalRows: 0,
64
    search: '',
65
    fkOptions: {},
66
    inputTypes: {},
67
    error: null,
68
    success: null
69
  })
70
}
71

72
const showTable = (req, res) => {
12✔
73
  const user = { id: req.session.userId, name: req.session.userName }
4✔
74
  const tables = getAllTables()
4✔
75
  const { tableName } = req.params
4✔
76

77
  if (!tables.includes(tableName)) return res.status(404).send('Table not found')
4✔
78

79
  const page = Math.max(1, parseInt(req.query.page) || 1)
3✔
80
  const offset = (page - 1) * PAGE_SIZE
3✔
81
  const search = (req.query.search || '').trim()
3✔
82
  const columns = getColumns(tableName)
3✔
83
  const fkOptions = getForeignKeyOptions(getForeignKeys(tableName))
3✔
84
  const inputTypes = getInputTypes(columns)
3✔
85

86
  let totalRows, rows
87
  if (tableName === 'consultations') {
3!
NEW
88
    const consultFrom = `
×
89
      FROM consultations c
90
      LEFT JOIN staff     stf ON c.lecturer_id = stf.staff_number
91
      LEFT JOIN students  st  ON c.organiser   = st.student_number
92
    `
NEW
93
    const courseInfo = `(
×
94
      SELECT cr.course_code || ' — ' || cr.course_name
95
      FROM courses cr
96
      JOIN staff_courses sc ON sc.course_code = cr.course_code
97
      JOIN enrollments   e  ON e.course_code  = cr.course_code
98
      WHERE sc.staff_number = c.lecturer_id AND e.student_number = c.organiser
99
      LIMIT 1
100
    ) AS course_info`
NEW
101
    const select = `SELECT c.*, c.rowid, stf.name AS lecturer_name, st.name AS organiser_name, ${courseInfo}`
×
NEW
102
    if (search) {
×
NEW
103
      const like = `%${search}%`
×
NEW
104
      const searchWhere = `WHERE (c.consultation_title LIKE ? OR c.consultation_date LIKE ?
×
105
        OR c.lecturer_id LIKE ? OR CAST(c.organiser AS TEXT) LIKE ? OR c.status LIKE ?)`
NEW
106
      const sp = [like, like, like, like, like]
×
NEW
107
      totalRows = db.prepare(`SELECT COUNT(*) as count ${consultFrom} ${searchWhere}`).get(...sp).count
×
NEW
108
      rows = db.prepare(`${select} ${consultFrom} ${searchWhere} ORDER BY c.consultation_date DESC LIMIT ? OFFSET ?`).all(...sp, PAGE_SIZE, offset)
×
109
    } else {
NEW
110
      totalRows = db.prepare(`SELECT COUNT(*) as count FROM consultations`).get().count
×
NEW
111
      rows = db.prepare(`${select} ${consultFrom} ORDER BY c.consultation_date DESC LIMIT ? OFFSET ?`).all(PAGE_SIZE, offset)
×
112
    }
113
  } else if (search) {
3✔
114
    const { whereClauses, params } = buildSearchQuery(columns, search)
1✔
115
    totalRows = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}" WHERE ${whereClauses}`).get(...params).count
1✔
116
    rows = db.prepare(`SELECT *, rowid as rowid FROM "${tableName}" WHERE ${whereClauses} LIMIT ? OFFSET ?`).all(...params, PAGE_SIZE, offset)
1✔
117
  } else {
118
    totalRows = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get().count
2✔
119
    rows = db.prepare(`SELECT *, rowid as rowid FROM "${tableName}" LIMIT ? OFFSET ?`).all(PAGE_SIZE, offset)
2✔
120
  }
121

122
  const totalPages = Math.max(1, Math.ceil(totalRows / PAGE_SIZE))
3✔
123

124
  res.render('admin-dashboard', {
3✔
125
    user,
126
    tables,
127
    activeTable: tableName,
128
    isReadOnly: READ_ONLY_TABLES.includes(tableName),
129
    columns,
130
    rows,
131
    page,
132
    totalPages,
133
    totalRows,
134
    search,
135
    fkOptions,
136
    inputTypes,
137
    error: req.query.error || null,
6✔
138
    success: req.query.success || null
6✔
139
  })
140
}
141

142
const createRecord = async (req, res) => {
12✔
143
  const tables = getAllTables()
11✔
144
  const { tableName } = req.params
11✔
145
  if (!tables.includes(tableName)) return res.status(404).send('Table not found')
11!
146
  if (READ_ONLY_TABLES.includes(tableName))
11✔
147
    return res.redirect(`/admin/table/${tableName}?error=This+table+is+read-only`)
7✔
148

149
  const columns = getColumns(tableName)
4✔
150
  const fields = columns.map(c => c.name)
8✔
151
  const values = fields.map(f => (req.body[f] !== '' && req.body[f] !== undefined) ? req.body[f] : null)
8✔
152
  const placeholders = fields.map(() => '?').join(', ')
8✔
153
  const fieldList = fields.map(f => `"${f}"`).join(', ')
8✔
154

155
  try {
4✔
156
    const result = db.prepare(`INSERT INTO "${tableName}" (${fieldList}) VALUES (${placeholders})`).run(...values)
4✔
157
    logAdminAudit({
2✔
158
      adminId: req.session.userId,
159
      action: 'INSERT',
160
      tableName,
161
      rowId: result.lastInsertRowid,
162
      newData: req.body
163
    })
164
    await logActivity(req.session.userId, ActionTypes.ADMIN_USER_ADD, [{ table: tableName, id: result.lastInsertRowid }])
2✔
165
    res.redirect(`/admin/table/${tableName}?success=Record+added`)
2✔
166
  } catch (err) {
167
    const fkOptions = getForeignKeyOptions(getForeignKeys(tableName))
2✔
168
    const inputTypes = getInputTypes(columns)
2✔
169
    const totalRows = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get().count
2✔
170
    const totalPages = Math.max(1, Math.ceil(totalRows / PAGE_SIZE))
2✔
171
    const rows = db.prepare(`SELECT *, rowid as rowid FROM "${tableName}" LIMIT ?`).all(PAGE_SIZE)
2✔
172
    res.render('admin-dashboard', {
2✔
173
      user: { id: req.session.userId, name: req.session.userName },
174
      tables,
175
      activeTable: tableName,
176
      columns,
177
      rows,
178
      page: 1,
179
      totalPages,
180
      totalRows,
181
      search: '',
182
      fkOptions,
183
      inputTypes,
184
      error: friendlyError(err.message),
185
      success: null
186
    })
187
  }
188
}
189

190
const updateRecord = async (req, res) => {
12✔
191
  const tables = getAllTables()
5✔
192
  const { tableName, rowId } = req.params
5✔
193
  if (!tables.includes(tableName)) return res.status(404).send('Table not found')
5!
194
  if (READ_ONLY_TABLES.includes(tableName))
5✔
195
    return res.redirect(`/admin/table/${tableName}?error=This+table+is+read-only`)
1✔
196

197
  const columns = getColumns(tableName)
4✔
198
  const updatable = columns.filter(c => c.pk === 0)
8✔
199
  if (updatable.length === 0) { return res.redirect(`/admin/table/${tableName}?error=This+table+has+no+editable+columns`) }
4!
200

201
  const existingRecord = db.prepare(`SELECT *, rowid FROM "${tableName}" WHERE rowid = ?`).get(rowId)
4✔
202
  if (!existingRecord)
4✔
203
    return res.redirect(`/admin/table/${tableName}?error=Record+not+found`)
1✔
204

205
  const setClauses = updatable.map(c => `"${c.name}" = ?`).join(', ')
3✔
206
  const values = [
3✔
207
    ...updatable.map(c => (req.body[c.name] !== '' && req.body[c.name] !== undefined) ? req.body[c.name] : null),
3✔
208
    rowId
209
  ]
210

211
  try {
3✔
212
    const result = db.prepare(`UPDATE "${tableName}" SET ${setClauses} WHERE rowid = ?`).run(...values)
3✔
213
    if (result.changes === 0)
2!
UNCOV
214
      return res.redirect(`/admin/table/${tableName}?error=Record+not+found`)
×
215
    logAdminAudit({
2✔
216
      adminId: req.session.userId,
217
      action: 'UPDATE',
218
      tableName,
219
      rowId,
220
      oldData: existingRecord,
221
      newData: req.body
222
    })
223
    await logActivity(req.session.userId, ActionTypes.ADMIN_USER_EDIT, [{ table: tableName, id: rowId }])
2✔
224
    res.redirect(`/admin/table/${tableName}?success=Record+updated`)
2✔
225
  } catch (err) {
226
    res.redirect(`/admin/table/${tableName}?error=${encodeURIComponent(friendlyError(err.message))}`)
1✔
227
  }
228
}
229

230
const deleteRecord = async (req, res) => {
12✔
231
  const tables = getAllTables()
6✔
232
  const { tableName, rowId } = req.params
6✔
233
  if (!tables.includes(tableName)) return res.status(404).send('Table not found')
6!
234
  if (tableName === 'admins') return res.redirect('/admin/table/admins?error=Admin+accounts+cannot+be+deleted')
6✔
235
  if (READ_ONLY_TABLES.includes(tableName))
5✔
236
    return res.redirect(`/admin/table/${tableName}?error=Audit+log+entries+cannot+be+deleted`)
1✔
237

238
  const existingRecord = db.prepare(`SELECT *, rowid FROM "${tableName}" WHERE rowid = ?`).get(rowId)
4✔
239
  if (!existingRecord)
4✔
240
    return res.redirect(`/admin/table/${tableName}?error=Record+not+found`)
1✔
241

242
  try {
3✔
243
    const result = db.prepare(`DELETE FROM "${tableName}" WHERE rowid = ?`).run(rowId)
3✔
244
    if (result.changes === 0)
2!
UNCOV
245
      return res.redirect(`/admin/table/${tableName}?error=Record+not+found`)
×
246
    logAdminAudit({
2✔
247
      adminId: req.session.userId,
248
      action: 'DELETE',
249
      tableName,
250
      rowId,
251
      oldData: existingRecord
252
    })
253
    await logActivity(req.session.userId, ActionTypes.ADMIN_USER_DELETE, [{ table: tableName, id: rowId }])
2✔
254
    res.redirect(`/admin/table/${tableName}?success=Record+deleted`)
2✔
255
  } catch (err) {
256
    res.redirect(`/admin/table/${tableName}?error=${encodeURIComponent(friendlyError(err.message))}`)
1✔
257
  }
258
}
259

260
module.exports = { showAdminDashboard, showTable, createRecord, updateRecord, deleteRecord }
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc