| 37651 |
vikas |
1 |
## Meta lead-ads import — upload, preview, choose distribution, commit.
|
|
|
2 |
## Model: recentImports (List<MetaLeadImport>)
|
|
|
3 |
## The preview step exists because roughly half a typical export cannot be resolved to a sales
|
|
|
4 |
## territory; the user needs to see that before writing, not after.
|
|
|
5 |
|
|
|
6 |
<style>
|
|
|
7 |
.meta-import { --navy:#1f2a5e; --coral:#f0483e; --ink:#26324d; --muted:#7683a4; --line:#e5e9f2; --bg:#f8fafd; }
|
|
|
8 |
.meta-import { color:var(--ink); }
|
|
|
9 |
|
|
|
10 |
/* ---- page header ---- */
|
|
|
11 |
.meta-import .mi-head { margin-bottom:18px; }
|
|
|
12 |
.meta-import .mi-head h2 { margin:0; font-size:22px; color:var(--navy); font-weight:700; }
|
|
|
13 |
.meta-import .mi-head p { margin:5px 0 0; font-size:13px; color:#59668a; max-width:70ch; }
|
|
|
14 |
|
|
|
15 |
/* ---- cards: one per step, never nested ---- */
|
|
|
16 |
.meta-import .mi-card { border:1px solid var(--line); border-radius:12px; padding:18px 20px; margin-bottom:16px; background:#fff; }
|
|
|
17 |
.meta-import .mi-card > h4 { margin:0 0 14px; font-size:12px; color:var(--muted); font-weight:700;
|
|
|
18 |
text-transform:uppercase; letter-spacing:.6px; display:flex; align-items:baseline; gap:8px; }
|
|
|
19 |
.meta-import .mi-card > h4 .mi-step { display:inline-flex; align-items:center; justify-content:center;
|
|
|
20 |
width:20px; height:20px; border-radius:50%; background:var(--navy); color:#fff; font-size:11px; flex:none; }
|
|
|
21 |
.meta-import .mi-note { font-size:12px; color:var(--muted); margin-top:10px; line-height:1.5; }
|
|
|
22 |
.meta-import code { background:#f1f4fa; color:#8d2119; padding:1px 5px; border-radius:4px; font-size:11.5px; }
|
|
|
23 |
|
|
|
24 |
/* ---- step 1: file row. Flex so the button never wraps under the input. ---- */
|
|
|
25 |
.meta-import .mi-filerow { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
|
|
26 |
.meta-import .mi-hidden-file { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; }
|
|
|
27 |
.meta-import .mi-choose { display:inline-flex; align-items:center; gap:7px; height:38px; padding:0 15px;
|
|
|
28 |
border:1px solid #c9d3e6; border-radius:8px; background:#fff; color:var(--navy); font-size:13px;
|
|
|
29 |
font-weight:600; cursor:pointer; margin:0; white-space:nowrap; }
|
|
|
30 |
.meta-import .mi-choose:hover { background:var(--bg); border-color:var(--navy); }
|
|
|
31 |
/* Takes the leftover space so the filename truncates instead of pushing the button to a new line. */
|
|
|
32 |
.meta-import .mi-filename { flex:1 1 200px; min-width:0; font-size:13px; color:var(--muted);
|
|
|
33 |
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
|
34 |
.meta-import .mi-filename.has-file { color:var(--ink); font-weight:600; }
|
|
|
35 |
.meta-import .mi-btn { display:inline-flex; align-items:center; gap:7px; height:38px; padding:0 18px;
|
|
|
36 |
border:1px solid transparent; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer;
|
|
|
37 |
white-space:nowrap; flex:none; }
|
|
|
38 |
.meta-import .mi-btn[disabled] { opacity:.5; cursor:not-allowed; }
|
|
|
39 |
.meta-import .mi-btn-navy { background:var(--navy); color:#fff; }
|
|
|
40 |
.meta-import .mi-btn-coral { background:var(--coral); color:#fff; }
|
|
|
41 |
.meta-import .mi-btn-ghost { background:#fff; color:var(--navy); border-color:#c9d3e6; }
|
|
|
42 |
|
|
|
43 |
/* ---- tallies ---- */
|
|
|
44 |
.meta-import .mi-counts { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:10px; margin-bottom:14px; }
|
|
|
45 |
.meta-import .mi-count { border:1px solid var(--line); border-radius:10px; padding:11px 13px; background:var(--bg); }
|
|
|
46 |
.meta-import .mi-count .n { font-size:22px; font-weight:800; line-height:1.15; }
|
|
|
47 |
.meta-import .mi-count .l { font-size:10.5px; color:var(--muted); text-transform:uppercase; letter-spacing:.4px; margin-top:2px; }
|
|
|
48 |
.meta-import .mi-count.created .n { color:#1a8f4c; }
|
|
|
49 |
.meta-import .mi-count.duplicate .n { color:#a9720a; }
|
|
|
50 |
.meta-import .mi-count.blocked .n, .meta-import .mi-count.rejected .n { color:var(--coral); }
|
|
|
51 |
|
|
|
52 |
/* ---- outcome pills ---- */
|
|
|
53 |
.meta-import .mi-pill { display:inline-block; padding:2px 9px; border-radius:20px; font-size:10.5px; font-weight:700; white-space:nowrap; letter-spacing:.3px; }
|
|
|
54 |
.meta-import .mi-CREATED { background:#e5f6ec; color:#1a8f4c; }
|
|
|
55 |
.meta-import .mi-DUPLICATE { background:#fff3dc; color:#a9720a; }
|
|
|
56 |
.meta-import .mi-BLOCKED_DND { background:#fde7e5; color:var(--coral); }
|
|
|
57 |
.meta-import .mi-REJECTED { background:#fde7e5; color:var(--coral); }
|
|
|
58 |
.meta-import .mi-hold { background:#eef1f6; color:var(--muted); font-size:10.5px; padding:2px 8px; border-radius:20px; white-space:nowrap; }
|
|
|
59 |
|
|
|
60 |
/* ---- tables ---- */
|
|
|
61 |
.meta-import table.mi-rows { width:100%; font-size:12.5px; border-collapse:collapse; }
|
|
|
62 |
.meta-import table.mi-rows th { text-align:left; font-size:10.5px; color:var(--muted); text-transform:uppercase;
|
|
|
63 |
letter-spacing:.4px; padding:8px; border-bottom:1px solid var(--line); background:var(--bg);
|
|
|
64 |
position:sticky; top:0; z-index:1; }
|
|
|
65 |
.meta-import table.mi-rows td { padding:8px; border-bottom:1px solid var(--line); vertical-align:top; }
|
|
|
66 |
.meta-import table.mi-rows tbody tr:hover { background:#fbfcfe; }
|
|
|
67 |
.meta-import table.mi-rows td.num { text-align:right; font-variant-numeric:tabular-nums; }
|
|
|
68 |
.meta-import .mi-scroll { max-height:440px; overflow:auto; border:1px solid var(--line); border-radius:8px; }
|
|
|
69 |
|
|
|
70 |
/* ---- step 3: distribution ---- */
|
|
|
71 |
.meta-import .mi-fields { display:flex; gap:14px; flex-wrap:wrap; align-items:flex-end; }
|
|
|
72 |
.meta-import .mi-field { display:flex; flex-direction:column; gap:5px; }
|
|
|
73 |
.meta-import .mi-field label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.4px; font-weight:600; }
|
|
|
74 |
.meta-import .mi-select { height:38px; padding:0 10px; border:1px solid #c9d3e6; border-radius:8px;
|
|
|
75 |
background:#fff; font-size:13px; color:var(--ink); min-width:270px; }
|
|
|
76 |
|
|
|
77 |
.meta-import .mi-dist-grid { display:grid; grid-template-columns:auto minmax(220px,1fr); gap:20px; align-items:center; }
|
|
|
78 |
.meta-import .mi-explain { font-size:12.5px; color:var(--muted); line-height:1.55; padding-left:20px;
|
|
|
79 |
border-left:2px solid var(--line); min-height:38px; display:flex; align-items:center; }
|
|
|
80 |
.meta-import .mi-select-narrow { min-width:220px; }
|
|
|
81 |
@media (max-width:900px) {
|
|
|
82 |
.meta-import .mi-dist-grid { grid-template-columns:1fr; }
|
|
|
83 |
.meta-import .mi-explain { padding-left:0; border-left:0; }
|
|
|
84 |
}
|
|
|
85 |
|
|
|
86 |
/* ---- footer bar ---- */
|
|
|
87 |
.meta-import .mi-actions { display:flex; align-items:center; justify-content:space-between; gap:14px;
|
|
|
88 |
flex-wrap:wrap; margin-top:16px; padding-top:14px; border-top:1px solid var(--line); }
|
|
|
89 |
.meta-import .mi-actions .mi-note { margin-top:0; }
|
|
|
90 |
/* Belt and braces: pins the button right even if justify-content is lost. */
|
|
|
91 |
.meta-import .mi-actions .mi-btn { margin-left:auto; }
|
|
|
92 |
|
|
|
93 |
.meta-import .mi-err { background:#fde7e5; border:1px solid #f5c6c2; color:#8d2119; border-radius:8px;
|
|
|
94 |
padding:11px 13px; font-size:13px; margin-top:12px; }
|
|
|
95 |
.meta-import .mi-warn { color:#8d2119; font-weight:600; }
|
|
|
96 |
</style>
|
|
|
97 |
|
|
|
98 |
<section class="wrapper meta-import">
|
|
|
99 |
|
|
|
100 |
<ol class="breadcrumb">
|
|
|
101 |
<li><i class="fa fa-home"></i><a href="${rc.contextPath}/dashboard">Home</a></li>
|
|
|
102 |
<li><a href="#" class="lead">Leads</a></li>
|
|
|
103 |
<li>Import Meta Leads</li>
|
|
|
104 |
</ol>
|
|
|
105 |
|
|
|
106 |
<div class="mi-head">
|
|
|
107 |
<h2>Import Meta Leads</h2>
|
|
|
108 |
<p>Upload a lead-ads export from Meta. You will see exactly what happens to every row, and
|
|
|
109 |
choose who works the new leads, before anything is saved.</p>
|
|
|
110 |
</div>
|
|
|
111 |
|
|
|
112 |
## ---- Step 1 : file ----
|
|
|
113 |
<div class="mi-card">
|
|
|
114 |
<h4><span class="mi-step">1</span> Choose the export</h4>
|
|
|
115 |
<div class="mi-filerow">
|
|
|
116 |
<label class="mi-choose" for="miFile"><i class="fa fa-folder-open"></i> Choose file</label>
|
|
|
117 |
<input type="file" id="miFile" class="mi-hidden-file" accept=".xls,.xlsx,.csv">
|
|
|
118 |
<span class="mi-filename" id="miFileName">No file chosen</span>
|
|
|
119 |
<button type="button" class="mi-btn mi-btn-navy" id="miPreviewBtn" disabled>
|
|
|
120 |
<i class="fa fa-search"></i> Preview
|
|
|
121 |
</button>
|
|
|
122 |
</div>
|
|
|
123 |
<div class="mi-note">
|
|
|
124 |
Meta's export is named <code>.xls</code> but is really XML — that is handled.
|
|
|
125 |
<code>.xlsx</code> and <code>.csv</code> exports work too.
|
|
|
126 |
</div>
|
|
|
127 |
<div id="miError" class="mi-err" style="display:none;"></div>
|
|
|
128 |
</div>
|
|
|
129 |
|
|
|
130 |
## ---- Steps 2 and 3 appear together once a file has been previewed ----
|
|
|
131 |
<div id="miResults" style="display:none;">
|
|
|
132 |
|
|
|
133 |
<div class="mi-card">
|
|
|
134 |
<h4><span class="mi-step">2</span> <span id="miResultTitle">What will happen</span></h4>
|
|
|
135 |
|
|
|
136 |
<div class="mi-counts">
|
|
|
137 |
<div class="mi-count"><div class="n" id="miTotal">0</div><div class="l">Rows in file</div></div>
|
|
|
138 |
<div class="mi-count created"><div class="n" id="miCreated">0</div><div class="l">New leads</div></div>
|
|
|
139 |
<div class="mi-count duplicate"><div class="n" id="miDuplicate">0</div><div class="l">Already known</div></div>
|
|
|
140 |
<div class="mi-count blocked"><div class="n" id="miBlocked">0</div><div class="l">Do not call</div></div>
|
|
|
141 |
<div class="mi-count rejected"><div class="n" id="miRejected">0</div><div class="l">Unusable</div></div>
|
|
|
142 |
</div>
|
|
|
143 |
|
|
|
144 |
<div class="mi-note mi-warn" id="miBalanceWarn" style="display:none;"></div>
|
|
|
145 |
<div class="mi-note" id="miHoldNote" style="margin-top:0;margin-bottom:12px;"></div>
|
|
|
146 |
|
|
|
147 |
<div class="mi-scroll">
|
|
|
148 |
<table class="mi-rows">
|
|
|
149 |
<thead>
|
|
|
150 |
<tr>
|
|
|
151 |
<th>Row</th><th>Name</th><th>Mobile</th><th>Shop</th>
|
|
|
152 |
<th>City</th><th>State</th><th>Region</th><th>Value</th><th>Outcome</th>
|
|
|
153 |
</tr>
|
|
|
154 |
</thead>
|
|
|
155 |
<tbody id="miRows"></tbody>
|
|
|
156 |
</table>
|
|
|
157 |
</div>
|
|
|
158 |
</div>
|
|
|
159 |
|
|
|
160 |
<div class="mi-card" id="miDistCard">
|
|
|
161 |
<h4><span class="mi-step">3</span> Who should work these leads?</h4>
|
|
|
162 |
<div class="mi-dist-grid">
|
|
|
163 |
<div class="mi-fields">
|
|
|
164 |
<div class="mi-field">
|
|
|
165 |
<label for="miDistribution">Distribution</label>
|
|
|
166 |
<select class="mi-select" id="miDistribution">
|
|
|
167 |
<option value="ROUND_ROBIN">Share evenly between all Call Center agents</option>
|
|
|
168 |
<option value="BALANCED">Give more to whoever has the fewest open leads</option>
|
|
|
169 |
<option value="RANDOM">Random agent per lead</option>
|
|
|
170 |
<option value="SINGLE">All to one agent…</option>
|
|
|
171 |
<option value="NONE">Leave unassigned for now</option>
|
|
|
172 |
</select>
|
|
|
173 |
</div>
|
|
|
174 |
<div class="mi-field" id="miAgentWrap" style="display:none;">
|
|
|
175 |
<label for="miAgent">Agent</label>
|
|
|
176 |
<select class="mi-select mi-select-narrow" id="miAgent">
|
|
|
177 |
<option value="">Loading agents…</option>
|
|
|
178 |
</select>
|
|
|
179 |
</div>
|
|
|
180 |
</div>
|
|
|
181 |
## The explanation sits beside the control rather than under it, so the right half of
|
|
|
182 |
## the row is used and the effect of the choice is read at the same time as the choice.
|
|
|
183 |
<div class="mi-explain" id="miDistNote"></div>
|
|
|
184 |
</div>
|
|
|
185 |
|
|
|
186 |
<div class="mi-actions" id="miCommitBar">
|
|
|
187 |
<span class="mi-note" id="miCommitHint"></span>
|
|
|
188 |
<button type="button" class="mi-btn mi-btn-coral" id="miCommitBtn">
|
|
|
189 |
<i class="fa fa-upload"></i> Import these leads
|
|
|
190 |
</button>
|
|
|
191 |
</div>
|
|
|
192 |
</div>
|
|
|
193 |
|
|
|
194 |
<div class="mi-card" id="miSplitCard" style="display:none;">
|
|
|
195 |
<h4>Who got what</h4>
|
|
|
196 |
<div id="miSplit"></div>
|
|
|
197 |
</div>
|
|
|
198 |
|
|
|
199 |
</div>
|
|
|
200 |
|
|
|
201 |
#if($recentImports && !$recentImports.isEmpty())
|
|
|
202 |
<div class="mi-card">
|
|
|
203 |
<h4>Recent imports</h4>
|
|
|
204 |
<table class="mi-rows">
|
|
|
205 |
<thead>
|
|
|
206 |
<tr>
|
|
|
207 |
<th>File</th><th>When</th><th class="num">Rows</th><th class="num">Created</th>
|
|
|
208 |
<th class="num">Duplicate</th><th class="num">DND</th><th class="num">Rejected</th>
|
|
|
209 |
</tr>
|
|
|
210 |
</thead>
|
|
|
211 |
<tbody>
|
|
|
212 |
#foreach($imp in $recentImports)
|
|
|
213 |
<tr>
|
|
|
214 |
<td>$!imp.getFileName()</td>
|
|
|
215 |
<td>#if($imp.getCreatedTimestamp())$imp.getCreatedTimestamp()#end</td>
|
|
|
216 |
<td class="num">$imp.getTotalRows()</td>
|
|
|
217 |
<td class="num">$imp.getCreatedCount()</td>
|
|
|
218 |
<td class="num">$imp.getDuplicateCount()</td>
|
|
|
219 |
<td class="num">$imp.getBlockedCount()</td>
|
|
|
220 |
<td class="num">$imp.getRejectedCount()</td>
|
|
|
221 |
</tr>
|
|
|
222 |
#end
|
|
|
223 |
</tbody>
|
|
|
224 |
</table>
|
|
|
225 |
</div>
|
|
|
226 |
#end
|
|
|
227 |
|
|
|
228 |
</section>
|
|
|
229 |
|
|
|
230 |
<script type="text/javascript">
|
|
|
231 |
(function () {
|
|
|
232 |
// Raw fetch rather than the common.js helpers: those attach a global loader modal and an
|
|
|
233 |
// alert-on-error to every request, which is the wrong shape for a form that reports its own
|
|
|
234 |
// per-row errors inline.
|
|
|
235 |
var previewed = false;
|
|
|
236 |
var lastCreated = 0;
|
|
|
237 |
|
|
|
238 |
function el(id) { return document.getElementById(id); }
|
|
|
239 |
function esc(s) {
|
|
|
240 |
if (s === null || s === undefined) { return ''; }
|
|
|
241 |
return ('' + s).replace(/[&<>"]/g, function (c) {
|
|
|
242 |
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c];
|
|
|
243 |
});
|
|
|
244 |
}
|
|
|
245 |
function showError(msg) {
|
|
|
246 |
el('miError').textContent = msg;
|
|
|
247 |
el('miError').style.display = 'block';
|
|
|
248 |
el('miResults').style.display = 'none';
|
|
|
249 |
}
|
|
|
250 |
function clearError() { el('miError').style.display = 'none'; }
|
|
|
251 |
|
|
|
252 |
function post(url, file, extra) {
|
|
|
253 |
var fd = new FormData();
|
|
|
254 |
fd.append('file', file);
|
|
|
255 |
if (extra) { Object.keys(extra).forEach(function (k) { if (extra[k] !== null && extra[k] !== '') { fd.append(k, extra[k]); } }); }
|
|
|
256 |
return fetch(context + url, { method: 'POST', body: fd, credentials: 'same-origin',
|
|
|
257 |
headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
|
|
258 |
.then(function (r) {
|
|
|
259 |
return r.json().then(function (j) {
|
|
|
260 |
if (!r.ok) { throw new Error(j && j.error ? j.error : ('Request failed: ' + r.status)); }
|
|
|
261 |
return j;
|
|
|
262 |
});
|
|
|
263 |
});
|
|
|
264 |
}
|
|
|
265 |
|
|
|
266 |
function render(data, committed) {
|
|
|
267 |
clearError();
|
|
|
268 |
el('miResults').style.display = 'block';
|
|
|
269 |
el('miResultTitle').textContent = committed ? 'Imported' : 'What will happen';
|
|
|
270 |
el('miTotal').textContent = data.total;
|
|
|
271 |
el('miCreated').textContent = data.created;
|
|
|
272 |
el('miDuplicate').textContent = data.duplicate;
|
|
|
273 |
el('miBlocked').textContent = data.blocked;
|
|
|
274 |
el('miRejected').textContent = data.rejected;
|
|
|
275 |
|
|
|
276 |
// If the file stops accounting for itself, the person importing it is the one who needs
|
|
|
277 |
// to know — not just the log.
|
|
|
278 |
if (data.balanced === false) {
|
|
|
279 |
el('miBalanceWarn').style.display = 'block';
|
|
|
280 |
el('miBalanceWarn').textContent =
|
|
|
281 |
'Warning: the outcomes do not add up to the number of rows. Please report this before relying on the import.';
|
|
|
282 |
} else {
|
|
|
283 |
el('miBalanceWarn').style.display = 'none';
|
|
|
284 |
}
|
|
|
285 |
|
|
|
286 |
var held = 0;
|
|
|
287 |
var html = '';
|
|
|
288 |
(data.rows || []).forEach(function (r) {
|
|
|
289 |
if (r.outcome === 'CREATED' && !r.region) { held++; }
|
|
|
290 |
html += '<tr>'
|
|
|
291 |
+ '<td>' + esc(r.rowNumber) + '</td>'
|
|
|
292 |
+ '<td>' + esc(r.name) + '</td>'
|
|
|
293 |
+ '<td>' + esc(r.mobile) + '</td>'
|
|
|
294 |
+ '<td>' + esc(r.outlet) + '</td>'
|
|
|
295 |
+ '<td>' + esc(r.city) + '</td>'
|
|
|
296 |
+ '<td>' + esc(r.state) + '</td>'
|
|
|
297 |
+ '<td>' + (r.region ? esc(r.region)
|
|
|
298 |
: '<span class="mi-hold">hold — assign manually</span>') + '</td>'
|
|
|
299 |
+ '<td>' + (r.band ? esc(r.band) : '—') + '</td>'
|
|
|
300 |
+ '<td><span class="mi-pill mi-' + esc(r.outcome) + '">' + esc(r.outcome) + '</span>'
|
|
|
301 |
+ (r.reason ? '<div class="mi-note" style="margin-top:2px;">' + esc(r.reason) + '</div>' : '')
|
|
|
302 |
+ '</td>'
|
|
|
303 |
+ '</tr>';
|
|
|
304 |
});
|
|
|
305 |
el('miRows').innerHTML = html;
|
|
|
306 |
|
|
|
307 |
el('miHoldNote').innerHTML = held > 0
|
|
|
308 |
? held + ' of the new leads have no sales region — their state maps to more than one '
|
|
|
309 |
+ 'territory, or to none. They import into the <b>HOLD queue</b> for manual assignment.'
|
|
|
310 |
: '';
|
|
|
311 |
|
|
|
312 |
if (committed) {
|
|
|
313 |
el('miDistCard').style.display = 'none';
|
|
|
314 |
el('miCommitBar').style.display = 'none';
|
|
|
315 |
el('miCommitHint').textContent = '';
|
|
|
316 |
} else {
|
|
|
317 |
el('miDistCard').style.display = 'block';
|
|
|
318 |
// Cleared rather than set to a value: an inline display would override the stylesheet's
|
|
|
319 |
// flex layout and left-pack the hint and button.
|
|
|
320 |
el('miCommitBar').style.display = '';
|
|
|
321 |
el('miCommitBtn').disabled = data.created === 0;
|
|
|
322 |
el('miCommitHint').textContent = data.created === 0
|
|
|
323 |
? 'Nothing new to import from this file.'
|
|
|
324 |
: 'Nothing has been saved yet.';
|
|
|
325 |
}
|
|
|
326 |
if (data.truncated) {
|
|
|
327 |
el('miHoldNote').innerHTML += ' <em>(showing the first ' + (data.rows || []).length + ' rows)</em>';
|
|
|
328 |
}
|
|
|
329 |
}
|
|
|
330 |
|
|
|
331 |
var agentsLoaded = false;
|
|
|
332 |
function loadAgents() {
|
|
|
333 |
if (agentsLoaded) { return Promise.resolve(); }
|
|
|
334 |
return fetch(context + '/lms/meta-import/agents', { credentials: 'same-origin',
|
|
|
335 |
headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
|
|
336 |
.then(function (r) { return r.json(); })
|
|
|
337 |
.then(function (j) {
|
|
|
338 |
var sel = el('miAgent');
|
|
|
339 |
var list = (j && j.agents) || [];
|
|
|
340 |
if (!list.length) {
|
|
|
341 |
sel.innerHTML = '<option value="">No active Call Center agents</option>';
|
|
|
342 |
} else {
|
|
|
343 |
sel.innerHTML = list.map(function (a) {
|
|
|
344 |
return '<option value="' + a.authId + '">' + esc(a.name || a.email) + '</option>';
|
|
|
345 |
}).join('');
|
|
|
346 |
}
|
|
|
347 |
agentsLoaded = true;
|
|
|
348 |
})
|
|
|
349 |
.catch(function () { el('miAgent').innerHTML = '<option value="">Could not load agents</option>'; });
|
|
|
350 |
}
|
|
|
351 |
|
|
|
352 |
function describeDistribution() {
|
|
|
353 |
var v = el('miDistribution').value;
|
|
|
354 |
var n = lastCreated;
|
|
|
355 |
var notes = {
|
|
|
356 |
ROUND_ROBIN: 'Each agent gets a turn, so the split is even and predictable.',
|
|
|
357 |
BALANCED: 'Counts every agent\u2019s current open leads first, then tops up whoever is carrying least.',
|
|
|
358 |
RANDOM: 'Matches how single leads are assigned elsewhere; over a batch the split will be uneven.',
|
|
|
359 |
SINGLE: 'Everything in this file goes to one agent.',
|
|
|
360 |
NONE: 'Imported with no owner \u2014 they will sit in the HOLD queue until someone hands them out.'
|
|
|
361 |
};
|
|
|
362 |
el('miDistNote').textContent = (notes[v] || '') + (n ? ' ' + n + ' lead(s) to assign.' : '');
|
|
|
363 |
// Cleared, not set to 'block' — .mi-field is a flex column and an inline display would
|
|
|
364 |
// drop its label/select gap.
|
|
|
365 |
el('miAgentWrap').style.display = (v === 'SINGLE') ? '' : 'none';
|
|
|
366 |
if (v === 'SINGLE') { loadAgents(); }
|
|
|
367 |
}
|
|
|
368 |
el('miDistribution').addEventListener('change', describeDistribution);
|
|
|
369 |
|
|
|
370 |
function renderSplit(perAgent) {
|
|
|
371 |
if (!perAgent || !perAgent.length) { el('miSplitCard').style.display = 'none'; return; }
|
|
|
372 |
el('miSplitCard').style.display = 'block';
|
|
|
373 |
el('miSplit').innerHTML = '<table class="mi-rows"><thead><tr><th>Agent</th><th>Leads</th></tr></thead><tbody>'
|
|
|
374 |
+ perAgent.map(function (a) {
|
|
|
375 |
return '<tr><td>' + esc(a.name) + '</td><td>' + esc(a.count) + '</td></tr>';
|
|
|
376 |
}).join('') + '</tbody></table>';
|
|
|
377 |
}
|
|
|
378 |
|
|
|
379 |
el('miPreviewBtn').addEventListener('click', function () {
|
|
|
380 |
var file = el('miFile').files[0];
|
|
|
381 |
if (!file) { showError('Choose a file first.'); return; }
|
|
|
382 |
clearError();
|
|
|
383 |
el('miPreviewBtn').disabled = true;
|
|
|
384 |
post('/lms/meta-import/preview', file)
|
|
|
385 |
.then(function (data) { previewed = true; lastCreated = data.created; render(data, false); describeDistribution(); })
|
|
|
386 |
.catch(function (e) { previewed = false; showError(e.message); })
|
|
|
387 |
.then(function () { el('miPreviewBtn').disabled = false; });
|
|
|
388 |
});
|
|
|
389 |
|
|
|
390 |
el('miCommitBtn').addEventListener('click', function () {
|
|
|
391 |
var file = el('miFile').files[0];
|
|
|
392 |
if (!file) { showError('Choose a file first.'); return; }
|
|
|
393 |
if (!previewed) { showError('Preview the file before importing.'); return; }
|
|
|
394 |
if (!confirm('Import these leads? This cannot be undone.')) { return; }
|
|
|
395 |
el('miCommitBtn').disabled = true;
|
|
|
396 |
// The file is posted again rather than the server holding state between the two steps, and
|
|
|
397 |
// the commit re-evaluates from scratch — so a lead someone else created since the preview
|
|
|
398 |
// is caught rather than duplicated.
|
|
|
399 |
var dist = el('miDistribution').value;
|
|
|
400 |
var agentId = (dist === 'SINGLE') ? el('miAgent').value : null;
|
|
|
401 |
if (dist === 'SINGLE' && !agentId) { showError('Choose the agent these leads should go to.'); el('miCommitBtn').disabled = false; return; }
|
|
|
402 |
post('/lms/meta-import/commit', file, { distribution: dist, agentId: agentId })
|
|
|
403 |
.then(function (data) { render(data, true); renderSplit(data.perAgent); })
|
|
|
404 |
.catch(function (e) { showError(e.message); el('miCommitBtn').disabled = false; });
|
|
|
405 |
});
|
|
|
406 |
|
|
|
407 |
// Native file inputs cannot be aligned reliably across browsers, so the real input is hidden
|
|
|
408 |
// behind a styled label and the chosen name is rendered here instead.
|
|
|
409 |
el('miFile').addEventListener('change', function () {
|
|
|
410 |
var file = this.files[0];
|
|
|
411 |
var label = el('miFileName');
|
|
|
412 |
label.textContent = file ? file.name : 'No file chosen';
|
|
|
413 |
label.classList.toggle('has-file', !!file);
|
|
|
414 |
label.title = file ? file.name : '';
|
|
|
415 |
el('miPreviewBtn').disabled = !file;
|
|
|
416 |
// A different file invalidates the preview.
|
|
|
417 |
previewed = false;
|
|
|
418 |
el('miResults').style.display = 'none';
|
|
|
419 |
el('miSplitCard').style.display = 'none';
|
|
|
420 |
clearError();
|
|
|
421 |
});
|
|
|
422 |
})();
|
|
|
423 |
</script>
|