Skip to content

Commit

Permalink
Merge pull request #97 from pvdthings/web-shifts-page
Browse files Browse the repository at this point in the history
Volunteer Module
  • Loading branch information
dillonfagan authored Nov 25, 2024
2 parents 70a31d3 + 8758493 commit b0c1bf2
Show file tree
Hide file tree
Showing 25 changed files with 1,372 additions and 939 deletions.
40 changes: 40 additions & 0 deletions apps/api/apps/catalog/routes/things.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const router = express.Router();
const { getCatalogData } = require('../services/catalog');
const { getThingDetails } = require('../services/thingDetails');
const { getItemDetails } = require('../services/itemDetails');
const { enroll, getShifts } = require('../services/shifts');
const { findMember } = require('../../../services/borrowers');

router.get('/', async (req, res) => {
try {
Expand Down Expand Up @@ -33,4 +35,42 @@ router.get('/items/:id', async (req, res) => {
}
});

router.get('/volunteer/shifts', async (req, res) => {
const email = req.headers['x-email'];
try {
res.send(await getShifts({ email }));
} catch (error) {
console.error(error);
res.status(error.status || 500).send({ errors: [error] });
}
});

router.post('/volunteer/auth', async (req, res) => {
const { email } = req.body;
try {
const member = await findMember({ email });

if (member) {
res.send(member);
} else {
res.sendStatus(403);
}
} catch (error) {
console.error(error);
res.status(error.status || 500).send({ errors: [error] });
}
});

router.post('/volunteer/shifts/enroll', async (req, res) => {
const email = req.headers['x-email'];
const { shifts } = req.body;
try {
await enroll(email, shifts);
res.sendStatus(204);
} catch (error) {
console.error(error);
res.status(error.status || 500).send({ errors: [error] });
}
});

module.exports = router;
59 changes: 59 additions & 0 deletions apps/api/apps/catalog/services/shifts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const { findMember } = require("../../../services/borrowers");
const { fetchJobs, updateJobAssignments } = require("../../../services/jobs/service");
const { sendJobsNotification } = require("../../../services/messages");

const dateFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' };

const formatTime = (date) => {
let hours = date.getHours();
let minutes = date.getMinutes();
let ampm = hours >= 12 ? 'pm' : 'am';

hours = hours % 12; hours = hours ? hours : 12;
minutes = minutes < 10 ? '0' + minutes : minutes;
return hours + ':' + minutes + ampm;
};

const getShifts = async ({ email }) => {
const member = await getMember(email);
const jobs = await fetchJobs();

return jobs.map((j) => {
const date = new Date(j.startTime);
let endDate = new Date(j.startTime);
endDate.setSeconds(date.getSeconds() + j.duration);

return {
id: j.id,
title: j.title,
date: date.toLocaleString('en-US', dateFormatOptions),
timespan: `${formatTime(date)} - ${formatTime(endDate)}`,
description: j.description,
enrolled: j.members.some((m) => m.id === member?.id),
volunteers: j.members.filter((m) => m.id !== member?.id).map((m) => ({
name: m.name,
firstName: m.name.split(' ')?.[0],
keyholder: m.keyholder
}))
};
});
};

const getMember = async (email) => {
return !!email ? await findMember({ email }) : undefined;
};

const enroll = async (email, shifts) => {
try {
updateJobAssignments(email, shifts).then(async () => {
const newShifts = shifts.filter((s) => !s.remove);
if (newShifts.length) {
await sendJobsNotification({ recipient: email, ids: newShifts.map((s) => s.id) });
}
});
} catch (error) {
throw error;
}
};

module.exports = { enroll, getShifts };
1 change: 1 addition & 0 deletions apps/api/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const Table = {
Inventory: 'Inventory',
Things: 'Things',
Borrowers: 'Members',
Jobs: 'Jobs',
Loans: 'Loans',
Payments: 'Transactions'
};
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pvdthings-api",
"version": "1.21.3",
"version": "1.22.0",
"description": "",
"main": "server.js",
"scripts": {
Expand Down
12 changes: 11 additions & 1 deletion apps/api/services/borrowers.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const mapBorrower = (record) => {
email: record.get('Email'),
phone: record.get('Phone')
},
issues: mapIssues(record)
issues: mapIssues(record),
keyholder: !!record.get('Keyholder')
}
}

Expand Down Expand Up @@ -51,6 +52,14 @@ const fetchBorrower = async ({ id }) => {
return mapBorrower(record);
}

const findMember = async ({ email }) => {
const matches = await borrowers.select({
filterByFormula: `{Email} = '${email}'`
}).all();

return matches.length ? mapBorrower(matches[0]) : undefined;
};

const updateBorrower = async (id, { email, phone }) => {
let updatedFields = {};

Expand All @@ -63,5 +72,6 @@ const updateBorrower = async (id, { email, phone }) => {
module.exports = {
fetchBorrowers,
fetchBorrower,
findMember,
updateBorrower
};
3 changes: 3 additions & 0 deletions apps/api/services/jobs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { fetchJobs } = require('./service');

module.exports = { fetchJobs };
60 changes: 60 additions & 0 deletions apps/api/services/jobs/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const { base, Table } = require('../../db');

const jobs = base(Table.Jobs);
const members = base(Table.Borrowers);

const fetchJobs = async () => {
const records = await jobs.select({ view: 'Future' }).all();

return await Promise.all(records.map(async (r) => {
const memberIds = r.get('Members') || [];

return {
id: r.id,
title: r.get('Title'),
canceled: !!r.get('Canceled'),
description: r.get('Description'),
members: await Promise.all(memberIds.map(async (id) => {
const memberRecord = await members.find(id);

return {
id: memberRecord.id,
name: memberRecord.get('Name'),
keyholder: !!memberRecord.get('Keyholder')
};
})),
startTime: r.get('Start Time'),
duration: r.get('Duration')
};
}));
};

const updateJobAssignments = async (email, jobs) => {
const matches = await members.select({
filterByFormula: `{Email} = '${email}'`
}).all();

if (!matches.length) {
throw new Error('Email provided does not match any member.');
}

const memberRecord = matches[0];
const existingJobs = memberRecord.get('Jobs') || [];

const toAdd = jobs.filter((j) => !j.remove).map((j) => j.id);
const toRemove = jobs.filter((j) => !!j.remove).map((j) => j.id);

const updatedJobs = [
...toAdd,
...existingJobs.filter((id) => !toRemove.includes(id))
];

await memberRecord.updateFields({
'Jobs': updatedJobs
});
};

module.exports = {
fetchJobs,
updateJobAssignments
};
17 changes: 16 additions & 1 deletion apps/api/services/messages/messages.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const LOAN_REMINDER_WEBHOOK_URL = process.env.LOAN_REMINDER_WEBHOOK_URL;
const JOBS_NOTIFICATION_WEBHOOK_URL = process.env.JOBS_NOTIFICATION_WEBHOOK_URL;

async function sendLoanReminder({ loanNumber }) {
await fetch(LOAN_REMINDER_WEBHOOK_URL, {
Expand All @@ -12,6 +13,20 @@ async function sendLoanReminder({ loanNumber }) {
});
}

async function sendJobsNotification({ recipient, ids }) {
await fetch(JOBS_NOTIFICATION_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
recipient,
jobs: ids
})
});
}

module.exports = {
sendLoanReminder
sendLoanReminder,
sendJobsNotification
};
Loading

0 comments on commit b0c1bf2

Please sign in to comment.