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

    יב

    @יב

    77
    מוניטין
    108
    צפיות בפרופיל
    450
    פוסטים
    0
    עוקבים
    7
    עוקב אחרי
    תאריך הצטרפות
    נראה לאחרונה

    יב הפסקת מעקב מעקב

    הפוסטים הטובים ביותר שנוצרו על ידי יב

    • קריאת דוחות באקסל, ללא צורך בהמרה כלשהו

      תמיד רציתי לקרוא את הדו"ח נתונים בקובץ אקסל, ושהם יתעדנו מיידית בקובץ, ללא צורך להוריד קבצים על גבי קבצים, כל פעם דו"ח חדש.
      נכון, שיש גוגל שיטס, אבל אני לא כ"כ מבין בפונקציות שם, אז מבחינתי זה היה מסובך... - ומאידך, באקסל למרות שצריכים לכתוב פונקציות, אך הפונקציה שאביא כאן, היא כה פשוטה, שכל מתחיל יוכל להשתמש בזה.
      התחלנו...
      פותחים קובץ אקסל חדש.
      נכנסים ל 'נתונים' .... 'מהאינטרנט'
      2366291b-aa0c-4dd7-b5ea-17f7db421bcf-image.png
      ונפתח חלון כזה
      1df83817-160c-4ce7-b9b5-98f396fc46f7-image.png
      היכן שכתוב 'כתוב URL' - מכניסים את הקוד הבא

      https://call2all.co.il/ym/api/RenderYMGRFile?wath=ivr2:/1/ApprovalAll.ymgr&convertType=csv&LoadLang=1&token=0773137770:123456
      

      אחרי - ivr2: - מכניסים את נתיב השלוחה, ושם הקובץ [בדוגמא, זה קובץ ApprovalAll.ymgr שבשלוחה 1].
      את - 0773137770 - משנים למספר המערכת שלכם.
      ואת - 123456 - לסיסמה שלכם.
      את - LoadLang=1 - עדיף להשאיר ככה, וכך תקבלו את הנתונים, עם השמות שהגדרתם לקובץ, וכן לא תקבלו את מה שהגדרתם להחביאם. - אך אם ברצונכם לקבל את כל הנתונים (הקשות וכדו'), ללא שמות (בכותרת), תחליפו את מס' 1, ל-0.
      ולוחצים אישור.
      אחר-כך נפתח חלון כזה, - ולוחצים 'טען'
      761352dc-1cfc-4958-bbdd-156ea69d7d66-image.png
      האקסל פותח גיליון חדש, ושם מתעדכנים כל הזמן הנתונים. [כמובן שאפשר למחוק את הגיליון הראשון (גיליון 1)].
      לצורך ריענון הנתונים, ניתן להקיש 'רענן' ב'כלי הטבלאות'
      bce08a11-52f5-48c6-8e2c-65a30641203b-image.png
      למרות שנראה לי שזה דבר פשוט למשתמשים [שהרי מטעם זה, לכאורה, לא ראיתי כתוב על זה בפורום], אך נראה לי, שכמוני הרבה משתמשים חיפשו איך לקרוא את הדוחו"ת והנתונים ישירות בקובץ אקסל, לכן החלטתי להעלותו כאן, וסליחה מכל אלו שחושבים שזה דבר פשוט...
      למען ההגינות, הרעיון לקוח ממה ש @מיכאלוש כתב בגוגל שיטס, חן חן לו...

      פורסם בהסברים מסודרים ממשתמשים
      י
      יב
    • שירות לקוחות ללא עלות יחידות, ו/או הקלטת כל השיחות של הטלפון האישי שלכם. פלוס, תא קולי אישית.‏

      רצית להקליט אוטומטי כל השיחות האישיות שלכם?‏
      רצית לקבל אחר-כך את השיחות לאימייל?‏
      ‏כמו כן, רצית, שבאם אתה לא יכול לענות, תהיה אפשרות להשאיר לך תא קולי?‏
      וכמובן, אחר-כך שיהיה לך אפשרות לשמוע בטלפון את התא-קולי, ו/או לקבל אותם באימייל?‏
      וכמובן, אם אתה רוצה להשתמש בשירות לקוחות מתקדמת ללא עלות יחידות.
      ‏כל ההגדרות לפניך!!

      דבר ראשון;‏
      קרדיט ל; @מתעניין, שעזר לי מאוד מאוד!!!.‏
      וכן, קרידט ל @חדר-1, שהכל נלקח מההשראה שלו.‏


      איך זה עובד – בכללי?‏

      ‎1‎‏. אתה צריך לעשות הפניית שיחות מהטלפון שלך, למערכת שלך (לא קשור להגדרה כלשהו במערכת, אלא בטלפון האישי שלך).‏
      ‎2‎‏. חשוב! ההפניה צריכה להיות, שבאם אין תשובה לאחר 5 שניות הוא מופנה למספר המערכת ‏שלך (כלומר, ב-5 שניות הראשונות, הוא מתקשר רגיל לטלפון האישי שלך, כך, תראה מי ‏המתקשר [וכמו כן, תוכל לקבל צינתוק, כמו שאכתוב להלן]).‏
      ‎3‎‏. מגדירים, שכל מי שמתקשר דרך הפניית שיחות מהטלפון שלך, יגיע לשלוחה מסוימת ‏‏[שנכתוב להלן].‏
      ‎4‎‏. מגדירים, שליחת צינתוק, כך - שתדעו שמתקשרים אליך.‏
      ‎5‎‏. מגדירים, שרק מתקשר אחד יוכל לדבר איתך, ואם יש שניים שמתקשרים, השני עובר לתא קולי.‏
      ‎6‎‏. מגדירים, שאם אין מענה לאחר 40 שניות [לדוגמא], זה עובר לתא-קולי.‏
      ‏7. מגדירים, שלוחת וועידה, ועל ידי זה אתה מדבר עם המתקשר. – וכל השיחות מוקלטות.‏
      ‏8. מגדירים, קבלת אימייל מהשיחות, והתא קולי.‏
      ‏9. מגדירים, שלוחת השמעה, לתא קולי.‏

      מתחילים;


      שלב ראשון - הפניית שיחות


      מגדירים שכל מי שהתקשר באמצעות ההפנייה, יועבר לשלוחה 9000 (לדוגמא) בשלוחה הראשית.
      בשלוחה הראשית, יש להגדיר בנוסף להגדרות השלוחה הפניה לפי מספר טלפון שהפנה את השיחה.

      type=סוג שלוחה ראשית
      check_did_and_go_to_folder=yes
      did_directed_check=yes
      

      וכמו כן, יש להוסיף בשלוחה הראשית, גם קובץ בשם Did_Go_To.ini.
      ולהכניס בתוכו ההגדרה;

      07777777-Directed-0527666666=/9000
      

      כלומר, כל מי שחייג למערכת 07777777 [החליפו במספר המערכת שלכם] והופנה מהטלפון 0527666666 [החליפו למספר האישי שלכם] יגיע לשלוחה /9000.
      שימו לב;
      ישנם חברות תקשורת שלא ניתן לעשות הפניה הנזכרת, ואם תגדירו רק 'הפניה אם אין תשובה', זה מייצר אוטומטית הפניה כוללת.


      שלב שני - הרשמה לקבלת צינתוקים


      פותחים שלוחה [זמנית] בשלוחה מסויימת (לא משנה איפוא).
      ומגדירים בקובץ ext.ini אישור קבלת צינתוקים;

      type=tzintuk
      list_tzintuk=123
      

      מתקשרים לקו, ובשלוחה זו יש להוסיף את עצמכם לרשימת הצינתוקים.
      לאחר הוספת מספרכם לרשימת צינתוקים, יש למחוק מיד את השלוחה, כדי שאחרים לא יצטרפו לרשימת צינתוקים זו
      הערה; אם יש לכם כבר רשימת צינתוקים שמספרו 123, יש להחליף list_tzintuk=מספר כלשהו


      שלב שלישי - שליחת צינתוק מהמתקשר לטלפון האישי שלכם


      כאמור, המתקשר מגיע מיד לשלוחה /9000.
      בשלוחה זו, המתקשר שולח לכם צינתוק התרעה.

      type=tzintuk
      tzintuk_admin=yes
      list_tzintuk=123
      tzintuk_end=/9000/1
      run_tzintuk_automat=yes
      tzintuk_timeout=4
      title=שליחת צינתוק לטלפון האישי
      

      שים לב: אורך הצינתוק הוא 4 שניות, כדי שתוכלו להתקשר מיד לקו, ושהמתקשר לא יצטרך להמתין מידי הרבה זמן, עד שאתם עונים.

      • יש להחליף הודעת M3338
          %D7%A7%D7%95%D7%91%D7%A5-%D7%A9%D7%A7%D7%98.wav
        .

      שלב רביעי - שרק מתקשר אחד יוכל לעבור לדבר איתכם, והשאר יועברו לתא קולי


      אחרי שהמאזין שלח לכם צינתוק, מגדירים בשלוחה /9000/1
      סוג של סינון, שרק מתקשר אחד יכול לעבור לדבר אתכם, והשאר עוברים לתא קולי.

      type=go_to_folder_count
      go_to_folder_count=1
      go_to_folder=/9000/2
      go_to_folder_big=/9000/3
      title=פילטר לכניסה רק מתקשר 1
      
      • יש להחליף הודעת M2526
          %D7%A7%D7%95%D7%91%D7%A5-%D7%A9%D7%A7%D7%98.wav
        .

      שלב חמישי - הפעלת חדר וועידה


      אחרי שהמתקשר עבר את הסינון, הוא מועבר לשלוחה /9000/2.
      יש להגדיר את השלוחה כחדר וועידה, שהמתקשר (=מאזין) מושתק עד שהמנהל מפעיל את החדר וועידה, ובינתיים, הוא שומע מוסיקה בהמתנה [אפשר ליצור מוסיקה בהמתנה באופן עצמאי ].
      בנוסף, יש להגדיר, שבמידה ואתה לא יכול לענות אחרי 40 שניות, הוא גם כן עובר לתא קולי.

      type=confbridge
      conf_bridge_type=speech
      conf_bridge_exit=yes
      conf_bridge_folder_to_play=/9/1
      limit_call_seconds=yes
      limit_call_seconds_disconnect_time=40
      limit_call_seconds_end_time_go_to=/9000/2/1
      confbridge_live_no_goto=wait_live
      confbridge_music_on_hold=המוסיקה בהמתנה שלכם
      confbridge_end_goto=hangup
      title=חדר וועידה של המטלפן
      email_send=yes
      email_address=הכניסו את האימייל שלכם@gmail.com
      email_name=הקלטות מתקשרים
      email_title=שיחות
      

      שים לב: אנחנו מפעילים את חדר הוועידה, בשלוחה שהמנהל נכנס לשם (ראו להלן). - ולכן, במידה ואתם משנים את שלוחת המנהל, יש לשנות conf_bridge_folder_to_play=השלוחה שהמנהל נכנס שם**

      • יש להחליף הודעות M1008, M1815,M1816, M1167
          %D7%A7%D7%95%D7%91%D7%A5-%D7%A9%D7%A7%D7%98.wav
        .

      שלב שישי - המשך השיחה, או העברה לתא קולי


      כמו שמוגדר בשלוחה /9000/2, שאחרי דקה הוא יוצא מהשלוחה הנזכרת.
      אבל מה עושים, אם כבר עניתם לטלפון ואתם צריכים להמשיך את השיחה? - או לחילופין, עבר 40 שניות, והמתקשר אמור לעבור לשלוחת תא קולי?
      בשביל זה אנחנו מגדירים את שלוחה /9000/2/1.

      type=confbridge
      conf_bridge_type=speech
      conf_bridge_exit=yes
      title=המשך שיחה, או העברה לתא קולי
      confbridge_live_no_goto=/9000/3
      conf_bridge_folder_to_play=/9/1
      confbridge_end_goto=hangup
      

      @Evi770 ממליץ להחליף את הודעות M1815, M1816, M1167 ו M1008

        %D7%A7%D7%95%D7%91%D7%A5-%D7%A9%D7%A7%D7%98.wav
      .


      שלב שביעי - תיבת תא קולי


      בשלוחה /9000/3 מגדירים

      type=record
      hangup_insert_file=yes
      say_record_number=no
      say_record_menu=no
      record_end_goto=/9000/3/1
      folder_move=/9/2
      email_send=yes
      email_address=כתובת האימייל שלכם@gmail.com
      email_name=הקלטות שיחות
      email_title=שיחות
      title=תא קולי
      

      שים לב: כאן מוגדר, שההקלטות מועברות לשלוחת /9/2, ושם יכולים לשמוע את התא קולי, אם ברצונכם להחליף למיקום אחר יש להחליף folder_move=למיקום אתם רוצים.


      שלב שמיני - הפעלת צינתוק - עבור התרעה להשארת תא קולי


      בשלוחה /9000/3/1 מגדירים;

      type=tzintuk
      tzintuk_admin=yes
      list_tzintuk=123
      run_tzintuk_automat=yes
      tzintuk_end=hangup
      conf_bridge_exit=yes
      title=צינתוק
      
      • יש להחליף הודעת M3338
          %D7%A7%D7%95%D7%91%D7%A5-%D7%A9%D7%A7%D7%98.wav
        .

      עד כאן ההגדרות מצד המאזין, עכשיו אנחנו צריכים להגדיר את ההגדרות מצד המנהל.


      שלב תשיעי - הגדרת תפריט למנהל


      המנהל נכנס לשלוחה 9 שבתפריט הראשי.
      אנחנו מגדירים בשבילו שתי אופציות;

      1. לענות לטלפון (הפעלת חדר וועידה).
      2. לשמוע תא-קולי.

      בשלוחה 9/ אנחנו מגדירים, שרק מי שנרשם לרשימת הצינתוקים יוכל להיכנס, ומי שלא רשום יצטרך להקיש קוד (הקוד מיועד במידה ותרצה לשמוע את תא קולי מטלפון אחר, או במידה ותרצה להפעיל את הוועידה מטלפון אחר).

      type=menu
      go_to_from_tzintuk=yes
      check_list_tzintuk=123
      go_to_from_tzintuk_found=in_extension
      go_to_from_tzintuk_blocked=in_extension
      go_to_from_tzintuk_not_found=in_extension
      go_to_from_tzintuk_blocked_enter_password=כאן תכתבו את הקוד
      go_to_from_tzintuk_not_found_enter_password=כאן תכתבו את הקוד
      go_to_from_tzintuk_blocked_password_error_go_to=/
      go_to_from_tzintuk_not_found_password_error_go_to=/
      title=סינון כניסה לפי טלפון, או קוד
      

      תוכלו להעלות קובץ M0000 - לענות לטלפון, הקישו 1. לתא קולי הקישו 2.


      שלב עשירי - התקשרות עם המתקשר


      בשלוחת /9/1
      אנחנו מגדירים הפעלת שידור חי - כלומר, עונים לטלפון.

      type=confbridge
      conf_bridge_type=admin
      conf_bridge_message_start_after_beep=no
      conf_bridge_beep=no
      conf_bridge_exit=yes
      confbridge_end_goto=hangup
      email_title=שיחות
      email_send=yes
      email_address=תכתבו את כתובת האימייל שלכם@gmail.com
      email_name=הקלטות שיחות
      

      שלב אחד עשר - שמיעת תא קולי


      בשלוחת /9/2
      אנחנו מגדירם שמיעת התא קולי;

      type=playfile
      title=שמיעת הודעות תא קולי
      
      פורסם בעזרה הדדית למשתמשים מתקדמים
      י
      יב
    • RE: יש אפשרות לעצור את ההקלטה, ולהקיש על המשך?

      @איפכא-מסתברא מה לא טוב עם continue_recording - הקלטת המשך להקלטה
      ?

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

      @אופיר בנוסף למה ש @עידו אמר, אני אישית מוסיף ידע. - ידע {לדעתי} שווה כסף, וגם אם אני יכול לבנות קו מסובך במהירות בגלל שאני בקי בהגדרות, אני לא אחשב את שעות עבודה * 60 לשעה {לדוגמא}. - אני תמיד אבקש גם מחיר על הידע, כמובן, שללקוח אני לא אומר שהוא משלם על הידע, רק על העבודה.

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

      אפשר לעשות את זה מכל אתר ומכל שידור.
      נכנסים לאתר של השידור חי, ומקישים על F12, נפתח רשימה צדדית, מקישים על media [וכשהשידור חי פועל] נפתח שם קישור ישיר לשידור, מעתיקים מאתר השידור לאתר ימות.
      2023-03-19 01-10-16 00_00_00-00_00_30.gif

      פורסם בחדש במערכת
      י
      יב
    • RE: באג בהשמעת קבצים

      @מתעמק גא"מ

      פורסם בבאגים במערכת
      י
      יב
    • RE: העברת נתונים אוטומטית מהמערכת לקובץ גוגל שיטס

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

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

      @eliyahu לדעתי. האפשרות הראשונה יותר טובה (לברירת מחדל). - כיון, שאם במערכת מוגדר זיהוי, זה אומר שאין גישה חופשית לכולם.
      מנגד, לפעמים יש זיהוי רק על שלוחה מסוימת - לצורך העניין שלוחה 2, כך, שאם הראשון מתקשר לשלוחה 1 והוא נכנס ללא זיהוי, והשני מתקשר לשלוחה 2 והוא נכנס עם זיהוי, לפי זה, כמובן שברירת המחדל עדיף שיהיה לפי הטלפון.
      בכל אופן, צריך שיהיו שתי האפשרויות, ותלוי מאד איך המערכת מסתכלת על זיהוי כברירת מחדל...

      פורסם בחדש במערכת
      י
      יב
    • RE: להעביר שלוחה עם תתי שלוחות וכל הקבצים

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

      פורסם בשאלות ועזרה הדדית
      י
      יב
    • RE: שאלה בנושא; מעבר לשלוחה אחרת באמצעות מקש במהלך ההשמעה 🔼

      @צדיק-תמים @ספר-דברים תודה!

      פורסם בשאלות ועזרה הדדית
      י
      יב

    פוסטים אחרונים שנוצרו על ידי יב

    • RE: איך להגדיר שמאזין מסויים לא יצטרך סיסמה?

      @אA איך כתבת?

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

      @אA לכאו' לפי מחוייג זה אמור לעבוד ככה;

      1. בודק את מספר המחוייג - ובקובץ מוגדר להעביר אותו לשלוחה אחרת.
      2. במידה ואין מוגדר, הוא נשאר בשלוחה, ששם יש את הסיסמה/פילטר

      על פניו זה מהלך אחר מהרעיון הנ"ל.

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

      @אA מעניין.
      אבל מה ההגדרות? כי לכאורה לא אמור להיות בעיה.

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

      @יעקב-יצחק תקח את הרעיון מכאן

      פורסם בשאלות ועזרה הדדית
      י
      יב
    • RE: לינק של הקלטה

      @איל-משולש אולי עשית לינק קשיח לשלוחה 2?

      פורסם בבאגים במערכת
      י
      יב
    • RE: שיום רשימת צנתוקים

      @מנסה מה לא טוב עם הקובץ PhonesName ?

      פורסם בבקשות לפיתוח
      י
      יב
    • תגובה | דוח ניתוח האזנה

      בנוגע למה שעשה @ע.ג. בדוח נתוני האזנה.
      עשיתי שינויים קלים לצרכיי, ואני משתף את זה.
      הוספה;

      1. שמות השלוחות.
      2. חיפוש חופשי (לא יודע אם זה נוגע לכולם, אבל לי אישית זה הועיל).
      3. קבצים/מאזינים מובילים.
      4. במידה ומישהו שמע כמה פעמים קובץ, יש אפשרות לראות את מספר הפעמים.
      5. חלוקה של הדוח לדפים (ברירת מחדל 20 שורות בדף, ניתן לשינוי)

      <!DOCTYPE html>
      <html lang="he" dir="rtl">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>מערכת ניתוח נתוני האזנה - Pro Dashboard</title>
          <!-- Tailwind CSS -->
          <script src="https://cdn.tailwindcss.com"></script>
          <!-- XLSX Library -->
          <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
          <!-- Lucide Icons -->
          <script src="https://unpkg.com/lucide@latest"></script>
          <style>
              @import url('https://fonts.googleapis.com/css2?family=Assistant:wght@300;400;600;700&display=swap');
              
              body {
                  font-family: 'Assistant', sans-serif;
                  background-color: #f8fafc;
                  color: #1e293b;
              }
      
              .glass-card {
                  background: rgba(255, 255, 255, 0.95);
                  backdrop-filter: blur(10px);
                  border: 1px solid rgba(255, 255, 255, 0.2);
                  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
              }
      
              .custom-scrollbar::-webkit-scrollbar { width: 6px; height: 6px; }
              .custom-scrollbar::-webkit-scrollbar-track { background: #f1f5f9; }
              .custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
              .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
      
              .animate-fade-in { animation: fadeIn 0.5s ease-out forwards; }
              @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
      
              .progress-fill { transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); }
              .bg-green-gradient { background: linear-gradient(90deg, #10b981, #34d399); }
              .bg-orange-gradient { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
              .bg-red-gradient { background: linear-gradient(90deg, #ef4444, #f87171); }
      
              .filter-row input, .filter-row select {
                  background: #ffffff;
                  border: 1px solid #e2e8f0;
                  font-size: 11px;
                  padding: 4px 6px;
                  border-radius: 4px;
                  width: 100%;
                  outline: none;
                  transition: all 0.2s;
              }
              .filter-row input:focus { border-color: #6366f1; box-shadow: 0 0 0 1px #6366f1; }
      
              .page-btn {
                  padding-left: 0.75rem;
                  padding-right: 0.75rem;
                  padding-top: 0.25rem;
                  padding-bottom: 0.25rem;
                  border-radius: 0.5rem;
                  border-width: 1px;
                  border-color: #e2e8f0;
                  font-size: 0.875rem;
                  line-height: 1.25rem;
                  font-weight: 500;
                  transition-property: all;
                  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
                  transition-duration: 150ms;
              }
              .page-btn:hover { background-color: #f8fafc; }
              .page-btn:disabled { opacity: 0.5; }
              .page-btn.active {
                  background-color: #4f46e5;
                  color: #ffffff;
                  border-color: #4f46e5;
              }
      
              .detail-row { background-color: #f8fafc; }
          </style>
      </head>
      <body class="min-h-screen pb-12">
      
          <!-- Header -->
          <nav class="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200">
              <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                  <div class="flex justify-between h-16 items-center">
                      <div class="flex items-center gap-3">
                          <div class="bg-indigo-600 p-2 rounded-lg shadow-lg shadow-indigo-200">
                              <i data-lucide="bar-chart-3" class="text-white w-6 h-6"></i>
                          </div>
                          <h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-600 to-violet-600">
                              מנתח נתוני האזנה Pro
                          </h1>
                      </div>
                      <div id="statusIndicator" class="text-sm font-medium text-slate-500 flex items-center gap-2">
                          <span class="w-2 h-2 rounded-full bg-slate-300"></span>
                          ממתין לפעולה
                      </div>
                  </div>
              </div>
          </nav>
      
          <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8 space-y-8">
              
              <!-- Info & Setup Section -->
              <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
                  
                  <!-- Setup Panel -->
                  <div class="lg:col-span-2 glass-card rounded-2xl p-6 animate-fade-in">
                      <div class="flex items-center gap-2 mb-6">
                          <i data-lucide="settings" class="text-indigo-600 w-5 h-5"></i>
                          <h2 class="text-lg font-semibold">הגדרות שליפה</h2>
                      </div>
                      
                      <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                          <div class="space-y-2">
                              <label class="text-sm font-medium text-slate-700">מתאריך</label>
                              <input type="date" id="from-date" class="w-full px-4 py-2 rounded-xl border border-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none">
                          </div>
                          <div class="space-y-2">
                              <label class="text-sm font-medium text-slate-700">עד תאריך</label>
                              <input type="date" id="to-date" class="w-full px-4 py-2 rounded-xl border border-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none">
                          </div>
                          <div class="md:col-span-2 space-y-2">
                              <label class="text-sm font-medium text-slate-700">טוקן API</label>
                              <div class="flex gap-2">
                                  <div class="relative flex-1">
                                      <input type="password" id="api-token" placeholder="הזן טוקן..." class="w-full px-4 py-2 pr-10 rounded-xl border border-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none">
                                      <i data-lucide="key" class="absolute left-3 top-2.5 text-slate-400 w-4 h-4"></i>
                                  </div>
                                  <button onclick="clearToken()" class="px-3 py-2 text-rose-500 hover:bg-rose-50 rounded-xl transition-colors" title="נקה טוקן">
                                      <i data-lucide="trash-2" class="w-5 h-5"></i>
                                  </button>
                              </div>
                          </div>
                      </div>
      
                      <div class="mt-8 flex flex-wrap gap-4">
                          <button onclick="fetchData()" id="fetch-btn" class="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-8 py-3 rounded-xl font-semibold transition-all shadow-lg shadow-indigo-200 active:scale-95">
                              <i data-lucide="download" class="w-5 h-5"></i>
                              משוך נתונים
                          </button>
                          <button onclick="exportToExcel()" id="export-btn" class="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-3 rounded-xl font-semibold transition-all shadow-lg shadow-emerald-200 active:scale-95 hidden">
                              <i data-lucide="file-spreadsheet" class="w-5 h-5"></i>
                              ייצא לאקסל
                          </button>
                      </div>
                  </div>
      
                  <!-- Important Info Box -->
                  <div class="glass-card rounded-2xl p-6 bg-indigo-50/50 border-indigo-100 animate-fade-in" style="animation-delay: 0.1s">
                      <div class="flex items-center gap-2 mb-4">
                          <i data-lucide="info" class="text-indigo-600 w-5 h-5"></i>
                          <h2 class="text-lg font-semibold text-indigo-900">מידע חשוב</h2>
                      </div>
                      <div class="text-xs text-indigo-800 space-y-4 leading-relaxed">
                          <div class="bg-white/80 p-3 rounded-lg border border-indigo-100 shadow-sm">
                              <p class="font-bold mb-1 text-indigo-900">דרישות מערכת:</p>
                              <p>המערכת מיועדת למערכות בהן מוגדר שמירת דוח יומי. יש לוודא שבקובץ <code class="bg-indigo-100 px-1 rounded">ivr.ini</code> מופיעה ההגדרה:</p>
                              <code class="block mt-1 font-mono text-indigo-600 bg-indigo-100/50 p-1 rounded text-center font-bold">log_playback_play_stop=yes</code>
                          </div>
                          <div class="space-y-2 opacity-90">
                              <p class="flex items-start gap-2">
                                  <span class="mt-1 w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0"></span>
                                  <span>המערכת משקללת רצף האזנה אמיתי ומסננת כפילויות האזנה או דילוגים.</span>
                              </p>
                              <p class="flex items-start gap-2">
                                  <span class="mt-1 w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0"></span>
                                  <span>התוצאה משקפת זמן האזנה בפועל לתוכן הקובץ.</span>
                              </p>
                              <p class="flex items-start gap-2">
                                  <span class="mt-1 w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0"></span>
                                  <span>לא מומלץ להפיק דוחות בין 4 ל-5 לפנות בוקר (זמן עדכון שרתים).</span>
                              </p>
                          </div>
                      </div>
                  </div>
              </div>
      
              <!-- Stats Grid -->
              <div id="statsContainer" class="grid grid-cols-1 md:grid-cols-4 gap-6 hidden animate-fade-in">
                  <div class="glass-card rounded-2xl p-5 border-r-4 border-indigo-500">
                      <p class="text-sm text-slate-500 font-medium">סה"כ רשומות</p>
                      <h3 id="statTotal" class="text-2xl font-bold mt-1">0</h3>
                  </div>
                  <div class="glass-card rounded-2xl p-5 border-r-4 border-emerald-500">
                      <p class="text-sm text-slate-500 font-medium">ממוצע האזנה</p>
                      <h3 id="statAvg" class="text-2xl font-bold mt-1">0%</h3>
                  </div>
                  <div class="glass-card rounded-2xl p-5 border-r-4 border-amber-500">
                      <p class="text-sm text-slate-500 font-medium">האזנה נטו (שעות)</p>
                      <h3 id="statNetHours" class="text-2xl font-bold mt-1">0</h3>
                  </div>
                  <div class="glass-card rounded-2xl p-5 border-r-4 border-rose-500">
                      <p class="text-sm text-slate-500 font-medium">האזנה ברוטו (שעות)</p>
                      <h3 id="statGrossHours" class="text-2xl font-bold mt-1">0</h3>
                  </div>
              </div>
      
              <!-- Top Files & Top Listeners -->
              <div id="insightsContainer" class="grid grid-cols-1 lg:grid-cols-2 gap-8 hidden animate-fade-in">
                  <!-- Top Files -->
                  <div class="glass-card rounded-2xl p-6">
                      <div class="flex items-center gap-2 mb-6">
                          <i data-lucide="trending-up" class="text-indigo-600 w-5 h-5"></i>
                          <h2 class="text-lg font-semibold">קבצים מובילים בהאזנה</h2>
                      </div>
                      <div id="topFilesList" class="space-y-4">
                          <!-- Top files will be injected here -->
                      </div>
                  </div>
                  <!-- Top Listeners -->
                  <div class="glass-card rounded-2xl p-6">
                      <div class="flex items-center gap-2 mb-6">
                          <i data-lucide="award" class="text-amber-500 w-5 h-5"></i>
                          <h2 class="text-lg font-semibold text-slate-800">מאזינים מובילים (נטו)</h2>
                      </div>
                      <div id="topListenersList" class="space-y-4">
                          <!-- Top listeners will be injected here -->
                      </div>
                  </div>
              </div>
      
              <!-- Main Table Area -->
              <div class="glass-card rounded-2xl overflow-hidden animate-fade-in" style="animation-delay: 0.2s">
                  <div class="p-6 border-b border-slate-100 bg-slate-50/50 flex flex-wrap gap-4 items-end justify-between">
                      <div class="flex flex-wrap gap-4 items-end">
                          <div class="w-64 space-y-2">
                              <label class="text-xs font-bold text-slate-500 uppercase tracking-wider">חיפוש חופשי</label>
                              <div class="relative">
                                  <input type="text" id="searchInput" oninput="applyMultiFilter()" placeholder="שם, טלפון, קובץ..." class="w-full px-4 py-2 pr-10 rounded-xl border border-slate-200 outline-none focus:ring-2 focus:ring-indigo-500">
                                  <i data-lucide="search" class="absolute left-3 top-2.5 text-slate-400 w-4 h-4"></i>
                              </div>
                          </div>
                          <div class="w-32 space-y-2">
                              <label class="text-xs font-bold text-slate-500 uppercase tracking-wider">אחוז מינימלי</label>
                              <input type="number" id="filter-percent" oninput="applyMultiFilter()" class="w-full px-4 py-2 rounded-xl border border-slate-200 outline-none focus:ring-2 focus:ring-indigo-500" placeholder="0">
                          </div>
                      </div>
                      <div id="statFiltered" class="text-xs font-bold text-indigo-600 bg-indigo-50 px-3 py-1 rounded-full">0 רשומות מוצגות</div>
                  </div>
      
                  <div class="custom-scrollbar overflow-x-auto">
                      <table id="results-table" class="w-full text-right border-collapse hidden">
                          <thead>
                              <tr id="header-row" class="bg-slate-50 text-slate-500 text-[11px] font-bold uppercase tracking-wider">
                                  <!-- Dynamic Headers -->
                              </tr>
                              <tr id="filter-row" class="filter-row bg-white border-b border-slate-100">
                                  <!-- Dynamic Filters -->
                              </tr>
                          </thead>
                          <tbody id="table-body" class="divide-y divide-slate-100 text-sm">
                              <!-- Results -->
                          </tbody>
                      </table>
                      <div id="emptyState" class="p-20 text-center text-slate-400">
                          <div class="flex flex-col items-center gap-4">
                              <i data-lucide="database" class="w-12 h-12 opacity-20"></i>
                              <p class="text-lg">טרם נטענו נתונים</p>
                          </div>
                      </div>
                  </div>
      
                  <!-- Pagination -->
                  <div id="pagination-container" class="px-6 py-4 bg-slate-50 border-t border-slate-200 flex flex-wrap items-center justify-between gap-4 hidden">
                      <div class="flex items-center gap-4">
                          <span class="text-sm text-slate-500">הצג</span>
                          <select id="rows-per-page" onchange="updateRowsPerPage()" class="px-2 py-1 rounded border border-slate-200 text-sm outline-none">
                              <option value="10">10</option>
                              <option value="20" selected>20</option>
                              <option value="50">50</option>
                              <option value="100">100</option>
                          </select>
                      </div>
                      <div class="flex items-center gap-2" id="page-numbers"></div>
                      <div class="text-sm text-slate-500" id="pagination-info"></div>
                  </div>
              </div>
          </main>
      
          <!-- Loading Overlay -->
          <div id="loadingOverlay" class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-[100] flex items-center justify-center hidden">
              <div class="bg-white rounded-2xl p-8 flex flex-col items-center gap-4 shadow-2xl max-w-sm w-full mx-4">
                  <div class="w-16 h-16 border-4 border-indigo-100 border-t-indigo-600 rounded-full animate-spin"></div>
                  <div class="text-center">
                      <h3 class="text-lg font-bold text-slate-800">מעבד נתונים...</h3>
                      <p id="loadingStatus" class="text-sm text-slate-500 mt-1">מתחבר לשרת</p>
                  </div>
                  <div class="w-full bg-slate-100 rounded-full h-2 mt-2 overflow-hidden">
                      <div id="loadingProgress" class="bg-indigo-600 h-full transition-all duration-300" style="width: 0%"></div>
                  </div>
              </div>
          </div>
      
          <script>
              // --- Initialization ---
              lucide.createIcons();
              
              // Helper: Remove Hebrew Nikud (diacritics)
              function removeNikud(str) {
                  if (!str) return "";
                  return str.replace(/[\u0591-\u05C7]/g, "");
              }
      
              // Default dates: From 7 days ago to today
              const todayDate = new Date();
              const lastWeekDate = new Date();
              lastWeekDate.setDate(todayDate.getDate() - 7);
              
              document.getElementById('from-date').value = lastWeekDate.toISOString().split('T')[0];
              document.getElementById('to-date').value = todayDate.toISOString().split('T')[0];
      
              const savedToken = localStorage.getItem('api_token_cached');
              if (savedToken) document.getElementById('api-token').value = savedToken;
      
              let currentData = [];
              let filteredData = [];
              let sortDirections = Array(12).fill(true);
              let hasEnterId = true;
              let currentPage = 1;
              let rowsPerPage = 20;
              let expandedRows = new Set();
              const extensionTitlesCache = {};
      
              // --- Core Logic ---
      
              function clearToken() {
                  if (confirm("האם למחוק את הטוקן השמור?")) {
                      localStorage.removeItem('api_token_cached');
                      document.getElementById('api-token').value = '';
                  }
              }
      
              async function fetchData() {
                  const fromDate = document.getElementById('from-date').value;
                  const toDate = document.getElementById('to-date').value;
                  const token = document.getElementById('api-token').value;
                  
                  if (!fromDate || !toDate || !token) { alert("נא להזין את כל הפרטים"); return; }
                  localStorage.setItem('api_token_cached', token);
      
                  const overlay = document.getElementById('loadingOverlay');
                  const statusText = document.getElementById('loadingStatus');
                  const progressBar = document.getElementById('loadingProgress');
                  overlay.classList.remove('hidden');
      
                  const dates = getDatesRange(new Date(fromDate), new Date(toDate));
                  const dataMap = {};
                  hasEnterId = true;
      
                  try {
                      for (let i = 0; i < dates.length; i++) {
                          const date = dates[i];
                          statusText.innerText = `מושך נתונים: ${date}`;
                          progressBar.style.width = `${Math.round(((i + 1) / dates.length) * 100)}%`;
      
                          const url = `https://www.call2all.co.il/ym/api/RenderYMGRFile?token=${token}&wath=ivr2:/Log/LogPlaybackPlayStop/LogPlaybackPlayStop.${date}.ymgr&convertType=csv&notLoadLang=1`;
                          const response = await fetch(url);
                          if (response.ok) {
                              const csv = await response.text();
                              processCsvSegment(csv, dataMap, date);
                          }
                      }
      
                      // Fetch Extension Titles
                      const uniquePaths = [...new Set(Object.values(dataMap).map(item => item.info.Folder))];
                      await fetchExtensionTitles(uniquePaths, token);
      
                      finalizeData(dataMap);
                  } catch (err) {
                      alert(`שגיאה: ${err.message}`);
                  } finally {
                      overlay.classList.add('hidden');
                  }
              }
      
              async function fetchExtensionTitles(paths, token) {
                  const statusText = document.getElementById('loadingStatus');
                  const parents = new Set();
                  
                  paths.forEach(p => {
                      if (!p) return;
                      const parts = p.split('/').filter(x => x);
                      if (parts.length === 0) return;
                      
                      if (parts.length === 1) {
                          parents.add("/");
                      } else {
                          parts.pop();
                          parents.add(parts.join('/'));
                      }
                  });
      
                  const parentPaths = Array.from(parents);
                  for (let i = 0; i < parentPaths.length; i++) {
                      const pPath = parentPaths[i];
                      statusText.innerText = `מעדכן שמות שלוחות: ${pPath}`;
                      try {
                          const url = `https://www.call2all.co.il/ym/api/GetIVR2Dir?token=${token}&path=${pPath}`;
                          const response = await fetch(url);
                          if (response.ok) {
                              const res = await response.json();
                              if (res.dirs && Array.isArray(res.dirs)) {
                                  res.dirs.forEach(d => {
                                      const fullPath = (pPath === "/" || pPath === "") ? d.name : `${pPath}/${d.name}`;
                                      if (d.extTitle) {
                                          extensionTitlesCache[fullPath] = d.extTitle;
                                      }
                                  });
                              }
                          }
                      } catch (e) {
                          console.error(`Error fetching titles for ${pPath}:`, e);
                      }
                  }
              }
      
              function processCsvSegment(text, dataMap, date) {
                  const rows = text.replace(/^\uFEFF/, "").trim().split(/\r?\n/).map(r => r.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/));
                  if (rows.length < 2) return;
                  const headers = rows[0].map(h => h.replace(/"/g, '').trim());
      
                  if (!headers.includes("EnterId")) hasEnterId = false;
      
                  for (let i = 1; i < rows.length; i++) {
                      let row = {};
                      headers.forEach((h, idx) => row[h] = (rows[i][idx] || "").replace(/"/g, '').trim());
                      if (!row["FileLength"] || row["FileLength"] === "0:0:0") continue;
      
                      // Normalize folder path
                      row["Folder"] = (row["Folder"] || "").replace(/^\/+|\/+$/g, '');
      
                      const flenSec = timeToSeconds(row["FileLength"]);
                      const identityKey = hasEnterId ? row["EnterId"] : row["Phone"];
                      const key = `${identityKey}_${row["Folder"]}_${row["Current"]}_${row["FileLength"]}`;
                      
                      if (!dataMap[key]) {
                          dataMap[key] = { info: row, intervals: [], flenSec: flenSec, rawSessions: [] };
                      }
      
                      let start = (parseInt(row["PositionPlay"]) || 0) / 1000;
                      let stop = (isNaN(parseInt(row["PositionStop"])) || parseInt(row["PositionStop"]) === -1) ? flenSec : parseInt(row["PositionStop"]) / 1000;
                      
                      dataMap[key].intervals.push([start, stop]);
                      dataMap[key].rawSessions.push({
                          date: date,
                          start: start,
                          stop: stop,
                          duration: stop - start,
                          time: row["Time"] || ""
                      });
                  }
              }
      
              function finalizeData(dataMap) {
                  currentData = Object.values(dataMap).map(item => {
                      const netSec = mergeIntervals(item.intervals);
                      const grossSec = item.rawSessions.reduce((acc, s) => acc + s.duration, 0);
                      
                      // Find last session
                      const last = item.rawSessions.reduce((prev, current) => {
                          const prevDt = `${prev.date} ${prev.time}`;
                          const currDt = `${current.date} ${current.time}`;
                          return currDt > prevDt ? current : prev;
                      });
      
                      return {
                          ...item,
                          netSec: netSec,
                          grossSec: grossSec,
                          lastDate: last.date,
                          lastTime: last.time,
                          percent: item.flenSec > 0 ? Math.min(100, Math.round((netSec / item.flenSec) * 100)) : 0
                      };
                  });
                  
                  if (currentData.length > 0) {
                      setupTableStructure();
                      updateStats();
                      updateInsights();
                      applyMultiFilter();
                      
                      document.getElementById('results-table').classList.remove('hidden');
                      document.getElementById('emptyState').classList.add('hidden');
                      document.getElementById('statsContainer').classList.remove('hidden');
                      document.getElementById('insightsContainer').classList.remove('hidden');
                      document.getElementById('export-btn').classList.remove('hidden');
                      document.getElementById('pagination-container').classList.remove('hidden');
                      document.getElementById('statusIndicator').innerHTML = `<span class="w-2 h-2 rounded-full bg-emerald-500"></span> נתונים נטענו`;
                  } else {
                      alert("לא נמצאו נתונים לטווח זה");
                  }
              }
      
              function updateInsights() {
                  // Top Files
                  const fileStats = {};
                  currentData.forEach(d => {
                      const key = `${d.info.Folder} > ${d.info.Current}`;
                      if (!fileStats[key]) fileStats[key] = { name: d.info.Current, folder: d.info.Folder, totalNet: 0, count: 0 };
                      fileStats[key].totalNet += d.netSec;
                      fileStats[key].count++;
                  });
                  const topFiles = Object.values(fileStats).sort((a, b) => b.totalNet - a.totalNet).slice(0, 5);
                  const maxFileNet = topFiles[0]?.totalNet || 1;
                  document.getElementById('topFilesList').innerHTML = topFiles.map(f => {
                      const folderTitle = extensionTitlesCache[f.folder] ? ` (${extensionTitlesCache[f.folder]})` : '';
                      return `
                      <div class="space-y-1">
                          <div class="flex justify-between text-sm">
                              <span class="font-bold text-slate-700 truncate ml-4">${f.name}</span>
                              <span class="text-slate-400 font-mono text-xs">${formatSeconds(f.totalNet)}</span>
                          </div>
                          <div class="w-full bg-slate-100 h-2 rounded-full overflow-hidden">
                              <div class="bg-indigo-500 h-full" style="width: ${(f.totalNet / maxFileNet) * 100}%"></div>
                          </div>
                          <div class="text-[10px] text-slate-400">${f.folder}${folderTitle} | ${f.count} מאזינים</div>
                      </div>
                  `}).join('');
      
                  // Top Listeners
                  const listenerStats = {};
                  currentData.forEach(d => {
                      const id = hasEnterId ? d.info.EnterId : d.info.Phone;
                      const name = hasEnterId ? d.info.ValName : d.info.Phone;
                      if (!listenerStats[id]) listenerStats[id] = { name: name, id: id, totalNet: 0 };
                      listenerStats[id].totalNet += d.netSec;
                  });
                  const topListeners = Object.values(listenerStats).sort((a, b) => b.totalNet - a.totalNet).slice(0, 5);
                  const maxListenerNet = topListeners[0]?.totalNet || 1;
                  document.getElementById('topListenersList').innerHTML = topListeners.map(l => `
                      <div class="space-y-1">
                          <div class="flex justify-between text-sm">
                              <div class="flex flex-col">
                                  <span class="font-bold text-slate-700">${l.name}</span>
                                  <span class="text-[10px] text-slate-400 font-mono">${l.id}</span>
                              </div>
                              <span class="text-amber-600 font-mono text-xs font-bold">${formatSeconds(l.totalNet)}</span>
                          </div>
                          <div class="w-full bg-slate-50 h-2 rounded-full overflow-hidden">
                              <div class="bg-amber-400 h-full" style="width: ${(l.totalNet / maxListenerNet) * 100}%"></div>
                          </div>
                      </div>
                  `).join('');
              }
      
              function setupTableStructure() {
                  const headerRow = document.getElementById('header-row');
                  const filterRow = document.getElementById('filter-row');
                  
                  const commonHeaders = `
                      <th class="px-4 py-3 cursor-pointer" onclick="sortTable('folder')">שלוחה ↕</th>
                      <th class="px-4 py-3 cursor-pointer" onclick="sortTable('file')">קובץ ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('last')">האזנה אחרונה ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('length')">אורך ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('net')">נטו ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('gross')">ברוטו ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('percent')">אחוז ↕</th>
                      <th class="px-4 py-3 text-center">פעולות</th>
                  `;
                  
                  const commonFilters = `
                      <th class="px-2 py-2"><input type="text" id="f-folder" oninput="applyMultiFilter()" placeholder="סנן שלוחה..."></th>
                      <th class="px-2 py-2"><input type="text" id="f-file" oninput="applyMultiFilter()" placeholder="סנן קובץ..."></th>
                      <th></th><th></th><th></th><th></th><th></th><th></th>
                  `;
      
                  if (hasEnterId) {
                      headerRow.innerHTML = `<th class="px-4 py-3 cursor-pointer" onclick="sortTable('school')">כיתה ↕</th><th class="px-4 py-3 cursor-pointer" onclick="sortTable('name')">שם וזיהוי ↕</th>` + commonHeaders;
                      filterRow.innerHTML = `<th class="px-2 py-2"><input type="text" id="f-school" oninput="applyMultiFilter()" placeholder="סנן כיתה..."></th><th class="px-2 py-2"><input type="text" id="f-name" oninput="applyMultiFilter()" placeholder="סנן שם או זיהוי..."></th>` + commonFilters;
                  } else {
                      headerRow.innerHTML = `<th class="px-4 py-3 cursor-pointer" onclick="sortTable('name')">טלפון ↕</th>` + commonHeaders;
                      filterRow.innerHTML = `<th class="px-2 py-2"><input type="text" id="f-name" oninput="applyMultiFilter()" placeholder="סנן טלפון..."></th>` + commonFilters;
                  }
              }
      
              function renderTable() {
                  const tbody = document.getElementById('table-body');
                  const totalItems = filteredData.length;
                  const startIdx = (currentPage - 1) * rowsPerPage;
                  const pageData = filteredData.slice(startIdx, startIdx + rowsPerPage);
      
                  let html = '';
                  pageData.forEach((item, idx) => {
                      const colorClass = item.percent > 80 ? 'bg-green-gradient' : (item.percent > 40 ? 'bg-orange-gradient' : 'bg-red-gradient');
                      const textClass = item.percent > 80 ? 'text-emerald-600' : (item.percent > 40 ? 'text-amber-600' : 'text-rose-600');
                      const rowId = `row-${startIdx + idx}`;
                      const isExpanded = expandedRows.has(rowId);
                      const hasDuplicates = item.rawSessions.length > 1;
      
                      let identityCell = '';
                      if (hasEnterId) {
                          identityCell = `<td class="px-4 py-3 text-slate-500">${item.info.School || '-'}</td>
                                         <td class="px-4 py-3">
                                             <div class="font-bold text-slate-800">${item.info.ValName}</div>
                                             <div class="text-[10px] text-slate-400">${item.info.EnterId}</div>
                                         </td>`;
                      } else {
                          identityCell = `<td class="px-4 py-3 font-bold text-slate-800">${item.info.Phone}</td>`;
                      }
      
                      html += `
                          <tr class="hover:bg-slate-50 transition-colors">
                              ${identityCell}
                              <td class="px-4 py-3">
                                  <div class="flex flex-col">
                                      <span class="bg-slate-100 px-2 py-1 rounded text-[10px] font-bold text-slate-500 w-fit">${item.info.Folder}</span>
                                      ${extensionTitlesCache[item.info.Folder] ? `<span class="text-[10px] text-indigo-600 font-medium mt-1 leading-tight">${extensionTitlesCache[item.info.Folder]}</span>` : ''}
                                  </div>
                              </td>
                              <td class="px-4 py-3 text-slate-600 truncate max-w-[150px]">${item.info.Current}</td>
                              <td class="px-4 py-3 text-center">
                                  <div class="text-[11px] font-bold text-slate-700">${item.lastDate}</div>
                                  <div class="text-[10px] text-slate-400 font-mono">${item.lastTime}</div>
                              </td>
                              <td class="px-4 py-3 text-center font-mono text-slate-400">${formatSeconds(item.flenSec)}</td>
                              <td class="px-4 py-3 text-center font-mono text-indigo-600 font-bold">${formatSeconds(item.netSec)}</td>
                              <td class="px-4 py-3 text-center font-mono text-slate-400 text-xs">${formatSeconds(item.grossSec)}</td>
                              <td class="px-4 py-3">
                                  <div class="flex items-center gap-3 min-w-[100px]">
                                      <div class="flex-1 bg-slate-100 rounded-full h-1.5 overflow-hidden">
                                          <div class="progress-fill h-full ${colorClass}" style="width: ${item.percent}%"></div>
                                      </div>
                                      <span class="text-xs font-bold ${textClass}">${item.percent}%</span>
                                  </div>
                              </td>
                              <td class="px-4 py-3 text-center">
                                  <button onclick="toggleRow('${rowId}')" class="flex items-center gap-1 mx-auto px-3 py-1.5 rounded-lg text-[10px] font-bold transition-all shadow-sm ${hasDuplicates ? 'bg-indigo-600 text-white hover:bg-indigo-700' : 'bg-slate-100 text-slate-300 cursor-default'}">
                                      <i data-lucide="${isExpanded ? 'chevron-up' : 'list-plus'}" class="w-3.5 h-3.5"></i>
                                      ${isExpanded ? 'סגור' : 'פירוט האזנות'}
                                  </button>
                              </td>
                          </tr>
                      `;
      
                      if (isExpanded) {
                          html += `
                              <tr class="detail-row animate-fade-in">
                                  <td colspan="12" class="px-8 py-4">
                                      <div class="bg-white rounded-xl border border-indigo-100 shadow-sm overflow-hidden">
                                          <div class="bg-indigo-50 px-4 py-2 text-[10px] font-bold text-indigo-600 border-b border-indigo-100">פירוט האזנות כפולות / חוזרות</div>
                                          <table class="w-full text-[11px] text-right">
                                              <thead class="bg-slate-50 text-slate-400 font-bold uppercase">
                                                  <tr>
                                                      <th class="px-4 py-2">תאריך</th>
                                                      <th class="px-4 py-2">שעה</th>
                                                      <th class="px-4 py-2">התחלה בקובץ</th>
                                                      <th class="px-4 py-2">סיום בקובץ</th>
                                                      <th class="px-4 py-2">משך האזנה</th>
                                                  </tr>
                                              </thead>
                                              <tbody class="divide-y divide-slate-50">
                                                  ${item.rawSessions.map(s => `
                                                      <tr>
                                                          <td class="px-4 py-2">${s.date}</td>
                                                          <td class="px-4 py-2">${s.time}</td>
                                                          <td class="px-4 py-2 font-mono">${formatSeconds(s.start)}</td>
                                                          <td class="px-4 py-2 font-mono">${formatSeconds(s.stop)}</td>
                                                          <td class="px-4 py-2 font-bold text-indigo-600">${formatSeconds(s.duration)}</td>
                                                      </tr>
                                                  `).join('')}
                                              </tbody>
                                          </table>
                                      </div>
                                  </td>
                              </tr>
                          `;
                      }
                  });
                  
                  tbody.innerHTML = html || `<tr><td colspan="12" class="p-10 text-center text-slate-400">אין תוצאות</td></tr>`;
                  renderPaginationControls(totalItems, Math.ceil(totalItems / rowsPerPage));
                  lucide.createIcons();
              }
      
              function toggleRow(id) {
                  if (expandedRows.has(id)) expandedRows.delete(id);
                  else expandedRows.add(id);
                  renderTable();
              }
      
              function applyMultiFilter() {
                  const search = removeNikud(document.getElementById('searchInput').value.toLowerCase());
                  const minP = parseInt(document.getElementById('filter-percent').value) || 0;
                  
                  const fSchool = removeNikud(document.getElementById('f-school')?.value.toLowerCase() || "");
                  const fName = removeNikud(document.getElementById('f-name')?.value.toLowerCase() || "");
                  const fFolder = removeNikud(document.getElementById('f-folder')?.value.toLowerCase() || "");
                  const fFile = removeNikud(document.getElementById('f-file')?.value.toLowerCase() || "");
      
                  filteredData = currentData.filter(item => {
                      const tSchool = removeNikud((item.info.School || "").toLowerCase());
                      const tName = removeNikud((hasEnterId ? (item.info.ValName || "") : item.info.Phone).toLowerCase());
                      const tFolder = removeNikud(item.info.Folder.toLowerCase());
                      const tFolderTitle = removeNikud((extensionTitlesCache[item.info.Folder] || "").toLowerCase());
                      const tFile = removeNikud(item.info.Current.toLowerCase());
                      const tId = removeNikud((item.info.EnterId || "").toLowerCase());
                      
                      const matchSearch = tName.includes(search) || tFolder.includes(search) || tFile.includes(search) || tId.includes(search) || tFolderTitle.includes(search);
                      const matchPercent = item.percent >= minP;
                      
                      const matchSpecific = tSchool.includes(fSchool) && 
                                          (tName.includes(fName) || tId.includes(fName)) && 
                                          (tFolder.includes(fFolder) || tFolderTitle.includes(fFolder)) && 
                                          tFile.includes(fFile);
      
                      return matchSearch && matchPercent && matchSpecific;
                  });
      
                  document.getElementById('statFiltered').innerText = `${filteredData.length.toLocaleString()} רשומות מוצגות`;
                  currentPage = 1;
                  renderTable();
              }
      
              function updateStats() {
                  const total = currentData.length;
                  const avg = total > 0 ? Math.round(currentData.reduce((s, d) => s + d.percent, 0) / total) : 0;
                  const netSec = currentData.reduce((s, d) => s + d.netSec, 0);
                  const grossSec = currentData.reduce((s, d) => s + d.grossSec, 0);
      
                  document.getElementById('statTotal').innerText = total.toLocaleString();
                  document.getElementById('statAvg').innerText = `${avg}%`;
                  document.getElementById('statNetHours').innerText = Math.round(netSec / 3600).toLocaleString();
                  document.getElementById('statGrossHours').innerText = Math.round(grossSec / 3600).toLocaleString();
              }
      
              function renderPaginationControls(totalItems, totalPages) {
                  const container = document.getElementById('page-numbers');
                  const info = document.getElementById('pagination-info');
                  if (totalItems === 0) { container.innerHTML = ''; info.innerText = ''; return; }
      
                  info.innerText = `מציג ${((currentPage-1)*rowsPerPage)+1}-${Math.min(currentPage*rowsPerPage, totalItems)} מתוך ${totalItems}`;
                  let btns = `<button onclick="changePage(${currentPage-1})" class="page-btn" ${currentPage===1?'disabled':''}><i data-lucide="chevron-right" class="w-4 h-4"></i></button>`;
                  for (let i = 1; i <= totalPages; i++) {
                      if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
                          btns += `<button onclick="changePage(${i})" class="page-btn ${i===currentPage?'active':''}">${i}</button>`;
                      } else if (i === currentPage - 3 || i === currentPage + 3) {
                          btns += `<span class="px-1 text-slate-300">...</span>`;
                      }
                  }
                  btns += `<button onclick="changePage(${currentPage+1})" class="page-btn" ${currentPage===totalPages?'disabled':''}><i data-lucide="chevron-left" class="w-4 h-4"></i></button>`;
                  container.innerHTML = btns;
              }
      
              function changePage(p) { currentPage = p; renderTable(); }
              function updateRowsPerPage() { rowsPerPage = parseInt(document.getElementById('rows-per-page').value); currentPage = 1; renderTable(); }
      
              function sortTable(key) {
                  const idx = ['school','name','folder','file','last','length','net','gross','percent'].indexOf(key);
                  sortDirections[idx] = !sortDirections[idx];
                  const dir = sortDirections[idx] ? 1 : -1;
                  
                  currentData.sort((a, b) => {
                      let v1, v2;
                      if (key === 'school') { v1 = a.info.School || ""; v2 = b.info.School || ""; }
                      else if (key === 'name') { v1 = (hasEnterId ? a.info.ValName : a.info.Phone) || ""; v2 = (hasEnterId ? b.info.ValName : b.info.Phone) || ""; }
                      else if (key === 'folder') { v1 = a.info.Folder; v2 = b.info.Folder; }
                      else if (key === 'file') { v1 = a.info.Current; v2 = b.info.Current; }
                      else if (key === 'last') { v1 = `${a.lastDate} ${a.lastTime}`; v2 = `${b.lastDate} ${b.lastTime}`; }
                      else if (key === 'length') { v1 = a.flenSec; v2 = b.flenSec; }
                      else if (key === 'net') { v1 = a.netSec; v2 = b.netSec; }
                      else if (key === 'gross') { v1 = a.grossSec; v2 = b.grossSec; }
                      else if (key === 'percent') { v1 = a.percent; v2 = b.percent; }
                      return v1 < v2 ? -1 * dir : (v1 > v2 ? 1 * dir : 0);
                  });
                  applyMultiFilter();
              }
      
              function timeToSeconds(t){
                  const p = t.trim().split(':').map(Number);
                  return p.length === 3 ? p[0]*3600 + p[1]*60 + p[2] : (p.length === 2 ? p[0]*60 + p[1] : 0);
              }
              function formatSeconds(s){
                  let h=Math.floor(s/3600), m=Math.floor((s%3600)/60), sec=Math.floor(s%60);
                  return `${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}:${sec.toString().padStart(2,'0')}`;
              }
              function mergeIntervals(arr){
                  if(!arr.length) return 0;
                  arr.sort((a,b) => a[0]-b[0]);
                  let total = 0, [start, end] = arr[0];
                  for(let i=1; i<arr.length; i++){
                      if(arr[i][0] <= end) end = Math.max(end, arr[i][1]);
                      else { total += (end - start); [start, end] = arr[i]; }
                  }
                  return total + (end - start);
              }
              function getDatesRange(s,e){
                  const d=[]; let c=new Date(s);
                  while(c<=e){ d.push(c.toISOString().split('T')[0]); c.setDate(c.getDate()+1); }
                  return d;
              }
      
              function exportToExcel() {
                  const exportedData = filteredData.map(item => {
                      let obj = {};
                      if (hasEnterId) {
                          obj["כיתה"] = item.info.School || '-';
                          obj["שם תלמיד"] = item.info.ValName || '';
                          obj["זיהוי"] = item.info.EnterId || '';
                          obj["שלוחה"] = item.info.Folder + (extensionTitlesCache[item.info.Folder] ? ` (${extensionTitlesCache[item.info.Folder]})` : '');
                          obj["שם הקובץ"] = item.info.Current;
                          obj["תאריך האזנה אחרונה"] = item.lastDate;
                          obj["שעת האזנה אחרונה"] = item.lastTime;
                          obj["אורך קובץ"] = formatSeconds(item.flenSec);
                          obj["האזנה נטו"] = formatSeconds(item.netSec);
                          obj["האזנה ברוטו"] = formatSeconds(item.grossSec);
                          obj["אחוז האזנה"] = item.percent + '%';
                      } else {
                          obj["טלפון"] = item.info.Phone;
                          obj["שלוחה"] = item.info.Folder + (extensionTitlesCache[item.info.Folder] ? ` (${extensionTitlesCache[item.info.Folder]})` : '');
                          obj["שם הקובץ"] = item.info.Current;
                          obj["תאריך האזנה אחרונה"] = item.lastDate;
                          obj["שעת האזנה אחרונה"] = item.lastTime;
                          obj["אורך קובץ"] = formatSeconds(item.flenSec);
                          obj["האזנה נטו"] = formatSeconds(item.netSec);
                          obj["האזנה ברוטו"] = formatSeconds(item.grossSec);
                          obj["אחוז האזנה"] = item.percent + '%';
                      }
                      return obj;
                  });
                  const ws = XLSX.utils.json_to_sheet(exportedData);
                  const wb = XLSX.utils.book_new();
                  XLSX.utils.book_append_sheet(wb, ws, "דוח האזנה");
                  XLSX.writeFile(wb, `דוח_האזנה_${new Date().toISOString().slice(0,10)}.xlsx`);
              }
          </script>
      </body>
      </html>
      
      

      פורסם בפורום מפתחים API
      י
      יב
    • RE: ביטול חיוג למספר המשני

      @יאיר-32 לפי הפילטר הזה

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

      @יעקב-יצחק אולי תנסה עם זה
      קרדיט @ע.ג.

      פורסם בבאגים במערכת
      י
      יב
    • RE: מערכת מכירות לגוגל שיטס

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

      פורסם בשאלות ועזרה הדדית
      י
      יב