feature/fitlien-616 #2

Closed
Ghost wants to merge 5 commits from feature/fitlien-616 into dev
8 changed files with 2945 additions and 5 deletions

View File

@ -1,6 +1,7 @@
{ {
"projects": { "projects": {
"debug": "fitlien-dev", "debug": "fitlien-dev",
"release": "fitlien" "release": "fitlien",
"default": "fitlien-dev"
} }
} }

View File

@ -1,5 +1,6 @@
{ {
"firestore": { "firestore": {
"enabled": false,
"rules": "firestore.rules", "rules": "firestore.rules",
"indexes": "firestore.indexes.json" "indexes": "firestore.indexes.json"
}, },
@ -14,8 +15,7 @@
"firebase-debug.*.log", "firebase-debug.*.log",
"*.local" "*.local"
], ],
"predeploy": [ "predeploy": []
]
} }
], ],
"storage": { "storage": {
@ -23,5 +23,20 @@
}, },
"remoteconfig": { "remoteconfig": {
"template": "remoteconfig.template.json" "template": "remoteconfig.template.json"
},
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"storage": {
"port": 9199
},
"ui": {
"enabled": true
},
"singleProjectMode": true
} }
} }

View File

@ -0,0 +1,12 @@
{
"mailgun": {
"api_key": "a4540f0b68d40922a8ee203a44ec880c-623424ea-7d804677",
"server": "fitlien.cosqnet.com",
"from_address": "postmaster@fitlien.cosqnet.com"
},
"twilio": {
"account_sid": "AC5cfaae728ba68fb1aa6756d973b6e32b",
"auth_token": "886ed704c7918078f361f5f88b42ffc0",
"phone_number": "+12315005309"
}
}

View File

@ -8,6 +8,7 @@
"name": "functions", "name": "functions",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"dotenv": "^16.4.7",
"firebase-admin": "^12.6.0", "firebase-admin": "^12.6.0",
"firebase-functions": "^6.0.1", "firebase-functions": "^6.0.1",
"form-data": "^4.0.1", "form-data": "^4.0.1",
@ -2341,6 +2342,17 @@
"url": "https://github.com/fb55/domutils?sponsor=1" "url": "https://github.com/fb55/domutils?sponsor=1"
} }
}, },
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@ -15,6 +15,7 @@
}, },
"main": "lib/index.js", "main": "lib/index.js",
"dependencies": { "dependencies": {
"dotenv": "^16.4.7",
"firebase-admin": "^12.6.0", "firebase-admin": "^12.6.0",
"firebase-functions": "^6.0.1", "firebase-functions": "^6.0.1",
"form-data": "^4.0.1", "form-data": "^4.0.1",

View File

@ -1,15 +1,22 @@
import { onRequest } from "firebase-functions/v2/https"; import { onRequest } from "firebase-functions/v2/https";
import { Request } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https";
import * as admin from 'firebase-admin'; import * as admin from 'firebase-admin';
import { Message } from "firebase-admin/messaging";
import * as express from "express"; import * as express from "express";
import * as logger from "firebase-functions/logger"; import * as logger from "firebase-functions/logger";
import { onDocumentCreated } from "firebase-functions/firestore"; import { onDocumentCreated } from "firebase-functions/firestore";
// import * as dotenv from 'dotenv';
import * as functions from "firebase-functions";
// dotenv.config();
admin.initializeApp();
const formData = require('form-data'); const formData = require('form-data');
const Mailgun = require('mailgun.js'); const Mailgun = require('mailgun.js');
const { convert } = require('html-to-text'); const { convert } = require('html-to-text');
const twilio = require('twilio') const twilio = require('twilio')
export const sendEmailMessage = onRequest({ export const sendEmailMessage = onRequest({
region: '#{SERVICES_RGN}#' region: '#{SERVICES_RGN}#'
}, (request: Request, response: express.Response) => { }, (request: Request, response: express.Response) => {
@ -103,7 +110,7 @@ export const notifyInvitation = onDocumentCreated({
return null; return null;
} }
const message: admin.messaging.Message = { const message: Message = {
notification: { notification: {
title: 'New Gym Invitation', title: 'New Gym Invitation',
body: `${invitation.invitedByName} has invited you to join ${invitation.gymName}`, body: `${invitation.invitedByName} has invited you to join ${invitation.gymName}`,
@ -135,4 +142,299 @@ export const notifyInvitation = onDocumentCreated({
console.error('Error sending invitation notification:', error); console.error('Error sending invitation notification:', error);
return null; return null;
} }
}); });
export const notifyTrainerUpdate = onRequest(
{
region: '#{SERVICES_RGN}#'
},
async (request: Request, response: express.Response) => {
try {
const { trainerId, originalProfile,section,changedFields} = request.body;
if (!trainerId || !originalProfile || !section || !changedFields) {
response.status(400).json({ success: false, error: 'Missing required parameters' });
return;
}
const trainerData = originalProfile;
const trainerName=trainerData["normalizedName"]
let changesTable = '<table border="1" cellpadding="5" style="border-collapse: collapse;">';
changesTable += '<tr><th>Field</th><th>Old Value</th><th>New Value</th></tr>';
if (changedFields && typeof changedFields === 'object') {
for (const [field, newValue] of Object.entries(changedFields)) {
changesTable += '<tr>';
changesTable += `<td>${formatFieldName(field)}</td>`;
const oldValue = field.includes('.')
? field.split('.').reduce((obj, key) => obj && obj[key], trainerData)
: trainerData[field];
const formattedOldValue = formatValueForEmail(oldValue);
const formattedNewValue = formatValueForEmail(newValue);
changesTable += `<td>${formattedOldValue}</td>`;
changesTable += `<td>${formattedNewValue}</td>`;
changesTable += '</tr>';
}
}
changesTable += '</table>';
const emailContent = `
<h2>Trainer Profile Update</h2>
<p>Your trainer ${trainerName} has updated their ${section}.</p>
<h3>Changes Made:</h3>
${changesTable}
`;
const mailgun = new Mailgun(formData);
const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY});
const options = { wordwrap: 130 };
const textMessage = convert(emailContent, options);
const memberships = await admin
.firestore()
.collection('memberships')
.where('trainerId', '==', trainerId)
.get();
const userIds = memberships.docs.map((doc) => doc.data().userId);
const acceptedGyms = await admin
.firestore()
.collection('traineracceptedgyms')
.where('trainerProfileId', '==', trainerId)
.get();
const gymIds = acceptedGyms.docs.map((doc) => doc.data().gymId);
const notificationPromises = [];
const emailPromises = [];
for (const userId of userIds) {
const clientQuerySnapshot = await admin
.firestore()
.collection('client_profiles')
.where('uid', '==',userId)
.limit(1)
.get();
if (clientQuerySnapshot.empty) {
console.log(`No client found with uid: ${userId}`);
continue;
}
const clientDoc = clientQuerySnapshot.docs[0];
const clientData = clientDoc.data();
const fcmToken = clientData.fcmToken;
const clientEmail = clientData.email;
if (fcmToken) {
const message = generateTrainerUpdateMessage(trainerId,section,trainerName,fcmToken);
notificationPromises.push(admin.messaging().send(message));
}
if (clientEmail) {
const emailPromise = generateTrainerUpdateMail(mailGunClient,textMessage,emailContent,trainerName,clientEmail);
emailPromises.push(emailPromise);
}
}
for (const gymId of gymIds) {
const gymQuerySnapshot = await admin
.firestore()
.collection('gyms')
.where('id', '==',gymId)
.limit(1)
.get();
if (gymQuerySnapshot.empty) {
console.log(`No gym found with id: ${gymId}`);
continue;
}
const gymDoc = gymQuerySnapshot.docs[0];
const gymData = gymDoc.data();
const fcmToken = gymData.fcmToken;
const gymEmail = gymData.email;
if (fcmToken) {
const message = generateTrainerUpdateMessage(trainerId,section,trainerName,fcmToken);
notificationPromises.push(admin.messaging().send(message));
}
if (gymEmail) {
const emailPromise = generateTrainerUpdateMail(mailGunClient,textMessage,emailContent,trainerName,gymEmail);
emailPromises.push(emailPromise);
}
}
await Promise.all([...notificationPromises, ...emailPromises]);
logger.info(`Notifications sent to clients of trainer: ${trainerId}`);
response.json({
success: true,
message: `Notifications sent to ${notificationPromises.length} clients, Emails sent to ${emailPromises.length} clients`
});
} catch (error) {
logger.error('Error sending trainer update notifications:', error);
response.status(500).json({ success: false, error: "failed to send notifications" });
}
}
);
function formatFieldName(field: string): string {
const formatted = field.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
return formatted
.replace('Normalized ', '')
.replace('Work Experiences', 'Work Experience');
}
function formatValueForEmail(value: any): string {
if (value === undefined || value === null) {
return 'N/A';
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return value.toString();
}
if (value instanceof Date) {
return value.toLocaleDateString();
}
if (Array.isArray(value)) {
if (value.length > 0 && typeof value[0] === 'object') {
let table = '<table border="1" cellpadding="3" style="border-collapse: collapse; font-size: 0.9em;">';
if (value[0].hasOwnProperty('title') && value[0].hasOwnProperty('year')) {
table += '<tr><th>Title</th><th>Year</th></tr>';
value.forEach((item: any) => {
table += `<tr><td>${item.title}</td><td>${item.year}</td></tr>`;
});
} else if (value[0].hasOwnProperty('name') && value[0].hasOwnProperty('year')) {
table += '<tr><th>Name</th><th>Year</th><th>Validity</th><th>Country</th></tr>';
value.forEach((item: any) => {
table += `<tr><td>${item.name}</td><td>${item.year}</td><td>${item.validityYear}</td><td>${item.country}</td></tr>`;
});
} else if (value[0].hasOwnProperty('organization') && value[0].hasOwnProperty('role')) {
table += '<tr><th>Organization</th><th>Role</th><th>Duration</th></tr>';
value.forEach((item: any) => {
table += `<tr><td>${item.organization}</td><td>${item.role}</td><td>${item.duration}</td></tr>`;
});
} else {
const keys = Object.keys(value[0]);
table += '<tr>' + keys.map(k => `<th>${formatFieldName(k)}</th>`).join('') + '</tr>';
value.forEach((item: any) => {
table += '<tr>' + keys.map(k => `<td>${formatValueForEmail(item[k])}</td>`).join('') + '</tr>';
});
}
table += '</table>';
return table;
} else {
return value.join(', ');
}
}
if (typeof value === 'object') {
if (Object.keys(value).some(key => ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'].includes(key))) {
let scheduleTable = '<table border="1" cellpadding="3" style="border-collapse: collapse; font-size: 0.9em;">';
scheduleTable += '<tr><th>Day</th><th>Time Slots</th></tr>';
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
days.forEach(day => {
if (value[day] && Array.isArray(value[day])) {
const timeSlots = value[day].map((slot: string) => {
return slot.replace('TimeSlot.', '').replace(/^\w/, c => c.toUpperCase());
}).join(', ');
scheduleTable += `<tr><td>${day}</td><td>${timeSlots}</td></tr>`;
}
});
scheduleTable += '</table>';
return scheduleTable;
}
try {
return JSON.stringify(value, null, 2)
.replace(/[{}"]/g, '')
.replace(/,\n/g, '<br>')
.replace(/:/g, ': ');
} catch (e) {
return '[Complex Object]';
}
}
return String(value);
}
function generateTrainerUpdateMessage(trainerId: string, section: string, trainerName: string, fcmToken: string) : Message {
return {
notification: {
title: 'Trainer Profile Update',
body: `${trainerName} has updated their ${section}`,
},
data: {
type: 'trainer_profile_update',
trainerId: trainerId,
section: section,
trainerName: trainerName,
},
android: {
priority: 'high',
notification: {
channelId: 'trainer_updates_channel',
priority: 'high',
defaultSound: true,
defaultVibrateTimings: true,
icon: '@mipmap/ic_launcher',
clickAction: 'FLUTTER_NOTIFICATION_CLICK',
},
},
token: fcmToken,
};
}
function generateTrainerUpdateMail(mailGunClient:any,textMessage:any,emailContent:string,trainerName: string, clientEmail: string) : Promise<any> {
return mailGunClient.messages.create(process.env.MAILGUN_SERVER, {
from: process.env.MAILGUN_FROM_ADDRESS,
to: clientEmail,
subject: `Your trainer ${trainerName} has updated their profile`,
text: textMessage,
html: emailContent
});
}

2592
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"firebase-functions": "^6.3.2"
}
}