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