mattrguerra/meridian-skillsmainPrivate on GitHub, published here in full

widget-template.html

Language HTML7.1 KB99 lines

skills/bitrix-pipeline-board/references/widget-template.html

  1. Scaffold for the pipeline board widget passed to visualize:show_widget.
  2. Replace the D array with the parsed leads. The script computes the
  3. per-owner stacked bar and builds the filterable, linked table.
  4. Each lead object: {id, n: "title", o: "Owner Full Name", s: "Stage Name", g: "intake|active|parked"}
  5. Metric card numbers (Total / Active / Parked / Intake) and the owner filter
  6. pill counts are hardcoded in the markup below. Recompute them from D before
  7. emitting, or generate the cards/pills from D in the script. Owner pills must
  8. list every owner present in D, highest count first.
  9. ----------------------------------------------------------------------
  10. <div style="padding: 1rem 0;">
  11. <h2 class="sr-only">Interactive board of open Meridian Display Co. leads grouped by owner and pipeline stage, each linking to its Bitrix record.</h2>
  12. <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:12px; margin-bottom:1.25rem;">
  13. <div style="background:var(--color-background-secondary); border-radius:var(--border-radius-md); padding:1rem;">
  14. <div style="font-size:13px; color:var(--color-text-secondary);">Total open</div>
  15. <div style="font-size:24px; font-weight:500;" id="m-total">0</div>
  16. </div>
  17. <div style="background:var(--color-background-secondary); border-radius:var(--border-radius-md); padding:1rem;">
  18. <div style="font-size:13px; color:var(--color-text-secondary);">Active</div>
  19. <div style="font-size:24px; font-weight:500;" id="m-active">0</div>
  20. </div>
  21. <div style="background:var(--color-background-secondary); border-radius:var(--border-radius-md); padding:1rem;">
  22. <div style="font-size:13px; color:var(--color-text-secondary);">Parked</div>
  23. <div style="font-size:24px; font-weight:500;" id="m-parked">0</div>
  24. </div>
  25. <div style="background:var(--color-background-secondary); border-radius:var(--border-radius-md); padding:1rem;">
  26. <div style="font-size:13px; color:var(--color-text-secondary);">Intake / new</div>
  27. <div style="font-size:24px; font-weight:500;" id="m-intake">0</div>
  28. </div>
  29. </div>
  30. <div style="display:flex; flex-wrap:wrap; gap:16px; margin-bottom:8px; font-size:12px; color:var(--color-text-secondary);">
  31. <span style="display:flex; align-items:center; gap:4px;"><span style="width:10px;height:10px;border-radius:2px;background:#888780;"></span>Intake</span>
  32. <span style="display:flex; align-items:center; gap:4px;"><span style="width:10px;height:10px;border-radius:2px;background:#1D9E75;"></span>Active</span>
  33. <span style="display:flex; align-items:center; gap:4px;"><span style="width:10px;height:10px;border-radius:2px;background:#BA7517;"></span>Parked</span>
  34. </div>
  35. <div style="position:relative; width:100%; height:260px; margin-bottom:1.5rem;">
  36. <canvas id="ownerChart" role="img" aria-label="Stacked bar chart of open leads by owner, split into intake, active and parked.">Open leads by owner.</canvas>
  37. </div>
  38. <div id="pills" style="display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:12px;">
  39. <select id="stageSel" style="margin-left:auto; width:auto; min-width:160px;"></select>
  40. </div>
  41. <div id="tableWrap" style="border:0.5px solid var(--color-border-tertiary); border-radius:var(--border-radius-md); overflow:hidden;"></div>
  42. <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
  43. <script>
  44. const D=[ /* parsed leads go here */ ];
  45. const BASE="https://meridian.bitrix24.example/crm/lead/details/";
  46. const gColor={intake:"#888780",active:"#1D9E75",parked:"#BA7517"};
  47. document.getElementById('m-total').textContent=D.length;
  48. ['intake','active','parked'].forEach(g=>{const e=document.getElementById('m-'+g);if(e)e.textContent=D.filter(d=>d.g===g).length;});
  49. const owners=[...new Set(D.map(d=>d.o))].sort((a,b)=>D.filter(d=>d.o===b).length-D.filter(d=>d.o===a).length);
  50. const stack={intake:[],active:[],parked:[]};
  51. owners.forEach(o=>{["intake","active","parked"].forEach(g=>{stack[g].push(D.filter(d=>d.o===o&&d.g===g).length);});});
  52. new Chart(document.getElementById('ownerChart'),{
  53. type:'bar',
  54. data:{labels:owners.map(o=>o.split(' ')[0]),datasets:[
  55. {label:'Intake',data:stack.intake,backgroundColor:gColor.intake},
  56. {label:'Active',data:stack.active,backgroundColor:gColor.active},
  57. {label:'Parked',data:stack.parked,backgroundColor:gColor.parked}
  58. ]},
  59. options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,
  60. scales:{x:{stacked:true,ticks:{precision:0}},y:{stacked:true}},
  61. plugins:{legend:{display:false}}}
  62. });
  63. const pills=document.getElementById('pills');
  64. const stageSel=document.getElementById('stageSel');
  65. const mkPill=(label,o)=>{const s=document.createElement('span');s.className='owner-f';s.dataset.o=o;s.textContent=label;s.style.cssText='cursor:pointer;font-size:13px;padding:5px 12px;border:0.5px solid var(--color-border-secondary);border-radius:var(--border-radius-md);';return s;};
  66. const allPill=mkPill('All ('+D.length+')','all');allPill.style.background='var(--color-background-secondary)';
  67. pills.insertBefore(allPill,stageSel);
  68. owners.forEach(o=>{const c=D.filter(d=>d.o===o).length;pills.insertBefore(mkPill(o.split(' ')[0]+' ('+c+')',o),stageSel);});
  69. const stages=[...new Set(D.map(d=>d.s))];
  70. stageSel.innerHTML='<option value="all">All stages</option>'+stages.map(s=>`<option value="${s}">${s} (${D.filter(d=>d.s===s).length})</option>`).join('');
  71. let curO="all",curS="all";
  72. function badge(g){return `<span style="font-size:11px;padding:2px 8px;border-radius:var(--border-radius-md);background:${g==='parked'?'#FAEEDA':g==='intake'?'#F1EFE8':'#E1F5EE'};color:${g==='parked'?'#633806':g==='intake'?'#444441':'#0F6E56'};">`;}
  73. function render(){
  74. const rows=D.filter(d=>(curO==="all"||d.o===curO)&&(curS==="all"||d.s===curS));
  75. let h='<table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;"><thead><tr style="background:var(--color-background-secondary);text-align:left;"><th style="padding:8px 10px;width:62px;">ID</th><th style="padding:8px 10px;">Lead</th><th style="padding:8px 10px;width:150px;">Stage</th><th style="padding:8px 10px;width:90px;">Owner</th></tr></thead><tbody>';
  76. rows.forEach(d=>{h+=`<tr style="border-top:0.5px solid var(--color-border-tertiary);"><td style="padding:7px 10px;"><a href="${BASE}${d.id}/" style="color:var(--color-text-info);text-decoration:none;">${d.id}<i class="ti ti-external-link" style="font-size:13px;vertical-align:-1px;margin-left:3px;" aria-hidden="true"></i></a></td><td style="padding:7px 10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${d.n}</td><td style="padding:7px 10px;">${badge(d.g)}${d.s}</span></td><td style="padding:7px 10px;color:var(--color-text-secondary);">${d.o.split(' ')[0]}</td></tr>`;});
  77. h+='</tbody></table>';
  78. if(!rows.length)h='<div style="padding:1rem;color:var(--color-text-secondary);">No leads match.</div>';
  79. document.getElementById('tableWrap').innerHTML=h;
  80. }
  81. document.querySelectorAll('.owner-f').forEach(el=>el.addEventListener('click',()=>{curO=el.dataset.o;document.querySelectorAll('.owner-f').forEach(x=>x.style.background='');el.style.background='var(--color-background-secondary)';render();}));
  82. stageSel.addEventListener('change',()=>{curS=stageSel.value;render();});
  83. render();
  84. </script>
  85. </div>