Files
orion/static/admin/js/messages.js
Samir Boulahtit 6de80a2d62 fix: resolve JS errors on messages and platform-homepage pages
- messages.js: Add missing `error` property required by error_state macro
- platform-homepage.html: Use `<template x-if>` instead of `x-show` to
  prevent Alpine from evaluating `page.template` when `page` is null

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 22:08:28 +01:00

494 lines
16 KiB
JavaScript

/**
* Admin Messages Page
*
* Handles the messaging interface including:
* - Conversation list with filtering and pagination
* - Message thread display
* - Sending messages with attachments
* - Creating new conversations
*/
const messagesLog = window.LogConfig?.createLogger('MESSAGES') || console;
/**
* Admin Messages Component
*/
function adminMessages(initialConversationId = null) {
return {
...data(),
currentPage: 'messages',
// Loading states
loading: true,
error: null,
loadingConversations: false,
loadingMessages: false,
sendingMessage: false,
creatingConversation: false,
// Conversations state
conversations: [],
page: 1,
skip: 0,
limit: 20,
totalConversations: 0,
totalUnread: 0,
// Filters
filters: {
conversation_type: '',
is_closed: ''
},
// Selected conversation
selectedConversationId: initialConversationId,
selectedConversation: null,
// Reply form
replyContent: '',
attachedFiles: [],
// Compose modal
showComposeModal: false,
compose: {
recipientType: '',
recipientId: null,
subject: '',
message: ''
},
recipients: [],
loadingRecipients: false,
// Polling
pollInterval: null,
/**
* Initialize component
*/
async init() {
messagesLog.debug('Initializing messages page');
await this.loadConversations();
if (this.selectedConversationId) {
await this.loadConversation(this.selectedConversationId);
}
this.loading = false;
// Start polling for new messages
this.startPolling();
},
/**
* Start polling for updates
*/
startPolling() {
this.pollInterval = setInterval(async () => {
// Only poll if we have a selected conversation
if (this.selectedConversationId && !document.hidden) {
await this.refreshCurrentConversation();
}
// Update unread count
await this.updateUnreadCount();
}, 30000); // 30 seconds
},
/**
* Stop polling
*/
destroy() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
},
// ============================================================================
// CONVERSATIONS LIST
// ============================================================================
/**
* Load conversations with current filters
*/
async loadConversations() {
this.loadingConversations = true;
try {
this.skip = (this.page - 1) * this.limit;
const params = new URLSearchParams();
params.append('skip', this.skip);
params.append('limit', this.limit);
if (this.filters.conversation_type) {
params.append('conversation_type', this.filters.conversation_type);
}
if (this.filters.is_closed !== '') {
params.append('is_closed', this.filters.is_closed);
}
const response = await apiClient.get(`/admin/messages?${params}`);
this.conversations = response.conversations || [];
this.totalConversations = response.total || 0;
this.totalUnread = response.total_unread || 0;
messagesLog.debug(`Loaded ${this.conversations.length} conversations`);
} catch (error) {
messagesLog.error('Failed to load conversations:', error);
window.showToast?.('Failed to load conversations', 'error');
} finally {
this.loadingConversations = false;
}
},
/**
* Update unread count
*/
async updateUnreadCount() {
try {
const response = await apiClient.get('/admin/messages/unread-count');
this.totalUnread = response.total_unread || 0;
} catch (error) {
messagesLog.error('Failed to update unread count:', error);
}
},
/**
* Select a conversation
*/
async selectConversation(conversationId) {
if (this.selectedConversationId === conversationId) return;
this.selectedConversationId = conversationId;
await this.loadConversation(conversationId);
// Update URL without reload
const url = new URL(window.location);
url.pathname = `/admin/messages/${conversationId}`;
window.history.pushState({}, '', url);
},
/**
* Load conversation detail
*/
async loadConversation(conversationId) {
this.loadingMessages = true;
try {
const response = await apiClient.get(`/admin/messages/${conversationId}?mark_read=true`);
this.selectedConversation = response;
// Update unread count in list
const conv = this.conversations.find(c => c.id === conversationId);
if (conv) {
this.totalUnread = Math.max(0, this.totalUnread - conv.unread_count);
conv.unread_count = 0;
}
messagesLog.debug(`Loaded conversation ${conversationId} with ${response.messages?.length || 0} messages`);
// Scroll to bottom
await this.$nextTick();
this.scrollToBottom();
} catch (error) {
messagesLog.error('Failed to load conversation:', error);
window.showToast?.('Failed to load conversation', 'error');
} finally {
this.loadingMessages = false;
}
},
/**
* Refresh current conversation
*/
async refreshCurrentConversation() {
if (!this.selectedConversationId) return;
try {
const response = await apiClient.get(`/admin/messages/${this.selectedConversationId}?mark_read=true`);
const oldCount = this.selectedConversation?.messages?.length || 0;
const newCount = response.messages?.length || 0;
this.selectedConversation = response;
// If new messages, scroll to bottom
if (newCount > oldCount) {
await this.$nextTick();
this.scrollToBottom();
}
} catch (error) {
messagesLog.error('Failed to refresh conversation:', error);
}
},
/**
* Scroll messages to bottom
*/
scrollToBottom() {
const container = this.$refs.messagesContainer;
if (container) {
container.scrollTop = container.scrollHeight;
}
},
// ============================================================================
// SENDING MESSAGES
// ============================================================================
/**
* Handle file selection
*/
handleFileSelect(event) {
const files = Array.from(event.target.files);
this.attachedFiles = files;
messagesLog.debug(`Selected ${files.length} files`);
},
/**
* Send a message
*/
async sendMessage() {
if (!this.replyContent.trim() && this.attachedFiles.length === 0) return;
if (!this.selectedConversationId) return;
this.sendingMessage = true;
try {
const formData = new FormData();
formData.append('content', this.replyContent);
for (const file of this.attachedFiles) {
formData.append('files', file);
}
const response = await fetch(`/api/v1/admin/messages/${this.selectedConversationId}/messages`, {
method: 'POST',
body: formData,
headers: {
'Authorization': `Bearer ${window.getAuthToken?.() || ''}`
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to send message');
}
const message = await response.json();
// Add to messages
if (this.selectedConversation) {
this.selectedConversation.messages.push(message);
this.selectedConversation.message_count++;
}
// Clear form
this.replyContent = '';
this.attachedFiles = [];
if (this.$refs.fileInput) {
this.$refs.fileInput.value = '';
}
// Scroll to bottom
await this.$nextTick();
this.scrollToBottom();
messagesLog.debug(`Sent message ${message.id}`);
} catch (error) {
messagesLog.error('Failed to send message:', error);
window.showToast?.(error.message || 'Failed to send message', 'error');
} finally {
this.sendingMessage = false;
}
},
// ============================================================================
// CONVERSATION ACTIONS
// ============================================================================
/**
* Close conversation
*/
async closeConversation() {
if (!confirm('Are you sure you want to close this conversation?')) return;
try {
await apiClient.post(`/admin/messages/${this.selectedConversationId}/close`);
if (this.selectedConversation) {
this.selectedConversation.is_closed = true;
}
// Update in list
const conv = this.conversations.find(c => c.id === this.selectedConversationId);
if (conv) {
conv.is_closed = true;
}
window.showToast?.('Conversation closed', 'success');
} catch (error) {
messagesLog.error('Failed to close conversation:', error);
window.showToast?.('Failed to close conversation', 'error');
}
},
/**
* Reopen conversation
*/
async reopenConversation() {
try {
await apiClient.post(`/admin/messages/${this.selectedConversationId}/reopen`);
if (this.selectedConversation) {
this.selectedConversation.is_closed = false;
}
// Update in list
const conv = this.conversations.find(c => c.id === this.selectedConversationId);
if (conv) {
conv.is_closed = false;
}
window.showToast?.('Conversation reopened', 'success');
} catch (error) {
messagesLog.error('Failed to reopen conversation:', error);
window.showToast?.('Failed to reopen conversation', 'error');
}
},
// ============================================================================
// CREATE CONVERSATION
// ============================================================================
/**
* Load recipients for compose modal
*/
async loadRecipients() {
if (!this.compose.recipientType) {
this.recipients = [];
return;
}
this.loadingRecipients = true;
try {
const response = await apiClient.get(`/admin/messages/recipients?recipient_type=${this.compose.recipientType}&limit=100`);
this.recipients = response.recipients || [];
messagesLog.debug(`Loaded ${this.recipients.length} recipients`);
} catch (error) {
messagesLog.error('Failed to load recipients:', error);
window.showToast?.('Failed to load recipients', 'error');
} finally {
this.loadingRecipients = false;
}
},
/**
* Create new conversation
*/
async createConversation() {
if (!this.compose.recipientId || !this.compose.subject.trim()) return;
this.creatingConversation = true;
try {
// Determine conversation type
const conversationType = this.compose.recipientType === 'vendor'
? 'admin_vendor'
: 'admin_customer';
// Get vendor_id if customer
let vendorId = null;
if (this.compose.recipientType === 'customer') {
const recipient = this.recipients.find(r => r.id === parseInt(this.compose.recipientId));
vendorId = recipient?.vendor_id;
}
const response = await apiClient.post('/admin/messages', {
conversation_type: conversationType,
subject: this.compose.subject,
recipient_type: this.compose.recipientType,
recipient_id: parseInt(this.compose.recipientId),
vendor_id: vendorId,
initial_message: this.compose.message || null
});
// Close modal and reset
this.showComposeModal = false;
this.compose = {
recipientType: '',
recipientId: null,
subject: '',
message: ''
};
this.recipients = [];
// Reload conversations and select new one
await this.loadConversations();
await this.selectConversation(response.id);
window.showToast?.('Conversation created', 'success');
} catch (error) {
messagesLog.error('Failed to create conversation:', error);
window.showToast?.(error.message || 'Failed to create conversation', 'error');
} finally {
this.creatingConversation = false;
}
},
// ============================================================================
// HELPERS
// ============================================================================
/**
* Get other participant name
*/
getOtherParticipantName() {
if (!this.selectedConversation?.participants) return 'Unknown';
const other = this.selectedConversation.participants.find(
p => p.participant_type !== 'admin'
);
return other?.participant_info?.name || 'Unknown';
},
/**
* Format relative time
*/
formatRelativeTime(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'Now';
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
if (diff < 172800) return 'Yesterday';
if (diff < 604800) return `${Math.floor(diff / 86400)}d`;
return date.toLocaleDateString();
},
/**
* Format time for messages
*/
formatTime(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
if (isToday) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
return date.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
};
}
// Make available globally
window.adminMessages = adminMessages;