feat(prospecting): add complete prospecting module for lead discovery and scoring
Some checks failed
Some checks failed
Migrates scanning pipeline from marketing-.lu-domains app into Orion module. Supports digital (domain scan) and offline (manual capture) lead channels with enrichment, scoring, campaign management, and interaction tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
149
app/modules/prospecting/static/admin/js/prospects.js
Normal file
149
app/modules/prospecting/static/admin/js/prospects.js
Normal file
@@ -0,0 +1,149 @@
|
||||
// static/admin/js/prospects.js
|
||||
|
||||
const prospectsLog = window.LogConfig.createLogger('prospecting-prospects');
|
||||
|
||||
function prospectsList() {
|
||||
return {
|
||||
...data(),
|
||||
|
||||
currentPage: 'prospects',
|
||||
|
||||
prospects: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
|
||||
// Filters
|
||||
search: '',
|
||||
filterChannel: '',
|
||||
filterStatus: '',
|
||||
filterTier: '',
|
||||
|
||||
// Pagination
|
||||
pagination: { page: 1, per_page: 20, total: 0, pages: 0 },
|
||||
|
||||
// Create modal
|
||||
showCreateModal: false,
|
||||
creating: false,
|
||||
newProspect: {
|
||||
channel: 'digital',
|
||||
domain_name: '',
|
||||
business_name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
city: '',
|
||||
source: 'street',
|
||||
notes: '',
|
||||
},
|
||||
|
||||
async init() {
|
||||
await I18n.loadModule('prospecting');
|
||||
|
||||
if (window._prospectsListInit) return;
|
||||
window._prospectsListInit = true;
|
||||
|
||||
if (window.PlatformSettings) {
|
||||
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
||||
}
|
||||
|
||||
prospectsLog.info('Prospects list initializing');
|
||||
await this.loadProspects();
|
||||
},
|
||||
|
||||
async loadProspects() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: this.pagination.page,
|
||||
per_page: this.pagination.per_page,
|
||||
});
|
||||
if (this.search) params.set('search', this.search);
|
||||
if (this.filterChannel) params.set('channel', this.filterChannel);
|
||||
if (this.filterStatus) params.set('status', this.filterStatus);
|
||||
if (this.filterTier) params.set('tier', this.filterTier);
|
||||
|
||||
const response = await apiClient.get('/admin/prospecting/prospects?' + params);
|
||||
this.prospects = response.items || [];
|
||||
this.pagination.total = response.total || 0;
|
||||
this.pagination.pages = response.pages || 0;
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
prospectsLog.error('Failed to load prospects', err);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async createProspect() {
|
||||
this.creating = true;
|
||||
try {
|
||||
const payload = { ...this.newProspect };
|
||||
if (payload.channel === 'digital') {
|
||||
delete payload.business_name;
|
||||
delete payload.phone;
|
||||
delete payload.email;
|
||||
delete payload.city;
|
||||
} else {
|
||||
delete payload.domain_name;
|
||||
}
|
||||
await apiClient.post('/admin/prospecting/prospects', payload);
|
||||
Utils.showToast('Prospect created', 'success');
|
||||
this.showCreateModal = false;
|
||||
this.resetNewProspect();
|
||||
await this.loadProspects();
|
||||
} catch (err) {
|
||||
Utils.showToast('Failed: ' + err.message, 'error');
|
||||
} finally {
|
||||
this.creating = false;
|
||||
}
|
||||
},
|
||||
|
||||
resetNewProspect() {
|
||||
this.newProspect = {
|
||||
channel: 'digital',
|
||||
domain_name: '',
|
||||
business_name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
city: '',
|
||||
source: 'street',
|
||||
notes: '',
|
||||
};
|
||||
},
|
||||
|
||||
goToPage(page) {
|
||||
this.pagination.page = page;
|
||||
this.loadProspects();
|
||||
},
|
||||
|
||||
statusBadgeClass(status) {
|
||||
const classes = {
|
||||
pending: 'text-yellow-700 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-700',
|
||||
active: 'text-green-700 bg-green-100 dark:text-green-100 dark:bg-green-700',
|
||||
contacted: 'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700',
|
||||
converted: 'text-purple-700 bg-purple-100 dark:text-purple-100 dark:bg-purple-700',
|
||||
inactive: 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700',
|
||||
error: 'text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700',
|
||||
};
|
||||
return classes[status] || classes.pending;
|
||||
},
|
||||
|
||||
tierBadgeClass(tier) {
|
||||
const classes = {
|
||||
top_priority: 'text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700',
|
||||
quick_win: 'text-orange-700 bg-orange-100 dark:text-orange-100 dark:bg-orange-700',
|
||||
strategic: 'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700',
|
||||
low_priority: 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700',
|
||||
};
|
||||
return classes[tier] || classes.low_priority;
|
||||
},
|
||||
|
||||
scoreColor(score) {
|
||||
if (score == null) return 'text-gray-400';
|
||||
if (score >= 70) return 'text-red-600';
|
||||
if (score >= 50) return 'text-orange-600';
|
||||
if (score >= 30) return 'text-blue-600';
|
||||
return 'text-gray-600';
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user