diff --git a/app/index.js b/app/index.js index 0a1bfdb3..2afaf13c 100644 --- a/app/index.js +++ b/app/index.js @@ -1,3 +1,6 @@ +// Set server timezone to UTC to avoid issues with date parsing +process.env.TZ = 'UTC' + const path = require('path') const express = require('express') const bodyParser = require('body-parser') diff --git a/app/manage/index.js b/app/manage/index.js index a44ca0be..4fd7b603 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -302,18 +302,18 @@ function manageRouter (nextApp) { } ) - // Use same page for two routes - const assignBadgePageRoute = [ - can('organization:edit'), - (req, res) => nextApp.render(req, res, '/badges/assign', req.params) - ] + // New badge assignment router.get( '/organizations/:id/badges/assign/:userId', - ...assignBadgePageRoute + can('organization:edit'), + (req, res) => nextApp.render(req, res, '/badges-assignment/new', req.params) ) + + // Edit badge assignment router.get( '/organizations/:id/badges/:badgeId/assign/:userId', - ...assignBadgePageRoute + can('organization:edit'), + (req, res) => nextApp.render(req, res, '/badges-assignment/edit', req.params) ) return router diff --git a/components/profile-modal.js b/components/profile-modal.js index bb83481d..18ae1c02 100644 --- a/components/profile-modal.js +++ b/components/profile-modal.js @@ -3,6 +3,7 @@ import { isEmpty } from 'ramda' import theme from '../styles/theme' import Popup from 'reactjs-popup' import Button from './button' +import SvgSquare from '../components/svg-square' function renderActions (actions) { return ( @@ -43,7 +44,32 @@ function renderActions (actions) { ) } -export default function ProfileModal ({ user, attributes, onClose, actions }) { +function renderBadges (badges) { + if (!badges || badges.length === 0) { + return null + } + + return ( + + {badges.map((b) => ( + + + + + ))} +
+ + {b.name}
+ ) +} + +export default function ProfileModal ({ + user, + attributes, + badges, + onClose, + actions +}) { actions = actions || [] let profileContent =
User does not have a profile
if (!isEmpty(attributes)) { @@ -87,13 +113,18 @@ export default function ProfileModal ({ user, attributes, onClose, actions }) { `} } - return
- { user.img ? : '' } -

- {user.name} - {!isEmpty(actions) && renderActions(actions)} -

- {profileContent} - -
+ return ( +
+ {user.img ? : ''} +

+ {user.name} + {!isEmpty(actions) && renderActions(actions)} +

+ {profileContent} + {renderBadges(badges)} + +
+ ) } diff --git a/components/svg-square.js b/components/svg-square.js new file mode 100644 index 00000000..0c33d9bc --- /dev/null +++ b/components/svg-square.js @@ -0,0 +1,9 @@ +import React from 'react' + +export default function SvgSquare ({ color, size = 20 }) { + return ( + + + + ) +} diff --git a/pages/badges/assign.js b/pages/badges-assignment/edit.js similarity index 72% rename from pages/badges/assign.js rename to pages/badges-assignment/edit.js index 9298cb7b..f78c4a39 100644 --- a/pages/badges/assign.js +++ b/pages/badges-assignment/edit.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' +import * as Yup from 'yup' import { Formik, Field, Form } from 'formik' import APIClient from '../../lib/api-client' import Button from '../../components/button' @@ -39,13 +40,13 @@ function Section ({ children }) { ) } -export default class AssignBadge extends Component { +export default class EditBadgeAssignment extends Component { static async getInitialProps ({ query }) { if (query) { return { orgId: query.id, - badgeId: query.badgeId, - userId: query.userId + badgeId: parseInt(query.badgeId), + userId: parseInt(query.userId) } } } @@ -64,22 +65,26 @@ export default class AssignBadge extends Component { } async loadData () { - const { orgId, badgeId } = this.props + const { orgId, badgeId, userId } = this.props try { const org = await apiClient.get(`/organizations/${orgId}`) - let badge, badges + const badge = await apiClient.get( + `/organizations/${orgId}/badges/${badgeId}` + ) + let assignment + if (badge && badge.users) { + assignment = badge.users.find((u) => u.id === parseInt(userId)) + } - if (badgeId) { - badge = await apiClient.get(`/organizations/${orgId}/badges/${badgeId}`) - } else { - badges = await apiClient.get(`/organizations/${orgId}/badges`) + if (!assignment) { + throw Error('Badge assignment not found.') } this.setState({ org, badge, - badges + assignment }) } catch (error) { console.error(error) @@ -99,8 +104,8 @@ export default class AssignBadge extends Component { return
Loading...
} - const { orgId, userId } = this.props - const { badges, badge, user } = this.state + const { orgId, userId, badgeId } = this.props + const { badge, assignment } = this.state return ( <> @@ -111,66 +116,57 @@ export default class AssignBadge extends Component { { + validationSchema={Yup.object().shape({ + assignedAt: Yup.date().required( + 'Please select an assignment date.' + ), + validUntil: Yup.date().when( + 'assignedAt', + (assignedAt, schema) => + assignedAt && + schema.min( + assignedAt, + 'End date must be after the start date.' + ) + ) + })} + onSubmit={async ({ assignedAt, validUntil }) => { try { const payload = { assigned_at: assignedAt, valid_until: validUntil !== '' ? validUntil : null } - if (!user) { - await apiClient.post( - `/organizations/${orgId}/badges/${badgeId}/assign/${userId}`, - payload - ) - Router.push( - join( - URL, - `/organizations/${orgId}/badges/${badgeId}/assign/${userId}` - ) - ) - } else { - await apiClient.patch( - `/organizations/${orgId}/member/${userId}/badge/${badgeId}`, - payload - ) - toast.info('Badge updated successfully.') - } + await apiClient.patch( + `/organizations/${orgId}/member/${userId}/badge/${badgeId}`, + payload + ) + toast.info('Badge updated successfully.') this.loadData() } catch (error) { console.log(error) toast.error(`Unexpected error, please try again later.`) } }} - render={({ isSubmitting, values }) => { + render={({ isSubmitting, values, errors }) => { return (

User: {userId} (OSM id)

- {badge ? ( -
-

Badge: {badge && badge.name}

-
- ) : ( -
- - - - {badges.map((b) => ( - - ))} - -
- )} +
+

Badge: {badge && badge.name}

+
+ {errors.assignedAt && ( +
{errors.assignedAt}
+ )}
@@ -186,6 +185,9 @@ export default class AssignBadge extends Component { type='date' value={values.validUntil} /> + {errors.validUntil && ( +
{errors.validUntil}
+ )}
- - ) - }} - /> - {users.length > 0 && ( - ({ - ...u, - assignedAt: u.assignedAt && toDateString(u.assignedAt), - validUntil: u.validUntil && toDateString(u.validUntil) - }))} - columns={columns} - onRowClick={({ id }) => - Router.push( - join( - URL, - `/organizations/${orgId}/badges/${badgeId}/assign/${id}` - ) +
({ + ...u, + assignedAt: u.assignedAt && toDateString(u.assignedAt), + validUntil: u.validUntil && toDateString(u.validUntil) + }))} + emptyPlaceHolder='No members have this badge assigned. Badges can be assigned via user profile actions.' + columns={columns} + onRowClick={({ id }) => + Router.push( + join( + URL, + `/organizations/${orgId}/badges/${badgeId}/assign/${id}` ) - } - /> - )} + ) + } + /> ) } diff --git a/pages/organization.js b/pages/organization.js index ff68fa15..d45659ee 100644 --- a/pages/organization.js +++ b/pages/organization.js @@ -8,6 +8,7 @@ import SectionHeader from '../components/section-header' import Table from '../components/table' import theme from '../styles/theme' import AddMemberForm from '../components/add-member-form' +import SvgSquare from '../components/svg-square' import Button from '../components/button' import Modal from 'react-modal' import ProfileModal from '../components/profile-modal' @@ -74,10 +75,18 @@ export default class Organization extends Component { const { id } = this.props try { + // Fetch profile attributes const profileInfo = await getUserOrgProfile(id, user.id) + + // Fetch badges for this organization + const profileBadges = ( + await apiClient.get(`/user/${user.id}/badges`) + ).badges.filter((b) => b.organization_id === parseInt(id)) + this.setState({ profileInfo, profileMeta: user, + profileBadges, modalIsOpen: true }) } catch (e) { @@ -233,11 +242,7 @@ export default class Organization extends Component {
{ return { ...row, - color: () => ( - - - - ) + color: () => } })} columns={columns} onRowClick={ ({ id: badgeId }) => Router.push( @@ -307,6 +312,7 @@ export default class Organization extends Component { let profileActions = [] if (this.state.modalIsOpen && isUserOwner) { + console.log('here') const profileId = parseInt(this.state.profileMeta.id) const isProfileManager = contains(profileId, managerIds) const isProfileOwner = contains(profileId, ownerIds) @@ -338,14 +344,13 @@ export default class Organization extends Component { } } - if (isProfileManager || isProfileOwner) { - profileActions.push({ - name: 'Assign a Badge', - onClick: () => Router.push( + profileActions.push({ + name: 'Assign a Badge', + onClick: () => + Router.push( join(URL, `/organizations/${org.id}/badges/assign/${profileId}`) ) - }) - } + }) } return ( @@ -415,6 +420,7 @@ export default class Organization extends Component { }} isOpen={this.state.modalIsOpen}>