@mahe_pkm/buzl-capi 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Buzl_GoogleAppsScript_Template.gs +948 -0
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/assets/js/buzl-tracking.js +390 -0
- package/bin/cli.js +155 -0
- package/package.json +54 -0
- package/src/cli/terminal.js +388 -0
- package/src/core/injector.js +321 -0
- package/src/core/rollback.js +281 -0
- package/src/core/scanner.js +506 -0
- package/src/core/tester.js +357 -0
- package/src/gui/public/app.js +942 -0
- package/src/gui/public/index.html +634 -0
- package/src/gui/public/style.css +1130 -0
- package/src/gui/server.js +232 -0
- package/src/index.js +47 -0
- package/src/templates/GoogleAppsScript.gs +948 -0
- package/src/templates/buzl-tracking.js +475 -0
- package/src/templates/gtm-meta-snippets.js +149 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =========================================================================
|
|
3
|
+
* BUZL MULTI-TAB CRM & FORM DISPATCHER SCRIPT
|
|
4
|
+
* Google Apps Script Web App & Automated Sheet CRM Engine
|
|
5
|
+
* =========================================================================
|
|
6
|
+
*
|
|
7
|
+
* FEATURES:
|
|
8
|
+
* 1. 'All Leads' (Master Sheet): Inbound leads insert at Row 2 (Top of sheet).
|
|
9
|
+
* 2. Guaranteed Forward Layout: Handled By & Comments sit right next to Lead Stage.
|
|
10
|
+
* 3. Universal Fuzzy Matcher: Case-insensitive & whitespace-tolerant header mapping.
|
|
11
|
+
* 4. Official Buzl Navy Header: #1E4E9E background with bold white text (#FFFFFF).
|
|
12
|
+
* 5. Full-Row Highlighting: Entire rows (A:Z) color-coded by Lead Stage (LOWER formula).
|
|
13
|
+
* 6. Global Time & 12-Hour Format: 'dd-MMM-yyyy hh:mm a' with dynamic timezone.
|
|
14
|
+
* 7. Colored Sheet Tabs: Bottom tabs styled with high-contrast CRM colors.
|
|
15
|
+
* 8. Clean Team Tabs: Placed at the very end; auto-deleted when rep has 0 leads.
|
|
16
|
+
* 9. Shift-Proof INDIRECT Formulas: Sub-tabs never break when rows insert at top.
|
|
17
|
+
* 10. '⚡ Buzl Lead CRM' Top Menu: 1-click management, upgrades, and newest-first sorting.
|
|
18
|
+
* =========================================================================
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Core column definitions
|
|
22
|
+
var MASTER_SHEET_NAME = 'All Leads';
|
|
23
|
+
var STATUS_TABS = ['New', 'Contacted', 'Qualified', 'Converted', 'Spam', 'Test'];
|
|
24
|
+
var DEFAULT_TEAM_MEMBERS = ['Dr. Samya', 'Dr. John', 'Sales Rep', 'Front Desk'];
|
|
25
|
+
|
|
26
|
+
// Forward Operational Layout: Handled By and Comments sit right next to Lead Stage
|
|
27
|
+
var STANDARD_CRM_COLUMNS = [
|
|
28
|
+
'Lead Stage',
|
|
29
|
+
'Handled By',
|
|
30
|
+
'Comments',
|
|
31
|
+
'Is Qualified',
|
|
32
|
+
'Qualified Date',
|
|
33
|
+
'Is Spam',
|
|
34
|
+
'Event Time',
|
|
35
|
+
'Action Source',
|
|
36
|
+
'Source',
|
|
37
|
+
'UTM Source',
|
|
38
|
+
'UTM Campaign',
|
|
39
|
+
'Lead ID'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
var TAB_COLORS = {
|
|
43
|
+
'All Leads': '#1E293B', // Deep Charcoal / Slate
|
|
44
|
+
'New': '#0284C7', // Ocean Blue
|
|
45
|
+
'Contacted': '#D97706', // Warm Amber
|
|
46
|
+
'Qualified': '#16A34A', // Emerald Green
|
|
47
|
+
'Converted': '#9333EA', // Royal Purple
|
|
48
|
+
'Spam': '#DC2626', // Crimson Red
|
|
49
|
+
'Test': '#64748B' // Cool Slate Gray
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
var COLUMN_ALIASES = {
|
|
53
|
+
name: ['Name', 'Full Name', 'Patient Name', 'Client Name', 'Customer'],
|
|
54
|
+
location: ['Location', 'City', 'Address', 'Branch', 'Site Location'],
|
|
55
|
+
phone: ['Phone', 'Mobile', 'Contact', 'Tel', 'WhatsApp'],
|
|
56
|
+
email: ['Email', 'E-mail', 'Mail'],
|
|
57
|
+
service: ['Service', 'Treatment', 'Inquiry', 'Department', 'Specialty'],
|
|
58
|
+
leadStage: ['Lead Stage', 'Lead stage', 'Stage', 'Status', 'Lead Status'],
|
|
59
|
+
handledBy: ['Handled By', 'Handled by', 'handledby', 'Rep', 'Assigned To', 'Doctor', 'Staff'],
|
|
60
|
+
comments: ['Comments', 'Comment', 'Notes', 'Note', 'Remarks', 'Call Notes'],
|
|
61
|
+
isQualified: ['Is Qualified', 'Qualified', 'Qualified?'],
|
|
62
|
+
qualifiedDate: ['Qualified Date', 'Qualification Date', 'Date Qualified'],
|
|
63
|
+
isSpam: ['Is Spam', 'Spam', 'Spam?'],
|
|
64
|
+
eventTime: ['Event Time', 'Timestamp', 'Date', 'Time', 'Created At'],
|
|
65
|
+
actionSource: ['Action Source', 'ActionSource', 'Source Type'],
|
|
66
|
+
source: ['Source', 'Source / Form', 'Form', 'Page', 'Source/Form'],
|
|
67
|
+
utmSource: ['UTM Source', 'utm_source'],
|
|
68
|
+
utmCampaign: ['UTM Campaign', 'utm_campaign'],
|
|
69
|
+
leadId: ['Lead ID', 'lead_id', 'leadId', 'ID']
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Universal Case-Insensitive Header Index Matcher
|
|
74
|
+
*/
|
|
75
|
+
function findHeaderIndex(headers, aliases) {
|
|
76
|
+
if (!headers || !headers.length) return -1;
|
|
77
|
+
for (var i = 0; i < headers.length; i++) {
|
|
78
|
+
var clean = (headers[i] || '').toString().trim().toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
79
|
+
for (var a = 0; a < aliases.length; a++) {
|
|
80
|
+
var aliasClean = aliases[a].toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
81
|
+
if (clean === aliasClean) {
|
|
82
|
+
return i;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Main Webhook Receiver
|
|
91
|
+
*/
|
|
92
|
+
function doPost(e) {
|
|
93
|
+
var lock = LockService.getScriptLock();
|
|
94
|
+
lock.tryLock(30000);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
var ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
98
|
+
var sheet = getOrCreateMasterSheet(ss);
|
|
99
|
+
var tz = getSpreadsheetTz(ss);
|
|
100
|
+
|
|
101
|
+
// Parse incoming payload
|
|
102
|
+
var data = {};
|
|
103
|
+
if (e.postData && e.postData.contents) {
|
|
104
|
+
try {
|
|
105
|
+
data = JSON.parse(e.postData.contents);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
data = e.parameter || {};
|
|
108
|
+
}
|
|
109
|
+
} else if (e.parameter) {
|
|
110
|
+
data = e.parameter;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
var contact = data.contact || {};
|
|
114
|
+
var rawFields = data.rawFields || data.rawPayload || {};
|
|
115
|
+
var utm = data.utm || {};
|
|
116
|
+
|
|
117
|
+
// 1. Core Contact Fields
|
|
118
|
+
var name = data.name || contact.name || data.bizName || rawFields.name || rawFields.fullName || '';
|
|
119
|
+
var phone = data.phone || contact.phone || data.bizPhone || rawFields.phone || rawFields.mobile || '';
|
|
120
|
+
var location = data.location || contact.location || data.bizLocation || data.siteLocation || rawFields.location || rawFields.city || '';
|
|
121
|
+
var email = data.email || contact.email || rawFields.email || '';
|
|
122
|
+
|
|
123
|
+
// 2. Dynamic Inquiry Fields (next to Phone)
|
|
124
|
+
var excludedKeys = [
|
|
125
|
+
'name', 'bizname', 'fullname', 'phone', 'bizphone', 'mobile',
|
|
126
|
+
'location', 'bizlocation', 'city', 'leadid', 'timestamp', 'utm',
|
|
127
|
+
'rawfields', 'rawpayload', 'contact', 'source', 'actionsource',
|
|
128
|
+
'sitelocation', 'eventsourceurl', 'useragent', 'fbclid', 'fbc',
|
|
129
|
+
'fbp', 'leadstage', 'isqualified', 'qualifieddate', 'isspam',
|
|
130
|
+
'handledby', 'comments', 'istest', 'email', 'eventtime'
|
|
131
|
+
];
|
|
132
|
+
var dynamicFields = {};
|
|
133
|
+
|
|
134
|
+
for (var k in rawFields) {
|
|
135
|
+
if (excludedKeys.indexOf(k.toLowerCase()) === -1 && typeof rawFields[k] !== 'object' && rawFields[k] !== '') {
|
|
136
|
+
dynamicFields[capitalize(k)] = rawFields[k];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (var dk in data) {
|
|
140
|
+
if (excludedKeys.indexOf(dk.toLowerCase()) === -1 && typeof data[dk] !== 'object' && data[dk] !== '') {
|
|
141
|
+
var cleanDk = capitalize(dk);
|
|
142
|
+
if (!dynamicFields[cleanDk]) dynamicFields[cleanDk] = data[dk];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Default 'Service' if discovered in source
|
|
147
|
+
if (Object.keys(dynamicFields).length === 0 && data.source && data.source !== 'Website Form') {
|
|
148
|
+
dynamicFields['Service'] = data.source;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 3. Status & CRM Fields (12-Hour Format: dd-MMM-yyyy hh:mm a)
|
|
152
|
+
var leadDate = new Date();
|
|
153
|
+
if (data.timestamp || data.eventTime) {
|
|
154
|
+
var candidate = new Date(data.timestamp || data.eventTime);
|
|
155
|
+
if (!isNaN(candidate.getTime())) leadDate = candidate;
|
|
156
|
+
}
|
|
157
|
+
var timestamp = Utilities.formatDate(leadDate, tz, "dd-MMM-yyyy hh:mm a");
|
|
158
|
+
|
|
159
|
+
var isTestSubmission = data.isTest === true || (rawFields && rawFields.isTest === true) || (utm.source && utm.source.toString().indexOf('test') !== -1);
|
|
160
|
+
var leadStage = isTestSubmission ? 'Test' : (data.leadStage || 'New');
|
|
161
|
+
var isQualified = data.isQualified || 'No';
|
|
162
|
+
var qualifiedDate = data.qualifiedDate || '';
|
|
163
|
+
if (leadStage === 'Qualified' && !qualifiedDate) {
|
|
164
|
+
qualifiedDate = Utilities.formatDate(leadDate, tz, "dd-MMM-yyyy");
|
|
165
|
+
}
|
|
166
|
+
var isSpam = data.isSpam || 'No';
|
|
167
|
+
|
|
168
|
+
// Clean 'Rep - ' from Handled By if present
|
|
169
|
+
var rawHandledBy = data.handledBy || '';
|
|
170
|
+
var handledBy = rawHandledBy.replace(/^rep\s*[-:]?\s*/i, '').trim();
|
|
171
|
+
|
|
172
|
+
var comments = data.comments || '';
|
|
173
|
+
var actionSource = data.actionSource || 'website';
|
|
174
|
+
var source = data.source || data.formName || data.formId || 'Website Form';
|
|
175
|
+
var utmSource = utm.source || data.utm_source || '';
|
|
176
|
+
var utmCampaign = utm.campaign || data.utm_campaign || '';
|
|
177
|
+
var leadId = data.leadId || data.event_id || data.buzl_lead_id || ('lead-' + Date.now());
|
|
178
|
+
|
|
179
|
+
// 4. Ensure Form-First CRM Layout & Auto-Migrate Legacy Columns if present
|
|
180
|
+
var headers = ensureCrmLayout(sheet, dynamicFields, false);
|
|
181
|
+
|
|
182
|
+
// 5. Build New Lead Row
|
|
183
|
+
var rowData = [];
|
|
184
|
+
for (var i = 0; i < headers.length; i++) {
|
|
185
|
+
var h = headers[i];
|
|
186
|
+
var cleanH = h.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
187
|
+
|
|
188
|
+
if (cleanH === 'name') rowData.push(name);
|
|
189
|
+
else if (cleanH === 'location') rowData.push(location);
|
|
190
|
+
else if (cleanH === 'phone') rowData.push(phone);
|
|
191
|
+
else if (cleanH === 'email') rowData.push(email);
|
|
192
|
+
else if (cleanH === 'leadstage') rowData.push(leadStage);
|
|
193
|
+
else if (cleanH === 'handledby') rowData.push(handledBy);
|
|
194
|
+
else if (cleanH === 'comments') rowData.push(comments);
|
|
195
|
+
else if (cleanH === 'isqualified') rowData.push(isQualified);
|
|
196
|
+
else if (cleanH === 'qualifieddate') rowData.push(qualifiedDate);
|
|
197
|
+
else if (cleanH === 'isspam') rowData.push(isSpam);
|
|
198
|
+
else if (cleanH === 'eventtime') rowData.push(timestamp);
|
|
199
|
+
else if (cleanH === 'actionsource') rowData.push(actionSource);
|
|
200
|
+
else if (cleanH === 'source' || cleanH === 'sourceform') rowData.push(source);
|
|
201
|
+
else if (cleanH === 'utmsource') rowData.push(utmSource);
|
|
202
|
+
else if (cleanH === 'utmcampaign') rowData.push(utmCampaign);
|
|
203
|
+
else if (cleanH === 'leadid') rowData.push(leadId);
|
|
204
|
+
else if (dynamicFields[h] !== undefined) rowData.push(dynamicFields[h]);
|
|
205
|
+
else rowData.push('');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Insert at Row 2 (Top of Sheet, below header)
|
|
209
|
+
sheet.insertRowBefore(2);
|
|
210
|
+
sheet.getRange(2, 1, 1, rowData.length).setValues([rowData]);
|
|
211
|
+
sheet.getRange(2, 1, 1, rowData.length).setFontWeight('normal');
|
|
212
|
+
var insertedRowIndex = 2;
|
|
213
|
+
|
|
214
|
+
// 6. Ensure Dropdowns, Formatting, Number Formats, and Sub-Tabs exist
|
|
215
|
+
applyDropdownValidations(sheet, headers, insertedRowIndex);
|
|
216
|
+
applyDateFormatting(sheet, headers, insertedRowIndex);
|
|
217
|
+
applyColorCoding(sheet, headers);
|
|
218
|
+
initializeCrmTabs(ss, headers);
|
|
219
|
+
|
|
220
|
+
return ContentService.createTextOutput(JSON.stringify({
|
|
221
|
+
status: 'success',
|
|
222
|
+
message: 'Lead recorded at top of CRM sheet',
|
|
223
|
+
leadId: leadId,
|
|
224
|
+
row: insertedRowIndex
|
|
225
|
+
})).setMimeType(ContentService.MimeType.JSON);
|
|
226
|
+
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return ContentService.createTextOutput(JSON.stringify({
|
|
229
|
+
status: 'error',
|
|
230
|
+
message: error.toString()
|
|
231
|
+
})).setMimeType(ContentService.MimeType.JSON);
|
|
232
|
+
|
|
233
|
+
} finally {
|
|
234
|
+
lock.releaseLock();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function doGet(e) {
|
|
239
|
+
var tz = 'UTC';
|
|
240
|
+
try { tz = getSpreadsheetTz(SpreadsheetApp.getActiveSpreadsheet()); } catch (err) {}
|
|
241
|
+
return ContentService.createTextOutput(JSON.stringify({
|
|
242
|
+
status: 'online',
|
|
243
|
+
service: 'Buzl Multi-Tab CRM Sync Web App',
|
|
244
|
+
timestamp: Utilities.formatDate(new Date(), tz, "dd-MMM-yyyy hh:mm:ss a")
|
|
245
|
+
})).setMimeType(ContentService.MimeType.JSON);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get active spreadsheet timezone safely
|
|
250
|
+
*/
|
|
251
|
+
function getSpreadsheetTz(ss) {
|
|
252
|
+
try {
|
|
253
|
+
if (ss && ss.getSpreadsheetTimeZone) {
|
|
254
|
+
var tz = ss.getSpreadsheetTimeZone();
|
|
255
|
+
if (tz) return tz;
|
|
256
|
+
}
|
|
257
|
+
return Session.getScriptTimeZone() || 'UTC';
|
|
258
|
+
} catch (e) {
|
|
259
|
+
return 'UTC';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Format Header Row 1 with official Buzl Navy Blue and crisp white text
|
|
265
|
+
*/
|
|
266
|
+
function formatHeaderRow(sheet, numCols) {
|
|
267
|
+
if (!numCols || numCols < 1) numCols = sheet.getLastColumn() || 1;
|
|
268
|
+
var range = sheet.getRange(1, 1, 1, numCols);
|
|
269
|
+
range.setFontWeight('bold')
|
|
270
|
+
.setBackground('#1E4E9E')
|
|
271
|
+
.setFontColor('#FFFFFF')
|
|
272
|
+
.setFontSize(10)
|
|
273
|
+
.setVerticalAlignment('middle');
|
|
274
|
+
try {
|
|
275
|
+
sheet.setRowHeight(1, 32);
|
|
276
|
+
} catch (e) {}
|
|
277
|
+
sheet.setFrozenRows(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Ensure Master Sheet is named 'All Leads'
|
|
282
|
+
*/
|
|
283
|
+
function getOrCreateMasterSheet(ss) {
|
|
284
|
+
var sheet = ss.getSheetByName(MASTER_SHEET_NAME);
|
|
285
|
+
if (!sheet) {
|
|
286
|
+
var allSheets = ss.getSheets();
|
|
287
|
+
for (var s = 0; s < allSheets.length; s++) {
|
|
288
|
+
var sName = allSheets[s].getName();
|
|
289
|
+
if (STATUS_TABS.indexOf(sName) === -1 && sName.indexOf('Rep -') !== 0) {
|
|
290
|
+
try {
|
|
291
|
+
allSheets[s].setName(MASTER_SHEET_NAME);
|
|
292
|
+
sheet = allSheets[s];
|
|
293
|
+
break;
|
|
294
|
+
} catch (e) {}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!sheet) {
|
|
298
|
+
sheet = ss.insertSheet(MASTER_SHEET_NAME, 0);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
sheet.setTabColor(TAB_COLORS['All Leads']);
|
|
303
|
+
} catch (e) {}
|
|
304
|
+
return sheet;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Ensure Form-First CRM Layout & Auto-Migrate Legacy Sheets with Resilient Fuzzy Mapping
|
|
309
|
+
*/
|
|
310
|
+
function ensureCrmLayout(sheet, dynamicFields, forceReorganize) {
|
|
311
|
+
var lastRow = sheet.getLastRow();
|
|
312
|
+
var lastCol = sheet.getLastColumn();
|
|
313
|
+
var tz = getSpreadsheetTz(sheet.getParent());
|
|
314
|
+
|
|
315
|
+
// Empty sheet -> Build initial headers
|
|
316
|
+
if (lastRow === 0) {
|
|
317
|
+
var initialHeaders = ['Name', 'Location', 'Phone'];
|
|
318
|
+
if (dynamicFields) {
|
|
319
|
+
for (var df in dynamicFields) initialHeaders.push(df);
|
|
320
|
+
}
|
|
321
|
+
if (initialHeaders.indexOf('Service') === -1) {
|
|
322
|
+
initialHeaders.push('Service');
|
|
323
|
+
}
|
|
324
|
+
initialHeaders = initialHeaders.concat(STANDARD_CRM_COLUMNS);
|
|
325
|
+
|
|
326
|
+
sheet.appendRow(initialHeaders);
|
|
327
|
+
formatHeaderRow(sheet, initialHeaders.length);
|
|
328
|
+
applyDateFormatting(sheet, initialHeaders);
|
|
329
|
+
return initialHeaders;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
var allData = sheet.getDataRange().getValues();
|
|
333
|
+
var oldHeaders = allData[0] || [];
|
|
334
|
+
var oldRows = allData.slice(1);
|
|
335
|
+
|
|
336
|
+
var leadStageIdx = findHeaderIndex(oldHeaders, COLUMN_ALIASES.leadStage);
|
|
337
|
+
var handledByIdx = findHeaderIndex(oldHeaders, COLUMN_ALIASES.handledBy);
|
|
338
|
+
var commentsIdx = findHeaderIndex(oldHeaders, COLUMN_ALIASES.comments);
|
|
339
|
+
|
|
340
|
+
// Determine if column reorganization is needed
|
|
341
|
+
var needsReorganization = forceReorganize === true ||
|
|
342
|
+
leadStageIdx === -1 ||
|
|
343
|
+
handledByIdx === -1 ||
|
|
344
|
+
commentsIdx === -1 ||
|
|
345
|
+
handledByIdx !== leadStageIdx + 1 ||
|
|
346
|
+
commentsIdx !== leadStageIdx + 2;
|
|
347
|
+
|
|
348
|
+
if (needsReorganization) {
|
|
349
|
+
// 1. Build index mapping for all standard fields
|
|
350
|
+
var idxMap = {};
|
|
351
|
+
for (var key in COLUMN_ALIASES) {
|
|
352
|
+
idxMap[key] = findHeaderIndex(oldHeaders, COLUMN_ALIASES[key]);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 2. Build target headers: Name, Location, Phone, [Email if present], [Dynamic fields / Service], Standard CRM Columns
|
|
356
|
+
var targetHeaders = ['Name', 'Location', 'Phone'];
|
|
357
|
+
if (idxMap.email > -1) {
|
|
358
|
+
targetHeaders.push('Email');
|
|
359
|
+
}
|
|
360
|
+
if (dynamicFields) {
|
|
361
|
+
for (var k in dynamicFields) {
|
|
362
|
+
if (targetHeaders.indexOf(k) === -1) targetHeaders.push(k);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (targetHeaders.indexOf('Service') === -1) {
|
|
366
|
+
targetHeaders.push('Service');
|
|
367
|
+
}
|
|
368
|
+
targetHeaders = targetHeaders.concat(STANDARD_CRM_COLUMNS);
|
|
369
|
+
|
|
370
|
+
// 3. Preserve any additional custom columns from old headers
|
|
371
|
+
for (var c = 0; c < oldHeaders.length; c++) {
|
|
372
|
+
var rawH = (oldHeaders[c] || '').toString().trim();
|
|
373
|
+
if (!rawH) continue;
|
|
374
|
+
var matched = false;
|
|
375
|
+
for (var aKey in COLUMN_ALIASES) {
|
|
376
|
+
if (findHeaderIndex([rawH], COLUMN_ALIASES[aKey]) > -1) {
|
|
377
|
+
matched = true;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (!matched && targetHeaders.indexOf(rawH) === -1) {
|
|
382
|
+
targetHeaders.push(rawH);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 4. Migrate every existing row into the target columns
|
|
387
|
+
var migratedRows = [];
|
|
388
|
+
for (var r = 0; r < oldRows.length; r++) {
|
|
389
|
+
var oldRow = oldRows[r];
|
|
390
|
+
|
|
391
|
+
var rawPayload = {};
|
|
392
|
+
var rawPayloadIdx = -1;
|
|
393
|
+
for (var hi = 0; hi < oldHeaders.length; hi++) {
|
|
394
|
+
if ((oldHeaders[hi] || '').toString().toLowerCase().includes('raw')) {
|
|
395
|
+
rawPayloadIdx = hi;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (rawPayloadIdx > -1 && oldRow[rawPayloadIdx]) {
|
|
400
|
+
try { rawPayload = JSON.parse(oldRow[rawPayloadIdx]); } catch (e) {}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
var rowName = (idxMap.name > -1 ? oldRow[idxMap.name] : '') || rawPayload.name || '';
|
|
404
|
+
var rowLoc = (idxMap.location > -1 ? oldRow[idxMap.location] : '') || rawPayload.location || rawPayload.siteLocation || '';
|
|
405
|
+
var rowPhone = (idxMap.phone > -1 ? oldRow[idxMap.phone] : '') || rawPayload.phone || '';
|
|
406
|
+
var rowEmail = (idxMap.email > -1 ? oldRow[idxMap.email] : '') || rawPayload.email || '';
|
|
407
|
+
var rowService = (idxMap.service > -1 ? oldRow[idxMap.service] : '') || (rawPayload.rawFields && rawPayload.rawFields.service) || rawPayload.service || '';
|
|
408
|
+
var rowStage = (idxMap.leadStage > -1 && oldRow[idxMap.leadStage]) ? oldRow[idxMap.leadStage] : 'New';
|
|
409
|
+
var rowHandledBy = (idxMap.handledBy > -1 ? oldRow[idxMap.handledBy] : '').toString().replace(/^rep\s*[-:]?\s*/i, '').trim();
|
|
410
|
+
var rowComments = idxMap.comments > -1 ? oldRow[idxMap.comments] : '';
|
|
411
|
+
var rowIsQual = idxMap.isQualified > -1 ? (oldRow[idxMap.isQualified] || 'No') : 'No';
|
|
412
|
+
var rowQualDate = idxMap.qualifiedDate > -1 ? oldRow[idxMap.qualifiedDate] : '';
|
|
413
|
+
var rowIsSpam = idxMap.isSpam > -1 ? (oldRow[idxMap.isSpam] || 'No') : 'No';
|
|
414
|
+
|
|
415
|
+
var rawTime = (idxMap.eventTime > -1 ? oldRow[idxMap.eventTime] : '') || rawPayload.timestamp;
|
|
416
|
+
var rowDateObj = new Date();
|
|
417
|
+
if (rawTime) {
|
|
418
|
+
var parsed = new Date(rawTime);
|
|
419
|
+
if (!isNaN(parsed.getTime())) rowDateObj = parsed;
|
|
420
|
+
}
|
|
421
|
+
var formattedRowTime = Utilities.formatDate(rowDateObj, tz, "dd-MMM-yyyy hh:mm a");
|
|
422
|
+
|
|
423
|
+
var rowAction = (idxMap.actionSource > -1 ? oldRow[idxMap.actionSource] : '') || 'website';
|
|
424
|
+
var rowSource = (idxMap.source > -1 ? oldRow[idxMap.source] : '') || rawPayload.source || 'Website Form';
|
|
425
|
+
var rowUtmSrc = (idxMap.utmSource > -1 ? oldRow[idxMap.utmSource] : '') || (rawPayload.utm && rawPayload.utm.source) || '';
|
|
426
|
+
var rowUtmCmp = (idxMap.utmCampaign > -1 ? oldRow[idxMap.utmCampaign] : '') || (rawPayload.utm && rawPayload.utm.campaign) || '';
|
|
427
|
+
var rowLeadId = (idxMap.leadId > -1 ? oldRow[idxMap.leadId] : '') || rawPayload.leadId || ('lead-' + (r + 1));
|
|
428
|
+
|
|
429
|
+
var newRow = [];
|
|
430
|
+
for (var th = 0; th < targetHeaders.length; th++) {
|
|
431
|
+
var tHeader = targetHeaders[th];
|
|
432
|
+
var cleanTH = tHeader.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
433
|
+
|
|
434
|
+
if (cleanTH === 'name') newRow.push(rowName);
|
|
435
|
+
else if (cleanTH === 'location') newRow.push(rowLoc);
|
|
436
|
+
else if (cleanTH === 'phone') newRow.push(rowPhone);
|
|
437
|
+
else if (cleanTH === 'email') newRow.push(rowEmail);
|
|
438
|
+
else if (cleanTH === 'service') newRow.push(rowService);
|
|
439
|
+
else if (cleanTH === 'leadstage') newRow.push(rowStage);
|
|
440
|
+
else if (cleanTH === 'handledby') newRow.push(rowHandledBy);
|
|
441
|
+
else if (cleanTH === 'comments') newRow.push(rowComments);
|
|
442
|
+
else if (cleanTH === 'isqualified') newRow.push(rowIsQual);
|
|
443
|
+
else if (cleanTH === 'qualifieddate') newRow.push(rowQualDate);
|
|
444
|
+
else if (cleanTH === 'isspam') newRow.push(rowIsSpam);
|
|
445
|
+
else if (cleanTH === 'eventtime') newRow.push(formattedRowTime);
|
|
446
|
+
else if (cleanTH === 'actionsource') newRow.push(rowAction);
|
|
447
|
+
else if (cleanTH === 'source' || cleanTH === 'sourceform') newRow.push(rowSource);
|
|
448
|
+
else if (cleanTH === 'utmsource') newRow.push(rowUtmSrc);
|
|
449
|
+
else if (cleanTH === 'utmcampaign') newRow.push(rowUtmCmp);
|
|
450
|
+
else if (cleanTH === 'leadid') newRow.push(rowLeadId);
|
|
451
|
+
else {
|
|
452
|
+
var originalIdx = oldHeaders.indexOf(tHeader);
|
|
453
|
+
newRow.push(originalIdx > -1 ? oldRow[originalIdx] : '');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
migratedRows.push(newRow);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 5. Clear sheet and write reorganized forward structure
|
|
460
|
+
sheet.clear();
|
|
461
|
+
sheet.appendRow(targetHeaders);
|
|
462
|
+
formatHeaderRow(sheet, targetHeaders.length);
|
|
463
|
+
|
|
464
|
+
if (migratedRows.length > 0) {
|
|
465
|
+
sheet.getRange(2, 1, migratedRows.length, targetHeaders.length).setValues(migratedRows);
|
|
466
|
+
}
|
|
467
|
+
applyDateFormatting(sheet, targetHeaders);
|
|
468
|
+
return targetHeaders;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Already properly organized: ensure styling and return
|
|
472
|
+
formatHeaderRow(sheet, oldHeaders.length);
|
|
473
|
+
applyDateFormatting(sheet, oldHeaders);
|
|
474
|
+
return oldHeaders;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Apply native date/time number formats to Event Time & Qualified Date
|
|
479
|
+
*/
|
|
480
|
+
function applyDateFormatting(sheet, headers, targetRow) {
|
|
481
|
+
var timeCol = findHeaderIndex(headers, COLUMN_ALIASES.eventTime) + 1;
|
|
482
|
+
var qualDateCol = findHeaderIndex(headers, COLUMN_ALIASES.qualifiedDate) + 1;
|
|
483
|
+
var startRow = targetRow || 2;
|
|
484
|
+
var numRows = targetRow ? 1 : Math.max(sheet.getLastRow() - 1, 1000);
|
|
485
|
+
|
|
486
|
+
if (timeCol > 0) {
|
|
487
|
+
sheet.getRange(startRow, timeCol, numRows, 1).setNumberFormat('dd-mmm-yyyy hh:mm am/pm');
|
|
488
|
+
}
|
|
489
|
+
if (qualDateCol > 0) {
|
|
490
|
+
sheet.getRange(startRow, qualDateCol, numRows, 1).setNumberFormat('dd-mmm-yyyy');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Initialize / Update Auto-Filtered Status Tabs with shift-proof INDIRECT formulas
|
|
496
|
+
*/
|
|
497
|
+
function initializeCrmTabs(ss, headers) {
|
|
498
|
+
var master = getOrCreateMasterSheet(ss);
|
|
499
|
+
if (!headers || headers.length === 0) {
|
|
500
|
+
headers = ensureCrmLayout(master, null, false);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
var lastColLetter = getColLetter(headers.length);
|
|
504
|
+
var stageCol = findHeaderIndex(headers, COLUMN_ALIASES.leadStage) + 1;
|
|
505
|
+
var qualCol = findHeaderIndex(headers, COLUMN_ALIASES.isQualified) + 1;
|
|
506
|
+
var spamCol = findHeaderIndex(headers, COLUMN_ALIASES.isSpam) + 1;
|
|
507
|
+
|
|
508
|
+
var stageColLetter = getColLetter(stageCol);
|
|
509
|
+
var qualColLetter = getColLetter(qualCol);
|
|
510
|
+
var spamColLetter = getColLetter(spamCol);
|
|
511
|
+
|
|
512
|
+
// Keep master at index 1
|
|
513
|
+
try {
|
|
514
|
+
ss.setActiveSheet(master);
|
|
515
|
+
ss.moveActiveSheet(1);
|
|
516
|
+
} catch (e) {}
|
|
517
|
+
|
|
518
|
+
STATUS_TABS.forEach(function (tabName, index) {
|
|
519
|
+
var tab = ss.getSheetByName(tabName);
|
|
520
|
+
if (!tab) {
|
|
521
|
+
tab = ss.insertSheet(tabName);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Ensure status tab sequence (tabs 2 through 7)
|
|
525
|
+
try {
|
|
526
|
+
ss.setActiveSheet(tab);
|
|
527
|
+
ss.moveActiveSheet(index + 2);
|
|
528
|
+
} catch (e) {}
|
|
529
|
+
|
|
530
|
+
// Tab Color
|
|
531
|
+
if (TAB_COLORS[tabName]) {
|
|
532
|
+
try { tab.setTabColor(TAB_COLORS[tabName]); } catch (e) {}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Set Header with official Buzl Navy Blue
|
|
536
|
+
tab.getRange(1, 1, 1, headers.length).setValues([headers]);
|
|
537
|
+
formatHeaderRow(tab, headers.length);
|
|
538
|
+
|
|
539
|
+
// Clear previous formula contents below row 1 to prevent collision (#REF!)
|
|
540
|
+
var maxRows = Math.max(tab.getLastRow(), 2);
|
|
541
|
+
tab.getRange(2, 1, maxRows, tab.getLastColumn() || headers.length).clearContent();
|
|
542
|
+
|
|
543
|
+
// Shift-Proof Dynamic Filter Formula using INDIRECT and case-insensitive LOWER matching
|
|
544
|
+
var formula = '';
|
|
545
|
+
if (tabName === 'New') {
|
|
546
|
+
formula = `=IFERROR(FILTER(INDIRECT("'${MASTER_SHEET_NAME}'!A2:${lastColLetter}"), LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${stageColLetter}2:${stageColLetter}")) = "new"), "No new leads yet")`;
|
|
547
|
+
} else if (tabName === 'Contacted') {
|
|
548
|
+
formula = `=IFERROR(FILTER(INDIRECT("'${MASTER_SHEET_NAME}'!A2:${lastColLetter}"), LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${stageColLetter}2:${stageColLetter}")) = "contacted"), "No contacted leads yet")`;
|
|
549
|
+
} else if (tabName === 'Qualified') {
|
|
550
|
+
formula = `=IFERROR(FILTER(INDIRECT("'${MASTER_SHEET_NAME}'!A2:${lastColLetter}"), (LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${stageColLetter}2:${stageColLetter}")) = "qualified") + (LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${qualColLetter}2:${qualColLetter}")) = "yes")), "No qualified leads yet")`;
|
|
551
|
+
} else if (tabName === 'Converted') {
|
|
552
|
+
formula = `=IFERROR(FILTER(INDIRECT("'${MASTER_SHEET_NAME}'!A2:${lastColLetter}"), LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${stageColLetter}2:${stageColLetter}")) = "converted"), "No converted leads yet")`;
|
|
553
|
+
} else if (tabName === 'Spam') {
|
|
554
|
+
formula = `=IFERROR(FILTER(INDIRECT("'${MASTER_SHEET_NAME}'!A2:${lastColLetter}"), (LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${stageColLetter}2:${stageColLetter}")) = "spam") + (LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${spamColLetter}2:${spamColLetter}")) = "yes")), "No spam leads")`;
|
|
555
|
+
} else if (tabName === 'Test') {
|
|
556
|
+
formula = `=IFERROR(FILTER(INDIRECT("'${MASTER_SHEET_NAME}'!A2:${lastColLetter}"), LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${stageColLetter}2:${stageColLetter}")) = "test"), "No test leads")`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (formula) {
|
|
560
|
+
tab.getRange('A2').setFormula(formula);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
applyColorCoding(tab, headers);
|
|
564
|
+
applyDateFormatting(tab, headers);
|
|
565
|
+
applyDropdownValidations(tab, headers, null, ss);
|
|
566
|
+
applyColumnWidths(tab, headers);
|
|
567
|
+
applyDropdownValidations(tab, headers, null, ss);
|
|
568
|
+
applyColumnWidths(tab, headers);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Check, prune, and sync team member tabs at the very end
|
|
572
|
+
syncTeamMemberTabs(ss, headers);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Dynamic Team Member Tabs: Positioned at the very end; auto-pruned when 0 leads
|
|
577
|
+
*/
|
|
578
|
+
function syncTeamMemberTabs(ss, headers) {
|
|
579
|
+
var master = getOrCreateMasterSheet(ss);
|
|
580
|
+
if (!headers) {
|
|
581
|
+
headers = master.getRange(1, 1, 1, master.getLastColumn()).getValues()[0];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
var handledByIndex = findHeaderIndex(headers, COLUMN_ALIASES.handledBy);
|
|
585
|
+
if (handledByIndex === -1) return;
|
|
586
|
+
|
|
587
|
+
var lastColLetter = getColLetter(headers.length);
|
|
588
|
+
var handledByColLetter = getColLetter(handledByIndex + 1);
|
|
589
|
+
|
|
590
|
+
// 1. Calculate active lead counts per team member
|
|
591
|
+
var repCounts = {};
|
|
592
|
+
if (master.getLastRow() >= 2) {
|
|
593
|
+
var values = master.getRange(2, handledByIndex + 1, master.getLastRow() - 1, 1).getValues();
|
|
594
|
+
values.forEach(function (row) {
|
|
595
|
+
var rawVal = (row[0] || '').toString().trim();
|
|
596
|
+
if (rawVal && rawVal !== '') {
|
|
597
|
+
var cleanVal = rawVal.replace(/^rep\s*[-:]?\s*/i, '').trim();
|
|
598
|
+
if (cleanVal) {
|
|
599
|
+
repCounts[cleanVal] = (repCounts[cleanVal] || 0) + 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 2. Auto-Prune: Delete any team tabs for reps who now have 0 leads
|
|
606
|
+
var allSheets = ss.getSheets();
|
|
607
|
+
allSheets.forEach(function (s) {
|
|
608
|
+
var sName = s.getName();
|
|
609
|
+
if (sName !== MASTER_SHEET_NAME && STATUS_TABS.indexOf(sName) === -1) {
|
|
610
|
+
var cleanSName = sName.replace(/^rep\s*[-:]?\s*/i, '').trim();
|
|
611
|
+
if (!repCounts[cleanSName] || repCounts[cleanSName] === 0) {
|
|
612
|
+
try {
|
|
613
|
+
ss.deleteSheet(s);
|
|
614
|
+
} catch (err) {}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// 3. Create or update tabs for active members and move them to the very end
|
|
620
|
+
var uniqueReps = Object.keys(repCounts);
|
|
621
|
+
|
|
622
|
+
uniqueReps.forEach(function (repName) {
|
|
623
|
+
if (!repCounts[repName] || repCounts[repName] < 1) return;
|
|
624
|
+
|
|
625
|
+
var tabName = repName; // Direct clean name (no 'Rep -')
|
|
626
|
+
|
|
627
|
+
// Auto-migrate legacy 'Rep - <Name>' tabs if present
|
|
628
|
+
var legacyTab = ss.getSheetByName('Rep - ' + repName);
|
|
629
|
+
var tab = ss.getSheetByName(tabName);
|
|
630
|
+
if (!tab && legacyTab) {
|
|
631
|
+
try {
|
|
632
|
+
legacyTab.setName(tabName);
|
|
633
|
+
tab = legacyTab;
|
|
634
|
+
} catch (e) {
|
|
635
|
+
tab = legacyTab;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (!tab) {
|
|
639
|
+
tab = ss.insertSheet(tabName);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
try { tab.setTabColor('#0D9488'); } catch (e) {} // Elegant Teal
|
|
643
|
+
|
|
644
|
+
tab.getRange(1, 1, 1, headers.length).setValues([headers]);
|
|
645
|
+
formatHeaderRow(tab, headers.length);
|
|
646
|
+
|
|
647
|
+
var maxRows = Math.max(tab.getLastRow(), 2);
|
|
648
|
+
tab.getRange(2, 1, maxRows, tab.getLastColumn() || headers.length).clearContent();
|
|
649
|
+
|
|
650
|
+
// Shift-Proof INDIRECT formula matching both clean and legacy names
|
|
651
|
+
var formula = `=IFERROR(FILTER(INDIRECT("'${MASTER_SHEET_NAME}'!A2:${lastColLetter}"), (LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${handledByColLetter}2:${handledByColLetter}")) = "${repName.toLowerCase()}") + (LOWER(INDIRECT("'${MASTER_SHEET_NAME}'!${handledByColLetter}2:${handledByColLetter}")) = "rep - ${repName.toLowerCase()}")), "No leads assigned to ${repName}")`;
|
|
652
|
+
tab.getRange('A2').setFormula(formula);
|
|
653
|
+
|
|
654
|
+
applyColorCoding(tab, headers);
|
|
655
|
+
applyDateFormatting(tab, headers);
|
|
656
|
+
|
|
657
|
+
// Strictly position team member tab at the very end
|
|
658
|
+
try {
|
|
659
|
+
ss.setActiveSheet(tab);
|
|
660
|
+
ss.moveActiveSheet(ss.getNumSheets());
|
|
661
|
+
} catch (e) {}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Dynamic Team Member List Aggregator
|
|
667
|
+
* Merges DEFAULT_TEAM_MEMBERS with any active members assigned in 'All Leads'
|
|
668
|
+
*/
|
|
669
|
+
function getTeamMembersList(ss, master) {
|
|
670
|
+
var members = DEFAULT_TEAM_MEMBERS.slice();
|
|
671
|
+
if (!master && ss) master = getOrCreateMasterSheet(ss);
|
|
672
|
+
if (master && master.getLastRow() >= 2) {
|
|
673
|
+
var headers = master.getRange(1, 1, 1, master.getLastColumn()).getValues()[0];
|
|
674
|
+
var handledByIndex = findHeaderIndex(headers, COLUMN_ALIASES.handledBy);
|
|
675
|
+
if (handledByIndex > -1) {
|
|
676
|
+
var vals = master.getRange(2, handledByIndex + 1, master.getLastRow() - 1, 1).getValues();
|
|
677
|
+
vals.forEach(function (row) {
|
|
678
|
+
var raw = (row[0] || '').toString().trim();
|
|
679
|
+
if (raw) {
|
|
680
|
+
var clean = raw.replace(/^rep\s*[-:]?\s*/i, '').trim();
|
|
681
|
+
if (clean && members.indexOf(clean) === -1) {
|
|
682
|
+
members.push(clean);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return members;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Professional Column Widths for Optimal Readability
|
|
693
|
+
*/
|
|
694
|
+
function applyColumnWidths(sheet, headers) {
|
|
695
|
+
if (!sheet || !headers) return;
|
|
696
|
+
try {
|
|
697
|
+
sheet.setHiddenGridlines(false);
|
|
698
|
+
} catch (e) {}
|
|
699
|
+
|
|
700
|
+
var widthMap = {
|
|
701
|
+
name: 160,
|
|
702
|
+
location: 130,
|
|
703
|
+
phone: 135,
|
|
704
|
+
email: 185,
|
|
705
|
+
service: 150,
|
|
706
|
+
leadStage: 125,
|
|
707
|
+
handledBy: 140,
|
|
708
|
+
comments: 220,
|
|
709
|
+
isQualified: 95,
|
|
710
|
+
qualifiedDate: 120,
|
|
711
|
+
isSpam: 95,
|
|
712
|
+
eventTime: 165,
|
|
713
|
+
actionSource: 120,
|
|
714
|
+
source: 140,
|
|
715
|
+
utmSource: 120,
|
|
716
|
+
utmCampaign: 130,
|
|
717
|
+
leadId: 130
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
for (var i = 0; i < headers.length; i++) {
|
|
721
|
+
var colNum = i + 1;
|
|
722
|
+
for (var key in COLUMN_ALIASES) {
|
|
723
|
+
var aliases = COLUMN_ALIASES[key];
|
|
724
|
+
for (var a = 0; a < aliases.length; a++) {
|
|
725
|
+
if ((headers[i] || '').toString().trim().toLowerCase() === aliases[a].toLowerCase()) {
|
|
726
|
+
if (widthMap[key]) {
|
|
727
|
+
try {
|
|
728
|
+
sheet.setColumnWidth(colNum, widthMap[key]);
|
|
729
|
+
} catch (e) {}
|
|
730
|
+
}
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Dropdown Validations for Status, Team Members, and Spam
|
|
740
|
+
* Applies across full column (rows 2 to 1000) or a targeted row
|
|
741
|
+
*/
|
|
742
|
+
function applyDropdownValidations(sheet, headers, targetRow, ss) {
|
|
743
|
+
if (!sheet || !headers || headers.length === 0) return;
|
|
744
|
+
var stageCol = findHeaderIndex(headers, COLUMN_ALIASES.leadStage) + 1;
|
|
745
|
+
var qualCol = findHeaderIndex(headers, COLUMN_ALIASES.isQualified) + 1;
|
|
746
|
+
var spamCol = findHeaderIndex(headers, COLUMN_ALIASES.isSpam) + 1;
|
|
747
|
+
var handledByCol = findHeaderIndex(headers, COLUMN_ALIASES.handledBy) + 1;
|
|
748
|
+
|
|
749
|
+
var stageRule = SpreadsheetApp.newDataValidation()
|
|
750
|
+
.requireValueInList(['New', 'Contacted', 'Qualified', 'Converted', 'Spam', 'Test'], true)
|
|
751
|
+
.build();
|
|
752
|
+
|
|
753
|
+
var yesNoRule = SpreadsheetApp.newDataValidation()
|
|
754
|
+
.requireValueInList(['No', 'Yes'], true)
|
|
755
|
+
.build();
|
|
756
|
+
|
|
757
|
+
var startRow = targetRow || 2;
|
|
758
|
+
var numRows = targetRow ? 1 : Math.max(sheet.getLastRow() - 1, 1000);
|
|
759
|
+
if (numRows < 1) numRows = 1;
|
|
760
|
+
|
|
761
|
+
if (stageCol > 0) {
|
|
762
|
+
sheet.getRange(startRow, stageCol, numRows, 1).setDataValidation(stageRule);
|
|
763
|
+
}
|
|
764
|
+
if (qualCol > 0) {
|
|
765
|
+
sheet.getRange(startRow, qualCol, numRows, 1).setDataValidation(yesNoRule);
|
|
766
|
+
}
|
|
767
|
+
if (spamCol > 0) {
|
|
768
|
+
sheet.getRange(startRow, spamCol, numRows, 1).setDataValidation(yesNoRule);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Handled By Dropdown Validation (Team Members)
|
|
772
|
+
if (handledByCol > 0) {
|
|
773
|
+
var parentSs = ss || (sheet.getParent ? sheet.getParent() : SpreadsheetApp.getActiveSpreadsheet());
|
|
774
|
+
var teamList = getTeamMembersList(parentSs, sheet.getName() === MASTER_SHEET_NAME ? sheet : null);
|
|
775
|
+
if (teamList && teamList.length > 0) {
|
|
776
|
+
var handledByRule = SpreadsheetApp.newDataValidation()
|
|
777
|
+
.requireValueInList(teamList, true)
|
|
778
|
+
.build();
|
|
779
|
+
sheet.getRange(startRow, handledByCol, numRows, 1).setDataValidation(handledByRule);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Conditional Formatting: Full-Row Highlighting for Lead Stage (with case-insensitive LOWER matching)
|
|
787
|
+
*/
|
|
788
|
+
function applyColorCoding(sheet, headers) {
|
|
789
|
+
var stageCol = findHeaderIndex(headers, COLUMN_ALIASES.leadStage) + 1;
|
|
790
|
+
if (stageCol === 0) return;
|
|
791
|
+
|
|
792
|
+
var lastRow = Math.max(sheet.getLastRow(), 1000);
|
|
793
|
+
var numCols = headers.length;
|
|
794
|
+
var stageColLetter = getColLetter(stageCol);
|
|
795
|
+
|
|
796
|
+
// Full-Row Range spanning Column A to last column
|
|
797
|
+
var fullRowRange = sheet.getRange(2, 1, lastRow - 1, numCols);
|
|
798
|
+
|
|
799
|
+
var colorMap = [
|
|
800
|
+
{ text: 'new', bg: '#EBF5FF', fg: '#0369A1' }, // Soft Ice Blue
|
|
801
|
+
{ text: 'contacted', bg: '#FEF9C3', fg: '#854D0E' }, // Soft Pale Amber
|
|
802
|
+
{ text: 'qualified', bg: '#DCFCE7', fg: '#166534' }, // Soft Mint Green
|
|
803
|
+
{ text: 'converted', bg: '#F3E8FF', fg: '#6B21A8' }, // Soft Lavender Purple
|
|
804
|
+
{ text: 'spam', bg: '#FEE2E2', fg: '#991B1B' }, // Soft Blush Red
|
|
805
|
+
{ text: 'test', bg: '#F3F4F6', fg: '#4B5563' } // Soft Slate Gray
|
|
806
|
+
];
|
|
807
|
+
|
|
808
|
+
var newRules = [];
|
|
809
|
+
colorMap.forEach(function (c) {
|
|
810
|
+
var formula = '=$' + stageColLetter + '2="' + c.text + '"';
|
|
811
|
+
var rule = SpreadsheetApp.newConditionalFormatRule()
|
|
812
|
+
.whenFormulaSatisfied(formula)
|
|
813
|
+
.setBackground(c.bg)
|
|
814
|
+
.setFontColor(c.fg)
|
|
815
|
+
.setRanges([fullRowRange])
|
|
816
|
+
.build();
|
|
817
|
+
newRules.push(rule);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
sheet.setConditionalFormatRules(newRules);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Custom UI Menu inside Google Sheets
|
|
825
|
+
*/
|
|
826
|
+
function onOpen() {
|
|
827
|
+
try {
|
|
828
|
+
SpreadsheetApp.getUi().createMenu('⚡ Buzl Lead CRM')
|
|
829
|
+
.addItem('⚡ Initialize & Upgrade All CRM Tabs', 'menuInitCrm')
|
|
830
|
+
.addItem('🔄 Sort All Leads (Newest First)', 'menuSortNewestFirst')
|
|
831
|
+
.addItem('🎨 Apply Status Color Coding', 'menuColorCoding')
|
|
832
|
+
.addItem('👥 Sync Team Member Tabs', 'menuSyncReps')
|
|
833
|
+
.addItem('📋 Apply Dropdowns & Layout to All Sheets', 'menuApplyDropdownsAndLayout')
|
|
834
|
+
.addToUi();
|
|
835
|
+
} catch (e) {}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function menuInitCrm() {
|
|
839
|
+
var ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
840
|
+
var master = getOrCreateMasterSheet(ss);
|
|
841
|
+
var headers = ensureCrmLayout(master, { Service: 'General Consultation' }, true);
|
|
842
|
+
initializeCrmTabs(ss, headers);
|
|
843
|
+
applyDropdownValidations(master, headers, null, ss);
|
|
844
|
+
applyColumnWidths(master, headers);
|
|
845
|
+
applyColorCoding(master, headers);
|
|
846
|
+
applyDateFormatting(master, headers);
|
|
847
|
+
SpreadsheetApp.getUi().alert('✔ Buzl CRM Upgraded! Columns reorganized forward, full-row colors active, and team tabs synced.');
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function menuSortNewestFirst() {
|
|
851
|
+
var ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
852
|
+
var master = getOrCreateMasterSheet(ss);
|
|
853
|
+
if (master.getLastRow() < 3) {
|
|
854
|
+
SpreadsheetApp.getUi().alert('Not enough rows to sort.');
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
var headers = master.getRange(1, 1, 1, master.getLastColumn()).getValues()[0];
|
|
858
|
+
var timeCol = findHeaderIndex(headers, COLUMN_ALIASES.eventTime) + 1;
|
|
859
|
+
if (timeCol > 0) {
|
|
860
|
+
var range = master.getRange(2, 1, master.getLastRow() - 1, master.getLastColumn());
|
|
861
|
+
range.sort({ column: timeCol, ascending: false });
|
|
862
|
+
SpreadsheetApp.getUi().alert('✔ Master sheet sorted: newest leads are on top!');
|
|
863
|
+
} else {
|
|
864
|
+
SpreadsheetApp.getUi().alert('Event Time column not found for sorting.');
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function menuColorCoding() {
|
|
869
|
+
var ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
870
|
+
var master = getOrCreateMasterSheet(ss);
|
|
871
|
+
var headers = master.getRange(1, 1, 1, master.getLastColumn()).getValues()[0];
|
|
872
|
+
applyColorCoding(master, headers);
|
|
873
|
+
SpreadsheetApp.getUi().alert('✔ Full-row conditional status colors applied!');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function menuSyncReps() {
|
|
877
|
+
var ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
878
|
+
syncTeamMemberTabs(ss);
|
|
879
|
+
SpreadsheetApp.getUi().alert('✔ Team Member tabs refreshed & pruned!');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function menuApplyDropdownsAndLayout() {
|
|
883
|
+
var ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
884
|
+
var master = getOrCreateMasterSheet(ss);
|
|
885
|
+
var headers = master.getRange(1, 1, 1, master.getLastColumn()).getValues()[0];
|
|
886
|
+
|
|
887
|
+
applyDropdownValidations(master, headers, null, ss);
|
|
888
|
+
applyColumnWidths(master, headers);
|
|
889
|
+
|
|
890
|
+
STATUS_TABS.forEach(function (name) {
|
|
891
|
+
var tab = ss.getSheetByName(name);
|
|
892
|
+
if (tab) {
|
|
893
|
+
applyDropdownValidations(tab, headers, null, ss);
|
|
894
|
+
applyColumnWidths(tab, headers);
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
syncTeamMemberTabs(ss, headers);
|
|
899
|
+
SpreadsheetApp.getUi().alert('✔ Full-column dropdowns and column widths applied to all sheets!');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Trigger: On Edit in Google Sheets
|
|
904
|
+
*/
|
|
905
|
+
function onEdit(e) {
|
|
906
|
+
if (!e || !e.range) return;
|
|
907
|
+
var sheet = e.range.getSheet();
|
|
908
|
+
if (sheet.getName() !== MASTER_SHEET_NAME) return;
|
|
909
|
+
|
|
910
|
+
var row = e.range.getRow();
|
|
911
|
+
var col = e.range.getColumn();
|
|
912
|
+
if (row <= 1) return;
|
|
913
|
+
|
|
914
|
+
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
|
|
915
|
+
var stageCol = findHeaderIndex(headers, COLUMN_ALIASES.leadStage) + 1;
|
|
916
|
+
var qualCol = findHeaderIndex(headers, COLUMN_ALIASES.isQualified) + 1;
|
|
917
|
+
var qualDateCol = findHeaderIndex(headers, COLUMN_ALIASES.qualifiedDate) + 1;
|
|
918
|
+
var handledByCol = findHeaderIndex(headers, COLUMN_ALIASES.handledBy) + 1;
|
|
919
|
+
|
|
920
|
+
if (col === stageCol && (e.value || '').toString().toLowerCase() === 'qualified') {
|
|
921
|
+
if (qualCol > 0) sheet.getRange(row, qualCol).setValue('Yes');
|
|
922
|
+
if (qualDateCol > 0) {
|
|
923
|
+
var tz = getSpreadsheetTz(SpreadsheetApp.getActiveSpreadsheet());
|
|
924
|
+
var today = Utilities.formatDate(new Date(), tz, "dd-MMM-yyyy");
|
|
925
|
+
sheet.getRange(row, qualDateCol).setValue(today);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (col === handledByCol) {
|
|
930
|
+
syncTeamMemberTabs(SpreadsheetApp.getActiveSpreadsheet(), headers);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function getColLetter(colIndex) {
|
|
935
|
+
var temp, letter = '';
|
|
936
|
+
while (colIndex > 0) {
|
|
937
|
+
temp = (colIndex - 1) % 26;
|
|
938
|
+
letter = String.fromCharCode(temp + 65) + letter;
|
|
939
|
+
colIndex = Math.floor((colIndex - temp - 1) / 26);
|
|
940
|
+
}
|
|
941
|
+
return letter;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function capitalize(str) {
|
|
945
|
+
if (!str) return '';
|
|
946
|
+
var clean = str.replace(/([A-Z])/g, ' $1').replace(/[-_]/g, ' ').trim();
|
|
947
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
948
|
+
}
|