diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml new file mode 100644 index 0000000..9a5f1d1 --- /dev/null +++ b/.github/workflows/CD.yml @@ -0,0 +1,30 @@ +name: EC2 Deploy + +on: + push: + branches: dev + +jobs: + Deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Deploy in EC2 + env: + PRIVATE_KEY: ${{ secrets.AWS_EC2_PRIVATE_KEY }} + HOSTNAME : ${{ secrets.AWS_EC2_HOSTNAME }} + USER: ${{ secrets.AWS_EC2_USER }} + + run: | + echo "$PRIVATE_KEY" > private_key.pem && chmod 600 private_key.pem + ssh -o StrictHostKeyChecking=no -i private_key.pem ${USER}@${HOSTNAME} ' + + #Now we have got the access of EC2 and we will start the deploy . + cd ~/aiss-backend && + git checkout dev && + git stash && + git pull origin dev && + yarn && + pm2 restart app.js + ' diff --git a/app.js b/app.js index 925d5cc..1b6f518 100644 --- a/app.js +++ b/app.js @@ -14,6 +14,7 @@ const email = require('./routes/nodeMailer'); const app = express(); const catalogRouter = require('./routes/catalog'); +const dayRouter = require('./routes/day'); const PORT = process.env.PORT || 3001; @@ -33,6 +34,7 @@ app.use('/users', users); app.use('/catalog', catalogRouter); app.use('/nodeMailer', email); app.use('/auth', authRouter); +app.use('/day', dayRouter); app.listen(PORT, () => { console.log(`Server listening on ${PORT}`); diff --git a/common/utils.js b/common/utils.js index 14fad66..76a57a4 100644 --- a/common/utils.js +++ b/common/utils.js @@ -20,6 +20,44 @@ const isISODate = (str) => { } }; +// dependency for catalog.js +const isInteger = (value) => { + return value && /^\d+$/.test(value); +}; + +// dependency for publishedSchedule.js +const calculateYear = (eventDate, gradeLevel) => { + const currentDay = new Date(eventDate); + if (gradeLevel && gradeLevel.length === 1) { + // console.log('current day', currentDay.getFullYear() + (currentDay.getMonth() >= 7 ? 2 : 1)); + if (gradeLevel[0].toLowerCase() === 'junior') { + // if the current month is august or later + // then junior will be current year + 2 + // otherwise junior will be current year + 1 + // months are zero indexed + return [(currentDay.getFullYear() + (currentDay.getMonth() >= 7 ? 2 : 1)).toString(10)]; + } + if (gradeLevel[0].toLowerCase() === 'senior') { + // if the current month is august or later + // then senior will be current year + 1 + // otherwise senior will be current year + return [(currentDay.getFullYear() + (currentDay.getMonth() >= 7 ? 1 : 0)).toString(10)]; + } + if (gradeLevel[0].toLowerCase() === 'both') { + return [ + (currentDay.getFullYear() + (currentDay.getMonth() >= 7 ? 1 : 0)).toString(10), + (currentDay.getFullYear() + (currentDay.getMonth() >= 7 ? 2 : 1)).toString(10), + ]; + } + } else if (gradeLevel && gradeLevel.length > 1) { + return [ + (currentDay.getFullYear() + (currentDay.getMonth() >= 7 ? 1 : 0)).toString(10), + (currentDay.getFullYear() + (currentDay.getMonth() >= 7 ? 2 : 1)).toString(10), + ]; + } + return []; +}; + const isObject = (o) => { return o === Object(o) && !isArray(o) && typeof o !== 'function' && !isISODate(o); }; @@ -34,7 +72,8 @@ const keysToCamel = (data) => { }); return newData; } - if (isArray(data)) { + if (isArray(data) && data.length) { + // console.log(data) return data.map((i) => { return keysToCamel(i); }); @@ -45,11 +84,46 @@ const keysToCamel = (data) => { data[0] === '{' && data[data.length - 1] === '}' ) { - let parsedList = data.replaceAll('"', ''); - parsedList = parsedList.slice(1, parsedList.length - 1).split(','); - return parsedList; + if (data.length > 2) { + let parsedList = data.replaceAll('"', ''); + parsedList = parsedList.slice(1, parsedList.length - 1).split(','); + return parsedList; + } + return []; } return data; }; -module.exports = { keysToCamel }; +const getSeasonFromMonthAndYear = (month, year) => { + // spring + // march-may -> winter [year] + if (month >= 0 && month <= 4) { + return `Spring ${year}`; + } + // summer + // june-august -> summer [year] + if (month >= 5 && month <= 7) { + return `Summer ${year}`; + } + // fall + // september-november -> fall [year] + return `Fall ${year}`; +}; + +const getMonthRangeFromSeason = (season) => { + if (season === 'spring') { + return [0, 4]; + } + if (season === 'summer') { + return [5, 7]; + } + return [8, 12]; +}; + +module.exports = { + keysToCamel, + isInteger, + calculateYear, + getSeasonFromMonthAndYear, + getMonthRangeFromSeason, +}; diff --git a/routes/catalog.js b/routes/catalog.js index 6d914b2..6208087 100644 --- a/routes/catalog.js +++ b/routes/catalog.js @@ -1,15 +1,75 @@ const express = require('express'); - const { db } = require('../server/db'); const catalogRouter = express.Router(); -const { keysToCamel } = require('../common/utils'); +const { keysToCamel, isInteger } = require('../common/utils'); // -- GET - Returns all data from the catalog table catalogRouter.get('/', async (req, res) => { try { - const allInfo = await db.query(`SELECT * from catalog;`); - res.status(200).json(keysToCamel(allInfo)); + const { title, eventType, subject, season, year } = req.query; + + let { limit, page } = req.query; + limit = isInteger(limit) ? parseInt(limit, 10) : 10; + page = isInteger(page) ? parseInt(page, 10) : 1; + + const offset = (page - 1) * limit; + + let query = 'FROM catalog WHERE 1=1 AND hidden = false'; + // removed space at beginning here + + const params = []; + + if (title) { + query += ' AND (title ILIKE $1'; + query += ' OR host ILIKE $1'; + query += ' OR description ILIKE $1) '; + params.push(`%${title}%`); + } else { + params.push(''); + } + + if (subject) { + const array = subject.split(','); + query += ' AND subject && $2::subject[]'; + params.push(array); + } else { + params.push(''); + } + + if (eventType) { + const array = eventType.split(','); + query += ' AND event_type && $3::event[]'; + params.push(array); + } else { + params.push(''); + } + + if (season) { + const array = season.split(','); + query += ' AND season && $4::season[]'; + params.push(array); + } else { + params.push(''); + } + + if (year) { + const array = year.split(','); + query += ' AND year && $5::year[]'; + params.push(array); + } else { + params.push(''); + } + + const eventCount = await db.query(`SELECT COUNT(*) ${query};`, params); + + query += ' ORDER BY title ASC LIMIT $6 OFFSET $7;'; + params.push(limit); + params.push(offset); + + const reqInfo = await db.query(`SELECT * ${query}`, params); + + res.status(200).json(keysToCamel({ events: reqInfo, count: eventCount })); } catch (err) { res.status(500).send(err.message); } @@ -19,8 +79,8 @@ catalogRouter.get('/', async (req, res) => { catalogRouter.get('/:id', async (req, res) => { try { const { id } = req.params; - const allUsers = await db.query(`SELECT * FROM catalog WHERE id = $1;`, [id]); - res.status(200).json(keysToCamel(allUsers)); + const response = await db.query(`SELECT * FROM catalog WHERE id = $1;`, [id]); + res.status(200).json(keysToCamel(response)); } catch (err) { res.status(500).send(err.message); } @@ -28,13 +88,13 @@ catalogRouter.get('/:id', async (req, res) => { // -- POST - Adds a new row to the catalog table catalogRouter.post('/', async (req, res) => { - const { host, title, eventType, subject, description, year, season, location } = req.body; + const { host, title, eventType, subject, description, year, season } = req.body; try { const returnedData = await db.query( - `INSERT INTO catalog (id, host, title, event_type, subject, description, year, season, location) - VALUES (nextval('catalog_id_seq'), $1, $2, $3, $4, $5, $6, $7, $8) + `INSERT INTO catalog (id, host, title, event_type, subject, description, year, season, hidden) + VALUES (nextval('catalog_id_seq'), $1, $2, $3::event[], $4::subject[], $5, $6::year[], $7::season[], false) RETURNING id;`, - [host, title, eventType, subject, description, year, season, location], + [host, title, eventType, subject, description, year, season], ); res.status(201).json({ id: returnedData[0].id, status: 'Success' }); } catch (err) { @@ -50,34 +110,46 @@ catalogRouter.post('/', async (req, res) => { catalogRouter.put('/:id', async (req, res) => { try { const { id } = req.params; - const { host, title, eventType, subject, description, year, location, season } = req.body; - - const updatedCatalog = await db.query( - `UPDATE catalog SET - ${host ? 'host = $(host), ' : ''} - ${title ? 'title = $(title),' : ''} - ${eventType ? 'event_type = $(eventType), ' : ''} - ${subject ? 'subject = $(subject), ' : ''} - ${description ? 'description = $(description), ' : ''} - ${year ? 'year = $(year), ' : ''} - ${location ? 'location = $(location), ' : ''} - ${season ? 'season = $(season), ' : ''} - id = '${id}' - WHERE id = '${id}' + const { host, title, eventType, subject, description, year, season } = req.body; + + const { count } = ( + await db.query(`SELECT COUNT(*) FROM published_schedule WHERE event_id = $1;`, [id]) + )[0]; + + if (count === 1) { + const updatedCatalog = await db.query( + `UPDATE catalog SET + ${host ? 'host = $(host), ' : ''} + ${title ? 'title = $(title),' : ''} + ${eventType ? 'event_type = $(eventType)::event[], ' : ''} + ${subject ? 'subject = $(subject)::subject[], ' : ''} + ${description ? 'description = $(description), ' : ''} + ${year ? 'year = $(year)::year[], ' : ''} + ${season ? 'season = $(season)::season[], ' : ''} + id = '${id}' + WHERE id = '${id}' + RETURNING *;`, + { + host, + title, + eventType, + subject, + description, + year, + id, + season, + }, + ); + res.status(200).send(keysToCamel(updatedCatalog)); + } else { + const newCatalogEvent = await db.query( + `INSERT INTO catalog (id, host, title, event_type, subject, description, year, season, hidden) + VALUES (nextval('catalog_id_seq'), $1, $2, $3::event[], $4::subject[], $5, $6::year[], $7::season[], false) RETURNING *;`, - { - host, - title, - eventType, - subject, - description, - year, - id, - location, - season, - }, - ); - res.status(200).send(keysToCamel(updatedCatalog)); + [host, title, eventType, subject, description, year, season], + ); + res.status(200).send(keysToCamel(newCatalogEvent)); + } } catch (err) { res.status(500).send(err.message); } @@ -87,8 +159,14 @@ catalogRouter.put('/:id', async (req, res) => { catalogRouter.delete('/:id', async (req, res) => { try { const { id } = req.params; - const delUser = await db.query(`DELETE FROM catalog WHERE id = $1 RETURNING *;`, [id]); - res.status(200).send(keysToCamel(delUser)); + const inUse = await db.query(`SELECT * FROM published_schedule WHERE event_id = $1;`, [id]); + let hidden; + if (inUse && inUse.length) { + hidden = await db.query(`UPDATE catalog SET hidden = true WHERE id = $1 RETURNING *;`, [id]); + } else { + hidden = await db.query(`DELETE FROM catalog WHERE id = $1 RETURNING *;`, [id]); + } + res.status(200).send(keysToCamel(hidden)); } catch (err) { res.status(500).send(err.message); } diff --git a/routes/day.js b/routes/day.js new file mode 100644 index 0000000..9f9b469 --- /dev/null +++ b/routes/day.js @@ -0,0 +1,125 @@ +const express = require('express'); +const { db } = require('../server/db'); +const { keysToCamel } = require('../common/utils'); + +const dayRouter = express.Router(); + +// GET - returns all days in the table +dayRouter.get('/', async (req, res) => { + try { + const allDays = await db.query(`SELECT * FROM day;`); + res.status(200).json(keysToCamel(allDays)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// GET - returns a day by eventDate parameter +dayRouter.get('/date', async (req, res) => { + try { + const { eventDate } = req.query; + const dayByDate = await db.query(`SELECT * FROM day WHERE event_date = $1;`, [eventDate]); + res.status(200).json(keysToCamel(dayByDate)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// GET - returns a day by id +dayRouter.get('/:id', async (req, res) => { + try { + const { id } = req.params; + const dayById = await db.query(`SELECT * FROM day WHERE id = $1;`, [id]); + res.status(200).json(keysToCamel(dayById)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// POST - creates a new day +dayRouter.post('/', async (req, res) => { + try { + const { eventDate, location, notes } = req.body; + const existingDay = await db.query(`SELECT * FROM day WHERE event_date = $1;`, [eventDate]); + if (existingDay.length) { + // day exists but has no events --> update location + notes return existing day_id + if (existingDay[0].day_count === 0) { + await db.query(`UPDATE day SET location = $1, notes = $2 WHERE id = $3;`, [ + location, + notes, + existingDay[0].id, + ]); + res.status(201).json({ + status: 'Success', + id: existingDay[0].id, + }); + } else { + res.status(201).json({ + status: 'Failed', + message: 'Day already exists', + }); + } + return; + } + + const newDay = await db.query( + ` + INSERT INTO day ( + id, + event_date, + start_time, + end_time, + location, + notes, + day_count + ) VALUES ( + nextval('day_id_seq'), $1, '23:59:59', '00:00:00', $2, $3, 0 + ) RETURNING id; + `, + [eventDate, location, notes], + ); + res.status(201).json({ + status: 'Success', + id: newDay[0].id, + }); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// PUT - modifies an existing day +dayRouter.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const { eventDate, startTime, endTime, location, notes } = req.body; + const updatedDay = await db.query( + ` + UPDATE day + SET + event_date = COALESCE($1, event_date), + start_time = COALESCE($2, start_time), + end_time = COALESCE($3, end_time), + location = COALESCE($4, location), + notes = COALESCE($5, notes) + WHERE id = $6 + RETURNING *; + `, + [eventDate, startTime, endTime, location, notes, id], + ); + res.status(200).send(keysToCamel(updatedDay)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// DELETE - deletes an existing day +dayRouter.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + const deletedDay = await db.query(`DELETE FROM day WHERE id = $1 RETURNING *;`, [id]); + res.status(200).send(keysToCamel(deletedDay)); + } catch (err) { + res.status(500).send(err.message); + } +}); +module.exports = dayRouter; diff --git a/routes/publishedSchedule.js b/routes/publishedSchedule.js index 6793999..099c46c 100644 --- a/routes/publishedSchedule.js +++ b/routes/publishedSchedule.js @@ -1,6 +1,11 @@ const express = require('express'); const { db } = require('../server/db'); -const { keysToCamel } = require('../common/utils'); +const { + keysToCamel, + calculateYear, + getSeasonFromMonthAndYear, + getMonthRangeFromSeason, +} = require('../common/utils'); const publishedScheduleRouter = express.Router(); @@ -11,6 +16,7 @@ publishedScheduleRouter.get('/', async (req, res) => { ` SELECT PS.id, + PS.day_id, C.host, C.title, PS.confirmed, @@ -18,7 +24,8 @@ publishedScheduleRouter.get('/', async (req, res) => { PS.start_time, PS.end_time, PS.cohort, - PS.notes + PS.notes, + PS.created_on FROM published_schedule PS LEFT JOIN catalog C ON PS.event_id = C.id; @@ -30,6 +37,301 @@ publishedScheduleRouter.get('/', async (req, res) => { } }); +// GET/published-schedule/recently-added - returns the rows that were added in the past week +publishedScheduleRouter.get('/recently-added', async (req, res) => { + try { + const recentAddResult = await db.query( + ` + SELECT + PS.id, + C.title, + C.event_type, + C.year, + PS.start_time, + PS.end_time, + PS.confirmed, + PS.confirmed_on, + PS.cohort, + PS.notes, + PS.created_on + FROM published_schedule PS + LEFT JOIN catalog C ON PS.event_id = C.id + WHERE PS.created_on > current_date - 7 AND confirmed = true + ORDER BY created_on DESC; + `, + ); + res.status(200).json(keysToCamel(recentAddResult)); + } catch (err) { + res.status(400).send(err.message); + } +}); + +// GET/published-schedule/recently-confirmed - returns the rows that were confirmed in the past week +publishedScheduleRouter.get('/recently-confirmed', async (req, res) => { + try { + const recentConfirm = await db.query( + ` + SELECT + PS.id, + C.title, + C.event_type, + C.year, + PS.start_time, + PS.end_time, + PS.confirmed, + PS.confirmed_on, + PS.cohort, + PS.notes, + PS.created_on + FROM published_schedule PS + LEFT JOIN catalog C ON PS.event_id = C.id + WHERE PS.confirmed_on > current_date - 7 + ORDER BY created_on DESC; + `, + ); + res.status(200).json(keysToCamel(recentConfirm)); + } catch (err) { + res.status(400).send(err.message); + } +}); + +// GET /published-schedule/all-seasons - return all the seasons +publishedScheduleRouter.get('/all-seasons', async (req, res) => { + const getSeason = (date) => { + const formattedDate = new Date(date.event_date); + const year = formattedDate.getFullYear(); + const month = formattedDate.getMonth(); + return getSeasonFromMonthAndYear(month, year); + }; + + try { + const allDatesResult = await db.query( + ` + SELECT D.event_date + FROM + published_schedule AS PS, day AS D + WHERE + D.id = PS.day_id + ORDER BY + D.event_date DESC; + `, + ); + const allUniqueSeasonsResult = []; + + // Get all unique seasons by order of season, from most recent to least + allDatesResult.forEach((row) => { + const season = getSeason(row); + if (!allUniqueSeasonsResult.includes(season)) { + allUniqueSeasonsResult.push(season); + } + }); + + res.status(200).json(keysToCamel(allUniqueSeasonsResult)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// GET /published-schedule/season - returns rows that match the season +publishedScheduleRouter.get('/season', async (req, res) => { + try { + let startTime; + let endTime; + + const { season, year } = req.query; + + // getting the intervals for each season + if (season.toLowerCase() === 'spring') { + startTime = `${year}-01-01`; + endTime = `${year}-05-31`; + } else if (season.toLowerCase() === 'summer') { + startTime = `${year}-06-01`; + endTime = `${year}-08-31`; + } else { + startTime = `${year}-09-01`; + endTime = `${year}-12-31`; + } + + const seasonResult = await db.query( + ` + WITH seasonPS AS + ( + SELECT + PS.id, + PS.event_id, + PS.day_id, + D.id AS day_day_id, + D.event_date, + D.start_time AS day_start_time, + D.end_time AS day_end_time, + D.location, + D.notes AS day_notes, + C.title, + C.event_type, + C.year, + C.description, + PS.start_time, + PS.end_time, + PS.confirmed, + PS.confirmed_on, + PS.cohort, + PS.notes, + PS.created_on + FROM published_schedule PS + LEFT JOIN catalog C ON PS.event_id = C.id + LEFT JOIN day D on PS.day_id = D.id + WHERE + D.event_date >= $1::date AND D.event_date <= $2::date + AND D.id = PS.day_id + ORDER BY PS.start_time ASC + ) + SELECT event_date, + json_build_object ( + 'id', seasonPS.day_id, + 'event_date', seasonPS.event_date, + 'start_time', seasonPS.day_start_time, + 'end_time', seasonPS.day_end_time, + 'location', seasonPS.location, + 'notes', seasonPS.day_notes + ) as day, + JSON_AGG( + json_build_object ( + 'id', seasonPS.id, + 'event_id', seasonPS.event_id, + 'title', seasonPS.title, + 'description', seasonPS.description, + 'event_type', seasonPS.event_type, + 'year', seasonPS.year, + 'start_time', seasonPS.start_time, + 'end_time', seasonPS.end_time, + 'confirmed', seasonPS.confirmed, + 'confirmed_on', seasonPS.confirmed_on, + 'cohort', seasonPS.cohort, + 'notes', seasonPS.notes + ) + ) AS data + FROM seasonPS + GROUP BY event_date, day_id, day_start_time, day_end_time, location, day_notes + ORDER BY event_date ASC; + `, + [startTime, endTime], + ); + res.status(200).json(keysToCamel(seasonResult)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// GET /published-schedule/stats - returns stats of event types and subjects for a specific season +publishedScheduleRouter.get('/stats', async (req, res) => { + try { + const { season, year } = req.query; + const [monthStart, monthEnd] = getMonthRangeFromSeason(season); + + const statResult = await db.query( + ` + WITH all_event_types AS ( + SELECT DISTINCT unnest(event_type) AS event_type + FROM catalog + ), + all_subjects AS ( + SELECT DISTINCT unnest(subject) AS subject + FROM catalog + ), + all_permutations AS ( + SELECT aet.event_type, asu.subject + FROM all_event_types aet + CROSS JOIN all_subjects asu + ) + SELECT + COALESCE(ap.event_type::text, 'Total') AS event_type, + COALESCE(ap.subject::text, 'Total') AS subject, + COALESCE(COUNT(DISTINCT ps_event_id), 0) AS total_count + FROM all_permutations ap + LEFT JOIN ( + SELECT *, + ps.day_id AS ps_day_id, + ps.id AS ps_event_id + FROM catalog c + JOIN published_schedule ps ON c.id = ps.event_id + JOIN day d ON PS.day_id = d.id + WHERE EXTRACT(MONTH FROM d.event_date) >= $1 + AND EXTRACT(MONTH FROM d.event_date) <= $2 + AND EXTRACT(YEAR FROM d.event_date) = $3 + ) c ON ap.event_type = ANY(c.event_type) AND ap.subject = ANY(c.subject) + GROUP BY ROLLUP (ap.event_type), ROLLUP (ap.subject) + ORDER BY CASE WHEN ap.event_type IS NULL THEN 1 ELSE 0 END, + CASE WHEN ap.subject IS NULL THEN 1 ELSE 0 END, + ap.event_type NULLS FIRST, + ap.subject NULLS FIRST; + `, + [monthStart, monthEnd, year], + ); + + res.status(200).json(keysToCamel(statResult)); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// GET /published-schedule/date - returns all events occurring on a specific date +publishedScheduleRouter.get('/dayId', async (req, res) => { + try { + const { dayId } = req.query; + + const seasonResult = await db.query( + ` + SELECT + json_build_object( + 'id', D.id, + 'event_date', D.event_date, + 'start_time', D.start_time, + 'end_time', D.end_time, + 'location', D.location, + 'notes', D.notes + ) AS day_data, + JSON_AGG( + json_build_object ( + 'id', PS.id, + 'day_id', PS.day_id, + 'title', C.title, + 'event_type', C.event_type, + 'year', C.year, + 'host', C.host, + 'start_time', PS.start_time, + 'end_time', PS.end_time, + 'confirmed', PS.confirmed, + 'confirmed_on', PS.confirmed_on, + 'cohort', PS.cohort, + 'notes', PS.notes + ) + ) AS data + FROM day D + LEFT JOIN published_schedule PS ON PS.day_id = D.id + LEFT JOIN catalog C ON PS.event_id = C.id + WHERE D.id = $1 + GROUP BY d.event_date, d.id + ORDER BY d.event_date; + `, + [dayId], + ); + + seasonResult[0].data.sort((a, b) => { + if (a.start_time < b.start_time) { + return -1; + } + if (a.start_time > b.start_time) { + return 1; + } + return 0; + }); + + res.status(200).json(keysToCamel(seasonResult)[0]); + } catch (err) { + res.status(500).send(err.message); + } +}); + // GET/:id - returns the rows that match the given id publishedScheduleRouter.get('/:id', async (req, res) => { try { @@ -38,14 +340,22 @@ publishedScheduleRouter.get('/:id', async (req, res) => { ` SELECT PS.id, + PS.day_id, + PS.event_id, C.host, C.title, + C.event_type, + C.season, + C.subject, + C.year, + C.description, PS.confirmed, PS.confirmed_on, PS.start_time, PS.end_time, PS.cohort, - PS.notes + PS.notes, + PS.created_on FROM published_schedule PS LEFT JOIN catalog C ON PS.event_id = C.id @@ -60,27 +370,56 @@ publishedScheduleRouter.get('/:id', async (req, res) => { }); // POST - Adds a new row to the published_schedule table +// NOTE: there is a requirement that the day already exist, +// as that is how we are able to calculate the cohort from the event date publishedScheduleRouter.post('/', async (req, res) => { - const { eventId, confirmed, confirmedOn, startTime, endTime, cohort, notes } = req.body; + const currDate = new Date(); + const { eventId, dayId, confirmed, startTime, endTime, cohort, notes } = req.body; try { + const dayResult = await db.query( + `UPDATE day SET day_count = day_count + 1 WHERE id = $1 RETURNING *;`, + [dayId], + ); + const { eventDate } = dayResult ? keysToCamel(dayResult[0]) : null; + await db.query( + ` + UPDATE day + SET + start_time = CASE WHEN $1 < start_time THEN $1 ELSE start_time END, + end_time = CASE WHEN $2 > end_time THEN $2 ELSE end_time END + WHERE id = $3; + `, + [startTime, endTime, dayId], + ); const returnedData = await db.query( ` INSERT INTO published_schedule ( - id, event_id, + day_id, confirmed, confirmed_on, start_time, end_time, cohort, - notes + notes, + created_on ) VALUES - (nextval('published_schedule_id_seq'), $1, $2, $3, $4, $5, $6, $7) - RETURNING id; + ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id, created_on; `, - [eventId, confirmed, confirmedOn, startTime, endTime, cohort, notes], + [ + eventId, + dayId, + confirmed, + null, + startTime, + endTime, + calculateYear(eventDate, cohort), + notes, + currDate, + ], ); res.status(201).json({ status: 'Success', @@ -92,27 +431,101 @@ publishedScheduleRouter.post('/', async (req, res) => { }); // PUT/:id - Updates an existing row given an id +// NOTE: there is a requirement that the selected DAY already exist; this is how +// we are able to grab the event day from the day table for use in the cohort +// NOTE: if the day that you're moving the event FROM will have 0 associated events, +// IT WILL BE DELETED publishedScheduleRouter.put('/:id', async (req, res) => { try { const { id } = req.params; - const { eventId, confirmed, confirmedOn, startTime, endTime, cohort, notes } = req.body; + + const { eventId, dayId, confirmed, confirmedOn, startTime, endTime, cohort, notes, createdOn } = + req.body; + + // get the current day from the PS table + const psDayIdResult = keysToCamel( + await db.query(`SELECT day_id FROM published_schedule WHERE id = $1;`, id), + )[0]; + // extract the old day id + const psDayId = psDayIdResult.dayId; + // now we need to grab the data from the table (should use dayId unless it is null) + const dayResult = keysToCamel( + await db.query(`SELECT * FROM day WHERE id = $1;`, [dayId || psDayId]), + )[0]; + // grab the eventDate so that you can set the years + const { eventDate } = dayResult; + // update the PS + const updatedPublishedSchedule = await db.query( ` UPDATE published_schedule SET event_id = COALESCE($1, event_id), - confirmed = COALESCE($2, confirmed), - confirmed_on = COALESCE($3, confirmed_on), - start_time = COALESCE($4, start_time), - end_time = COALESCE($5, end_time), - cohort = COALESCE($6, cohort), - notes = COALESCE($7, notes) - WHERE id = $8 + day_id = COALESCE($2, day_id), + confirmed = COALESCE($3, confirmed), + confirmed_on = COALESCE($4, confirmed_on), + start_time = COALESCE($5, start_time), + end_time = COALESCE($6, end_time), + cohort = COALESCE($7, cohort), + notes = COALESCE($8, notes), + created_on = COALESCE($9, created_on) + + WHERE id = $10 RETURNING *; `, - [eventId, confirmed, confirmedOn, startTime, endTime, cohort, notes, id], + [ + eventId, + dayId, + confirmed, + confirmedOn, + startTime, + endTime, + cohort ? calculateYear(eventDate, cohort) : cohort, + notes, + createdOn, + id, + ], ); + // if day was modified we need to query and reset the min/max + if (dayId) { + if (startTime) { + await db.query( + `UPDATE day SET start_time = (SELECT MIN(start_time) FROM published_schedule WHERE day_id = $1) WHERE id = $1; + UPDATE day SET start_time = (SELECT MIN(start_time) FROM published_schedule WHERE day_id = $2) WHERE id = $2;`, + [psDayId, dayId], + ); + } + if (endTime) { + await db.query( + `UPDATE day SET end_time = (SELECT MAX(end_time) FROM published_schedule WHERE day_id = $1) WHERE id = $1; + UPDATE day SET end_time = (SELECT MAX(end_time) FROM published_schedule WHERE day_id = $2) WHERE id = $2;`, + [psDayId, dayId], + ); + } + const dayCountResult = await db.query( + `UPDATE day SET day_count = day_count + 1 WHERE id = $1; UPDATE day SET day_count = day_count - 1 WHERE id = $2 RETURNING day_count;`, + [dayId, psDayId], + ); + const { dayCount } = keysToCamel(dayCountResult); + // if start time was passed alongside day we need to update the old day and change the new day + if (dayCount === 0) { + await db.query(`DELETE FROM day WHERE id = $1`, [psDayId]); + } + } else { + if (startTime) { + await db.query( + `UPDATE day SET start_time = (SELECT MIN(start_time) FROM published_schedule WHERE day_id = $1) WHERE id = $1;`, + [psDayId], + ); + } + if (endTime) { + await db.query( + `UPDATE day SET end_time = (SELECT MAX(end_time) FROM published_schedule WHERE day_id = $1) WHERE id = $1;`, + [psDayId], + ); + } + } res.status(200).json(keysToCamel(updatedPublishedSchedule)); } catch (err) { res.status(500).send(err.message); @@ -120,9 +533,12 @@ publishedScheduleRouter.put('/:id', async (req, res) => { }); // DELETE/:id - deletes an existing row given an id +// NOTE: if the day that you're deleting the event FROM will have 0 associated events, +// IT WILL BE DELETED publishedScheduleRouter.delete('/:id', async (req, res) => { try { const { id } = req.params; + // delete PS entry const deletedEntry = await db.query( ` DELETE FROM published_schedule @@ -130,7 +546,36 @@ publishedScheduleRouter.delete('/:id', async (req, res) => { `, [id], ); - res.status(200).send(deletedEntry); + // grab relevant info from the deleted row + const { dayId, startTime, endTime } = keysToCamel(deletedEntry[0]); + + // update the day table + const updatedDay = await db.query( + `UPDATE day SET day_count = day_count - 1 WHERE id = $1 RETURNING *;`, + [dayId], + ); + const dayResult = keysToCamel(updatedDay[0]); + const { dayCount } = dayResult; + // if the day has 0 events delete day + if (dayCount === 0) { + await db.query(`DELETE FROM day WHERE id = $1`, [dayId]); + } else { + // if the event start time was the earliest change to earliest in PS table for that day + if (startTime === dayResult.startTime) { + await db.query( + `UPDATE day SET start_time = (SELECT MIN(start_time) FROM published_schedule WHERE day_id = $1) WHERE id = $1`, + [dayId], + ); + } + // if the event end time was the latest change to latest in PS table for that day + if (endTime === dayResult.endTime) { + await db.query( + `UPDATE day SET end_time = (SELECT MAX(end_time) FROM published_schedule WHERE day_id = $1) WHERE id = $1`, + [dayId], + ); + } + } + res.status(200).send(keysToCamel(deletedEntry)); } catch (err) { res.status(500).send(err.message); } diff --git a/routes/users.js b/routes/users.js index 3fc3702..f030500 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,5 +1,5 @@ const express = require('express'); -const { keysToCamel } = require('../common/utils'); +const { keysToCamel, isInteger } = require('../common/utils'); const { db } = require('../server/db'); const userRouter = express.Router(); @@ -28,22 +28,78 @@ userRouter.get('/:uid', async (req, res) => { userRouter.get('/pending-accounts', async (req, res) => { try { - const pendingAccounts = await db.query(`SELECT * FROM users WHERE approved = FALSE;`); + const { accountType } = req.query; + let queryString = 'SELECT * FROM users WHERE approved = FALSE '; + if (accountType === 'admin') { + queryString += `AND type = 'admin'`; + } else if (accountType === 'student') { + queryString += `AND type = 'student'`; + } + const pendingAccounts = await db.query(`${queryString} ORDER BY first_name ASC;`); res.status(200).json(keysToCamel(pendingAccounts)); } catch (err) { res.status(500).send(err.message); } }); +userRouter.get('/approved-accounts', async (req, res) => { + try { + const { keyword, accountType } = req.query; + let { page, limit } = req.query; + page = isInteger(page) ? parseInt(page, 10) : 1; + limit = isInteger(limit) ? parseInt(limit, 10) : 10; + const offset = (page - 1) * limit; + let queryString = 'FROM users WHERE approved = TRUE '; + if (accountType === 'admin') { + queryString += `AND type = 'admin'`; + } else if (accountType === 'student') { + queryString += `AND type = 'student'`; + } + let approvedAccounts; + let userCount; + let params = []; + if (keyword) { + params = [`%${keyword}%`, `%${keyword}%`, `%${keyword}%`, `%${keyword}%`, limit, offset]; + approvedAccounts = await db.query( + `SELECT * ${queryString} AND (first_name ILIKE $1 OR last_name ILIKE $2 OR email ILIKE $3 OR CONCAT(first_name, ' ', last_name) ILIKE $4) ORDER BY first_name ASC LIMIT $5 OFFSET $6;`, + params, + ); + userCount = await db.query( + `SELECT COUNT(*) ${queryString} AND (first_name ILIKE $1 OR last_name ILIKE $2 OR email ILIKE $3 OR CONCAT(first_name, ' ', last_name) ILIKE $4);`, + params, + ); + } else { + params = [limit, offset]; + approvedAccounts = await db.query( + `SELECT * ${queryString} ORDER BY first_name ASC LIMIT $1 OFFSET $2;`, + params, + ); + userCount = await db.query(`SELECT COUNT(*) ${queryString};`, params); + } + res.status(200).json(keysToCamel({ accounts: approvedAccounts, count: userCount })); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// logInWithEmailAndPassword() needs to get specific user id +userRouter.get('/:uid', async (req, res) => { + try { + const { uid } = req.params; + const user = await db.query(`SELECT * FROM users WHERE id = $1;`, [uid]); + res.status(200).json(keysToCamel(user)); + } catch (err) { + res.status(500).send(err.message); + } +}); + userRouter.post('/create', async (req, res) => { try { - const { id, email, type, approved } = req.body; - await db.query(`INSERT INTO users (id, email, "type", approved) VALUES ($1, $2, $3, $4);`, [ - id, - email, - type, - approved, - ]); + const { id, email, type, approved, approvedOn, firstName, lastName } = req.body; + await db.query( + `INSERT INTO users (id, email, "type", approved, approved_on, first_name, last_name) VALUES ($1, $2, $3, $4, $5, $6, $7);`, + [id, email, type, approved, approvedOn, firstName, lastName], + ); res.status(201).json({ id, }); @@ -59,9 +115,10 @@ userRouter.post('/create', async (req, res) => { userRouter.put('/approve/:uid', async (req, res) => { try { const { uid } = req.params; + const currDate = new Date(); const updatedApproval = await db.query( - `UPDATE users SET approved = TRUE WHERE id = $1 RETURNING *;`, - [uid], + `UPDATE users SET approved = TRUE, approved_on = $1 WHERE id = $2 RETURNING *;`, + [currDate, uid], ); return res.status(200).send(keysToCamel(updatedApproval)); } catch (err) { diff --git a/server/schema/catalog.sql b/server/schema/catalog.sql index c3096a2..1457236 100644 --- a/server/schema/catalog.sql +++ b/server/schema/catalog.sql @@ -1,17 +1,17 @@ CREATE TYPE event AS ENUM ('guest speaker', 'study-trip', 'workshop', 'other'); -CREATE TYPE subject AS ENUM ('life skills', 'science', 'technology', 'engineering', 'math', 'college readiness'); -CREATE TYPE year AS ENUM ('junior', 'senior', 'both'); -CREATE TYPE season AS ENUM ('spring', 'summer', 'fall', 'winter'); +CREATE TYPE subject AS ENUM ('life skills', 'science', 'technology', 'engineering', 'math', 'college readiness', 'soft skills', 'writing', 'other'); +CREATE TYPE year AS ENUM ('junior', 'senior'); +CREATE TYPE season AS ENUM ('spring', 'summer', 'fall'); DROP TABLE IF EXISTS catalog; CREATE TABLE catalog ( id SERIAL PRIMARY KEY, - host VARCHAR(50) NOT NULL, + host VARCHAR(50), title VARCHAR(50) NOT NULL, - event_type event NOT NULL, - subject subject NOT NULL, - description VARCHAR(50) NOT NULL, - year year NOT NULL, - season season, - location VARCHAR(256) + event_type event[] NOT NULL DEFAULT '{}', + subject subject[] NOT NULL DEFAULT '{}', + description VARCHAR(256), + year year[] NOT NULL DEFAULT '{}', + season season[] NOT NULL DEFAULT '{}', + hidden BOOLEAN NOT NULL, ); diff --git a/server/schema/day.sql b/server/schema/day.sql new file mode 100644 index 0000000..c29f5d2 --- /dev/null +++ b/server/schema/day.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS day; +CREATE TABLE IF NOT EXISTS day ( + id serial PRIMARY KEY, + event_date DATE UNIQUE NOT NULL, + start_time TIME NOT NULL, + end_time TIME NOT NULL, + location VARCHAR( 256 ) NOT NULL, + notes VARCHAR( 250 ), + day_count INTEGER +); \ No newline at end of file diff --git a/server/schema/published_schedule.sql b/server/schema/published_schedule.sql index 7d04a9e..a00a378 100644 --- a/server/schema/published_schedule.sql +++ b/server/schema/published_schedule.sql @@ -1,13 +1,19 @@ DROP TABLE IF EXISTS published_schedule; CREATE TABLE IF NOT EXISTS published_schedule ( - id serial NOT NULL, + id serial NOT NULL PRIMARY KEY, event_id integer NOT NULL, + day_id integer NOT NULL, confirmed boolean NOT NULL, - confirmed_on date NOT NULL, - start_time timestamp NOT NULL, - end_time timestamp NOT NULL, + confirmed_on date, + start_time time NOT NULL, + end_time time NOT NULL, cohort varchar[] NOT NULL, notes varchar(100), + created_on date NOT NULL, FOREIGN KEY (event_id) - REFERENCES catalog (id) + REFERENCES catalog (id), + FOREIGN KEY (day_id) + REFERENCES day (id) ON DELETE CASCADE ); + +CREATE INDEX idx_day_id ON published_schedule (day_id); diff --git a/server/schema/users.sql b/server/schema/users.sql index b652586..f9598e0 100644 --- a/server/schema/users.sql +++ b/server/schema/users.sql @@ -1,4 +1,4 @@ -CREATE TYPE account_type as ENUM ('superadmin', 'admin'); +CREATE TYPE account_type as ENUM ('admin', 'student'); DROP TABLE IF EXISTS users; @@ -6,5 +6,8 @@ CREATE TABLE users ( id VARCHAR ( 256 ) PRIMARY KEY, email VARCHAR ( 50 ) NOT NULL, type account_type NOT NULL, - approved BOOLEAN NOT NULL + approved BOOLEAN NOT NULL, + approved_on DATE NOT NULL, + first_name VARCHAR(50) NOT NULL, + last_name VARCHAR(50) NOT NULL ); \ No newline at end of file