diff --git a/.firebaserc b/.firebaserc index 008864b..7a602e8 100644 --- a/.firebaserc +++ b/.firebaserc @@ -1,6 +1,7 @@ { "projects": { "debug": "fitlien-dev", - "release": "fitlien" + "release": "fitlien", + "default": "fitlien-dev" } } diff --git a/firebase.json b/firebase.json index da6f4cb..301c4ec 100644 --- a/firebase.json +++ b/firebase.json @@ -1,5 +1,6 @@ { "firestore": { + "enabled": false, "rules": "firestore.rules", "indexes": "firestore.indexes.json" }, @@ -14,8 +15,7 @@ "firebase-debug.*.log", "*.local" ], - "predeploy": [ - ] + "predeploy": [] } ], "storage": { @@ -23,5 +23,20 @@ }, "remoteconfig": { "template": "remoteconfig.template.json" + }, + "emulators": { + "functions": { + "port": 5001 + }, + "firestore": { + "port": 8080 + }, + "storage": { + "port": 9199 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true } } diff --git a/functions/.runtimeconfig.json b/functions/.runtimeconfig.json new file mode 100644 index 0000000..d87d508 --- /dev/null +++ b/functions/.runtimeconfig.json @@ -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" + } +} diff --git a/functions/package-lock.json b/functions/package-lock.json index a2c4591..2cd1512 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -8,6 +8,7 @@ "name": "functions", "version": "0.0.0", "dependencies": { + "dotenv": "^16.4.7", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", @@ -2341,6 +2342,17 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/functions/package.json b/functions/package.json index d7c10c4..041fb56 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,6 +15,7 @@ }, "main": "lib/index.js", "dependencies": { + "dotenv": "^16.4.7", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", diff --git a/functions/src/index.ts b/functions/src/index.ts index 6945fb9..fa69917 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -5,17 +5,23 @@ import { Message } from "firebase-admin/messaging"; import * as express from "express"; import * as logger from "firebase-functions/logger"; 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 Mailgun = require('mailgun.js'); const { convert } = require('html-to-text'); const twilio = require('twilio') + export const sendEmailMessage = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response: express.Response) => { const mailgun = new Mailgun(formData); - const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); + const mailGunClient = mailgun.client({ username: 'api', key: functions.config().MAILGUN_API_KEY }); const toAddress = request.body.toAddress; const subject = request.body.subject; @@ -25,8 +31,8 @@ export const sendEmailMessage = onRequest({ }; const textMessage = convert(message, options); - mailGunClient.messages.create(process.env.MAILGUN_SERVER, { - from: process.env.MAILGUN_FROM_ADDRESS, + mailGunClient.messages.create(functions.config().MAILGUN_SERVER, { + from: functions.config().MAILGUN_FROM_ADDRESS, to: toAddress, subject: subject, text: textMessage, @@ -43,12 +49,12 @@ export const sendEmailMessage = onRequest({ export const sendSMSMessage = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response: express.Response) => { - const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); + const client = twilio(functions.config().TWILIO_ACCOUNT_SID, functions.config().TWILIO_AUTH_TOKEN); const { to, body } = request.body; client.messages .create({ body: body, - from: process.env.TWILIO_PHONE_NUMBER, + from: functions.config().TWILIO_PHONE_NUMBER, to: to }) .then((message: any) => { @@ -144,29 +150,35 @@ export const notifyTrainerUpdate = onRequest( }, async (request: Request, response: express.Response) => { try { - const { trainerId, section, trainerName, changedFields} = request.body; + const { trainerId, originalProfile,section,changedFields} = request.body; - if (!trainerId || !section || !trainerName || !changedFields) { + 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 = ''; - changesTable += ''; + changesTable += ''; if (changedFields && typeof changedFields === 'object') { - for (const [field, value] of Object.entries(changedFields)) { + for (const [field, newValue] of Object.entries(changedFields)) { changesTable += ''; - changesTable += ``; + changesTable += ``; - // Handle different types of values (strings, arrays, objects) - if (Array.isArray(value)) { - changesTable += ``; - } else if (typeof value === 'object' && value !== null) { - changesTable += ``; - } else { - changesTable += ``; - } + const oldValue = field.includes('.') + ? field.split('.').reduce((obj, key) => obj && obj[key], trainerData) + : trainerData[field]; + + const formattedOldValue = formatValueForEmail(oldValue); + const formattedNewValue = formatValueForEmail(newValue); + + changesTable += ``; + changesTable += ``; changesTable += ''; } @@ -174,6 +186,21 @@ export const notifyTrainerUpdate = onRequest( changesTable += '
FieldNew Value
FieldOld ValueNew Value
${field}${formatFieldName(field)}${value.join(', ')}${JSON.stringify(value)}${value}${formattedOldValue}${formattedNewValue}
'; + const emailContent = ` +

Trainer Profile Update

+

Your trainer ${trainerName} has updated their ${section}.

+

Changes Made:

+ ${changesTable} + `; + + const mailgun = new Mailgun(formData); + const mailGunClient = mailgun.client({ username: 'api', key: functions.config().mailgun.api_key}); + + const options = { wordwrap: 130 }; + const textMessage = convert(emailContent, options); + + + const memberships = await admin .firestore() .collection('memberships') @@ -219,56 +246,45 @@ export const notifyTrainerUpdate = onRequest( const clientEmail = clientData.email; if (fcmToken) { - const message : Message ={ - 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, - }; - + // const message : Message ={ + // 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, + // }; + const message = generateTrainerUpdateMessage(trainerId,section,trainerName,fcmToken); notificationPromises.push(admin.messaging().send(message)); } if (clientEmail) { - const emailContent = ` -

Trainer Profile Update

-

Your trainer ${trainerName} has updated their ${section}.

-

Changes Made:

- ${changesTable} - `; + + // const emailPromise = mailGunClient.messages.create(functions.config().mailgun.server, { + // from: functions.config().mailgun.from_address, + // to: clientEmail, + // subject: `Your trainer ${trainerName} has updated their profile`, + // text: textMessage, + // html: emailContent + // }); + const emailPromise = generateTrainerUpdateMail(mailGunClient,textMessage,emailContent,trainerName,clientEmail); - 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 emailPromise = 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 - }); - emailPromises.push(emailPromise); } } @@ -293,55 +309,46 @@ export const notifyTrainerUpdate = onRequest( const gymEmail = gymData.email; if (fcmToken) { - const message :Message={ - 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, - }; + // const message :Message={ + // 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, + // }; + + const message = generateTrainerUpdateMessage(trainerId,section,trainerName,fcmToken); notificationPromises.push(admin.messaging().send(message)); } if (gymEmail) { - const emailContent = ` -

Trainer Profile Update

-

Your trainer ${trainerName} has updated their ${section}.

-

Changes Made:

- ${changesTable} - `; + // const emailPromise = mailGunClient.messages.create(functions.config().mailgun.server, { + // from: functions.config().mailgun.from_address, + // to: gymEmail, + // subject: `Your trainer ${trainerName} has updated their profile`, + // text: textMessage, + // html: emailContent + // }); - 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 emailPromise = mailGunClient.messages.create(process.env.MAILGUN_SERVER, { - from: process.env.MAILGUN_FROM_ADDRESS, - to: gymEmail, - subject: `Your trainer ${trainerName} has updated their profile`, - text: textMessage, - html: emailContent - }); + const emailPromise = generateTrainerUpdateMail(mailGunClient,textMessage,emailContent,trainerName,gymEmail); emailPromises.push(emailPromise); } @@ -361,3 +368,135 @@ export const notifyTrainerUpdate = onRequest( } } ); + +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 = ''; + + if (value[0].hasOwnProperty('title') && value[0].hasOwnProperty('year')) { + table += ''; + value.forEach((item: any) => { + table += ``; + }); + } else if (value[0].hasOwnProperty('name') && value[0].hasOwnProperty('year')) { + table += ''; + value.forEach((item: any) => { + table += ``; + }); + } else if (value[0].hasOwnProperty('organization') && value[0].hasOwnProperty('role')) { + table += ''; + value.forEach((item: any) => { + table += ``; + }); + } else { + const keys = Object.keys(value[0]); + table += '' + keys.map(k => ``).join('') + ''; + value.forEach((item: any) => { + table += '' + keys.map(k => ``).join('') + ''; + }); + } + + table += '
TitleYear
${item.title}${item.year}
NameYearValidityCountry
${item.name}${item.year}${item.validityYear}${item.country}
OrganizationRoleDuration
${item.organization}${item.role}${item.duration}
${formatFieldName(k)}
${formatValueForEmail(item[k])}
'; + 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 = ''; + scheduleTable += ''; + + 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 += ``; + } + }); + + scheduleTable += '
DayTime Slots
${day}${timeSlots}
'; + return scheduleTable; + } + + try { + return JSON.stringify(value, null, 2) + .replace(/[{}"]/g, '') + .replace(/,\n/g, '
') + .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 { + return mailGunClient.messages.create(functions.config().mailgun.server, { + from: functions.config().mailgun.from_address, + to: clientEmail, + subject: `Your trainer ${trainerName} has updated their profile`, + text: textMessage, + html: emailContent + }); + +} \ No newline at end of file