-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* fix local-dev * add a whole bunch of group related stuff * implement add/remove members * setup groups to hide shift signup unless your group is gtg * add helper functions, fix serverside comparison
- Loading branch information
Showing
26 changed files
with
920 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import Knex from 'knex'; | ||
import { DateTime } from 'luxon'; | ||
import knexConfig from '../knexfile'; | ||
import { getConfig } from '../config/config'; | ||
import Group from '../models/group/group'; | ||
import GroupViewModel from '../view_models/group'; | ||
import User from '../models/user/user'; | ||
|
||
const knex = Knex(knexConfig[getConfig().Environment]); | ||
|
||
export default class GroupController { | ||
public static async GetGroupViewModels( | ||
groups: Group[], | ||
): Promise<GroupViewModel[]> { | ||
const groupViewModels: Promise<GroupViewModel>[] = groups.map( | ||
async (group): Promise<GroupViewModel> => { | ||
const members = await Group.relatedQuery('members').for(group.id); | ||
if (!members) { | ||
return { | ||
group, | ||
members: [], | ||
}; | ||
} | ||
|
||
return { | ||
group, | ||
members: members.map((member) => User.fromJson(member)), | ||
}; | ||
}, | ||
); | ||
|
||
const viewModels: GroupViewModel[] = await Promise.all(groupViewModels); | ||
|
||
return viewModels; | ||
} | ||
|
||
public static async GetAllGroupsForUser(user: User): Promise<Group[]> { | ||
const groups = await knex<Group>('groups') | ||
.from('group_members') | ||
.where({ | ||
userID: user.id, | ||
}) | ||
.join('groups', 'group_members.groupID', '=', 'groups.id'); | ||
|
||
return groups; | ||
} | ||
|
||
public static async UserCanSignupForShifts( | ||
user: User, | ||
rosterID: number, | ||
): Promise<boolean> { | ||
const groups = await GroupController.GetAllGroupsForUser(user); | ||
|
||
if (groups.length === 0) { | ||
return false; | ||
} | ||
|
||
groups.filter((group) => group.rosterID === rosterID); | ||
|
||
groups.sort( | ||
(a, b) => | ||
a.shiftSignupOpenDate.getTime() - b.shiftSignupOpenDate.getTime(), | ||
); | ||
|
||
if ( | ||
DateTime.fromJSDate(groups[0].shiftSignupOpenDate).setZone('utc', { | ||
keepLocalTime: true, | ||
}) > DateTime.utc() | ||
) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Knex } from 'knex'; | ||
|
||
export async function up(knex: Knex): Promise<void> { | ||
return knex.schema.createTable('groups', (table) => { | ||
table.increments('id').primary(); | ||
table.string('name').notNullable(); | ||
table.string('description').notNullable(); | ||
table.integer('rosterID').notNullable(); | ||
table.timestamp('shiftSignupOpenDate', { useTz: false }).notNullable(); | ||
}); | ||
} | ||
|
||
export async function down(knex: Knex): Promise<void> { | ||
return knex.schema.dropTableIfExists('groups'); | ||
} |
12 changes: 12 additions & 0 deletions
12
packages/backend/migrations/20240419003738_group_members.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { Knex } from 'knex'; | ||
|
||
export async function up(knex: Knex): Promise<void> { | ||
return knex.schema.createTable('group_members', (table) => { | ||
table.integer('userID'); | ||
table.integer('groupID'); | ||
}); | ||
} | ||
|
||
export async function down(knex: Knex): Promise<void> { | ||
return knex.schema.dropTableIfExists('group_members'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Model } from 'objection'; | ||
import { RJSFSchema } from '@rjsf/utils'; | ||
import User from '../user/user'; | ||
|
||
export default class Group extends Model { | ||
id!: number; | ||
|
||
name!: string; | ||
|
||
description!: string; | ||
|
||
rosterID!: number; | ||
|
||
shiftSignupOpenDate!: Date; | ||
|
||
// Table name is the only required property. | ||
static tableName = 'groups'; | ||
|
||
// Optional JSON schema. This is not the database schema! Nothing is generated | ||
// based on this. This is only used for validation. Whenever a model instance | ||
// is created it is checked against this schema. http://json-schema.org/. | ||
static jsonSchema = { | ||
type: 'object', | ||
|
||
properties: { | ||
id: { type: 'integer' }, | ||
}, | ||
}; | ||
|
||
static formSchema: RJSFSchema = { | ||
title: 'Create a Group', | ||
type: 'object', | ||
required: ['name', 'description', 'rosterID', 'shiftSignupOpenDate'], | ||
properties: { | ||
name: { | ||
type: 'string', | ||
title: 'Group Name', | ||
default: '', | ||
}, | ||
description: { | ||
type: 'string', | ||
title: 'Description', | ||
default: '', | ||
}, | ||
rosterID: { | ||
type: 'integer', | ||
title: 'Roster ID', | ||
default: 0, | ||
}, | ||
shiftSignupOpenDate: { | ||
type: 'string', | ||
title: 'Shift Signup Open Date', | ||
format: 'date-time', | ||
default: '', | ||
}, | ||
}, | ||
}; | ||
|
||
static relationMappings = { | ||
members: { | ||
relation: Model.ManyToManyRelation, | ||
modelClass: User, | ||
join: { | ||
from: 'groups.id', | ||
through: { | ||
from: 'group_members.groupID', | ||
to: 'group_members.userID', | ||
}, | ||
to: 'users.id', | ||
}, | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import express, { Request, Response, Router } from 'express'; | ||
import Group from '../models/group/group'; | ||
import hasPermission from '../middleware/rbac'; | ||
import GroupController from '../controllers/group'; | ||
|
||
const router: Router = express.Router(); | ||
|
||
/* GET all group viewModels. */ | ||
router.get( | ||
'/viewModels', | ||
hasPermission('groups:readAll'), | ||
async (req: Request, res: Response) => { | ||
const groups = await Group.query(); | ||
|
||
const viewModels = await GroupController.GetGroupViewModels(groups); | ||
res.json(viewModels); | ||
}, | ||
); | ||
|
||
/* GET a group by ID. */ | ||
router.get( | ||
'/:id', | ||
hasPermission('groups:read'), | ||
async (req: Request, res: Response) => { | ||
const groupID = req.params.id; | ||
|
||
const group = await Group.query().findById(groupID); | ||
|
||
if (!group) { | ||
res.status(404).send('Group not found'); | ||
return; | ||
} | ||
|
||
res.json(group); | ||
}, | ||
); | ||
|
||
/* POST a new group. */ | ||
router.post( | ||
'/', | ||
hasPermission('groups:create'), | ||
async (req: Request, res: Response) => { | ||
const newGroup = req.body; | ||
|
||
console.log(newGroup); | ||
|
||
const group = await Group.query().insert(newGroup); | ||
|
||
res.json(group); | ||
}, | ||
); | ||
|
||
/* UPDATE a group. */ | ||
router.put( | ||
'/:id', | ||
hasPermission('groups:edit'), | ||
async (req: Request, res: Response) => { | ||
const updatedGroup = req.body; | ||
const groupID = req.params.id; | ||
|
||
const group = await Group.query().findById(groupID); | ||
|
||
if (!group) { | ||
res.status(404).send('Group not found'); | ||
return; | ||
} | ||
|
||
await Group.query().findById(groupID).patch(updatedGroup); | ||
|
||
res.json(updatedGroup); | ||
}, | ||
); | ||
|
||
// GET members of a group | ||
router.get( | ||
'/:id/members', | ||
hasPermission('groups:readMembers'), | ||
async (req: Request, res: Response) => { | ||
const groupID = req.params.id; | ||
|
||
if (!groupID || groupID === 'undefined') { | ||
res.status(400).send('Group ID is required'); | ||
return; | ||
} | ||
|
||
const members = await Group.relatedQuery('members').for(groupID); | ||
|
||
if (!members) { | ||
res.status(404).send('Members not found'); | ||
return; | ||
} | ||
|
||
res.json(members); | ||
}, | ||
); | ||
|
||
// POST a new member to a group | ||
router.post( | ||
'/:id/members/:memberID', | ||
hasPermission('groups:addMember'), | ||
async (req: Request, res: Response) => { | ||
const { id: groupID, memberID: newMemberID } = req.params; | ||
|
||
if (!groupID || groupID === 'undefined') { | ||
res.status(400).send('Group ID is required'); | ||
return; | ||
} | ||
|
||
if (!newMemberID || newMemberID === 'undefined') { | ||
res.status(400).send('Member ID is required'); | ||
return; | ||
} | ||
|
||
const group = await Group.query().findById(groupID); | ||
|
||
if (!group) { | ||
res.status(404).send('Group not found'); | ||
return; | ||
} | ||
|
||
await group.$relatedQuery('members').relate(newMemberID); | ||
|
||
res.json(true); | ||
}, | ||
); | ||
|
||
// DELETE a member from a group | ||
router.delete( | ||
'/:id/members/:memberID', | ||
hasPermission('groups:removeMember'), | ||
async (req: Request, res: Response) => { | ||
const { id: groupID, memberID } = req.params; | ||
|
||
if (!groupID || groupID === 'undefined') { | ||
res.status(400).send('Group ID is required'); | ||
return; | ||
} | ||
|
||
if (!memberID || memberID === 'undefined') { | ||
res.status(400).send('Member ID is required'); | ||
return; | ||
} | ||
|
||
const group = await Group.query().findById(groupID); | ||
|
||
if (!group) { | ||
res.status(404).send('Group not found'); | ||
return; | ||
} | ||
|
||
await group.$relatedQuery('members').unrelate().where('id', memberID); | ||
|
||
res.json(true); | ||
}, | ||
); | ||
|
||
export default router; |
Oops, something went wrong.