diff --git a/components/admin/permit-holders/Header.tsx b/components/admin/permit-holders/Header.tsx index 8553ec85..b62018ca 100644 --- a/components/admin/permit-holders/Header.tsx +++ b/components/admin/permit-holders/Header.tsx @@ -18,6 +18,7 @@ import { import { ChevronDownIcon, ChevronLeftIcon } from '@chakra-ui/icons'; // Chakra UI icon import Link from 'next/link'; // Link import { ApplicantStatus } from '@lib/graphql/types'; +import { CurrentApplication } from '@tools/admin/permit-holders/current-application'; import PermitHolderStatusBadge from '@components/admin/PermitHolderStatusBadge'; import ConfirmDeleteApplicantModal from '@components/admin/permit-holders/table/ConfirmDeleteApplicantModal'; import SetPermitHolderToInactiveModal from '@components/admin/permit-holders/table/ConfirmSetInactiveModal'; @@ -31,12 +32,13 @@ type PermitHolderHeaderProps = { status: ApplicantStatus; inactiveReason?: string; notes: string; + mostRecentApplication: CurrentApplication | null; }; readonly refetch: () => void; }; export default function PermitHolderHeader({ - applicant: { id, name, status, inactiveReason, notes }, + applicant: { id, name, status, inactiveReason, notes, mostRecentApplication }, refetch, }: PermitHolderHeaderProps) { const router = useRouter(); @@ -108,13 +110,15 @@ export default function PermitHolderHeader({ > {`Set as ${status === 'ACTIVE' ? 'Inactive' : 'Active'}`} - - {'Delete Permit Holder'} - + {mostRecentApplication?.processing?.status == 'COMPLETED' ? null : ( + + {'Delete Permit Holder'} + + )} diff --git a/components/admin/requests/Header.tsx b/components/admin/requests/Header.tsx index 878aedb8..fd84db69 100644 --- a/components/admin/requests/Header.tsx +++ b/components/admin/requests/Header.tsx @@ -25,6 +25,7 @@ import { ApplicationStatus, ApplicationType, PermitType } from '@lib/graphql/typ import { titlecase } from '@tools/string'; import { formatDateYYYYMMDD, formatDateYYYYMMDDLocal } from '@lib/utils/date'; import { getPermanentPermitExpiryDate } from '@lib/utils/permit-expiry'; +import { useEffect, useState } from 'react'; // React type RequestHeaderProps = { readonly id: number; @@ -87,6 +88,22 @@ export default function RequestHeader({ const router = useRouter(); + const [backLink, setBackLink] = useState('/admin'); + const generateBackLink = () => { + let status; + const routerQuery = router.query; + if (routerQuery === undefined || routerQuery.origin === undefined) { + status = applicationStatus; + } else { + status = routerQuery.origin; + } + setBackLink(`/admin?tab=${status}`); + }; + + useEffect(() => { + generateBackLink(); + }, []); + // Delete application modal state const { isOpen: isDeleteApplicationModalOpen, @@ -96,12 +113,13 @@ export default function RequestHeader({ return ( - + All requests + diff --git a/lib/graphql/types.ts b/lib/graphql/types.ts index dc050b9b..01950c8f 100644 --- a/lib/graphql/types.ts +++ b/lib/graphql/types.ts @@ -178,6 +178,8 @@ export type ApplicationsReportColumn = | 'APPLICANT_NAME' | 'APPLICANT_DATE_OF_BIRTH' | 'APP_NUMBER' + | 'PHONE_NUMBER' + | 'HOME_ADDRESS' | 'APPLICATION_DATE' | 'PAYMENT_METHOD' | 'FEE_AMOUNT' diff --git a/lib/reports/resolvers.ts b/lib/reports/resolvers.ts index 939c5eb4..87969820 100644 --- a/lib/reports/resolvers.ts +++ b/lib/reports/resolvers.ts @@ -223,6 +223,12 @@ export const generateApplicationsReport: Resolver< firstName: true, middleName: true, lastName: true, + addressLine1: true, + addressLine2: true, + city: true, + province: true, + postalCode: true, + phone: true, type: true, createdAt: true, paymentMethod: true, @@ -262,6 +268,10 @@ export const generateApplicationsReport: Resolver< firstName, middleName, lastName, + phone, + addressLine1, + addressLine2, + postalCode, type, createdAt, processingFee, @@ -292,6 +302,9 @@ export const generateApplicationsReport: Resolver< id: applicant?.id, applicantName: formatFullName(firstName, middleName, lastName), dateOfBirth: dateOfBirth && formatDateYYYYMMDD(dateOfBirth), + address: formatStreetAddress(addressLine1, addressLine2), + postalCode: formatPostalCode(postalCode), + phone: formatPhoneNumber(phone), rcdPermitId: permit?.rcdPermitId ? `#${permit.rcdPermitId}` : null, applicationDate: createdAt ? formatDateYYYYMMDDLocal(createdAt, true) : null, processingFee: `$${processingFee}`, @@ -314,9 +327,17 @@ export const generateApplicationsReport: Resolver< } ); - const csvHeaders = APPLICATIONS_COLUMNS.filter(({ value }) => columnsSet.has(value)).map( - ({ name, reportColumnId }) => ({ id: reportColumnId, title: name }) - ); + const filteredColumns = APPLICATIONS_COLUMNS.filter(({ value }) => columnsSet.has(value)); + const csvHeaders: Array<{ id: string; title: string }> = []; + for (const { name, reportColumnId } of filteredColumns) { + if (typeof reportColumnId === 'string') { + csvHeaders.push({ id: reportColumnId, title: name }); + } else { + for (const [columnLabel, columnId] of reportColumnId) { + csvHeaders.push({ id: columnId, title: columnLabel }); + } + } + } // Generate CSV string from csv object. const csvStringifier = createObjectCsvStringifier({ diff --git a/lib/reports/schema.ts b/lib/reports/schema.ts index 62ea95f4..c0485c19 100644 --- a/lib/reports/schema.ts +++ b/lib/reports/schema.ts @@ -25,6 +25,8 @@ export default gql` APPLICANT_NAME APPLICANT_DATE_OF_BIRTH APP_NUMBER + PHONE_NUMBER + HOME_ADDRESS APPLICATION_DATE PAYMENT_METHOD FEE_AMOUNT diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index a6d9da1b..d2fb8c95 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -44,6 +44,39 @@ import { formatFullName } from '@lib/utils/format'; // String formatter util import { formatDateYYYYMMDDLocal } from '@lib/utils/date'; // Date Formatter Util import EmptyMessage from '@components/EmptyMessage'; +interface RouterQuery { + tab?: string; +} + +export const getTabIndex = (routerQuery: RouterQuery): number => { + if (routerQuery === undefined || routerQuery.tab === undefined) { + return 1; + } + const tabName = routerQuery.tab; + switch (tabName) { + case 'ALL': + return 0; + case 'PENDING': + return 1; + case 'IN_PROGRESS': + return 2; + case 'COMPLETED': + return 3; + case 'REJECTED': + return 4; + default: + return 1; + } +}; + +const tabIndexToStatus: { [key: number]: ApplicationStatus | 'ALL' } = { + 0: 'ALL', + 1: 'PENDING', + 2: 'IN_PROGRESS', + 3: 'COMPLETED', + 4: 'REJECTED', +}; + // Placeholder columns const COLUMNS: Column[] = [ { @@ -126,6 +159,7 @@ const PAGE_SIZE = 20; const Requests: NextPage = () => { // Router const router = useRouter(); + const routerQuery: RouterQuery = router.query; // Filters const [statusFilter, setStatusFilter] = useState('PENDING'); @@ -145,6 +179,20 @@ const Requests: NextPage = () => { const [pageNumber, setPageNumber] = useState(0); const [recordsCount, setRecordsCount] = useState(0); + // Tabs + const [tabIndex, setTabIndex] = useState(0); + const getTabFromRoute = (): number => { + const index = getTabIndex(routerQuery); + setTabIndex(index); + return index; + }; + + const handleTabChange = () => { + const status = tabIndexToStatus[tabIndex]; + setStatusFilter(status === 'ALL' ? null : status); + router.push({ query: { tab: status } }); + }; + // Make query to applications resolver const { refetch, loading } = useQuery( GET_APPLICATIONS_QUERY, @@ -191,6 +239,16 @@ const Requests: NextPage = () => { } ); + // Determine the active tab on page load based on the route + useEffect(() => { + getTabFromRoute(); + }, []); + + useEffect(() => { + if (tabIndex === null) return; + handleTabChange(); + }, [tabIndex]); + // Set page number to 0 after every filter or sort change useEffect(() => { setPageNumber(0); @@ -228,48 +286,19 @@ const Requests: NextPage = () => { - - - { - setStatusFilter(null); - }} - > - All - - { - setStatusFilter('PENDING'); - }} - > - Pending - - { - setStatusFilter('IN_PROGRESS'); - }} - > - In Progress - - { - setStatusFilter('COMPLETED'); - }} - > - Completed - - { - setStatusFilter('REJECTED'); - }} - > - Rejected - + { + setTabIndex(index); + }} + > + + All + Pending + In Progress + Completed + Rejected @@ -379,13 +408,23 @@ const Requests: NextPage = () => { {requestsData.length > 0 ? ( <> + + + router.push(`/admin/request/${id}`)} + onRowClick={({ id }) => + router.push(`/admin/request/${id}?origin=${tabIndexToStatus[tabIndex]}`) + } /> diff --git a/pages/admin/permit-holders.tsx b/pages/admin/permit-holders.tsx index 49d38aae..8ada9cde 100644 --- a/pages/admin/permit-holders.tsx +++ b/pages/admin/permit-holders.tsx @@ -39,6 +39,7 @@ import { USER_STATUSES, PermitHolderToUpdateStatus, } from '@tools/admin/permit-holders/permit-holders-table'; +import { CurrentApplication } from '@tools/admin/permit-holders/current-application'; import DateRangePicker from '@components/DateRangePicker'; // Day Picker component import useDateRangePicker from '@tools/hooks/useDateRangePicker'; // Day Picker hook import { SortOptions, SortOrder } from '@tools/types'; // Sorting types @@ -273,10 +274,16 @@ const PermitHolders: NextPage = () => { Header: 'Actions', Cell: ({ row: { - original: { id, status }, + original: { id, status, mostRecentApplication }, }, }: { - row: { original: { id: number; status: ApplicantStatus } }; + row: { + original: { + id: number; + status: ApplicantStatus; + mostRecentApplication: CurrentApplication | null; + }; + }; }) => { return ( @@ -301,17 +308,19 @@ const PermitHolders: NextPage = () => { > {`Set as ${status === 'ACTIVE' ? 'Inactive' : 'Active'}`} - { - event.stopPropagation(); - setPermitHolderToDelete(id); - onOpenDeleteApplicantModal(); - }} - > - {'Delete Permit Holder'} - + {mostRecentApplication?.processing?.status == 'COMPLETED' ? null : ( + { + event.stopPropagation(); + setPermitHolderToDelete(id); + onOpenDeleteApplicantModal(); + }} + > + {'Delete Permit Holder'} + + )} ); @@ -494,6 +503,14 @@ const PermitHolders: NextPage = () => { {permitHolderData && permitHolderData.length > 0 ? ( <> + + +
= [ @@ -41,6 +42,7 @@ export type PermitHolderRow = Pick; + mostRecentApplication: CurrentApplication | null; }; /** @@ -70,6 +72,11 @@ export const GET_PERMIT_HOLDERS_QUERY = gql` rcdPermitId } status + mostRecentApplication { + processing { + status + } + } } totalCount } @@ -95,6 +102,7 @@ export type PermitHolder = Pick< | 'status' > & { mostRecentPermit: Pick; + mostRecentApplication: CurrentApplication | null; }; export type GetPermitHoldersResponse = { diff --git a/tools/admin/reports.ts b/tools/admin/reports.ts index c019d3e5..ce75b470 100644 --- a/tools/admin/reports.ts +++ b/tools/admin/reports.ts @@ -12,7 +12,7 @@ export enum GenerateReportStep { export const APPLICATIONS_COLUMNS: Array<{ name: string; value: ApplicationsReportColumn; - reportColumnId: string; + reportColumnId: string | Array<[string, string]>; }> = [ { name: 'User ID', @@ -35,9 +35,24 @@ export const APPLICATIONS_COLUMNS: Array<{ reportColumnId: 'rcdPermitId', }, { - name: 'Application Date', - value: 'APPLICATION_DATE', - reportColumnId: 'applicationDate', + name: 'Phone Number', + value: 'PHONE_NUMBER', + reportColumnId: 'phone', + }, + { + name: 'Home Address', + value: 'HOME_ADDRESS', + reportColumnId: [ + ['Address', 'address'], + ['City', 'city'], + ['Province', 'province'], + ['Postal Code', 'postalCode'], + ], + }, + { + name: 'Donation Amount', + value: 'DONATION_AMOUNT', + reportColumnId: 'donationAmount', }, { name: 'Payment Method', @@ -50,9 +65,9 @@ export const APPLICATIONS_COLUMNS: Array<{ reportColumnId: 'processingFee', }, { - name: 'Donation Amount', - value: 'DONATION_AMOUNT', - reportColumnId: 'donationAmount', + name: 'Second Donation Amount', + value: 'SECOND_DONATION_AMOUNT', + reportColumnId: 'secondDonationAmount', }, { name: 'Second Payment Method', @@ -65,9 +80,9 @@ export const APPLICATIONS_COLUMNS: Array<{ reportColumnId: 'secondProcessingFee', }, { - name: 'Second Donation Amount', - value: 'SECOND_DONATION_AMOUNT', - reportColumnId: 'secondDonationAmount', + name: 'Application Date', + value: 'APPLICATION_DATE', + reportColumnId: 'applicationDate', }, { name: 'Total Amount',