Skip to main content
Glama

JobNimbus MCP Remote Server

stamford-pending-signature-report.html24.6 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Stamford - Pending Customer Approval Report</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #333; padding: 20px; min-height: 100vh; } .container { max-width: 1600px; margin: 0 auto; background: white; border-radius: 20px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); overflow: hidden; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; text-align: center; } .header h1 { font-size: 2.5rem; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); } .header p { font-size: 1.2rem; opacity: 0.95; } .summary-section { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; padding: 40px; background: #f8f9fa; } .summary-card { background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: transform 0.3s ease, box-shadow 0.3s ease; } .summary-card:hover { transform: translateY(-5px); box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2); } .summary-card h3 { font-size: 0.9rem; color: #666; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; } .summary-card .value { font-size: 2rem; font-weight: bold; color: #667eea; margin-bottom: 5px; } .summary-card .subtitle { font-size: 0.85rem; color: #999; } .content { padding: 40px; } .filters { background: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 30px; } .filter-group { display: flex; gap: 15px; flex-wrap: wrap; align-items: center; } .filter-label { font-weight: 600; color: #667eea; margin-right: 10px; } input[type="text"], select { padding: 8px 15px; border: 2px solid #ddd; border-radius: 8px; font-size: 0.9rem; transition: border-color 0.3s ease; } input[type="text"]:focus, select:focus { outline: none; border-color: #667eea; } .table-container { overflow-x: auto; background: white; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } table { width: 100%; border-collapse: collapse; min-width: 1200px; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } th { padding: 15px; text-align: left; font-weight: 600; text-transform: uppercase; font-size: 0.85rem; letter-spacing: 0.5px; } th.sortable { cursor: pointer; user-select: none; } th.sortable:hover { background: rgba(255, 255, 255, 0.1); } tbody tr { border-bottom: 1px solid #eee; transition: background-color 0.2s ease; } tbody tr:hover { background-color: #f8f9fa; } td { padding: 15px; font-size: 0.9rem; } .job-number { font-weight: 600; color: #667eea; } .estimate-value { font-weight: 700; color: #28a745; font-size: 1.1rem; } .estimate-value.high { color: #dc3545; } .estimate-value.medium { color: #ffc107; } .sales-rep { display: inline-block; padding: 5px 12px; background: #e3f2fd; color: #1976d2; border-radius: 20px; font-size: 0.85rem; font-weight: 500; } .address { color: #666; font-size: 0.85rem; } .date-badge { display: inline-block; padding: 4px 10px; background: #f0f0f0; border-radius: 15px; font-size: 0.8rem; color: #666; } .priority-badge { display: inline-block; padding: 4px 10px; border-radius: 15px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; } .priority-high { background: #fee; color: #c00; } .priority-medium { background: #fff3cd; color: #856404; } .priority-low { background: #d4edda; color: #155724; } .no-estimate { color: #999; font-style: italic; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 30px; } .stat-card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stat-card h4 { color: #667eea; margin-bottom: 15px; font-size: 1.1rem; } .stat-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; } .stat-item:last-child { border-bottom: none; } .stat-label { color: #666; font-size: 0.9rem; } .stat-value { font-weight: 600; color: #333; } @media (max-width: 768px) { .header h1 { font-size: 1.8rem; } .summary-section { grid-template-columns: 1fr; padding: 20px; } .content { padding: 20px; } table { font-size: 0.85rem; } } .action-buttons { display: flex; gap: 10px; margin-bottom: 20px; } .btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 0.9rem; font-weight: 600; transition: all 0.3s ease; } .btn-primary { background: #667eea; color: white; } .btn-primary:hover { background: #5568d3; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3); } .btn-secondary { background: #6c757d; color: white; } .btn-secondary:hover { background: #5a6268; } </style> </head> <body> <div class="container"> <div class="header"> <h1>Stamford - Pending Customer Approval Report</h1> <p>Jobs Awaiting Customer Signature - January-October 2025</p> </div> <div class="summary-section"> <div class="summary-card"> <h3>Total Pending Jobs</h3> <div class="value" id="totalJobs">0</div> <div class="subtitle">Awaiting customer signature</div> </div> <div class="summary-card"> <h3>Total Estimate Value</h3> <div class="value" id="totalValue">$0</div> <div class="subtitle">Combined estimate amount</div> </div> <div class="summary-card"> <h3>Average Job Value</h3> <div class="value" id="avgValue">$0</div> <div class="subtitle">Mean estimate per job</div> </div> <div class="summary-card"> <h3>Highest Value Job</h3> <div class="value" id="maxValue">$0</div> <div class="subtitle">Largest pending estimate</div> </div> </div> <div class="content"> <div class="filters"> <div class="filter-group"> <span class="filter-label">Search:</span> <input type="text" id="searchInput" placeholder="Search by customer, job name, or address..." style="width: 300px;"> <span class="filter-label">Sales Rep:</span> <select id="salesRepFilter"> <option value="">All Sales Reps</option> </select> <span class="filter-label">Sort By:</span> <select id="sortSelect"> <option value="value-desc">Highest Value First</option> <option value="value-asc">Lowest Value First</option> <option value="date-desc">Newest First</option> <option value="date-asc">Oldest First</option> </select> </div> </div> <div class="action-buttons"> <button class="btn btn-primary" onclick="exportToCSV()">Export to CSV</button> <button class="btn btn-secondary" onclick="window.print()">Print Report</button> </div> <div class="table-container"> <table id="jobsTable"> <thead> <tr> <th class="sortable" onclick="sortTable('number')">Job #</th> <th class="sortable" onclick="sortTable('name')">Job Name</th> <th class="sortable" onclick="sortTable('customer')">Customer</th> <th class="sortable" onclick="sortTable('estimate')">Estimate</th> <th>Sales Rep</th> <th>Address</th> <th class="sortable" onclick="sortTable('date')">Date Created</th> <th>Days Pending</th> <th>Priority</th> </tr> </thead> <tbody id="tableBody"> <!-- Data will be populated by JavaScript --> </tbody> </table> </div> <div class="stats-grid"> <div class="stat-card"> <h4>By Sales Representative</h4> <div id="salesRepStats"></div> </div> <div class="stat-card"> <h4>Value Distribution</h4> <div id="valueDistribution"></div> </div> <div class="stat-card"> <h4>Age Distribution</h4> <div id="ageDistribution"></div> </div> </div> </div> </div> <script> // Pending Jobs Data - Filtered from JobNimbus const pendingJobs = [ {"number": "1874", "name": "99 Huckleberry Hill Rd - Slate Repairs", "customer": "Jack Montgomery Deign", "estimate": 2790, "sales_rep": "Deivis Castro", "address": "99 Huckleberry Hill Rd, New Canaan, CT", "date": "2025-10-08"}, {"number": "1864", "name": "406 Courtland Avenue", "customer": "Courtland Manor Condominiums", "estimate": 60869, "sales_rep": "Deivis Castro", "address": "406 Courtland Avenue, Stamford, CT", "date": "2025-10-02"}, {"number": "1863", "name": "165 N Pasture Lane", "customer": "Raymond Ayoub", "estimate": 21270, "sales_rep": "Bill Tyson", "address": "165 N Pasture Lane, Stratford, CT", "date": "2025-10-02"}, {"number": "1861", "name": "220 Banks Road", "customer": "Mathew Degennaro", "estimate": 0, "sales_rep": "Bill Tyson", "address": "220 Banks Road, Easton, CT", "date": "2025-10-01"}, {"number": "1858", "name": "11Sport Hill Road", "customer": "Danny Rodrigues", "estimate": 9497, "sales_rep": "Deivis Castro", "address": "11 Sport Hill Road, Easton, CT", "date": "2025-09-29"}, {"number": "1853", "name": "57 Belltown Rd Repairs", "customer": "Shaun Azari", "estimate": 6200, "sales_rep": "Deivis Castro", "address": "57 Belltown Rd, Stamford, CT", "date": "2025-09-22"}, {"number": "1850", "name": "68 Leonard St - Wood Shingle", "customer": "Al Candito", "estimate": 28800, "sales_rep": "Deivis Castro", "address": "68 Leonard St, Stamford, CT", "date": "2025-09-22"}, {"number": "1835", "name": "28 Raymond Ter - Roof & Skylight", "customer": "Susan Gonzales", "estimate": 13132, "sales_rep": "Deivis Castro", "address": "28 Raymond Terrace, Norwalk, CT", "date": "2025-09-08"}, {"number": "1831", "name": "22 Timber Ln Wood Shakes Siding Repairs", "customer": "Sara Wallace", "estimate": 1250, "sales_rep": "Deivis Castro", "address": "22 Timber Ln, Wesport, CT", "date": "2025-09-08"}, {"number": "1826", "name": "146 Ridgecrest Rd Roof Replacement", "customer": "Kimberly Quinones", "estimate": 41341, "sales_rep": "Deivis Castro", "address": "146 Ridgecrest Rd, Stamford, CT", "date": "2025-09-03"}, {"number": "1824", "name": "52 Shinnacock Trail House Paint and Repairs", "customer": "Gypsy Rios", "estimate": 16982, "sales_rep": "Deivis Castro", "address": "52 Shinnacock Trail, Shelton, CT", "date": "2025-09-02"}, {"number": "1816", "name": "47 Shadow Ln", "customer": "Mike Thomas (4349573)", "estimate": 29574, "sales_rep": "Deivis Castro", "address": "47 Shadow Ln, Ridgefield, CT", "date": "2025-08-27"}, {"number": "1803", "name": "111 Mulberry Street, Roof Replacement", "customer": "Jeff Chirico", "estimate": 22236, "sales_rep": "Deivis Castro", "address": "111 Mulberry Street,, Stamford, CT", "date": "2025-08-19"}, {"number": "1800", "name": "46 Dandy Dr Roof Replacement", "customer": "Patrick Dean", "estimate": 77016, "sales_rep": "Deivis Castro", "address": "46 Dandy Dr, CosCob, CT", "date": "2025-08-15"}, {"number": "1794", "name": "700 Steamboat Rd Slate Repairs/Replacement", "customer": "Elizabeth Sahlin", "estimate": 4711, "sales_rep": "Deivis Castro", "address": "700 Steamboat Rd, Greenwich,, CT", "date": "2025-08-07"}, {"number": "1791", "name": "14 Nursery Ct Roof Replacement & Skylights Installation", "customer": "Sean M", "estimate": 29193, "sales_rep": "Deivis Castro", "address": "14 Nursery Ct, Norwalk, CT", "date": "2025-08-06"} ]; let filteredJobs = [...pendingJobs]; function calculateDaysPending(dateStr) { const created = new Date(dateStr); const now = new Date(); const diffTime = Math.abs(now - created); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; } function getPriority(estimate, daysPending) { if (estimate > 50000 || daysPending > 60) return 'high'; if (estimate > 20000 || daysPending > 30) return 'medium'; return 'low'; } function renderTable() { const tbody = document.getElementById('tableBody'); tbody.innerHTML = ''; filteredJobs.forEach(job => { const daysPending = calculateDaysPending(job.date); const priority = getPriority(job.estimate, daysPending); const estimateClass = job.estimate > 50000 ? 'high' : job.estimate > 20000 ? 'medium' : ''; const row = ` <tr> <td class="job-number">#${job.number}</td> <td>${job.name}</td> <td>${job.customer}</td> <td class="estimate-value ${estimateClass}"> ${job.estimate > 0 ? '$' + job.estimate.toLocaleString('en-US') : '<span class="no-estimate">No estimate</span>'} </td> <td><span class="sales-rep">${job.sales_rep || 'Unassigned'}</span></td> <td class="address">${job.address}</td> <td><span class="date-badge">${job.date}</span></td> <td>${daysPending} days</td> <td><span class="priority-badge priority-${priority}">${priority}</span></td> </tr> `; tbody.innerHTML += row; }); } function updateSummary() { const jobsWithEstimate = filteredJobs.filter(j => j.estimate > 0); const totalValue = jobsWithEstimate.reduce((sum, j) => sum + j.estimate, 0); const avgValue = jobsWithEstimate.length > 0 ? totalValue / jobsWithEstimate.length : 0; const maxValue = jobsWithEstimate.length > 0 ? Math.max(...jobsWithEstimate.map(j => j.estimate)) : 0; document.getElementById('totalJobs').textContent = filteredJobs.length; document.getElementById('totalValue').textContent = '$' + totalValue.toLocaleString('en-US'); document.getElementById('avgValue').textContent = '$' + Math.round(avgValue).toLocaleString('en-US'); document.getElementById('maxValue').textContent = '$' + maxValue.toLocaleString('en-US'); } function updateStats() { // Sales Rep Stats const salesRepStats = {}; filteredJobs.forEach(job => { const rep = job.sales_rep || 'Unassigned'; if (!salesRepStats[rep]) { salesRepStats[rep] = { count: 0, value: 0 }; } salesRepStats[rep].count++; salesRepStats[rep].value += job.estimate; }); const salesRepHTML = Object.entries(salesRepStats) .sort((a, b) => b[1].value - a[1].value) .map(([rep, stats]) => ` <div class="stat-item"> <span class="stat-label">${rep}</span> <span class="stat-value">${stats.count} jobs ($${stats.value.toLocaleString('en-US')})</span> </div> `).join(''); document.getElementById('salesRepStats').innerHTML = salesRepHTML; // Value Distribution const ranges = [ { label: '$0 - $10K', min: 0, max: 10000 }, { label: '$10K - $30K', min: 10000, max: 30000 }, { label: '$30K - $50K', min: 30000, max: 50000 }, { label: '$50K+', min: 50000, max: Infinity } ]; const valueDistHTML = ranges.map(range => { const count = filteredJobs.filter(j => j.estimate >= range.min && j.estimate < range.max).length; return ` <div class="stat-item"> <span class="stat-label">${range.label}</span> <span class="stat-value">${count} jobs</span> </div> `; }).join(''); document.getElementById('valueDistribution').innerHTML = valueDistHTML; // Age Distribution const ageRanges = [ { label: '0-15 days', min: 0, max: 15 }, { label: '16-30 days', min: 16, max: 30 }, { label: '31-60 days', min: 31, max: 60 }, { label: '60+ days', min: 61, max: Infinity } ]; const ageDistHTML = ageRanges.map(range => { const count = filteredJobs.filter(j => { const days = calculateDaysPending(j.date); return days >= range.min && days <= range.max; }).length; return ` <div class="stat-item"> <span class="stat-label">${range.label}</span> <span class="stat-value">${count} jobs</span> </div> `; }).join(''); document.getElementById('ageDistribution').innerHTML = ageDistHTML; } function populateFilters() { const salesReps = [...new Set(pendingJobs.map(j => j.sales_rep || 'Unassigned'))].sort(); const salesRepFilter = document.getElementById('salesRepFilter'); salesReps.forEach(rep => { const option = document.createElement('option'); option.value = rep; option.textContent = rep; salesRepFilter.appendChild(option); }); } function applyFilters() { const searchTerm = document.getElementById('searchInput').value.toLowerCase(); const salesRep = document.getElementById('salesRepFilter').value; filteredJobs = pendingJobs.filter(job => { const matchesSearch = !searchTerm || job.name.toLowerCase().includes(searchTerm) || job.customer.toLowerCase().includes(searchTerm) || job.address.toLowerCase().includes(searchTerm); const matchesSalesRep = !salesRep || (job.sales_rep || 'Unassigned') === salesRep; return matchesSearch && matchesSalesRep; }); applySorting(); } function applySorting() { const sortBy = document.getElementById('sortSelect').value; switch(sortBy) { case 'value-desc': filteredJobs.sort((a, b) => b.estimate - a.estimate); break; case 'value-asc': filteredJobs.sort((a, b) => a.estimate - b.estimate); break; case 'date-desc': filteredJobs.sort((a, b) => new Date(b.date) - new Date(a.date)); break; case 'date-asc': filteredJobs.sort((a, b) => new Date(a.date) - new Date(b.date)); break; } renderTable(); updateSummary(); updateStats(); } function exportToCSV() { const headers = ['Job #', 'Job Name', 'Customer', 'Estimate', 'Sales Rep', 'Address', 'Date Created', 'Days Pending']; const rows = filteredJobs.map(job => [ job.number, job.name, job.customer, job.estimate, job.sales_rep || 'Unassigned', job.address, job.date, calculateDaysPending(job.date) ]); let csv = headers.join(',') + '\n'; rows.forEach(row => { csv += row.map(cell => `"${cell}"`).join(',') + '\n'; }); const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'stamford-pending-approval-' + new Date().toISOString().split('T')[0] + '.csv'; a.click(); } // Event Listeners document.getElementById('searchInput').addEventListener('input', applyFilters); document.getElementById('salesRepFilter').addEventListener('change', applyFilters); document.getElementById('sortSelect').addEventListener('change', applySorting); // Initialize populateFilters(); applyFilters(); </script> </body> </html>

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/benitocabrerar/jobnimbus-mcp-remote'

If you have feedback or need assistance with the MCP directory API, please join our Discord server