Skip to content

Commit

Permalink
Fix/stats query (#207)
Browse files Browse the repository at this point in the history
* fix: get events participant query, returning participant from another event

* fix: count query should be left join

* refactor: added startTime for attendance query

* chore: update code with previously refactored function

* feat: update endpoint & component with updated stat data to be shown

* fix: revert property name change

* fix: join with filtered participant query based on eventID

* fix: tooltip should be on fully attended stat

* fix: attended true at more than equal
  • Loading branch information
ghaniswara authored Apr 23, 2024
1 parent 9a1a3dd commit 433c593
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 141 deletions.
9 changes: 7 additions & 2 deletions app/(server)/_features/activity-log/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,19 @@ export const countGuestParticipant = async (roomID: string) => {
const res = await db
.select({ value: countDistinct(sql`${activitiesLog.meta} ->> 'clientID'`) })
.from(activitiesLog)
.fullJoin(
.leftJoin(
participant,
and(
eq(sql`${activitiesLog.meta} ->> 'clientID'`, participant.clientId),
eq(sql`${activitiesLog.meta} ->> 'roomID'`, roomID)
)
)
.where(isNull(participant.clientId));
.where(
and(
isNull(participant.clientId),
eq(sql`${activitiesLog.meta} ->> 'roomID'`, roomID)
)
);

return res[0];
};
Expand Down
59 changes: 38 additions & 21 deletions app/(server)/_features/event/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
participant as participants,
selectEvent,
} from './schema';
import { DBQueryConfig, SQL, and, count, eq, isNull, sql } from 'drizzle-orm';
import { DBQueryConfig, SQL, and, count, eq, isNull, sql } from 'drizzle-orm';
import { PageMeta } from '@/_shared/types/types';
import { User, users } from '../user/schema';
import { activitiesLog } from '../activity-log/schema';
Expand Down Expand Up @@ -445,61 +445,70 @@ export class EventRepo implements iEventRepo {
}

const subQueryConnectedClient = db
.select()
.select(
{
meta: activitiesLog.meta,
eventID : events.id
}
)
.from(activitiesLog)
.innerJoin(
events,
eq(events.roomId, sql`${activitiesLog.meta} ->> 'roomID'`)
and(eq(events.roomId, sql`${activitiesLog.meta} ->> 'roomID'`),eq(activitiesLog.name,"RoomDuration"))
)
.where(eq(events.id, eventId))
.as('ConnectedClientsLog');

// Removes the duplicates clientID
const subQueryUniqueConnectedClient = db
.selectDistinctOn([sql`${subQueryConnectedClient.activities_logs.meta} ->> 'clientID'`], {
.selectDistinctOn([sql`${subQueryConnectedClient.meta} ->> 'clientID'`], {
clientID:
sql<string>`${subQueryConnectedClient.activities_logs.meta} ->> 'clientID'`.as(
sql<string>`${subQueryConnectedClient.meta} ->> 'clientID'`.as(
'clientID'
),
name: sql<string>`${subQueryConnectedClient.activities_logs.meta} ->> 'name'`.as(
name: sql<string>`${subQueryConnectedClient.meta} ->> 'name'`.as(
'name'
),
eventID: subQueryConnectedClient.eventID,
})
.from(subQueryConnectedClient)
.as('UniqueConnectedClients');

// TODO : Query the alias for same clientID with same name
// subQueryGetAlias

const participantMatchEventID = db.select().from(participants).where(eq(participants.eventID, eventId)).as('participantEventID');

// add the isRegistered and isJoined field and email
const finalQuery = await db
.select({
clientID: subQueryUniqueConnectedClient.clientID,
name: sql<string>`
CASE
WHEN ${participants.firstName} IS NULL THEN ${subQueryUniqueConnectedClient.name}
ELSE CONCAT_WS(' ', ${participants.firstName}, ${participants.lastName})
WHEN ${participantMatchEventID.firstName} IS NULL THEN ${subQueryUniqueConnectedClient.name}
ELSE CONCAT_WS(' ', ${participantMatchEventID.firstName}, ${participantMatchEventID.lastName})
END
`.as('name'),
email: participants.email,
email: participantMatchEventID.email,
isRegistered: sql<boolean>`
CASE
WHEN ${participants.clientId} IS NULL THEN ${false}
WHEN ${participantMatchEventID.clientId} IS NULL THEN ${false}
ELSE ${true}
END
`.as('isRegistered'),
isJoined: sql<boolean>`
CASE
WHEN (${participants.clientId} IS NOT NULL AND ${subQueryUniqueConnectedClient.clientID} IS NOT NULL)
OR (${participants.clientId} IS NULL AND ${subQueryUniqueConnectedClient.clientID} IS NOT NULL) THEN ${true}
WHEN (${participantMatchEventID.clientId} IS NOT NULL AND ${subQueryUniqueConnectedClient.clientID} IS NOT NULL)
OR (${participantMatchEventID.clientId} IS NULL AND ${subQueryUniqueConnectedClient.clientID} IS NOT NULL) THEN ${true}
ELSE ${false}
END
`.as('isJoined'),
eventID : sql<number>` COALESCE(${participantMatchEventID.eventID},${subQueryUniqueConnectedClient.eventID})`.as('eventID')
})
.from(subQueryUniqueConnectedClient)
.fullJoin(
participants,
eq(participants.clientId, subQueryUniqueConnectedClient.clientID)
participantMatchEventID,
eq(participantMatchEventID.clientId, subQueryUniqueConnectedClient.clientID)
).as('participants')

const { data, meta } = await db.transaction(async (tx) => {
Expand All @@ -522,7 +531,15 @@ export class EventRepo implements iEventRepo {
return { data, meta };
}

async getParticipantAttendancePercentage(eventId: number): Promise<participantAttendances> {
/**
* Get the list of participants that fully attended the event based on the percentage of the event duration
*
*
* @param eventId the eventID
* @param percentage percentage value to be counted as fully attended (0-100)
* @returns list of participant that satisfy the percentage condition
*/
async getFullyAttendedParticipant(eventId: number,percentage:number): Promise<participantAttendances> {
const { participants, event } = await db.transaction(async (tx) => {
const event = await tx.query.events.findFirst({
where: eq(events.id, eventId)
Expand Down Expand Up @@ -551,8 +568,8 @@ export class EventRepo implements iEventRepo {

const participantAttendance = participants.map((participant) => {
const parsedLogs = ArrayRoomDurationMeta.parse(participant.combined_logs)
const totalDuration = getTotalJoinDuration(parsedLogs, event.endTime)/1000;
const isAttended = ((totalDuration / eventDuration) * 100) > 80;
const totalDuration = getTotalJoinDuration(parsedLogs,event.startTime, event.endTime)/1000;
const isAttended = ((totalDuration / eventDuration) * 100) >= percentage;
return {
clientID: participant.clientID,
joinDuration: totalDuration,
Expand All @@ -575,16 +592,16 @@ export class EventRepo implements iEventRepo {
}


function getTotalJoinDuration(intervals: z.infer<typeof RoomDurationMeta>[], sessionEndTime: Date): number {
function getTotalJoinDuration(intervals: z.infer<typeof RoomDurationMeta>[], sessionStartTime: Date, sessionEndTime: Date): number {
// Sort intervals by joinTime
intervals.sort((a, b) => a.joinTime.getTime() - b.joinTime.getTime());

let totalDuration = 0;
let currentEndTime: number | null = null;

for (const interval of intervals) {
// Check if interval is within session end time
const validJoinTime = interval.joinTime <= sessionEndTime;
// Check if interval is within session start and end time
const validJoinTime = interval.joinTime >= sessionStartTime && interval.joinTime <= sessionEndTime;
const validLeaveTime = interval.leaveTime <= sessionEndTime;

if (validJoinTime && validLeaveTime) {
Expand All @@ -609,7 +626,7 @@ function getTotalJoinDuration(intervals: z.infer<typeof RoomDurationMeta>[], ses
// No need to process further intervals if we've reached session end time
break;
}
// Ignore intervals outside session end time
// Ignore intervals outside session start and end time
}

return totalDuration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ export async function GET(
string,
{ isAttended: boolean; joinDuration: number }
>();
const registeredAttendance =
await eventRepo.getParticipantAttendancePercentage(event.id);
const registeredAttendance = await eventRepo.getFullyAttendedParticipant(
event.id,
80
);
registeredAttendance.participant.forEach((participant) => {
registeredAttendanceMap.set(participant.clientID, {
isAttended: participant.isAttended,
Expand Down
60 changes: 25 additions & 35 deletions app/(server)/api/events/[slugOrId]/stat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { cookies } from 'next/headers';
import { getCurrentAuthenticated } from '@/(server)/_shared/utils/get-current-authenticated';
import {
countGuestParticipant,
countParticipants,
countRegisteredParticipant,
} from '@/(server)/_features/activity-log/repository';
import { EventType } from '@/_shared/types/event';
Expand Down Expand Up @@ -80,47 +81,36 @@ export async function GET(
);
}

const countRegistirees = (
await eventRepo.countRegistiree(existingEvent?.id)
).value;
const countRegisteredJoin = (
await countRegisteredParticipant(existingEvent.roomId)
).value;
const countGuestJoin = (await countGuestParticipant(existingEvent.roomId))
const registered = (await eventRepo.countRegistiree(existingEvent?.id))
.value;
const totalJoined = countGuestJoin + countRegisteredJoin;

const registeredAttendance =
await eventRepo.getParticipantAttendancePercentage(existingEvent?.id);
const attended = (await countRegisteredParticipant(existingEvent.roomId))
.value;
const registeredAttendance = await eventRepo.getFullyAttendedParticipant(
existingEvent?.id,
80
);
const guest = (await countGuestParticipant(existingEvent.roomId)).value;
const total = (await countParticipants(existingEvent.roomId)).value;

const data: EventType.GetStatsResponse['data'] = {
count: {
registeree: countRegistirees || 0,

totalJoined: totalJoined,
registereeJoin: countRegisteredJoin || 0,
guestsJoin: countGuestJoin || 0,

registeredAttendance: registeredAttendance.attendedCount || 0,
registered: registered || 0,
totalJoined: total,
attended: attended || 0,
fullyAttended: registeredAttendance.attendedCount || 0,
guest: guest || 0,
},
percentage: {
guestCountJoin: ((countGuestJoin / totalJoined) * 100).toFixed(2),
registeredCountJoin: (
(countRegisteredJoin / totalJoined) *
100
).toFixed(2),
registeredCountRegisteree: (
(countRegisteredJoin / countRegistirees) *
100
).toFixed(2),
registeredAttendCountJoin: (
(registeredAttendance.attendedCount / totalJoined) *
100
).toFixed(2),
registeredAttendCountRegisteree: (
(registeredAttendance.attendedCount / countRegistirees) *
100
).toFixed(2),
attended: `${
registered ? ((attended / registered) * 100).toFixed(1) : '0'
}`,
fullyAttended: `${
registered
? ((registeredAttendance.attendedCount / registered) * 100).toFixed(
1
)
: '0'
}`,
},
};

Expand Down
34 changes: 18 additions & 16 deletions app/_features/event/components/event-past-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,13 @@ function ParticipantTable({ eventID }: { eventID: string | number }) {
scope="col"
className="whitespace-nowrap px-3 py-2 text-xs font-medium lg:px-6 lg:py-3"
>
Join
Attend
</th>
<th
scope="col"
className="whitespace-nowrap px-3 py-2 text-xs font-medium lg:px-6 lg:py-3"
>
Attend
Fully Attended
</th>
</tr>
</thead>
Expand Down Expand Up @@ -340,25 +340,19 @@ function ParticipantItem({
{participant.email}
</td>
<td className="min-w-52 max-w-80 truncate whitespace-nowrap p-3 text-zinc-400 lg:p-6">
<BooleanItem isTrue={participant.isRegistered}></BooleanItem>
{participant.isRegistered ? (
<BooleanItem isTrue={participant.isRegistered}></BooleanItem>
) : (
<GuestIcon />
)}
</td>
<td className="min-w-52 max-w-80 truncate whitespace-nowrap p-3 text-zinc-400 lg:p-6">
<BooleanItem isTrue={participant.isJoined}></BooleanItem>
</td>
<td className="min-w-52 max-w-80 truncate whitespace-nowrap p-3 text-zinc-400 lg:p-6">
{participant.isRegistered ? (
BooleanItem({ isTrue: participant.isAttended || false })
) : (
<Tooltip content="Attendance for Guests Coming Soon">
<Button
isIconOnly
size="sm"
className="bg-zinc-900 ring-1 ring-zinc-800 hover:bg-zinc-900"
>
<InfoIcon className="stroke-zinc-300" />
</Button>
</Tooltip>
)}
{participant.isRegistered
? BooleanItem({ isTrue: participant.isAttended || false })
: null}
</td>
</tr>
);
Expand All @@ -373,3 +367,11 @@ function BooleanItem({ isTrue }: { isTrue: boolean }) {
</div>
);
}

function GuestIcon() {
return (
<div className="flex h-fit w-fit gap-2 rounded-md px-2 py-1 ring-1 ring-zinc-800">
Guest
</div>
);
}
Loading

0 comments on commit 433c593

Please sign in to comment.