Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Staff dashboard upgrade #44

Merged
merged 2 commits into from
Mar 7, 2024
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
5 changes: 1 addition & 4 deletions global-includes/rpc-declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,5 @@ export class RemoteProcedures {
static sanitizeMessage: (message: TicketMessage) => TicketMessage;
static deleteGridImages: (paths: string[]) => Promise<void>;
static getUserResumeFilename: (userID: string) => Promise<string>;
/**
* If `base64Resume` is empty, any existing resume will be deleted
*/
static setUserResume: (userID: string, base64Resume: string | "", resumeFilename: string) => Promise<void>;
static setUserResume: (userID: string, base64Resume: string | "", resumeFilename: string) => Promise<void>; // If `base64Resume` is empty, any existing resume will be deleted
}
11 changes: 4 additions & 7 deletions global-includes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,8 @@ const noUpdate = { allowApiUpdate: false };
@Entity<User>("users", {
allowApiCrud: true,
apiPrefilter: () => (
remult.isAllowed([UserRole.Admin, UserRole.Staff]) ?
{} :
{ id: remult.user?.id }
),
remult.isAllowed([UserRole.Admin, UserRole.Staff]) ? {} : { id: remult.user?.id }
),
})
export class User extends EntityBase {
@Fields.uuid()
Expand Down Expand Up @@ -402,14 +400,13 @@ export class User extends EntityBase {
}
}

@BackendMethod({allowed: true})
@BackendMethod({ allowed: true })
static async getExistingResumeName() {
const user = remult.user as User;
if (!user) {
throw "Not logged in";
} else {
return await RemoteProcedures.getUserResumeFilename(user.id);
}
return await RemoteProcedures.getUserResumeFilename(user.id);
}

@BackendMethod({ allowed: true })
Expand Down
16 changes: 16 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ async function createServer() {
next();
}
});
// TODO: Proper way to server a resume file on an
// app.get("/api/resume/:id", remultConfig.withRemult, async (req, res) => {
// const user = remult.user;
// if (!user) {
// res.sendStatus(403);
// return;
// }

// let id = req.params.id ?? user.id;
// if (!remult.isAllowed([ UserRole.Admin, UserRole.Staff ])) {
// id = user.id;
// }

// console.log(projectRoot);
// res.sendFile(getUserResumePath(id), { root: projectRoot });
// });
if (dev) {
// in development mode, we create and use the vite and next.js development
// servers and route traffic to them based on the subdomain for the request
Expand Down
18 changes: 5 additions & 13 deletions staff-frontend/layouts/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";

import { Badge, Layout, Menu, ConfigProvider, theme } from "antd";
import { ContactsOutlined, LockOutlined, MessageOutlined, UnlockOutlined, LinkOutlined, FileImageOutlined } from "@ant-design/icons";
import { ContactsOutlined, LockOutlined, MessageOutlined, UnlockOutlined, LinkOutlined, FileImageOutlined, UserOutlined } from "@ant-design/icons";

import Link from "next/link";
import { useRouter } from "next/router";
Expand All @@ -16,19 +16,11 @@ export default function KHEStaffLayout({ children }) {
const unreadMail = useUnreadMailCount();
const pages = [
{ key: "/", label: "KHE 2024 Dashboard" },
{ icon: <UserOutlined />, key: "/users", label: "Users" },
{ icon: <ContactsOutlined />, key: "/emailLists", label: "Email Lists" },
{
icon: <MessageOutlined />, key: "/tickets",
label: <span>Support Tickets <Badge count={unreadMail} /></span>
},
{
icon: <LinkOutlined />, key: "/redirects",
label: "Redirect Links"
},
{
icon: <FileImageOutlined />, key: "/imageLayout",
label: "Layout Images"
}
{ icon: <MessageOutlined />, key: "/tickets", label: <span>Support Tickets <Badge count={unreadMail} /></span> },
{ icon: <LinkOutlined />, key: "/redirects", label: "Redirect Links" },
{ icon: <FileImageOutlined />, key: "/imageLayout", label: "Layout Images" }
].map(page => ({ ...page, label: (<Link href={page.key}>{page.label}</Link>) }));

const user = useUser()[0];
Expand Down
6 changes: 1 addition & 5 deletions staff-frontend/pages/emailLists.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Email, EmailListNotes, EmailSource, SentListMail, isEmailRegex } from "
import { Card, Layout, Button, Input, Upload, Popconfirm, Select, Modal } from "antd";
const { Sider, Footer, Content } = Layout;
const { confirm, error } = Modal;
const { Meta } = Card;
import { PlusOutlined, UploadOutlined, DeleteOutlined, FileAddFilled, SaveOutlined } from "@ant-design/icons";
import layoutStyle from "../layouts/layout.module.css";
import style from "./emailLists.module.css";
Expand Down Expand Up @@ -222,10 +221,7 @@ export default function EmailLists() {
keyForAddButton={menuKeys.add}
enclose={editableSection => [
{ key: menuKeys.compose, label: "Write a mail" },
{
key: menuKeys.list, label: "Email Lists",
children: editableSection
},
{ key: menuKeys.list, label: "Email Lists", children: editableSection },
{ key: menuKeys.sent, label: "Mail what was sent" }
]}
/>
Expand Down
116 changes: 116 additions & 0 deletions staff-frontend/pages/users.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useEffect, useState } from "react";
import { remult } from "remult";
import KHELayout from "../layouts/layout";
import { User } from "../../global-includes/users";

import { Button, Card, Layout, Modal, Row, Col, Divider } from "antd";
const { Content } = Layout;

export default function UsersManager() {

const [users, setUsers] = useState([]);
const [viewing, setViewing] = useState(null);
const [loading, setLoading] = useState(false);
const repo = remult.repo(User);

const showReview = (user) => setViewing(user);
const closeReview = () => setViewing(null);

const approveUser = async (user) => {
setLoading(true);
await repo.save({ id: user.id, applicationApproved: true });
closeReview();
setLoading(false);
update()
}

const checkInUser = async (user) => {
setLoading(true);
await repo.save({ id: user.id, checkedIn: true });
closeReview();
setLoading(false);
update()
}

const loadUsers = () => {
repo.find().then(users => setUsers(users));
}

const getActions = (user) => {
const checkedIn = user.checkedIn;
const actions = [<Button key="view" onClick={() => showReview(user)}>View Application</Button>];
if (user.applicationApproved)
actions.push(<Button key="checkin" disabled={checkedIn} type="primary" onClick={() => checkInUser(user)}>{checkedIn ? "Checked in" : "Check in"}</Button>)
return actions;
}

useEffect(loadUsers, []);

const cardStyle = {
width: 350,
margin: 6
}

return <KHELayout>
<Layout>
<Content>
{users.map((user, i) =>
<div key={i}>
<Card
title={user.email}
extra={<small>{user.roles.join(", ")}</small>}
actions={getActions(user)}
style={cardStyle}>
{user.submittedApplication && !user.applicationApproved && <strong>This user is awaiting approval!</strong>}
<p>This account is registered with <strong>{user.method}</strong>. It was created on <strong>{user.createdAt.toLocaleDateString()}</strong>.</p>
</Card>
</div>
)}
</Content>
</Layout>

{/* TODO: really do not like this really long JSON access syntax (viewing.registration.someotherlongname),
this should become it's own component at some point
*/}
<Modal
width="800px"
title={<p>Application for <strong>{viewing?.registration.name}</strong></p>}
open={viewing !== null}
onCancel={closeReview}
onOk={() => approveUser(viewing)}
okButtonProps={viewing?.applicationApproved ? { disabled: true } : { loading }}
okText="Approve"
>
{viewing &&
<>
<Divider orientation="left" plain>Personal</Divider>
<Row gutter={16}>
<Col span={8}><strong>Age:</strong> {viewing.registration.age}</Col>
<Col span={8}><strong>School:</strong> {viewing.registration.school}</Col>
<Col span={8}><strong>Phone:</strong> {viewing.registration.phone}</Col>
<Col span={8}><strong>Class Standing:</strong> {viewing.registration.schoolStatus}</Col>
<Col span={8}><strong>Gender:</strong> {viewing.registration.gender}</Col>
<Col span={8}><strong>Major:</strong> {viewing.registration.major}</Col>
<Col span={8}><strong>Website:</strong> {viewing.registration.link}</Col>
<Col span={8}><strong>Attended KHE:</strong> {viewing.registration.attendedKhe ? "Yes" : "No"}</Col>
<Col span={8}><strong>Pronouns:</strong> {viewing.registration.pronouns}</Col>
<Col span={8}><strong>Ethnicity:</strong> {viewing.registration.ethnicity}</Col>
<Col span={8}><strong>Sexuality:</strong> {viewing.registration.sexuality}</Col>
<Col span={8}><strong>Shirt Size:</strong> {viewing.registration.shirtSize}</Col>
<Col span={8}><strong>State:</strong> {viewing.registration.state}</Col>
<Col span={8}><strong>Country:</strong> {viewing.registration.country}</Col>
</Row>
<Divider orientation="left" plain>Dietary Restrictions</Divider>
<Row gutter={16}>
<Col span={12}>{viewing.registration.dietaryRestrictions.join(", ") || "This user has no dietary restrictions."}</Col>
</Row>
<Divider orientation="left" plain>MLH</Divider>
<Row gutter={16}>
<Col span={12}>{viewing.registration.name} has <strong>{viewing.registration.mlhConduct ? "accepted" : "not accepted"}</strong> the MLH Code of Conduct.</Col>
<Col span={12}>{viewing.registration.name} has <strong>{viewing.registration.mlhShare ? "accepted" : "not accepted"}</strong> the MLH Share.</Col>
</Row>
</>
}
</Modal>
</KHELayout>
}