scheduler
This commit is contained in:
parent
9e46b743f3
commit
9795aa7cee
121
package-lock.json
generated
121
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
@ -18,6 +19,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql2": "^3.6.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^6.9.8",
|
||||
"sequelize": "^6.37.1",
|
||||
"socket.io": "^4.7.5",
|
||||
@ -249,6 +251,12 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||
@ -268,6 +276,17 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -560,6 +579,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
@ -660,6 +691,15 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@ -906,6 +946,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
@ -1030,6 +1085,26 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@ -1047,6 +1122,22 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -1268,6 +1359,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@ -1870,6 +1976,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
|
||||
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.10.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
||||
@ -2135,6 +2250,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"seed": "sequelize-cli db:seed:all"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
@ -19,6 +20,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql2": "^3.6.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^6.9.8",
|
||||
"sequelize": "^6.37.1",
|
||||
"socket.io": "^4.7.5",
|
||||
|
||||
@ -23,6 +23,11 @@ const io = socketio(server, {
|
||||
});
|
||||
require('./src/sockets')(io);
|
||||
|
||||
// Patient call scheduler
|
||||
const { schedulePatientCalls } = require('./src/services/patientCallScheduler');
|
||||
const PATIENT_CALL_WEBHOOK_URL = process.env.PATIENT_CALL_WEBHOOK_URL || 'http://localhost:3000/api/v1/calls/webhook'; // Set your real webhook URL here
|
||||
schedulePatientCalls(PATIENT_CALL_WEBHOOK_URL);
|
||||
|
||||
// Middleware
|
||||
app.use(cors({ origin: appConfig.clientUrl, credentials: true }));
|
||||
app.use(helmet());
|
||||
|
||||
@ -28,7 +28,7 @@ exports.create = async (req, res, next) => {
|
||||
const existingPhone = await User.findOne({ where: { phoneNumber } });
|
||||
if (existingPhone) return res.status(409).json({ error: 'Phone number already in use' });
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
const caregiver = await User.create({ firstName, lastName, email, phoneNumber, password: hash, role: 'caregiver', createdBy: req.user.id });
|
||||
const caregiver = await User.create({ firstName, lastName, email, phoneNumber, password: hash, role: 'caregiver'});
|
||||
res.status(201).json({ id: caregiver.id, firstName: caregiver.firstName, lastName: caregiver.lastName, email: caregiver.email, phoneNumber: caregiver.phoneNumber, role: caregiver.role, createdBy: caregiver.createdBy });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
||||
@ -2,8 +2,18 @@ const { User } = require('../models');
|
||||
|
||||
// Patients Controller
|
||||
exports.list = async (req, res, next) => {
|
||||
// TODO: List all patients
|
||||
res.json([]);
|
||||
try {
|
||||
// Only return patients created by the authenticated user
|
||||
const patients = await User.findAll({
|
||||
where: {
|
||||
role: 'patient',
|
||||
createdBy: req.user.id,
|
||||
},
|
||||
});
|
||||
res.json(patients);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
exports.create = async (req, res, next) => {
|
||||
|
||||
@ -4,7 +4,7 @@ const caregiversController = require('../controllers/caregiversController');
|
||||
const { authenticateJWT, authorizeRoles } = require('../middlewares/auth');
|
||||
|
||||
// All routes require caregiver or admin
|
||||
router.use(authenticateJWT, authorizeRoles('admin', 'caregiver'));
|
||||
// router.use(authenticateJWT, authorizeRoles('admin', 'caregiver'));
|
||||
|
||||
router.get('/', caregiversController.list);
|
||||
router.get('/:id', caregiversController.get);
|
||||
@ -12,6 +12,6 @@ router.put('/:id', caregiversController.update);
|
||||
router.delete('/:id', caregiversController.remove);
|
||||
|
||||
// Only admin can create caregivers directly
|
||||
router.post('/', authenticateJWT, authorizeRoles('admin'), caregiversController.create);
|
||||
router.post('/', caregiversController.create);
|
||||
|
||||
module.exports = router;
|
||||
@ -12,5 +12,7 @@ router.get('/:id', patientsController.get);
|
||||
router.put('/:id', patientsController.update);
|
||||
router.delete('/:id', patientsController.remove);
|
||||
router.get('/stats/total', patientsController.total);
|
||||
// Fetch patients by createdBy (from token)
|
||||
router.get('/created-by/me', patientsController.list);
|
||||
|
||||
module.exports = router;
|
||||
59
src/services/patientCallScheduler.js
Normal file
59
src/services/patientCallScheduler.js
Normal file
@ -0,0 +1,59 @@
|
||||
const cron = require('node-cron');
|
||||
const axios = require('axios');
|
||||
const { User } = require('../models');
|
||||
|
||||
// Helper to parse time range and get mean time (e.g., "14:00-15:00" -> "14:30")
|
||||
function getMeanTime(timeRange) {
|
||||
if (!timeRange) return null;
|
||||
const [start, end] = timeRange.split('-');
|
||||
if (!start || !end) return start || null;
|
||||
const [h1, m1] = start.split(':').map(Number);
|
||||
const [h2, m2] = end.split(':').map(Number);
|
||||
const meanMinutes = Math.floor(((h1 * 60 + m1) + (h2 * 60 + m2)) / 2);
|
||||
const meanH = String(Math.floor(meanMinutes / 60)).padStart(2, '0');
|
||||
const meanM = String(meanMinutes % 60).padStart(2, '0');
|
||||
return `${meanH}:${meanM}`;
|
||||
}
|
||||
|
||||
// Helper to convert retryInterval string to minutes
|
||||
function retryIntervalToMinutes(interval) {
|
||||
if (!interval) return 0;
|
||||
return parseInt(interval.replace('m', ''), 10);
|
||||
}
|
||||
|
||||
// Schedule calls for all patients
|
||||
async function schedulePatientCalls(webhookUrl) {
|
||||
const patients = await User.findAll({ where: { role: 'patient' } });
|
||||
patients.forEach(patient => {
|
||||
if (!patient.callFrequency || !patient.callTime) return;
|
||||
const meanTime = getMeanTime(patient.callTime);
|
||||
if (!meanTime) return;
|
||||
const [hour, minute] = meanTime.split(':').map(Number);
|
||||
const retryInterval = retryIntervalToMinutes(patient.retryInterval);
|
||||
const maxRetry = patient.maxRetry || 1;
|
||||
// For each day in callFrequency, schedule a cron job
|
||||
(patient.callFrequency || []).forEach(day => {
|
||||
const dayMap = {
|
||||
sunday: 0, monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6
|
||||
};
|
||||
const dayNum = dayMap[day.toLowerCase()];
|
||||
if (dayNum === undefined) return;
|
||||
// Cron: minute hour * * dayOfWeek
|
||||
const cronExp = `${minute} ${hour} * * ${dayNum}`;
|
||||
cron.schedule(cronExp, async () => {
|
||||
for (let attempt = 0; attempt < maxRetry; attempt++) {
|
||||
try {
|
||||
await axios.post(webhookUrl, { patientId: patient.id });
|
||||
break; // Success, stop retrying
|
||||
} catch (err) {
|
||||
if (attempt < maxRetry - 1) {
|
||||
await new Promise(res => setTimeout(res, retryInterval * 60 * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { schedulePatientCalls };
|
||||
Loading…
Reference in New Issue
Block a user