Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,3 @@ public/precache-manifest.*

# Project management (internal)
.project/
package-lock.json
11 changes: 10 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ class App extends React.Component<Props, State> {
/>
<PrivateRoute
auth={this.state.auth}
allowedRoles={[Role.ADMIN, Role.PROGRAMLEADER, Role.SITELEADER]}
allowedRoles={[Role.ADMIN]}
userRole={role}
path="/AllUsers"
component={AllUsersPage}
Expand Down Expand Up @@ -888,6 +888,15 @@ class App extends React.Component<Props, State> {
render={(props: {
history: H.History
}) : React.ReactElement=> <UsersPage {...props}/>}
/>
<PrivateRoute
auth={auth}
path="/LeadersAllUsers"
allowedRoles={[Role.ADMIN]}
userRole={role}
render={(props: {
history: H.History
}) : React.ReactElement=> <UsersPage {...props}/>}
/>

<PrivateRoute
Expand Down
17 changes: 0 additions & 17 deletions src/components/BurgerMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import * as Types from '../constants/Types';
import * as H from 'history';
import Firebase from './Firebase';
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
import GroupIcon from '@material-ui/icons/Group';

const styles: object = {
toolbarIcon: {
Expand Down Expand Up @@ -489,22 +488,6 @@ class BurgerMenu extends React.Component<Props, State>{
/>
</ListItem>
</>}
{(role == Role.ADMIN || role == Role.PROGRAMLEADER || role == Role.SITELEADER) && <>
<ListItem
button
onClick={() => this.props.handleNavigation( (): void => {
this.setState({ menu: 16, chalkOpen: false });
this.props.history.push("/AllUsers");
})}
>
<ListItemIcon>
<GroupIcon style={{ fill: Constants.Colors.SE }} />
</ListItemIcon>
<ListItemText
primary="All Users"
/>
</ListItem>
</>}
{(role == Role.ADMIN || role == Role.PROGRAMLEADER || role == Role.SITELEADER) && <>
<ListItem
button
Expand Down
17 changes: 15 additions & 2 deletions src/components/Firebase/Firebase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4744,22 +4744,35 @@ class Firebase {
lastName: string
email: string
role: string
school: string
program: string
archived: boolean
lastLogin: Date | null
}> = []

// Fetch all programs to build a lookup map
const programsSnapshot = await this.db.collection('programs').get()
const programsMap = new Map<string, string>()
programsSnapshot.docs.forEach(doc => {
programsMap.set(doc.id, doc.data().name || '')
})

const usersSnapshot = await this.db.collection('users').get()

usersSnapshot.docs.forEach(doc => {
const data = doc.data()
// Get first program name from user's programs array
let programName = ''
if (data.programs && Array.isArray(data.programs) && data.programs.length > 0) {
const firstProgramId = data.programs[0].id || data.programs[0]
programName = programsMap.get(firstProgramId) || ''
}
result.push({
id: doc.id,
firstName: data.firstName || '',
lastName: data.lastName || '',
email: data.email || '',
role: data.role || '',
school: data.school || '',
program: programName,
archived: data.archived || false,
lastLogin: data.lastLogin ? data.lastLogin.toDate() : null,
})
Expand Down
40 changes: 23 additions & 17 deletions src/components/UsersComponents/AllUsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ import * as Types from '../../constants/Types'

const TableRow = styled.tr`
background-color: white;
:nth-child(odd) { background-color: #eaeaea; }
:nth-child(odd) { background-color: rgb(234, 234, 234); }
&:hover { background-color: rgba(9, 136, 236, 0.4); cursor: pointer; }
`

const TableCell = styled.td`
padding: 10px 8px;
padding: 4px 8px;
text-align: left;
font-size: 1.25rem;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
font-weight: 400;
line-height: 1.6;
`

const StatusBadge = styled.span<{ archived: boolean }>`
Expand Down Expand Up @@ -78,7 +82,7 @@ class AllUsersTable extends React.Component<Props, State> {
if (search) {
const s = search.toLowerCase()
users = users.filter(u =>
`${u.firstName} ${u.lastName} ${u.email} ${u.role} ${u.school}`.toLowerCase().includes(s)
`${u.firstName} ${u.lastName} ${u.email} ${u.role} ${u.program}`.toLowerCase().includes(s)
)
}
if (roleFilter) users = users.filter(u => u.role === roleFilter)
Expand Down Expand Up @@ -106,11 +110,11 @@ class AllUsersTable extends React.Component<Props, State> {

handleExport = () => {
const users = this.getFilteredUsers()
const headers = ['Last Name', 'First Name', 'Email', 'Role', 'School', 'Status', 'Last Login']
const headers = ['Last Name', 'First Name', 'Email', 'Role', 'Program', 'Status', 'Last Login']
const escape = (val: string) => `"${(val || '').replace(/"/g, '""')}"`
const rows = users.map(u => [
escape(u.lastName), escape(u.firstName), escape(u.email),
escape(this.formatRole(u.role)), escape(u.school || ''),
escape(this.formatRole(u.role)), escape(u.program || ''),
escape(u.archived ? 'Archived' : 'Active'),
escape(this.formatDate(u.lastLogin))
].join(','))
Expand All @@ -129,7 +133,7 @@ class AllUsersTable extends React.Component<Props, State> {
const roles = [...new Set(this.props.users.map(u => u.role))].sort()

const SortHeader = ({ field, label }: { field: string; label: string }) => (
<th style={{ padding: '10px 8px', cursor: 'pointer' }} onClick={() => this.handleSort(field)}>
<th style={{ padding: '4px 8px', cursor: 'pointer', fontSize: '1.25rem', fontWeight: 500 }} onClick={() => this.handleSort(field)}>
<TableSortLabel active={sortField === field} direction={sortField === field ? sortDir : 'asc'}>
<strong>{label}</strong>
</TableSortLabel>
Expand Down Expand Up @@ -172,10 +176,10 @@ class AllUsersTable extends React.Component<Props, State> {
<SortHeader field="firstName" label="First Name" />
<SortHeader field="email" label="Email" />
<SortHeader field="role" label="Role" />
<SortHeader field="school" label="School" />
<SortHeader field="program" label="Program" />
<SortHeader field="archived" label="Status" />
<SortHeader field="lastLogin" label="Last Login" />
<th style={{ padding: '10px 8px' }}><strong>Actions</strong></th>
<th style={{ padding: '4px 8px', textAlign: 'center', fontSize: '1.25rem', fontWeight: 500 }}><strong>Edit</strong></th>
</tr>
</thead>
<tbody>
Expand All @@ -187,15 +191,9 @@ class AllUsersTable extends React.Component<Props, State> {
<TableCell>{user.firstName}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{this.formatRole(user.role)}</TableCell>
<TableCell>{user.school}</TableCell>
<TableCell><StatusBadge archived={user.archived}>{user.archived ? 'Archived' : 'Active'}</StatusBadge></TableCell>
<TableCell>{this.formatDate(user.lastLogin)}</TableCell>
<TableCell onClick={e => e.stopPropagation()} style={{ whiteSpace: 'nowrap' }}>
<Tooltip title="Edit user">
<IconButton size="small" onClick={() => this.props.onUserClick?.(user)}>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
<TableCell>{user.program}</TableCell>
<TableCell onClick={e => e.stopPropagation()} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<StatusBadge archived={user.archived}>{user.archived ? 'Archived' : 'Active'}</StatusBadge>
<Tooltip title={user.archived ? 'Activate user' : 'Archive user'}>
<Switch
size="small"
Expand All @@ -205,6 +203,14 @@ class AllUsersTable extends React.Component<Props, State> {
/>
</Tooltip>
</TableCell>
<TableCell>{this.formatDate(user.lastLogin)}</TableCell>
<TableCell onClick={e => e.stopPropagation()} style={{ textAlign: 'center' }}>
<Tooltip title="Edit user">
<IconButton size="small" onClick={() => this.props.onUserClick?.(user)}>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</tbody>
Expand Down
1 change: 1 addition & 0 deletions src/constants/Types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,5 +235,6 @@ export interface User {
lastLogin?: Date,
email?: string,
school?: string,
program?: string,
archived?: boolean
}
2 changes: 1 addition & 1 deletion src/views/protected/AdminViews/AllUsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,5 @@ class AllUsersPage extends React.Component<Props, State> {
}

export default connect(state => ({
isAdmin: [Role.ADMIN, Role.PROGRAMLEADER, Role.SITELEADER].includes(state.coachState.role),
isAdmin: state.coachState.role === Role.ADMIN,
}))(AllUsersPage)
57 changes: 56 additions & 1 deletion src/views/protected/UsersViews/UsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Coaches from "../../../components/UsersComponents/Coaches";
import Skeleton from "./Skeleton"
import Archives from "../../../components/UsersComponents/Archives";
import Sites from "../../../components/UsersComponents/Sites";
import AllUsersTable from "../../../components/UsersComponents/AllUsersTable";


const styles: object = {
Expand Down Expand Up @@ -108,6 +109,8 @@ interface State {
currentPage: string
propFilter: Array<string>
sendToSites: Array<Object>
allUsers: Types.User[]
allUsersLoading: boolean
}

function checkCurrent(item: string) {
Expand Down Expand Up @@ -136,7 +139,9 @@ class UsersPage extends React.Component<Props, State> {
archivedTeachers: [],
archivedCoaches: [],
propFilter: [],
sendToSites: []
sendToSites: [],
allUsers: [],
allUsersLoading: true
}
}

Expand Down Expand Up @@ -510,6 +515,10 @@ class UsersPage extends React.Component<Props, State> {
let sendToSites = this.buildSiteData(coachData, siteData, programData, filter ? filter : []);
this.setState({sendToSites: sendToSites});

// Load All Users if on that route and user is admin
if (this.props.userRole === 'admin' && location.pathname === '/LeadersAllUsers') {
this.loadAllUsers()
}

}

Expand Down Expand Up @@ -546,6 +555,29 @@ class UsersPage extends React.Component<Props, State> {
this.setState({programData: data})
}

loadAllUsers = async () => {
if (this.props.userRole !== 'admin') return
this.setState({ allUsersLoading: true })
try {
const users = await this.context.getAllUsers()
this.setState({ allUsers: users, allUsersLoading: false })
} catch (e) {
console.error('Error loading all users:', e)
this.setState({ allUsersLoading: false })
}
}

handleAllUserClick = (user: Types.User) => {
// Could open edit dialog - for now just log
console.log('User clicked:', user)
}

handleAllUserArchive = async (user: Types.User) => {
await this.context.db.collection('users').doc(user.id).update({ archived: !user.archived })
this.setState(s => ({
allUsers: s.allUsers.map(u => u.id === user.id ? { ...u, archived: !user.archived } : u)
}))
}

static propTypes = {
classes: PropTypes.exact({
Expand Down Expand Up @@ -589,6 +621,16 @@ class UsersPage extends React.Component<Props, State> {
</li>
)
})}
{userRole === 'admin' && (
<li style={{float: 'left'}}>
<a style={Styles.navLinks(checkCurrent('/LeadersAllUsers'))} onClick={() => {
this.loadAllUsers()
this.props.history.push('/LeadersAllUsers')
}}>
All Users
</a>
</li>
)}
</ul>
<ul style={{listStyle: 'none'}}>
<li style={{float: 'right', marginRight:'3vw'}}>
Expand All @@ -604,6 +646,19 @@ class UsersPage extends React.Component<Props, State> {
<Grid item xs={12} style={{paddingTop:"1em"}}>
<Switch location={location} key={location.pathname}>
<Route path="/LeadersUsers" component={Skeleton} />
<Route path="/LeadersAllUsers" render={() =>
userRole === 'admin' ? (
<div style={{ padding: '30px 30px 40px 30px' }}>
<AllUsersTable
users={this.state.allUsers}
onUserClick={this.handleAllUserClick}
onArchiveClick={this.handleAllUserArchive}
/>
</div>
) : (
<div style={{ padding: '2em' }}>You must be an admin to access this page.</div>
)
} />
<Route path="/LeadersCoaches" render={(props) =>
<Coaches
changePage={(pageName: string) => this.changePage(pageName)}
Expand Down
Loading