// ==UserScript==
// @name Pixifi Master Tool - Search
// @namespace http://tampermonkey.net/
// @version 1.0
// @description A standalone script that registers a Lead/Client search tool with the Master Tools window.
// @match https://www.pixifi.com/admin/*
// @license GPL
// @grant none
// ==/UserScript==
(function () {
'use strict';
/**
* We'll define our search tool here. We use a function so we can call it later.
*/
const mySearchTool = {
name: '',
domainRegex: /https:\/\/www\.pixifi\.com\/admin\/leads\//, // Only render on leads/* pages
render(parentContainer) {
const searchQueryInput = document.createElement('input');
Object.assign(searchQueryInput.style, {
width: '200px',
marginBottom: '5px',
padding: '5px',
border: '1px solid #ccc',
borderRadius: '3px',
display: 'block'
});
searchQueryInput.type = 'text';
searchQueryInput.placeholder = 'Search Query';
const searchButton = document.createElement('button');
Object.assign(searchButton.style, {
display: 'block',
padding: '5px 10px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontWeight: 'bold',
textAlign: 'center'
});
searchButton.textContent = 'Search';
searchButton.addEventListener('click', () => {
const searchQuery = searchQueryInput.value.trim();
if (searchQuery) {
searchLeadsAndClients(searchQuery);
} else {
alert('Please enter a Search Query.');
}
});
parentContainer.appendChild(searchQueryInput);
parentContainer.appendChild(searchButton);
}
};
/*************************************************************************/
/* HELPER #1: Show a modal that lists possible results */
/*************************************************************************/
function showSelectionModal(links, type) {
const modal = document.createElement('div');
Object.assign(modal.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: '#fff',
padding: '20px',
borderRadius: '10px',
boxShadow: '0px 4px 6px rgba(0,0,0,0.3)',
zIndex: '2000',
fontFamily: 'Arial, sans-serif',
maxWidth: '400px',
overflowY: 'auto'
});
// We'll insert a UL to list the leads/clients
modal.innerHTML = `
<h3>Select a ${type}</h3>
<ul id="modalListContainer" style="list-style: none; padding: 0; margin: 0;">
${links
.map(
(link, index) =>
`<li style="margin-bottom: 10px;">
<button style="width: 100%; padding: 10px; text-align: left;" data-index="${index}">
${link.innerText || link.getAttribute('href')}
</button>
</li>`
)
.join('')}
</ul>
<button id="closeModal" style="
display: block;
margin: 20px auto 0;
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;">
Close
</button>
`;
// If more than 8 items, set the UL to have a fixed max-height and auto-scroll
if (links.length > 8) {
const listContainer = modal.querySelector('#modalListContainer');
Object.assign(listContainer.style, {
maxHeight: '480px',
overflowY: 'auto'
});
}
// Link button click => open the URL in a new tab
modal.querySelectorAll('button[data-index]').forEach(button => {
button.addEventListener('click', e => {
const index = e.target.getAttribute('data-index');
const link = links[index].getAttribute('href');
const absoluteLink = new URL(link, window.location.origin).href;
window.open(absoluteLink, '_blank');
});
});
// Close button
modal.querySelector('#closeModal').addEventListener('click', () => {
document.body.removeChild(modal);
});
document.body.appendChild(modal);
}
/*************************************************************************/
/* HELPER #2: Check if a string might be a phone number, ignoring punctuation */
/*************************************************************************/
function isPhoneNumberLike(query) {
// Strip out parentheses, spaces, and dashes
const digits = query.replace(/[\(\)\s\-]/g, '');
// A simple check: if it’s 10 or 11 digits, call it a “phone number”
return /^\d{10,11}$/.test(digits);
}
/*************************************************************************/
/* HELPER #3: Generate all standard phone number variants from digits */
/*************************************************************************/
function getPhoneNumberVariants(rawQuery) {
// 1) Strip to just digits
const digits = rawQuery.replace(/\D/g, '');
// Because we might have 10 or 11 digits, handle that
// For example: 10-digit => 1234567890
// or 11-digit => 11234567890
// If 11-digit, we assume the first digit might be a leading country code (1).
// You may or may not want that logic. This is just an example.
// In this example, we’ll assume we want to handle 10-digit only:
// If the user typed 11 digits, but the leading digit is "1," we’ll strip it to 10 for the variants below.
let phone10 = digits;
if (digits.length === 11 && digits.startsWith('1')) {
phone10 = digits.substring(1);
}
// If it’s not exactly 10 at this point, just return [digits] or handle differently
if (phone10.length !== 10) {
return [digits];
}
// phone10 is now something like "1234567890"
// Generate the possible variations. Examples:
// 1. (123) 456-7890
// 2. (123) 4567890
// 3. (123)4567890
// 4. 1234567890
// 5. 123 456-7890
// 6. 123 4567890
// 7. 123 456 7890
// ...
// You can generate as many as you need:
const area = phone10.substring(0, 3); // 123
const prefix = phone10.substring(3, 6); // 456
const line = phone10.substring(6); // 7890
return [
`(${area}) ${prefix}-${line}`,
`(${area}) ${prefix}${line}`,
`(${area})${prefix}${line}`,
`${area}${prefix}${line}`,
`${area} ${prefix}-${line}`,
`${area} ${prefix}${line}`,
`${area} ${prefix} ${line}`,
// ...add any other permutations you want
];
}
/*************************************************************************/
/* HELPER #4: Make a POST call to lead-search or client-search */
/*************************************************************************/
// We'll extract “getLeads” and “getClients” into separate functions
// that simply return the array of DOM <a> elements (or an empty array).
async function getLeads(searchQuery) {
try {
const response = await fetch("https://www.pixifi.com/admin/fn/leads/getLeads/", {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"x-requested-with": "XMLHttpRequest"
},
referrer: "https://www.pixifi.com/admin/leads/",
referrerPolicy: "strict-origin-when-cross-origin",
body: new URLSearchParams({
clientID: "12295",
page: 1,
section: "id",
searchQuery,
dir: "D",
viewFilter: "all"
}).toString(),
method: "POST",
mode: "cors",
credentials: "include"
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const leadLinks = [...doc.querySelectorAll('a[href^="/admin/leads/"]')]
.filter(link => {
const txt = link.textContent
.replace(/\u00A0/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return txt.length > 0;
})
.filter((link, index, self) => {
const href = link.getAttribute('href');
return (
index === self.findIndex(otherLink => otherLink.getAttribute('href') === href)
);
});
return leadLinks;
} catch (error) {
console.error('getLeads Error:', error);
// Return an empty array so that the caller can handle “nothing found”
return [];
}
}
async function getClients(searchQuery) {
try {
const response = await fetch("https://www.pixifi.com/admin/fn/clients/getClientListing/", {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"x-requested-with": "XMLHttpRequest"
},
referrer: "https://www.pixifi.com/admin/clients/",
referrerPolicy: "strict-origin-when-cross-origin",
body: new URLSearchParams({
clientID: "12295",
page: 1,
searchQuery,
section: "name",
dir: "A",
archived: "unarchived",
card: "all"
}).toString(),
method: "POST",
mode: "cors",
credentials: "include"
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const clientLinks = [...doc.querySelectorAll('a[href^="/admin/clients/"]')]
.filter(link => {
const txt = link.textContent
.replace(/\u00A0/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return txt.length > 0;
})
.filter(link => !link.getAttribute('href').startsWith('/admin/clients/delete/'))
.filter((link, index, self) => {
const href = link.getAttribute('href');
return (
index === self.findIndex(otherLink => otherLink.getAttribute('href') === href)
);
});
return clientLinks;
} catch (error) {
console.error('getClients Error:', error);
return [];
}
}
/*************************************************************************/
/* MAIN SEARCH FLOW: Try leads, then clients, then phone expansions */
/*************************************************************************/
async function searchLeadsAndClients(searchQuery) {
console.log(`Searching leads with query: ${searchQuery}`);
const leadLinks = await getLeads(searchQuery);
if (leadLinks.length > 0) {
showSelectionModal(leadLinks, 'Lead');
return;
}
console.warn('No leads found, now searching clients...');
const clientLinks = await getClients(searchQuery);
if (clientLinks.length > 0) {
showSelectionModal(clientLinks, 'Client');
return;
}
console.warn('No clients found. Checking if the query is phone-like...');
if (isPhoneNumberLike(searchQuery)) {
// Generate phone number variants
console.log('Query looks like a phone number. Searching all variants in parallel...');
const phoneVariants = getPhoneNumberVariants(searchQuery);
// Run leads/clients calls in parallel for each variant
// We’ll gather all leads, all clients into a single array.
const allFetches = phoneVariants.map(async variant => {
const theseLeads = await getLeads(variant);
const theseClients = await getClients(variant);
return { variant, leads: theseLeads, clients: theseClients };
});
const results = await Promise.all(allFetches);
// Flatten out all the leads and all the clients from each variant
let combinedLeads = [];
let combinedClients = [];
results.forEach(r => {
if (r.leads.length > 0) {
combinedLeads = combinedLeads.concat(r.leads);
}
if (r.clients.length > 0) {
combinedClients = combinedClients.concat(r.clients);
}
});
// Deduplicate by href
const uniqueLeads = deduplicateByHref(combinedLeads);
const uniqueClients = deduplicateByHref(combinedClients);
// Show leads if any
if (uniqueLeads.length > 0) {
showSelectionModal(uniqueLeads, 'Lead (Phone Variants)');
}
// Show clients if any
if (uniqueClients.length > 0) {
showSelectionModal(uniqueClients, 'Client (Phone Variants)');
}
// If still nothing
if (uniqueLeads.length === 0 && uniqueClients.length === 0) {
alert('No leads or clients found (even after phone expansions).');
}
} else {
alert('No leads or clients found. Please refine your search.');
}
}
function deduplicateByHref(links) {
const seen = new Set();
return links.filter(link => {
const href = link.getAttribute('href');
if (seen.has(href)) {
return false;
}
seen.add(href);
return true;
});
}
/*************************************************************************/
/* Attempt to register our tool with the MasterTools if it exists */
/*************************************************************************/
const MAX_ATTEMPTS = 10;
let attempts = 0;
function tryRegisterSearchTool() {
if (window.MasterTools && typeof window.MasterTools.registerTool === 'function') {
window.MasterTools.registerTool(mySearchTool);
} else if (attempts < MAX_ATTEMPTS) {
attempts++;
setTimeout(tryRegisterSearchTool, 500);
} else {
console.warn('Master Tools not found. The Search Tool will not be registered.');
}
}
tryRegisterSearchTool();
})();