diff --git a/src/features/user/Profile/components/MyContributions/index.tsx b/src/features/user/Profile/components/MyContributions/index.tsx index 7261606142..e411438586 100644 --- a/src/features/user/Profile/components/MyContributions/index.tsx +++ b/src/features/user/Profile/components/MyContributions/index.tsx @@ -76,6 +76,7 @@ export default function MyContributions({ const _detailInfo = trpc.myForest.stats.useQuery( { profileId: `${profile.id}`, + slug: `${profile.slug}`, }, { enabled: !!profile.id, @@ -84,7 +85,8 @@ export default function MyContributions({ ); const _conservedGeoJsonData = trpc.myForest.contributionsGeoJson.useQuery( { - profileId: `${profile.id}`, + profileId: profile.id, + slug: profile.slug, purpose: Purpose.CONSERVATION, }, { @@ -95,7 +97,8 @@ export default function MyContributions({ const _treePlantedGeoJsonData = trpc.myForest.contributionsGeoJson.useQuery( { - profileId: `${profile.id}`, + profileId: profile.id, + slug: profile.slug, purpose: Purpose.TREES, }, { @@ -107,7 +110,8 @@ export default function MyContributions({ const _plantedTreesContribution = trpc.myForest.contributions.useInfiniteQuery( { - profileId: `${profile.id}`, + profileId: profile.id, + slug: profile.slug, limit: 15, purpose: Purpose.TREES, }, @@ -121,7 +125,8 @@ export default function MyContributions({ const _conservationContribution = trpc.myForest.contributions.useInfiniteQuery( { - profileId: `${profile.id}`, + profileId: profile.id, + slug: profile.slug, limit: 15, purpose: Purpose.CONSERVATION, }, diff --git a/src/server/procedures/myForest/contributions.ts b/src/server/procedures/myForest/contributions.ts index 6885d79f83..acd891f1bc 100644 --- a/src/server/procedures/myForest/contributions.ts +++ b/src/server/procedures/myForest/contributions.ts @@ -9,347 +9,373 @@ export const contributions = procedure .input( z.object({ profileId: z.string(), + slug: z.string(), purpose: z.nullable(z.nativeEnum(Purpose)).optional(), limit: z.number(), cursor: z.string().nullish(), skip: z.number().optional(), }) ) - .query(async ({ input: { profileId, limit, cursor, skip, purpose } }) => { - const profile = await prisma.profile.findFirst({ - where: { - guid: profileId, - }, - }); - - if (!profile) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Profile not found', + .query( + async ({ input: { profileId, slug, limit, cursor, skip, purpose } }) => { + const profile = await prisma.profile.findFirst({ + where: { + guid: profileId, + }, }); - } - const _cursor = cursor ? cursor.split(',') : undefined; - - const contributionsCursor = - _cursor?.[0] !== 'undefined' && _cursor?.[0] !== 'null' - ? _cursor?.[0] - : undefined; - - const giftDataCursor = - _cursor?.[1] !== 'undefined' && _cursor?.[1] !== 'null' - ? _cursor?.[1] - : undefined; - - // Fetch contributions and gifts - - const contributions = await prisma.contribution.findMany({ - select: { - guid: true, - purpose: true, - treeCount: true, - quantity: true, - plantDate: true, - contributionType: true, - tenant: { - select: { - guid: true, - name: true, + if (!profile) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Profile not found', + }); + } + + const groupTreecounterData = await prisma.$queryRaw< + { + profile_id: string; + }[] + >` + SELECT p.guid as profile_id + FROM profile p + INNER JOIN treecounter t ON p.treecounter_id = t.id + INNER JOIN treecounter_group child ON child.treecounter_id = t.id + INNER JOIN treecounter_group parent ON child.root_id = parent.id + WHERE parent.slug = ${slug}; + `; + + const profileIds = + groupTreecounterData.length > 0 + ? groupTreecounterData.map(({ profile_id }) => profile_id) + : [profileId]; + + const _cursor = cursor ? cursor.split(',') : undefined; + + const contributionsCursor = + _cursor?.[0] !== 'undefined' && _cursor?.[0] !== 'null' + ? _cursor?.[0] + : undefined; + + const giftDataCursor = + _cursor?.[1] !== 'undefined' && _cursor?.[1] !== 'null' + ? _cursor?.[1] + : undefined; + + // Fetch contributions and gifts + + const contributions = await prisma.contribution.findMany({ + select: { + guid: true, + purpose: true, + treeCount: true, + quantity: true, + plantDate: true, + contributionType: true, + tenant: { + select: { + guid: true, + name: true, + }, }, - }, - bouquetContributions: { - select: { - purpose: true, - treeCount: true, - quantity: true, - plantDate: true, - contributionType: true, - tenant: { - select: { - guid: true, - name: true, + bouquetContributions: { + select: { + purpose: true, + treeCount: true, + quantity: true, + plantDate: true, + contributionType: true, + tenant: { + select: { + guid: true, + name: true, + }, }, - }, - plantProject: { - select: { - allowDonations: true, - guid: true, - name: true, - image: true, - country: true, - unit: true, - location: true, - geoLatitude: true, - geoLongitude: true, - tpo: true, + plantProject: { + select: { + allowDonations: true, + guid: true, + name: true, + image: true, + country: true, + unit: true, + location: true, + geoLatitude: true, + geoLongitude: true, + tpo: true, + }, }, }, + orderBy: { + plantDate: 'desc', + }, }, - orderBy: { - plantDate: 'desc', + plantProject: { + select: { + allowDonations: true, + guid: true, + name: true, + image: true, + country: true, + unit: true, + location: true, + geoLatitude: true, + geoLongitude: true, + tpo: true, + }, }, + giftTo: true, }, - plantProject: { - select: { - allowDonations: true, - guid: true, - name: true, - image: true, - country: true, - unit: true, - location: true, - geoLatitude: true, - geoLongitude: true, - tpo: true, + where: { + profile: { + guid: { in: profileIds }, }, - }, - giftTo: true, - }, - where: { - profile: { - guid: profileId, - }, - deletedAt: null, - OR: [ - { - contributionType: 'donation', - paymentStatus: 'paid', - plantProject: { - purpose: { - in: !purpose - ? ['trees', 'conservation', 'bouquet'] - : purpose === Purpose.TREES - ? ['trees', 'bouquet'] - : ['conservation', 'bouquet'], + deletedAt: null, + OR: [ + { + contributionType: 'donation', + paymentStatus: 'paid', + plantProject: { + purpose: { + in: !purpose + ? ['trees', 'conservation', 'bouquet'] + : purpose === Purpose.TREES + ? ['trees', 'bouquet'] + : ['conservation', 'bouquet'], + }, + ...(purpose + ? { + OR: [ + { bouquetPurpose: purpose }, + { bouquetPurpose: null }, + ], + } + : {}), }, - ...(purpose + bouquetDonationId: { + equals: null, + }, + }, + { + ...(purpose === undefined || purpose === Purpose.TREES ? { - OR: [{ bouquetPurpose: purpose }, { bouquetPurpose: null }], + contributionType: 'planting', + isVerified: 1, + bouquetDonationId: { + equals: null, + }, } : {}), }, - bouquetDonationId: { - equals: null, - }, - }, - { - ...(purpose === undefined || purpose === Purpose.TREES - ? { - contributionType: 'planting', - isVerified: 1, - bouquetDonationId: { - equals: null, - }, - } - : {}), + ], + plantDate: { + lte: contributionsCursor + ? new Date(contributionsCursor) + : new Date(), }, - ], - plantDate: { - lte: contributionsCursor ? new Date(contributionsCursor) : new Date(), }, - }, - orderBy: { - plantDate: 'desc', - }, - skip: skip, - take: _cursor && _cursor[0] === 'undefined' ? 0 : limit + 1, - }); - - const gifts = await prisma.gift.findMany({ - select: { - plantDate: true, - value: true, - guid: true, - recipient: true, - metadata: true, - purpose: true, - type: true, - redemptionDate: true, - }, - where: { - recipient: { - guid: profileId, + orderBy: { + plantDate: 'desc', }, - purpose: { - equals: - purpose === Purpose.TREES ? Purpose.TREES : Purpose.CONSERVATION, + skip: skip, + take: _cursor && _cursor[0] === 'undefined' ? 0 : limit + 1, + }); + + const gifts = await prisma.gift.findMany({ + select: { + plantDate: true, + value: true, + guid: true, + recipient: true, + metadata: true, + purpose: true, + type: true, + redemptionDate: true, }, - OR: [ - { - plantDate: { - lte: giftDataCursor ? new Date(giftDataCursor) : new Date(), - }, + where: { + recipient: { + guid: { in: profileIds }, }, - { - redemptionDate: { - lte: giftDataCursor ? new Date(giftDataCursor) : new Date(), + purpose: { + equals: + purpose === Purpose.TREES ? Purpose.TREES : Purpose.CONSERVATION, + }, + OR: [ + { + plantDate: { + lte: giftDataCursor ? new Date(giftDataCursor) : new Date(), + }, }, + { + redemptionDate: { + lte: giftDataCursor ? new Date(giftDataCursor) : new Date(), + }, + }, + ], + }, + orderBy: { + plantDate: 'desc', + }, + skip: skip, + take: _cursor && _cursor[1] === 'undefined' ? 0 : limit + 1, + }); + + // There are gifts in the database that don't have an image, so we need to fetch them separately here + // and fetch the images from the project table and prep them for the response + + const giftProjectsWithoutImage = + gifts.length > 0 + ? gifts + .filter( + (gift) => + !JSON.parse(JSON.stringify(gift.metadata))?.project?.image + ) + .map( + (gift) => JSON.parse(JSON.stringify(gift.metadata))?.project?.id + ) + : []; + + const projectsWithImage = await prisma.project.findMany({ + select: { + guid: true, + image: true, + }, + where: { + guid: { + in: giftProjectsWithoutImage, }, - ], - }, - orderBy: { - plantDate: 'desc', - }, - skip: skip, - take: _cursor && _cursor[1] === 'undefined' ? 0 : limit + 1, - }); - - // There are gifts in the database that don't have an image, so we need to fetch them separately here - // and fetch the images from the project table and prep them for the response - - const giftProjectsWithoutImage = - gifts.length > 0 - ? gifts - .filter( - (gift) => - !JSON.parse(JSON.stringify(gift.metadata))?.project?.image - ) - .map( + }, + }); + + // There are gifts in the database that don't have an allowDonations field, so we need to fetch them separately here + // and fetch the allowDonations from the project table and prep them for the response + + const giftProjectIds = + gifts.length > 0 + ? gifts.map( (gift) => JSON.parse(JSON.stringify(gift.metadata))?.project?.id ) - : []; - - const projectsWithImage = await prisma.project.findMany({ - select: { - guid: true, - image: true, - }, - where: { - guid: { - in: giftProjectsWithoutImage, + : []; + + const giftProjects = await prisma.project.findMany({ + select: { + guid: true, + allowDonations: true, }, - }, - }); - - // There are gifts in the database that don't have an allowDonations field, so we need to fetch them separately here - // and fetch the allowDonations from the project table and prep them for the response - - const giftProjectIds = - gifts.length > 0 - ? gifts.map( - (gift) => JSON.parse(JSON.stringify(gift.metadata))?.project?.id - ) - : []; - - const giftProjects = await prisma.project.findMany({ - select: { - guid: true, - allowDonations: true, - }, - where: { - guid: { - in: giftProjectIds, + where: { + guid: { + in: giftProjectIds, + }, }, - }, - }); - - // Process the prepared data - - function processGiftData( - giftObjects: typeof gifts, - projectsWithImage: { - guid: string; - image: string | null; - }[], - giftProjects: { - guid: string; - allowDonations: boolean; - }[] - ) { - return giftObjects.map((giftObject) => { - const projectId = JSON.parse(JSON.stringify(giftObject.metadata)) - ?.project?.id; - const projectImage = projectsWithImage.find( - (project) => project.guid === projectId - )?.image; - const projectAllowDonations = giftProjects.find( - (project) => project.guid === projectId - )?.allowDonations; - - const giftMetadata = JSON.parse(JSON.stringify(giftObject.metadata)); - - const _gift = { - ...giftObject, - _type: 'gift', - quantity: giftObject.value ? giftObject.value / 100 : 0, - metadata: { - ...(giftObject?.metadata as object), - project: { - ...giftMetadata?.project, - image: giftMetadata?.project?.image ?? projectImage, + }); + + // Process the prepared data + + function processGiftData( + giftObjects: typeof gifts, + projectsWithImage: { + guid: string; + image: string | null; + }[], + giftProjects: { + guid: string; + allowDonations: boolean; + }[] + ) { + return giftObjects.map((giftObject) => { + const projectId = JSON.parse(JSON.stringify(giftObject.metadata)) + ?.project?.id; + const projectImage = projectsWithImage.find( + (project) => project.guid === projectId + )?.image; + const projectAllowDonations = giftProjects.find( + (project) => project.guid === projectId + )?.allowDonations; + + const giftMetadata = JSON.parse(JSON.stringify(giftObject.metadata)); + + const _gift = { + ...giftObject, + _type: 'gift', + quantity: giftObject.value ? giftObject.value / 100 : 0, + metadata: { + ...(giftObject?.metadata as object), + project: { + ...giftMetadata?.project, + image: giftMetadata?.project?.image ?? projectImage, + }, }, - }, - allowDonations: projectAllowDonations, - plantDate: giftObject.plantDate - ? giftObject.plantDate - : giftObject.redemptionDate, - }; + allowDonations: projectAllowDonations, + plantDate: giftObject.plantDate + ? giftObject.plantDate + : giftObject.redemptionDate, + }; - delete _gift.redemptionDate; + delete _gift.redemptionDate; - return _gift; - }); - } + return _gift; + }); + } + + function processContributionData( + contributionResults: typeof contributions + ) { + return contributionResults.map((contribution) => { + return { + ...contribution, + _type: 'contribution', + }; + }); + } - function processContributionData( - contributionResults: typeof contributions - ) { - return contributionResults.map((contribution) => { - return { - ...contribution, - _type: 'contribution', - }; + const combinedData = [ + ...processContributionData(contributions), + ...processGiftData(gifts, projectsWithImage, giftProjects), + ]; + + const sortedData = combinedData.sort((a, b) => { + // Move objects with null plantDate to the beginning + if (!a.plantDate) return 1; + if (!b.plantDate) return -1; + return b.plantDate.getTime() - a.plantDate.getTime(); }); - } - const combinedData = [ - ...processContributionData(contributions), - ...processGiftData(gifts, projectsWithImage, giftProjects), - ]; - - const sortedData = combinedData.sort((a, b) => { - // Move objects with null plantDate to the beginning - if (!a.plantDate) return 1; - if (!b.plantDate) return -1; - return b.plantDate.getTime() - a.plantDate.getTime(); - }); - - const data = sortedData.slice(0, limit); - let nextCursor; - if (sortedData.length > limit) { - const nextItem = sortedData[limit]; // Get the (limit + 1)-th item - let nextContributionCursor: Date | undefined | null; - let nextGiftDataCursor: Date | undefined | null; - - // Iterate over the remaining items to find the next cursors - for (const item of sortedData.slice(limit)) { - if (item._type === 'contribution' && !nextContributionCursor) { - nextContributionCursor = item.plantDate; - } else if (item._type === 'gift' && !nextGiftDataCursor) { - nextGiftDataCursor = item.plantDate; + const data = sortedData.slice(0, limit); + let nextCursor; + if (sortedData.length > limit) { + const nextItem = sortedData[limit]; // Get the (limit + 1)-th item + let nextContributionCursor: Date | undefined | null; + let nextGiftDataCursor: Date | undefined | null; + + // Iterate over the remaining items to find the next cursors + for (const item of sortedData.slice(limit)) { + if (item._type === 'contribution' && !nextContributionCursor) { + nextContributionCursor = item.plantDate; + } else if (item._type === 'gift' && !nextGiftDataCursor) { + nextGiftDataCursor = item.plantDate; + } + + // Break if both cursors are found + if (nextContributionCursor && nextGiftDataCursor) { + break; + } } - // Break if both cursors are found - if (nextContributionCursor && nextGiftDataCursor) { - break; + // If only one type of data reached the limit, set the cursor for the other type + if (!nextContributionCursor) { + nextContributionCursor = + nextItem._type === 'contribution' ? nextItem.plantDate : undefined; + } + if (!nextGiftDataCursor) { + nextGiftDataCursor = + nextItem._type === 'gift' ? nextItem.plantDate : undefined; } - } - // If only one type of data reached the limit, set the cursor for the other type - if (!nextContributionCursor) { - nextContributionCursor = - nextItem._type === 'contribution' ? nextItem.plantDate : undefined; - } - if (!nextGiftDataCursor) { - nextGiftDataCursor = - nextItem._type === 'gift' ? nextItem.plantDate : undefined; + nextCursor = `${nextContributionCursor?.toISOString()},${nextGiftDataCursor?.toISOString()}`; } - nextCursor = `${nextContributionCursor?.toISOString()},${nextGiftDataCursor?.toISOString()}`; + return { + data, + nextCursor, + }; } - - return { - data, - nextCursor, - }; - }); + ); diff --git a/src/server/procedures/myForest/contributionsGeoJson.ts b/src/server/procedures/myForest/contributionsGeoJson.ts index 174722f5fd..05640c57cb 100644 --- a/src/server/procedures/myForest/contributionsGeoJson.ts +++ b/src/server/procedures/myForest/contributionsGeoJson.ts @@ -130,10 +130,11 @@ export const contributionsGeoJson = procedure .input( z.object({ profileId: z.string(), + slug: z.string(), purpose: z.nullable(z.nativeEnum(Purpose)).optional(), }) ) - .query(async ({ input: { profileId, purpose } }) => { + .query(async ({ input: { profileId, slug, purpose } }) => { const profile = await prisma.profile.findFirst({ where: { guid: profileId, @@ -147,6 +148,25 @@ export const contributionsGeoJson = procedure }); } + const groupTreecounterData = await prisma.$queryRaw< + { + profile_id: string; + }[] + >` + SELECT p.guid as profile_id + FROM profile p + INNER JOIN treecounter t ON p.treecounter_id = t.id + INNER JOIN treecounter_group child ON child.treecounter_id = t.id + INNER JOIN treecounter_group parent ON child.root_id = parent.id + WHERE parent.slug = ${slug}; + `; + + const profileIds = Prisma.join( + groupTreecounterData.length > 0 + ? groupTreecounterData.map(({ profile_id }) => profile_id) + : [profileId] + ); + let purposes; let join = Prisma.sql`LEFT JOIN project pp ON c.plant_project_id = pp.id`; let registerTreesClause = Prisma.sql`OR ( @@ -174,7 +194,7 @@ export const contributionsGeoJson = procedure ${join} JOIN profile p ON p.id = c.profile_id LEFT JOIN profile tpo ON pp.tpo_id = tpo.id - WHERE p.guid = ${profileId} + WHERE p.guid IN (${profileIds}) AND c.deleted_at IS null AND ( ( @@ -191,7 +211,9 @@ export const contributionsGeoJson = procedure g.metadata as metadata, g.created as created FROM gift g JOIN profile p ON g.recipient_id = p.id - WHERE p.guid = ${profileId} AND g.purpose IN (${Prisma.join(purposes)}) + WHERE p.guid IN (${profileIds}) AND g.purpose IN (${Prisma.join( + purposes + )}) `; const giftProjectsWithoutImage = diff --git a/src/server/procedures/myForest/stats.ts b/src/server/procedures/myForest/stats.ts index 1c64e8791a..e656869a64 100644 --- a/src/server/procedures/myForest/stats.ts +++ b/src/server/procedures/myForest/stats.ts @@ -9,14 +9,16 @@ import { GiftStatsQueryResult, StatsResult, } from '../../../features/common/types/myForest'; +import { Prisma } from '@prisma/client'; export const stats = procedure .input( z.object({ profileId: z.string(), + slug: z.string(), }) ) - .query(async ({ input: { profileId } }) => { + .query(async ({ input: { profileId, slug } }) => { const profile = await prisma.profile.findFirst({ where: { guid: profileId, @@ -30,6 +32,29 @@ export const stats = procedure }); } + const groupTreecounterData = await prisma.$queryRaw< + { + profile_id: string; + }[] + >` + SELECT p.guid as profile_id + FROM profile p + INNER JOIN treecounter t ON p.treecounter_id = t.id + INNER JOIN treecounter_group child ON child.treecounter_id = t.id + INNER JOIN treecounter_group parent ON child.root_id = parent.id + WHERE parent.slug = ${slug}; + `; + + // console.log('groupTreecounterData:', groupTreecounterData); + + const profileIds = Prisma.join( + groupTreecounterData.length > 0 + ? groupTreecounterData.map(({ profile_id }) => profile_id) + : [profileId] + ); + + // console.log(profileIds); + const contributionData = await prisma.$queryRaw< ContributionStatsQueryResult[] >` @@ -43,7 +68,7 @@ export const stats = procedure LEFT JOIN project pp ON c.plant_project_id = pp.id JOIN profile p ON p.id = c.profile_id WHERE - p.guid = ${profileId} + p.guid IN (${profileIds}) AND c.deleted_at IS NULL AND ( ( @@ -65,7 +90,7 @@ export const stats = procedure SUM(CASE WHEN (g.purpose = 'conservation') THEN (value/100) ELSE 0 END) AS conserved FROM gift g JOIN profile p ON g.recipient_id = p.id - WHERE p.guid = ${profileId} + WHERE p.guid IN (${profileIds}) `; // console.log('giftData:', giftData); @@ -85,7 +110,7 @@ export const stats = procedure LEFT JOIN project pp ON c.plant_project_id = pp.id JOIN profile p ON p.id = c.profile_id WHERE - p.guid = ${profileId} + p.guid IN (${profileIds}) AND c.deleted_at IS NULL AND pp.guid IS NOT NULL AND pp.country IS NOT NULL @@ -108,7 +133,7 @@ export const stats = procedure gift g JOIN profile p ON g.recipient_id = p.id WHERE - p.guid = ${profileId} + p.guid IN (${profileIds}) AND g.deleted_at IS NULL AND g.purpose IN ('trees', 'conservation') AND (metadata ->> '$.project.id') IS NOT NULL