Small bug fixes
This commit is contained in:
parent
10f080fd13
commit
5795ec737b
BIN
data/campaign.db
BIN
data/campaign.db
Binary file not shown.
Binary file not shown.
20
src/app.py
20
src/app.py
@ -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:
|
||||
|
||||
Binary file not shown.
@ -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:
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user