Updated
This commit is contained in:
parent
02a8c1187a
commit
371620d228
@ -13,7 +13,7 @@ export * from './shared/config';
|
||||
export { sendEmailSES } from './email';
|
||||
export { sendSMSMessage } from './sms';
|
||||
export { accessFile } from './storage';
|
||||
export { processNotificationOnCreate,checkExpiredMemberships,filterMembers } from './notifications';
|
||||
export { processNotificationOnCreate,checkExpiredMemberships } from './notifications';
|
||||
export * from './payments';
|
||||
export { getPlaceDetails, getPlacesAutocomplete } from './places';
|
||||
export { registerClient } from './users';
|
||||
|
||||
@ -1,453 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
export { processNotificationOnCreate } from './processNotification';
|
||||
export { checkExpiredMemberships } from "./membershipStatusNotifications";
|
||||
export { filterMembers } from "./filterMembersRequest";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user