Added Filtering logic

This commit is contained in:
Sharon Dcruz 2025-08-25 12:36:59 +05:30
parent b61003e185
commit 6f2d12c802

View File

@ -0,0 +1,453 @@
import { onCall } from "firebase-functions/v2/https";
import { getLogger, getAdmin } from "../shared/config";
const app = getAdmin();
const logger = getLogger();
interface FilterMembersRequest {
gymId: string;
filter: string;
sortBy: string;
sortAscending: boolean;
searchQuery?: string;
limit?: number;
lastDocumentId?: string;
role?: string;
trainerAssignedMembershipIds?: string[];
}
interface MembershipWithDetails {
membershipId: string;
memberData: any;
fields: { [key: string]: string };
renewalDate?: string;
daysUntilExpiry?: number;
hasPersonalTraining: boolean;
trainerAssignments?: any[];
timeSlots?: any[];
}
interface FilterMembersResponse {
members: MembershipWithDetails[];
hasMore: boolean;
lastDocumentId?: string;
totalCount: number;
}
export const filterMembers = onCall(
{
region: "#{SERVICES_RGN}#",
cors: true,
},
async (request): Promise<FilterMembersResponse> => {
try {
const {
gymId,
filter,
sortBy,
sortAscending,
searchQuery,
limit = 20,
lastDocumentId,
role,
trainerAssignedMembershipIds,
} = request.data as FilterMembersRequest;
logger.info(`Filtering members for gym ${gymId} with filter: ${filter}`);
if (!gymId) {
throw new Error("gymId is required");
}
let query = app
.firestore()
.collection("memberships")
.where("gymId", "==", gymId);
if (filter === "Active") {
query = query.where("status", "==", "ACTIVE");
} else if (filter === "Pending") {
query = query.where("status", "==", "PENDING");
} else if (filter === "Expired") {
query = query.where("status", "==", "EXPIRED");
}
let orderByField = "createdAt";
let orderByDirection: "asc" | "desc" = sortAscending ? "asc" : "desc";
switch (sortBy) {
case "Name":
orderByField = "createdAt";
break;
case "Expiry Date":
orderByField = "expirationDate";
break;
case "Join Date":
orderByField = "createdAt";
break;
default:
orderByField = "createdAt";
}
query = query.orderBy(orderByField, orderByDirection);
if (lastDocumentId) {
const lastDoc = await app
.firestore()
.collection("memberships")
.doc(lastDocumentId)
.get();
if (lastDoc.exists) {
query = query.startAfter(lastDoc);
}
}
const fetchLimit = ["All", "Active", "Pending", "Expired"].includes(
filter
)
? limit
: limit * 3;
query = query.limit(fetchLimit);
const snapshot = await query.get();
const allMembers: MembershipWithDetails[] = [];
const batchSize = 10;
const docs = snapshot.docs;
for (let i = 0; i < docs.length; i += batchSize) {
const batch = docs.slice(i, i + batchSize);
const batchResults = await Promise.allSettled(
batch.map(async (doc) => {
const memberData = doc.data();
const membershipId = doc.id;
const fields = await getClientFieldsByMembershipId(membershipId);
let renewalDate: Date | undefined;
let daysUntilExpiry: number | undefined;
if (memberData.subscription) {
const payments = await getPaymentsForMembership(membershipId);
if (payments.length > 0) {
const latestPayment = payments[0];
renewalDate = calculateRenewalDateFromPayment(
memberData.subscription,
latestPayment.dateTimestamp
);
if (memberData.status === "ACTIVE") {
const now = new Date();
const timeDiff = renewalDate.getTime() - now.getTime();
daysUntilExpiry = Math.max(
0,
Math.floor(timeDiff / (1000 * 3600 * 24))
);
}
}
}
const hasPersonalTraining =
memberData.subscription?.hasPersonalTraining === true;
let trainerAssignments: any[] = [];
let timeSlots: any[] = [];
if (hasPersonalTraining) {
trainerAssignments = await getTrainerAssignmentsForMembership(
membershipId
);
if (trainerAssignments.length > 0) {
const latestAssignment = trainerAssignments[0];
timeSlots = latestAssignment.timeSlot || [];
}
}
return {
membershipId,
memberData,
fields,
renewalDate: renewalDate?.toISOString(),
daysUntilExpiry,
hasPersonalTraining,
trainerAssignments,
timeSlots,
};
})
);
batchResults.forEach((result) => {
if (result.status === "fulfilled" && result.value) {
allMembers.push(result.value);
}
});
}
let filteredMembers = allMembers;
if (role === "Trainer" && trainerAssignedMembershipIds) {
filteredMembers = allMembers.filter((member) =>
trainerAssignedMembershipIds.includes(member.membershipId)
);
}
if (searchQuery && searchQuery.length >= 2) {
const searchLower = searchQuery.toLowerCase();
filteredMembers = filteredMembers.filter((member) => {
const firstName = member.fields["first-name"]?.toLowerCase() || "";
const lastName = member.fields["last-name"]?.toLowerCase() || "";
const phone = member.fields["phone-number"]?.toLowerCase() || "";
const email = member.fields["email"]?.toLowerCase() || "";
return (
firstName.includes(searchLower) ||
lastName.includes(searchLower) ||
phone.includes(searchLower) ||
email.includes(searchLower)
);
});
}
if (
filter !== "All" &&
filter !== "Active" &&
filter !== "Pending" &&
filter !== "Expired"
) {
filteredMembers = filteredMembers.filter((member) => {
switch (filter) {
case "Personal Training":
return member.hasPersonalTraining;
case "Expiring in 10 Days":
return (
member.memberData.status === "ACTIVE" &&
member.daysUntilExpiry != null &&
member.daysUntilExpiry > 0 &&
member.daysUntilExpiry <= 10
);
case "Expiring in 30 Days":
return (
member.memberData.status === "ACTIVE" &&
member.daysUntilExpiry != null &&
member.daysUntilExpiry > 10 &&
member.daysUntilExpiry <= 30
);
case "Expired in 30 days":
return applyExpiredDateFilter(member.memberData, 30);
case "Expired in 60 days":
return applyExpiredDateFilter(member.memberData, 60);
default:
return true;
}
});
}
if (sortBy === "Name") {
filteredMembers.sort((a, b) => {
const aName = `${a.fields["first-name"] || ""} ${
a.fields["last-name"] || ""
}`.trim();
const bName = `${b.fields["first-name"] || ""} ${
b.fields["last-name"] || ""
}`.trim();
const comparison = aName.localeCompare(bName);
return sortAscending ? comparison : -comparison;
});
} else if (sortBy === "Expiry Date") {
filteredMembers.sort((a, b) => {
const aDate = a.renewalDate ? new Date(a.renewalDate) : new Date(0);
const bDate = b.renewalDate ? new Date(b.renewalDate) : new Date(0);
const comparison = aDate.getTime() - bDate.getTime();
return sortAscending ? comparison : -comparison;
});
}
const startIndex = 0;
const endIndex = Math.min(startIndex + limit, filteredMembers.length);
const paginatedMembers = filteredMembers.slice(startIndex, endIndex);
const hasMore = endIndex < filteredMembers.length;
const lastMember =
paginatedMembers.length > 0
? paginatedMembers[paginatedMembers.length - 1]
: null;
logger.info(
`Returning ${paginatedMembers.length} members out of ${filteredMembers.length} filtered`
);
return {
members: paginatedMembers,
hasMore,
lastDocumentId: lastMember?.membershipId,
totalCount: filteredMembers.length,
};
} catch (error) {
logger.error("Error filtering members:", error);
throw new Error(
`Failed to filter members: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
);
async function getClientFieldsByMembershipId(
membershipId: string
): Promise<{ [key: string]: string }> {
try {
const membershipDoc = await app
.firestore()
.collection("memberships")
.doc(membershipId)
.get();
if (!membershipDoc.exists) {
return {};
}
const membershipData = membershipDoc.data();
const userId = membershipData?.userId;
if (!userId) {
return {};
}
const clientDoc = await app
.firestore()
.collection("client_profiles")
.doc(userId)
.get();
if (!clientDoc.exists) {
return {};
}
return clientDoc.data()?.fields || {};
} catch (error) {
logger.error(
`Error getting client fields for membership ${membershipId}:`,
error
);
return {};
}
}
async function getPaymentsForMembership(membershipId: string): Promise<any[]> {
try {
const docSnapshot = await app
.firestore()
.collection("membership_payments")
.doc(membershipId)
.get();
if (!docSnapshot.exists) {
return [];
}
const data = docSnapshot.data();
const paymentsData = data?.payments || [];
const payments = paymentsData.map((payment: any) => ({
...payment,
dateTimestamp: payment.dateTimestamp.toDate
? payment.dateTimestamp.toDate()
: new Date(payment.dateTimestamp),
createdAt: payment.createdAt.toDate
? payment.createdAt.toDate()
: new Date(payment.createdAt),
}));
payments.sort(
(a: any, b: any) => b.createdAt.getTime() - a.createdAt.getTime()
);
return payments;
} catch (error) {
logger.error(
`Error getting payments for membership ${membershipId}:`,
error
);
return [];
}
}
function calculateRenewalDateFromPayment(
subscription: any,
paymentDate: Date
): Date {
const renewalDate = new Date(paymentDate);
const frequency = subscription.frequency || "Monthly";
switch (frequency.toLowerCase()) {
case "monthly":
renewalDate.setMonth(renewalDate.getMonth() + 1);
break;
case "quarterly":
renewalDate.setMonth(renewalDate.getMonth() + 3);
break;
case "half-yearly":
renewalDate.setMonth(renewalDate.getMonth() + 6);
break;
case "yearly":
renewalDate.setFullYear(renewalDate.getFullYear() + 1);
break;
default:
renewalDate.setMonth(renewalDate.getMonth() + 1);
}
return renewalDate;
}
async function getTrainerAssignmentsForMembership(
membershipId: string
): Promise<any[]> {
try {
const querySnapshot = await app
.firestore()
.collection("personal_trainer_assignments")
.where("membershipId", "==", membershipId)
.get();
return querySnapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
} catch (error) {
logger.error(
`Error getting trainer assignments for membership ${membershipId}:`,
error
);
return [];
}
}
function applyExpiredDateFilter(memberData: any, days: number): boolean {
const status = memberData.status;
const expirationDate = memberData.expirationDate;
if (status !== "EXPIRED" || !expirationDate) {
return false;
}
try {
const expiredDate = expirationDate.toDate
? expirationDate.toDate()
: new Date(expirationDate);
const now = new Date();
const targetDaysAgo = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
return (
expiredDate.getTime() < now.getTime() &&
expiredDate.getTime() > targetDaysAgo.getTime()
);
} catch (error) {
logger.error("Error parsing expiration date:", error);
return false;
}
}