message template

This commit is contained in:
2026-02-11 14:59:06 -05:00
parent 921d300e31
commit 3aaf9fbac2
16 changed files with 344 additions and 20 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
app/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,94 @@
const { splitSite } = require("../middlewares");
const db = require("../models");
const MsgCustomTemplate = db.msg_custom_template;
// Create a new template
exports.create = (req, res) => {
if (!req.body.title) {
res.status(400).send({ message: "Title can not be empty!" });
return;
}
const site = splitSite.findSiteNumber(req);
const template = new MsgCustomTemplate({
title: req.body.title,
chinese: req.body.chinese || '',
english: req.body.english || '',
status: req.body.status || 'active',
create_by: req.body.create_by,
create_date: req.body.create_date || new Date(),
edit_by: req.body.edit_by,
edit_date: req.body.edit_date,
site
});
template.save(template)
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message: err.message || "Some error occurred while creating the template."
});
});
};
// Retrieve all templates
exports.getAll = (req, res) => {
var condition = {};
condition = splitSite.splitSiteGet(req, condition);
MsgCustomTemplate.find(condition)
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message: err.message || "Some error occurred while retrieving templates."
});
});
};
// Get one template by id
exports.getOne = (req, res) => {
const id = req.params.id;
MsgCustomTemplate.findById(id)
.then(data => {
if (!data)
res.status(404).send({ message: "Not found template with id " + id });
else res.send(data);
})
.catch(err => {
res.status(500).send({ message: "Error retrieving template with id=" + id });
});
};
// Update a template by id
exports.update = (req, res) => {
if (!req.body) {
return res.status(400).send({ message: "Data to update can not be empty!" });
}
const id = req.params.id;
MsgCustomTemplate.findByIdAndUpdate(id, req.body, { useFindAndModify: false })
.then(data => {
if (!data) {
res.status(404).send({ message: `Cannot update template with id=${id}.` });
} else res.send({ success: true, message: "Template was updated successfully." });
})
.catch(err => {
res.status(500).send({ success: false, message: "Error updating template with id=" + id });
});
};
// Delete a template by id
exports.remove = (req, res) => {
const id = req.params.id;
MsgCustomTemplate.findByIdAndRemove(id)
.then(data => {
if (!data) {
res.status(404).send({ message: `Cannot delete template with id=${id}.` });
} else {
res.send({ message: "Template was deleted successfully!" });
}
})
.catch(err => {
res.status(500).send({ message: "Could not delete template with id=" + id });
});
};

View File

@@ -35,4 +35,5 @@ db.attendance_note = require("./attendance-note.model")(mongoose);
db.carousel = require("./carousel.model")(mongoose);
db.fingerprint_attendance = require("./fingerprint-attendance.model")(mongoose);
db.dailyRoutesTemplate = require("./daily-routes-template.model")(mongoose);
db.msg_custom_template = require("./msg-custom-template.model")(mongoose);
module.exports = db;

View File

@@ -0,0 +1,23 @@
module.exports = mongoose => {
var schema = mongoose.Schema(
{
title: String,
chinese: String,
english: String,
status: String,
create_by: String,
create_date: Date,
edit_by: String,
edit_date: Date,
site: Number
},
{ collection: 'msg_custom_template', timestamps: true }
);
schema.method("toJSON", function() {
const { __v, _id, ...object } = this.toObject();
object.id = _id;
return object;
});
const MsgCustomTemplate = mongoose.model("msg_custom_template", schema);
return MsgCustomTemplate;
};

View File

@@ -0,0 +1,19 @@
const { authJwt } = require("../middlewares");
module.exports = app => {
const templates = require("../controllers/msg-custom-template.controller.js");
app.use((req, res, next) => {
res.header(
"Access-Control-Allow-Headers",
"x-access-token, Origin, Content-Type, Accept"
);
next();
});
var router = require("express").Router();
router.get("/", [authJwt.verifyToken], templates.getAll);
router.post("/", [authJwt.verifyToken], templates.create);
router.get("/:id", [authJwt.verifyToken], templates.getOne);
router.put("/:id", [authJwt.verifyToken], templates.update);
router.delete("/:id", [authJwt.verifyToken], templates.remove);
app.use('/api/msg-custom-templates', router);
};

View File

@@ -1,16 +1,16 @@
{
"files": {
"main.css": "/static/css/main.6616f3cb.css",
"main.js": "/static/js/main.60ebc889.js",
"main.js": "/static/js/main.8452576d.js",
"static/js/787.c4e7f8f9.chunk.js": "/static/js/787.c4e7f8f9.chunk.js",
"static/media/landing.png": "/static/media/landing.d4c6072db7a67dff6a78.png",
"index.html": "/index.html",
"main.6616f3cb.css.map": "/static/css/main.6616f3cb.css.map",
"main.60ebc889.js.map": "/static/js/main.60ebc889.js.map",
"main.8452576d.js.map": "/static/js/main.8452576d.js.map",
"787.c4e7f8f9.chunk.js.map": "/static/js/787.c4e7f8f9.chunk.js.map"
},
"entrypoints": [
"static/css/main.6616f3cb.css",
"static/js/main.60ebc889.js"
"static/js/main.8452576d.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"><link rel="manifest" href="/manifest.json"/><title>Worldshine Transportation</title><script defer="defer" src="/static/js/main.60ebc889.js"></script><link href="/static/css/main.6616f3cb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"><link rel="manifest" href="/manifest.json"/><title>Worldshine Transportation</title><script defer="defer" src="/static/js/main.8452576d.js"></script><link href="/static/css/main.6616f3cb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
client/.DS_Store vendored

Binary file not shown.

View File

@@ -1,14 +1,23 @@
import React, {useState, useEffect} from "react";
import { useNavigate } from "react-router-dom";
import { AuthService, MessageService } from "../../services";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
import { Columns, Download, Send, PencilSquare, PersonSquare, Plus } from "react-bootstrap-icons";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Modal, Button } from "react-bootstrap";
import { Columns, Download, Send, PencilSquare, PersonSquare, Plus, Trash } from "react-bootstrap-icons";
const MessageList = () => {
const navigate = useNavigate();
const [messages, setMessages] = useState([]);
const [activeTab, setActiveTab] = useState('allMessages');
const params = new URLSearchParams(window.location.search)
// Custom template state
const [customTemplates, setCustomTemplates] = useState([]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingTemplate, setEditingTemplate] = useState(null);
const [templateTitle, setTemplateTitle] = useState('');
const [templateChinese, setTemplateChinese] = useState('');
const [templateEnglish, setTemplateEnglish] = useState('');
useEffect(() => {
if (!AuthService.canAddOrEditRoutes() && !AuthService.canViewRoutes() &&!AuthService.canAccessLegacySystem()) {
window.alert('You haven\'t login yet OR this user does not have access to this page. Please change a dispatcher or admin account to login.')
@@ -16,12 +25,16 @@ const MessageList = () => {
navigate(`/login`);
}
MessageService.getMessages().then(data => {
console.log(data);
setMessages(data.data);
})
});
fetchCustomTemplates();
}, []);
const fetchCustomTemplates = () => {
MessageService.getCustomTemplates().then(data => {
setCustomTemplates(data?.data || []);
});
};
const redirectToAdmin = () => {
if (params.get('from') === 'medical') {
@@ -29,7 +42,6 @@ const MessageList = () => {
} else {
navigate(`/admin/customer-report`)
}
}
const goToSendMessage = () => {
@@ -44,6 +56,76 @@ const MessageList = () => {
navigate(`/messages/`)
}
// Custom template functions
const openCreateModal = () => {
setEditingTemplate(null);
setTemplateTitle('');
setTemplateChinese('');
setTemplateEnglish('');
setShowCreateModal(true);
};
const openEditModal = (template) => {
setEditingTemplate(template);
setTemplateTitle(template.title || '');
setTemplateChinese(template.chinese || '');
setTemplateEnglish(template.english || '');
setShowCreateModal(true);
};
const closeModal = () => {
setShowCreateModal(false);
setEditingTemplate(null);
setTemplateTitle('');
setTemplateChinese('');
setTemplateEnglish('');
};
const handleSaveTemplate = () => {
if (!templateTitle.trim()) {
window.alert('Template Title is required.');
return;
}
if (!templateChinese.trim()) {
window.alert('Message Content (Chinese) is required.');
return;
}
if (!templateEnglish.trim()) {
window.alert('Message Content (English) is required.');
return;
}
const userName = localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name;
const data = {
title: templateTitle.trim(),
chinese: templateChinese.trim(),
english: templateEnglish.trim()
};
if (editingTemplate) {
data.edit_by = userName;
data.edit_date = new Date();
MessageService.updateCustomTemplate(editingTemplate.id, data).then(() => {
fetchCustomTemplates();
closeModal();
});
} else {
data.create_by = userName;
data.create_date = new Date();
MessageService.createCustomTemplate(data).then(() => {
fetchCustomTemplates();
closeModal();
});
}
};
const handleDeleteTemplate = (id) => {
if (window.confirm('Are you sure you want to delete this template?')) {
MessageService.deleteCustomTemplate(id).then(() => {
fetchCustomTemplates();
});
}
};
return (
<>
@@ -61,7 +143,7 @@ const MessageList = () => {
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="allMessages" id="messages-tab">
<Tabs activeKey={activeTab} onSelect={(k) => setActiveTab(k)} id="messages-tab">
<Tab eventKey="allMessages" title="All Messages">
<table className="personnel-info-table">
<thead>
@@ -89,14 +171,97 @@ const MessageList = () => {
</tbody>
</table>
</Tab>
<Tab eventKey="messageTemplate" title="Message Template">
<table className="personnel-info-table">
<thead>
<tr>
<th className="th-index">No.</th>
<th>Template Title</th>
<th>Message Content (Chinese)</th>
<th>Message Content (English)</th>
<th style={{ width: '80px' }}>Actions</th>
</tr>
</thead>
<tbody>
{
customTemplates && customTemplates.map((template, index) => <tr key={template.id}>
<td className="td-index">{index + 1}</td>
<td>{template?.title}</td>
<td style={{ maxWidth: '300px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{template?.chinese}</td>
<td style={{ maxWidth: '300px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{template?.english}</td>
<td>
<PencilSquare size={16} className="clickable me-2" onClick={() => openEditModal(template)} />
<Trash size={16} className="clickable" color="#dc3545" onClick={() => handleDeleteTemplate(template.id)} />
</td>
</tr>)
}
{(!customTemplates || customTemplates.length === 0) && (
<tr><td colSpan="5" style={{ textAlign: 'center', color: '#999', padding: '24px' }}>No templates yet. Create one to get started.</td></tr>
)}
</tbody>
</table>
</Tab>
</Tabs>
<div className="list-func-panel">
{activeTab === 'allMessages' && (
<>
<button className="btn btn-primary me-2" onClick={() => goToCreate()}><Plus size={16}></Plus>Create New Message</button>
<button className="btn btn-primary me-2" onClick={() => goToSendMessage()}><Send size={16} className="me-2"></Send> Send Message</button>
{/* <button className="btn btn-primary"><Download size={16} className="me-2"></Download>Export</button> */}
</>
)}
{activeTab === 'messageTemplate' && (
<button className="btn btn-primary me-2" onClick={openCreateModal}><Plus size={16} className="me-2"></Plus>Create Message Template</button>
)}
</div>
</div>
</div>
{/* Create / Edit Message Template Modal */}
<Modal show={showCreateModal} onHide={closeModal} centered>
<Modal.Header closeButton>
<Modal.Title>{editingTemplate ? 'Edit Message Template' : 'Create New Message Template'}</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="mb-3">
<label className="form-label"><strong>Template Title</strong> <span className="text-danger">*</span></label>
<input
type="text"
className="form-control"
placeholder="e.g., Pick Up"
value={templateTitle}
onChange={(e) => setTemplateTitle(e.target.value)}
/>
</div>
<div className="mb-3">
<label className="form-label"><strong>Message Content (Chinese)</strong> <span className="text-danger">*</span></label>
<textarea
className="form-control"
rows={4}
placeholder="e.g., 尊敬的{Name}:您的今天乘坐的车辆已于{Time}出发,司机{Driver},车号{VehicleNum},请及时做好准备准时登车。"
value={templateChinese}
onChange={(e) => setTemplateChinese(e.target.value)}
/>
</div>
<div className="mb-3">
<label className="form-label"><strong>Message Content (English)</strong> <span className="text-danger">*</span></label>
<textarea
className="form-control"
rows={4}
placeholder="e.g., {Name}: Your vehicle today has departed at {Time}, driver {Driver}, vehicle number {VehicleNum}, please be ready to board on time."
value={templateEnglish}
onChange={(e) => setTemplateEnglish(e.target.value)}
/>
</div>
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleSaveTemplate}>
Save
</Button>
<Button variant="light" onClick={closeModal}>
Cancel
</Button>
</Modal.Footer>
</Modal>
</>
)
};

View File

@@ -49,6 +49,23 @@ const getSentMessages = () => {
return http.get(`/messages/sent-messages/all`);
}
// Custom Message Templates
const getCustomTemplates = () => {
return http.get('/msg-custom-templates');
}
const createCustomTemplate = (data) => {
return http.post('/msg-custom-templates', data);
}
const updateCustomTemplate = (id, data) => {
return http.put(`/msg-custom-templates/${id}`, data);
}
const deleteCustomTemplate = (id) => {
return http.delete(`/msg-custom-templates/${id}`);
}
export const MessageService = {
getMessage,
getMessages,
@@ -59,5 +76,9 @@ export const MessageService = {
updateMessageToken,
createMessageToken,
sendMessage,
getSentMessages
getSentMessages,
getCustomTemplates,
createCustomTemplate,
updateCustomTemplate,
deleteCustomTemplate
};

View File

@@ -251,6 +251,7 @@ require("./app/routes/attendance-note.routes")(app);
require("./app/routes/carousel.routes")(app);
require("./app/routes/fingerprint-attendance.routes")(app);
require("./app/routes/daily-routes-template.routes")(app);
require("./app/routes/msg-custom-template.routes")(app);
require("./app/scheduler/reminderScheduler");