From f7ac5e09aed43b5df651b08cdedd9a5a25c48a1b Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Tue, 7 May 2024 16:17:26 +0800 Subject: [PATCH 01/14] Add page for responses --- components/Event/Event.tsx | 4 +++- pages/event/[pid]/responses.tsx | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 pages/event/[pid]/responses.tsx diff --git a/components/Event/Event.tsx b/components/Event/Event.tsx index e06238a..d29624b 100644 --- a/components/Event/Event.tsx +++ b/components/Event/Event.tsx @@ -174,7 +174,9 @@ const Event = ({ pid }: { pid: string }) => { ) : ( diff --git a/pages/event/[pid]/responses.tsx b/pages/event/[pid]/responses.tsx new file mode 100644 index 0000000..e2192e7 --- /dev/null +++ b/pages/event/[pid]/responses.tsx @@ -0,0 +1,20 @@ +import Link from 'next/link'; +import Image from 'next/image'; + +export default function responses() { + return ( + <> + window.history.back()} + style={{ + margin: '0 0 0 20px', + }}> + + + responses + + ); +} + +export { getServerSideProps } from '@/lib/getServerSideProps'; From d46839b8a5abe0bbf3d5e849a1d124be38f2e5c7 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Tue, 7 May 2024 16:25:03 +0800 Subject: [PATCH 02/14] Copy and paste volunteer log table over --- .../ApplicationTable/ApplicationTable.tsx | 255 ++++++++++++++++++ .../ApplicationTable/ApplicationTableImpl.tsx | 131 +++++++++ pages/event/[pid]/responses.tsx | 3 +- 3 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 components/Table/ApplicationTable/ApplicationTable.tsx create mode 100644 components/Table/ApplicationTable/ApplicationTableImpl.tsx diff --git a/components/Table/ApplicationTable/ApplicationTable.tsx b/components/Table/ApplicationTable/ApplicationTable.tsx new file mode 100644 index 0000000..a525a2c --- /dev/null +++ b/components/Table/ApplicationTable/ApplicationTable.tsx @@ -0,0 +1,255 @@ +import React, { useState, useEffect, useRef, createContext } from 'react'; +import type { TableProps } from 'antd'; +import { Button, Space, Table, Input, Tag, InputRef, message } from 'antd'; +import type { + ColumnType, + FilterValue, + SorterResult, +} from 'antd/es/table/interface'; +import Highlighter from 'react-highlight-words'; +import { SearchOutlined } from '@ant-design/icons'; +import * as XLSX from 'xlsx'; +import { saveAs } from 'file-saver'; +import { TableContainer } from '@/styles/table.styles'; +import type { FilterConfirmProps } from 'antd/es/table/interface'; +import { + VolunteerLogDataIndex, + VolunteerLogRowData, +} from '@/utils/table-types'; +import ApplicationTableImpl from './ApplicationTableImpl'; + +export const VolunteerLogTableContext = createContext<{ + getColumnSearchProps: ( + dataIndex: VolunteerLogDataIndex + ) => ColumnType; + rowSelection: any; + sortedInfo: SorterResult; + handleChange: TableProps['onChange']; + successMessage: (str: string) => void; + errorMessage: (str: string) => void; +}>({ + getColumnSearchProps: () => ({}), + rowSelection: {}, + sortedInfo: {}, + handleChange: () => {}, + successMessage: () => {}, + errorMessage: () => {}, +}); + +const ApplicationTable = () => { + const [filterTable, setFilterTable] = useState([]); + const [isFiltering, setIsFilter] = useState(false); + const [filteredInfo, setFilteredInfo] = useState< + Record + >({}); + const [sortedInfo, setSortedInfo] = useState< + SorterResult + >({}); + const [searchText, setSearchText] = useState(''); + const [searchedColumn, setSearchedColumn] = useState(''); + const searchInput = useRef(null); + + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const onSelectChange = newSelectedRowKeys => { + setSelectedRowKeys(newSelectedRowKeys); + }; + + const rowSelection = { + selectedRowKeys, + onChange: onSelectChange, + }; + + const handleApproveLog = () => { + console.log(selectedRowKeys); + }; + + const handleRejectLog = () => { + console.log(selectedRowKeys); + }; + + const handleChange: TableProps['onChange'] = ( + pagination, + filters, + sorter + ) => { + setFilteredInfo(filters); + setSortedInfo(sorter as SorterResult); + }; + + const handleSearch = ( + selectedKeys: string[], + confirm: (param?: FilterConfirmProps) => void, + dataIndex: VolunteerLogDataIndex + ) => { + confirm(); + setSearchText(selectedKeys[0]); + setSearchedColumn(dataIndex); + }; + + const handleReset = (clearFilters: () => void) => { + clearFilters(); + setSearchText(''); + }; + + // Function to get column search properties for VolunteerLogRowData + const getColumnSearchProps = ( + dataIndex: VolunteerLogDataIndex + ): ColumnType => ({ + // Configuration for the filter dropdown + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + close, + }) => ( + // Custom filter dropdown UI +
e.stopPropagation()}> + {/* Input for searching */} + + setSelectedKeys(e.target.value ? [e.target.value] : []) + } + onPressEnter={() => + handleSearch(selectedKeys as string[], confirm, dataIndex) + } + style={{ marginBottom: 8, display: 'block' }} + /> + {/* Buttons for search, reset, filter, and close */} + + + + {/* Filter button with logic to set search parameters */} + + {/* Close button for the filter dropdown */} + + +
+ ), + // Configuration for the filter icon + filterIcon: (filtered: boolean) => ( + + ), + // Filtering logic applied on each record + onFilter: (value, record) => + (record[dataIndex] ?? '') + .toString() + .toLowerCase() + .includes((value as string).toLowerCase()), + // Callback when the filter dropdown visibility changes + onFilterDropdownOpenChange: visible => { + // Select the search input when the filter dropdown opens + if (visible) { + setTimeout(() => searchInput.current?.select(), 100); + } + }, + // Render function to highlight search results + render: text => + searchedColumn === dataIndex ? ( + + ) : ( + text + ), + }); + + const clearFilters = () => { + setFilteredInfo({}); + }; + + // function to export what is on the table at the time to an excel file + const handleExport = () => { + const workbook = XLSX.utils.table_to_book( + document.querySelector('#table-container') + ); + const excelBuffer = XLSX.write(workbook, { + bookType: 'xlsx', + type: 'array', + }); + const blob = new Blob([excelBuffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + // TODO: automatically add date to file name for easier organization + saveAs(blob, 'volunteers.xlsx'); + }; + + const [messageApi, contextHolder] = message.useMessage(); + + const successMessage = (message: string) => { + messageApi.open({ + type: 'success', + content: message, + }); + }; + + const errorMessage = (message: string) => { + messageApi.open({ + type: 'error', + content: message, + }); + }; + + return ( + <> + {contextHolder} + + + + + + + ); +}; + +export default ApplicationTable; diff --git a/components/Table/ApplicationTable/ApplicationTableImpl.tsx b/components/Table/ApplicationTable/ApplicationTableImpl.tsx new file mode 100644 index 0000000..1024f17 --- /dev/null +++ b/components/Table/ApplicationTable/ApplicationTableImpl.tsx @@ -0,0 +1,131 @@ +import { VolunteerLogRowData } from '@/utils/table-types'; +import { fetcher } from '@/utils/utils'; +import { QueriedVolunteerLogDTO } from 'bookem-shared/src/types/database'; +import React, { useContext, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import Link from 'next/link'; +import { Table } from 'antd'; +import { convertVolunteerLogDataToRowData } from '@/utils/table-utils'; +import { TableContainer } from '@/styles/table.styles'; +import { ColumnsType, Key } from 'antd/es/table/interface'; +import { VolunteerLogTableContext } from './ApplicationTable'; +import { LOCALE_DATE_FORMAT } from '@/utils/constants'; + +const ApplicationTableImpl = () => { + const { + getColumnSearchProps, + rowSelection, + sortedInfo, + handleChange, + errorMessage, + } = useContext(VolunteerLogTableContext); + + const [status, setStatus] = useState('pending'); + const { data, error, isLoading, mutate } = useSWR( + '/api/volunteer-logs/' + status, + fetcher, + { + onSuccess: data => { + setDataForTable(convertVolunteerLogDataToRowData(data)); + }, + revalidateOnFocus: true, + revalidateOnReconnect: true, + } + ); + + const handleSelectStatus = (value: string) => { + fetch('/api/volunteer-logs/' + value) + .then(data => data.json()) + .then(data => setDataForTable(convertVolunteerLogDataToRowData(data))) + .then(() => setStatus(value)) + .catch(err => { + errorMessage('Sorry an error occurred'); + console.error(err); + }); + }; + + const [dataForTable, setDataForTable] = useState([]); + + const columns: ColumnsType = [ + { + title: 'Volunteer', + dataIndex: 'userName', + key: 'userName', + ...getColumnSearchProps('userName'), + ellipsis: true, + }, + { + title: 'Volunteer Email', + dataIndex: 'userEmail', + key: 'userEmail', + render(_: any, { userEmail }: VolunteerLogRowData) { + return {userEmail}; + }, + ellipsis: true, + }, + { + title: 'Event', + dataIndex: 'eventName', + key: 'eventName', + ...getColumnSearchProps('eventName'), + ellipsis: true, + }, + { + title: 'Date attended', + dataIndex: 'date', + key: 'date', + // Custom sorter based on date values + sorter: (a: VolunteerLogRowData, b: VolunteerLogRowData) => { + return a.date.getTime() - b.date.getTime(); + }, + // Configuring the sort order based on the 'date' column + sortOrder: sortedInfo.columnKey === 'date' ? sortedInfo.order : null, + + render(_: any, { date }: VolunteerLogRowData) { + return <>{date.toLocaleString('en-US', LOCALE_DATE_FORMAT)}; + }, + }, + { + title: 'Hours', + dataIndex: 'hours', + key: 'hours', + }, + { + title: 'Books Donated', + dataIndex: 'numBooks', + key: 'numBooks', + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + }, + ]; + // Refetch data when data is updated + useEffect(() => { + mutate(); + }, [mutate, data]); + + // check for errors and loading + if (error) return
Failed to load event table
; + if (isLoading) return
Loading...
; + + return ( + <> + +
+ + + + + ); +}; + +export default ApplicationTableImpl; diff --git a/pages/event/[pid]/responses.tsx b/pages/event/[pid]/responses.tsx index e2192e7..dc5b495 100644 --- a/pages/event/[pid]/responses.tsx +++ b/pages/event/[pid]/responses.tsx @@ -1,5 +1,6 @@ import Link from 'next/link'; import Image from 'next/image'; +import ApplicationTable from '@/components/Table/ApplicationTable/ApplicationTable'; export default function responses() { return ( @@ -12,7 +13,7 @@ export default function responses() { }}> - responses + ); } From ef8f187c818e605db75bb11a71aa4804b5f4cff1 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Tue, 7 May 2024 16:26:47 +0800 Subject: [PATCH 03/14] Copy table header --- .../ApplicationTable/ApplicationTableImpl.tsx | 6 + .../Table/ApplicationTable/TableHeader.tsx | 144 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 components/Table/ApplicationTable/TableHeader.tsx diff --git a/components/Table/ApplicationTable/ApplicationTableImpl.tsx b/components/Table/ApplicationTable/ApplicationTableImpl.tsx index 1024f17..190cc11 100644 --- a/components/Table/ApplicationTable/ApplicationTableImpl.tsx +++ b/components/Table/ApplicationTable/ApplicationTableImpl.tsx @@ -10,6 +10,7 @@ import { TableContainer } from '@/styles/table.styles'; import { ColumnsType, Key } from 'antd/es/table/interface'; import { VolunteerLogTableContext } from './ApplicationTable'; import { LOCALE_DATE_FORMAT } from '@/utils/constants'; +import TableHeader from './TableHeader'; const ApplicationTableImpl = () => { const { @@ -113,6 +114,11 @@ const ApplicationTableImpl = () => { return ( <> +
void; + mutate: () => void; + status: string; +}) => { + const { rowSelection, errorMessage, successMessage } = useContext( + VolunteerLogTableContext + ); + + const [statusOptions, setStatusOptions] = useState([]); + + useEffect(() => { + setStatusOptions( + Object.values(VolunteerLogStatus).map(value => ({ value, label: value })) + ); + }, []); + + const handleApprove = () => { + if (rowSelection.selectedRowKeys.length === 0) { + errorMessage('No rows selected'); + return; + } + + fetch('/api/volunteer-logs/approved', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(rowSelection.selectedRowKeys), + }) + .then(response => { + if (!response.ok) { + throw new Error('Failed to approve hours'); + } + return response.json(); + }) + .then(data => { + if (data.status === 'error') { + errorMessage(data.message); + } else { + successMessage(data.message); + } + }) + .then(() => mutate()) + .catch(err => { + errorMessage('Sorry an error occurred'); + console.error(err); + }); + }; + + const handleReject = () => { + if (rowSelection.selectedRowKeys.length === 0) { + errorMessage('No rows selected'); + return; + } + fetch('/api/volunteer-logs/rejected', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(rowSelection.selectedRowKeys), + }) + .then(response => { + if (!response.ok) { + throw new Error('Failed to reject hours'); + } + return response.json(); + }) + .then(data => { + if (data.status === 'error') { + errorMessage(data.message); + } else { + successMessage(data.message); + } + }) + .then(() => mutate()) + .catch(err => { + errorMessage('Sorry an error occurred'); + console.error(err); + }); + }; + + return ( + <> + + + Choose status: +