diff --git a/app/components/Reporting/ManageReports.tsx b/app/components/Reporting/ManageReports.tsx
new file mode 100644
index 0000000000..4ac2f663ab
--- /dev/null
+++ b/app/components/Reporting/ManageReports.tsx
@@ -0,0 +1,108 @@
+import styled from 'styled-components';
+import { useToast } from 'components/AppProvider';
+import { useState } from 'react';
+import { DateTime } from 'luxon';
+import { useArchiveReportingGcpeMutation } from 'schema/mutations/reporting/archiveReportingGcpeMutation';
+import { ConnectionHandler } from 'relay-runtime';
+import * as Sentry from '@sentry/nextjs';
+import ReportRow from './ReportRow';
+
+const StyledH2 = styled.h2`
+ margin-top: 12px;
+`;
+
+const ManageReports = ({ reportList, connectionId }) => {
+ const { showToast, hideToast } = useToast();
+ const [updateReportingGcpeByRowId] = useArchiveReportingGcpeMutation();
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleArchive = (report) => {
+ const { __id: reportConnectionId, rowId, createdAt } = report;
+ const formattedFileName = `Generated ${DateTime.fromISO(createdAt)
+ .setZone('America/Los_Angeles')
+ .toLocaleString(DateTime.DATETIME_FULL)}`;
+ hideToast();
+ setIsLoading(true);
+ const variables = {
+ input: {
+ reportingGcpePatch: {
+ archivedAt: new Date().toISOString(),
+ },
+ rowId,
+ },
+ };
+ updateReportingGcpeByRowId({
+ variables,
+ updater: (store) => {
+ const connection = store.get(connectionId);
+ store.delete(reportConnectionId);
+ ConnectionHandler.deleteNode(connection, reportConnectionId);
+ },
+ onError: (response) => {
+ setIsLoading(false);
+ showToast(
+ 'Error archiving GCPE file. Please try again later.',
+ 'error',
+ 5000
+ );
+ Sentry.captureException({
+ name: `Error archiving GCPE file: ${formattedFileName}`,
+ message: response,
+ });
+ },
+ onCompleted: () => {
+ setIsLoading(false);
+ showToast('GCPE file archived successfully', 'success', 5000);
+ },
+ });
+ };
+
+ const handleDownload = async (report: any) => {
+ const { rowId, createdAt } = report;
+ await fetch('/api/reporting/gcpe/regenerate', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ rowId }),
+ })
+ .then((response) => {
+ response.blob().then((blob) => {
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${DateTime.fromISO(createdAt)
+ .setZone('America/Los_Angeles')
+ .toLocaleString(
+ DateTime.DATETIME_FULL
+ )}_Connectivity_Projects_GCPE_List.xlsx`;
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ });
+ })
+ .catch((error) => {
+ showToast('Error downloading file. Please try again later.', error);
+ });
+ };
+
+ return (
+ <>
+ Manage My Reports
+ {reportList.map((edge) => (
+ handleDownload(edge.node)}
+ onArchive={() => handleArchive(edge.node)}
+ isLoading={isLoading}
+ reportType="GCPE"
+ />
+ ))}
+ >
+ );
+};
+
+export default ManageReports;
diff --git a/app/components/Reporting/ReportRow.tsx b/app/components/Reporting/ReportRow.tsx
new file mode 100644
index 0000000000..17da79eccb
--- /dev/null
+++ b/app/components/Reporting/ReportRow.tsx
@@ -0,0 +1,87 @@
+import { faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import useModal from 'lib/helpers/useModal';
+import GenericConfirmationModal from 'lib/theme/widgets/GenericConfirmationModal';
+import { DateTime } from 'luxon';
+import styled from 'styled-components';
+
+const StyledFileDiv = styled('div')`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ word-break: break-word;
+ margin-left: 16px;
+ margin-top: 10px;
+ & svg {
+ margin: 0px 8px;
+ }
+`;
+
+const StyledDeleteBtn = styled('button')`
+ &:hover {
+ opacity: 0.6;
+ }
+`;
+
+const StyledLink = styled.button`
+ width: 380px;
+ color: ${(props) => props.theme.color.links};
+ text-decoration-line: underline;
+ text-align: left;
+`;
+
+const ReportRow = ({
+ report,
+ onDownload,
+ onArchive,
+ isLoading,
+ reportType,
+}) => {
+ const { id, createdAt } = report;
+
+ const deleteConfirmationModal = useModal();
+
+ const formattedFileName = `Generated ${DateTime.fromISO(createdAt)
+ .setZone('America/Los_Angeles')
+ .toLocaleString(DateTime.DATETIME_FULL)}`;
+
+ return (
+ <>
+
+ {
+ e.preventDefault();
+ onDownload();
+ }}
+ >
+ {formattedFileName}
+
+ ) => {
+ e.preventDefault();
+ deleteConfirmationModal.open();
+ }}
+ disabled={isLoading}
+ >
+
+
+
+ {
+ onArchive();
+ deleteConfirmationModal.close();
+ }}
+ {...deleteConfirmationModal}
+ />
+ >
+ );
+};
+
+export default ReportRow;
diff --git a/app/components/Reporting/Tabs.tsx b/app/components/Reporting/Tabs.tsx
index 4ee3a86ae2..7ada9fe983 100644
--- a/app/components/Reporting/Tabs.tsx
+++ b/app/components/Reporting/Tabs.tsx
@@ -10,12 +10,19 @@ const StyledNav = styled.nav`
const Tabs = () => {
const router = useRouter();
const gcpeHref = '/analyst/reporting/gcpe';
+ const manageReportsHref = '/analyst/reporting/manage-reports';
// this a bare page to handle any future reporting tabs
return (
GCPE
+
+ Manage Reports
+
);
};
diff --git a/app/pages/analyst/reporting/manage-reports.tsx b/app/pages/analyst/reporting/manage-reports.tsx
new file mode 100644
index 0000000000..9ad7d3f861
--- /dev/null
+++ b/app/pages/analyst/reporting/manage-reports.tsx
@@ -0,0 +1,79 @@
+import { Layout } from 'components';
+import { DashboardTabs } from 'components/AnalystDashboard';
+import { graphql } from 'react-relay';
+import { usePreloadedQuery } from 'react-relay/hooks';
+import { withRelay, RelayProps } from 'relay-nextjs';
+import styled from 'styled-components';
+import defaultRelayOptions from 'lib/relay/withRelayOptions';
+import Tabs from 'components/Reporting/Tabs';
+import ManageReports from 'components/Reporting/ManageReports';
+import { manageReportsQuery } from '__generated__/manageReportsQuery.graphql';
+
+const getManageReportingQuery = graphql`
+ query manageReportsQuery {
+ allReportingGcpes(
+ first: 9999
+ condition: { archivedAt: null }
+ orderBy: ID_DESC
+ ) @connection(key: "ManageReporting_allReportingGcpes") {
+ __id
+ edges {
+ node {
+ __id
+ id
+ rowId
+ createdAt
+ createdBy
+ ccbcUserByCreatedBy {
+ id
+ sessionSub
+ }
+ }
+ }
+ }
+ session {
+ sub
+ ...DashboardTabs_query
+ }
+ }
+`;
+
+const StyledContainer = styled.div`
+ width: 100%;
+ height: 100%;
+`;
+
+const ManageReporting = ({
+ preloadedQuery,
+}: RelayProps, manageReportsQuery>) => {
+ const query = usePreloadedQuery(getManageReportingQuery, preloadedQuery);
+ const { allReportingGcpes, session } = query;
+
+ const reportList =
+ allReportingGcpes &&
+ [...allReportingGcpes.edges].filter((data: any) => {
+ return (
+ data.node !== null &&
+ data.node?.ccbcUserByCreatedBy.sessionSub === session?.sub
+ );
+ });
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default withRelay(
+ ManageReporting,
+ getManageReportingQuery,
+ defaultRelayOptions
+);
diff --git a/app/schema/mutations/reporting/archiveReportingGcpeMutation.ts b/app/schema/mutations/reporting/archiveReportingGcpeMutation.ts
new file mode 100644
index 0000000000..67b23f2eae
--- /dev/null
+++ b/app/schema/mutations/reporting/archiveReportingGcpeMutation.ts
@@ -0,0 +1,25 @@
+import { graphql } from 'react-relay';
+import { archiveReportingGcpeMutation } from '__generated__/archiveReportingGcpeMutation.graphql';
+import useMutationWithErrorMessage from '../useMutationWithErrorMessage';
+
+const mutation = graphql`
+ mutation archiveReportingGcpeMutation(
+ $input: UpdateReportingGcpeByRowIdInput!
+ ) {
+ updateReportingGcpeByRowId(input: $input) {
+ reportingGcpe {
+ id
+ archivedAt
+ createdAt
+ }
+ }
+ }
+`;
+
+const useArchiveReportingGcpeMutation = () =>
+ useMutationWithErrorMessage(
+ mutation,
+ () => 'An error occured while attempting to archive the intake'
+ );
+
+export { mutation, useArchiveReportingGcpeMutation };
diff --git a/app/tests/pages/analyst/reporting/manage-reports.test.tsx b/app/tests/pages/analyst/reporting/manage-reports.test.tsx
new file mode 100644
index 0000000000..8861656581
--- /dev/null
+++ b/app/tests/pages/analyst/reporting/manage-reports.test.tsx
@@ -0,0 +1,204 @@
+import { act, fireEvent, screen, waitFor } from '@testing-library/react';
+
+import compiledManageReportingQuery, {
+ manageReportsQuery,
+} from '__generated__/manageReportsQuery.graphql';
+import ManageReporting from 'pages/analyst/reporting/manage-reports';
+import PageTestingHelper from '../../../utils/pageTestingHelper';
+
+const mockQueryPayload = {
+ Query() {
+ return {
+ session: {
+ sub: '4e0ac88c-bf05-49ac-948f-7fd53c7a9fd6',
+ authRole: 'cbc_admin',
+ },
+ allReportingGcpes: {
+ edges: [
+ {
+ node: {
+ rowId: 1,
+ createdAt: '2024-06-28T20:05:52.383864+00:00',
+ ccbcUserByCreatedBy: {
+ sessionSub: '4e0ac88c-bf05-49ac-948f-7fd53c7a9fd6',
+ },
+ },
+ },
+ {
+ node: {
+ rowId: 2,
+ createdAt: '2024-06-27T01:50:51.270249+00:00',
+ ccbcUserByCreatedBy: {
+ sessionSub: '500ac88c-bf05-49ac-948f-7fd53c7a9fd6',
+ },
+ },
+ },
+ {
+ node: {
+ rowId: 3,
+ createdAt: '2024-06-26T01:51:05.63794+00:00',
+ ccbcUserByCreatedBy: {
+ sessionSub: '4e0ac88c-bf05-49ac-948f-7fd53c7a9fd6',
+ },
+ },
+ },
+ {
+ node: {
+ rowId: 4,
+ createdAt: '2024-06-25T01:53:22.963979+00:00',
+ ccbcUserByCreatedBy: {
+ sessionSub: '500ac88c-bf05-49ac-948f-7fd53c7a9fd6',
+ },
+ },
+ },
+ {
+ node: {
+ rowId: 5,
+ createdAt: '2024-06-24T01:55:04.948302+00:00',
+ ccbcUserByCreatedBy: {
+ sessionSub: '500ac88c-bf05-49ac-948f-7fd53c7a9fd6',
+ },
+ },
+ },
+ ],
+ },
+ };
+ },
+};
+
+jest.mock('@bcgov-cas/sso-express/dist/helpers');
+
+global.fetch = jest.fn(() =>
+ Promise.resolve({
+ blob: () =>
+ Promise.resolve(
+ new Blob(['test content'], {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ })
+ ),
+ headers: {
+ get: (header) => {
+ const headers = {
+ rowId: '1',
+ };
+ return headers[header];
+ },
+ },
+ })
+) as jest.Mock;
+
+// Mock the window.URL functions
+window.URL.createObjectURL = jest.fn();
+window.URL.revokeObjectURL = jest.fn();
+
+const pageTestingHelper = new PageTestingHelper({
+ pageComponent: ManageReporting,
+ compiledQuery: compiledManageReportingQuery,
+ defaultQueryResolver: mockQueryPayload,
+ defaultQueryVariables: {},
+});
+
+describe('The Gcpe reporting page', () => {
+ beforeEach(() => {
+ pageTestingHelper.reinit();
+ pageTestingHelper.setMockRouterValues({
+ pathname: '/analyst/reporting/manage-reports',
+ });
+ });
+
+ it('renders correct headings', async () => {
+ pageTestingHelper.loadQuery();
+ pageTestingHelper.renderPage();
+
+ expect(screen.getByText('Manage My Reports')).toBeInTheDocument();
+ });
+
+ it('load correct reports belong to user', async () => {
+ pageTestingHelper.loadQuery();
+ pageTestingHelper.renderPage();
+
+ expect(screen.getAllByTestId('file-download-link')).toHaveLength(2);
+ expect(
+ screen.getByText(/Generated June 28, 2024 at 1:05 p.m. PDT/)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/Generated June 25, 2024 at 6:51 p.m. PDT/)
+ ).toBeInTheDocument();
+ });
+
+ it('downloads an existing report', async () => {
+ pageTestingHelper.loadQuery();
+ pageTestingHelper.renderPage();
+
+ const firstReport = screen.getAllByTestId('file-download-link')[0];
+
+ await act(async () => {
+ fireEvent.click(firstReport);
+ });
+
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('delete report raises a confirmation dialog', async () => {
+ pageTestingHelper.loadQuery();
+ pageTestingHelper.renderPage();
+
+ const deleteButton = screen.getAllByTestId('file-delete-btn')[0];
+
+ fireEvent.click(deleteButton);
+
+ expect(
+ screen.getByText(/Are you sure you want to delete this GCPE file/)
+ ).toBeInTheDocument();
+ });
+
+ it('delete report calls correct mutation', async () => {
+ pageTestingHelper.loadQuery();
+ pageTestingHelper.renderPage();
+
+ expect(
+ screen.getByText(/Generated June 28, 2024 at 1:05 p.m. PDT/)
+ ).toBeInTheDocument();
+
+ const deleteButton = screen.getAllByTestId('file-delete-btn')[0];
+
+ await act(async () => {
+ fireEvent.click(deleteButton);
+ });
+
+ const confirmButton = screen.getByRole('button', {
+ name: 'Delete',
+ });
+
+ expect(confirmButton).toBeInTheDocument();
+ fireEvent.click(confirmButton);
+
+ pageTestingHelper.expectMutationToBeCalled('archiveReportingGcpeMutation', {
+ input: {
+ reportingGcpePatch: expect.anything(),
+ rowId: 1,
+ },
+ });
+
+ pageTestingHelper.environment.mock.resolveMostRecentOperation({
+ data: {
+ updateReportingGcpeByRowId: {
+ reportingGcpe: {
+ rowId: 1,
+ id: 'string',
+ },
+ },
+ },
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/GCPE file archived successfully/)
+ ).toBeInTheDocument();
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+});