Find page numbers in Limoanywhere for zone codes from Google Sheets and add them to column H
Size
24.0 KB
Version
1.1.27
Created
Dec 19, 2025
Updated
about 2 months ago
1// ==UserScript==
2// @name Limoanywhere Page Finder for Google Sheets
3// @description Find page numbers in Limoanywhere for zone codes from Google Sheets and add them to column H
4// @version 1.1.27
5// @match https://*.docs.google.com/*
6// @match https://manage.mylimobiz.com/*
7// @icon https://ssl.gstatic.com/docs/spreadsheets/spreadsheets_2023q4.ico
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 console.log('Limoanywhere Page Finder extension loaded');
13
14 // ========== GOOGLE SHEETS FUNCTIONS ==========
15
16 async function extractSheetDataManually() {
17 const instructions = `
18INSTRUCTIONS:
19
201. Select all your filtered data (columns A through G, all 562 rows)
212. Copy it (Ctrl+C or Cmd+C)
223. Click OK below
234. The extension will read from your clipboard
24
25Make sure:
26- Column B has Zone From codes
27- Column D has Rates
28- Only include rows with rates < 130
29 `;
30
31 alert(instructions);
32
33 // Try to read from clipboard
34 try {
35 const clipboardText = await navigator.clipboard.readText();
36 console.log('Clipboard data received, length:', clipboardText.length);
37
38 // Parse the clipboard data (TSV format from Google Sheets)
39 const lines = clipboardText.split('\n');
40 const data = [];
41
42 lines.forEach((line, index) => {
43 if (!line.trim()) return;
44
45 const columns = line.split('\t');
46 console.log(`Row ${index}: Found ${columns.length} columns`);
47
48 if (columns.length >= 4) {
49 const vehicleCode = columns[0].trim(); // Column A
50 const zoneFrom = columns[1].trim(); // Column B - Zone From
51 const zoneTo = columns[2].trim(); // Column C - Zone To
52 const rateText = columns[3].trim(); // Column D - Rate
53 const rate = parseFloat(rateText.replace(/[^0-9.-]/g, ''));
54
55 // Skip header row
56 if (vehicleCode === 'Vehicle Code' || zoneFrom === 'Zone From (Code)') {
57 console.log('Skipping header row');
58 return;
59 }
60
61 if (!isNaN(rate) && rate < 130 && rate > 0 && zoneFrom) {
62 data.push({
63 rowIndex: index + 2, // Approximate row number
64 zoneCode: zoneFrom,
65 zoneTo: zoneTo,
66 vehicleCode: vehicleCode,
67 rate: rate
68 });
69 console.log(`Found: Row ${index + 2}, Vehicle: ${vehicleCode}, Zone From: ${zoneFrom}, Zone To: ${zoneTo}, Rate: ${rate}`);
70 }
71 }
72 });
73
74 console.log(`Total found: ${data.length} entries with rates < 130`);
75 return data;
76
77 } catch (error) {
78 console.error('Error reading clipboard:', error);
79 alert('Could not read clipboard. Please make sure you copied the data first.');
80 return [];
81 }
82 }
83
84 async function extractSheetData() {
85 console.log('Extracting data from Google Sheets...');
86
87 const data = [];
88
89 // Try to find all divs that might contain cell data
90 const allCells = document.querySelectorAll('div[role="gridcell"]');
91 console.log(`Found ${allCells.length} cells with role="gridcell"`);
92
93 // Group cells by row
94 const rowMap = {};
95 allCells.forEach(cell => {
96 const rowIndex = cell.getAttribute('aria-rowindex');
97 const colIndex = cell.getAttribute('aria-colindex');
98
99 if (rowIndex && colIndex) {
100 if (!rowMap[rowIndex]) {
101 rowMap[rowIndex] = {};
102 }
103 rowMap[rowIndex][colIndex] = cell.textContent.trim();
104 }
105 });
106
107 console.log(`Found ${Object.keys(rowMap).length} rows`);
108
109 // Process the row map
110 Object.keys(rowMap).forEach(rowIndex => {
111 const row = rowMap[rowIndex];
112 const zoneCode = row['2']; // Column B
113 const rateText = row['4']; // Column D
114
115 if (zoneCode && rateText) {
116 const rate = parseFloat(rateText.replace(/[^0-9.-]/g, ''));
117
118 if (!isNaN(rate) && rate < 130 && rate > 0 && zoneCode) {
119 data.push({
120 rowIndex: parseInt(rowIndex),
121 zoneCode: zoneCode,
122 rate: rate
123 });
124 console.log(`Found: Row ${rowIndex}, Zone: ${zoneCode}, Rate: ${rate}`);
125 }
126 }
127 });
128
129 console.log(`Total found: ${data.length} entries with rates < 130`);
130 return data;
131 }
132
133 async function writePageNumbersToSheet(pageNumberMap) {
134 console.log('Writing page numbers to column H...');
135
136 // This function will write the page numbers to column H
137 // For each row in pageNumberMap, we need to click on the cell in column H and enter the page number
138
139 for (const [rowIndex, pageNumber] of Object.entries(pageNumberMap)) {
140 try {
141 // Find the cell in column H (aria-colindex="8") for this row
142 const cell = document.querySelector(`[role="gridcell"][aria-rowindex="${rowIndex}"][aria-colindex="8"]`);
143
144 if (cell) {
145 // Click the cell to select it
146 cell.click();
147 await new Promise(resolve => setTimeout(resolve, 100));
148
149 // Type the page number
150 const input = cell.querySelector('input') || cell;
151 if (input.tagName === 'INPUT') {
152 input.value = pageNumber;
153 input.dispatchEvent(new Event('input', { bubbles: true }));
154 } else {
155 // Use document.execCommand for contenteditable cells
156 document.execCommand('insertText', false, pageNumber.toString());
157 }
158
159 // Press Enter to confirm
160 const enterEvent = new KeyboardEvent('keydown', {
161 key: 'Enter',
162 code: 'Enter',
163 keyCode: 13,
164 bubbles: true
165 });
166 cell.dispatchEvent(enterEvent);
167
168 await new Promise(resolve => setTimeout(resolve, 100));
169 }
170 } catch (error) {
171 console.error(`Error writing to row ${rowIndex}:`, error);
172 }
173 }
174
175 console.log('Finished writing page numbers');
176 }
177
178 function createSheetsUI() {
179 // Check if button already exists
180 if (document.getElementById('limo-finder-button')) {
181 return;
182 }
183
184 const button = document.createElement('button');
185 button.id = 'limo-finder-button';
186 button.textContent = 'Find Limoanywhere Pages';
187 button.style.cssText = `
188 position: fixed;
189 top: 20px;
190 right: 20px;
191 background: #1a73e8;
192 color: white;
193 border: none;
194 border-radius: 4px;
195 padding: 10px 16px;
196 font-size: 14px;
197 font-weight: 500;
198 cursor: pointer;
199 box-shadow: 0 2px 8px rgba(0,0,0,0.15);
200 z-index: 10000;
201 font-family: 'Google Sans', Arial, sans-serif;
202 transition: background 0.2s;
203 `;
204
205 button.addEventListener('mouseenter', () => {
206 button.style.background = '#1557b0';
207 });
208
209 button.addEventListener('mouseleave', () => {
210 button.style.background = '#1a73e8';
211 });
212
213 button.addEventListener('click', async () => {
214 button.textContent = 'Extracting data...';
215 button.disabled = true;
216
217 try {
218 // First try automatic extraction
219 let sheetData = await extractSheetData();
220
221 // If no data found, try manual clipboard method
222 if (sheetData.length === 0) {
223 console.log('Automatic extraction failed, trying clipboard method...');
224 sheetData = await extractSheetDataManually();
225 }
226
227 if (sheetData.length === 0) {
228 alert('No entries found with rates < 130. Please make sure:\n1. You have scrolled through all filtered data\n2. Column B has zone codes\n3. Column D has rates\n4. Or try copying the data to clipboard first');
229 button.textContent = 'Find Limoanywhere Pages';
230 button.disabled = false;
231 return;
232 }
233
234 alert(`Found ${sheetData.length} entries with rates < 130. Opening Limoanywhere...`);
235
236 // Store the data in GM storage
237 await GM.setValue('sheetData', JSON.stringify(sheetData));
238
239 // Open Limoanywhere in a new tab
240 button.textContent = 'Opening Limoanywhere...';
241 await GM.openInTab('https://manage.mylimobiz.com/admin/manageRateZones.asp?stab=rateManagement&action=batchEdit&fixedRateMatrixId=18105', false);
242
243 button.textContent = 'Processing in Limoanywhere tab...';
244
245 } catch (error) {
246 console.error('Error:', error);
247 alert('Error: ' + error.message);
248 button.textContent = 'Find Limoanywhere Pages';
249 button.disabled = false;
250 }
251 });
252
253 document.body.appendChild(button);
254
255 // Create a second button to write results to column H
256 const writeButton = document.createElement('button');
257 writeButton.id = 'limo-write-button';
258 writeButton.textContent = 'Write Results to Column H';
259 writeButton.style.cssText = `
260 position: fixed;
261 top: 70px;
262 right: 20px;
263 background: #0f9d58;
264 color: white;
265 border: none;
266 border-radius: 4px;
267 padding: 10px 16px;
268 font-size: 14px;
269 font-weight: 500;
270 cursor: pointer;
271 box-shadow: 0 2px 8px rgba(0,0,0,0.15);
272 z-index: 10000;
273 font-family: 'Google Sans', Arial, sans-serif;
274 transition: background 0.2s;
275 `;
276
277 writeButton.addEventListener('mouseenter', () => {
278 writeButton.style.background = '#0d8043';
279 });
280
281 writeButton.addEventListener('mouseleave', () => {
282 writeButton.style.background = '#0f9d58';
283 });
284
285 writeButton.addEventListener('click', async () => {
286 writeButton.textContent = 'Preparing...';
287 writeButton.disabled = true;
288
289 try {
290 const pageNumberMapStr = await GM.getValue('pageNumberMap');
291 const sheetDataStr = await GM.getValue('sheetData');
292
293 if (!pageNumberMapStr || !sheetDataStr) {
294 alert('No results found. Please run the search in Limoanywhere first.');
295 writeButton.textContent = 'Write Results to Column H';
296 writeButton.disabled = false;
297 return;
298 }
299
300 const pageNumberMap = JSON.parse(pageNumberMapStr);
301 const sheetData = JSON.parse(sheetDataStr);
302
303 // Create a map of zone codes to page numbers
304 const zoneToPageMap = {};
305 sheetData.forEach(entry => {
306 if (pageNumberMap[entry.rowIndex]) {
307 zoneToPageMap[entry.zoneCode] = pageNumberMap[entry.rowIndex];
308 }
309 });
310
311 const foundCount = Object.keys(zoneToPageMap).length;
312
313 // Create a simple text format: Zone Code = Page Number
314 let resultsText = 'Zone Code\tPage Number\n';
315 Object.entries(zoneToPageMap).sort().forEach(([zone, page]) => {
316 resultsText += `${zone}\t${page}\n`;
317 });
318
319 // Copy to clipboard
320 await GM.setClipboard(resultsText);
321
322 alert(`Found ${foundCount} page numbers!\n\nThe results have been copied to your clipboard in this format:\nZone Code → Page Number\n\nYou can:\n1. Paste this into a new column to see the mapping\n2. Use VLOOKUP to match zone codes and fill column H\n\nFormula for H2: =VLOOKUP(B2, PasteRange, 2, FALSE)`);
323
324 writeButton.textContent = 'Write Results to Column H';
325 writeButton.disabled = false;
326
327 } catch (error) {
328 console.error('Error:', error);
329 alert('Error: ' + error.message);
330 writeButton.textContent = 'Write Results to Column H';
331 writeButton.disabled = false;
332 }
333 });
334
335 document.body.appendChild(writeButton);
336
337 console.log('Google Sheets UI created');
338 }
339
340 // ========== LIMOANYWHERE FUNCTIONS ==========
341
342 async function searchLimoanywhere() {
343 console.log('Starting Limoanywhere search...');
344
345 // Get the sheet data from storage
346 const sheetDataStr = await GM.getValue('sheetData');
347 if (!sheetDataStr) {
348 alert('No sheet data found. Please run the extension from Google Sheets first.');
349 return;
350 }
351
352 const sheetData = JSON.parse(sheetDataStr);
353 console.log(`Searching for ${sheetData.length} zone codes`);
354
355 // Get or initialize the search state
356 let searchStateStr = await GM.getValue('searchState');
357 let searchState;
358
359 if (!searchStateStr) {
360 // Create a map of zone codes to search for
361 const zoneCodeMap = {};
362 sheetData.forEach(entry => {
363 if (!zoneCodeMap[entry.zoneCode]) {
364 zoneCodeMap[entry.zoneCode] = [];
365 }
366 zoneCodeMap[entry.zoneCode].push(entry);
367 });
368
369 // Get current page from URL
370 const urlParams = new URLSearchParams(window.location.search);
371 const currentPageFromUrl = parseInt(urlParams.get('pNum')) || 1;
372
373 searchState = {
374 currentPage: currentPageFromUrl,
375 totalPages: 815, // FULL MODE: Search all pages
376 pageNumberMap: {},
377 zoneCodeMap: zoneCodeMap,
378 isSearching: true
379 };
380 } else {
381 searchState = JSON.parse(searchStateStr);
382 // Update current page from URL
383 const urlParams = new URLSearchParams(window.location.search);
384 const currentPageFromUrl = parseInt(urlParams.get('pNum')) || 1;
385 searchState.currentPage = currentPageFromUrl;
386 }
387
388 const { currentPage, totalPages, pageNumberMap, zoneCodeMap } = searchState;
389
390 console.log(`Processing page ${currentPage}/${totalPages}...`);
391
392 // Get entries from current page
393 const entries = getCurrentPageEntries();
394 console.log(`Found ${entries.length} entries on page ${currentPage}`);
395
396 // Check each entry against our zone codes
397 entries.forEach(entry => {
398 if (zoneCodeMap[entry.zoneFrom]) {
399 // Found a match! Record the page number for all rows with this zone code
400 zoneCodeMap[entry.zoneFrom].forEach(sheetEntry => {
401 pageNumberMap[sheetEntry.rowIndex] = currentPage;
402 });
403 console.log(`Match found: ${entry.zoneFrom} on page ${currentPage}`);
404 }
405 });
406
407 // Update status
408 const foundCount = Object.keys(pageNumberMap).length;
409 updateStatusPanel(currentPage, totalPages, foundCount, sheetData.length);
410
411 // Save progress BEFORE navigating
412 searchState.pageNumberMap = pageNumberMap;
413 searchState.currentPage = currentPage + 1;
414 await GM.setValue('searchState', JSON.stringify(searchState));
415 console.log(`Saved progress: page ${currentPage}, found ${foundCount}`);
416
417 // Move to next page or finish
418 if (currentPage < totalPages) {
419 // Navigate to next page
420 console.log(`Navigating to page ${currentPage + 1}...`);
421 await new Promise(resolve => setTimeout(resolve, 500));
422 await goToPage(currentPage + 1);
423 } else {
424 // Search complete!
425 console.log('Search complete!');
426 console.log('Page number map:', pageNumberMap);
427
428 // Store the final results
429 await GM.setValue('pageNumberMap', JSON.stringify(pageNumberMap));
430
431 // Clear search state
432 await GM.deleteValue('searchState');
433
434 // Show completion message
435 showCompletionMessage(pageNumberMap, sheetData.length);
436 }
437 }
438
439 function getCurrentPageEntries() {
440 const entries = [];
441 const rows = document.querySelectorAll('#AutoNumber3 tr');
442
443 for (let i = 1; i < rows.length; i++) {
444 const inputs = rows[i].querySelectorAll('input[name="rateZoneFrom"], input[name="rateZoneTo"], input[name="rateZoneRate"]');
445 if (inputs.length >= 3) {
446 entries.push({
447 zoneFrom: inputs[0].value,
448 zoneTo: inputs[1].value,
449 rate: parseFloat(inputs[2].value)
450 });
451 }
452 }
453
454 return entries;
455 }
456
457 async function goToPage(pageNumber) {
458 const pageSelect = document.querySelector('select[name="pageNo"]');
459 if (pageSelect) {
460 pageSelect.selectedIndex = pageNumber - 1;
461 pageSelect.dispatchEvent(new Event('change', { bubbles: true }));
462
463 // Alternative: navigate directly
464 const url = `https://manage.mylimobiz.com/admin/manageRateZones.asp?stab=rateManagement&action=batchEdit&fixedRateMatrixId=18105&rateZoneFrom=&rateZoneTo=&rateZoneVehType=&filter=&pNum=${pageNumber}&pageSize=50`;
465 window.location.href = url;
466 }
467 }
468
469 function createLimoUI() {
470 // Create status panel
471 const panel = document.createElement('div');
472 panel.id = 'limo-status-panel';
473 panel.style.cssText = `
474 position: fixed;
475 top: 20px;
476 right: 20px;
477 width: 350px;
478 background: white;
479 border: 2px solid #1a73e8;
480 border-radius: 8px;
481 box-shadow: 0 4px 16px rgba(0,0,0,0.2);
482 z-index: 10000;
483 font-family: Arial, sans-serif;
484 padding: 20px;
485 `;
486
487 panel.innerHTML = `
488 <h3 style="margin: 0 0 15px 0; color: #1a73e8;">Limoanywhere Search</h3>
489 <div id="limo-status-content">
490 <p>Initializing...</p>
491 </div>
492 <button id="limo-start-button" style="
493 width: 100%;
494 background: #1a73e8;
495 color: white;
496 border: none;
497 border-radius: 4px;
498 padding: 10px;
499 font-size: 14px;
500 font-weight: 500;
501 cursor: pointer;
502 margin-top: 15px;
503 ">Start Search</button>
504 `;
505
506 document.body.appendChild(panel);
507
508 // Add button handler
509 document.getElementById('limo-start-button').addEventListener('click', async () => {
510 document.getElementById('limo-start-button').disabled = true;
511 document.getElementById('limo-start-button').textContent = 'Searching...';
512 await searchLimoanywhere();
513 });
514
515 // Check if there's a search in progress and auto-resume
516 GM.getValue('searchState').then(searchStateStr => {
517 if (searchStateStr) {
518 const searchState = JSON.parse(searchStateStr);
519 if (searchState.isSearching) {
520 console.log('Resuming search automatically...');
521 document.getElementById('limo-start-button').disabled = true;
522 document.getElementById('limo-start-button').textContent = 'Searching...';
523 setTimeout(() => {
524 searchLimoanywhere();
525 }, 1000);
526 }
527 }
528 });
529
530 console.log('Limoanywhere UI created');
531 }
532
533 function updateStatusPanel(currentPage, totalPages, foundCount, totalCount) {
534 const content = document.getElementById('limo-status-content');
535 if (content) {
536 const percentage = Math.round((currentPage / totalPages) * 100);
537 content.innerHTML = `
538 <p><strong>Progress:</strong> Page ${currentPage} of ${totalPages} (${percentage}%)</p>
539 <p><strong>Found:</strong> ${foundCount} of ${totalCount} entries</p>
540 <div style="width: 100%; background: #e0e0e0; border-radius: 4px; height: 20px; margin-top: 10px;">
541 <div style="width: ${percentage}%; background: #1a73e8; height: 100%; border-radius: 4px; transition: width 0.3s;"></div>
542 </div>
543 `;
544 }
545 }
546
547 function showCompletionMessage(pageNumberMap, totalCount) {
548 const content = document.getElementById('limo-status-content');
549 if (content) {
550 const foundCount = Object.keys(pageNumberMap).length;
551 content.innerHTML = `
552 <p style="color: #0f9d58; font-weight: bold;">✓ Search Complete!</p>
553 <p><strong>Found:</strong> ${foundCount} of ${totalCount} entries</p>
554 <p style="margin-top: 15px;">The page numbers have been saved. You can now return to Google Sheets to write them to column H.</p>
555 <button id="copy-results-button" style="
556 width: 100%;
557 background: #0f9d58;
558 color: white;
559 border: none;
560 border-radius: 4px;
561 padding: 10px;
562 font-size: 14px;
563 font-weight: 500;
564 cursor: pointer;
565 margin-top: 10px;
566 ">Copy Results to Clipboard</button>
567 `;
568
569 document.getElementById('copy-results-button').addEventListener('click', async () => {
570 const resultsText = JSON.stringify(pageNumberMap, null, 2);
571 await GM.setClipboard(resultsText);
572 alert('Results copied to clipboard!');
573 });
574 }
575 }
576
577 // ========== INITIALIZATION ==========
578
579 function init() {
580 if (document.readyState === 'loading') {
581 document.addEventListener('DOMContentLoaded', init);
582 return;
583 }
584
585 // Check if we're on Google Sheets
586 if (window.location.href.includes('docs.google.com/spreadsheets')) {
587 console.log('On Google Sheets page, initializing...');
588 setTimeout(() => {
589 createSheetsUI();
590 }, 2000);
591 }
592
593 // Check if we're on Limoanywhere
594 else if (window.location.href.includes('manage.mylimobiz.com')) {
595 console.log('On Limoanywhere page, initializing...');
596 setTimeout(() => {
597 createLimoUI();
598 }, 2000);
599 }
600 }
601
602 init();
603})();