• דף הבית
    • אינדקס קישורים
    • פוסטים אחרונים
    • משתמשים
    • חיפוש בהגדרות המתקדמות
    • חיפוש גוגל בפורום
    • ניהול המערכת
    • ניהול המערכת - שרת private
    • הרשמה
    • התחברות
    1. דף הבית
    2. ישראל10
    3. פוסטים
    י מנותק
    • פרופיל
    • עוקב אחרי 0
    • עוקבים 2
    • נושאים 45
    • פוסטים 252
    • קבוצות 0

    פוסטים

    פוסטים אחרונים הגבוה ביותר שנוי במחלוקת
    • RE: ממשק HTML לבדיקת כמות מאזינים בו זמנית (ערוצים)

      @מסובך-מאוד אני מעלה את זה וזה לא עולה.
      מעלה את הקוד, מי שיודע איך להעלות את זה בקובץ, מוזמן.

      <!doctype html>
      <html lang="he" dir="rtl">
      <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width,initial-scale=1">
      <title>ספר מספרים - ימות המשיח</title>
      <style>
      :root{--bg:#f3f5f4;--surface:#fff;--ink:#14201c;--muted:#5d6b66;--line:#d9e0dd;--accent:#0f7a5c;--accent-soft:#e1f2ec;--warn:#b45309;--bad:#b42318;color-scheme:light}
      @media (prefers-color-scheme:dark){:root{--bg:#0e1513;--surface:#16201d;--ink:#e6eeeb;--muted:#93a39d;--line:#2a3733;--accent:#3fc596;--accent-soft:#183a2f;--warn:#f0a454;--bad:#f2786d;color-scheme:dark}}
      *{box-sizing:border-box}[hidden]{display:none!important}
      body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 "Assistant",system-ui,-apple-system,"Segoe UI",Arial,sans-serif;padding:28px 16px 60px}
      .wrap{max-width:980px;margin-inline:auto;display:grid;gap:18px}
      h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem)}h2{margin:0;font-size:1.15rem}
      .lede,.muted{color:var(--muted)}.small{font-size:.86rem}
      .card{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:20px;display:grid;gap:14px;min-width:0}
      .row{display:flex;flex-wrap:wrap;gap:12px;align-items:end}
      .field{display:grid;gap:4px;flex:1 1 180px;min-width:0}
      label{font-size:.85rem;font-weight:600;color:var(--muted)}
      input,select{font:inherit;color:var(--ink);background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:9px 11px;width:100%}
      input.ltr{direction:ltr;text-align:left}
      button{font:inherit;font-weight:700;border-radius:9px;padding:9px 16px;cursor:pointer;border:1px solid var(--accent);background:var(--accent);color:var(--surface)}
      button.ghost{background:transparent;color:var(--accent)}button:disabled{opacity:.5;cursor:default}
      .seg{display:inline-flex;border:1px solid var(--line);border-radius:10px;padding:3px;gap:3px;background:var(--bg)}
      .seg button{border:0;background:transparent;color:var(--muted);padding:6px 14px;border-radius:7px}
      .seg button[aria-pressed="true"]{background:var(--surface);color:var(--ink)}
      .status{font-size:.92rem;padding:9px 12px;border-radius:9px;background:var(--accent-soft)}
      .status.err{border-inline-start:3px solid var(--bad)}.status.warn{border-inline-start:3px solid var(--warn)}
      .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
      .kpi{border:1px solid var(--line);border-radius:12px;padding:12px 14px;display:grid;gap:2px}
      .kpi .v{font:700 1.8rem/1.1 ui-monospace,Consolas,monospace;direction:ltr;text-align:start}
      .kpi .k{font-size:.85rem;color:var(--muted);font-weight:600}
      .scroll{overflow-x:auto}
      table{border-collapse:collapse;width:100%;font-size:.9rem}
      th,td{text-align:start;padding:6px 10px;border-bottom:1px solid var(--line);white-space:nowrap}
      th{font-size:.8rem;color:var(--muted)}
      td.num{font-family:ui-monospace,Consolas,monospace;direction:ltr;text-align:end}
      progress{width:100%;height:10px}
      </style>
      </head>
      <body>
      <div class="wrap">
      <header>
        <h1>ספר מספרים</h1>
        <p class="lede">שולף את כל לוגי הכניסות של המערכת, מאחד לפי מספר טלפון, ומייצא לאקסל: מי התקשר, מאיזה אזור, באילו שעות, ומאז מתי.</p>
      </header>
      
      <section class="card">
        <div class="row" style="justify-content:space-between">
          <h2>חיבור למערכת</h2>
          <div class="seg" role="group" aria-label="שיטת התחברות">
            <button type="button" id="m-pass" aria-pressed="true">מספר מערכת + סיסמה</button>
            <button type="button" id="m-token" aria-pressed="false">טוקן / API KEY</button>
          </div>
        </div>
        <form id="conn" class="row" autocomplete="off">
          <div class="field" id="f-num"><label for="num">מספר מערכת</label><input id="num" class="ltr" inputmode="numeric" placeholder="0771234567"></div>
          <div class="field" id="f-pass"><label for="pass">סיסמת ניהול</label><input id="pass" class="ltr" type="password"></div>
          <div class="field" id="f-token" hidden><label for="token">טוקן או מפתח API</label><input id="token" class="ltr" type="password"></div>
          <button type="submit" id="btn-conn">התחבר</button>
        </form>
        <div id="conn-status" class="status" hidden></div>
        <p class="small muted" style="margin:0">הפרטים נשלחים ישירות מהדפדפן לשרת של ימות המשיח (call2all.co.il) ולא נשמרים בשום מקום.</p>
      </section>
      
      <section class="card">
        <h2>שליפה</h2>
        <div class="row">
          <div class="field"><label for="from">מחודש (ריק = הכי ישן שיש)</label><input id="from" type="month"></div>
          <div class="field"><label for="to">עד חודש (ריק = הכי חדש)</label><input id="to" type="month"></div>
          <button type="button" id="btn-pull" disabled>משוך את כל ההיסטוריה</button>
        </div>
        <progress id="prog" value="0" max="1" hidden></progress>
        <div id="pull-status" class="status" hidden></div>
      
        <details>
          <summary style="cursor:pointer;font-weight:700">אופציונלי: קובץ התאמה של טלפון לעיר ולשם</summary>
          <p class="small muted">קובץ CSV עם כותרות: <b>טלפון</b>, <b>עיר</b>, ואפשר גם <b>שם</b> (או phone, city, name). המספרים שנמצאים בו יקבלו עיר ושם בעמודות נפרדות באקסל. בלי קובץ כזה העמודה "אזור" מבוססת על הקידומת בלבד.</p>
          <input id="mapfile" type="file" accept=".csv,.txt">
          <div id="map-status" class="small muted"></div>
        </details>
        <details>
          <summary style="cursor:pointer;font-weight:700">אין גישה דרך ה-API? העלה קבצי לוג ידנית</summary>
          <p class="small muted">אפשר לבחור כמה קבצי LogFolderEnterExit (CSV או ymgr) יחד. שמות הקבצים צריכים להכיל את החודש כדי שהסדר יהיה נכון.</p>
          <input id="logfiles" type="file" accept=".csv,.ymgr,.txt" multiple>
        </details>
      </section>
      
      <section class="card" id="res" hidden>
        <div class="row" style="justify-content:space-between">
          <h2>תוצאה</h2>
          <div class="row" style="gap:8px">
            <button type="button" id="btn-xlsx">הורד Excel</button>
            <button type="button" class="ghost" id="btn-csv">הורד CSV</button>
          </div>
        </div>
        <div id="xlsx-warn" class="status warn" hidden></div>
        <div class="kpis" id="kpis"></div>
        <div class="scroll"><table><thead><tr><th>טלפון</th><th>אזור</th><th>עיר</th><th>ראשון</th><th>אחרון</th><th>ותק (ימים)</th><th>שיחות</th><th>חלק יום עיקרי</th><th>סטטוס</th></tr></thead><tbody id="prev"></tbody></table></div>
        <p class="small muted" style="margin:0">מוצגים 100 המספרים עם הכי הרבה שיחות. הקובץ המלא כולל את כולם.</p>
      </section>
      </div>
      
      <script src="https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.4.0/exceljs.min.js" async></script>
      <script>
      /* ===== ליבה: פענוח לוג, צבירה לפי מספר, בניית חוברת אקסל ===== */
      const T = (() => {
        const ANON = new Set(['', '0', 'unknown', 'anonymous', 'חסוי', 'private', 'restricted', 'blocked']);
        const isAnon = p => ANON.has(String(p || '').trim().toLowerCase());
        const DAY = 86400000;
      
        function parseYmgrLine(line) {
          const o = {};
          for (const part of line.split('%')) { const i = part.indexOf('#'); if (i > 0) o[part.slice(0, i).trim()] = part.slice(i + 1).trim(); }
          return o;
        }
        function parseCSV(text) {
          const rows = []; let row = [], cur = '', q = false;
          for (let i = 0; i < text.length; i++) {
            const c = text[i];
            if (q) { if (c === '"') { if (text[i + 1] === '"') { cur += '"'; i++; } else q = false; } else cur += c; }
            else if (c === '"') q = true;
            else if (c === ',') { row.push(cur); cur = ''; }
            else if (c === '\n' || c === '\r') { if (c === '\r' && text[i + 1] === '\n') i++; row.push(cur); cur = ''; if (row.some(x => x.trim() !== '')) rows.push(row); row = []; }
            else cur += c;
          }
          row.push(cur); if (row.some(x => x.trim() !== '')) rows.push(row);
          return rows;
        }
        const HEAD_MAP = [
          ['Phone', /^(phone|טלפון|מספר טלפון|מספר הטלפון|טלפון המתקשר|מספר טלפון של המתקשר)$/i],
          ['Folder', /^(folder|תיקיה|תיקייה|שלוחה|תיקיה - שלוחה במערכת)$/i],
          ['EnterDate', /^(enterdate|תאריך|תאריך לועזי|תאריך כניסה)$/i],
          ['EnterTime', /^(entertime|שעת כניסה|שעת הכניסה|שעה)$/i],
          ['ExitTime', /^(exittime|שעת יציאה|שעת היציאה)$/i],
          ['TimeTotal', /^(timetotal|סך הכל זמן האזנה|סה"כ זמן האזנה|זמן האזנה|משך)$/i],
        ];
        function parseLogText(text) {
          text = String(text).replace(/^\uFEFF/, '');
          const lines = text.split(/\r?\n/).filter(l => l.trim());
          if (!lines.length) return [];
          const ymgrLike = lines.slice(0, 20).filter(l => /[A-Za-z]+#/.test(l) && l.includes('%')).length;
          if (ymgrLike >= Math.min(3, lines.length)) return lines.map(parseYmgrLine);
          const rows = parseCSV(text);
          if (rows.length < 2) return [];
          const head = rows[0].map(h => h.trim()), idx = {};
          head.forEach((h, i) => { for (const [k, re] of HEAD_MAP) if (idx[k] === undefined && re.test(h)) idx[k] = i; });
          return rows.slice(1).map(r => { const o = {}; for (const k in idx) o[k] = (r[idx[k]] || '').trim(); return o; });
        }
        function parseDate(s) {
          if (!s) return null; s = String(s).trim();
          let y, m, d, mm;
          if ((mm = s.match(/^(\d{4})[-\/.](\d{1,2})[-\/.](\d{1,2})/))) { y = +mm[1]; m = +mm[2]; d = +mm[3]; }
          else if ((mm = s.match(/^(\d{1,2})[-\/.](\d{1,2})[-\/.](\d{2,4})/))) { d = +mm[1]; m = +mm[2]; y = +mm[3]; if (y < 100) y += 2000; }
          else return null;
          if (m < 1 || m > 12 || d < 1 || d > 31) return null;
          return [y, m, d];
        }
        function parseTime(s) {
          if (!s) return null; s = String(s).trim();
          if (s.includes(' ')) s = s.slice(s.lastIndexOf(' ') + 1);
          const m = s.match(/^(\d{1,2})[:\-.](\d{2})(?:[:\-.](\d{2}))?$/);
          if (!m) return null;
          const h = +m[1], mi = +m[2], se = +(m[3] || 0);
          if (h > 23 || mi > 59 || se > 59) return null;
          return h * 3600 + mi * 60 + se;
        }
        function parseDur(s) {
          if (s === undefined || s === null) return null; s = String(s).trim(); if (s === '') return null;
          if (/^\d+(\.\d+)?$/.test(s)) return Math.round(+s);
          const m = s.match(/^(\d+):(\d{2})(?::(\d{2}))?$/);
          if (m) return m[3] !== undefined ? (+m[1]) * 3600 + (+m[2]) * 60 + (+m[3]) : (+m[1]) * 60 + (+m[2]);
          return null;
        }
        const at = (d, sec) => new Date(d[0], d[1] - 1, d[2], Math.floor(sec / 3600), Math.floor(sec / 60) % 60, sec % 60).getTime();
      
        /* טלפון -> צורה אחידה: 0501234567 / 021234567 / +<מספר חו"ל> / '' אם חסוי */
        function normPhone(raw) {
          if (isAnon(raw)) return '';
          let d = String(raw).replace(/\D/g, '');
          if (!d) return '';
          if (d.startsWith('00972')) d = '0' + d.slice(5);
          else if (d.startsWith('972') && d.length >= 11) d = '0' + d.slice(3);
          else if (d.startsWith('00')) return '+' + d.slice(2);
          else if (d.length === 9 && d[0] === '5') d = '0' + d;
          return d;
        }
        const LAND = { '02': 'ירושלים והסביבה', '03': 'גוש דן / מרכז', '04': 'צפון וחיפה', '08': 'שפלה ודרום', '09': 'השרון' };
        function phoneInfo(p) {
          if (p[0] === '+') return { region: 'חו"ל', type: 'חו"ל' };
          if (/^05\d{8}$/.test(p)) return { region: 'נייד (אין אזור)', type: 'נייד' };
          if (/^0[23489]\d{7}$/.test(p)) return { region: LAND[p.slice(0, 2)], type: 'קווי' };
          if (/^07\d{8}$/.test(p)) return { region: 'קו ארצי / VoIP (07x)', type: 'קווי ארצי' };
          return { region: 'לא מזוהה', type: 'לא מזוהה' };
        }
      
        function toIntervals(records) {
          const out = [], diag = { rows: records.length, ok: 0, bad: 0 };
          for (const r of records) {
            const d = parseDate(r.EnterDate), t0 = parseTime(r.EnterTime);
            if (!d || t0 === null) { diag.bad++; continue; }
            const start = at(d, t0), t1 = parseTime(r.ExitTime), dur = parseDur(r.TimeTotal);
            let end = null;
            if (t1 !== null && t1 >= t0) end = at(d, t1);
            else if (t1 !== null) {
              if (dur !== null) end = start + dur * 1000;
              else if (t0 - t1 >= 12 * 3600) end = at(d, t1) + DAY;
              else end = start;
            } else if (dur !== null) end = start + dur * 1000;
            if (end === null) { diag.bad++; continue; }
            end = Math.max(end, start + 1000);
            out.push({ phone: normPhone(r.Phone), folder: String(r.Folder || '').trim(), start, end }); diag.ok++;
          }
          return { intervals: out, diag };
        }
        function mergeCalls(intervals, gapMs = 3000) {
          const byPhone = new Map(), calls = [];
          for (const iv of intervals) {
            if (isAnon(iv.phone)) { calls.push({ ...iv, folders: [iv.folder] }); continue; }
            if (!byPhone.has(iv.phone)) byPhone.set(iv.phone, []);
            byPhone.get(iv.phone).push(iv);
          }
          for (const [, list] of byPhone) {
            list.sort((a, b) => a.start - b.start);
            let cur = { ...list[0], folders: [list[0].folder] };
            for (let i = 1; i < list.length; i++) {
              const n = list[i];
              if (n.start <= cur.end + gapMs) { cur.end = Math.max(cur.end, n.end); if (!cur.folders.includes(n.folder)) cur.folders.push(n.folder); }
              else { calls.push(cur); cur = { ...n, folders: [n.folder] }; }
            }
            calls.push(cur);
          }
          return calls;
        }
      
        /* ----- צבירה ----- */
        const BUCKETS = ['לילה (00-06)', 'בוקר (06-12)', 'צהריים ואחה"צ (12-18)', 'ערב (18-24)'];
        const bucketOf = h => h < 6 ? 0 : h < 12 ? 1 : h < 18 ? 2 : 3;
        const mkey = t => { const d = new Date(t); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); };
        const dnum = t => { const d = new Date(t); return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY); };
      
        function newAgg() {
          return { phones: new Map(), months: new Map(), heat: Array.from({ length: 7 }, () => new Array(24).fill(0)), anonCalls: 0, rows: 0, bad: 0, files: 0 };
        }
        function addRecords(agg, records) {
          const { intervals, diag } = toIntervals(records);
          agg.rows += diag.rows; agg.bad += diag.bad; agg.files++;
          for (const c of mergeCalls(intervals)) {
            const dt = new Date(c.start), h = dt.getHours();
            const mk = mkey(c.start);
            let m = agg.months.get(mk); if (!m) { m = { calls: 0, phones: new Set() }; agg.months.set(mk, m); }
            m.calls++;
            agg.heat[dt.getDay()][h]++;
            if (isAnon(c.phone)) { agg.anonCalls++; continue; }
            m.phones.add(c.phone);
            let r = agg.phones.get(c.phone);
            if (!r) { r = { phone: c.phone, first: c.start, last: c.start, calls: 0, totalMs: 0, b: [0, 0, 0, 0], hours: new Uint32Array(24), days: new Set(), folders: new Map() }; agg.phones.set(c.phone, r); }
            if (c.start < r.first) r.first = c.start;
            if (c.start > r.last) r.last = c.start;
            r.calls++; r.totalMs += c.end - c.start; r.b[bucketOf(h)]++; r.hours[h]++; r.days.add(dnum(c.start));
            for (const f of c.folders) r.folders.set(f, (r.folders.get(f) || 0) + 1);
          }
        }
        const argmax = a => { let bi = 0; for (let i = 1; i < a.length; i++) if (a[i] > a[bi]) bi = i; return bi; };
      
        function finalize(agg, nowMs, cityMap) {
          cityMap = cityMap || new Map();
          const rows = [];
          for (const r of agg.phones.values()) {
            const info = phoneInfo(r.phone), cm = cityMap.get(r.phone) || {};
            const sinceLast = Math.max(0, Math.floor((nowMs - r.last) / DAY));
            let mainF = '', mx = 0; for (const [f, n] of r.folders) if (n > mx) { mx = n; mainF = f; }
            rows.push({
              phone: r.phone, region: info.region, type: info.type, city: cm.city || '', name: cm.name || '',
              first: r.first, last: r.last,
              tenureDays: Math.max(0, Math.floor((nowMs - r.first) / DAY)), sinceLast,
              spanDays: Math.floor((r.last - r.first) / DAY),
              activeDays: r.days.size, calls: r.calls, totalHours: r.totalMs / 3600000, avgMin: r.totalMs / 60000 / r.calls,
              b: r.b, mainBucket: BUCKETS[argmax(r.b)], peakHour: argmax(r.hours),
              status: sinceLast <= 30 ? 'פעיל (עד 30 יום)' : sinceLast <= 90 ? 'מתקרר (30-90 יום)' : 'רדום (מעל 90 יום)',
              folder: mainF
            });
          }
          rows.sort((a, b) => b.calls - a.calls);
          const firstByMonth = new Map();
          for (const r of rows) { const k = mkey(r.first); firstByMonth.set(k, (firstByMonth.get(k) || 0) + 1); }
          const months = [...new Set([...agg.months.keys(), ...firstByMonth.keys()])].sort().map(k => ({
            month: k, calls: agg.months.get(k) ? agg.months.get(k).calls : 0, active: agg.months.get(k) ? agg.months.get(k).phones.size : 0, fresh: firstByMonth.get(k) || 0
          }));
          const regions = new Map();
          for (const r of rows) {
            let g = regions.get(r.region); if (!g) { g = { region: r.region, numbers: 0, calls: 0, hours: 0, tenure: 0, active: 0 }; regions.set(r.region, g); }
            g.numbers++; g.calls += r.calls; g.hours += r.totalHours; g.tenure += r.tenureDays; if (r.sinceLast <= 30) g.active++;
          }
          const regionList = [...regions.values()].sort((a, b) => b.numbers - a.numbers);
          const bucketList = BUCKETS.map((name, i) => ({ name, calls: rows.reduce((s, r) => s + r.b[i], 0), mainFor: rows.filter(r => r.mainBucket === name).length }));
          return {
            rows, months, regions: regionList, buckets: bucketList, heat: agg.heat,
            summary: {
              numbers: rows.length, calls: rows.reduce((s, r) => s + r.calls, 0), anonCalls: agg.anonCalls, logRows: agg.rows, badRows: agg.bad, files: agg.files,
              first: rows.length ? Math.min(...rows.map(r => r.first)) : null, last: rows.length ? Math.max(...rows.map(r => r.last)) : null,
              active30: rows.filter(r => r.sinceLast <= 30).length, mapped: rows.filter(r => r.city).length,
              onceOnly: rows.filter(r => r.calls === 1).length, now: nowMs
            }
          };
        }
      
        function parseCityMap(text) {
          const rows = parseCSV(String(text).replace(/^\uFEFF/, ''));
          if (rows.length < 2) return new Map();
          const head = rows[0].map(h => h.trim().toLowerCase());
          const ip = head.findIndex(h => /^(phone|טלפון|מספר|מספר טלפון)$/.test(h));
          const ic = head.findIndex(h => /^(city|עיר|יישוב|ישוב)$/.test(h));
          const inm = head.findIndex(h => /^(name|שם)$/.test(h));
          const m = new Map();
          if (ip < 0) return m;
          for (const r of rows.slice(1)) { const p = normPhone(r[ip]); if (p) m.set(p, { city: ic >= 0 ? (r[ic] || '').trim() : '', name: inm >= 0 ? (r[inm] || '').trim() : '' }); }
          return m;
        }
      
        /* ----- ייצוא ----- */
        const xd = ms => new Date(ms - new Date(ms).getTimezoneOffset() * 60000); // ExcelJS כותב UTC, אז מזיזים כדי שיוצג שעון מקומי
        const DOW = ['ראשון', 'שני', 'שלישי', 'רביעי', 'חמישי', 'שישי', 'שבת'];
        const MAIN_COLS = [
          ['טלפון', 16, '@'], ['אזור (לפי קידומת)', 20], ['סוג קו', 11], ['עיר (מקובץ התאמה)', 18], ['שם (מקובץ התאמה)', 18],
          ['הופעה ראשונה', 17, 'dd/mm/yyyy hh:mm'], ['הופעה אחרונה', 17, 'dd/mm/yyyy hh:mm'],
          ['ותק (ימים מההופעה הראשונה)', 15, '#,##0'], ['ימים מאז האחרונה', 12, '#,##0'], ['ימים בין ראשונה לאחרונה', 13, '#,##0'], ['ימים פעילים', 10, '#,##0'], ['סטטוס', 18],
          ['שיחות', 9, '#,##0'], ['סה"כ האזנה (שעות)', 12, '#,##0.0'], ['ממוצע לשיחה (דקות)', 12, '#,##0.0'],
          ['שיחות בלילה (00-06)', 11, '#,##0'], ['שיחות בבוקר (06-12)', 11, '#,##0'], ['שיחות בצהריים (12-18)', 11, '#,##0'], ['שיחות בערב (18-24)', 11, '#,##0'],
          ['חלק יום עיקרי', 20], ['שעה נפוצה', 9, '0'], ['שלוחה עיקרית', 14, '@']
        ];
        const mainRow = r => [r.phone, r.region, r.type, r.city, r.name, xd(r.first), xd(r.last), r.tenureDays, r.sinceLast, r.spanDays, r.activeDays, r.status,
          r.calls, +r.totalHours.toFixed(2), +r.avgMin.toFixed(2), r.b[0], r.b[1], r.b[2], r.b[3], r.mainBucket, r.peakHour, r.folder];
      
        function buildWorkbook(ExcelJS, res) {
          const wb = new ExcelJS.Workbook(); wb.creator = 'ספר מספרים'; wb.created = new Date();
          const hdr = ws => { const r = ws.getRow(1); r.font = { bold: true, color: { argb: 'FFFFFFFF' } }; r.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF0F7A5C' } }; r.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }; r.height = 34; };
          const s = res.summary, fd = ms => ms ? xd(ms) : '';
      
          const w1 = wb.addWorksheet('סיכום', { views: [{ rightToLeft: true }] });
          w1.columns = [{ width: 38 }, { width: 24 }];
          const kv = [
            ['נוצר בתאריך', xd(s.now)], ['מספרי טלפון שונים', s.numbers], ['שיחות (אחרי איחוד מעברי שלוחה)', s.calls], ['שיחות ממספר חסוי (לא בטבלת המספרים)', s.anonCalls],
            ['מספרים שהתקשרו פעם אחת בלבד', s.onceOnly], ['מספרים פעילים ב-30 הימים האחרונים', s.active30], ['מספרים עם עיר מקובץ ההתאמה', s.mapped],
            ['ההופעה הראשונה בלוגים', fd(s.first)], ['ההופעה האחרונה בלוגים', fd(s.last)], ['קבצי לוג שנקראו', s.files], ['שורות לוג', s.logRows], ['שורות שלא נקראו', s.badRows]
          ];
          kv.forEach(([k, v]) => { const r = w1.addRow([k, v]); r.getCell(1).font = { bold: true }; if (v instanceof Date) r.getCell(2).numFmt = 'dd/mm/yyyy hh:mm'; else if (typeof v === 'number') r.getCell(2).numFmt = '#,##0'; r.getCell(2).alignment = { horizontal: 'right' }; });
          w1.addRow([]);
          const note = w1.addRow(['הערות']); note.font = { bold: true };
          ['* "ותק" = כמה זמן עבר מההופעה הראשונה של המספר בלוגים ועד יום יצירת הקובץ. לא יודעים מתי נרשם בפועל, רק מתי נראה לראשונה בלוג שנשמר במערכת.',
           '* "אזור" נקבע לפי הקידומת בלבד. מספר נייד לא נושא מידע על עיר, ומספר קווי נושא רק אזור כללי.',
           '* חלק היום נקבע לפי שעת תחילת השיחה.'].forEach(t => { const r = w1.addRow([t]); r.alignment = { wrapText: true }; w1.mergeCells(r.number, 1, r.number, 2); r.height = 44; });
      
          const w2 = wb.addWorksheet('מספרים', { views: [{ rightToLeft: true, state: 'frozen', xSplit: 1, ySplit: 1 }] });
          w2.columns = MAIN_COLS.map(c => ({ header: c[0], width: c[1], style: c[2] ? { numFmt: c[2] } : {} }));
          hdr(w2);
          w2.addRows(res.rows.map(mainRow));
          w2.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: MAIN_COLS.length } };
      
          const w3 = wb.addWorksheet('לפי אזור', { views: [{ rightToLeft: true, state: 'frozen', ySplit: 1 }] });
          w3.columns = [['אזור', 26], ['מספרים', 12], ['שיחות', 12], ['שעות האזנה', 14], ['ותק ממוצע (ימים)', 16], ['פעילים ב-30 יום', 14], ['% מכלל המספרים', 14]].map(c => ({ header: c[0], width: c[1] }));
          hdr(w3);
          res.regions.forEach(g => w3.addRow([g.region, g.numbers, g.calls, +g.hours.toFixed(1), Math.round(g.tenure / g.numbers), g.active, g.numbers / Math.max(1, s.numbers)]));
          w3.getColumn(7).numFmt = '0.0%';
      
          const w4 = wb.addWorksheet('שעות ביום', { views: [{ rightToLeft: true, state: 'frozen', xSplit: 1, ySplit: 1 }] });
          w4.addRow(['יום \\ שעה', ...Array.from({ length: 24 }, (_, h) => String(h).padStart(2, '0'))]); hdr(w4);
          w4.getColumn(1).width = 12; for (let c = 2; c <= 25; c++) w4.getColumn(c).width = 7;
          res.heat.forEach((row, d) => w4.addRow([DOW[d], ...row]));
          w4.addConditionalFormatting({ ref: 'B2:Y8', rules: [{ type: 'colorScale', cfvo: [{ type: 'min' }, { type: 'max' }], color: [{ argb: 'FFFFFFFF' }, { argb: 'FF0F7A5C' }] }] });
          w4.addRow([]);
          const hh = w4.addRow(['חלק יום', 'שיחות', 'מספרים שזה החלק העיקרי שלהם']); hh.font = { bold: true };
          res.buckets.forEach(b => w4.addRow([b.name, b.calls, b.mainFor]));
      
          const w5 = wb.addWorksheet('לפי חודש', { views: [{ rightToLeft: true, state: 'frozen', ySplit: 1 }] });
          w5.columns = [['חודש', 12], ['שיחות', 12], ['מספרים פעילים', 14], ['מספרים חדשים (הופיעו לראשונה)', 22]].map(c => ({ header: c[0], width: c[1] }));
          hdr(w5);
          res.months.forEach(m => w5.addRow([m.month, m.calls, m.active, m.fresh]));
          return wb;
        }
      
        function toCSV(res) {
          const esc = v => { v = v instanceof Date ? v.toISOString().slice(0, 16).replace('T', ' ') : String(v); return /[",\n]/.test(v) ? '"' + v.replace(/"/g, '""') + '"' : v; };
          const lines = [MAIN_COLS.map(c => esc(c[0])).join(',')];
          for (const r of res.rows) { const row = mainRow(r); row[0] = '\t' + row[0]; lines.push(row.map(esc).join(',')); }
          return '\uFEFF' + lines.join('\r\n');
        }
      
        return { isAnon, normPhone, phoneInfo, parseLogText, parseCityMap, newAgg, addRecords, finalize, buildWorkbook, toCSV, mkey, BUCKETS };
      })();
      if (typeof module !== 'undefined') module.exports = T;
      </script>
      
      <script>
      (() => {
      if (typeof document === 'undefined') return;
      const $ = id => document.getElementById(id);
      const BASE = 'https://www.call2all.co.il/ym/api/';
      let mode = 'pass', token = null, result = null, cityMap = new Map();
      const pad = n => String(n).padStart(2, '0');
      const fmtNum = n => Number(n).toLocaleString('he-IL');
      const fmtD = t => { const d = new Date(t); return pad(d.getDate()) + '/' + pad(d.getMonth() + 1) + '/' + d.getFullYear(); };
      function show(el, msg, kind) { el.hidden = !msg; el.className = 'status' + (kind ? ' ' + kind : ''); el.textContent = msg || ''; }
      const tick = () => new Promise(r => setTimeout(r, 0));
      
      function netError(e) { return e instanceof TypeError ? new Error('הדפדפן לא הצליח לפנות לשרת של ימות המשיח (אין אינטרנט או שהבקשה נחסמה). אפשר להעלות קבצי לוג ידנית.') : e; }
      async function api(method, params = {}) {
        let res; try { res = await fetch(BASE + method + '?' + new URLSearchParams({ token, ...params })); } catch (e) { throw netError(e); }
        const txt = await res.text(); let j; try { j = JSON.parse(txt); } catch { throw new Error('תשובה לא צפויה מהשרת (' + res.status + ').'); }
        if (j.responseStatus && j.responseStatus !== 'OK') throw new Error(j.message || ('השרת החזיר: ' + j.responseStatus));
        return j;
      }
      function decode(buf) {
        try { return new TextDecoder('utf-8', { fatal: true }).decode(buf); }
        catch { try { return new TextDecoder('windows-1255').decode(buf); } catch { return new TextDecoder().decode(buf); } }
      }
      async function apiText(method, params = {}) {
        let res; try { res = await fetch(BASE + method + '?' + new URLSearchParams({ token, ...params })); } catch (e) { throw netError(e); }
        if (res.status === 404) throw new Error('הקובץ לא נמצא במערכת.');
        const txt = decode(await res.arrayBuffer());
        if (/^\s*\{/.test(txt)) { try { const j = JSON.parse(txt); if (j.responseStatus && j.responseStatus !== 'OK') throw new Error(j.message || j.responseStatus); } catch (e) { if (!(e instanceof SyntaxError)) throw e; } }
        return txt;
      }
      
      function setMode(m) { mode = m; $('m-pass').setAttribute('aria-pressed', m === 'pass'); $('m-token').setAttribute('aria-pressed', m === 'token'); $('f-num').hidden = $('f-pass').hidden = m !== 'pass'; $('f-token').hidden = m !== 'token'; }
      $('m-pass').onclick = () => setMode('pass'); $('m-token').onclick = () => setMode('token');
      
      $('conn').addEventListener('submit', async e => {
        e.preventDefault(); const st = $('conn-status');
        if (mode === 'pass') { const n = $('num').value.trim(), p = $('pass').value.trim(); if (!n || !p) return show(st, 'הקלד מספר מערכת וסיסמה.', 'err'); token = n + ':' + p; }
        else { const t = $('token').value.trim(); if (!t) return show(st, 'הדבק טוקן או מפתח API.', 'err'); token = t; }
        $('btn-conn').disabled = true; show(st, 'מתחבר…');
        try { const s = await api('GetSession'); show(st, 'מחובר' + (s.username ? ' למערכת ' + s.username : '') + (s.name ? ' · ' + s.name : '')); $('btn-pull').disabled = false; }
        catch (err) { token = null; $('btn-pull').disabled = true; show(st, 'ההתחברות נכשלה: ' + err.message, 'err'); }
        finally { $('btn-conn').disabled = false; }
      });
      
      $('mapfile').onchange = async e => {
        const f = e.target.files[0]; if (!f) return;
        cityMap = T.parseCityMap(decode(await f.arrayBuffer()));
        $('map-status').textContent = cityMap.size ? 'נטענו ' + fmtNum(cityMap.size) + ' מספרים מקובץ ההתאמה.' : 'לא זוהו שורות. ודא שיש עמודת "טלפון" ועמודת "עיר".';
        if (result) { /* ייושם בשליפה הבאה */ $('map-status').textContent += ' יחול בשליפה הבאה.'; }
      };
      
      function finish(agg) {
        result = T.finalize(agg, Date.now(), cityMap);
        const s = result.summary;
        const k = [['מספרים שונים', s.numbers], ['שיחות', s.calls], ['פעילים ב-30 יום', s.active30], ['התקשרו פעם אחת', s.onceOnly], ['שיחות חסויות', s.anonCalls]];
        $('kpis').innerHTML = ''; k.forEach(([a, b]) => { const d = document.createElement('div'); d.className = 'kpi'; d.innerHTML = '<span class="k"></span><span class="v"></span>'; d.firstChild.textContent = a; d.lastChild.textContent = fmtNum(b); $('kpis').appendChild(d); });
        const tb = $('prev'); tb.innerHTML = '';
        for (const r of result.rows.slice(0, 100)) {
          const tr = document.createElement('tr');
          [[r.phone, 'num'], [r.region], [r.city], [fmtD(r.first), 'num'], [fmtD(r.last), 'num'], [fmtNum(r.tenureDays), 'num'], [fmtNum(r.calls), 'num'], [r.mainBucket], [r.status]].forEach(([t, c]) => { const td = document.createElement('td'); td.textContent = t; if (c) td.className = c; tr.appendChild(td); });
          tb.appendChild(tr);
        }
        $('res').hidden = false;
      }
      
      $('btn-pull').onclick = async () => {
        const st = $('pull-status'), pg = $('prog'); $('btn-pull').disabled = true; pg.hidden = false; pg.value = 0;
        try {
          show(st, 'טוען את רשימת הקבצים בתיקיית Log…');
          const d = await api('GetIVR2Dir', { path: 'ivr2:/Log' });
          let months = (d.files || []).map(f => f.name || f.fileName || '').map(n => (n.match(/^LogFolderEnterExit-(\d{4}-\d{2})/) || [])[1]).filter(Boolean);
          months = [...new Set(months)].sort();
          const a = $('from').value, b = $('to').value;
          if (a) months = months.filter(m => m >= a); if (b) months = months.filter(m => m <= b);
          if (!months.length) throw new Error('לא נמצאו קבצי LogFolderEnterExit בתיקיית Log (בטווח שנבחר).');
          const agg = T.newAgg(), failed = [];
          pg.max = months.length;
          for (let i = 0; i < months.length; i++) {
            show(st, 'מוריד ומעבד ' + months[i] + ' (' + (i + 1) + ' מתוך ' + months.length + ') · עד עכשיו ' + fmtNum(agg.phones.size) + ' מספרים');
            try { T.addRecords(agg, T.parseLogText(await apiText('DownloadFile', { path: 'ivr2:/Log/LogFolderEnterExit-' + months[i] + '.ymgr' }))); }
            catch (err) { failed.push(months[i] + ' (' + err.message + ')'); }
            pg.value = i + 1; await tick();
          }
          if (!agg.phones.size) throw new Error('לא נקראו נתונים. ' + (failed.length ? 'כשלו: ' + failed.join('; ') : ''));
          finish(agg);
          show(st, 'הסתיים: ' + fmtNum(result.summary.numbers) + ' מספרים מתוך ' + months.length + ' חודשים.' + (failed.length ? ' לא נטענו: ' + failed.join('; ') : ''), failed.length ? 'warn' : '');
        } catch (err) { show(st, err.message, 'err'); }
        finally { $('btn-pull').disabled = !token; pg.hidden = true; }
      };
      
      $('logfiles').onchange = async e => {
        const st = $('pull-status'), files = [...e.target.files].sort((a, b) => a.name.localeCompare(b.name));
        if (!files.length) return;
        const agg = T.newAgg();
        for (const f of files) { show(st, 'מעבד ' + f.name + '…'); T.addRecords(agg, T.parseLogText(decode(await f.arrayBuffer()))); await tick(); }
        if (!agg.phones.size) return show(st, 'לא זוהו שורות לוג בקבצים.', 'err');
        finish(agg); show(st, 'הסתיים: ' + fmtNum(result.summary.numbers) + ' מספרים מ-' + files.length + ' קבצים.');
      };
      
      function save(blob, name) { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 4000); }
      const stamp = () => { const d = new Date(); return d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()); };
      $('btn-xlsx').onclick = async () => {
        if (!result) return;
        if (typeof ExcelJS === 'undefined') return show($('xlsx-warn'), 'ספריית האקסל לא נטענה (כנראה חסומה בסינון או שאין אינטרנט). אפשר להוריד CSV, שנפתח באקסל.', 'warn');
        $('btn-xlsx').disabled = true; show($('xlsx-warn'), 'בונה קובץ אקסל…');
        try { await tick(); const buf = await T.buildWorkbook(ExcelJS, result).xlsx.writeBuffer(); save(new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), 'phone-book-' + stamp() + '.xlsx'); show($('xlsx-warn'), ''); }
        catch (err) { show($('xlsx-warn'), 'בניית האקסל נכשלה: ' + err.message + '. נסה CSV.', 'err'); }
        finally { $('btn-xlsx').disabled = false; }
      };
      $('btn-csv').onclick = () => { if (result) save(new Blob([T.toCSV(result)], { type: 'text/csv;charset=utf-8' }), 'phone-book-' + stamp() + '.csv'); };
      })();
      </script>
      </body>
      </html>
      
      
      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10
    • RE: ממשק HTML לבדיקת כמות מאזינים בו זמנית (ערוצים)

      @מסובך-מאוד לא מצליח להעלות...

      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10
    • RE: ממשק HTML לבדיקת כמות מאזינים בו זמנית (ערוצים)

      @איל-משולש בניתי קובץ נחמד לקבלת נתונים עם אפשרות להוריד לאקסל.
      yemot-phone-book.html

      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10
    • RE: ממשק HTML לבדיקת כמות מאזינים בו זמנית (ערוצים)

      @איל-משולש שלך יותר טוב..

      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10
    • RE: דרמה!! מישהו שמע על זה????

      @אז-נדברו כתב:

      @מוטי-לוין נבחנת כעת אפשרות של תמחור של 0.5 יחידות על כל דקת שיחה מעבר למגבלת ערוצים, וכך מי שירצה יוכל לאפשר חיוג מעבר למכסה.

      אני חושב שזה רעיון מושלם!!!
      לפי נתונים של החודשים האחרונים, יספיק לי 20 ערוצים, אבל יש בערך 7 או 10 פעמים בחודש שיש בערך 25 ערוצים, בסך הכול בחודש האחרון היו 3 דקות שעקפתי את ה20 ערוצים, אני חושב שזה יספק פיתרון טוב ומושלם!!!

      פורסם בשאלות ועזרה הדדית
      י
      ישראל10
    • RE: ממשק HTML לבדיקת כמות מאזינים בו זמנית (ערוצים)

      @איל-משולש בסוף זה עבד...
      כמוך, רק שלך יותר תקין.
      1ceee909-fbdd-40dc-a452-48cf54ba4091-image.jpeg

      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10
    • RE: ממשק HTML לבדיקת כמות מאזינים בו זמנית (ערוצים)

      @איל-משולש אני בכלל לא בטוח ששלך לא עובד...
      אני בודק את זה..
      עריכה.
      יש מחלוקת עצובה ביניכם...
      שלך טוען שהגעתי ל108 ערוצים, בעוד שהוא טוען שהגעתי ל39 ערוצים בלבד...
      ואני נוטה להאמין לשלך, כי הוצאתי אז צינטוק ל2000 איש ביחד..., לכאורה חזרו מיד לפחות 100
      אבל אני רואה שהוא לא הצליח לשאוב את כל הזמנים...
      אני בודק רק את הזמנים המועדים לפורענות...

      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10
    • RE: ממשק HTML לבדיקת כמות מאזינים בו זמנית (ערוצים)

      @איל-משולש ואיפה אני רואה את זה

      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10
    • RE: ממשק HTML לבדיקת כמות מאזינים בו זמנית (ערוצים)

      @איל-משולש זה הגיוני שיש לי מעל 20 אלף דקות ואני לא עובר את ה12 חיבורים ביחד?
      עריכה, בחרתי בכל החודשים וזה הביא לי נתונים אחרים...
      19b6e6ae-15f1-4986-b3cc-494131aabcb2-image.jpeg

      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10
    • RE: דרמה!! מישהו שמע על זה????

      @אז-נדברו ובאופציה שהמערכת מחייגת ללקוח, [לא קמפיין קולי] זה נחשב כערוץ?

      פורסם בשאלות ועזרה הדדית
      י
      ישראל10
    • RE: דרמה!! מישהו שמע על זה????

      בשביל להבין.
      יש אופציה של לשלוח שיחה במקום צינטוק, האם זה יעזור למגבלה?
      אם יש לי מעל 20 אלף קולות ואני מקבל מספר 1700 רק מי שמתקשר למספר הזה הוא לא שומע הודעות ולא מוגבל וכל האחרים כן?

      פורסם בשאלות ועזרה הדדית
      י
      ישראל10
    • RE: מודול שליחת פקס לא עובד

      @הללוהו היה לי מערכת שעבדה כמה שנים!!!
      וביום בהיר הפסיקה לעבוד.
      ימות לא מגיבים, לא יודע מה לעשות!

      פורסם בעזרה הדדית למשתמשים מתקדמים
      י
      ישראל10
    • RE: פניות תח"צ בטלפון

      @שמואל-ש
      ואחרי?

      פורסם בעזרה הדדית למשתמשים מתקדמים
      י
      ישראל10
    • RE: פניות תח"צ בטלפון

      מה מתקדם?

      פורסם בעזרה הדדית למשתמשים מתקדמים
      י
      ישראל10
    • RE: קווי תוכן גדולים - שמות ומספרים

      @Omoran
      מערכת "שהזמן גרמא"
      המערכת שולחת תזכורות טלפוניות לפני זמני היום, [נץ החמה, ק"ש ראשון ושני, תפילה ראשון ושני, חצות היום והלילה, מנחה גדולה וקטנה, שקיעת החמה והדלקת נרות שבת ויו"ט].
      התזכורת נשלחת לפי הזמן שבוחרים, [ החל מ5 דקות לפני הזמן ועוד שעה] לפני הזמן.
      ישנם כשבעים ערים מעודכנים, והתזכורת נשלחת בדיוק מרבי של עד 29 שניות הקדמה או איחור.
      מספר הראשי המערכת הוא 0795686170.
      ישנם מספרי משנה עוקבים שמהם נשלח התזכורות.
      0795686171 נץ החמה
      0795686172 ק"ש ראשון וכן הלאה.
      נ.ב. כיום רשומים מעל 12.000 מספרים שונים לזכורות ונשלחים למעלה מ21.000 תזכורות ביום. [ניתן לשמוע את הנתונים בשלוחה 0]

      פורסם בשאלות ועזרה הדדית
      י
      ישראל10
    • RE: פניות תח"צ בטלפון

      @שמואל-ש @מתנסה-php-api @WIZNITZ אדרבה.
      תרימו את הכפפה ביחד.
      [אם בהמשך יצטרכו תשלום סמלי בשביל שרת או כיוצא בזה, אני מוכן לשלם].

      פורסם בעזרה הדדית למשתמשים מתקדמים
      י
      ישראל10
    • RE: מבצע לזמן מוגבל למניידים מספר וירטואל פלוס - שיחות ללא חיוב יחידות

      כאן כתוב "הכניסו ניתוב שיחות קולי למספר הווירטואלי שיענה במקומכם מחוץ לשעות העבודה"
      8e2976ce-f56b-4b91-baaf-ae008b78394c-image.png
      מה הכוונה?

      פורסם בחדש במערכת
      י
      ישראל10
    • RE: שליחת וקבלת מיילים באמצעות הטלפון הכשר בחינם | abaye | מדריך מפורט

      @אביי-ורבא
      א. הגדרתי משתנה... 36325a39-636e-4148-84a2-c7ba1d8237f4-image.png
      והרצתי עוד פעם, וזה לא הגדיר...
      ב. עשיתי הרצה, וזה התגובה.
      49d6f1cf-2fa6-41ee-8a75-248ae54781b0-image.png
      f2d4312b-330c-4da9-b3a6-e244d5ffdfb9-image.png

      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10
    • RE: שליחת וקבלת מיילים באמצעות הטלפון הכשר בחינם | abaye | מדריך מפורט

      @אביי-ורבא הגדרתי.
      זה לא עבד לי...
      דבר ראשון זה לא הגדיר את שלוחה 9 של התפריט שבחרתי אלא של התפריט הראשי.
      דבר שני הוא אומר הקלט את תוכן ההודעה, ומשמיע הקלטה ולא תימלול.
      דבר שלישי המייל לא נשלח...
      זה אמור להראות ככה?
      96d6e888-b71f-4a81-870e-7a447533629d-image.png

      פורסם בטיפים עצות והדגמות מהמשתמשים
      י
      ישראל10