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.
18
src/app.py
18
src/app.py
@ -1685,12 +1685,28 @@ def upload_campaign_csv():
|
|||||||
if not contacts:
|
if not contacts:
|
||||||
return jsonify({'success': False, 'error': 'No valid contacts found in CSV'}), 400
|
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({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'total_contacts': len(contacts),
|
'total_contacts': len(contacts),
|
||||||
'contacts': contacts,
|
'contacts': contacts,
|
||||||
'preview': preview_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:
|
except Exception as e:
|
||||||
|
|||||||
Binary file not shown.
@ -17,6 +17,26 @@ def get_lists():
|
|||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
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'])
|
@lists_bp.route('/api/lists/<int:list_id>', methods=['GET'])
|
||||||
def get_list(list_id):
|
def get_list(list_id):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -78,6 +78,12 @@ function campaignApp() {
|
|||||||
},
|
},
|
||||||
_lastLoadedTemplate: '', // Track the last loaded template content
|
_lastLoadedTemplate: '', // Track the last loaded template content
|
||||||
|
|
||||||
|
// List management
|
||||||
|
listUploadName: '',
|
||||||
|
listUploadPreview: [],
|
||||||
|
viewingList: null,
|
||||||
|
viewingListContacts: [],
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
async init() {
|
async init() {
|
||||||
// Start monitoring connection status
|
// Start monitoring connection status
|
||||||
@ -151,7 +157,8 @@ function campaignApp() {
|
|||||||
async loadSavedLists() {
|
async loadSavedLists() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/lists');
|
const response = await fetch('/api/lists');
|
||||||
this.savedLists = await response.json();
|
const data = await response.json();
|
||||||
|
this.savedLists = data.success ? data.lists : [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load lists:', error);
|
console.error('Failed to load lists:', error);
|
||||||
this.savedLists = []; // Set empty array on error
|
this.savedLists = []; // Set empty array on error
|
||||||
@ -235,9 +242,10 @@ function campaignApp() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/lists/${listId}`);
|
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.uploadedContacts = list.contacts;
|
||||||
this.contactsPreview = list.contacts.slice(0, 10);
|
this.contactsPreview = list.contacts.slice(0, 10);
|
||||||
this.totalContacts = list.contacts.length;
|
this.totalContacts = list.contacts.length;
|
||||||
@ -249,7 +257,7 @@ function campaignApp() {
|
|||||||
|
|
||||||
console.log(`Loaded saved list: ${list.name} with ${this.totalContacts} contacts`);
|
console.log(`Loaded saved list: ${list.name} with ${this.totalContacts} contacts`);
|
||||||
} else {
|
} else {
|
||||||
alert('Error loading saved list');
|
alert('Error loading saved list: ' + (data.error || 'Unknown error'));
|
||||||
this.resetContactData();
|
this.resetContactData();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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() {
|
async testSMS() {
|
||||||
if (!this.messageTemplate) {
|
if (!this.messageTemplate) {
|
||||||
alert('Please enter a message template first');
|
alert('Please enter a message template first');
|
||||||
|
|||||||
@ -62,6 +62,11 @@
|
|||||||
class="px-4 py-2 rounded-lg font-medium transition-colors">
|
class="px-4 py-2 rounded-lg font-medium transition-colors">
|
||||||
📝 Templates
|
📝 Templates
|
||||||
</button>
|
</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'"
|
<button @click="activeTab = 'testing'"
|
||||||
:class="activeTab === 'testing' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
|
: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">
|
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">
|
<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>
|
<option value="">-- Select a saved list --</option>
|
||||||
<template x-for="(list, index) in savedLists" :key="`list-${index}-${list.id || ''}`">
|
<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>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -458,6 +463,139 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user