diff --git a/TEMPLATE_FIX_SUMMARY.md b/TEMPLATE_FIX_SUMMARY.md new file mode 100644 index 0000000..a27ab98 --- /dev/null +++ b/TEMPLATE_FIX_SUMMARY.md @@ -0,0 +1,42 @@ +#!/bin/bash +echo "๐ŸŽฏ Template Loading Bug Fix Summary" +echo "==================================" +echo +echo "โœ… ISSUES IDENTIFIED AND FIXED:" +echo +echo "1. DUPLICATE loadTemplate FUNCTIONS" +echo " - Problem: Two loadTemplate functions in dashboard.js (lines 342 & 660)" +echo " - The second function overwrote the first, causing templates to not load" +echo " - Solution: Renamed the simple one to loadLegacyTemplate" +echo +echo "2. CONFLICTING EVENT LISTENERS" +echo " - Problem: Both Alpine.js @change and manual addEventListener on select" +echo " - This caused race conditions and multiple calls to different functions" +echo " - Solution: Removed manual event listener, kept only Alpine.js @change" +echo +echo "3. IMMEDIATE TEMPLATE CLEARING" +echo " - Problem: @input=\"selectedTemplate = ''\" cleared template immediately" +echo " - This could interfere with template loading process" +echo " - Solution: Added intelligent clearing that only clears when user modifies content" +echo +echo "4. DEBUG INTERFERENCE" +echo " - Problem: Debug functions and test code interfering with normal operation" +echo " - Solution: Removed debug functions and test event listeners" +echo +echo "โœ… CURRENT STATE:" +echo " - Only one loadTemplate function (async, handles database templates)" +echo " - Clean event handling via Alpine.js only" +echo " - Intelligent template clearing behavior" +echo " - Simplified, robust code" +echo +echo "๐Ÿงช TESTING STEPS:" +echo "1. Open http://localhost:5000" +echo "2. In Create Campaign section, click 'Use Saved Template' dropdown" +echo "3. Select any template (e.g., 'Volunteer Check-In')" +echo "4. Verify message template field is populated with template content" +echo "5. Verify 'Template loaded successfully' message appears" +echo "6. Test that manual editing clears the selected template" +echo +echo "๐Ÿ”ง FILES MODIFIED:" +echo " - src/static/js/dashboard.js (removed duplicate functions, cleaned up)" +echo " - src/templates/dashboard.html (removed conflicting event listeners)" diff --git a/data/campaign.db b/data/campaign.db index 1cf354c..d4ac453 100644 Binary files a/data/campaign.db and b/data/campaign.db differ diff --git a/docs/instruct.md b/docs/instruct.md index 8c9a3e1..ab79f58 100644 --- a/docs/instruct.md +++ b/docs/instruct.md @@ -682,7 +682,109 @@ const list = await response.json(); } ``` -#### Analytics & Reporting +#### Templates Management + +**GET /api/templates** +```javascript +// Get all message templates +const response = await fetch('/api/templates'); +const templates = await response.json(); + +// Response format: +[ + { + "id": 1, + "name": "Volunteer Check-In", + "content": "Hi {name}! Hope all is well. Are you available this weekend?", + "description": "Check availability for volunteer events", + "category": "volunteer", + "is_favorite": 0, + "times_used": 0, + "created_at": "2025-08-25 16:39:21", + "updated_at": "2025-08-25 16:39:21" + } +] +``` + +**POST /api/templates** +```javascript +// Create new template +const templateData = { + name: "Follow-up Message", + content: "Hi {name}! Following up on our conversation...", + description: "Follow up template", + category: "followup", + is_favorite: 0 +}; + +const response = await fetch('/api/templates', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(templateData) +}); + +// Response: +{ + "success": true, + "template_id": 6, + "message": "Template created successfully" +} +``` + +**GET /api/templates/:id** +```javascript +// Get specific template +const response = await fetch('/api/templates/1'); +const data = await response.json(); + +// Response: +{ + "success": true, + "template": { + "id": 1, + "name": "Volunteer Check-In", + "template": "Hi {name}! Hope all is well. Are you available this weekend?", + "description": "Check availability for volunteer events", + "category": "volunteer", + "usage_count": 3 + } +} +``` + +**PUT /api/templates/:id** +```javascript +// Update template +const updateData = { + name: "Updated Template Name", + content: "Updated message content", + is_favorite: 1 +}; + +const response = await fetch('/api/templates/1', { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(updateData) +}); +``` + +**DELETE /api/templates/:id** +```javascript +// Delete template +const response = await fetch('/api/templates/1', { method: 'DELETE' }); +const result = await response.json(); + +// Response: +{ + "success": true, + "message": "Template deleted successfully" +} +``` + +**POST /api/templates/:id/use** +```javascript +// Mark template as used (increments usage_count) +await fetch('/api/templates/1/use', { method: 'POST' }); +``` **GET /api/analytics** ```javascript diff --git a/src/__pycache__/app.cpython-311.pyc b/src/__pycache__/app.cpython-311.pyc index 76db7ef..caaba71 100644 Binary files a/src/__pycache__/app.cpython-311.pyc and b/src/__pycache__/app.cpython-311.pyc differ diff --git a/src/app.py b/src/app.py index 1b1b6a9..9d999e5 100644 --- a/src/app.py +++ b/src/app.py @@ -643,6 +643,37 @@ def init_db(): ) ''') + # Create message templates table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS message_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + template TEXT NOT NULL, + description TEXT, + category TEXT DEFAULT 'general', + is_favorite INTEGER DEFAULT 0, + usage_count INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Insert default templates if table is empty + cursor.execute("SELECT COUNT(*) FROM message_templates") + if cursor.fetchone()[0] == 0: + default_templates = [ + ("Volunteer Check-In", "Hi {name}! Hope all is well. Are you available this weekend to help with the volunteer event?", "Check availability for volunteer events", "volunteer"), + ("Event Reminder", "Hi {name}! Just a friendly reminder about our upcoming event. Looking forward to seeing you there!", "Remind contacts about upcoming events", "reminder"), + ("Thank You Message", "Hi {name}! Thank you so much for your help and support. It means a lot to us!", "Express gratitude to contacts", "gratitude"), + ("Follow Up", "Hi {name}! Following up on our previous conversation. Let me know if you have any questions!", "Follow up on previous communications", "followup"), + ("General Outreach", "Hi {name}! Hope you're doing well. Wanted to reach out and see how things are going.", "General outreach and connection", "general") + ] + + cursor.executemany( + "INSERT INTO message_templates (name, template, description, category) VALUES (?, ?, ?, ?)", + default_templates + ) + conn.commit() conn.close() @@ -1439,7 +1470,12 @@ def list_campaigns(): @app.route('/api/templates') def get_templates(): """Get message templates""" - templates = query_db("SELECT * FROM templates ORDER BY times_used DESC") + templates = query_db(""" + SELECT id, name, template as content, description, category, + is_favorite, usage_count as times_used, created_at, updated_at + FROM message_templates + ORDER BY is_favorite DESC, usage_count DESC, name ASC + """) return jsonify([dict(t) for t in templates]) @app.route('/api/templates', methods=['POST']) @@ -1447,11 +1483,94 @@ def save_template(): """Save message template""" data = request.json execute_db( - "INSERT INTO templates (name, content, variables) VALUES (?, ?, ?)", - (data.get('name'), data.get('content'), json.dumps(data.get('variables', []))) + """INSERT INTO message_templates (name, template, description, category, is_favorite) + VALUES (?, ?, ?, ?, ?)""", + (data.get('name', ''), data.get('content', ''), data.get('description', ''), + data.get('category', 'general'), data.get('is_favorite', 0)) ) return jsonify({"success": True}) +@app.route('/api/templates/', methods=['GET']) +def get_template_by_id(template_id): + """Get specific template by ID""" + template = query_db( + "SELECT * FROM message_templates WHERE id = ?", + (template_id,), one=True + ) + + if not template: + return jsonify({'success': False, 'error': 'Template not found'}), 404 + + return jsonify({ + 'success': True, + 'template': dict(template) + }) + +@app.route('/api/templates/', methods=['PUT']) +def update_template_by_id(template_id): + """Update existing template""" + data = request.json + + # Check if template exists + template = query_db("SELECT id FROM message_templates WHERE id = ?", (template_id,), one=True) + if not template: + return jsonify({'success': False, 'error': 'Template not found'}), 404 + + # Build dynamic update + fields = [] + values = [] + + if 'name' in data: + fields.append('name = ?') + values.append(data['name']) + if 'content' in data: + fields.append('template = ?') + values.append(data['content']) + if 'description' in data: + fields.append('description = ?') + values.append(data['description']) + if 'category' in data: + fields.append('category = ?') + values.append(data['category']) + if 'is_favorite' in data: + fields.append('is_favorite = ?') + values.append(data['is_favorite']) + + if fields: + fields.append('updated_at = CURRENT_TIMESTAMP') + values.append(template_id) + + execute_db( + f"UPDATE message_templates SET {', '.join(fields)} WHERE id = ?", + values + ) + + return jsonify({'success': True}) + +@app.route('/api/templates/', methods=['DELETE']) +def delete_template_by_id(template_id): + """Delete template""" + # Check if template exists + template = query_db("SELECT name FROM message_templates WHERE id = ?", (template_id,), one=True) + if not template: + return jsonify({'success': False, 'error': 'Template not found'}), 404 + + execute_db("DELETE FROM message_templates WHERE id = ?", (template_id,)) + + return jsonify({ + 'success': True, + 'message': f'Template deleted successfully' + }) + +@app.route('/api/templates//use', methods=['POST']) +def use_template_by_id(template_id): + """Mark template as used (increment usage counter)""" + execute_db( + "UPDATE message_templates SET usage_count = usage_count + 1 WHERE id = ?", + (template_id,) + ) + return jsonify({'success': True}) + @app.route('/api/csv/upload', methods=['POST']) def upload_csv(): """Upload and parse CSV file""" diff --git a/src/static/js/dashboard.js b/src/static/js/dashboard.js index d4a13e6..8972fbc 100644 --- a/src/static/js/dashboard.js +++ b/src/static/js/dashboard.js @@ -65,7 +65,20 @@ function campaignApp() { smsTest: '' }, - // Initialization + // Template management + selectedTemplate: '', + savedTemplates: [], + editingTemplate: null, + templateForm: { + name: '', + content: '', + description: '', + category: 'general', + is_favorite: 0 + }, + _lastLoadedTemplate: '', // Track the last loaded template content + + // Initialization async init() { // Start monitoring connection status await this.checkConnectionStatus(); @@ -74,6 +87,7 @@ function campaignApp() { // Load initial data await this.loadAnalytics(); await this.loadSavedLists(); + await this.loadSavedTemplates(); await this.loadRecentCampaigns(); await this.loadFollowups(); @@ -308,26 +322,217 @@ function campaignApp() { } }, + // Template Management Methods + async loadSavedTemplates() { + try { + const response = await fetch('/api/templates'); + const data = await response.json(); + this.savedTemplates = data || []; + } catch (error) { + console.error('Error loading templates:', error); + this.savedTemplates = []; + } + }, + + async loadTemplate(templateId) { + if (!templateId) { + this.selectedTemplate = ''; + return; + } + + try { + // Convert templateId to number for comparison since select values are strings + const numericTemplateId = parseInt(templateId); + + const template = this.savedTemplates.find(t => t.id === numericTemplateId); + + if (template) { + // Set the selected template ID first + this.selectedTemplate = templateId; + + // Apply template content to message template field + // Handle both 'template' and 'content' fields for consistency + const templateContent = template.template || template.content; + + if (templateContent) { + this.messageTemplate = templateContent; + + // Store the template content for comparison + this._lastLoadedTemplate = templateContent; + + console.log(`โœ… Loaded template: ${template.name}`); + + // Mark template as used (but don't await to avoid blocking UI) + fetch(`/api/templates/${templateId}/use`, { method: 'POST' }) + .catch(error => console.log('Usage tracking failed:', error)); + } else { + console.error('โŒ Template content is empty'); + alert('Template content is empty'); + } + } else { + console.error('โŒ Template not found with ID:', numericTemplateId); + alert(`Template not found with ID: ${numericTemplateId}`); + this.selectedTemplate = ''; + } + } catch (error) { + console.error('โŒ Error loading template:', error); + this.selectedTemplate = ''; + alert('Failed to load template: ' + error.message); + } + }, + + clearTemplate() { + this.selectedTemplate = ''; + this.messageTemplate = ''; + this._lastLoadedTemplate = ''; + }, + + // Check if message template was manually modified + onMessageTemplateChange() { + // Only clear selected template if user manually modified the content + if (this.selectedTemplate && this._lastLoadedTemplate && + this.messageTemplate !== this._lastLoadedTemplate) { + this.selectedTemplate = ''; + this._lastLoadedTemplate = ''; + } + }, + async saveTemplate() { const name = prompt('Template name:'); if (!name) return; + const description = prompt('Template description (optional):') || ''; + const category = prompt('Category (general, volunteer, reminder, gratitude, followup):') || 'general'; + try { - await fetch('/api/templates', { + const response = await fetch('/api/templates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, content: this.messageTemplate, - variables: ['name', 'phone', 'date', 'time'] + description: description, + category: category, + is_favorite: 0 }) }); - alert('Template saved!'); + + const result = await response.json(); + if (result.success) { + alert('Template saved!'); + await this.loadSavedTemplates(); + } else { + alert('Error saving template: ' + result.error); + } } catch (error) { + console.error('Error saving template:', error); alert('Error saving template: ' + error.message); } }, + async saveNewTemplate() { + if (!this.templateForm.name || !this.templateForm.content) { + alert('Please fill in template name and content'); + return; + } + + try { + const url = this.editingTemplate + ? `/api/templates/${this.editingTemplate.id}` + : '/api/templates'; + const method = this.editingTemplate ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.templateForm) + }); + + const result = await response.json(); + if (result.success) { + alert(this.editingTemplate ? 'Template updated!' : 'Template created!'); + await this.loadSavedTemplates(); + this.resetTemplateForm(); + } else { + alert('Error: ' + result.error); + } + } catch (error) { + console.error('Error saving template:', error); + alert('Error saving template: ' + error.message); + } + }, + + loadTemplateForEditing(template) { + this.editingTemplate = template; + this.templateForm = { + name: template.name, + content: template.template || template.content, + description: template.description || '', + category: template.category || 'general', + is_favorite: template.is_favorite || 0 + }; + }, + + cancelEditTemplate() { + this.resetTemplateForm(); + }, + + resetTemplateForm() { + this.editingTemplate = null; + this.templateForm = { + name: '', + content: '', + description: '', + category: 'general', + is_favorite: 0 + }; + }, + + async deleteTemplate(templateId, templateName) { + if (!confirm(`Are you sure you want to delete the template "${templateName}"?`)) { + return; + } + + try { + const response = await fetch(`/api/templates/${templateId}`, { + method: 'DELETE' + }); + const result = await response.json(); + + if (result.success) { + alert('Template deleted!'); + await this.loadSavedTemplates(); + } else { + alert('Error deleting template: ' + result.error); + } + } catch (error) { + console.error('Error deleting template:', error); + alert('Error deleting template: ' + error.message); + } + }, + + async toggleTemplateFavorite(template) { + try { + const response = await fetch(`/api/templates/${template.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + is_favorite: template.is_favorite ? 0 : 1 + }) + }); + + const result = await response.json(); + if (result.success) { + await this.loadSavedTemplates(); + } else { + alert('Error updating template: ' + result.error); + } + } catch (error) { + console.error('Error updating template:', error); + alert('Error updating template: ' + error.message); + } + }, + async testSMS() { if (!this.messageTemplate) { alert('Please enter a message template first'); @@ -442,8 +647,8 @@ function campaignApp() { return new Date(dateStr).toLocaleTimeString(); }, - // Load template helpers - loadTemplate(type) { + // Legacy template helper for backward compatibility (if needed) + loadLegacyTemplate(type) { const templates = { initial: "Hi {name}! Hope all is well. I am wondering if you got my last email with the next couple weeks canvassing shifts? We are out every day and would love to have you join us. It is last minute however we are also out today, 5 - 8 PM starting at the Old Strathcona Community League. If you can make it, please let me know. Cheers!", followup: "Hi {name}, just following up on my previous message about volunteering. We have several shifts available this week and would love to have you join us. When works best for you?", diff --git a/src/templates/dashboard.html b/src/templates/dashboard.html index 4721c0b..34492ff 100644 --- a/src/templates/dashboard.html +++ b/src/templates/dashboard.html @@ -10,7 +10,7 @@ -
+
@@ -57,6 +57,11 @@ class="px-4 py-2 rounded-lg font-medium transition-colors"> ๐Ÿ’ฌ Conversations +
+
+ + +
+ โœ“ Template loaded successfully + +
+
+
+
+ Preview: + +
@@ -322,13 +352,119 @@
+ + +
+
+

๐Ÿ“ Message Templates

+ + +
+

+ Create New Template + Edit Template +

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

Saved Templates

+
+ No templates saved yet. Create one above to get started! +
+
+ +
+
+
+
- - - + + +