All checks were successful
BotUI CI / build (push) Successful in 5m13s
358 lines
15 KiB
HTML
358 lines
15 KiB
HTML
<!-- CRM - Customer Relationship Management -->
|
|
<!-- Dynamics nomenclature: Lead → Opportunity → Account/Contact -->
|
|
|
|
<link rel="stylesheet" href="/suite/crm/crm.css">
|
|
<script src="/suite/js/vendor/htmx.min.js"></script>
|
|
<script src="/suite/js/vendor/htmx-json-enc.js"></script>
|
|
<script src="/suite/js/security-bootstrap.js"></script>
|
|
|
|
<div class="crm-container">
|
|
<!-- Header -->
|
|
<header class="crm-header">
|
|
<div class="crm-header-left">
|
|
<h1 data-i18n="crm-title">CRM</h1>
|
|
<nav class="crm-tabs">
|
|
<button class="crm-tab active" data-view="pipeline" data-i18n="crm-pipeline">Pipeline</button>
|
|
<button class="crm-tab" data-view="deals" data-i18n="crm-deals">Deals</button>
|
|
<button class="crm-tab" data-view="accounts" data-i18n="crm-accounts">Accounts</button>
|
|
<button class="crm-tab" data-view="contacts" data-i18n="crm-contacts">Contacts</button>
|
|
<button class="crm-tab" data-view="campaigns" data-i18n="crm-campaigns">Campaigns</button>
|
|
</nav>
|
|
</div>
|
|
<div class="crm-header-right">
|
|
<div class="crm-search">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
|
</svg>
|
|
<input type="text"
|
|
placeholder="Search deals, accounts..."
|
|
data-i18n-placeholder="crm-search-placeholder-deals"
|
|
hx-get="/api/ui/crm/search"
|
|
hx-trigger="keyup changed delay:300ms"
|
|
hx-target="#crm-search-results">
|
|
</div>
|
|
<button class="btn-primary" id="crm-new-btn">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
<span data-i18n="crm-new">New</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Search Results Dropdown -->
|
|
<div id="crm-search-results" class="crm-search-results"></div>
|
|
|
|
<!-- Pipeline View (Default) -->
|
|
<div id="crm-pipeline-view" class="crm-view active">
|
|
<div class="pipeline-container">
|
|
<!-- Lead Stage -->
|
|
<div class="pipeline-column" data-stage="new">
|
|
<div class="pipeline-header">
|
|
<span class="pipeline-title" data-i18n="crm-stage-lead">Lead</span>
|
|
<span class="pipeline-count" hx-get="/api/ui/crm/count?stage=new" hx-trigger="load">0</span>
|
|
</div>
|
|
<div class="pipeline-cards"
|
|
hx-get="/api/ui/crm/pipeline?stage=new"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
<!-- Lead cards loaded via HTMX -->
|
|
</div>
|
|
<button class="pipeline-add" hx-get="/suite/crm/partials/lead-form.html" hx-target="#crm-modal-content" hx-on::after-request="openCrmModal()">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
<span data-i18n="crm-add-lead">Add Lead</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Qualified Stage -->
|
|
<div class="pipeline-column" data-stage="qualified">
|
|
<div class="pipeline-header">
|
|
<span class="pipeline-title" data-i18n="crm-stage-qualified">Qualified</span>
|
|
<span class="pipeline-count" hx-get="/api/crm/count?stage=qualified" hx-trigger="load">0</span>
|
|
</div>
|
|
<div class="pipeline-cards"
|
|
hx-get="/api/crm/pipeline?stage=qualified"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Proposal Stage -->
|
|
<div class="pipeline-column" data-stage="proposal">
|
|
<div class="pipeline-header">
|
|
<span class="pipeline-title" data-i18n="crm-stage-proposal">Proposal</span>
|
|
<span class="pipeline-count" hx-get="/api/crm/count?stage=proposal" hx-trigger="load">0</span>
|
|
</div>
|
|
<div class="pipeline-cards"
|
|
hx-get="/api/crm/pipeline?stage=proposal"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Negotiation Stage -->
|
|
<div class="pipeline-column" data-stage="negotiation">
|
|
<div class="pipeline-header">
|
|
<span class="pipeline-title" data-i18n="crm-stage-negotiation">Negotiation</span>
|
|
<span class="pipeline-count" hx-get="/api/crm/count?stage=negotiation" hx-trigger="load">0</span>
|
|
</div>
|
|
<div class="pipeline-cards"
|
|
hx-get="/api/crm/pipeline?stage=negotiation"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Won Stage -->
|
|
<div class="pipeline-column won" data-stage="won">
|
|
<div class="pipeline-header">
|
|
<span class="pipeline-title" data-i18n="crm-stage-won">Won</span>
|
|
<span class="pipeline-count" hx-get="/api/crm/count?stage=won" hx-trigger="load">0</span>
|
|
</div>
|
|
<div class="pipeline-cards"
|
|
hx-get="/api/crm/pipeline?stage=won"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lost Stage -->
|
|
<div class="pipeline-column lost" data-stage="lost">
|
|
<div class="pipeline-header">
|
|
<span class="pipeline-title" data-i18n="crm-stage-lost">Lost</span>
|
|
<span class="pipeline-count" hx-get="/api/crm/count?stage=lost" hx-trigger="load">0</span>
|
|
</div>
|
|
<div class="pipeline-cards"
|
|
hx-get="/api/crm/pipeline?stage=lost"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pipeline Summary -->
|
|
<div class="pipeline-summary">
|
|
<div class="summary-card">
|
|
<span class="summary-label" data-i18n="crm-total-value">Total Pipeline Value</span>
|
|
<span class="summary-value" hx-get="/api/ui/crm/stats/pipeline-value" hx-trigger="load">$0</span>
|
|
</div>
|
|
<div class="summary-card">
|
|
<span class="summary-label" data-i18n="crm-conversion-rate">Conversion Rate</span>
|
|
<span class="summary-value" hx-get="/api/ui/crm/stats/conversion-rate" hx-trigger="load">0%</span>
|
|
</div>
|
|
<div class="summary-card">
|
|
<span class="summary-label" data-i18n="crm-avg-deal">Avg Deal Size</span>
|
|
<span class="summary-value" hx-get="/api/ui/crm/stats/avg-deal" hx-trigger="load">$0</span>
|
|
</div>
|
|
<div class="summary-card">
|
|
<span class="summary-label" data-i18n="crm-this-month">Won This Month</span>
|
|
<span class="summary-value success" hx-get="/api/ui/crm/stats/won-month" hx-trigger="load">$0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Deals List View -->
|
|
<div id="crm-deals-view" class="crm-view">
|
|
<div class="crm-list-header">
|
|
<div class="list-filters">
|
|
<select hx-get="/api/crm/deals" hx-trigger="change" hx-target="#deals-table-body" hx-include="this">
|
|
<option value="all" data-i18n="crm-filter-all">All Deals</option>
|
|
<option value="new" data-i18n="crm-filter-new">New</option>
|
|
<option value="qualified" data-i18n="crm-filter-qualified">Qualified</option>
|
|
<option value="proposal" data-i18n="crm-filter-proposal">Proposal</option>
|
|
<option value="negotiation" data-i18n="crm-filter-negotiation">Negotiation</option>
|
|
<option value="won" data-i18n="crm-filter-won">Won</option>
|
|
<option value="lost" data-i18n="crm-filter-lost">Lost</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<table class="crm-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-i18n="crm-col-title">Title</th>
|
|
<th data-i18n="crm-col-value">Value</th>
|
|
<th data-i18n="crm-col-stage">Stage</th>
|
|
<th data-i18n="crm-col-contact">Contact</th>
|
|
<th data-i18n="crm-col-account">Account</th>
|
|
<th data-i18n="crm-col-probability">Probability</th>
|
|
<th data-i18n="crm-col-close-date">Expected Close</th>
|
|
<th data-i18n="crm-col-owner">Owner</th>
|
|
<th data-i18n="crm-col-actions">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="deals-table-body" hx-get="/api/ui/crm/deals" hx-trigger="load">
|
|
<!-- Deals loaded via HTMX -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Accounts List View -->
|
|
<div id="crm-accounts-view" class="crm-view">
|
|
<table class="crm-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-i18n="crm-col-account">Account</th>
|
|
<th data-i18n="crm-col-industry">Industry</th>
|
|
<th data-i18n="crm-col-phone">Phone</th>
|
|
<th data-i18n="crm-col-city">City</th>
|
|
<th data-i18n="crm-col-revenue">Annual Revenue</th>
|
|
<th data-i18n="crm-col-contacts">Contacts</th>
|
|
<th data-i18n="crm-col-actions">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="accounts-table-body" hx-get="/api/ui/crm/accounts" hx-trigger="load">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Contacts List View -->
|
|
<div id="crm-contacts-view" class="crm-view">
|
|
<table class="crm-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-i18n="crm-col-name">Name</th>
|
|
<th data-i18n="crm-col-account">Account</th>
|
|
<th data-i18n="crm-col-title">Title</th>
|
|
<th data-i18n="crm-col-email">Email</th>
|
|
<th data-i18n="crm-col-phone">Phone</th>
|
|
<th data-i18n="crm-col-actions">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="contacts-table-body" hx-get="/api/ui/crm/contacts" hx-trigger="load">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Campaigns View -->
|
|
<div id="crm-campaigns-view" class="crm-view">
|
|
<div class="campaigns-grid" id="crmCampaignsList"
|
|
hx-get="/api/crm/campaigns"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
<div style="grid-column: 1 / -1; padding: 40px; text-align: center; color: var(--text-secondary);">
|
|
Loading campaigns...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal for forms -->
|
|
<div id="crm-modal" class="crm-modal">
|
|
<div class="crm-modal-backdrop" onclick="closeCrmModal()"></div>
|
|
<div class="crm-modal-content" id="crm-modal-content">
|
|
<!-- Form content loaded via HTMX -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail Panel -->
|
|
<div id="detail-panel" class="crm-detail-panel"></div>
|
|
|
|
<script>
|
|
(function() {
|
|
// Tab switching
|
|
document.querySelectorAll('.crm-tab').forEach(tab => {
|
|
tab.addEventListener('click', function() {
|
|
document.querySelectorAll('.crm-tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.crm-view').forEach(v => v.classList.remove('active'));
|
|
this.classList.add('active');
|
|
const view = this.dataset.view;
|
|
document.getElementById(`crm-${view}-view`).classList.add('active');
|
|
});
|
|
});
|
|
|
|
// New button dropdown
|
|
const newBtn = document.getElementById('crm-new-btn');
|
|
newBtn.addEventListener('click', function() {
|
|
// Default: open lead form
|
|
htmx.ajax('GET', '/suite/crm/partials/lead-form.html', '#crm-modal-content').then(() => {
|
|
openCrmModal();
|
|
});
|
|
});
|
|
|
|
// Modal functions
|
|
window.openCrmModal = function() {
|
|
document.getElementById('crm-modal').classList.add('open');
|
|
};
|
|
|
|
window.closeCrmModal = function() {
|
|
document.getElementById('crm-modal').classList.remove('open');
|
|
};
|
|
|
|
// Drag and drop for pipeline
|
|
const pipelineCards = document.querySelectorAll('.pipeline-cards');
|
|
pipelineCards.forEach(column => {
|
|
column.addEventListener('dragover', e => {
|
|
e.preventDefault();
|
|
column.classList.add('drag-over');
|
|
});
|
|
|
|
column.addEventListener('dragleave', () => {
|
|
column.classList.remove('drag-over');
|
|
});
|
|
|
|
column.addEventListener('drop', e => {
|
|
e.preventDefault();
|
|
column.classList.remove('drag-over');
|
|
const cardId = e.dataTransfer.getData('text/plain');
|
|
const newStage = column.closest('.pipeline-column').dataset.stage;
|
|
|
|
// Update via HTMX
|
|
htmx.ajax('POST', `/api/crm/opportunity/${cardId}/stage`, {
|
|
values: { stage: newStage }
|
|
});
|
|
});
|
|
});
|
|
|
|
// Initialize i18n if available
|
|
if (window.i18n && window.i18n.translatePage) {
|
|
window.i18n.translatePage();
|
|
}
|
|
|
|
// Submit lead form manually to ensure JSON is sent correctly
|
|
window.submitLeadForm = async function(event) {
|
|
const form = document.getElementById('leadForm');
|
|
const formData = new FormData(form);
|
|
const data = {};
|
|
const numericFields = ['value'];
|
|
|
|
for (let [key, value] of formData.entries()) {
|
|
if (numericFields.includes(key)) {
|
|
// Convert to number or null
|
|
data[key] = value ? parseFloat(value) : null;
|
|
} else if (value) {
|
|
// Only include non-empty strings
|
|
data[key] = value;
|
|
}
|
|
}
|
|
|
|
console.log('Submitting lead data:', data);
|
|
|
|
const token = localStorage.getItem('gb_token');
|
|
|
|
try {
|
|
const response = await fetch('/api/crm/leads', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + token
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (response.ok) {
|
|
closeCrmModal();
|
|
htmx.ajax('GET', '/api/ui/crm/pipeline?stage=new', {
|
|
target: '#crm-pipeline-view',
|
|
swap: 'innerHTML'
|
|
});
|
|
} else {
|
|
console.error('Lead creation failed:', response.status, await response.text());
|
|
}
|
|
} catch (err) {
|
|
console.error('Error creating lead:', err);
|
|
}
|
|
};
|
|
})();
|
|
</script>
|