Added Filtering logic
This commit is contained in:
parent
b61003e185
commit
6f2d12c802
453
functions/src/notifications/filterMembersRequest.ts
Normal file
453
functions/src/notifications/filterMembersRequest.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user