diff --git a/backoffice/index.js b/backoffice/index.js index 1f3f0169..b0dc5009 100644 --- a/backoffice/index.js +++ b/backoffice/index.js @@ -2,6 +2,7 @@ require('../environment') const { IDMClient } = require('./idm') const { EchoClient } = require('./echo') const hubspot = require('./hubspot') +const knex = require('../database/knex') const { PHASES, isValidPhase, isUserALearner, isUserActive, isUserInactive } = require('./util') module.exports = class BackOffice { @@ -78,7 +79,6 @@ module.exports = class BackOffice { }) } - getPhasesForUsers(users){ return this.echo.getPhasesForUsers(users) } diff --git a/database/commands.js b/database/commands.js index 9a794415..b22b0d0d 100644 --- a/database/commands.js +++ b/database/commands.js @@ -30,6 +30,37 @@ const setSkillCheck = ({user_id, label, checked, referrer}) => } }) +const logStatusUpdate = ({user_id, status}) => + knex + .insert({ + occurred_at: knex.fn.now(), + type: 'phase_4_status_update', + user_id, + metadata: {status}, + }) + .into('event_logs') + + +const setStatus = ({user_id, status}) => + logStatusUpdate({user_id, status}) + .then(() => { + const insert = knex('phase_4_status').insert({ + user_id, status, updated_at: knex.fn.now() + }).toString() + + const update = knex('phase_4_status') + .update({status}) + .where('phase_4_status.user_id', user_id) + + const query = util.format( + '%s ON CONFLICT (user_id) DO UPDATE SET %s', + insert.toString(), + update.toString().replace(/^update\s.*\sset\s/i, '') + ) + return knex.raw(query) + }) + module.exports = { setSkillCheck, + setStatus } diff --git a/database/migrations/20170926155324_phase_4_status.js b/database/migrations/20170926155324_phase_4_status.js new file mode 100644 index 00000000..c953ef23 --- /dev/null +++ b/database/migrations/20170926155324_phase_4_status.js @@ -0,0 +1,11 @@ + +exports.up = (knex, Promise) => + knex.schema.createTable('phase_4_status', table => { + table.string('user_id').notNullable().unique() + table.string('status').notNullable() + table.timestamp('updated_at') + }) + + +exports.down = (knex, Promise) => + knex.schema.dropTable('phase_4_status') diff --git a/database/queries.js b/database/queries.js index 2118404b..f04f2752 100644 --- a/database/queries.js +++ b/database/queries.js @@ -38,9 +38,16 @@ const hashChecksByLabel = checks => { return checkedMap } +const getPhase4Status = userIds => + knex + .select('*') + .from('phase_4_status') + .whereIn('user_id', userIds) + module.exports = { getChecksForUserAndLabels, getCheckLogsForUsers, + getPhase4Status, } diff --git a/web-server/assets/src/style/users.sass b/web-server/assets/src/style/users.sass index 7545037c..27ebe128 100644 --- a/web-server/assets/src/style/users.sass +++ b/web-server/assets/src/style/users.sass @@ -1,4 +1,14 @@ .users + &-status > * + display: inline-block + + &-status + margin-bottom: 15px + + &-status-content + font-size: 15px + margin: 0 20px + &-grid-controls padding-top: 0.25em text-align: right @@ -29,3 +39,5 @@ &-grid-member-avatar > img height: 10vh min-width: 10vh + &-display-inline + diplay: inline diff --git a/web-server/helpers.js b/web-server/helpers.js index 3178ce1c..deaf846f 100644 --- a/web-server/helpers.js +++ b/web-server/helpers.js @@ -186,6 +186,27 @@ module.exports = app => { }) } + request.getPhase4Users = function(){ + return this.backOffice.getAllUsers({ + includePhases: true, + phase: 4, + }) + } + + request.getPhase4UsersWithStatus = function(){ + return this.getPhase4Users() + .then(users => { + const userIds = users.map(user => user.id) + return queries.getPhase4Status(userIds).then(statuses => { + users.forEach(user => { + const status = statuses.find(status => status.user_id === user.id) + user.status = status ? status.status : '[no status found]' + }) + return users + }) + }) + } + next() }) } diff --git a/web-server/routes/phases.js b/web-server/routes/phases.js index 71899420..c1e460f0 100644 --- a/web-server/routes/phases.js +++ b/web-server/routes/phases.js @@ -1,4 +1,7 @@ +const bodyParser = require('body-parser') const queries = require('../../database/queries') +const commands = require('../../database/commands') +const urlEncodedBodyParser = bodyParser({ urlencoded: true }) module.exports = app => { @@ -6,7 +9,6 @@ module.exports = app => { response.renderMarkdownFile(`/phases/README.md`) }) - app.get('/phases/:phaseNumber', app.ensureTrailingSlash) app.use('/phases/:phaseNumber', (request, response, next) => { @@ -48,6 +50,24 @@ module.exports = app => { response.render('phases/goals', {title: 'Phase 3 Goals'}) }) + app.get('/phases/4/status', (request, response, next) => { + const userId = request.user.id + request.getPhase4UsersWithStatus() + .then(users => { + response.render('users/status', {title: 'Phase 4 Status', users, userId}) + }) + .catch(next) + }) + + app.post('/phases/4/status', urlEncodedBodyParser, (request, response, next) => { + const user_id = request.user.id + const {status} = request.body + commands.setStatus({user_id, status}) + .then(() => { + response.redirect('/phases/4/status') + }) + }) + app.use('/phases/:phaseNumber/dashboard', app.ensureAdmin) app.get('/phases/:phaseNumber/dashboard', (request, response, next) => { diff --git a/web-server/views/markdown.jade b/web-server/views/markdown.jade index 2ad2891a..b54783d0 100644 --- a/web-server/views/markdown.jade +++ b/web-server/views/markdown.jade @@ -13,6 +13,9 @@ block content a(href="/phases/#{phase.number}/skills") Skills li a(href="/phases/#{phase.number}/schedule") Schedule + if phase.number === 4 + li + a(href="/phases/4/status") Status if currentUser.isAdmin li a(href="/phases/#{phase.number}/dashboard") Dashboard diff --git a/web-server/views/users/status.jade b/web-server/views/users/status.jade new file mode 100644 index 00000000..3c7f9cec --- /dev/null +++ b/web-server/views/users/status.jade @@ -0,0 +1,27 @@ +extends ../layout + +block content + mixin usersList(_users) + for user in _users + .users-status + .users-grid-member.well(data-user=JSON.stringify(user)) + a(href='/users/'+user.handle) + .users-grid-member-name= user.name + .users-grid-member-handle= user.handle + .users-grid-member-avatar + img(src=user.avatarUrl) + p.users-status-content= user.status + if userId === user.id + button.btn.btn-primary(type='button' data-toggle='modal' data-target='#statusModal') Update Your Status + .modal.fade#statusModal(role='dialog') + .modal-dialog + .modal-content + .modal-header + button.close(type='button' data-dismiss='modal') × + h4.modal-title Update Your Status + .modal-body + form(action='/phases/4/status' method='post') + textarea(name='status' rows='6' cols='70') + button.btn.btn-default(type='submit') Update + h3 Phase #{phase.number} Status Reports + +usersList(users.filter(u => u.phase === 4)) \ No newline at end of file