Open/search chats, start new by phone, schedule & auto-send messages with improved reliability
Size
76.7 KB
Version
3.2.8
Created
Nov 15, 2025
Updated
15 days ago
1// ==UserScript==
2// @name WhatsApp Message Scheduler Pro
3// @description Open/search chats, start new by phone, schedule & auto-send messages with improved reliability
4// @version 3.2.8
5// @match https://*.web.whatsapp.com/*
6// @icon https://web.whatsapp.com/favicon/1x/f54/v4/
7// @namespace io.robomonkey.whatsapp.proscheduler
8// @author Robomonkey
9// @grant none
10// ==/UserScript==
11(function () {
12 'use strict';
13
14 const log = (...a) => console.log('[WAW Pro]', ...a);
15 const LS = { JOBS:'waw.jobs.v3', BOOK:'waw.addressbook.v1', PREFS:'waw.prefs.v1', PENDING:'waw.pending.v1', HISTORY:'waw.history.v1', FAB_POS:'waw.fabpos.v1', ARCHIVE:'waw.archive.v1' };
16 const load = (k, d) => { try { return JSON.parse(localStorage.getItem(k) || JSON.stringify(d)); } catch { return d; } };
17 const save = (k, v) => localStorage.setItem(k, JSON.stringify(v));
18 const loadJobs = () => load(LS.JOBS, []);
19 const saveJobs = (v) => save(LS.JOBS, v);
20 const loadBook = () => load(LS.BOOK, []);
21 const saveBook = (v) => save(LS.BOOK, v);
22 const loadPrefs = () => load(LS.PREFS, { panelOpen:false });
23 const savePrefs = (v) => save(LS.PREFS, v);
24 const loadHistory = () => load(LS.HISTORY, []);
25 const saveHistory = (v) => save(LS.HISTORY, v);
26 const loadArchive = () => load(LS.ARCHIVE, []);
27 const saveArchive = (v) => save(LS.ARCHIVE, v);
28 const loadFabPos = () => load(LS.FAB_POS, null);
29 const saveFabPos = (v) => save(LS.FAB_POS, v);
30 const uid = () => Math.random().toString(36).slice(2) + Date.now().toString(36);
31 const delay = (ms) => new Promise(r => setTimeout(r, ms));
32 const fmt = (ms) => new Date(ms).toLocaleString();
33 const fmtShort = (ms) => { const d = new Date(ms); return `${pad(d.getHours())}:${pad(d.getMinutes())}`; };
34 const pad = (n) => String(n).padStart(2, '0');
35 const isToday = (ms) => { const d = new Date(ms); const t = new Date(); return d.getDate()===t.getDate() && d.getMonth()===t.getMonth() && d.getFullYear()===t.getFullYear(); };
36
37 // Recurring messages helpers
38 function getNextOccurrence(baseTime, repeatType, weeklyDays = []) {
39 const base = new Date(baseTime);
40
41 if (repeatType === 'daily') {
42 return base.getTime() + 24 * 60 * 60 * 1000;
43 }
44
45 if (repeatType === 'weekly' && weeklyDays.length > 0) {
46 const currentDay = base.getDay();
47 const sortedDays = weeklyDays.map(Number).sort((a, b) => a - b);
48
49 let nextDay = sortedDays.find(d => d > currentDay);
50 let daysToAdd;
51
52 if (nextDay !== undefined) {
53 daysToAdd = nextDay - currentDay;
54 } else {
55 daysToAdd = 7 - currentDay + sortedDays[0];
56 }
57
58 return base.getTime() + daysToAdd * 24 * 60 * 60 * 1000;
59 }
60
61 return null;
62 }
63
64 // History management
65 function addToHistory(status, target, text, error=null){
66 const history = loadHistory();
67 history.push({ id: uid(), timestamp: Date.now(), status, target, text: text.substring(0, 100), error });
68 const weekAgo = Date.now() - 7*24*60*60*1000;
69 const filtered = history.filter(h => h.timestamp > weekAgo);
70 saveHistory(filtered);
71
72 // Also add to permanent archive
73 const archive = loadArchive();
74 archive.push({ id: uid(), timestamp: Date.now(), status, target, text, error });
75 saveArchive(archive);
76 }
77
78 function archiveJob(job) {
79 const archive = loadArchive();
80 archive.push({
81 id: uid(),
82 timestamp: job.when,
83 status: 'scheduled',
84 target: job.target.phone || job.target.name,
85 text: job.text,
86 repeat: job.repeat || null,
87 weeklyDays: job.weeklyDays || null,
88 archivedAt: Date.now()
89 });
90 saveArchive(archive);
91 }
92
93 function getStats(){
94 const history = loadHistory();
95 const today = history.filter(h => isToday(h.timestamp));
96 const jobs = loadJobs();
97 const pendingToday = jobs.filter(j => isToday(j.when));
98 return {
99 sentToday: today.filter(h => h.status === 'sent').length,
100 failedToday: today.filter(h => h.status === 'failed').length,
101 pendingToday: pendingToday.length,
102 pendingTotal: jobs.length,
103 recentSent: today.filter(h => h.status === 'sent').slice(-10).reverse()
104 };
105 }
106
107 function timeUntil(targetMs) {
108 const diff = targetMs - Date.now();
109 if (diff <= 0) return '⏰ Ready to send!';
110
111 const days = Math.floor(diff / (1000 * 60 * 60 * 24));
112 const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
113 const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
114 const seconds = Math.floor((diff % (1000 * 60)) / 1000);
115
116 if (days > 0) return `⏳ ${days}d ${hours}h ${minutes}m`;
117 if (hours > 0) return `⏳ ${hours}h ${minutes}m ${seconds}s`;
118 if (minutes > 0) return `⏳ ${minutes}m ${seconds}s`;
119 return `⏳ ${seconds}s`;
120 }
121
122 const selectors = {
123 appRoot: '#app',
124 searchBtn: '[data-testid="chat-list-search"], [data-icon="search"], [aria-label*="Search"], [aria-label*="חיפוש"]',
125 searchBox: [
126 'header [contenteditable="true"][role="textbox"]',
127 'aside [contenteditable="true"][role="textbox"]',
128 'header [contenteditable="true"]',
129 'aside [contenteditable="true"]',
130 'input[type="text"]'
131 ],
132 chatListItem: ['[data-testid="cell-frame-container"]','[role="listitem"]','div[tabindex][aria-label]'],
133 chatNameInList: ['[data-testid*="chatlist"]','[dir="auto"]','span[title]','span'],
134 headerTitle: ['[data-testid="conversation-info-header-chat-title"]','header span[title]'],
135 composer: [
136 'footer div[contenteditable="true"][role="textbox"]',
137 'footer [contenteditable="true"][data-lexical-editor="true"]',
138 'footer [contenteditable="true"]'
139 ],
140 sendButton: [
141 'footer [data-testid="compose-btn-send"]',
142 'footer [aria-label*="Send"]',
143 'footer [data-icon="send"]'
144 ]
145 };
146
147 function qsAny(list, root=document){ for (const sel of list){ const el=root.querySelector(sel); if (el) return el; } return null; }
148 function getActiveChatTitle(){ const el = qsAny(selectors.headerTitle); return el ? (el.getAttribute('title')||el.textContent||'').trim() : null; }
149 function normalizePhone(num){ const d = String(num).replace(/[^\d]/g,''); return d.startsWith('00') ? d.slice(2) : d; }
150
151 async function waitFor(sel, { root=document, timeout=30000, check=150 }={}) {
152 const t0 = Date.now();
153 return new Promise((res, rej)=>{
154 const it = setInterval(()=>{
155 const el = root.querySelector(sel);
156 if (el) { clearInterval(it); res(el); }
157 else if (Date.now()-t0 > timeout) { clearInterval(it); rej(new Error('Timeout: '+sel)); }
158 }, check);
159 });
160 }
161
162 function findSearchBox(){
163 const btn = document.querySelector(selectors.searchBtn);
164 if (btn) try { btn.click(); } catch {}
165 return qsAny(selectors.searchBox);
166 }
167
168 async function typeInto(el, text){
169 el.focus();
170 try { document.execCommand('selectAll', false, null); document.execCommand('delete', false, null); } catch {}
171 for (const ch of text){
172 el.dispatchEvent(new InputEvent('beforeinput', { inputType:'insertText', data:ch, bubbles:true }));
173 if ('value' in el) el.value += ch; else el.textContent = (el.textContent||'') + ch;
174 el.dispatchEvent(new InputEvent('input', { bubbles:true, data:ch }));
175 await delay(8);
176 }
177 }
178
179 function getSearchResults(){
180 const rows = [];
181 const containers = document.querySelectorAll(selectors.chatListItem.join(','));
182 containers.forEach(row=>{
183 let name=''; let nameEl=null;
184 for (const sel of selectors.chatNameInList){ const c=row.querySelector(sel); if (c){ nameEl=c; break; } }
185 if (nameEl) name = (nameEl.getAttribute('title')||nameEl.textContent||'').trim();
186 if (name) rows.push({ row, name });
187 });
188 const seen=new Set(), uniq=[];
189 for (const it of rows){ if (!seen.has(it.name)){ uniq.push(it); seen.add(it.name); } }
190 return uniq;
191 }
192
193 async function openChatByName(name, { exact=false }={}){
194 const box = findSearchBox(); if (!box) throw new Error('Search box not found');
195 await typeInto(box, name); await delay(500);
196 let results = getSearchResults(); if (!results.length){ await delay(700); results = getSearchResults(); }
197 if (!results.length) throw new Error('No matches');
198 let pick = results.find(r=>r.name.toLowerCase()===name.toLowerCase());
199 if (!pick && !exact) pick = results.find(r=>r.name.toLowerCase().includes(name.toLowerCase())) || results[0];
200 if (!pick) pick = results[0];
201 pick.row.click();
202 await delay(700);
203 try { await typeInto(box, ''); } catch {}
204 return getActiveChatTitle() || name;
205 }
206
207 async function openChatByPhone(phone, presetText){
208 const p = normalizePhone(phone);
209 const url = new URL('https://web.whatsapp.com/send'); url.searchParams.set('phone', p);
210 if (presetText) url.searchParams.set('text', presetText);
211 window.location.assign(url.toString());
212 await waitFor(selectors.appRoot, { timeout:60000 }).catch(()=>{});
213 await waitFor(selectors.composer.map(s=>s).join(','), { timeout:60000 });
214 await delay(800);
215 return getActiveChatTitle() || p;
216 }
217
218 function visible(el){ return !!(el && el.offsetParent !== null); }
219
220 async function getComposer({ requireVisible=true }={}){
221 for (const sel of selectors.composer){
222 const ed = document.querySelector(sel);
223 if (ed && (!requireVisible || visible(ed))) return ed;
224 }
225 return null;
226 }
227
228 function setTextViaSelection(ed, text){
229 try {
230 const r = document.createRange(); r.selectNodeContents(ed);
231 const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r);
232 document.execCommand('delete', false, null);
233 } catch {}
234 let usedExec = false;
235 if (document.queryCommandSupported && document.queryCommandSupported('insertText')){
236 usedExec = document.execCommand('insertText', false, text);
237 }
238 if (!usedExec){
239 ed.dispatchEvent(new InputEvent('beforeinput', { inputType:'insertFromPaste', data:text, bubbles:true }));
240 ed.textContent = text;
241 ed.dispatchEvent(new InputEvent('input', { bubbles:true, data:text }));
242 }
243 }
244
245 async function clickSendButtonIfAny(){
246 const btn = qsAny(selectors.sendButton);
247 if (btn){ btn.click(); await delay(120); return true; }
248 return false;
249 }
250
251 async function pressEnter(ed){
252 const kd = new KeyboardEvent('keydown', { bubbles:true, cancelable:true, key:'Enter', code:'Enter', keyCode:13 });
253 const ku = new KeyboardEvent('keyup', { bubbles:true, key:'Enter', code:'Enter', keyCode:13 });
254 ed.dispatchEvent(kd); ed.dispatchEvent(ku);
255 await delay(120);
256 }
257
258 async function ensureFocused(){
259 window.focus();
260 document.body.click();
261 await delay(50);
262 }
263
264 async function sendMessage(text, { retries=3 }={}){
265 for (let attempt=1; attempt<=retries; attempt++){
266 await ensureFocused();
267 let ed = await getComposer({ requireVisible:true });
268 if (!ed){ await delay(300); ed = await getComposer({ requireVisible:false }); }
269 if (!ed){ if (attempt===retries) throw new Error('Composer not found'); await delay(250); continue; }
270
271 ed.focus();
272 setTextViaSelection(ed, text);
273
274 const clicked = await clickSendButtonIfAny();
275 if (!clicked) await pressEnter(ed);
276
277 await delay(200);
278 const remaining = (ed.textContent||'').trim();
279 if (remaining.length === 0) return true;
280
281 await delay(200);
282 }
283 const edFinal = await getComposer({ requireVisible:false });
284 if (edFinal){
285 await pressEnter(edFinal); await delay(200);
286 if ((edFinal.textContent||'').trim().length === 0) return true;
287 }
288 throw new Error('Failed to send (text stayed in composer)');
289 }
290
291 const styles = `
292 .waw-panel{position:fixed;right:16px;bottom:16px;z-index:999999;width:420px;background:linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);color:#fff;border-radius:20px;padding:0;box-shadow:0 20px 60px rgba(0,0,0,.5), 0 0 0 1px rgba(255,255,255,.1);font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Arial;backdrop-filter:blur(10px)}
293 .waw-header{display:flex;justify-content:space-between;align-items:center;padding:20px 20px 16px 20px;background:linear-gradient(135deg, #25D366 0%, #20ba5a 100%);border-radius:20px 20px 0 0;box-shadow:0 4px 12px rgba(37,211,102,.3);cursor:move;user-select:none}
294 .waw-title{font-weight:900;font-size:16px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.2);letter-spacing:0.3px;pointer-events:none}
295 .waw-close{background:rgba(255,255,255,.2);border:none;color:#fff;cursor:pointer;font-size:18px;width:32px;height:32px;border-radius:50%;transition:all .2s;display:flex;align-items:center;justify-content:center;font-weight:700}
296 .waw-close:hover{background:rgba(255,255,255,.3);transform:rotate(90deg)}
297 .waw-content{padding:20px;max-height:520px;overflow-y:auto}
298 .waw-content::-webkit-scrollbar{width:8px}
299 .waw-content::-webkit-scrollbar-track{background:rgba(255,255,255,.05);border-radius:10px}
300 .waw-content::-webkit-scrollbar-thumb{background:rgba(37,211,102,.4);border-radius:10px}
301 .waw-content::-webkit-scrollbar-thumb:hover{background:rgba(37,211,102,.6)}
302 .waw-tabs{display:flex;gap:8px;margin-top:10px}
303 .waw-tab{flex:1;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);color:#e5e7eb;border-radius:12px;padding:10px;text-align:center;cursor:pointer;font-weight:700;transition:all .3s}
304 .waw-tab.active{background:#25D366;color:#111;border-color:#25D366;box-shadow:0 4px 12px rgba(37,211,102,.3)}
305 .waw-section{margin-top:16px;background:rgba(255,255,255,.03);border-radius:16px;padding:16px;border:1px solid rgba(255,255,255,.08)}
306 .waw-field{margin:12px 0}
307 .waw-field label{display:block;font-size:13px;color:#cbd5e1;margin-bottom:6px;font-weight:600}
308 .waw-input,.waw-textarea,.waw-dt{width:100%;background:rgba(255,255,255,.08);color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:12px;padding:12px;outline:none;transition:all .3s;font-size:14px}
309 .waw-input:focus,.waw-textarea:focus,.waw-dt:focus{border-color:#25D366;box-shadow:0 0 0 3px rgba(37,211,102,.15);background:rgba(255,255,255,.12)}
310 .waw-input::placeholder,.waw-textarea::placeholder{color:rgba(255,255,255,.4)}
311 .waw-row{display:flex;gap:8px}
312 .waw-btn{background:linear-gradient(135deg, #25D366 0%, #20ba5a 100%);color:#fff;border:none;padding:12px 20px;border-radius:12px;cursor:pointer;font-weight:800;transition:all .3s;box-shadow:0 4px 12px rgba(37,211,102,.3);font-size:14px}
313 .waw-btn:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(37,211,102,.4)}
314 .waw-btn:active{transform:translateY(0)}
315 .waw-btn.secondary{background:rgba(255,255,255,.1);color:#fff;box-shadow:none;border:1px solid rgba(255,255,255,.15)}
316 .waw-btn.secondary:hover{background:rgba(255,255,255,.15);transform:translateY(-1px)}
317 .waw-small{font-size:12px;color:#9aa7b2}
318 .waw-list{max-height:200px;overflow:auto;border-top:1px solid rgba(255,255,255,.1);padding-top:8px;margin-top:8px}
319 .waw-list::-webkit-scrollbar{width:6px}
320 .waw-list::-webkit-scrollbar-track{background:rgba(255,255,255,.05);border-radius:10px}
321 .waw-list::-webkit-scrollbar-thumb{background:rgba(37,211,102,.3);border-radius:10px}
322 .waw-item{display:flex;justify-content:space-between;gap:12px;padding:12px;border-bottom:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.02);border-radius:8px;margin-bottom:8px;transition:all .2s}
323 .waw-item:hover{background:rgba(255,255,255,.05);transform:translateX(-2px)}
324 .waw-item:last-child{border-bottom:none}
325 .waw-fab{position:fixed;right:16px;bottom:16px;width:64px;height:64px;border-radius:50%;border:none;background:linear-gradient(135deg, #25D366 0%, #20ba5a 100%);color:#fff;font-weight:900;cursor:pointer;z-index:999998;box-shadow:0 8px 24px rgba(37,211,102,.4), 0 0 0 0 rgba(37,211,102,.4);font-size:28px;transition:all .3s;display:flex;align-items:center;justify-content:center}
326 .waw-fab:hover{transform:scale(1.1) rotate(10deg);box-shadow:0 12px 32px rgba(37,211,102,.5)}
327 .waw-fab:active{transform:scale(0.95)}
328 @keyframes pulse{0%{box-shadow:0 8px 24px rgba(37,211,102,.4), 0 0 0 0 rgba(37,211,102,.4)}50%{box-shadow:0 8px 24px rgba(37,211,102,.4), 0 0 0 12px rgba(37,211,102,0)}100%{box-shadow:0 8px 24px rgba(37,211,102,.4), 0 0 0 0 rgba(37,211,102,0)}}
329 .waw-fab.has-jobs{animation:pulse 2s infinite}
330 .waw-diagnostics{background:#111827;border:1px solid #374151;border-radius:12px;padding:12px;margin-top:8px}
331 .waw-diagnostics div{font-size:12px;margin:4px 0}
332 .waw-divider{border-top:2px solid rgba(255,255,255,.1);padding-top:16px;margin-top:16px;position:relative}
333 .waw-divider::before{content:'';position:absolute;top:-1px;left:50%;transform:translateX(-50%);width:60px;height:2px;background:linear-gradient(90deg, transparent, #25D366, transparent)}
334 .waw-section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
335 .waw-section-title{font-weight:800;font-size:14px;color:#25D366;display:flex;align-items:center;gap:6px}
336 .waw-panel.dragging{transition:none;opacity:0.9}
337 `;
338
339 function el(t,a={},h=''){ const e=document.createElement(t); Object.entries(a).forEach(([k,v])=>e.setAttribute(k,v)); if(h) e.innerHTML=h; return e; }
340 function injectStyles(){ const s=document.createElement('style'); s.textContent=styles; document.head.appendChild(s); }
341
342 // Notification system
343 function showNotification(title, message, type = 'info') {
344 const notification = el('div', {
345 style: `position:fixed;top:20px;right:20px;z-index:1000000;background:${type === 'success' ? 'linear-gradient(135deg, #25D366 0%, #20ba5a 100%)' : type === 'error' ? 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)' : 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)'};color:#fff;padding:16px 20px;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.3);min-width:300px;max-width:400px;animation:slideIn 0.3s ease-out;font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Arial`
346 });
347
348 notification.innerHTML = `
349 <div style="display:flex;justify-content:space-between;align-items:start;gap:12px">
350 <div style="flex:1">
351 <div style="font-weight:800;font-size:14px;margin-bottom:6px">${title}</div>
352 <div style="font-size:12px;opacity:0.95;line-height:1.4">${message}</div>
353 </div>
354 <button style="background:rgba(255,255,255,.2);border:none;color:#fff;cursor:pointer;font-size:16px;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;flex-shrink:0" onclick="this.parentElement.parentElement.remove()">✕</button>
355 </div>
356 `;
357
358 if (!document.getElementById('notification-styles')) {
359 const style = el('style', {id: 'notification-styles'});
360 style.textContent = `
361 @keyframes slideIn {
362 from { transform: translateX(400px); opacity: 0; }
363 to { transform: translateX(0); opacity: 1; }
364 }
365 @keyframes slideOut {
366 from { transform: translateX(0); opacity: 1; }
367 to { transform: translateX(400px); opacity: 0; }
368 }
369 `;
370 document.head.appendChild(style);
371 }
372
373 document.body.appendChild(notification);
374
375 setTimeout(() => {
376 notification.style.animation = 'slideOut 0.3s ease-in';
377 setTimeout(() => notification.remove(), 300);
378 }, 5000);
379 }
380
381 function createPanel(){
382 const panel = el('div',{class:'waw-panel'}); panel.style.display='none';
383 panel.innerHTML = `
384 <div class="waw-header">
385 <div class="waw-title">WhatsApp Message Scheduler</div>
386 <button class="waw-close" title="Close">✕</button>
387 </div>
388
389 <div class="waw-content" style="max-height:500px;overflow-y:auto;margin-top:10px">
390
391 <!-- Daily Stats Dashboard -->
392 <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:16px">
393 <div style="background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);border-radius:12px;padding:12px;text-align:center;cursor:pointer;transition:all .2s" id="stat-sent-card" title="Click to see details">
394 <div style="font-size:24px;font-weight:900;color:#25D366" id="stat-sent">0</div>
395 <div class="waw-small" style="color:#25D366;font-weight:600">✅ Sent Today</div>
396 </div>
397 <div style="background:rgba(239,68,68,.15);border:1px solid rgba(239,68,68,.3);border-radius:12px;padding:12px;text-align:center;cursor:pointer;transition:all .2s" id="stat-failed-card" title="Click to see details">
398 <div style="font-size:24px;font-weight:900;color:#ef4444" id="stat-failed">0</div>
399 <div class="waw-small" style="color:#ef4444;font-weight:600">❌ Failed Today</div>
400 </div>
401 <div style="background:rgba(59,130,246,.15);border:1px solid rgba(59,130,246,.3);border-radius:12px;padding:12px;text-align:center;cursor:pointer;transition:all .2s" id="stat-pending-card" title="Click to see details">
402 <div style="font-size:24px;font-weight:900;color:#3b82f6" id="stat-pending">0</div>
403 <div class="waw-small" style="color:#3b82f6;font-weight:600">⏳ Pending</div>
404 </div>
405 </div>
406
407 <!-- Details Modal -->
408 <div id="details-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:1000000;align-items:center;justify-content:center">
409 <div style="background:linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);border-radius:16px;padding:20px;max-width:500px;width:90%;max-height:80vh;overflow:auto;box-shadow:0 20px 60px rgba(0,0,0,.5)">
410 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
411 <h3 id="modal-title" style="margin:0;color:#25D366;font-size:18px"></h3>
412 <button class="waw-close" id="close-modal" style="position:static">✕</button>
413 </div>
414 <div id="modal-content" style="max-height:60vh;overflow:auto"></div>
415 </div>
416 </div>
417
418 <div class="waw-field">
419 <label>📱 To: (search name or enter phone)</label>
420 <input class="waw-input" id="sch-search" placeholder="Type name or phone number...">
421 <div class="waw-list" id="sch-results" style="max-height:120px;display:none;margin-top:4px;border:1px solid #2a3942;border-radius:8px;background:#0b141a"></div>
422 </div>
423
424 <div class="waw-field">
425 <label>💬 Message</label>
426 <textarea class="waw-textarea" id="sch-text" rows="3" placeholder="Type your message..."></textarea>
427 </div>
428
429 <div class="waw-field">
430 <label>⏰ Send at</label>
431 <input class="waw-dt" id="sch-when" type="datetime-local">
432 </div>
433
434 <div class="waw-field">
435 <label>🔄 Repeat (Optional)</label>
436 <select class="waw-input" id="sch-repeat" style="cursor:pointer">
437 <option value="">One time only</option>
438 <option value="daily">Daily</option>
439 <option value="weekly">Weekly</option>
440 </select>
441 </div>
442
443 <div class="waw-field" id="weekly-days-field" style="display:none">
444 <label>📅 Repeat on days</label>
445 <div style="display:flex;gap:6px;flex-wrap:wrap">
446 <label style="display:flex;align-items:center;gap:4px;cursor:pointer;background:rgba(255,255,255,.05);padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.1);transition:all .2s">
447 <input type="checkbox" class="day-checkbox" value="0" style="cursor:pointer">
448 <span style="font-size:12px">Sun</span>
449 </label>
450 <label style="display:flex;align-items:center;gap:4px;cursor:pointer;background:rgba(255,255,255,.05);padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.1);transition:all .2s">
451 <input type="checkbox" class="day-checkbox" value="1" style="cursor:pointer">
452 <span style="font-size:12px">Mon</span>
453 </label>
454 <label style="display:flex;align-items:center;gap:4px;cursor:pointer;background:rgba(255,255,255,.05);padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.1);transition:all .2s">
455 <input type="checkbox" class="day-checkbox" value="2" style="cursor:pointer">
456 <span style="font-size:12px">Tue</span>
457 </label>
458 <label style="display:flex;align-items:center;gap:4px;cursor:pointer;background:rgba(255,255,255,.05);padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.1);transition:all .2s">
459 <input type="checkbox" class="day-checkbox" value="3" style="cursor:pointer">
460 <span style="font-size:12px">Wed</span>
461 </label>
462 <label style="display:flex;align-items:center;gap:4px;cursor:pointer;background:rgba(255,255,255,.05);padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.1);transition:all .2s">
463 <input type="checkbox" class="day-checkbox" value="4" style="cursor:pointer">
464 <span style="font-size:12px">Thu</span>
465 </label>
466 <label style="display:flex;align-items:center;gap:4px;cursor:pointer;background:rgba(255,255,255,.05);padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.1);transition:all .2s">
467 <input type="checkbox" class="day-checkbox" value="5" style="cursor:pointer">
468 <span style="font-size:12px">Fri</span>
469 </label>
470 <label style="display:flex;align-items:center;gap:4px;cursor:pointer;background:rgba(255,255,255,.05);padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.1);transition:all .2s">
471 <input type="checkbox" class="day-checkbox" value="6" style="cursor:pointer">
472 <span style="font-size:12px">Sat</span>
473 </label>
474 </div>
475 </div>
476
477 <button class="waw-btn" id="btn-schedule" style="width:100%;margin-bottom:16px">Schedule Message</button>
478
479 <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">
480 <button class="waw-btn secondary" id="btn-send-now" style="width:100%">🚀 Send Now</button>
481 <button class="waw-btn secondary" id="btn-clear-form" style="width:100%">🗑️ Clear</button>
482 </div>
483
484 <div style="border-top:2px solid #2a3942;padding-top:12px;margin-top:12px">
485 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
486 <label style="font-weight:700;font-size:13px">📅 Scheduled Messages</label>
487 <button class="waw-btn secondary" id="btn-refreshjobs" style="padding:6px 12px;font-size:11px">Refresh</button>
488 </div>
489 <div class="waw-list" id="jobs-list"></div>
490 </div>
491
492 <!-- Bulk Import Section -->
493 <div style="border-top:2px solid #2a3942;padding-top:12px;margin-top:12px">
494 <label style="font-weight:700;font-size:13px;display:block;margin-bottom:8px;color:#3b82f6">📂 Bulk Import from CSV</label>
495 <div class="waw-small" style="margin-bottom:8px;opacity:0.7">Format: target,message,datetime (e.g., John,Hello,2024-12-31 14:30)</div>
496 <input type="file" id="csv-file" accept=".csv,.txt" style="display:none">
497 <button class="waw-btn secondary" id="btn-import-csv" style="width:100%;margin-bottom:8px">📥 Choose CSV File</button>
498 <div id="import-preview" style="display:none;background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.3);border-radius:8px;padding:12px;margin-top:8px">
499 <div style="font-weight:700;margin-bottom:8px;color:#3b82f6">Preview:</div>
500 <div id="import-preview-content" style="max-height:120px;overflow:auto;font-size:11px"></div>
501 <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px">
502 <button class="waw-btn" id="btn-confirm-import" style="padding:8px">✅ Import All</button>
503 <button class="waw-btn secondary" id="btn-cancel-import" style="padding:8px">❌ Cancel</button>
504 </div>
505 </div>
506 </div>
507
508 <!-- Recent Sent Messages -->
509 <div style="border-top:2px solid #2a3942;padding-top:12px;margin-top:12px" id="recent-sent-section">
510 <label style="font-weight:700;font-size:13px;display:block;margin-bottom:8px;color:#25D366">📨 Recently Sent Today</label>
511 <div id="recent-sent-list" style="max-height:150px;overflow:auto"></div>
512 </div>
513
514 <!-- Export by Date Range Section -->
515 <div style="border-top:2px solid #2a3942;padding-top:12px;margin-top:12px">
516 <label style="font-weight:700;font-size:13px;display:block;margin-bottom:8px;color:#f59e0b">📅 Export by Date Range</label>
517 <button class="waw-btn secondary" id="btn-export-range" style="width:100%">📅 Export by Date Range</button>
518 </div>
519
520 <!-- Export Modal -->
521 <div id="export-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:1000001;align-items:center;justify-content:center">
522 <div style="background:linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);border-radius:16px;padding:20px;max-width:450px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.5)">
523 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
524 <h3 style="margin:0;color:#f59e0b;font-size:18px">📅 Export by Date Range</h3>
525 <button class="waw-close" id="close-export-modal" style="position:static">✕</button>
526 </div>
527
528 <div style="margin-bottom:16px">
529 <label style="display:block;font-size:13px;color:#cbd5e1;margin-bottom:6px;font-weight:600">📆 Start Date</label>
530 <input class="waw-dt" id="export-start-date" type="date" style="width:100%">
531 </div>
532
533 <div style="margin-bottom:16px">
534 <label style="display:block;font-size:13px;color:#cbd5e1;margin-bottom:6px;font-weight:600">📆 End Date</label>
535 <input class="waw-dt" id="export-end-date" type="date" style="width:100%">
536 </div>
537
538 <div style="margin-bottom:16px">
539 <label style="display:block;font-size:13px;color:#cbd5e1;margin-bottom:8px;font-weight:600">📋 Include:</label>
540 <div style="display:flex;gap:12px">
541 <label style="display:flex;align-items:center;gap:6px;cursor:pointer;background:rgba(255,255,255,.05);padding:10px 16px;border-radius:8px;border:1px solid rgba(255,255,255,.1);flex:1">
542 <input type="checkbox" id="export-include-history" checked style="cursor:pointer">
543 <span style="font-size:13px">History (Sent/Failed)</span>
544 </label>
545 <label style="display:flex;align-items:center;gap:6px;cursor:pointer;background:rgba(255,255,255,.05);padding:10px 16px;border-radius:8px;border:1px solid rgba(255,255,255,.1);flex:1">
546 <input type="checkbox" id="export-include-scheduled" checked style="cursor:pointer">
547 <span style="font-size:13px">Scheduled</span>
548 </label>
549 </div>
550 <div style="margin-top:8px">
551 <label style="display:flex;align-items:center;gap:6px;cursor:pointer;background:rgba(255,255,255,.05);padding:10px 16px;border-radius:8px;border:1px solid rgba(255,255,255,.1)">
552 <input type="checkbox" id="export-include-archive" checked style="cursor:pointer">
553 <span style="font-size:13px">Archive (Deleted/Dismissed)</span>
554 </label>
555 </div>
556 </div>
557
558 <div id="export-preview-section" style="display:none;margin-bottom:16px;background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.3);border-radius:8px;padding:12px">
559 <div style="font-weight:700;margin-bottom:6px;color:#f59e0b;font-size:13px">Preview:</div>
560 <div id="export-preview-text" style="font-size:12px;opacity:0.9"></div>
561 </div>
562
563 <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
564 <button class="waw-btn" id="btn-export-json" style="padding:10px">📄 Export JSON</button>
565 <button class="waw-btn" id="btn-export-csv" style="padding:10px">📊 Export CSV</button>
566 </div>
567 </div>
568 </div>
569
570 <div style="border-top:2px solid #2a3942;padding-top:12px;margin-top:12px">
571 <label style="font-weight:700;font-size:13px;display:block;margin-bottom:8px">📞 Address Book</label>
572 <div class="waw-row" style="margin-bottom:8px">
573 <input class="waw-input" id="book-label" placeholder="Name" style="flex:1">
574 <input class="waw-input" id="book-phone" placeholder="Phone" style="flex:1">
575 <button class="waw-btn" id="book-save" style="padding:8px 16px">Save</button>
576 </div>
577 <div class="waw-list" id="book-list"></div>
578 </div>
579
580 <div class="waw-small" style="text-align:center;margin-top:12px;padding-top:12px;border-top:1px solid #2a3942">Keep this tab open & logged-in for auto-send</div>
581 </div>
582 `;
583 document.body.appendChild(panel);
584 const fab = el('button',{class:'waw-fab',title:'Open Pro Scheduler'},'⏰'); document.body.appendChild(fab);
585
586 // Restore FAB position from localStorage
587 const savedFabPos = loadFabPos();
588 if (savedFabPos) {
589 fab.style.left = savedFabPos.left;
590 fab.style.top = savedFabPos.top;
591 fab.style.right = 'auto';
592 fab.style.bottom = 'auto';
593 }
594
595 fab.onclick = ()=>{
596 panel.style.display = panel.style.display==='none' ? 'block' : 'none';
597 const p = loadPrefs(); p.panelOpen = (panel.style.display!=='none'); savePrefs(p);
598 if (p.panelOpen){ refreshJobs(); renderBook(); prefillDate(); }
599 };
600 panel.querySelector('.waw-close').onclick = ()=>{
601 panel.style.display='none'; const p=loadPrefs(); p.panelOpen=false; savePrefs(p);
602 };
603
604 // FAB drag functionality - only with Shift key held
605 let fabDragging = false;
606 let fabCurrentX, fabCurrentY, fabInitialX, fabInitialY;
607
608 fab.addEventListener('mousedown', (e) => {
609 // Only allow dragging if Shift key is held
610 if (!e.shiftKey) return;
611
612 e.preventDefault();
613 fabDragging = true;
614 fab.style.cursor = 'grabbing';
615 fab.style.transition = 'none';
616 fabInitialX = e.clientX - fab.offsetLeft;
617 fabInitialY = e.clientY - fab.offsetTop;
618 });
619
620 document.addEventListener('mousemove', (e) => {
621 if (!fabDragging) return;
622 e.preventDefault();
623 fabCurrentX = e.clientX - fabInitialX;
624 fabCurrentY = e.clientY - fabInitialY;
625 fab.style.left = fabCurrentX + 'px';
626 fab.style.top = fabCurrentY + 'px';
627 fab.style.right = 'auto';
628 fab.style.bottom = 'auto';
629 });
630
631 document.addEventListener('mouseup', () => {
632 if (fabDragging) {
633 fabDragging = false;
634 fab.style.cursor = 'grab';
635 fab.style.transition = 'all .3s';
636 saveFabPos({
637 left: fab.style.left,
638 top: fab.style.top
639 });
640 }
641 });
642
643 fab.style.cursor = 'grab';
644 fab.title = 'Click to open | Hold Shift + Drag to move';
645
646 const modal = panel.querySelector('#details-modal');
647 const modalTitle = panel.querySelector('#modal-title');
648 const modalContent = panel.querySelector('#modal-content');
649 const closeModal = panel.querySelector('#close-modal');
650
651 closeModal.onclick = () => { modal.style.display = 'none'; };
652 modal.onclick = (e) => { if (e.target === modal) modal.style.display = 'none'; };
653
654 function showModal(title, items, type) {
655 modalTitle.textContent = title;
656 modalContent.innerHTML = '';
657
658 if (items.length === 0) {
659 modalContent.innerHTML = '<div class="waw-small" style="text-align:center;padding:20px;opacity:0.5">No items to display</div>';
660 } else {
661 items.forEach(item => {
662 const itemEl = el('div', {style: 'padding:12px;background:rgba(255,255,255,.05);border-radius:8px;margin-bottom:8px;border-left:3px solid ' + (type === 'sent' ? '#25D366' : type === 'failed' ? '#ef4444' : '#3b82f6')});
663
664 if (type === 'pending') {
665 const tgt = item.target.phone ? `📞 ${item.target.phone}` : `👤 ${item.target.name}`;
666 itemEl.innerHTML = `
667 <div style="display:flex;justify-content:space-between;margin-bottom:6px">
668 <span style="font-weight:700;color:#3b82f6">${tgt}</span>
669 <span style="opacity:0.7;font-size:11px">${fmt(item.when)}</span>
670 </div>
671 <div class="waw-small" style="color:#3b82f6;font-weight:600;margin-bottom:4px">${timeUntil(item.when)}</div>
672 <div style="opacity:0.8;font-size:14px">${item.text}</div>
673 `;
674 } else {
675 itemEl.innerHTML = `
676 <div style="display:flex;justify-content:space-between;margin-bottom:6px">
677 <span style="font-weight:700;color:${type === 'sent' ? '#25D366' : '#ef4444'}">${item.target}</span>
678 <span style="opacity:0.7;font-size:11px">${fmt(item.timestamp)}</span>
679 </div>
680 <div style="opacity:0.8;font-size:14px;margin-bottom:${item.error ? '6px' : '0'}">${item.text}</div>
681 ${item.error ? `<div style="color:#ef4444;font-size:11px;background:rgba(239,68,68,.1);padding:6px;border-radius:4px">❌ Error: ${item.error}</div>` : ''}
682 `;
683 }
684
685 modalContent.appendChild(itemEl);
686 });
687 }
688
689 modal.style.display = 'flex';
690 }
691
692 panel.querySelector('#stat-sent-card').onclick = () => {
693 const history = loadHistory();
694 const sentToday = history.filter(h => isToday(h.timestamp) && h.status === 'sent');
695 showModal('✅ Messages Sent Today', sentToday, 'sent');
696 };
697
698 panel.querySelector('#stat-failed-card').onclick = () => {
699 const history = loadHistory();
700 const failedToday = history.filter(h => isToday(h.timestamp) && h.status === 'failed');
701 showModal('❌ Failed Messages Today', failedToday, 'failed');
702 };
703
704 panel.querySelector('#stat-pending-card').onclick = () => {
705 const jobs = loadJobs().sort((a,b)=>a.when-b.when);
706 showModal('⏳ Pending Messages', jobs, 'pending');
707 };
708
709 const header = panel.querySelector('.waw-header');
710 let isDragging = false;
711 let currentX, currentY, initialX, initialY;
712
713 header.addEventListener('mousedown', (e) => {
714 if (e.target.closest('.waw-close')) return;
715 isDragging = true;
716 panel.classList.add('dragging');
717 initialX = e.clientX - panel.offsetLeft;
718 initialY = e.clientY - panel.offsetTop;
719 });
720
721 document.addEventListener('mousemove', (e) => {
722 if (!isDragging) return;
723 e.preventDefault();
724 currentX = e.clientX - initialX;
725 currentY = e.clientY - initialY;
726 panel.style.left = currentX + 'px';
727 panel.style.top = currentY + 'px';
728 panel.style.right = 'auto';
729 panel.style.bottom = 'auto';
730 });
731
732 document.addEventListener('mouseup', () => {
733 if (isDragging) {
734 isDragging = false;
735 panel.classList.remove('dragging');
736 }
737 });
738
739 let selectedTarget = null;
740 const searchInput = panel.querySelector('#sch-search');
741 const resultsBox = panel.querySelector('#sch-results');
742
743 const repeatSelect = panel.querySelector('#sch-repeat');
744 const weeklyDaysField = panel.querySelector('#weekly-days-field');
745
746 repeatSelect.onchange = () => {
747 if (repeatSelect.value === 'weekly') {
748 weeklyDaysField.style.display = 'block';
749 } else {
750 weeklyDaysField.style.display = 'none';
751 }
752 };
753
754 searchInput.oninput = ()=>{
755 const query = searchInput.value.trim().toLowerCase();
756 if (!query) {
757 resultsBox.style.display = 'none';
758 resultsBox.innerHTML = '';
759 selectedTarget = null;
760 return;
761 }
762
763 const chats = getSearchResults();
764 const book = loadBook();
765 const results = [];
766
767 chats.forEach(c => {
768 if (c.name.toLowerCase().includes(query)) {
769 results.push({ type: 'chat', label: c.name, value: c.name });
770 }
771 });
772
773 book.forEach(b => {
774 if (b.label.toLowerCase().includes(query) || b.phone.includes(query)) {
775 results.push({ type: 'contact', label: b.label + ' (' + b.phone + ')', value: b.phone, isPhone: true });
776 }
777 });
778
779 const digitsOnly = query.replace(/[^\d]/g, '');
780 if (digitsOnly.length >= 8) {
781 results.push({ type: 'phone', label: '📞 New number: ' + digitsOnly, value: digitsOnly, isPhone: true });
782 }
783
784 if (results.length === 0) {
785 resultsBox.style.display = 'none';
786 resultsBox.innerHTML = '';
787 } else {
788 resultsBox.style.display = 'block';
789 resultsBox.innerHTML = '';
790 results.forEach(r => {
791 const item = el('div', {class: 'waw-item', style: 'cursor:pointer;padding:8px;border-bottom:1px solid #2a3942'});
792 item.textContent = r.label;
793 item.onclick = ()=>{
794 selectedTarget = r;
795 searchInput.value = r.label;
796 resultsBox.style.display = 'none';
797 };
798 resultsBox.appendChild(item);
799 });
800 }
801 };
802
803 panel.querySelector('#btn-schedule').onclick = ()=>{
804 const text = panel.querySelector('#sch-text').value.trim();
805 const whenStr = panel.querySelector('#sch-when').value;
806 const repeatType = panel.querySelector('#sch-repeat').value;
807
808 if (!text) return alert('Message required');
809 if (!whenStr) return alert('Choose date & time');
810 const when = Date.parse(whenStr);
811 if (isNaN(when) || when<=Date.now()) return alert('Choose a future time');
812 if (!selectedTarget) return alert('Please select a contact or enter a phone number');
813
814 let weeklyDays = [];
815 if (repeatType === 'weekly') {
816 const checkboxes = panel.querySelectorAll('.day-checkbox:checked');
817 weeklyDays = Array.from(checkboxes).map(cb => cb.value);
818 if (weeklyDays.length === 0) {
819 return alert('Please select at least one day for weekly repeat');
820 }
821 }
822
823 const job = {
824 id: uid(),
825 when,
826 text,
827 target: selectedTarget.isPhone
828 ? { name: null, phone: normalizePhone(selectedTarget.value) }
829 : { name: selectedTarget.value, phone: null },
830 repeat: repeatType || null,
831 weeklyDays: weeklyDays.length > 0 ? weeklyDays : null
832 };
833
834 const jobs = loadJobs();
835 jobs.push(job);
836 saveJobs(jobs);
837
838 panel.querySelector('#sch-text').value='';
839 panel.querySelector('#sch-repeat').value='';
840 weeklyDaysField.style.display = 'none';
841 panel.querySelectorAll('.day-checkbox').forEach(cb => cb.checked = false);
842 searchInput.value = '';
843 selectedTarget = null;
844
845 refreshJobs();
846 alert(repeatType ? `Scheduled with ${repeatType} repeat!` : 'Scheduled');
847 };
848
849 panel.querySelector('#btn-send-now').onclick = async ()=>{
850 const text = panel.querySelector('#sch-text').value.trim();
851 if (!text) return alert('Message required');
852 if (!selectedTarget) return alert('Please select a contact or enter a phone number');
853
854 const job = {
855 id: uid(),
856 text,
857 target: selectedTarget.isPhone
858 ? { name: null, phone: normalizePhone(selectedTarget.value) }
859 : { name: selectedTarget.value, phone: null }
860 };
861
862 try {
863 await performJob(job);
864 const tgt = job.target.phone || job.target.name;
865 addToHistory('sent', tgt, job.text);
866
867 showNotification(
868 '✅ Message Sent',
869 `Sent to ${tgt}: ${job.text.substring(0, 50)}${job.text.length > 50 ? '...' : ''}`,
870 'success'
871 );
872
873 panel.querySelector('#sch-text').value='';
874 searchInput.value = '';
875 selectedTarget = null;
876 refreshJobs();
877 alert('✅ Message sent successfully!');
878 } catch(err) {
879 const tgt = job.target.phone || job.target.name;
880 addToHistory('failed', tgt, job.text, err.message);
881
882 showNotification(
883 '❌ Message Failed',
884 `Failed to send to ${tgt}: ${err.message}`,
885 'error'
886 );
887
888 refreshJobs();
889 alert('❌ Failed: ' + err.message);
890 }
891 };
892
893 panel.querySelector('#btn-clear-form').onclick = ()=>{
894 panel.querySelector('#sch-text').value='';
895 searchInput.value = '';
896 selectedTarget = null;
897 panel.querySelector('#sch-when').value='';
898 prefillDate();
899 };
900
901 panel.querySelector('#btn-refreshjobs').onclick = refreshJobs;
902
903 panel.querySelector('#book-save').onclick = ()=>{
904 const label = panel.querySelector('#book-label').value.trim();
905 const phone = panel.querySelector('#book-phone').value.trim();
906 if (!label || !phone) return alert('Label & phone required');
907 const book = loadBook();
908 const rec = { label, phone: normalizePhone(phone) };
909 const i = book.findIndex(x=>x.label.toLowerCase()===label.toLowerCase());
910 if (i>=0) book[i]=rec; else book.push(rec);
911 saveBook(book);
912 panel.querySelector('#book-label').value=''; panel.querySelector('#book-phone').value='';
913 renderBook();
914 };
915
916 let parsedCSVData = [];
917 const csvFileInput = panel.querySelector('#csv-file');
918 const importPreview = panel.querySelector('#import-preview');
919 const importPreviewContent = panel.querySelector('#import-preview-content');
920
921 panel.querySelector('#btn-import-csv').onclick = () => {
922 csvFileInput.click();
923 };
924
925 csvFileInput.onchange = async (e) => {
926 const file = e.target.files[0];
927 if (!file) return;
928
929 try {
930 const text = await file.text();
931 const lines = text.split('\n').filter(l => l.trim());
932 parsedCSVData = [];
933
934 lines.forEach((line, idx) => {
935 const parts = line.split(',').map(p => p.trim());
936 if (parts.length < 3) return;
937
938 const [target, message, datetime] = parts;
939 const when = Date.parse(datetime);
940
941 if (!target || !message || isNaN(when)) {
942 log('Skipping invalid line', idx + 1, ':', line);
943 return;
944 }
945
946 const digitsOnly = target.replace(/[^\d]/g, '');
947 const isPhone = digitsOnly.length >= 8;
948
949 parsedCSVData.push({
950 target: isPhone ? { name: null, phone: normalizePhone(target) } : { name: target, phone: null },
951 text: message,
952 when: when
953 });
954 });
955
956 if (parsedCSVData.length === 0) {
957 alert('No valid entries found in CSV');
958 return;
959 }
960
961 importPreviewContent.innerHTML = '';
962 parsedCSVData.forEach((item, idx) => {
963 const tgt = item.target.phone ? `📞 ${item.target.phone}` : `👤 ${item.target.name}`;
964 const previewItem = el('div', {style: 'padding:6px;background:rgba(255,255,255,.05);border-radius:4px;margin-bottom:4px'});
965 previewItem.innerHTML = `
966 <div style="font-weight:700;color:#3b82f6">${idx + 1}. ${tgt}</div>
967 <div style="opacity:0.8;font-size:10px">${item.text.substring(0, 50)}${item.text.length > 50 ? '...' : ''}</div>
968 <div style="opacity:0.6;font-size:10px">${fmt(item.when)}</div>
969 `;
970 importPreviewContent.appendChild(previewItem);
971 });
972
973 importPreview.style.display = 'block';
974 } catch (err) {
975 alert('Failed to read CSV: ' + err.message);
976 log('CSV import error:', err);
977 }
978 };
979
980 panel.querySelector('#btn-confirm-import').onclick = () => {
981 if (parsedCSVData.length === 0) return;
982
983 const jobs = loadJobs();
984 parsedCSVData.forEach(item => {
985 jobs.push({
986 id: uid(),
987 when: item.when,
988 text: item.text,
989 target: item.target
990 });
991 });
992 saveJobs(jobs);
993
994 alert(`✅ Imported ${parsedCSVData.length} messages successfully!`);
995 parsedCSVData = [];
996 importPreview.style.display = 'none';
997 csvFileInput.value = '';
998 refreshJobs();
999 };
1000
1001 panel.querySelector('#btn-cancel-import').onclick = () => {
1002 parsedCSVData = [];
1003 importPreview.style.display = 'none';
1004 csvFileInput.value = '';
1005 };
1006
1007 // Export by Date Range functionality
1008 const exportModal = panel.querySelector('#export-modal');
1009 const closeExportModal = panel.querySelector('#close-export-modal');
1010 const exportStartDate = panel.querySelector('#export-start-date');
1011 const exportEndDate = panel.querySelector('#export-end-date');
1012 const exportIncludeHistory = panel.querySelector('#export-include-history');
1013 const exportIncludeScheduled = panel.querySelector('#export-include-scheduled');
1014 const exportIncludeArchive = panel.querySelector('#export-include-archive');
1015 const exportPreviewSection = panel.querySelector('#export-preview-section');
1016 const exportPreviewText = panel.querySelector('#export-preview-text');
1017
1018 // Set default dates (last 30 days to today)
1019 const today = new Date();
1020 const thirtyDaysAgo = new Date(today);
1021 thirtyDaysAgo.setDate(today.getDate() - 30);
1022 exportStartDate.value = thirtyDaysAgo.toISOString().split('T')[0];
1023 exportEndDate.value = today.toISOString().split('T')[0];
1024
1025 panel.querySelector('#btn-export-range').onclick = () => {
1026 exportModal.style.display = 'flex';
1027 updateExportPreview();
1028 };
1029
1030 closeExportModal.onclick = () => {
1031 exportModal.style.display = 'none';
1032 };
1033
1034 exportModal.onclick = (e) => {
1035 if (e.target === exportModal) exportModal.style.display = 'none';
1036 };
1037
1038 // Update preview when dates or checkboxes change
1039 exportStartDate.onchange = updateExportPreview;
1040 exportEndDate.onchange = updateExportPreview;
1041 exportIncludeHistory.onchange = updateExportPreview;
1042 exportIncludeScheduled.onchange = updateExportPreview;
1043 exportIncludeArchive.onchange = updateExportPreview;
1044
1045 function updateExportPreview() {
1046 const data = getExportData();
1047 if (data.length === 0) {
1048 exportPreviewSection.style.display = 'none';
1049 } else {
1050 exportPreviewSection.style.display = 'block';
1051 const historyCount = data.filter(d => d.type === 'history').length;
1052 const scheduledCount = data.filter(d => d.type === 'scheduled').length;
1053 const archivedCount = data.filter(d => d.type === 'archive').length;
1054 exportPreviewText.innerHTML = `
1055 Found <strong>${data.length}</strong> total messages:<br>
1056 • <span style="color:#25D366">${historyCount}</span> history items (sent/failed)<br>
1057 • <span style="color:#3b82f6">${scheduledCount}</span> scheduled messages<br>
1058 • <span style="color:#f59e0b">${archivedCount}</span> archived items
1059 `;
1060 }
1061 }
1062
1063 function getExportData() {
1064 const startDate = new Date(exportStartDate.value);
1065 startDate.setHours(0, 0, 0, 0);
1066 const endDate = new Date(exportEndDate.value);
1067 endDate.setHours(23, 59, 59, 999);
1068
1069 const includeHistory = exportIncludeHistory.checked;
1070 const includeScheduled = exportIncludeScheduled.checked;
1071 const includeArchive = exportIncludeArchive.checked;
1072
1073 const data = [];
1074
1075 if (includeHistory) {
1076 const history = loadHistory();
1077 history.forEach(h => {
1078 const itemDate = new Date(h.timestamp);
1079 if (itemDate >= startDate && itemDate <= endDate) {
1080 data.push({
1081 type: 'history',
1082 status: h.status,
1083 target: h.target,
1084 message: h.text,
1085 timestamp: h.timestamp,
1086 date: fmt(h.timestamp),
1087 error: h.error || ''
1088 });
1089 }
1090 });
1091 }
1092
1093 if (includeScheduled) {
1094 const jobs = loadJobs();
1095 jobs.forEach(j => {
1096 const itemDate = new Date(j.when);
1097 if (itemDate >= startDate && itemDate <= endDate) {
1098 const target = j.target.phone ? j.target.phone : j.target.name;
1099 data.push({
1100 type: 'scheduled',
1101 status: 'pending',
1102 target: target,
1103 message: j.text,
1104 timestamp: j.when,
1105 date: fmt(j.when),
1106 repeat: j.repeat || '',
1107 weeklyDays: j.weeklyDays ? j.weeklyDays.join(',') : ''
1108 });
1109 }
1110 });
1111 }
1112
1113 if (includeArchive) {
1114 const archive = loadArchive();
1115 archive.forEach(a => {
1116 const itemDate = new Date(a.timestamp);
1117 if (itemDate >= startDate && itemDate <= endDate) {
1118 data.push({
1119 type: 'archive',
1120 status: a.status,
1121 target: a.target,
1122 message: a.text,
1123 timestamp: a.timestamp,
1124 date: fmt(a.timestamp),
1125 error: a.error || '',
1126 repeat: a.repeat || '',
1127 weeklyDays: a.weeklyDays || '',
1128 archivedAt: a.archivedAt ? fmt(a.archivedAt) : ''
1129 });
1130 }
1131 });
1132 }
1133
1134 // Sort by timestamp
1135 data.sort((a, b) => a.timestamp - b.timestamp);
1136
1137 return data;
1138 }
1139
1140 panel.querySelector('#btn-export-json').onclick = () => {
1141 const data = getExportData();
1142 if (data.length === 0) {
1143 alert('No data to export in the selected date range');
1144 return;
1145 }
1146
1147 const json = JSON.stringify(data, null, 2);
1148 const blob = new Blob([json], { type: 'application/json' });
1149 const url = URL.createObjectURL(blob);
1150 const a = document.createElement('a');
1151 a.href = url;
1152 a.download = `whatsapp-messages-${exportStartDate.value}-to-${exportEndDate.value}.json`;
1153 document.body.appendChild(a);
1154 a.click();
1155 document.body.removeChild(a);
1156 URL.revokeObjectURL(url);
1157
1158 showNotification(
1159 '✅ Export Successful',
1160 `Exported ${data.length} messages to JSON`,
1161 'success'
1162 );
1163 };
1164
1165 panel.querySelector('#btn-export-csv').onclick = () => {
1166 const data = getExportData();
1167 if (data.length === 0) {
1168 alert('No data to export in the selected date range');
1169 return;
1170 }
1171
1172 // CSV header
1173 let csv = 'Type,Status,Target,Message,Date,Timestamp,Error,Repeat,WeeklyDays,ArchivedAt\n';
1174
1175 // CSV rows
1176 data.forEach(item => {
1177 const row = [
1178 item.type,
1179 item.status,
1180 `"${item.target.replace(/"/g, '""')}"`,
1181 `"${item.message.replace(/"/g, '""')}"`,
1182 `"${item.date}"`,
1183 item.timestamp,
1184 `"${(item.error || '').replace(/"/g, '""')}"`,
1185 item.repeat || '',
1186 item.weeklyDays || '',
1187 item.archivedAt || ''
1188 ];
1189 csv += row.join(',') + '\n';
1190 });
1191
1192 const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
1193 const url = URL.createObjectURL(blob);
1194 const a = document.createElement('a');
1195 a.href = url;
1196 a.download = `whatsapp-messages-${exportStartDate.value}-to-${exportEndDate.value}.csv`;
1197 document.body.appendChild(a);
1198 a.click();
1199 document.body.removeChild(a);
1200 URL.revokeObjectURL(url);
1201
1202 showNotification(
1203 '✅ Export Successful',
1204 `Exported ${data.length} messages to CSV`,
1205 'success'
1206 );
1207 };
1208
1209 function jobRow(job){
1210 const tgt = job.target.phone ? `Phone: ${job.target.phone}` : `Name: ${job.target.name}`;
1211 const row = el('div',{class:'waw-item'});
1212 row.innerHTML = `
1213 <div style="flex:1">
1214 <div style="font-weight:700;font-size:13px">${fmt(job.when)}</div>
1215 <div class="waw-small countdown-${job.id}" style="color:#25D366;font-weight:600;margin:4px 0">${timeUntil(job.when)}</div>
1216 <div class="waw-small" style="opacity:0.8">${tgt}</div>
1217 <div class="waw-small" style="white-space:pre-wrap;max-width:240px;margin-top:6px;background:rgba(255,255,255,.05);padding:8px;border-radius:8px;border-left:3px solid #25D366">${job.text}</div>
1218 </div>
1219 <div style="display:flex;flex-direction:column;gap:6px;min-width:90px">
1220 <button class="waw-btn secondary" data-act="edit" data-id="${job.id}" style="padding:8px 12px;font-size:11px">✏️ Edit</button>
1221 <button class="waw-btn secondary" data-act="sendnow" data-id="${job.id}" style="padding:8px 12px;font-size:11px">🚀 Send</button>
1222 <button class="waw-btn secondary" data-act="del" data-id="${job.id}" style="padding:8px 12px;font-size:11px;background:rgba(220,38,38,.2);border-color:rgba(220,38,38,.3)">🗑️ Del</button>
1223 </div>`;
1224 return row;
1225 }
1226
1227 function refreshJobs(){
1228 const box = panel.querySelector('#jobs-list');
1229 const jobs = loadJobs().sort((a,b)=>a.when-b.when);
1230 box.innerHTML = jobs.length ? '' : '<div class="waw-small" style="text-align:center;padding:20px;opacity:0.5">No scheduled messages yet 📭</div>';
1231 jobs.forEach(j=>box.appendChild(jobRow(j)));
1232
1233 const fab = document.querySelector('.waw-fab');
1234 if (jobs.length > 0) {
1235 fab.classList.add('has-jobs');
1236 fab.innerHTML = jobs.length;
1237 } else {
1238 fab.classList.remove('has-jobs');
1239 fab.innerHTML = '⏰';
1240 }
1241
1242 updateStats();
1243
1244 box.onclick = async (e)=>{
1245 const b = e.target.closest('button'); if (!b) return;
1246 const act=b.getAttribute('data-act'); const id=b.getAttribute('data-id');
1247 const jobs = loadJobs(); const job = jobs.find(x=>x.id===id); if (!job) return;
1248 if (act==='del'){
1249 if (confirm('Delete this scheduled message?')) {
1250 archiveJob(job);
1251 saveJobs(jobs.filter(x=>x.id!==id));
1252 refreshJobs();
1253 }
1254 }
1255 if (act==='edit'){
1256 panel.querySelector('#sch-text').value = job.text;
1257 const dt = panel.querySelector('#sch-when');
1258 const t = new Date(job.when);
1259 const year = t.getFullYear();
1260 const month = pad(t.getMonth() + 1);
1261 const day = pad(t.getDate());
1262 const hours = pad(t.getHours());
1263 const minutes = pad(t.getMinutes());
1264 dt.value = `${year}-${month}-${day}T${hours}:${minutes}`;
1265
1266 if (job.target.phone) {
1267 searchInput.value = '📞 ' + job.target.phone;
1268 selectedTarget = { type: 'phone', label: job.target.phone, value: job.target.phone, isPhone: true };
1269 } else if (job.target.name) {
1270 searchInput.value = job.target.name;
1271 selectedTarget = { type: 'chat', label: job.target.name, value: job.target.name };
1272 }
1273
1274 saveJobs(jobs.filter(x=>x.id!==id));
1275 refreshJobs();
1276
1277 panel.querySelector('.waw-content').scrollTop = 0;
1278 }
1279 if (act==='sendnow'){
1280 if (confirm('Send this message now?')) {
1281 try {
1282 await performJob(job);
1283 const tgt = job.target.phone || job.target.name;
1284 addToHistory('sent', tgt, job.text);
1285
1286 showNotification(
1287 '✅ Message Sent',
1288 `Sent to ${tgt}: ${job.text.substring(0, 50)}${job.text.length > 50 ? '...' : ''}`,
1289 'success'
1290 );
1291
1292 saveJobs(loadJobs().filter(x=>x.id!==id));
1293 refreshJobs();
1294 alert('✅ Message sent successfully!');
1295 }
1296 catch(err){
1297 const tgt = job.target.phone || job.target.name;
1298 addToHistory('failed', tgt, job.text, err.message);
1299
1300 showNotification(
1301 '❌ Message Failed',
1302 `Failed to send to ${tgt}: ${err.message}`,
1303 'error'
1304 );
1305
1306 refreshJobs();
1307 alert('❌ Failed: '+err.message);
1308 }
1309 }
1310 }
1311 };
1312 }
1313
1314 function updateStats(){
1315 const stats = getStats();
1316 const sentEl = panel.querySelector('#stat-sent');
1317 const failedEl = panel.querySelector('#stat-failed');
1318 const pendingEl = panel.querySelector('#stat-pending');
1319
1320 if (sentEl) sentEl.textContent = stats.sentToday;
1321 if (failedEl) failedEl.textContent = stats.failedToday;
1322 if (pendingEl) pendingEl.textContent = stats.pendingTotal;
1323
1324 const recentList = panel.querySelector('#recent-sent-list');
1325 const recentSection = panel.querySelector('#recent-sent-section');
1326 if (recentList && stats.recentSent.length > 0) {
1327 recentSection.style.display = 'block';
1328 recentList.innerHTML = '';
1329 stats.recentSent.forEach(h => {
1330 const item = el('div', {class: 'waw-small', style: 'padding:8px;background:rgba(37,211,102,.08);border-radius:8px;margin-bottom:6px;border-left:3px solid #25D366;display:flex;justify-content:space-between;align-items:center;gap:8px'});
1331 item.innerHTML = `
1332 <div style="flex:1">
1333 <div style="display:flex;justify-content:space-between;margin-bottom:4px">
1334 <span style="font-weight:700;color:#25D366">${h.target}</span>
1335 <span style="opacity:0.7">${fmtShort(h.timestamp)}</span>
1336 </div>
1337 <div style="opacity:0.8">${h.text}</div>
1338 </div>
1339 <button class="waw-btn secondary" data-act="dismiss" data-id="${h.id}" style="padding:6px 10px;font-size:11px;min-width:auto;white-space:nowrap" title="Mark as seen">✓ Seen</button>
1340 `;
1341 recentList.appendChild(item);
1342 });
1343
1344 recentList.onclick = (e) => {
1345 const btn = e.target.closest('button[data-act="dismiss"]');
1346 if (!btn) return;
1347 const id = btn.getAttribute('data-id');
1348 const history = loadHistory();
1349 const item = history.find(h => h.id === id);
1350 if (item) {
1351 const archive = loadArchive();
1352 archive.push({
1353 ...item,
1354 archivedAt: Date.now(),
1355 dismissedAt: Date.now()
1356 });
1357 saveArchive(archive);
1358 }
1359 const filtered = history.filter(h => h.id !== id);
1360 saveHistory(filtered);
1361 updateStats();
1362 };
1363 } else if (recentSection) {
1364 recentSection.style.display = 'none';
1365 }
1366 }
1367
1368 function renderBook(){
1369 const list = panel.querySelector('#book-list');
1370 const book = loadBook().sort((a,b)=>a.label.localeCompare(b.label));
1371 list.innerHTML = book.length ? '' : '<div class="waw-small">Empty</div>';
1372 book.forEach(rec=>{
1373 const row = el('div',{class:'waw-item'});
1374 row.innerHTML = `
1375 <div>
1376 <div style="font-weight:700">${rec.label}</div>
1377 <div class="waw-small">${rec.phone}</div>
1378 </div>
1379 <div>
1380 <button class="waw-btn secondary" data-act="open" data-phone="${rec.phone}">Open</button>
1381 <button class="waw-btn secondary" data-act="use" data-phone="${rec.phone}">Use in scheduler</button>
1382 <button class="waw-btn secondary" data-act="del" data-label="${rec.label}">Delete</button>
1383 </div>`;
1384 list.appendChild(row);
1385 });
1386 list.onclick = async (e)=>{
1387 const b = e.target.closest('button'); if (!b) return;
1388 const act = b.getAttribute('data-act');
1389 if (act==='open'){
1390 const phone = b.getAttribute('data-phone');
1391 try { await openChatByPhone(phone); alert('Opened'); } catch(err){ alert('Failed: '+err.message); }
1392 } else if (act==='use'){
1393 const phone = b.getAttribute('data-phone');
1394 panel.querySelector('[data-tab="schedule"]').click();
1395 panel.querySelector('#sch-name').value=''; panel.querySelector('#sch-phone').value=phone;
1396 } else if (act==='del'){
1397 const label = b.getAttribute('data-label');
1398 saveBook(loadBook().filter(x=>x.label!==label)); renderBook();
1399 }
1400 };
1401 }
1402
1403 function prefillDate(){
1404 const dt = panel.querySelector('#sch-when');
1405 if (!dt.value){
1406 const t = new Date(Date.now() + 10*60*1000);
1407 const year = t.getFullYear();
1408 const month = pad(t.getMonth() + 1);
1409 const day = pad(t.getDate());
1410 const hours = pad(t.getHours());
1411 const minutes = pad(t.getMinutes());
1412 dt.value = `${year}-${month}-${day}T${hours}:${minutes}`;
1413 }
1414 }
1415
1416 return { refreshJobs, renderBook, prefillDate };
1417 }
1418
1419 async function performJob(job){
1420 const { name, phone } = job.target || {};
1421
1422 if (phone){
1423 log('Saving pending message before navigation');
1424 save(LS.PENDING, { text: job.text, jobId: job.id, timestamp: Date.now() });
1425 await openChatByPhone(phone, '');
1426 await delay(900);
1427 } else if (name){
1428 await openChatByName(name, { exact:false });
1429 await delay(700);
1430 } else {
1431 throw new Error('No target');
1432 }
1433
1434 await sendMessage(job.text, { retries:3 });
1435 }
1436
1437 async function tick(){
1438 const now = Date.now();
1439 const jobs = loadJobs();
1440 log('Tick running, checking jobs. Current time:', now, 'Jobs:', jobs.length);
1441 const due = jobs.filter(j=>j.when<=now);
1442 if (!due.length) return;
1443 log('Found', due.length, 'messages ready to send');
1444
1445 if (document.visibilityState === 'hidden') {
1446 log('Tab is hidden, bringing to front');
1447 window.focus();
1448 await delay(500);
1449 }
1450
1451 for (const j of due){
1452 const tgt = j.target.phone || j.target.name;
1453 try {
1454 log('Sending job:', j.id, 'to', j.target);
1455 await performJob(j);
1456 addToHistory('sent', tgt, j.text);
1457
1458 if (j.repeat) {
1459 const nextTime = getNextOccurrence(j.when, j.repeat, j.weeklyDays);
1460 if (nextTime) {
1461 const nextJob = {
1462 id: uid(),
1463 when: nextTime,
1464 text: j.text,
1465 target: j.target,
1466 repeat: j.repeat,
1467 weeklyDays: j.weeklyDays
1468 };
1469 const currentJobs = loadJobs().filter(x => x.id !== j.id);
1470 currentJobs.push(nextJob);
1471 saveJobs(currentJobs);
1472 log('Created next occurrence for recurring job:', nextJob.id, 'at', fmt(nextTime));
1473 }
1474 } else {
1475 saveJobs(loadJobs().filter(x=>x.id!==j.id));
1476 }
1477
1478 log('Job sent successfully:', j.id);
1479 await delay(900);
1480 } catch (e) {
1481 log('Job failed', j, e);
1482 addToHistory('failed', tgt, j.text, e.message);
1483 }
1484 }
1485 }
1486
1487 function startLoop(){ setInterval(tick, 5000); }
1488
1489 function updateCountdowns(){
1490 const jobs = loadJobs();
1491 jobs.forEach(job => {
1492 const el = document.querySelector(`.countdown-${job.id}`);
1493 if (el) el.textContent = timeUntil(job.when);
1494 });
1495 }
1496
1497 let wakeLock;
1498 async function requestWakeLock(){
1499 try { if ('wakeLock' in navigator) wakeLock = await navigator.wakeLock.request('screen'); }
1500 catch (e) { log('WakeLock error', e); }
1501 }
1502 function releaseWakeLock(){ try { wakeLock && wakeLock.release(); wakeLock = null; } catch {} }
1503
1504 async function init(){
1505 log('Initializing WhatsApp Pro Scheduler v3.3');
1506 injectStyles();
1507 const panelAPI = createPanel();
1508 try { await waitFor('#app', { timeout:60000 }); } catch {}
1509
1510 const fabBtn = document.querySelector('.waw-fab');
1511 const panelEl = document.querySelector('.waw-panel');
1512 const origFabClick = fabBtn.onclick;
1513 fabBtn.onclick = async () => {
1514 origFabClick();
1515 if (panelEl.style.display !== 'none') { await requestWakeLock(); }
1516 else { releaseWakeLock(); }
1517 };
1518 window.addEventListener('visibilitychange', ()=> {
1519 if (document.visibilityState === 'visible' && panelEl.style.display !== 'none') requestWakeLock();
1520 });
1521
1522 const pending = load(LS.PENDING, null);
1523 if (pending && pending.text && (Date.now() - pending.timestamp < 120000)) {
1524 log('Found pending message, sending:', pending.text);
1525 try {
1526 await waitFor(selectors.composer.join(','), { timeout:30000 });
1527 await delay(1000);
1528 await sendMessage(pending.text, { retries:3 });
1529 log('Pending message sent successfully');
1530 localStorage.removeItem(LS.PENDING);
1531 if (pending.jobId) {
1532 const jobs = loadJobs().filter(x => x.id !== pending.jobId);
1533 saveJobs(jobs);
1534 }
1535 const chatTitle = getActiveChatTitle();
1536 const target = chatTitle || pending.target || 'Unknown';
1537 addToHistory('sent', target, pending.text);
1538 log('Added to history:', target);
1539 } catch (e) {
1540 log('Failed to send pending message:', e);
1541 const chatTitle = getActiveChatTitle();
1542 const target = chatTitle || pending.target || 'Unknown';
1543 addToHistory('failed', target, pending.text, e.message);
1544 }
1545 }
1546
1547 startLoop();
1548 setInterval(updateCountdowns, 1000);
1549 const prefs = loadPrefs();
1550 if (prefs.panelOpen){
1551 document.querySelector('.waw-fab').click();
1552 }
1553 }
1554
1555 if (document.readyState==='loading') document.addEventListener('DOMContentLoaded', init);
1556 else init();
1557
1558})();