message template
This commit is contained in:
BIN
app/.DS_Store
vendored
BIN
app/.DS_Store
vendored
Binary file not shown.
94
app/controllers/msg-custom-template.controller.js
Normal file
94
app/controllers/msg-custom-template.controller.js
Normal 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 });
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
23
app/models/msg-custom-template.model.js
Normal file
23
app/models/msg-custom-template.model.js
Normal 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;
|
||||
};
|
||||
19
app/routes/msg-custom-template.routes.js
Normal file
19
app/routes/msg-custom-template.routes.js
Normal 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);
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
3
app/views/static/js/main.8452576d.js
Normal file
3
app/views/static/js/main.8452576d.js
Normal file
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
BIN
client/.DS_Store
vendored
Binary file not shown.
@@ -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,7 +56,77 @@ 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 (
|
||||
<>
|
||||
<div className="list row mb-4">
|
||||
@@ -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">
|
||||
<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 === '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>
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user