Skip to content

Commit

Permalink
added an 'Admin Action History' icon to the 'actions' column of all r…
Browse files Browse the repository at this point in the history
…eport tables. The icon pops up a panel with a list of lock/unlock events. Added a new API endpoint to provide the supporting data.
  • Loading branch information
banders committed Sep 26, 2024
1 parent ecc8624 commit 952e9c1
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 103 deletions.
19 changes: 2 additions & 17 deletions admin-frontend/src/components/ReportsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,7 @@
{{ formatDate(item.create_date) }}
</template>
<template #item.actions="{ item }">
<v-btn
aria-label="Open Report"
density="compact"
variant="plain"
icon="mdi-file-pdf-box"
:loading="isDownloadingPdf(item.report_id)"
:disabled="isDownloadingPdf(item.report_id)"
@click="viewReportInNewTab(item.report_id)"
></v-btn>
<v-btn
:aria-label="item.is_unlocked ? 'Lock report' : 'Unlock report'"
density="compact"
variant="plain"
:icon="item.is_unlocked ? 'mdi-lock-open' : 'mdi-lock'"
:color="item.is_unlocked ? 'success' : 'error'"
@click="lockUnlockReport(item.report_id, !item?.is_unlocked)"
></v-btn>
<ReportActions :report="item"></ReportActions>
</template>
</v-data-table-server>
</div>
Expand All @@ -94,6 +78,7 @@ import ApiService from '../services/apiService';
import ConfirmationDialog from './util/ConfirmationDialog.vue';
import { formatDate } from '../utils/date';
import { NotificationService } from '../services/notificationService';
import ReportActions from './reports/ReportActions.vue';
const reportsCurrentlyBeingDownloaded = ref({});
const reportSearchStore = useReportSearchStore();
Expand Down
78 changes: 75 additions & 3 deletions admin-frontend/src/components/reports/ReportActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,34 @@
@click="lockUnlockReport(props.report.report_id, !props.report.is_unlocked)"
></v-btn>

<v-btn
aria-label="Admin action history"
density="compact"
variant="plain"
icon="mdi-clock-time-four"
:loading="isLoadingAdminActionHistory"
@click="openAdminActionHistory(props.report.report_id)"
>
<v-icon size="large"></v-icon>
<v-menu activator="parent">
<v-card class="">
<v-card-text>
<div class="history-panel">
<ReportAdminActionHistoryView
v-if="!isLoadingAdminActionHistory && reportAdminActionHistory"
:report-admin-action-history="reportAdminActionHistory"
></ReportAdminActionHistoryView>
</div>
<v-skeleton-loader
v-if="isLoadingAdminActionHistory"
type="paragraph"
class="mt-0"
></v-skeleton-loader>
</v-card-text>
</v-card>
</v-menu>
</v-btn>

<!-- dialogs -->
<ConfirmationDialog ref="confirmDialog"> </ConfirmationDialog>
</template>
Expand All @@ -30,18 +58,28 @@ export default {
import ConfirmationDialog from '../util/ConfirmationDialog.vue';
import ApiService from '../../services/apiService';
import { Report } from '../../types/reports';
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import { NotificationService } from '../../services/notificationService';
import ReportAdminActionHistoryView from './ReportAdminActionHistoryPanel.vue';
import { ReportAdminActionHistory } from '../../types/reports';
const props = defineProps<{
report: Report;
}>();
const emits = defineEmits(['onLockStatusChanged']);
const isLoadingPdf = ref<boolean>(false);
const isLoadingAdminActionHistory = ref<boolean>(false);
const hadErrorLoadingAdminActionHistory = ref<boolean>(false);
const reportAdminActionHistory = ref<ReportAdminActionHistory | undefined>(
undefined,
);
const confirmDialog = ref<typeof ConfirmationDialog>();
onMounted(() => {
reset();
});
async function lockUnlockReport(reportId: string, makeUnlocked: boolean) {
const lockText = makeUnlocked ? 'unlock' : 'lock';
const isConfirmed = await confirmDialog.value?.open(
Expand Down Expand Up @@ -69,11 +107,45 @@ async function viewReportInNewTab(reportId: string) {
const objectUrl = URL.createObjectURL(pdfAsBlob);
window.open(objectUrl);
} catch (e) {
console.log(e);
NotificationService.pushNotificationError(
'Something went wrong. Unable to download report.',
);
}
isLoadingPdf.value = false;
}
async function openAdminActionHistory(reportId: string) {
//fetch the "admin action history" from the backend, then cache it
//so we don't need to look it up again
if (!isLoadingAdminActionHistory.value && !reportAdminActionHistory.value) {
await fetchAdminActionHistory(reportId);
}
}
async function fetchAdminActionHistory(reportId: string) {
isLoadingAdminActionHistory.value = true;
hadErrorLoadingAdminActionHistory.value = false;
try {
reportAdminActionHistory.value =
await ApiService.getReportAdminActionHistory(reportId);
} catch (e) {
hadErrorLoadingAdminActionHistory.value = true;
} finally {
isLoadingAdminActionHistory.value = false;
}
}
function reset() {
reportAdminActionHistory.value = undefined;
hadErrorLoadingAdminActionHistory.value = false;
}
</script>

<style>
.history-panel {
min-width: 280px;
min-height: 50px;
max-height: 210px;
overflow-y: auto;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<div v-if="!reportAdminActionHistory?.length">No admin events to show</div>
<div v-if="reportAdminActionHistory?.length" class="d-flex flex-column">
<div
v-for="item in reportAdminActionHistory"
:key="item.report_history_id"
class="d-flex flex-column"
>
<v-row no-gutters>
<v-col class="d-flex justify-center first-column">
<v-icon
:icon="item.is_unlocked ? 'mdi-lock-open' : 'mdi-lock'"
:color="item.is_unlocked ? 'success' : 'error'"
></v-icon>
</v-col>
<v-col :class="item.is_unlocked ? 'text-success' : 'text-error'"
><b>{{ item.is_unlocked ? 'Unlocked' : 'Locked' }}</b></v-col
>
</v-row>
<v-row no-gutters>
<v-col class="d-flex justify-center py-1 first-column"
><div class="vertical-bar"></div
></v-col>
<v-col class="mb-3">
<div v-if="item.admin_modified_date" class="d-flex align-center">
<div>
{{ formatIsoDateTimeAsLocalDate(item.admin_modified_date) }}
</div>
<small class="text-grey-darken-3 ms-2">
{{
formatIsoDateTimeAsLocalTime(item.admin_modified_date)
}}</small
>
</div>
<div v-if="item?.admin_user?.display_name">
By {{ item.admin_user.display_name }}
</div>
</v-col>
</v-row>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'ReportAdminActionHistoryPanel',
};
</script>
<script setup lang="ts">
import { ReportAdminActionHistory } from '../../types/reports';
import {
formatIsoDateTimeAsLocalDate,
formatIsoDateTimeAsLocalTime,
} from '../../utils/date';
defineProps<{
reportAdminActionHistory: ReportAdminActionHistory;
}>();
</script>
<style>
.first-column {
max-width: 25px !important;
margin-right: 4px;
}
.vertical-bar {
width: 3px;
background-color: #dddddd;
height: 100%;
}
</style>
17 changes: 17 additions & 0 deletions admin-frontend/src/services/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,23 @@ export default {
}
},

async getReportAdminActionHistory(reportId: string): Promise<any> {
try {
const resp = await apiAxios.get(
`${ApiRoutes.REPORTS}/${reportId}/admin-action-history`,
);
if (resp?.data) {
return resp.data;
}
throw new Error(
`Unable to fetch admin action history for report ${reportId}`,
);
} catch (e) {
console.log(`Failed to get admin action history for report - ${e}`);
throw e;
}
},

async getReportMetrics(): Promise<ReportMetrics> {
try {
const resp = await apiAxios.get(ApiRoutes.REPORT_METRICS);
Expand Down
11 changes: 11 additions & 0 deletions admin-frontend/src/types/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,14 @@ export type ReportMetrics = {
},
];
};

export type ReportAdminActionHistory = [
{
report_history_id: string;
is_unlocked: boolean;
admin_modified_date: string;
admin_user: {
display_name: string;
};
},
];
29 changes: 29 additions & 0 deletions backend/src/v1/routes/admin-report-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { adminReportService } from '../services/admin-report-service';
import { PayTransparencyUserError } from '../services/file-upload-service';
import { reportService } from '../services/report-service';
import { utils } from '../services/utils-service';
import { UserInputError } from '../types/errors';

enum Format {
CSV = 'text/csv',
Expand Down Expand Up @@ -159,4 +160,32 @@ router.get(
),
);

/**
* GET /admin-api/v1/reports/:report_id
* accepts 'pdf':
* Download pdf of the report for the users business
*/
router.get(
'/:report_id/admin-action-history',
utils.asyncHandler(
async (
req: Request<{ report_id: string }, null, null, null>,
res: Response,
) => {
// params
const reportId = req.params.report_id;
try {
const adminActionHistory =
await adminReportService.getReportAdminActionHistory(reportId);
return res.status(200).json(adminActionHistory);
} catch (err) {
if (err instanceof UserInputError) {
res.status(404).json({ error: 'Not found' });
}
res.status(500).json({ error: 'Something went wrong' });
}
},
),
);

export default router;
6 changes: 6 additions & 0 deletions backend/src/v1/services/admin-report-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ZoneOffset,
} from '@js-joda/core';
import createPrismaMock from 'prisma-mock';
import prisma from '../prisma/prisma-client';
import {
adminReportService,
adminReportServicePrivate,
Expand All @@ -26,6 +27,8 @@ let reports = [];
let admins = [];
let prismaClient: any;

jest.mock('./report-service');

const mockFindMany = jest.fn();

jest.mock('../prisma/prisma-client-readonly-replica', () => ({
Expand All @@ -43,6 +46,7 @@ jest.mock('../prisma/prisma-client-readonly-replica', () => ({
prismaClient.pay_transparency_report.findUnique(args),
count: (args) => prismaClient.pay_transparency_report.count(args),
},
$transaction: jest.fn().mockImplementation((callback) => callback(prisma)),
},
}));
jest.mock('../prisma/prisma-client', () => ({
Expand All @@ -58,6 +62,7 @@ jest.mock('../prisma/prisma-client', () => ({
update: (args) => prismaClient.pay_transparency_report.update(args),
},
$extends: jest.fn(),
$transaction: jest.fn().mockImplementation((callback) => callback(prisma)),
},
}));

Expand Down Expand Up @@ -600,6 +605,7 @@ describe('admin-report-service', () => {
expect(report.is_unlocked).toBeTruthy();
expect(report.admin_modified_date).toBe(report.report_unlock_date);
expect(report.admin_user_id).toBe('1234');
expect(reportService.movePublishedReportToHistory).toHaveBeenCalled();
});
it('should change report is_unlocked to false', async () => {
const report = await adminReportService.changeReportLockStatus(
Expand Down
Loading

0 comments on commit 952e9c1

Please sign in to comment.