Skip to content
Open
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
9 changes: 8 additions & 1 deletion backend/database/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,20 @@ const adminViewHours = async (req, res) => {
checkAdminPrivilege(zid, res);
// Fetch hours data for all zids
const query = `
SELECT hours.id, hours.zid, hours.num_hours, hours.description, hours.timestamp, hours.image_url, hours.status,
SELECT hours.id, hours.zid, hours.num_hours, hours.description, hours.timestamp, hours.image_url, hours.status, hours.tags,
users.zid, users.firstname, users.lastname, users.email
FROM hours
LEFT JOIN users ON hours.zid = users.zid
`;

const { rows } = await db.query(query, []);

// parse postgresql array
rows.forEach(row => {
const tags = row.tags.slice(1, -1);
row.tags = tags ? tags.split(',') : [];
});

return res.status(200).json(rows);
} catch (error) {
console.error("Error retrieving hours:", error);
Expand Down
17 changes: 12 additions & 5 deletions backend/database/mentee.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const { Status } = require("../enums.js");

// Mentee requests hours
const requestHours = async (req, res) => {
const { numHours, description, timestamp, imageUrl } = req.body;
const { numHours, description, timestamp, imageUrl, tags } = req.body;

const zid = verifyToken(req.headers["authorization"], res);
if (zid instanceof Object) {
Expand All @@ -25,11 +25,11 @@ const requestHours = async (req, res) => {
try {
// Insert hours request into the database
const insertQuery = `
INSERT INTO hours (id, zid, num_hours, description, timestamp, image_url, status)
VALUES ($1, $2, $3, $4, $5, $6, $7)
INSERT INTO hours (id, zid, num_hours, description, timestamp, image_url, status, tags)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`;
const id = uuidv4();
const values = [id, zid, numHours, description, timestamp, imageUrl, Status.PENDING];
const values = [id, zid, numHours, description, timestamp, imageUrl, Status.PENDING, tags];

await db.query(insertQuery, values);

Expand All @@ -55,13 +55,20 @@ const menteeViewHours = async (req, res) => {
try {
// Fetch hours data for a specific zid
const query = `
SELECT id, num_hours, description, timestamp, image_url, status
SELECT id, num_hours, description, timestamp, image_url, status, tags
FROM hours
WHERE zid = $1
`;

const params = [zid];
const { rows } = await db.query(query, params);

// parse postgresql array
rows.forEach(row => {
const tags = row.tags.slice(1, -1);
row.tags = tags ? tags.split(',') : [];
});

res.status(200).json(rows);
} catch (error) {
console.error("Error retrieving hours:", error);
Expand Down
36 changes: 34 additions & 2 deletions frontend/components/mentee/AddHoursModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useForm, zodResolver } from '@mantine/form';
import { hoursRequest } from '../../types/hours';
import { hoursRequest, hoursTag } from '../../types/hours';
import { addHoursSchema } from '../../types/schemas';
import { useEffect } from 'react';
import { Backdrop, Box, Button, Fade, Modal, Stack, TextField, Typography } from '@mui/material';
import { Backdrop, Box, Button, Chip, Fade, Modal, Stack, TextField, Typography } from '@mui/material';
import { styled } from '@mui/material/styles';
import { mapTagText } from '../../lib/helpers/tags';

const ModalBox = styled(Box)(({ theme }) => ({
position: 'absolute',
Expand Down Expand Up @@ -38,6 +39,7 @@ export const AddHoursModal = ({
hours: 0,
description: '',
imageUrl: '',
tags: [],
});

form.clearErrors();
Expand All @@ -63,6 +65,8 @@ export const AddHoursModal = ({
return `${date} ${time}`;
}

const tagOptions = Object.values(hoursTag);

return (
<Modal
aria-labelledby="transition-modal-title"
Expand Down Expand Up @@ -98,6 +102,7 @@ export const AddHoursModal = ({
description: form.values?.description,
timestamp: currentDateTime,
imageUrl: form.values?.imageUrl,
tags: form.values?.tags,
});
}}
>
Expand Down Expand Up @@ -157,6 +162,33 @@ export const AddHoursModal = ({
}
style={{ width: '100%', height: 250 }}
/>
<Typography>
Select tags:
</Typography>
<Stack direction="row" sx={{ flexWrap: 'wrap', gap: '6px' }}>
{tagOptions.sort().map((tag, k) => {
const selected = form.values?.tags?.includes(tag);

return (
<Chip
key={k}
sx={{ background: selected ? '#FCB14C' : undefined,
"&:hover": selected ? { backgroundColor: '#EFA94B' } : undefined }}
clickable
label={mapTagText(tag)}
onClick={() => {
if (selected) {
// remove tag
form?.setFieldValue('tags', form.values?.tags.filter((t: hoursTag) => t != tag));
} else {
// add tag
form?.setFieldValue('tags', [...form.values?.tags, tag]);
}
}}
/>
)
})}
</Stack>
</Stack>
<Stack direction="row" justifyContent="end" spacing={2}>
<Button variant="outlined" onClick={onClose}>
Expand Down
28 changes: 27 additions & 1 deletion frontend/components/mentee/HoursTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ButtonGroup,
Chip,
Paper,
styled,
Table,
TableBody,
TableCell,
Expand All @@ -11,9 +12,10 @@ import {
TablePagination,
TableRow,
} from '@mui/material';
import { hoursAdminActions, hoursImage, hoursInfo, hoursStatus } from '../../types/hours';
import { hoursAdminActions, hoursImage, hoursInfo, hoursStatus, hoursTag } from '../../types/hours';
import { ChangeEvent, useMemo, useState } from 'react';
import { UnstyledButton } from '@mantine/core';
import { mapTagText } from '../../lib/helpers/tags';

const mapStatusColor = (status: hoursStatus) => {
switch (status) {
Expand All @@ -28,6 +30,16 @@ const mapStatusColor = (status: hoursStatus) => {
}
};

const TagsContainer = styled('div')(({ theme }) => ({
display: 'flex',
gap: '6px',
flexWrap: 'wrap',
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
alignItems: 'flex-start',
},
}));

export const HoursTable = ({
hours,
actions,
Expand Down Expand Up @@ -99,6 +111,9 @@ export const HoursTable = ({
<TableCell>
<b>Proof</b>
</TableCell>
<TableCell width={'20%'}>
<b>Tags</b>
</TableCell>
<TableCell align={actions ? 'left' : 'right'}>
<b>Status</b>
</TableCell>
Expand Down Expand Up @@ -137,6 +152,17 @@ export const HoursTable = ({
/>
</UnstyledButton>
</TableCell>
<TableCell>
<TagsContainer>
{h.tags.sort().map((tag, k) => (
<Chip
key={k}
sx={{ background: '#FCB14C'}}
label={mapTagText(tag)}
/>
))}
</TagsContainer>
</TableCell>
<TableCell align={actions ? 'left' : 'right'}>
<Chip
sx={{ textTransform: 'uppercase' }}
Expand Down
16 changes: 16 additions & 0 deletions frontend/lib/helpers/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { hoursTag } from "../../types/hours";

const mapTagText = (tag: hoursTag) => {
switch (tag) {
case hoursTag.MENTOR:
return '1-1 Mentor Meeting';
case hoursTag.TRAINING:
return 'Compulsory Training';
case hoursTag.EVENT:
return 'WIT Events';
default:
return '';
}
};

export { mapTagText };
8 changes: 8 additions & 0 deletions frontend/types/hours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface hoursInfo {
description: string;
timestamp: string;
image_url: string;
tags: hoursTag[];
status: hoursStatus;
}

Expand All @@ -17,6 +18,7 @@ export interface hoursRequest {
description: string;
timestamp: string;
imageUrl: string;
tags: hoursTag[];
}

export interface hoursApproveRequest {
Expand Down Expand Up @@ -60,3 +62,9 @@ export interface cellHookData {
raw: any;
};
}

export enum hoursTag {
MENTOR = 'MENTOR',
TRAINING = 'TRAINING',
EVENT = 'EVENT',
}
2 changes: 2 additions & 0 deletions frontend/types/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from 'zod';
import { hoursTag } from './hours';

export const addHoursSchema = z.object({
hours: z
Expand All @@ -11,4 +12,5 @@ export const addHoursSchema = z.object({
.min(2, { message: 'Description must contain at least 2 characters' })
.max(2000, { message: 'Description must be less than 2000 characters' }),
imageUrl: z.string().url({ message: 'Invalid URL' }),
tags: z.array(z.nativeEnum(hoursTag))
});