Files
sasa-maillist/web/static/js/ui.js
James Pattinson 9b6a6dab06 Sub mamangement
2025-10-12 21:20:53 +00:00

648 lines
22 KiB
JavaScript

/**
* UI Helper Functions and Components
* Handles DOM manipulation, notifications, modals, and UI state
*/
class UIManager {
constructor() {
this.currentTab = 'lists';
this.currentEditingItem = null;
this.isLoading = false;
this.confirmCallback = null;
this.initializeEventListeners();
}
/**
* Initialize all event listeners
*/
initializeEventListeners() {
// Tab navigation
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.switchTab(e.target.dataset.tab);
});
});
// Modal close buttons
document.querySelectorAll('.modal-close, .modal .btn-secondary').forEach(btn => {
btn.addEventListener('click', (e) => {
this.closeModal(e.target.closest('.modal'));
});
});
// Click outside modal to close
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.closeModal(modal);
}
});
});
// Notification close
document.getElementById('notificationClose').addEventListener('click', () => {
this.hideNotification();
});
// Add buttons
document.getElementById('addListBtn').addEventListener('click', () => {
this.showListModal();
});
document.getElementById('addMemberBtn').addEventListener('click', () => {
this.showMemberModal();
});
// Member subscriptions modal
document.getElementById('memberSubscriptionsModalClose').addEventListener('click', () => {
this.closeModal(document.getElementById('memberSubscriptionsModal'));
});
document.getElementById('memberSubscriptionsCancelBtn').addEventListener('click', () => {
this.closeModal(document.getElementById('memberSubscriptionsModal'));
});
document.getElementById('memberSubscriptionsSaveBtn').addEventListener('click', () => {
this.handleMemberSubscriptionsSave();
});
// Form submissions
document.getElementById('listForm').addEventListener('submit', (e) => {
e.preventDefault();
this.handleListFormSubmit();
});
document.getElementById('memberForm').addEventListener('submit', (e) => {
e.preventDefault();
this.handleMemberFormSubmit();
});
document.getElementById('subscriptionForm').addEventListener('submit', (e) => {
e.preventDefault();
this.handleSubscriptionFormSubmit();
});
// Confirmation modal
document.getElementById('confirmOkBtn').addEventListener('click', () => {
if (this.confirmCallback) {
this.confirmCallback();
this.confirmCallback = null;
}
this.closeModal(document.getElementById('confirmModal'));
});
document.getElementById('confirmCancelBtn').addEventListener('click', () => {
this.confirmCallback = null;
this.closeModal(document.getElementById('confirmModal'));
});
}
/**
* Show/hide loading overlay
*/
setLoading(loading) {
this.isLoading = loading;
const overlay = document.getElementById('loadingOverlay');
if (loading) {
overlay.style.display = 'flex';
} else {
overlay.style.display = 'none';
}
}
/**
* Show notification
*/
showNotification(message, type = 'info') {
const notification = document.getElementById('notification');
const messageEl = document.getElementById('notificationMessage');
notification.className = `notification ${type}`;
messageEl.textContent = message;
notification.style.display = 'flex';
// Auto-hide after 5 seconds
setTimeout(() => {
this.hideNotification();
}, 5000);
}
/**
* Hide notification
*/
hideNotification() {
document.getElementById('notification').style.display = 'none';
}
/**
* Show confirmation dialog
*/
showConfirmation(message, callback) {
document.getElementById('confirmMessage').textContent = message;
this.confirmCallback = callback;
this.showModal(document.getElementById('confirmModal'));
}
/**
* Switch between tabs
*/
switchTab(tabName) {
this.currentTab = tabName;
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabName);
});
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === `${tabName}-tab`);
});
}
/**
* Show modal
*/
showModal(modal) {
modal.classList.add('active');
// Focus first input
const firstInput = modal.querySelector('input, select, textarea');
if (firstInput) {
setTimeout(() => firstInput.focus(), 100);
}
}
/**
* Close modal
*/
closeModal(modal) {
modal.classList.remove('active');
this.currentEditingItem = null;
// Reset forms
const form = modal.querySelector('form');
if (form) {
form.reset();
}
}
/**
* Show list modal (add/edit)
*/
showListModal(listData = null) {
const modal = document.getElementById('listModal');
const title = document.getElementById('listModalTitle');
const form = document.getElementById('listForm');
if (listData) {
// Edit mode
title.textContent = 'Edit Mailing List';
document.getElementById('listName').value = listData.list_name;
document.getElementById('listEmail').value = listData.list_email;
document.getElementById('listDescription').value = listData.description || '';
document.getElementById('listActive').checked = listData.active;
this.currentEditingItem = listData;
} else {
// Add mode
title.textContent = 'Add Mailing List';
form.reset();
document.getElementById('listActive').checked = true;
this.currentEditingItem = null;
}
this.showModal(modal);
}
/**
* Show member modal (add/edit)
*/
showMemberModal(memberData = null) {
const modal = document.getElementById('memberModal');
const title = document.getElementById('memberModalTitle');
const form = document.getElementById('memberForm');
if (memberData) {
// Edit mode
title.textContent = 'Edit Member';
document.getElementById('memberName').value = memberData.name;
document.getElementById('memberEmail').value = memberData.email;
document.getElementById('memberActive').checked = memberData.active;
this.currentEditingItem = memberData;
} else {
// Add mode
title.textContent = 'Add Member';
form.reset();
document.getElementById('memberActive').checked = true;
this.currentEditingItem = null;
}
this.showModal(modal);
}
/**
* Show subscription modal
*/
async showSubscriptionModal() {
const modal = document.getElementById('subscriptionModal');
try {
// Populate dropdowns
await this.populateSubscriptionDropdowns();
this.showModal(modal);
} catch (error) {
this.showNotification('Failed to load subscription data', 'error');
}
}
/**
* Populate subscription modal dropdowns
*/
async populateSubscriptionDropdowns() {
const listSelect = document.getElementById('subscriptionList');
const memberSelect = document.getElementById('subscriptionMember');
// Clear existing options
listSelect.innerHTML = '<option value="">Select a list...</option>';
memberSelect.innerHTML = '<option value="">Select a member...</option>';
try {
const [lists, members] = await Promise.all([
apiClient.getLists(),
apiClient.getMembers()
]);
// Populate lists
lists.forEach(list => {
if (list.active) {
const option = document.createElement('option');
option.value = list.list_email;
option.textContent = `${list.list_name} (${list.list_email})`;
listSelect.appendChild(option);
}
});
// Populate members
members.forEach(member => {
if (member.active) {
const option = document.createElement('option');
option.value = member.email;
option.textContent = `${member.name} (${member.email})`;
memberSelect.appendChild(option);
}
});
} catch (error) {
throw error;
}
}
/**
* Handle list form submission
*/
async handleListFormSubmit() {
const form = document.getElementById('listForm');
const formData = new FormData(form);
const listData = {
list_name: formData.get('listName'),
list_email: formData.get('listEmail'),
description: formData.get('listDescription') || null,
active: formData.get('listActive') === 'on'
};
try {
this.setLoading(true);
if (this.currentEditingItem) {
// Update existing list
await apiClient.updateList(this.currentEditingItem.list_id, listData);
this.showNotification('Mailing list updated successfully', 'success');
} else {
// Create new list
await apiClient.createList(listData);
this.showNotification('Mailing list created successfully', 'success');
}
this.closeModal(document.getElementById('listModal'));
await window.app.loadData();
} catch (error) {
this.handleError(error, 'Failed to save mailing list');
} finally {
this.setLoading(false);
}
}
/**
* Handle member form submission
*/
async handleMemberFormSubmit() {
const form = document.getElementById('memberForm');
const formData = new FormData(form);
const memberData = {
name: formData.get('memberName'),
email: formData.get('memberEmail'),
active: formData.get('memberActive') === 'on'
};
try {
this.setLoading(true);
if (this.currentEditingItem) {
// Update existing member
await apiClient.updateMember(this.currentEditingItem.member_id, memberData);
this.showNotification('Member updated successfully', 'success');
} else {
// Create new member
await apiClient.createMember(memberData);
this.showNotification('Member created successfully', 'success');
}
this.closeModal(document.getElementById('memberModal'));
await window.app.loadData();
} catch (error) {
this.handleError(error, 'Failed to save member');
} finally {
this.setLoading(false);
}
}
/**
* Handle subscription form submission
*/
async handleSubscriptionFormSubmit() {
const form = document.getElementById('subscriptionForm');
const formData = new FormData(form);
const subscriptionData = {
list_email: formData.get('subscriptionList'),
member_email: formData.get('subscriptionMember'),
active: true
};
try {
this.setLoading(true);
await apiClient.createSubscription(subscriptionData);
this.showNotification('Subscription created successfully', 'success');
this.closeModal(document.getElementById('subscriptionModal'));
await window.app.loadData();
} catch (error) {
this.handleError(error, 'Failed to create subscription');
} finally {
this.setLoading(false);
}
}
/**
* Show member subscriptions modal
*/
async showMemberSubscriptionsModal(member) {
const modal = document.getElementById('memberSubscriptionsModal');
// Update member info
document.getElementById('memberSubscriptionsName').textContent = member.name;
document.getElementById('memberSubscriptionsEmail').textContent = member.email;
document.getElementById('memberSubscriptionsTitle').textContent = `Manage Subscriptions - ${member.name}`;
try {
// Load all lists and member's current subscriptions
const [lists, memberSubscriptions] = await Promise.all([
apiClient.getLists(),
this.getMemberSubscriptions(member.member_id)
]);
this.currentMemberForSubscriptions = member;
this.renderSubscriptionCheckboxes(lists, memberSubscriptions);
this.showModal(modal);
} catch (error) {
this.handleError(error, 'Failed to load subscription data');
}
}
/**
* Get member's current subscriptions
*/
async getMemberSubscriptions(memberId) {
const subscriptions = [];
// Check each list to see if the member is subscribed
for (const [listId, members] of window.app.subscriptions) {
if (members.some(m => m.member_id === memberId)) {
const list = window.app.lists.find(l => l.list_id === listId);
if (list) {
subscriptions.push(list);
}
}
}
return subscriptions;
}
/**
* Render subscription checkboxes
*/
renderSubscriptionCheckboxes(lists, memberSubscriptions) {
const container = document.getElementById('subscriptionCheckboxList');
container.innerHTML = '';
this.subscriptionChanges = new Map(); // Track changes
lists.forEach(list => {
const isSubscribed = memberSubscriptions.some(sub => sub.list_id === list.list_id);
const item = document.createElement('div');
item.className = `subscription-item ${isSubscribed ? 'subscribed' : ''}`;
item.dataset.listId = list.list_id;
item.dataset.subscribed = isSubscribed.toString();
item.innerHTML = `
<div class="list-info">
<div class="list-name">${this.escapeHtml(list.list_name)}</div>
<div class="list-email">${this.escapeHtml(list.list_email)}</div>
</div>
<div class="subscription-toggle">
<div class="toggle-switch ${isSubscribed ? 'active' : ''}" data-list-id="${list.list_id}"></div>
<span class="subscription-status ${isSubscribed ? 'subscribed' : 'not-subscribed'}">
${isSubscribed ? 'Subscribed' : 'Not subscribed'}
</span>
</div>
`;
// Add click handlers
const toggleSwitch = item.querySelector('.toggle-switch');
const statusSpan = item.querySelector('.subscription-status');
const toggleSubscription = () => {
const currentlySubscribed = item.dataset.subscribed === 'true';
const newSubscribed = !currentlySubscribed;
// Update UI
item.dataset.subscribed = newSubscribed.toString();
item.className = `subscription-item ${newSubscribed ? 'subscribed' : ''}`;
toggleSwitch.className = `toggle-switch ${newSubscribed ? 'active' : ''}`;
statusSpan.className = `subscription-status ${newSubscribed ? 'subscribed' : 'not-subscribed'}`;
statusSpan.textContent = newSubscribed ? 'Subscribed' : 'Not subscribed';
// Track the change
const originallySubscribed = memberSubscriptions.some(sub => sub.list_id === list.list_id);
if (newSubscribed !== originallySubscribed) {
this.subscriptionChanges.set(list.list_id, {
list: list,
action: newSubscribed ? 'subscribe' : 'unsubscribe'
});
} else {
this.subscriptionChanges.delete(list.list_id);
}
// Update save button state
this.updateSubscriptionsSaveButton();
};
item.addEventListener('click', toggleSubscription);
toggleSwitch.addEventListener('click', (e) => {
e.stopPropagation();
toggleSubscription();
});
container.appendChild(item);
});
this.updateSubscriptionsSaveButton();
}
/**
* Update save button state based on changes
*/
updateSubscriptionsSaveButton() {
const saveBtn = document.getElementById('memberSubscriptionsSaveBtn');
const hasChanges = this.subscriptionChanges && this.subscriptionChanges.size > 0;
saveBtn.disabled = !hasChanges;
saveBtn.innerHTML = hasChanges
? `<i class="fas fa-save"></i> Save Changes (${this.subscriptionChanges.size})`
: `<i class="fas fa-save"></i> No Changes`;
}
/**
* Handle saving subscription changes
*/
async handleMemberSubscriptionsSave() {
if (!this.subscriptionChanges || this.subscriptionChanges.size === 0) {
return;
}
try {
this.setLoading(true);
const promises = [];
for (const [listId, change] of this.subscriptionChanges) {
if (change.action === 'subscribe') {
promises.push(
apiClient.createSubscription({
list_email: change.list.list_email,
member_email: this.currentMemberForSubscriptions.email,
active: true
})
);
} else {
promises.push(
apiClient.deleteSubscription(
change.list.list_email,
this.currentMemberForSubscriptions.email
)
);
}
}
await Promise.all(promises);
this.showNotification(`Successfully updated ${this.subscriptionChanges.size} subscription(s)`, 'success');
this.closeModal(document.getElementById('memberSubscriptionsModal'));
await window.app.loadData();
} catch (error) {
this.handleError(error, 'Failed to save subscription changes');
} finally {
this.setLoading(false);
}
}
/**
* Handle API errors
*/
handleError(error, defaultMessage = 'An error occurred') {
let message = defaultMessage;
if (error instanceof APIError) {
if (error.isAuthError()) {
message = 'Authentication failed. Please check your API token.';
window.app.logout();
} else if (error.isBadRequest()) {
message = error.message || 'Invalid request data';
} else if (error.isNotFound()) {
message = 'Resource not found';
} else if (error.isServerError()) {
message = 'Server error. Please try again later.';
} else {
message = error.message || defaultMessage;
}
} else {
message = error.message || defaultMessage;
}
this.showNotification(message, 'error');
console.error('Error:', error);
}
/**
* Create action button
*/
createActionButton(text, icon, className, onClick) {
const button = document.createElement('button');
button.className = `btn btn-sm ${className}`;
button.innerHTML = `<i class="fas fa-${icon}"></i> ${text}`;
button.addEventListener('click', onClick);
return button;
}
/**
* Create status badge
*/
createStatusBadge(active) {
const badge = document.createElement('span');
badge.className = `status-badge ${active ? 'active' : 'inactive'}`;
badge.innerHTML = `
<i class="fas fa-${active ? 'check' : 'times'}"></i>
${active ? 'Active' : 'Inactive'}
`;
return badge;
}
/**
* Format email as mailto link
*/
createEmailLink(email) {
const link = document.createElement('a');
link.href = `mailto:${email}`;
link.textContent = email;
link.style.color = 'var(--primary-color)';
return link;
}
/**
* Escape HTML to prevent XSS
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Create global UI manager instance
window.uiManager = new UIManager();