fix
This commit is contained in:
BIN
app/.DS_Store
vendored
BIN
app/.DS_Store
vendored
Binary file not shown.
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user