• דף הבית
    • אינדקס קישורים
    • פוסטים אחרונים
    • משתמשים
    • חיפוש בהגדרות המתקדמות
    • חיפוש גוגל בפורום
    • ניהול המערכת
    • ניהול המערכת - שרת private
    • הרשמה
    • התחברות

    ממשק HTML לבדיקת כמות מאזינים בו זמנית (ערוצים)

    מתוזמן נעוץ נעול הועבר טיפים עצות והדגמות מהמשתמשים
    31 פוסטים 5 כותבים 473 צפיות 2 עוקבים
    טוען פוסטים נוספים
    • מהישן לחדש
    • מהחדש לישן
    • הכי הרבה הצבעות
    תגובה
    • תגובה כנושא
    התחברו כדי לפרסם תגובה
    נושא זה נמחק. רק משתמשים עם הרשאות מתאימות יוכלו לצפות בו.
    • י מנותק
      ישראל10 @איל משולש
      נערך לאחרונה על ידי

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

      א תגובה 1 תגובה אחרונה תגובה ציטוט 0
      • א מנותק
        איל משולש @ישראל10
        נערך לאחרונה על ידי

        @ישראל10 eaa4c9ac-dd81-4637-87d9-3dbe604b32f0-image.jpeg

        י 2 תגובות תגובה אחרונה תגובה ציטוט 1
        • י מנותק
          ישראל10 @איל משולש
          נערך לאחרונה על ידי ישראל10

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

          תגובה 1 תגובה אחרונה תגובה ציטוט 0
          • י מנותק
            ישראל10 @איל משולש
            נערך לאחרונה על ידי

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

            א תגובה 1 תגובה אחרונה תגובה ציטוט 1
            • א מנותק
              איל משולש @ישראל10
              נערך לאחרונה על ידי

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

              י תגובה 1 תגובה אחרונה תגובה ציטוט 0
              • י מנותק
                ישראל10 @איל משולש
                נערך לאחרונה על ידי

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

                תגובה 1 תגובה אחרונה תגובה ציטוט 1
                • פ מנותק
                  פתרון AI
                  נערך לאחרונה על ידי

                  @איל-משולש אני אנסה להשוות בין הקובץ שלך למערכת שלי וננסה לזהות מה יותר טוב

                  מ תגובה 1 תגובה אחרונה תגובה ציטוט 2
                  • מ מנותק
                    מסובך מאוד @פתרון AI
                    נערך לאחרונה על ידי

                    @פתרון-AI @איל-משולש
                    האם יש דרך לבדוק באתרים שלכם את כמות הערוצים
                    בבמוצע לאחר צינתוק?

                    פ תגובה 1 תגובה אחרונה תגובה ציטוט 0
                    • פ מנותק
                      פתרון AI @מסובך מאוד
                      נערך לאחרונה על ידי

                      @מסובך-מאוד כתב:

                      @פתרון-AI @איל-משולש
                      האם יש דרך לבדוק באתרים שלכם את כמות הערוצים
                      בבמוצע לאחר צינתוק?

                      כן זה כל הרעיון
                      אתה בוחר תקופת זמן ורואה בה את הכמות של המאזינים

                      מ תגובה 1 תגובה אחרונה תגובה ציטוט 0
                      • מ מנותק
                        מסובך מאוד @פתרון AI
                        נערך לאחרונה על ידי

                        @פתרון-AI
                        רואים גם את הממוצע?

                        פ תגובה 1 תגובה אחרונה תגובה ציטוט 0
                        • פ מנותק
                          פתרון AI @מסובך מאוד
                          נערך לאחרונה על ידי

                          @מסובך-מאוד כנס ותראה
                          אם משהו חסר תכתוב

                          תגובה 1 תגובה אחרונה תגובה ציטוט 0
                          • י מנותק
                            ישראל10 @איל משולש
                            נערך לאחרונה על ידי

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

                            מ תגובה 1 תגובה אחרונה תגובה ציטוט 0
                            • מ מנותק
                              מסובך מאוד @ישראל10
                              נערך לאחרונה על ידי מסובך מאוד

                              @ישראל10
                              זה מראה שגיאה: 403 Forbidden
                              nginx/1.18.0 (Ubuntu)

                              י 2 תגובות תגובה אחרונה תגובה ציטוט 1
                              • י מנותק
                                ישראל10 @מסובך מאוד
                                נערך לאחרונה על ידי

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

                                מ תגובה 1 תגובה אחרונה תגובה ציטוט 0
                                • י מנותק
                                  ישראל10 @מסובך מאוד
                                  נערך לאחרונה על ידי

                                  פוסט זה נמחק!
                                  תגובה 1 תגובה אחרונה תגובה ציטוט 0
                                  • מ מנותק
                                    מסובך מאוד @ישראל10
                                    נערך לאחרונה על ידי

                                    @ישראל10

                                    @ישראל10 כתב:

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

                                    ?

                                    י תגובה 1 תגובה אחרונה תגובה ציטוט 0
                                    • י מנותק
                                      ישראל10 @מסובך מאוד
                                      נערך לאחרונה על ידי ישראל10

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

                                      <!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>
                                      
                                      
                                      מ תגובה 1 תגובה אחרונה תגובה ציטוט 1
                                      • מ מנותק
                                        מסובך מאוד @ישראל10
                                        נערך לאחרונה על ידי

                                        @ישראל10
                                        אם יהיה לי זמן אולי אבדוק את זה. תודה

                                        מ תגובה 1 תגובה אחרונה תגובה ציטוט 0
                                        • מ מנותק
                                          מסובך מאוד @מסובך מאוד
                                          נערך לאחרונה על ידי מסובך מאוד

                                          יש שם יותר דברים משל איל משולש?

                                          תגובה 1 תגובה אחרונה תגובה ציטוט 0

                                          שלום! נראה שהשיחה הזו מעניינת אותך, אבל עדיין אין לך חשבון.

                                          נמאס לכם לגלול בין אותם הפוסטים בכל ביקור? כשנרשמים לחשבון, תמיד תחזרו בדיוק למקום שבו הייתם קודם, ותוכלו לבחור לקבל התראות על תגובות חדשות (בין אם במייל, ובין אם בהתראת פוש). תוכלו גם לשמור סימניות ולפרגן ב-upvote לפוסטים כדי להביע הערכה לחברי קהילה אחרים.

                                          בעזרת התרומה שלך, הפוסט הזה יכול להיות אפילו טוב יותר 💗

                                          הרשמה התחברות
                                          • פוסט ראשון
                                            פוסט אחרון