Skip to content

Commit cb9b766

Browse files
feat: 2163 allow user to download testing activity results (#2221)
Co-authored-by: mcatherine1994 <mcatherine1994@gmail.com>
1 parent 02e136b commit cb9b766

File tree

12 files changed

+192
-13
lines changed

12 files changed

+192
-13
lines changed

frontend/package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"aws-amplify": "^6.15.0",
2424
"axios": "^1.12.0",
2525
"bignumber.js": "^9.0.0",
26+
"export-to-csv": "^1.4.0",
2627
"luxon": "^3.4.3",
2728
"material-react-table": "^3.2.1",
2829
"react": "^18.2.0",

frontend/src/api-service/consep/searchTestingActivitiesAPI.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { PaginatedTestingSearchResponseType, TestCodeType } from '../../types/co
77
export const searchTestingActivities = (
88
filter: ActivitySearchRequest,
99
page: number = 0,
10-
size: number = 20
10+
size: number = 20,
11+
unpaged: boolean = false
1112
) => {
12-
const url = `${ApiConfig.searchTestActivities}/search?page=${page}&size=${size}`;
13+
const url = `${ApiConfig.searchTestActivities}/search?page=${page}&size=${size}&unpaged=${unpaged}`;
1314
return api.post(url, filter).then((res): PaginatedTestingSearchResponseType => res.data);
1415
};
1516

frontend/src/types/consep/TestingSearchType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type TestingSearchResponseType = {
1212
purityPct: number | null;
1313
seedsPerGram: number | null;
1414
otherTestResult: number | null;
15-
testCompleteInd: boolean;
15+
testCompleteInd: number;
1616
acceptResultInd: number;
1717
significntStsInd: number;
1818
seedWithdrawalDate: string | null;

frontend/src/views/CONSEP/TestingActivities/TestSearch/TestListTable.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as Icons from '@carbon/icons-react';
44

55
import GenericTable from '../../../../components/GenericTable';
66
import ShowHideColumnControl from './ToolbarControls/ShowHideColumnControl';
7-
import { getTestingActivityListColumns } from './constants';
7+
import { getTestingActivityListColumns, columnVisibilityLocalStorageKey } from './constants';
88
import type {
99
TestingSearchResponseType,
1010
PaginationInfoType
@@ -13,6 +13,7 @@ import type {
1313
type TestListTableProp = {
1414
data: TestingSearchResponseType[];
1515
paginationInfo: PaginationInfoType;
16+
onExportData: () => void;
1617
isLoading?: boolean;
1718
onPageChange?: (pageIndex: number, pageSize: number) => void;
1819
};
@@ -21,10 +22,10 @@ const TestListTable = ({
2122
data,
2223
isLoading = false,
2324
paginationInfo,
24-
onPageChange = () => {}
25+
onPageChange,
26+
onExportData
2527
}: TestListTableProp) => {
2628
const tableBodyRef = useRef<HTMLTableSectionElement>(null);
27-
const columnVisibilityLocalStorageKey = 'test-activity-table-columns-visibility';
2829

2930
const actions = [
3031
{
@@ -58,7 +59,7 @@ const TestListTable = ({
5859
<Icons.DocumentExport size={16} className="concep-test-search-table-toolbar-button-icon" />
5960
),
6061
type: 'primary',
61-
action: () => {}
62+
action: () => onExportData()
6263
},
6364
{
6465
label: 'Filters',

frontend/src/views/CONSEP/TestingActivities/TestSearch/constants.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,97 @@ export const getTestingActivityListColumns = (): MRT_ColumnDef<TestingSearchResp
317317
...tableCellProps()
318318
}
319319
];
320+
321+
export const formatExportData = {
322+
seedlotDisplay: {
323+
header: 'Lot #',
324+
value: (row: TestingSearchResponseType) => row.seedlotDisplay
325+
},
326+
requestItem: {
327+
header: 'Request ID',
328+
value: (row: TestingSearchResponseType) => row.requestItem
329+
},
330+
species: {
331+
header: 'Sp',
332+
value: (row: TestingSearchResponseType) => row.species
333+
},
334+
testRank: {
335+
header: 'Rank',
336+
value: (row: TestingSearchResponseType) => row.testRank
337+
},
338+
testCategoryCd: {
339+
header: 'Category',
340+
value: (row: TestingSearchResponseType) => row.testCategoryCd
341+
},
342+
activityId: {
343+
header: 'Activity',
344+
value: (row: TestingSearchResponseType) => row.activityId
345+
},
346+
Result: {
347+
header: 'Result',
348+
value: (row: TestingSearchResponseType) => {
349+
switch (row.testCategoryCd) {
350+
case 'Germ': {
351+
const v = row.germinationPct;
352+
return v == null ? '' : `${v}%`;
353+
}
354+
case 'MC': {
355+
const v = row.moisturePct;
356+
return v == null ? '' : `${v}%`;
357+
}
358+
case 'SPG': {
359+
const v = row.seedsPerGram;
360+
return v == null ? '' : `${v}%`;
361+
}
362+
case 'PUR': {
363+
const v = row.purityPct;
364+
return v == null ? '' : `${v}%`;
365+
}
366+
default:
367+
return '';
368+
}
369+
}
370+
},
371+
pv: {
372+
header: 'PV',
373+
value: (row: TestingSearchResponseType) => row.pv
374+
},
375+
currentTestInd: {
376+
header: 'Curr',
377+
value: (row: TestingSearchResponseType) => (row.currentTestInd === 0 ? '' : 'Curr')
378+
},
379+
testCompleteInd: {
380+
header: 'Com',
381+
value: (row: TestingSearchResponseType) => (row.testCompleteInd === 0 ? '' : 'Com')
382+
},
383+
acceptResultInd: {
384+
header: 'Act',
385+
value: (row: TestingSearchResponseType) => (row.acceptResultInd === 0 ? '' : 'Act')
386+
},
387+
significntStsInd: {
388+
header: 'Sig',
389+
value: (row: TestingSearchResponseType) => (row.significntStsInd === 0 ? '' : 'Sig')
390+
},
391+
seedWithdrawalDate: {
392+
header: 'Wdrwl Date',
393+
value: (row: TestingSearchResponseType) => formatDateCell(row.seedWithdrawalDate)
394+
},
395+
revisedEndDt: {
396+
header: 'Sch End Date',
397+
value: (row: TestingSearchResponseType) => formatDateCell(row.revisedEndDt)
398+
},
399+
actualBeginDtTm: {
400+
header: 'Start Date',
401+
value: (row: TestingSearchResponseType) => formatDateCell(row.actualBeginDtTm)
402+
},
403+
actualEndDtTm: {
404+
header: 'End Date',
405+
value: (row: TestingSearchResponseType) => formatDateCell(row.actualEndDtTm)
406+
},
407+
riaComment: {
408+
header: 'Comments',
409+
value: (row: TestingSearchResponseType) => row.riaComment || ''
410+
}
411+
};
412+
413+
export const columnVisibilityLocalStorageKey = 'test-activity-table-columns-visibility';

frontend/src/views/CONSEP/TestingActivities/TestSearch/index.tsx

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
InlineNotification
1818
} from '@carbon/react';
1919
import { Search } from '@carbon/icons-react';
20+
// eslint-disable-next-line import/no-unresolved
21+
import { mkConfig, generateCsv, download } from 'export-to-csv';
2022

2123
import Breadcrumbs from '../../../../components/Breadcrumbs';
2224
import PageTitle from '../../../../components/PageTitle';
@@ -34,12 +36,20 @@ import AdvancedFilters from './AdvancedFilter';
3436
import {
3537
DATE_FORMAT, activityIds,
3638
testSearchCrumbs, iniActSearchValidation,
37-
errorMessages, minStartDate, maxEndDate
39+
errorMessages, minStartDate, maxEndDate,
40+
formatExportData, columnVisibilityLocalStorageKey
3841
} from './constants';
3942
import { THREE_HALF_HOURS, THREE_HOURS } from '../../../../config/TimeUnits';
4043
import { ActivitySearchRequest, ActivitySearchValidation } from './definitions';
4144
import './styles.scss';
4245

46+
const csvConfig = mkConfig({
47+
fieldSeparator: ',',
48+
decimalSeparator: '.',
49+
useKeysAsHeaders: true,
50+
filename: `Testing_Activity_Search_${new Date().toISOString().split('T')[0]}`
51+
});
52+
4353
const TestSearch = () => {
4454
const [hasSearched, setHasSearched] = useState(false);
4555
const [searchParams, setSearchParams] = useState<ActivitySearchRequest>(
@@ -106,6 +116,63 @@ const TestSearch = () => {
106116
}
107117
});
108118

119+
const exportMutation = useMutation({
120+
mutationFn: (filter: ActivitySearchRequest) => searchTestingActivities(filter, 0, 0, true),
121+
122+
onSuccess: (data) => {
123+
const visibilityConfig = JSON.parse(
124+
localStorage.getItem(columnVisibilityLocalStorageKey) || '{}'
125+
);
126+
127+
const isVisible = (key: string) => visibilityConfig[key] !== false;
128+
129+
type FilteredRow = Record<
130+
keyof TestingSearchResponseType,
131+
TestingSearchResponseType[keyof TestingSearchResponseType] | undefined
132+
>;
133+
134+
const filterItem = (item: TestingSearchResponseType): FilteredRow => Object
135+
.keys(item).reduce((acc, key) => {
136+
if (!isVisible(key)) return acc;
137+
if (!Object.hasOwnProperty.call(formatExportData, key)) return acc;
138+
139+
const k = key as keyof TestingSearchResponseType;
140+
acc[k] = item[k];
141+
return acc;
142+
}, {} as FilteredRow);
143+
144+
const formatItem = (item: FilteredRow) => Object.entries(item).reduce((acc, [key]) => {
145+
const config = formatExportData[key as keyof typeof formatExportData];
146+
147+
if (config) {
148+
acc[config.header] = config.value(item as TestingSearchResponseType);
149+
}
150+
151+
if (key === 'Result') {
152+
acc.Result = formatExportData.Result.value(item as TestingSearchResponseType);
153+
}
154+
155+
return acc;
156+
}, {} as Record<string, any>);
157+
158+
const filteredContent: FilteredRow[] = data.content.map(filterItem);
159+
const formattedContent = filteredContent.map(formatItem);
160+
161+
const csv = generateCsv(csvConfig)(formattedContent);
162+
download(csvConfig)(csv);
163+
},
164+
onError: (error) => {
165+
setAlert({
166+
status: 'error',
167+
message: `Failed to export data: ${error?.message || 'Unknown error'}`
168+
});
169+
}
170+
});
171+
172+
const handleExportData = () => {
173+
exportMutation.mutate(searchParams);
174+
};
175+
109176
const testTypeQuery = useQuery({
110177
queryKey: ['test-type-codes'],
111178
queryFn: getTestTypeCodes,
@@ -477,6 +544,7 @@ const TestSearch = () => {
477544
isLoading={searchMutation.isPending}
478545
paginationInfo={paginationInfo}
479546
onPageChange={handlePageChange}
547+
onExportData={handleExportData}
480548
/>
481549
) : (
482550
<TablePlaceholder />

oracle-api/src/main/java/ca/bc/gov/oracleapi/dto/consep/ActivitySearchResponseDto.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public record ActivitySearchResponseDto(
2222
Integer purityPct,
2323
Integer seedsPerGram,
2424
Integer otherTestResult,
25-
Boolean testCompleteInd,
25+
Integer testCompleteInd,
2626
Integer acceptResultInd,
2727
Integer significntStsInd,
2828
LocalDateTime seedWithdrawalDate,

oracle-api/src/main/java/ca/bc/gov/oracleapi/endpoint/consep/ActivitySearchEndpoint.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.web.bind.annotation.PostMapping;
2323
import org.springframework.web.bind.annotation.RequestBody;
2424
import org.springframework.web.bind.annotation.RequestMapping;
25+
import org.springframework.web.bind.annotation.RequestParam;
2526
import org.springframework.web.bind.annotation.RestController;
2627

2728
/**
@@ -58,8 +59,12 @@ public class ActivitySearchEndpoint {
5859
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
5960
public ActivitySearchPageResponseDto searchTestingActivities(
6061
@Valid @RequestBody ActivitySearchRequestDto filter,
62+
@RequestParam(defaultValue = "false") boolean unpaged,
6163
@ParameterObject @PageableDefault(size = 20) Pageable paginationParameters
6264
) {
65+
if (unpaged) {
66+
paginationParameters = Pageable.unpaged();
67+
}
6368
return activitySearchService.searchTestingActivities(filter, paginationParameters);
6469
}
6570

oracle-api/src/main/java/ca/bc/gov/oracleapi/entity/consep/ActivitySearchResultEntity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public class ActivitySearchResultEntity {
5858
private Integer otherTestResult;
5959

6060
@Column(name = "TEST_COMPLETE_IND")
61-
private Boolean testCompleteInd;
61+
private Integer testCompleteInd;
6262

6363
@Column(name = "ACCEPT_RESULT_IND")
6464
private Integer acceptResultInd;

0 commit comments

Comments
 (0)