This commit is contained in:
2026-02-11 17:07:31 -05:00
parent 9e60893393
commit 699f5bd562
8 changed files with 206 additions and 28 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
app/.DS_Store vendored

Binary file not shown.

View File

@@ -1,16 +1,16 @@
{
"files": {
"main.css": "/static/css/main.6616f3cb.css",
"main.js": "/static/js/main.0f4bfc0c.js",
"main.js": "/static/js/main.02f98813.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.0f4bfc0c.js.map": "/static/js/main.0f4bfc0c.js.map",
"main.02f98813.js.map": "/static/js/main.02f98813.js.map",
"787.c4e7f8f9.chunk.js.map": "/static/js/787.c4e7f8f9.chunk.js.map"
},
"entrypoints": [
"static/css/main.6616f3cb.css",
"static/js/main.0f4bfc0c.js"
"static/js/main.02f98813.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.0f4bfc0c.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.02f98813.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

View File

@@ -1,10 +1,12 @@
import React, {useState, useEffect} from "react";
import React, {useState, useEffect, useRef} from "react";
import { useNavigate } from "react-router-dom";
import { AuthService, MessageService, CustomerService } from "../../services";
import { Breadcrumb, Tabs, Tab, Modal, Button } from "react-bootstrap";
import { PencilSquare, Plus, Trash, Send, Eye, ArrowLeft } from "react-bootstrap-icons";
import { Breadcrumb, Tabs, Tab, Modal, Button, Dropdown } from "react-bootstrap";
import { PencilSquare, Plus, Trash, Send, Eye, ArrowLeft, Filter, Calendar3, Search, ChevronLeft, ChevronRight } from "react-bootstrap-icons";
import Select from 'react-select';
import DatePicker from 'react-datepicker';
import moment from 'moment';
import 'react-datepicker/dist/react-datepicker.css';
const MessageList = () => {
const navigate = useNavigate();
@@ -34,6 +36,20 @@ const MessageList = () => {
const [placeholderValues, setPlaceholderValues] = useState({});
const [sending, setSending] = useState(false);
// Search, Filter, Date state for All Sent Messages
const [searchText, setSearchText] = useState('');
const [filterLanguage, setFilterLanguage] = useState('');
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
const [filterDate, setFilterDate] = useState(null);
const [showDatePicker, setShowDatePicker] = useState(false);
const datePickerRef = useRef(null);
// Pagination state
const [sentPage, setSentPage] = useState(1);
const [sentPageSize, setSentPageSize] = useState(10);
const [templatePage, setTemplatePage] = useState(1);
const [templatePageSize, setTemplatePageSize] = useState(10);
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.')
@@ -42,13 +58,23 @@ const MessageList = () => {
}
fetchMessages();
fetchCustomTemplates();
// Fetch customers with text_msg_enabled
CustomerService.getAllActiveCustomers().then(data => {
const all = data?.data || [];
setEligibleCustomers(all.filter(c => c.text_msg_enabled === true));
});
}, []);
// Close date picker on outside click
useEffect(() => {
const handleClickOutside = (e) => {
if (datePickerRef.current && !datePickerRef.current.contains(e.target)) {
setShowDatePicker(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const fetchMessages = () => {
MessageService.getMessages().then(data => {
setMessages(data?.data || []);
@@ -148,7 +174,6 @@ const MessageList = () => {
}
};
// Extract {placeholder} patterns from both chinese and english content
const extractPlaceholders = (chinese, english) => {
const regex = /\{([^}]+)\}/g;
const set = new Set();
@@ -221,8 +246,105 @@ const MessageList = () => {
const previewChinese = replacePlaceholders(sendChinese);
const previewEnglish = replacePlaceholders(sendEnglish);
// Filter sent messages (those with the new title field)
const sentMessages = messages.filter(m => m.title || m.sent_date);
// ---- Filtering & Search for Sent Messages ----
const allSentMessages = messages.filter(m => m.title || m.sent_date);
const filteredSentMessages = allSentMessages.filter(msg => {
// Search filter
if (searchText.trim()) {
const q = searchText.toLowerCase();
const name = (msg?.recipient_name || msg?.message_name || '').toLowerCase();
const title = (msg?.title || msg?.message_title || '').toLowerCase();
const content = (msg?.message || msg?.message_body || '').toLowerCase();
if (!name.includes(q) && !title.includes(q) && !content.includes(q)) return false;
}
// Language filter
if (filterLanguage) {
if ((msg?.language || '').toLowerCase() !== filterLanguage.toLowerCase()) return false;
}
// Date filter
if (filterDate) {
if (!msg?.sent_date) return false;
const msgDate = moment(msg.sent_date).format('MM/DD/YYYY');
const selectedDateStr = moment(filterDate).format('MM/DD/YYYY');
if (msgDate !== selectedDateStr) return false;
}
return true;
});
// ---- Pagination helpers ----
const paginate = (items, page, pageSize) => {
const start = (page - 1) * pageSize;
return items.slice(start, start + pageSize);
};
const totalSentPages = Math.ceil(filteredSentMessages.length / sentPageSize) || 1;
const paginatedSent = paginate(filteredSentMessages, sentPage, sentPageSize);
const totalTemplatePages = Math.ceil(customTemplates.length / templatePageSize) || 1;
const paginatedTemplates = paginate(customTemplates, templatePage, templatePageSize);
// Reset to page 1 when filters change
useEffect(() => { setSentPage(1); }, [searchText, filterLanguage, filterDate]);
const renderPagination = (currentPage, totalPages, setPage, totalItems, pageSize) => {
if (totalItems === 0) return null;
const startItem = (currentPage - 1) * pageSize + 1;
const endItem = Math.min(currentPage * pageSize, totalItems);
const maxButtons = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2));
let endPage = Math.min(totalPages, startPage + maxButtons - 1);
if (endPage - startPage + 1 < maxButtons) {
startPage = Math.max(1, endPage - maxButtons + 1);
}
const pageNumbers = [];
for (let i = startPage; i <= endPage; i++) pageNumbers.push(i);
return (
<div className="d-flex align-items-center justify-content-between mt-3" style={{ fontSize: '13px' }}>
<div className="d-flex align-items-center" style={{ gap: '8px' }}>
<span>Showing</span>
<select
className="form-select form-select-sm"
style={{ width: '70px' }}
value={pageSize}
onChange={(e) => {
const newSize = parseInt(e.target.value);
if (setPage === setSentPage) { setSentPageSize(newSize); setSentPage(1); }
else { setTemplatePageSize(newSize); setTemplatePage(1); }
}}
>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<span className="text-muted">Showing {startItem} to {endItem} out of {totalItems} records</span>
<div className="d-flex align-items-center" style={{ gap: '4px' }}>
<button className="btn btn-outline-secondary btn-sm" disabled={currentPage === 1} onClick={() => setPage(currentPage - 1)}>
<ChevronLeft size={14} />
</button>
{pageNumbers.map(num => (
<button
key={num}
className={`btn btn-sm ${num === currentPage ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => setPage(num)}
style={{ minWidth: '32px' }}
>
{num}
</button>
))}
<button className="btn btn-outline-secondary btn-sm" disabled={currentPage === totalPages} onClick={() => setPage(currentPage + 1)}>
<ChevronRight size={14} />
</button>
</div>
</div>
);
};
// Get unique languages for the filter dropdown
const availableLanguages = [...new Set(allSentMessages.map(m => m?.language).filter(Boolean))];
return (
<>
@@ -239,7 +361,7 @@ const MessageList = () => {
<div className="app-main-content-list-func-container">
<Tabs activeKey={activeTab} onSelect={(k) => setActiveTab(k)} id="messages-tab">
<Tab eventKey="allMessages" title="All Sent Messages">
<table className="personnel-info-table">
<table className="personnel-info-table" style={{ marginTop: '16px' }}>
<thead>
<tr>
<th className="th-index">No.</th>
@@ -251,24 +373,25 @@ const MessageList = () => {
</tr>
</thead>
<tbody>
{sentMessages && sentMessages.map((msg, index) => (
{paginatedSent.map((msg, index) => (
<tr key={msg.id}>
<td className="td-index">{index + 1}</td>
<td className="td-index">{(sentPage - 1) * sentPageSize + index + 1}</td>
<td>{msg?.recipient_name || msg?.message_name || ''}</td>
<td>{msg?.title || msg?.message_title || ''}</td>
<td>{msg?.language || ''}</td>
<td>{msg?.sent_date ? moment(msg.sent_date).format('MM/DD/YYYY hh:mm A') : ''}</td>
<td>{msg?.sent_date ? moment(msg.sent_date).format('MM/DD/YYYY') : ''}</td>
<td style={{ maxWidth: '400px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{msg?.message || msg?.message_body || ''}</td>
</tr>
))}
{(!sentMessages || sentMessages.length === 0) && (
<tr><td colSpan="6" style={{ textAlign: 'center', color: '#999', padding: '24px' }}>No sent messages yet.</td></tr>
{filteredSentMessages.length === 0 && (
<tr><td colSpan="6" style={{ textAlign: 'center', color: '#999', padding: '24px' }}>No sent messages found.</td></tr>
)}
</tbody>
</table>
{renderPagination(sentPage, totalSentPages, setSentPage, filteredSentMessages.length, sentPageSize)}
</Tab>
<Tab eventKey="messageTemplate" title="Message Template">
<table className="personnel-info-table">
<table className="personnel-info-table" style={{ marginTop: '16px' }}>
<thead>
<tr>
<th className="th-index">No.</th>
@@ -279,9 +402,9 @@ const MessageList = () => {
</tr>
</thead>
<tbody>
{customTemplates && customTemplates.map((template, index) => (
{paginatedTemplates.map((template, index) => (
<tr key={template.id}>
<td className="td-index">{index + 1}</td>
<td className="td-index">{(templatePage - 1) * templatePageSize + 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>
@@ -291,19 +414,74 @@ const MessageList = () => {
</td>
</tr>
))}
{(!customTemplates || customTemplates.length === 0) && (
{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>
{renderPagination(templatePage, totalTemplatePages, setTemplatePage, customTemplates.length, templatePageSize)}
</Tab>
</Tabs>
<div className="list-func-panel">
<div className="list-func-panel" style={{ gap: '8px', alignItems: 'center' }}>
{activeTab === 'allMessages' && (
<button className="btn btn-primary me-2" onClick={openSendModal}><Send size={16} className="me-2" />Send New Message</button>
<>
<div className="d-flex align-items-center" style={{ position: 'relative' }}>
<Search size={12} style={{ position: 'absolute', left: '10px', color: '#999' }} />
<input
type="text"
className="form-control"
placeholder="Search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ paddingLeft: '30px', width: '180px', borderRadius: '6px', height: '31px', fontSize: '13px' }}
/>
</div>
<Dropdown show={showFilterDropdown} onToggle={(isOpen) => { setShowFilterDropdown(isOpen); if (isOpen) setShowDatePicker(false); }}>
<Dropdown.Toggle variant="outline-secondary" size="sm">
<Filter size={14} className="me-1" />Filter{filterLanguage ? `: ${filterLanguage}` : ''}
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item active={!filterLanguage} onClick={() => { setFilterLanguage(''); setShowFilterDropdown(false); }}>All Languages</Dropdown.Item>
{availableLanguages.map(lang => (
<Dropdown.Item key={lang} active={filterLanguage === lang} onClick={() => { setFilterLanguage(lang); setShowFilterDropdown(false); }}>
{lang.charAt(0).toUpperCase() + lang.slice(1)}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<div style={{ position: 'relative' }} ref={datePickerRef}>
<button
className="btn btn-outline-secondary btn-sm"
onClick={() => { setShowDatePicker(!showDatePicker); setShowFilterDropdown(false); }}
>
<Calendar3 size={14} className="me-1" />
{filterDate ? moment(filterDate).format('MM/DD/YYYY') : 'Select Date to View'}
</button>
{filterDate && (
<button
className="btn btn-link btn-sm p-0 ms-1"
onClick={(e) => { e.stopPropagation(); setFilterDate(null); setShowDatePicker(false); }}
title="Clear date"
style={{ fontSize: '14px', textDecoration: 'none', color: '#999' }}
>
</button>
)}
{showDatePicker && (
<div style={{ position: 'absolute', top: '100%', right: 0, zIndex: 1000, marginTop: '4px' }}>
<DatePicker
selected={filterDate}
onChange={(date) => { setFilterDate(date); setShowDatePicker(false); }}
inline
/>
</div>
)}
</div>
<button className="btn btn-primary btn-sm" onClick={openSendModal}><Send size={14} className="me-1" />Send New Message</button>
</>
)}
{activeTab === 'messageTemplate' && (
<button className="btn btn-primary me-2" onClick={openCreateTemplateModal}><Plus size={16} className="me-2" />Create Message Template</button>
<button className="btn btn-primary btn-sm" onClick={openCreateTemplateModal}><Plus size={16} className="me-1" />Create Message Template</button>
)}
</div>
</div>