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
71 changes: 42 additions & 29 deletions frontend/src/components/ExportButton/ExportButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import React, { useState, useRef } from 'react';
import { CSVLink } from 'react-csv';
import { download } from '../../icons/other';
import { Button } from '../FormElements/FormElements';
import styles from './exportButton.module.css';
Expand All @@ -13,36 +11,58 @@ type clickHandler = {
filename: string;
};


/**
* ExportButton
*
* A reusable button that fetches CSV data from a backend endpoint and triggers
* a download in the browser. Displays a toast on success or failure.
*
* @param toastMsg - The message to display in the toast on successful download.
* @param endpoint - The backend API endpoint to fetch CSV data from.
* @param csvCols - CSV string to use if the backend response is empty.
* @param filename - the filename for the downloaded CSV file.
*
* - Fetches CSV data from the specified `endpoint`.
* - Uses `csvCols` as fallback if the server returns empty.
* - Initiates download of CSV with the given `filename`.
* - Shows toast notifications for success or error with `useToast` context.
*/

const ExportButton = ({
toastMsg,
endpoint,
csvCols,
filename,
}: clickHandler) => {
const [downloadData, setDownloadData] = useState<string>('');
const { showToast } = useToast();
const csvLink = useRef<
CSVLink & HTMLAnchorElement & { link: HTMLAnchorElement }
>(null);

const downloadCSV = () => {
axios
.get(endpoint, {
const downloadCSV = async () => {
try {
// fetch csv string from stats table in backend
const res = await axios.get(endpoint, {
responseType: 'text',
transformResponse: [(data) => data],
})
.then((res) => res.data)
.then((data) => {
if (data === '') {
setDownloadData(csvCols);
} else {
setDownloadData(data);
}
if (csvLink.current) {
csvLink.current.link.click();
}
})
.then(() => showToast(toastMsg, ToastStatus.SUCCESS));
});
const data = res.data || csvCols;

// generate a download link and initiate the download
const blob = new Blob([data], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename || 'download.csv';
document.body.appendChild(link);
link.click();

//cleanup
document.body.removeChild(link);
URL.revokeObjectURL(url);

showToast(toastMsg, ToastStatus.SUCCESS);
} catch (error) {
showToast('Failed to download CSV', ToastStatus.ERROR);
}
};

return (
Expand All @@ -54,13 +74,6 @@ const ExportButton = ({
>
<img src={download} alt="capacity icon" /> Export
</Button>
<CSVLink
data={downloadData}
filename={filename}
className={styles.hidden}
ref={csvLink}
target="_blank"
/>
</>
);
};
Expand Down
268 changes: 268 additions & 0 deletions frontend/src/components/Modal/StatsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { useEmployees } from 'context/EmployeesContext';
import moment from 'moment';
import { Driver } from 'types';
import ExportButton from 'components/ExportButton/ExportButton';
import dayjs from 'dayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import styles from '../Modal/modal.module.css';
import { useState } from 'react';
import { useEffect } from 'react';
import { Button } from 'components/FormElements/FormElements';

const theme = createTheme();

type StatsModalProps = {
initStartDate: string;
initEndDate: string;
fromWho: string;
};

/**
* StatsModal Component
*
* A modal dialog that allows users to export ride statistics as a CSV file.
* Includes date range selection, shortcut buttons for common ranges,
* and conditional logic depending on whether the user is an admin or driver.
*
* @param initStartDate - Initial start date for the export (YYYY-MM-DD).
* @param initEndDate - Initial end date for the export (YYYY-MM-DD).
* @param fromWho - Determines user type: 'admin' or 'driver', affects CSV structure and endpoint.
*/

const StatsModal = ({
initStartDate,
initEndDate,
fromWho,
}: StatsModalProps) => {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const { drivers } = useEmployees();
const [startDate, setStartDate] = useState(initStartDate);
const [endDate, setEndDate] = useState(initEndDate);
const today = moment();

useEffect(() => {
if (open) {
setStartDate(initStartDate);
setEndDate(initEndDate);
}
}, [initStartDate, initEndDate, open]);

//Creates csv column string
const generateCols = () => {
const cols =
'Date,Daily Total,Daily Ride Count,Day No Shows,Day Cancels,Night Ride Count, Night No Shows, Night Cancels';
const finalCols = drivers.reduce(
(acc: string, curr: Driver) =>
`${acc},${curr.firstName} ${curr.lastName.substring(0, 1)}.`,
cols
);
return finalCols;
};

const colsByPerson = (fromWho: string) => {
if (fromWho === 'admin') return generateCols();
else {
return 'Date, Start Time, End Time, From, To, Status, Type';
}
};

const endpointByPerson = (fromWho: string) => {
if (fromWho === 'admin') return `/api/stats/download?from=${startDate}&to=${endDate}`;
else {
return `/api/rides/download/driver?from=${startDate}&to=${endDate}`;
}
};



//logic for handling when start and end dates are for shortcut buttons
const handleShortcut = (shortcut: string) => {
const today = dayjs();
if (shortcut === 'this week') {
const newStartDate = today.startOf('week').format('YYYY-MM-DD'); //beginning of week to today
const newEndDate = today.format('YYYY-MM-DD');
return { newStartDate, newEndDate };
} else if (shortcut === 'last week') {
const prevWeek = today.subtract(7, 'day'); //seven days ago to today
const newStartDate = prevWeek.startOf('week').format('YYYY-MM-DD');
const newEndDate = prevWeek.endOf('week').format('YYYY-MM-DD');
return { newStartDate, newEndDate };
} else if (shortcut === 'last 7') {
const newStartDate = today.subtract(7, 'day').format('YYYY-MM-DD'); //beginning of last week to end of last week
const newEndDate = today.format('YYYY-MM-DD');
return { newStartDate, newEndDate };
} else if (shortcut === 'curr month') {
const newStartDate = today.startOf('month').format('YYYY-MM-DD'); //beginning of month to today
const newEndDate = today.format('YYYY-MM-DD');
return { newStartDate, newEndDate };
} else if (shortcut === 'curr year') {
const newStartDate = today.startOf('year').format('YYYY-MM-DD'); //beginning of year to today
const newEndDate = today.format('YYYY-MM-DD');
return { newStartDate, newEndDate };
}
};

return (
<ThemeProvider theme={theme}>
<div style={{ alignContent: 'space-between' }}>
<Button onClick={handleOpen}>Export Data</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
slotProps={{
backdrop: {
className: styles.background,
},
}}
>
<Box className={styles.modal}>
<h3 className={styles.title} id="modal-modal-title">
Export Statistics
</h3>
<div className={styles.colBlock}>
<div className={styles.rowBlock}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="Start Date"
value={dayjs(startDate)}
onChange={(newValue) => {
if (newValue) {
setStartDate(newValue.format('YYYY-MM-DD'));
}
}}
maxDate={dayjs(today.format('YYYY-MM-DD'))} //data validation
/>

<DatePicker
label="End Date"
value={dayjs(endDate)}
onChange={(newValue) => {
if (newValue) {
setEndDate(newValue.format('YYYY-MM-DD'));
}
}}
minDate={dayjs(startDate)} //data validation
maxDate={dayjs(today.format('YYYY-MM-DD'))}
/>
</LocalizationProvider>
</div>
</div>

<div className={styles.btnList}>
<button
className={styles.btnListItem}
onClick={(event) => {
const newStartDate =
handleShortcut('this week')?.newStartDate ??
today.format('YYYY-MM-DD');
const newEndDate =
handleShortcut('this week')?.newEndDate ??
today.format('YYYY-MM-DD');
setStartDate(newStartDate);
setEndDate(newEndDate);
}}
>
This Week
</button>

<button
className={styles.btnListItem}
onClick={(event) => {
const newStartDate =
handleShortcut('last week')?.newStartDate ??
today.format('YYYY-MM-DD');
const newEndDate =
handleShortcut('last week')?.newEndDate ??
today.format('YYYY-MM-DD');
setStartDate(newStartDate);
setEndDate(newEndDate);
}}
>
Last Week{' '}
</button>

<button
className={styles.btnListItem}
onClick={(event) => {
const newStartDate =
handleShortcut('last 7')?.newStartDate ??
today.format('YYYY-MM-DD');
const newEndDate =
handleShortcut('last 7')?.newEndDate ??
today.format('YYYY-MM-DD');
setStartDate(newStartDate);
setEndDate(newEndDate);
}}
>
Last 7 Days
</button>
<button
className={styles.btnListItem}
onClick={(event) => {
const newStartDate =
handleShortcut('curr month')?.newStartDate ??
today.format('YYYY-MM-DD');
const newEndDate =
handleShortcut('curr month')?.newEndDate ??
today.format('YYYY-MM-DD');
setStartDate(newStartDate);
setEndDate(newEndDate);
}}
>
Current Month
</button>
<button
className={styles.btnListItem}
onClick={(event) => {
const newStartDate =
handleShortcut('curr year')?.newStartDate ??
today.format('YYYY-MM-DD');
const newEndDate =
handleShortcut('curr year')?.newEndDate ??
today.format('YYYY-MM-DD');
setStartDate(newStartDate);
setEndDate(newEndDate);
}}
>
Year to Date
</button>
</div>

<div
className={styles.buttonContainer}
style={{ marginTop: '10px' }}
>
<button
type="button"
className={styles.closeBtn}
onClick={handleClose}
>
Cancel
</button>
<div className={styles.submit}>
<ExportButton
toastMsg={`${startDate} to ${endDate} data has been downloaded.`}
endpoint={endpointByPerson(fromWho)}
csvCols={colsByPerson(fromWho)}
filename={`${startDate}_${endDate}_analytics.csv`}
/>
</div>
</div>
</Box>
</Modal>
</div>
</ThemeProvider>
);
};

export default StatsModal;
Loading
Loading