diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index a07996e5..bae25c3f 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -118,7 +118,7 @@ export default function ResumeHomePage() { '', ); const [shortcutSelected, setShortcutSelected, isShortcutInit] = - useSearchParams('shortcutSelected', 'All'); + useSearchParams('shortcutSelected', 'Unreviewed'); const [currentPage, setCurrentPage, isCurrentPageInit] = useSearchParams( 'currentPage', 1, @@ -182,20 +182,13 @@ export default function ResumeHomePage() { isSearchOptionsInit, ]); - const filterCountsQuery = trpc.useQuery( - ['resumes.resume.getTotalFilterCounts'], - { - staleTime: STALE_TIME, - }, - ); - const allResumesQuery = trpc.useQuery( [ 'resumes.resume.findAll', { experienceFilters: userFilters.experience, + isUnreviewed: userFilters.isUnreviewed, locationFilters: userFilters.location, - numComments: userFilters.numComments, roleFilters: userFilters.role, searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), skip, @@ -213,8 +206,8 @@ export default function ResumeHomePage() { 'resumes.resume.user.findUserStarred', { experienceFilters: userFilters.experience, + isUnreviewed: userFilters.isUnreviewed, locationFilters: userFilters.location, - numComments: userFilters.numComments, roleFilters: userFilters.role, searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), skip, @@ -233,8 +226,8 @@ export default function ResumeHomePage() { 'resumes.resume.user.findUserCreated', { experienceFilters: userFilters.experience, + isUnreviewed: userFilters.isUnreviewed, locationFilters: userFilters.location, - numComments: userFilters.numComments, roleFilters: userFilters.role, searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), skip, @@ -249,14 +242,6 @@ export default function ResumeHomePage() { }, ); - const getFilterCount = (filter: FilterLabel, value: string) => { - if (filterCountsQuery.isLoading) { - return 0; - } - const filterCountsData = filterCountsQuery.data!; - return filterCountsData[filter][value]; - }; - const onSubmitResume = () => { if (sessionData === null) { router.push('/api/auth/signin'); @@ -336,6 +321,18 @@ export default function ResumeHomePage() { starredResumesQuery.isFetching || myResumesQuery.isFetching; + const getTabFilterCounts = () => { + return getTabQueryData()?.filterCounts; + }; + + const getFilterCount = (filter: FilterLabel, value: string) => { + const filterCountsData = getTabFilterCounts(); + if (!filterCountsData) { + return 0; + } + return filterCountsData[filter][value]; + }; + return ( <> diff --git a/apps/portal/src/server/router/resumes/resumes-resume-router.ts b/apps/portal/src/server/router/resumes/resumes-resume-router.ts index 1b062c10..177a9189 100644 --- a/apps/portal/src/server/router/resumes/resumes-resume-router.ts +++ b/apps/portal/src/server/router/resumes/resumes-resume-router.ts @@ -11,8 +11,8 @@ export const resumesRouter = createRouter() .query('findAll', { input: z.object({ experienceFilters: z.string().array(), + isUnreviewed: z.boolean(), locationFilters: z.string().array(), - numComments: z.number().optional(), roleFilters: z.string().array(), searchValue: z.string(), skip: z.number(), @@ -25,7 +25,7 @@ export const resumesRouter = createRouter() locationFilters, experienceFilters, sortOrder, - numComments, + isUnreviewed, skip, searchValue, take, @@ -33,12 +33,8 @@ export const resumesRouter = createRouter() const userId = ctx.session?.user?.id; const totalRecords = await ctx.prisma.resumesResume.count({ where: { - ...(numComments === 0 && { - comments: { - none: {}, - }, - }), experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, location: { in: locationFilters }, role: { in: roleFilters }, title: { contains: searchValue, mode: 'insensitive' }, @@ -81,12 +77,8 @@ export const resumesRouter = createRouter() skip, take, where: { - ...(numComments === 0 && { - comments: { - none: {}, - }, - }), experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, location: { in: locationFilters }, role: { in: roleFilters }, title: { contains: searchValue, mode: 'insensitive' }, @@ -110,7 +102,105 @@ export const resumesRouter = createRouter() }; return resume; }); - return { mappedResumeData, totalRecords }; + + // Group by role and count, taking into account all role/experience/location/isUnreviewed filters and search value + const roleCounts = await ctx.prisma.resumesResume.groupBy({ + _count: { + _all: true, + }, + by: ['role'], + where: { + experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, + location: { in: locationFilters }, + role: { in: roleFilters }, + title: { contains: searchValue, mode: 'insensitive' }, + }, + }); + + // Map all nonzero counts from array to object where key = role and value = count + const mappedRoleCounts = Object.fromEntries( + roleCounts.map((rc) => [rc.role, rc._count._all]), + ); + + // Filter out roles with zero counts and map to object where key = role and value = 0 + const zeroRoleCounts = Object.fromEntries( + ROLES.filter((r) => !(r.value in mappedRoleCounts)).map((r) => [ + r.value, + 0, + ]), + ); + + // Combine to form singular role counts object + const processedRoleCounts = { + ...mappedRoleCounts, + ...zeroRoleCounts, + }; + + const experienceCounts = await ctx.prisma.resumesResume.groupBy({ + _count: { + _all: true, + }, + by: ['experience'], + where: { + experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, + location: { in: locationFilters }, + role: { in: roleFilters }, + title: { contains: searchValue, mode: 'insensitive' }, + }, + }); + const mappedExperienceCounts = Object.fromEntries( + experienceCounts.map((ec) => [ec.experience, ec._count._all]), + ); + const zeroExperienceCounts = Object.fromEntries( + EXPERIENCES.filter((e) => !(e.value in mappedExperienceCounts)).map( + (e) => [e.value, 0], + ), + ); + const processedExperienceCounts = { + ...mappedExperienceCounts, + ...zeroExperienceCounts, + }; + + const locationCounts = await ctx.prisma.resumesResume.groupBy({ + _count: { + _all: true, + }, + by: ['location'], + where: { + experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, + location: { in: locationFilters }, + role: { in: roleFilters }, + title: { contains: searchValue, mode: 'insensitive' }, + }, + }); + const mappedLocationCounts = Object.fromEntries( + locationCounts.map((lc) => [lc.location, lc._count._all]), + ); + const zeroLocationCounts = Object.fromEntries( + LOCATIONS.filter((l) => !(l.value in mappedLocationCounts)).map((l) => [ + l.value, + 0, + ]), + ); + const processedLocationCounts = { + ...mappedLocationCounts, + ...zeroLocationCounts, + }; + + const filterCounts = { + Experience: processedExperienceCounts, + Location: processedLocationCounts, + Role: processedRoleCounts, + }; + + return { + filterCounts, + mappedResumeData, + totalRecords, + }; }, }) .query('findOne', { diff --git a/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts b/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts index 7858afb3..18d11477 100644 --- a/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts +++ b/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters'; + import { createProtectedRouter } from '../context'; import type { Resume } from '~/types/resume'; @@ -64,8 +66,8 @@ export const resumesResumeUserRouter = createProtectedRouter() .query('findUserStarred', { input: z.object({ experienceFilters: z.string().array(), + isUnreviewed: z.boolean(), locationFilters: z.string().array(), - numComments: z.number().optional(), roleFilters: z.string().array(), searchValue: z.string(), skip: z.number(), @@ -80,19 +82,15 @@ export const resumesResumeUserRouter = createProtectedRouter() experienceFilters, searchValue, sortOrder, - numComments, + isUnreviewed, skip, take, } = input; const totalRecords = await ctx.prisma.resumesStar.count({ where: { resume: { - ...(numComments === 0 && { - comments: { - none: {}, - }, - }), experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, location: { in: locationFilters }, role: { in: roleFilters }, title: { contains: searchValue, mode: 'insensitive' }, @@ -144,12 +142,8 @@ export const resumesResumeUserRouter = createProtectedRouter() take, where: { resume: { - ...(numComments === 0 && { - comments: { - none: {}, - }, - }), experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, location: { in: locationFilters }, role: { in: roleFilters }, title: { contains: searchValue, mode: 'insensitive' }, @@ -176,14 +170,116 @@ export const resumesResumeUserRouter = createProtectedRouter() }; return resume; }); - return { mappedResumeData, totalRecords }; + + const roleCounts = await ctx.prisma.resumesResume.groupBy({ + _count: { + _all: true, + }, + by: ['role'], + where: { + experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, + location: { in: locationFilters }, + role: { in: roleFilters }, + stars: { + some: { + userId, + }, + }, + title: { contains: searchValue, mode: 'insensitive' }, + }, + }); + const mappedRoleCounts = Object.fromEntries( + roleCounts.map((rc) => [rc.role, rc._count._all]), + ); + const zeroRoleCounts = Object.fromEntries( + ROLES.filter((r) => !(r.value in mappedRoleCounts)).map((r) => [ + r.value, + 0, + ]), + ); + const processedRoleCounts = { + ...mappedRoleCounts, + ...zeroRoleCounts, + }; + + const experienceCounts = await ctx.prisma.resumesResume.groupBy({ + _count: { + _all: true, + }, + by: ['experience'], + where: { + experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, + location: { in: locationFilters }, + role: { in: roleFilters }, + stars: { + some: { + userId, + }, + }, + title: { contains: searchValue, mode: 'insensitive' }, + }, + }); + const mappedExperienceCounts = Object.fromEntries( + experienceCounts.map((ec) => [ec.experience, ec._count._all]), + ); + const zeroExperienceCounts = Object.fromEntries( + EXPERIENCES.filter((e) => !(e.value in mappedExperienceCounts)).map( + (e) => [e.value, 0], + ), + ); + const processedExperienceCounts = { + ...mappedExperienceCounts, + ...zeroExperienceCounts, + }; + + const locationCounts = await ctx.prisma.resumesResume.groupBy({ + _count: { + _all: true, + }, + by: ['location'], + where: { + experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, + location: { in: locationFilters }, + role: { in: roleFilters }, + stars: { + some: { + userId, + }, + }, + title: { contains: searchValue, mode: 'insensitive' }, + }, + }); + const mappedLocationCounts = Object.fromEntries( + locationCounts.map((lc) => [lc.location, lc._count._all]), + ); + const zeroLocationCounts = Object.fromEntries( + LOCATIONS.filter((l) => !(l.value in mappedLocationCounts)).map((l) => [ + l.value, + 0, + ]), + ); + const processedLocationCounts = { + ...mappedLocationCounts, + ...zeroLocationCounts, + }; + + const filterCounts = { + Experience: processedExperienceCounts, + Location: processedLocationCounts, + Role: processedRoleCounts, + }; + + return { filterCounts, mappedResumeData, totalRecords }; }, }) .query('findUserCreated', { input: z.object({ experienceFilters: z.string().array(), + isUnreviewed: z.boolean(), locationFilters: z.string().array(), - numComments: z.number().optional(), roleFilters: z.string().array(), searchValue: z.string(), skip: z.number(), @@ -198,18 +294,14 @@ export const resumesResumeUserRouter = createProtectedRouter() experienceFilters, sortOrder, searchValue, - numComments, + isUnreviewed, take, skip, } = input; const totalRecords = await ctx.prisma.resumesResume.count({ where: { - ...(numComments === 0 && { - comments: { - none: {}, - }, - }), experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, location: { in: locationFilters }, role: { in: roleFilters }, title: { contains: searchValue, mode: 'insensitive' }, @@ -250,12 +342,8 @@ export const resumesResumeUserRouter = createProtectedRouter() skip, take, where: { - ...(numComments === 0 && { - comments: { - none: {}, - }, - }), experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, location: { in: locationFilters }, role: { in: roleFilters }, title: { contains: searchValue, mode: 'insensitive' }, @@ -280,6 +368,96 @@ export const resumesResumeUserRouter = createProtectedRouter() }; return resume; }); - return { mappedResumeData, totalRecords }; + + const roleCounts = await ctx.prisma.resumesResume.groupBy({ + _count: { + _all: true, + }, + by: ['role'], + where: { + experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, + location: { in: locationFilters }, + role: { in: roleFilters }, + title: { contains: searchValue, mode: 'insensitive' }, + userId, + }, + }); + const mappedRoleCounts = Object.fromEntries( + roleCounts.map((rc) => [rc.role, rc._count._all]), + ); + const zeroRoleCounts = Object.fromEntries( + ROLES.filter((r) => !(r.value in mappedRoleCounts)).map((r) => [ + r.value, + 0, + ]), + ); + const processedRoleCounts = { + ...mappedRoleCounts, + ...zeroRoleCounts, + }; + + const experienceCounts = await ctx.prisma.resumesResume.groupBy({ + _count: { + _all: true, + }, + by: ['experience'], + where: { + experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, + location: { in: locationFilters }, + role: { in: roleFilters }, + title: { contains: searchValue, mode: 'insensitive' }, + userId, + }, + }); + const mappedExperienceCounts = Object.fromEntries( + experienceCounts.map((ec) => [ec.experience, ec._count._all]), + ); + const zeroExperienceCounts = Object.fromEntries( + EXPERIENCES.filter((e) => !(e.value in mappedExperienceCounts)).map( + (e) => [e.value, 0], + ), + ); + const processedExperienceCounts = { + ...mappedExperienceCounts, + ...zeroExperienceCounts, + }; + + const locationCounts = await ctx.prisma.resumesResume.groupBy({ + _count: { + _all: true, + }, + by: ['location'], + where: { + experience: { in: experienceFilters }, + isResolved: isUnreviewed ? false : {}, + location: { in: locationFilters }, + role: { in: roleFilters }, + title: { contains: searchValue, mode: 'insensitive' }, + userId, + }, + }); + const mappedLocationCounts = Object.fromEntries( + locationCounts.map((lc) => [lc.location, lc._count._all]), + ); + const zeroLocationCounts = Object.fromEntries( + LOCATIONS.filter((l) => !(l.value in mappedLocationCounts)).map((l) => [ + l.value, + 0, + ]), + ); + const processedLocationCounts = { + ...mappedLocationCounts, + ...zeroLocationCounts, + }; + + const filterCounts = { + Experience: processedExperienceCounts, + Location: processedLocationCounts, + Role: processedRoleCounts, + }; + + return { filterCounts, mappedResumeData, totalRecords }; }, }); diff --git a/apps/portal/src/utils/resumes/resumeFilters.ts b/apps/portal/src/utils/resumes/resumeFilters.ts index cf030d4e..a6fc9734 100644 --- a/apps/portal/src/utils/resumes/resumeFilters.ts +++ b/apps/portal/src/utils/resumes/resumeFilters.ts @@ -2,7 +2,7 @@ export type FilterId = 'experience' | 'location' | 'role'; export type FilterLabel = 'Experience' | 'Location' | 'Role'; export type CustomFilter = { - numComments: number; + isUnreviewed: boolean; }; export type RoleFilter = @@ -34,8 +34,7 @@ export type Filter = { options: Array>; }; -export type FilterState = Partial & - Record>; +export type FilterState = CustomFilter & Record>; export type SortOrder = 'latest' | 'mostComments' | 'popular'; @@ -94,20 +93,24 @@ export const LOCATIONS: Array> = [ export const INITIAL_FILTER_STATE: FilterState = { experience: Object.values(EXPERIENCES).map(({ value }) => value), + isUnreviewed: true, location: Object.values(LOCATIONS).map(({ value }) => value), role: Object.values(ROLES).map(({ value }) => value), }; export const SHORTCUTS: Array = [ { - filters: INITIAL_FILTER_STATE, + filters: { + ...INITIAL_FILTER_STATE, + isUnreviewed: false, + }, name: 'All', sortOrder: 'latest', }, { filters: { ...INITIAL_FILTER_STATE, - numComments: 0, + isUnreviewed: true, }, name: 'Unreviewed', sortOrder: 'latest', @@ -116,18 +119,23 @@ export const SHORTCUTS: Array = [ filters: { ...INITIAL_FILTER_STATE, experience: ['Entry Level (0 - 2 years)'], + isUnreviewed: false, }, name: 'Fresh Grad', sortOrder: 'latest', }, { - filters: INITIAL_FILTER_STATE, + filters: { + ...INITIAL_FILTER_STATE, + isUnreviewed: false, + }, name: 'Top 10', sortOrder: 'popular', }, { filters: { ...INITIAL_FILTER_STATE, + isUnreviewed: false, location: ['United States'], }, name: 'US Only',