diff --git a/src/composables/queries/useFamilyQuery.js b/src/composables/queries/useFamilyQuery.js new file mode 100644 index 000000000..99cdfc4c6 --- /dev/null +++ b/src/composables/queries/useFamilyQuery.js @@ -0,0 +1,30 @@ +import { useQuery } from '@tanstack/vue-query'; +import _isEmpty from 'lodash/isEmpty'; +import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; +import { fetchDocById } from '@/helpers/query/utils'; +import { FAMILY_QUERY_KEY } from '@/constants/queryKeys'; +import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; + +/** + * Family Query + * + * Query designed to fetch a single family record by its ID. + * + * @param {String} familyId – The ID of the family to fetch. + * @param {QueryOptions|undefined} queryOptions – Optional TanStack query options. + * @returns {UseQueryResult} The TanStack query result. + */ +const useFamilyQuery = (familyId, queryOptions = undefined) => { + // Ensure all necessary data is loaded before enabling the query. + const conditions = [() => !_isEmpty(familyId)]; + const { isQueryEnabled, options } = computeQueryOverrides(conditions, queryOptions); + + return useQuery({ + queryKey: [FAMILY_QUERY_KEY, familyId], + queryFn: () => fetchDocById(FIRESTORE_COLLECTIONS.FAMILIES, familyId), + enabled: isQueryEnabled, + ...options, + }); +}; + +export default useFamilyQuery; diff --git a/src/composables/queries/useFamilyQuery.test.js b/src/composables/queries/useFamilyQuery.test.js new file mode 100644 index 000000000..20c7e12fb --- /dev/null +++ b/src/composables/queries/useFamilyQuery.test.js @@ -0,0 +1,69 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import * as VueQuery from '@tanstack/vue-query'; +import { nanoid } from 'nanoid'; +import { withSetup } from '@/test-support/withSetup.js'; +import { fetchDocById } from '@/helpers/query/utils'; +import useFamilyQuery from './useFamilyQuery'; + +vi.mock('@/helpers/query/utils', () => ({ + fetchDocById: vi.fn().mockImplementation(() => []), +})); + +vi.mock('@tanstack/vue-query', async (getModule) => { + const original = await getModule(); + return { + ...original, + useQuery: vi.fn().mockImplementation(original.useQuery), + }; +}); + +describe('useFamilyQuery', () => { + let queryClient; + + beforeEach(() => { + queryClient = new VueQuery.QueryClient(); + }); + + afterEach(() => { + queryClient?.clear(); + }); + + it('should call query with correct parameters when fetching a specific family', () => { + const familyId = nanoid(); + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useFamilyQuery(familyId), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['family', familyId], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: true, + }), + }); + + expect(fetchDocById).toHaveBeenCalledWith('families', familyId); + }); + + it('should allow the query to be disabled via the passed query options', () => { + const familyId = nanoid(); + const queryOptions = { enabled: false }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useFamilyQuery(familyId, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['family', familyId], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + }); +}); diff --git a/src/constants/queryKeys.js b/src/constants/queryKeys.js index f8650264b..b6e0d1762 100644 --- a/src/constants/queryKeys.js +++ b/src/constants/queryKeys.js @@ -6,6 +6,7 @@ export const DISTRICT_QUERY_KEY = 'district'; export const DISTRICTS_QUERY_KEY = 'districts'; export const DISTRICT_SCHOOLS_QUERY_KEY = 'district-schools'; export const DSGF_ORGS_QUERY_KEY = 'dsgf-orgs'; +export const FAMILY_QUERY_KEY = 'family'; export const GROUP_QUERY_KEY = 'group'; export const GROUPS_QUERY_KEY = 'groups'; export const LEGAL_DOCS_QUERY_KEY = 'legal-docs'; diff --git a/src/pages/ScoreReport.vue b/src/pages/ScoreReport.vue index 8c814000d..4b3eabc27 100644 --- a/src/pages/ScoreReport.vue +++ b/src/pages/ScoreReport.vue @@ -3,18 +3,18 @@
-
+
Loading Org Info
-
+
{{ props.orgType }} Score Report
- {{ _toUpper(orgInfo?.name) }} + {{ _toUpper(orgData?.name) }}
@@ -234,7 +234,7 @@ :runs="computeAssignmentAndRunData.runsByTaskId[taskId]" :org-type="orgType" :org-id="orgId" - :org-info="orgInfo" + :org-info="orgData" :administration-info="administrationData" />
@@ -302,12 +302,14 @@ import { useAuthStore } from '@/store/auth'; import { getGrade } from '@bdelab/roar-utils'; import { exportCsv } from '@/helpers/query/utils'; import { getTitle } from '@/helpers/query/administrations'; +import useUserType from '@/composables/useUserType'; import useUserClaimsQuery from '@/composables/queries/useUserClaimsQuery'; import useAdministrationsQuery from '@/composables/queries/useAdministrationsQuery'; import useDistrictQuery from '@/composables/queries/useDistrictQuery'; import useSchoolQuery from '@/composables/queries/useSchoolQuery'; import useClassQuery from '@/composables/queries/useClassQuery'; import useGroupQuery from '@/composables/queries/useGroupQuery'; +import useFamilyQuery from '@/composables/queries/useFamilyQuery'; import useDistrictSchoolsQuery from '@/composables/queries/useDistrictSchoolsQuery'; import useAdministrationAssignmentsQuery from '@/composables/queries/useAdministrationAssignmentsQuery'; import { @@ -416,7 +418,7 @@ const handleExportToPdf = async () => { } doc.save( `roar-scores-${_kebabCase(getTitle(administrationData.value, isSuperAdmin.value))}-${_kebabCase( - orgInfo.value.name, + orgData.value.name, )}.pdf`, ); exportLoading.value = false; @@ -447,12 +449,11 @@ const filterSchools = ref([]); const filterGrades = ref([]); const pageLimit = ref(10); -const { isLoading: isLoadingClaims, data: userClaims } = useUserClaimsQuery({ +const { data: userClaims } = useUserClaimsQuery({ enabled: initialized, }); -const claimsLoaded = computed(() => !isLoadingClaims.value); -const isSuperAdmin = computed(() => Boolean(userClaims.value?.claims?.super_admin)); +const { isSuperAdmin } = useUserType(userClaims); const { data: administrationData } = useAdministrationsQuery([props.administrationId], { enabled: initialized, @@ -475,14 +476,13 @@ const orgQuery = computed(() => { case SINGULAR_ORG_TYPES.GROUPS: return useGroupQuery(props.orgId, queryOptions); case SINGULAR_ORG_TYPES.FAMILIES: - throw new Error('Families are not yet supported in this report.'); - // return useFamiliesQuery(props.orgId, queryOptions) + return useFamilyQuery(props.orgId, queryOptions); default: - return null; + throw new Error(`Unsupported org type: ${props.orgType}`); } }); -const { data: orgInfo, isLoading: isLoadingOrgInfo } = orgQuery.value; +const { data: orgData, isLoading: isLoadingOrgData } = orgQuery.value; const { isLoading: isLoadingAssignments, @@ -944,7 +944,7 @@ const exportSelected = (selectedRows) => { exportCsv( computedExportData, `roar-scores-${_kebabCase(getTitle(administrationData.value, isSuperAdmin.value))}-${_kebabCase( - orgInfo.value.name, + orgData.value.name, )}-selected.csv`, ); return; @@ -1018,7 +1018,7 @@ const exportAll = async () => { exportCsv( computedExportData, `roar-scores-${_kebabCase(getTitle(administrationData.value, isSuperAdmin.value))}-${_kebabCase( - orgInfo.value.name, + orgData.value.name, )}.csv`, ); return;