Files
MSPE/admin/messages.html

785 lines
30 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow, noarchive">
<title>Messages - MSPE Admin</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
<link rel="stylesheet" href="/admin/css/admin.css">
<style>
.messages-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
height: calc(100vh - 200px);
}
.messages-list {
background: white;
border-radius: 10px;
box-shadow: var(--shadow-sm);
overflow: hidden;
display: flex;
flex-direction: column;
}
.messages-list-header {
padding: 1.5rem;
border-bottom: 1px solid var(--gray-200);
display: flex;
justify-content: space-between;
align-items: center;
}
.messages-list-header h3 {
margin: 0;
}
.unread-count {
background: var(--primary);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
}
.messages-filters {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--gray-200);
display: flex;
gap: 1rem;
}
.messages-filters select {
padding: 0.5rem 1rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-sm);
font-size: 0.875rem;
}
.messages-items {
flex: 1;
overflow-y: auto;
}
.message-item {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--gray-200);
cursor: pointer;
transition: all 0.3s ease;
}
.message-item:hover {
background: var(--gray-50);
}
.message-item.active {
background: var(--primary);
color: white;
}
.message-item.active .message-meta,
.message-item.active .message-preview {
color: rgba(255,255,255,0.8);
}
.message-item.unread {
background: rgba(14, 165, 233, 0.05);
border-left: 3px solid var(--primary);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.message-sender {
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.message-sender .unread-dot {
width: 8px;
height: 8px;
background: var(--primary);
border-radius: 50%;
}
.message-meta {
font-size: 0.75rem;
color: var(--gray-500);
}
.message-preview {
font-size: 0.875rem;
color: var(--gray-600);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-details {
background: white;
border-radius: 10px;
box-shadow: var(--shadow-sm);
overflow: hidden;
display: flex;
flex-direction: column;
}
.message-details-header {
padding: 1.5rem;
border-bottom: 1px solid var(--gray-200);
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.message-details-header h3 {
margin: 0 0 0.5rem 0;
}
.message-details-header .meta {
font-size: 0.875rem;
color: var(--gray-500);
}
.message-actions-header {
display: flex;
gap: 0.5rem;
}
.message-actions-header button {
padding: 0.5rem 1rem;
border: 1px solid var(--gray-300);
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.3s ease;
}
.message-actions-header button:hover {
background: var(--gray-100);
}
.message-actions-header button.delete:hover {
background: #fee2e2;
color: #dc2626;
border-color: #dc2626;
}
.message-details-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
.contact-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--gray-200);
}
.contact-info-item {
display: flex;
flex-direction: column;
}
.contact-info-item label {
font-size: 0.75rem;
font-weight: 600;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.375rem;
}
.contact-info-item .value {
font-size: 0.9375rem;
color: var(--gray-700);
}
.contact-info-item .value a {
color: var(--primary);
text-decoration: none;
}
.message-body {
line-height: 1.7;
color: var(--gray-700);
}
.message-body h4 {
margin-bottom: 1rem;
color: var(--gray-900);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 4rem 2rem;
text-align: center;
color: var(--gray-500);
}
.empty-state i {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.3;
}
@media (max-width: 1024px) {
.messages-grid {
grid-template-columns: 1fr;
height: auto;
}
.messages-list {
max-height: 400px;
}
}
</style>
</head>
<body>
<div class="admin-wrapper">
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<a href="/admin/dashboard.html" class="sidebar-logo">
<i class="fas fa-cube"></i>
<span>MSPE</span>
</a>
<button class="sidebar-toggle" id="sidebar-toggle">
<i class="fas fa-bars"></i>
</button>
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<span class="nav-section-title">Main</span>
<ul class="nav-menu">
<li class="nav-item">
<a href="/admin/dashboard.html" class="nav-link">
<i class="fas fa-th-large"></i>
<span>Dashboard</span>
</a>
</li>
</ul>
</div>
<div class="nav-section">
<span class="nav-section-title">Content</span>
<ul class="nav-menu">
<li class="nav-item">
<a href="/admin/pages.html" class="nav-link">
<i class="fas fa-file-alt"></i>
<span>Pages</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/services.html" class="nav-link">
<i class="fas fa-concierge-bell"></i>
<span>Services</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio.html" class="nav-link">
<i class="fas fa-briefcase"></i>
<span>Portfolio</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/news.html" class="nav-link">
<i class="fas fa-newspaper"></i>
<span>News & Events</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/testimonials.html" class="nav-link">
<i class="fas fa-quote-right"></i>
<span>Testimonials</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/team.html" class="nav-link">
<i class="fas fa-users"></i>
<span>Team</span>
</a>
</li>
</ul>
</div>
<div class="nav-section">
<span class="nav-section-title">Media</span>
<ul class="nav-menu">
<li class="nav-item">
<a href="/admin/media.html" class="nav-link">
<i class="fas fa-images"></i>
<span>Media Library</span>
</a>
</li>
</ul>
</div>
<div class="nav-section">
<span class="nav-section-title">Inquiries</span>
<ul class="nav-menu">
<li class="nav-item active">
<a href="/admin/messages.html" class="nav-link">
<i class="fas fa-envelope"></i>
<span>Messages</span>
<span class="badge" id="sidebar-unread-count">0</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/subscribers.html" class="nav-link">
<i class="fas fa-bell"></i>
<span>Subscribers</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/bookings.html" class="nav-link">
<i class="fas fa-calendar-check"></i>
<span>Bookings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/availability.html" class="nav-link">
<i class="fas fa-clock"></i>
<span>Availability</span>
</a>
</li>
</ul>
</div>
<div class="nav-section">
<span class="nav-section-title">Settings</span>
<ul class="nav-menu">
<li class="nav-item">
<a href="/admin/settings.html" class="nav-link">
<i class="fas fa-cog"></i>
<span>Site Settings</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/users.html" class="nav-link">
<i class="fas fa-user-shield"></i>
<span>Admin Users</span>
</a>
</li>
</ul>
</div>
</nav>
<div class="sidebar-footer">
<a href="/index.html" class="view-site-btn" target="_blank">
<i class="fas fa-external-link-alt"></i>
<span>View Website</span>
</a>
</div>
</aside>
<main class="main-content">
<header class="admin-header">
<div class="header-left">
<button class="mobile-toggle" id="mobile-toggle">
<i class="fas fa-bars"></i>
</button>
<h1 class="page-title">Messages</h1>
</div>
<div class="header-right">
<div class="header-search">
<i class="fas fa-search"></i>
<input type="text" placeholder="Search messages..." id="search-input">
</div>
<div class="header-user">
<div class="user-avatar">
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
</div>
<div class="user-info">
<span class="user-name">Admin</span>
<span class="user-role">Administrator</span>
</div>
<div class="user-dropdown">
<button class="dropdown-toggle">
<i class="fas fa-chevron-down"></i>
</button>
<ul class="dropdown-menu">
<li><a href="/admin/settings.html"><i class="fas fa-cog"></i> Settings</a></li>
<li class="divider"></li>
<li><a href="#" id="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
</ul>
</div>
</div>
</div>
</header>
<div class="content-wrapper">
<div class="messages-grid">
<div class="messages-list">
<div class="messages-list-header">
<h3>Inbox</h3>
<span class="unread-count" id="unread-count">0 unread</span>
</div>
<div class="messages-filters">
<select id="status-filter">
<option value="">All Messages</option>
<option value="unread">Unread</option>
<option value="read">Read</option>
</select>
</div>
<div class="messages-items" id="messages-list">
<div class="empty-state">
<i class="fas fa-spinner fa-spin"></i>
<p>Loading messages...</p>
</div>
</div>
</div>
<div class="message-details" id="message-details">
<div class="empty-state">
<i class="fas fa-envelope-open-text"></i>
<p>Select a message to view details</p>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="/admin/js/admin.js"></script>
<script>
let allMessages = [];
let selectedMessageId = null;
document.addEventListener('DOMContentLoaded', function() {
loadMessages();
setupEventListeners();
checkUrlParams();
});
function setupEventListeners() {
document.getElementById('status-filter').addEventListener('change', filterMessages);
document.getElementById('search-input').addEventListener('input', MSPE.debounce(filterMessages, 300));
}
function checkUrlParams() {
const params = new URLSearchParams(window.location.search);
const messageId = params.get('id');
if (messageId) {
setTimeout(() => selectMessage(messageId), 500);
}
}
async function loadMessages() {
try {
const response = await MSPE.API.get('contact.php');
if (response && response.success) {
allMessages = response.data || [];
updateUnreadCount(response.unread || 0);
renderMessagesList(allMessages);
}
} catch (error) {
console.error('Failed to load messages:', error);
document.getElementById('messages-list').innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-circle"></i>
<p>Failed to load messages</p>
</div>
`;
}
}
function updateUnreadCount(count) {
document.getElementById('unread-count').textContent = `${count} unread`;
document.getElementById('sidebar-unread-count').textContent = count;
if (count === 0) {
document.getElementById('sidebar-unread-count').style.display = 'none';
} else {
document.getElementById('sidebar-unread-count').style.display = 'inline';
}
}
function renderMessagesList(messages) {
const container = document.getElementById('messages-list');
if (!messages || messages.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-inbox"></i>
<p>No messages yet</p>
<span style="font-size: 0.875rem;">Messages from your contact form will appear here</span>
</div>
`;
return;
}
container.innerHTML = messages.map(message => {
const isUnread = (message.status === 'unread');
const isActive = (message.id === selectedMessageId);
const name = `${message.first_name || ''} ${message.last_name || ''}`.trim();
const timeAgo = getTimeAgo(new Date(message.created_at));
const rawMessage = String(message.message || '');
const preview = rawMessage.length > 60
? rawMessage.substring(0, 60) + '...'
: rawMessage;
const safeId = MSPE.escapeHtml(message.id || '');
const safeName = MSPE.escapeHtml(name || 'Unknown Sender');
const safeTimeAgo = MSPE.escapeHtml(timeAgo || '');
const safePreview = MSPE.escapeHtml(preview || '');
return `
<div class="message-item ${isUnread ? 'unread' : ''} ${isActive ? 'active' : ''}"
data-id="${safeId}">
<div class="message-header">
<span class="message-sender">
${isUnread ? '<span class="unread-dot"></span>' : ''}
${safeName}
</span>
<span class="message-meta">${safeTimeAgo}</span>
</div>
<div class="message-preview">${safePreview}</div>
</div>
`;
}).join('');
container.querySelectorAll('.message-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.dataset.id;
if (id) selectMessage(id);
});
});
}
async function selectMessage(id) {
selectedMessageId = id;
// Update list selection
document.querySelectorAll('.message-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.id === id) {
item.classList.add('active');
item.classList.remove('unread');
}
});
try {
const response = await MSPE.API.get(`contact.php?id=${id}`);
if (response && response.success && response.data) {
const message = response.data;
renderMessageDetails(message);
// Update message in local array
const index = allMessages.findIndex(m => m.id === id);
if (index !== -1) {
allMessages[index].status = 'read';
}
// Update unread count
const unreadCount = allMessages.filter(m => m.status === 'unread').length;
updateUnreadCount(unreadCount);
}
} catch (error) {
console.error('Failed to load message:', error);
}
}
function renderMessageDetails(message) {
const container = document.getElementById('message-details');
const name = `${message.first_name || ''} ${message.last_name || ''}`.trim();
const date = new Date(message.created_at);
const formattedDate = date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
const serviceLabels = {
'it-support': 'IT Support',
'cybersecurity': 'Cybersecurity',
'cloud': 'Cloud Services',
'consulting': 'Business Consulting',
'general': 'General Inquiry'
};
const safeName = MSPE.escapeHtml(name || 'Unknown Sender');
const safeDate = MSPE.escapeHtml(formattedDate || '');
const safeEmail = MSPE.escapeHtml(message.email || '');
const safePhone = MSPE.escapeHtml(message.phone || 'Not provided');
const safeCompany = MSPE.escapeHtml(message.company || 'Not provided');
const safeService = MSPE.escapeHtml(serviceLabels[message.service] || message.service || 'Not specified');
const safeBudget = message.budget ? MSPE.escapeHtml(message.budget) : '';
const safeMessageBody = MSPE.escapeHtml(message.message || 'Not provided').replace(/\n/g, '<br>');
const safeId = MSPE.escapeHtml(message.id || '');
container.innerHTML = `
<div class="message-details-header">
<div>
<h3>${safeName}</h3>
<div class="meta">${safeDate}</div>
</div>
<div class="message-actions-header">
<button class="reply-btn" data-email="${safeEmail}">
<i class="fas fa-reply"></i> Reply
</button>
<button class="delete delete-btn" data-id="${safeId}">
<i class="fas fa-trash"></i> Delete
</button>
</div>
</div>
<div class="message-details-content">
<div class="contact-info">
<div class="contact-info-item">
<label>Email</label>
<div class="value"><a href="mailto:${safeEmail}">${safeEmail}</a></div>
</div>
<div class="contact-info-item">
<label>Phone</label>
<div class="value">${safePhone}</div>
</div>
<div class="contact-info-item">
<label>Company</label>
<div class="value">${safeCompany}</div>
</div>
<div class="contact-info-item">
<label>Service Interest</label>
<div class="value">${safeService}</div>
</div>
${message.budget ? `
<div class="contact-info-item">
<label>Budget</label>
<div class="value">${safeBudget}</div>
</div>
` : ''}
</div>
<div class="message-body">
<h4>Message</h4>
<p>${safeMessageBody}</p>
</div>
</div>
`;
const replyBtn = container.querySelector('.reply-btn');
if (replyBtn) {
replyBtn.addEventListener('click', () => {
const email = replyBtn.dataset.email || '';
if (email) replyToMessage(email);
});
}
const deleteBtn = container.querySelector('.delete-btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', () => {
const id = deleteBtn.dataset.id || '';
if (id) deleteMessage(id);
});
}
}
function replyToMessage(email) {
window.location.href = `mailto:${email}`;
}
async function deleteMessage(id) {
if (!await MSPE.confirmDialog('Are you sure you want to delete this message? This action cannot be undone.')) {
return;
}
try {
const response = await MSPE.API.delete(`contact.php?id=${id}`);
if (response && response.success) {
MSPE.showNotification('Message deleted successfully', 'success');
// Remove from local array
allMessages = allMessages.filter(m => m.id !== id);
// Clear details panel
selectedMessageId = null;
document.getElementById('message-details').innerHTML = `
<div class="empty-state">
<i class="fas fa-envelope-open-text"></i>
<p>Select a message to view details</p>
</div>
`;
// Re-render list
renderMessagesList(allMessages);
// Update unread count
const unreadCount = allMessages.filter(m => m.status === 'unread').length;
updateUnreadCount(unreadCount);
}
} catch (error) {
console.error('Failed to delete message:', error);
MSPE.showNotification('Failed to delete message', 'error');
}
}
function filterMessages() {
const status = document.getElementById('status-filter').value;
const search = document.getElementById('search-input').value.toLowerCase();
let filtered = allMessages;
if (status) {
filtered = filtered.filter(m => m.status === status);
}
if (search) {
filtered = filtered.filter(m =>
m.first_name.toLowerCase().includes(search) ||
m.last_name.toLowerCase().includes(search) ||
m.email.toLowerCase().includes(search) ||
m.message.toLowerCase().includes(search) ||
(m.company && m.company.toLowerCase().includes(search))
);
}
renderMessagesList(filtered);
}
function getTimeAgo(date) {
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
</script>
</body>
</html>