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 d9f6238..fb5f261 100644 --- a/common/utils.js +++ b/common/utils.js @@ -25,6 +25,34 @@ const isInteger = (value) => { return value && /^\d+$/.test(value); }; +// dependency for publishedSchedule.js +const calculateYear = (eventDate, gradeLevel) => { + if (gradeLevel) { + const currentDay = new Date(eventDate); + // console.log('current day', currentDay.getFullYear() + (currentDay.getMonth() >= 7 ? 2 : 1)); + if (gradeLevel.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.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.toLowerCase() === 'both') { + 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); }; @@ -57,4 +85,4 @@ const keysToCamel = (data) => { return data; }; -module.exports = { keysToCamel, isInteger }; +module.exports = { keysToCamel, isInteger, calculateYear }; diff --git a/routes/day.js b/routes/day.js new file mode 100644 index 0000000..388f8c1 --- /dev/null +++ b/routes/day.js @@ -0,0 +1,103 @@ +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 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 f6ba9e5..6956d29 100644 --- a/routes/publishedSchedule.js +++ b/routes/publishedSchedule.js @@ -1,6 +1,6 @@ const express = require('express'); const { db } = require('../server/db'); -const { keysToCamel } = require('../common/utils'); +const { keysToCamel, calculateYear } = require('../common/utils'); const publishedScheduleRouter = express.Router(); @@ -11,6 +11,7 @@ publishedScheduleRouter.get('/', async (req, res) => { ` SELECT PS.id, + PS.day_id, C.host, C.title, PS.confirmed, @@ -86,6 +87,56 @@ publishedScheduleRouter.get('/recently-confirmed', async (req, res) => { 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(); + // const day = formattedDate.getDate(); + + // winter + // december (11) -> winter [year + 1] + // january (0) - february (1) -> winter [year] + if (month === 11) { + return `Winter ${year + 1}`; + } + if (month === 0 || month === 1) { + return `Winter ${year}`; + } + // spring + // march-may -> winter [year] + if (month >= 2 && month <= 4) { + return `Winter ${year}`; + } + // summer + // june-august -> summer [year] + if (month >= 5 && month <= 7) { + return `Summer ${year}`; + } + // fall + // september-november -> fall [year] + return `Fall ${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 + `, + ); + const allSeasonsResult = allDatesResult.map((row) => { + return getSeason(row); + }); + const allUniqueSeasonsResult = [...new Set(allSeasonsResult)]; + res.status(200).json(keysToCamel(allUniqueSeasonsResult)); + } catch (err) { + res.status(500).send(err.message); } }); @@ -100,7 +151,11 @@ publishedScheduleRouter.get('/season', async (req, res) => { // getting the intervals for each season if (season.toLowerCase() === 'winter') { startTime = `${year - 1}-12-01`; - endTime = `${year}-02-29`; + if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) { + endTime = `${year}-02-29`; + } else { + endTime = `${year}-02-28`; + } } else if (season.toLowerCase() === 'spring') { startTime = `${year}-03-01`; endTime = `${year}-05-31`; @@ -118,6 +173,13 @@ publishedScheduleRouter.get('/season', async (req, res) => { ( SELECT PS.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, @@ -130,19 +192,43 @@ publishedScheduleRouter.get('/season', async (req, res) => { 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 - DATE(start_time) >= $1::date AND DATE(start_time) <= $2::date + D.event_date >= $1::date AND D.event_date <= $2::date + AND D.id = PS.day_id ) - SELECT DATE(seasonPS.start_time), JSON_AGG(seasonPS.*) AS data + 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, + 'title', seasonPS.title, + '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 DATE(start_time) - ORDER BY DATE(start_time) ASC; + 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(400).send(err.message); + res.status(500).send(err.message); } }); @@ -153,27 +239,41 @@ publishedScheduleRouter.get('/date', async (req, res) => { const seasonResult = 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 + 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, + '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 DATE(PS.start_time) = $1 - ORDER BY start_time ASC; + WHERE D.event_date = $1::date + GROUP BY d.event_date, d.id + ORDER BY d.event_date; `, [date], ); - res.status(200).json(keysToCamel(seasonResult)); + res.status(200).json(keysToCamel(seasonResult)[0]); } catch (err) { - res.status(400).send(err.message); + res.status(500).send(err.message); } }); @@ -185,6 +285,7 @@ publishedScheduleRouter.get('/:id', async (req, res) => { ` SELECT PS.id, + PS.day_id, C.host, C.title, PS.confirmed, @@ -208,16 +309,33 @@ 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, confirmedOn, 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, @@ -227,10 +345,20 @@ publishedScheduleRouter.post('/', async (req, res) => { created_on ) VALUES - (nextval('published_schedule_id_seq'), $1, $2, $3, $4, $5, $6, $7, $8) + ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, created_on; `, - [eventId, confirmed, confirmedOn, startTime, endTime, cohort, notes, currDate], + [ + eventId, + dayId, + confirmed, + confirmedOn, + startTime, + endTime, + calculateYear(eventDate, cohort), + notes, + currDate + ], ); res.status(201).json({ status: 'Success', @@ -242,29 +370,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, createdOn } = + + 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), - created_on = COALESCE($8, created_on) - WHERE id = $9 + 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, createdOn, 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); @@ -272,9 +472,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 @@ -282,7 +485,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/server/schema/day.sql b/server/schema/day.sql new file mode 100644 index 0000000..1ac0b41 --- /dev/null +++ b/server/schema/day.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS day; +CREATE TABLE IF NOT EXISTS day ( + id serial PRIMARY KEY, + event_date DATE NOT NULL, + start_time TIME NOT NULL, + end_time TIME NOT NULL, + location VARCHAR( 256 ) NOT NULL, + notes VARCHAR( 250 ) +); \ No newline at end of file diff --git a/server/schema/published_schedule.sql b/server/schema/published_schedule.sql index 484573c..90e6fe7 100644 --- a/server/schema/published_schedule.sql +++ b/server/schema/published_schedule.sql @@ -1,14 +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, + 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) ); + +CREATE INDEX idx_day_id ON published_schedule (day_id); \ No newline at end of file