16 KiB
Campaigns App Improvement Specification
Current State Analysis
The Campaigns app (botui/ui/suite/campaigns/campaigns.html) provides multi-channel marketing with:
- Campaign grid view with status badges
- Channel filters (Email, WhatsApp, Social)
- Lists and Templates tabs
- Basic campaign creation modal
- Metrics display (sent, opened, clicked)
Critical Issues to Fix
1. Missing Backend Integration
Problem: All data loading endpoints return empty or placeholder data
Fix Required:
GET /api/crm/campaigns - Returns campaign list
GET /api/crm/lists - Returns contact lists
GET /api/crm/templates - Returns message templates
POST /api/crm/campaigns - Creates new campaign
Expected Response Format:
{
"campaigns": [
{
"id": "uuid",
"name": "Welcome Series",
"status": "running",
"channels": ["email", "whatsapp"],
"metrics": {
"sent": 1250,
"opened": 890,
"clicked": 234
},
"budget": 500.00,
"scheduled_at": "2026-03-20T10:00:00Z"
}
]
}
2. Campaign Card Template Missing
Problem: Backend returns JSON but frontend expects HTML
Fix: Backend must return HTML fragments:
<div class="campaign-card" data-id="{{id}}">
<div class="campaign-card-header">
<h3 class="campaign-card-title">{{name}}</h3>
<span class="campaign-status {{status}}">{{status}}</span>
</div>
<div class="campaign-channels">
{{#each channels}}
<span class="campaign-channel-tag">
{{icon}} {{name}}
</span>
{{/each}}
</div>
<div class="campaign-metrics">
<div class="campaign-metric">
<span class="campaign-metric-value">{{metrics.sent}}</span>
<span class="campaign-metric-label">Sent</span>
</div>
<div class="campaign-metric">
<span class="campaign-metric-value">{{metrics.opened}}</span>
<span class="campaign-metric-label">Opened</span>
</div>
<div class="campaign-metric">
<span class="campaign-metric-value">{{metrics.clicked}}</span>
<span class="campaign-metric-label">Clicked</span>
</div>
</div>
<div class="campaign-actions">
<button class="campaign-action-btn" onclick="viewCampaign('{{id}}')">View</button>
<button class="campaign-action-btn" onclick="editCampaign('{{id}}')">Edit</button>
<button class="campaign-action-btn primary" onclick="launchCampaign('{{id}}')">Launch</button>
</div>
</div>
3. Form Submission Not Working
Problem: Modal form uses HTMX but doesn't refresh list after creation
Fix:
document.getElementById('campaign-form').addEventListener('htmx:afterRequest', function(e) {
if (e.detail.successful) {
hideCampaignModal();
// Refresh campaigns list
htmx.ajax('GET', '/api/crm/campaigns', {
target: '#campaignsList',
swap: 'innerHTML'
});
} else {
// Show error message
const error = e.detail.xhr.responseText;
showError(error);
}
});
4. Channel Filter Not Working
Problem: Filter function checks text content instead of data attributes
Fix:
<!-- Add data attribute to cards -->
<div class="campaign-card" data-channels="email,whatsapp">
<!-- Update filter function -->
<script>
function filterCampaigns(channel) {
const cards = document.querySelectorAll('.campaign-card');
cards.forEach(card => {
if (channel === 'all') {
card.style.display = '';
} else {
const channels = card.dataset.channels.split(',');
card.style.display = channels.includes(channel) ? '' : 'none';
}
});
}
</script>
Major Improvements Needed
5. Campaign Builder Wizard
What: Multi-step campaign creation with preview
Implementation:
<div id="campaign-wizard" class="wizard">
<!-- Step 1: Basic Info -->
<div class="wizard-step active" data-step="1">
<h3>Campaign Details</h3>
<input type="text" name="name" placeholder="Campaign Name" required>
<select name="channel" required>
<option value="email">Email</option>
<option value="whatsapp">WhatsApp</option>
<option value="multi">Multi-Channel</option>
</select>
<button onclick="nextStep()">Next</button>
</div>
<!-- Step 2: Audience -->
<div class="wizard-step" data-step="2">
<h3>Select Audience</h3>
<div class="list-selector">
<!-- Load contact lists -->
<div hx-get="/api/crm/lists" hx-trigger="load"></div>
</div>
<button onclick="prevStep()">Back</button>
<button onclick="nextStep()">Next</button>
</div>
<!-- Step 3: Content -->
<div class="wizard-step" data-step="3">
<h3>Create Message</h3>
<select name="template" onchange="loadTemplate(this.value)">
<option value="">Start from scratch</option>
<!-- Load templates -->
</select>
<textarea name="message" rows="10"></textarea>
<button onclick="prevStep()">Back</button>
<button onclick="nextStep()">Next</button>
</div>
<!-- Step 4: Schedule -->
<div class="wizard-step" data-step="4">
<h3>Schedule Campaign</h3>
<label>
<input type="radio" name="schedule_type" value="now" checked>
Send immediately
</label>
<label>
<input type="radio" name="schedule_type" value="later">
Schedule for later
</label>
<input type="datetime-local" name="scheduled_at" id="schedule-datetime">
<button onclick="prevStep()">Back</button>
<button onclick="nextStep()">Next</button>
</div>
<!-- Step 5: Review -->
<div class="wizard-step" data-step="5">
<h3>Review & Launch</h3>
<div class="campaign-preview">
<!-- Show summary of all settings -->
</div>
<button onclick="prevStep()">Back</button>
<button onclick="launchCampaign()" class="btn-primary">Launch Campaign</button>
</div>
</div>
<script>
let currentStep = 1;
function nextStep() {
if (validateStep(currentStep)) {
document.querySelector(`[data-step="${currentStep}"]`).classList.remove('active');
currentStep++;
document.querySelector(`[data-step="${currentStep}"]`).classList.add('active');
}
}
function prevStep() {
document.querySelector(`[data-step="${currentStep}"]`).classList.remove('active');
currentStep--;
document.querySelector(`[data-step="${currentStep}"]`).classList.add('active');
}
function validateStep(step) {
const stepEl = document.querySelector(`[data-step="${step}"]`);
const inputs = stepEl.querySelectorAll('[required]');
for (let input of inputs) {
if (!input.value) {
input.focus();
return false;
}
}
return true;
}
</script>
6. Contact List Management
What: Create and manage segmented contact lists
Implementation:
<div id="list-modal" class="modal">
<form id="list-form">
<h2>Create Contact List</h2>
<div class="form-group">
<label>List Name</label>
<input type="text" name="name" required>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" rows="3"></textarea>
</div>
<div class="form-group">
<label>Import Contacts</label>
<div class="import-options">
<button type="button" onclick="showCsvImport()">
📄 Upload CSV
</button>
<button type="button" onclick="showCrmImport()">
👥 Import from CRM
</button>
<button type="button" onclick="showManualAdd()">
➕ Add Manually
</button>
</div>
</div>
<div id="import-area" class="import-area">
<!-- Dynamic import UI -->
</div>
<div class="form-actions">
<button type="button" onclick="hideListModal()">Cancel</button>
<button type="submit">Create List</button>
</div>
</form>
</div>
<!-- CSV Import -->
<div id="csv-import" style="display: none;">
<input type="file" accept=".csv" onchange="handleCsvUpload(this)">
<div class="csv-preview">
<table id="csv-preview-table"></table>
</div>
<div class="csv-mapping">
<label>Map columns:</label>
<select name="name_column"></select>
<select name="email_column"></select>
<select name="phone_column"></select>
</div>
</div>
7. Message Template Editor
What: Rich template editor with variables and preview
Implementation:
<div id="template-editor" class="template-editor">
<div class="editor-toolbar">
<button onclick="insertVariable('{{name}}')">Name</button>
<button onclick="insertVariable('{{company}}')">Company</button>
<button onclick="insertVariable('{{custom_field}}')">Custom Field</button>
<button onclick="toggleBold()"><strong>B</strong></button>
<button onclick="toggleItalic()"><em>I</em></button>
<button onclick="insertLink()">🔗</button>
<button onclick="insertImage()">🖼️</button>
</div>
<div class="editor-content">
<textarea id="template-content" name="content"></textarea>
</div>
<div class="editor-preview">
<h4>Preview</h4>
<div id="template-preview"></div>
</div>
</div>
<script>
function insertVariable(variable) {
const textarea = document.getElementById('template-content');
const pos = textarea.selectionStart;
const text = textarea.value;
textarea.value = text.slice(0, pos) + variable + text.slice(pos);
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = pos + variable.length;
updatePreview();
}
function updatePreview() {
const content = document.getElementById('template-content').value;
const preview = document.getElementById('template-preview');
// Replace variables with sample data
const sampleData = {
name: 'John Doe',
company: 'Acme Corp',
custom_field: 'Sample Value'
};
let rendered = content;
for (let [key, value] of Object.entries(sampleData)) {
rendered = rendered.replace(new RegExp(`{{${key}}}`, 'g'), value);
}
preview.innerHTML = rendered;
}
document.getElementById('template-content').addEventListener('input', updatePreview);
</script>
8. Campaign Analytics Dashboard
What: Detailed metrics and performance tracking
Implementation:
<div id="campaign-analytics" class="analytics-dashboard">
<div class="analytics-header">
<h2>Campaign Performance</h2>
<select id="analytics-timeframe" onchange="loadAnalytics()">
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="all">All time</option>
</select>
</div>
<div class="analytics-grid">
<!-- Key Metrics -->
<div class="metric-card">
<span class="metric-label">Total Sent</span>
<span class="metric-value" hx-get="/api/crm/analytics/sent" hx-trigger="load">0</span>
<span class="metric-change positive">+12%</span>
</div>
<div class="metric-card">
<span class="metric-label">Open Rate</span>
<span class="metric-value" hx-get="/api/crm/analytics/open-rate" hx-trigger="load">0%</span>
<span class="metric-change positive">+5%</span>
</div>
<div class="metric-card">
<span class="metric-label">Click Rate</span>
<span class="metric-value" hx-get="/api/crm/analytics/click-rate" hx-trigger="load">0%</span>
<span class="metric-change negative">-2%</span>
</div>
<div class="metric-card">
<span class="metric-label">Conversion Rate</span>
<span class="metric-value" hx-get="/api/crm/analytics/conversion-rate" hx-trigger="load">0%</span>
<span class="metric-change positive">+8%</span>
</div>
</div>
<!-- Charts -->
<div class="analytics-charts">
<div class="chart-container">
<h3>Campaign Performance Over Time</h3>
<canvas id="performance-chart"></canvas>
</div>
<div class="chart-container">
<h3>Channel Breakdown</h3>
<canvas id="channel-chart"></canvas>
</div>
</div>
<!-- Top Campaigns -->
<div class="top-campaigns">
<h3>Top Performing Campaigns</h3>
<table class="analytics-table">
<thead>
<tr>
<th>Campaign</th>
<th>Channel</th>
<th>Sent</th>
<th>Opened</th>
<th>Clicked</th>
<th>Converted</th>
</tr>
</thead>
<tbody hx-get="/api/crm/analytics/top-campaigns" hx-trigger="load">
</tbody>
</table>
</div>
</div>
9. A/B Testing
What: Test different message variants
Implementation:
<div class="ab-test-setup">
<h3>A/B Test Setup</h3>
<div class="variant-container">
<div class="variant">
<h4>Variant A (50%)</h4>
<textarea name="variant_a_content"></textarea>
<input type="range" name="variant_a_percentage" min="0" max="100" value="50">
</div>
<div class="variant">
<h4>Variant B (50%)</h4>
<textarea name="variant_b_content"></textarea>
<input type="range" name="variant_b_percentage" min="0" max="100" value="50">
</div>
</div>
<div class="test-settings">
<label>Test Duration</label>
<input type="number" name="test_duration" value="24"> hours
<label>Success Metric</label>
<select name="success_metric">
<option value="open_rate">Open Rate</option>
<option value="click_rate">Click Rate</option>
<option value="conversion_rate">Conversion Rate</option>
</select>
<label>
<input type="checkbox" name="auto_select_winner">
Automatically send winning variant to remaining contacts
</label>
</div>
</div>
10. WhatsApp Integration
What: Send WhatsApp campaigns with media support
Implementation:
<div class="whatsapp-composer">
<div class="message-preview">
<div class="whatsapp-bubble">
<div class="message-content" id="whatsapp-preview">
<!-- Preview of message -->
</div>
</div>
</div>
<div class="message-editor">
<textarea name="message" placeholder="Type your message..."
oninput="updateWhatsAppPreview()"></textarea>
<div class="media-attachments">
<button onclick="attachImage()">📷 Image</button>
<button onclick="attachVideo()">🎥 Video</button>
<button onclick="attachDocument()">📄 Document</button>
</div>
<div class="quick-replies">
<h4>Quick Reply Buttons</h4>
<div id="quick-replies-list">
<input type="text" placeholder="Button 1" name="quick_reply_1">
<input type="text" placeholder="Button 2" name="quick_reply_2">
<input type="text" placeholder="Button 3" name="quick_reply_3">
</div>
</div>
</div>
</div>