Small bug fixes

This commit is contained in:
admin 2025-08-25 12:27:33 -06:00
parent 10f080fd13
commit 5795ec737b
7 changed files with 345 additions and 7 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1684,13 +1684,29 @@ def upload_campaign_csv():
if not contacts:
return jsonify({'success': False, 'error': 'No valid contacts found in CSV'}), 400
# Save as contact list for reuse (like the regular CSV upload)
list_id = None
list_name = None
try:
from models.contact_list import ContactList
cl = ContactList(app.config.get('DATABASE'))
cl.ensure_schema()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
list_name = f"{file.filename}_{timestamp}"
list_id = cl.create_list(list_name, file.filename, contacts)
logger.info(f"Auto-saved campaign contacts as list ID {list_id}: {list_name}")
except Exception as e:
logger.error(f"Failed to save contact list: {e}")
return jsonify({
'success': True,
'total_contacts': len(contacts),
'contacts': contacts,
'preview': preview_contacts,
'message': f'Successfully loaded {len(contacts)} contacts'
'list_id': list_id,
'list_name': list_name,
'message': f'Successfully loaded {len(contacts)} contacts' + (f' and saved as "{list_name}"' if list_name else '')
})
except Exception as e:

View File

@ -17,6 +17,26 @@ def get_lists():
return jsonify({'success': False, 'error': str(e)}), 500
@lists_bp.route('/api/lists', methods=['POST'])
def create_list():
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': 'No data provided'}), 400
name = data.get('name', '')
contacts = data.get('contacts', [])
filename = data.get('filename', 'manual_entry')
if not name or not contacts:
return jsonify({'success': False, 'error': 'Name and contacts are required'}), 400
list_id = model.create_list(name, filename, contacts)
return jsonify({'success': True, 'list_id': list_id, 'message': f'List created with ID {list_id}'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@lists_bp.route('/api/lists/<int:list_id>', methods=['GET'])
def get_list(list_id):
try:

View File

@ -78,6 +78,12 @@ function campaignApp() {
},
_lastLoadedTemplate: '', // Track the last loaded template content
// List management
listUploadName: '',
listUploadPreview: [],
viewingList: null,
viewingListContacts: [],
// Initialization
async init() {
// Start monitoring connection status
@ -151,7 +157,8 @@ function campaignApp() {
async loadSavedLists() {
try {
const response = await fetch('/api/lists');
this.savedLists = await response.json();
const data = await response.json();
this.savedLists = data.success ? data.lists : [];
} catch (error) {
console.error('Failed to load lists:', error);
this.savedLists = []; // Set empty array on error
@ -235,9 +242,10 @@ function campaignApp() {
try {
const response = await fetch(`/api/lists/${listId}`);
const list = await response.json();
const data = await response.json();
if (list && list.contacts) {
if (data.success && data.list && data.list.contacts) {
const list = data.list;
this.uploadedContacts = list.contacts;
this.contactsPreview = list.contacts.slice(0, 10);
this.totalContacts = list.contacts.length;
@ -249,7 +257,7 @@ function campaignApp() {
console.log(`Loaded saved list: ${list.name} with ${this.totalContacts} contacts`);
} else {
alert('Error loading saved list');
alert('Error loading saved list: ' + (data.error || 'Unknown error'));
this.resetContactData();
}
} catch (error) {
@ -533,6 +541,162 @@ function campaignApp() {
}
},
// === LIST MANAGEMENT METHODS ===
async handleListUpload(event) {
const file = event.target.files[0];
if (file) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/csv/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
this.listUploadPreview = data.recipients.slice(0, 10);
// Auto-save the list if no custom name is provided
if (!this.listUploadName.trim()) {
await this.loadSavedLists();
alert(`Contact list saved automatically as "${data.list_name}"`);
this.listUploadPreview = [];
event.target.value = ''; // Clear file input
}
} else {
alert('Error uploading file: ' + data.error);
}
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed: ' + error.message);
}
}
},
async saveListFromPreview() {
if (this.listUploadPreview.length === 0) {
alert('No contacts to save');
return;
}
try {
const listName = this.listUploadName.trim() ||
`Custom_List_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '_')}`;
const response = await fetch('/api/lists', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: listName,
contacts: this.listUploadPreview,
filename: 'custom_upload'
})
});
const data = await response.json();
if (data.success) {
alert(`Contact list saved as "${listName}"`);
this.listUploadPreview = [];
this.listUploadName = '';
await this.loadSavedLists();
} else {
alert('Error saving list: ' + data.error);
}
} catch (error) {
console.error('Error saving list:', error);
alert('Error saving list: ' + error.message);
}
},
async viewListContacts(list) {
try {
const response = await fetch(`/api/lists/${list.id}`);
const data = await response.json();
if (data.success) {
this.viewingList = data.list;
this.viewingListContacts = data.list.contacts || [];
} else {
alert('Error loading list details: ' + data.error);
}
} catch (error) {
console.error('Error loading list details:', error);
alert('Error loading list details: ' + error.message);
}
},
async useListForCampaign(list) {
// Switch to campaigns tab
this.activeTab = 'campaigns';
// Load the list data
this.selectedList = list.id;
await this.loadSavedList(list.id);
alert(`List "${list.name}" is now loaded for your campaign!`);
},
async downloadList(list) {
try {
const response = await fetch(`/api/lists/${list.id}`);
const data = await response.json();
if (data.success && data.list.contacts) {
// Convert to CSV
const contacts = data.list.contacts;
const headers = ['name', 'phone', 'email'];
const csvContent = [
headers.join(','),
...contacts.map(contact =>
headers.map(header =>
(contact[header] || '').toString().replace(/"/g, '""')
).map(field => `"${field}"`).join(',')
)
].join('\n');
// Download
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${list.name}.csv`;
a.click();
window.URL.revokeObjectURL(url);
} else {
alert('Error downloading list: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('Error downloading list:', error);
alert('Error downloading list: ' + error.message);
}
},
async deleteContactList(listId, listName) {
if (!confirm(`Are you sure you want to delete the list "${listName}"?`)) {
return;
}
try {
const response = await fetch(`/api/lists/${listId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
alert('List deleted!');
await this.loadSavedLists();
} else {
alert('Error deleting list: ' + result.error);
}
} catch (error) {
console.error('Error deleting list:', error);
alert('Error deleting list: ' + error.message);
}
},
// === TESTING METHODS ===
async testSMS() {
if (!this.messageTemplate) {
alert('Please enter a message template first');

View File

@ -62,6 +62,11 @@
class="px-4 py-2 rounded-lg font-medium transition-colors">
📝 Templates
</button>
<button @click="activeTab = 'lists'; loadSavedLists()"
:class="activeTab === 'lists' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-colors">
📋 Contact Lists
</button>
<button @click="activeTab = 'testing'"
:class="activeTab === 'testing' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-colors">
@ -158,7 +163,7 @@
<select x-model="selectedList" @change="loadSavedList($event.target.value)" class="w-full px-3 py-2 border rounded-lg">
<option value="">-- Select a saved list --</option>
<template x-for="(list, index) in savedLists" :key="`list-${index}-${list.id || ''}`">
<option :value="list.id" x-text="`${list.name} (${list.count || list.contact_count || 0} contacts)`"></option>
<option :value="list.id" x-text="`${list.name} (${list.total_contacts || 0} contacts)`"></option>
</template>
</select>
</div>
@ -458,6 +463,139 @@
</div>
</div>
</div>
<!-- Contact Lists Tab -->
<div x-show="activeTab === 'lists'" class="p-6">
<div class="max-w-6xl mx-auto">
<h2 class="text-2xl font-bold text-gray-800 mb-6">📋 Contact Lists</h2>
<!-- Upload CSV Section -->
<div class="mb-8 p-6 border rounded-lg bg-green-50">
<h3 class="font-semibold text-gray-700 mb-4">Upload New Contact List</h3>
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-1">CSV File</label>
<input type="file" @change="handleListUpload($event)"
accept=".csv"
class="w-full px-3 py-2 border rounded-lg">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-1">List Name (optional)</label>
<input type="text" x-model="listUploadName"
placeholder="Leave blank for auto-generated name"
class="w-full px-3 py-2 border rounded-lg">
</div>
</div>
<div x-show="listUploadPreview.length > 0" class="mt-4 p-4 bg-white rounded border">
<h4 class="font-medium mb-2">Preview (<span x-text="listUploadPreview.length"></span> contacts)</h4>
<div class="max-h-40 overflow-y-auto space-y-2">
<template x-for="(contact, index) in listUploadPreview" :key="`preview-${index}`">
<div class="text-sm bg-gray-50 p-2 rounded border flex justify-between">
<div>
<span class="font-medium" x-text="contact.name || 'No Name'"></span>
<span class="text-gray-600 ml-2" x-text="contact.phone"></span>
</div>
</div>
</template>
</div>
<button @click="saveListFromPreview()"
class="mt-3 bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
Save List
</button>
</div>
</div>
<!-- Lists Display -->
<div>
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-gray-700">Saved Lists</h3>
<button @click="loadSavedLists()" class="text-blue-600 hover:text-blue-800 text-sm">
🔄 Refresh
</button>
</div>
<div x-show="!savedLists || savedLists.length === 0" class="text-gray-500 text-center py-8 border rounded-lg">
No contact lists saved yet. Upload a CSV file above to get started!
</div>
<div x-show="savedLists && savedLists.length > 0" class="grid grid-cols-1 gap-4">
<template x-for="list in savedLists" :key="list.id">
<div class="border rounded-lg p-4 hover:bg-gray-50 transition-colors">
<div class="flex justify-between items-start mb-3">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<h4 class="font-medium text-gray-800" x-text="list.name"></h4>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
x-text="`${list.total_contacts} contacts`"></span>
</div>
<p class="text-sm text-gray-600" x-text="`From: ${list.original_filename}`"></p>
</div>
</div>
<div class="flex justify-between items-center text-xs text-gray-500 mb-3">
<span>Used <span x-text="list.usage_count || 0"></span> times</span>
<span>Created <span x-text="formatDate(list.created_at)"></span></span>
<span x-show="list.last_used_at">Last used: <span x-text="formatDate(list.last_used_at)"></span></span>
</div>
<div class="flex gap-2">
<button @click="viewListContacts(list)"
class="text-blue-600 hover:text-blue-800 text-sm px-3 py-1 border border-blue-200 rounded hover:bg-blue-50">
👁 View
</button>
<button @click="useListForCampaign(list)"
class="text-green-600 hover:text-green-800 text-sm px-3 py-1 border border-green-200 rounded hover:bg-green-50">
📤 Use for Campaign
</button>
<button @click="downloadList(list)"
class="text-purple-600 hover:text-purple-800 text-sm px-3 py-1 border border-purple-200 rounded hover:bg-purple-50">
💾 Download
</button>
<button @click="deleteContactList(list.id, list.name)"
class="text-red-600 hover:text-red-800 text-sm px-3 py-1 border border-red-200 rounded hover:bg-red-50">
🗑 Delete
</button>
</div>
</div>
</template>
</div>
<!-- List Detail Modal -->
<div x-show="viewingList" x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 max-w-4xl max-h-[80vh] w-full mx-4 overflow-hidden flex flex-col">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">
<span x-text="viewingList ? viewingList.name : ''"></span>
<span class="text-sm font-normal text-gray-600">
(<span x-text="viewingList ? viewingList.total_contacts : 0"></span> contacts)
</span>
</h3>
<button @click="viewingList = null" class="text-gray-500 hover:text-gray-700"></button>
</div>
<div class="flex-1 overflow-y-auto">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<template x-for="(contact, index) in viewingListContacts" :key="`contact-${index}`">
<div class="border rounded p-3 bg-gray-50">
<div class="font-medium" x-text="contact.name || 'No Name'"></div>
<div class="text-gray-600" x-text="contact.phone"></div>
<div x-show="contact.email" class="text-gray-500 text-sm" x-text="contact.email"></div>
</div>
</template>
</div>
</div>
<div class="mt-4 pt-4 border-t flex justify-end gap-2">
<button @click="viewingList = null"
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">
Close
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>