⬅ חזרה לתיקייה
✏️ עריכת 20260704_205726_index.html
C:\TripServer\backups\uploaded_files\MoneyHome_main\20260704_205726_index.html
<!doctype html> <html lang="he" dir="rtl"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <title>ניהול תקציב משפחתי</title> <style> :root{ --bg:#f5f7fb; --card:#ffffff; --ink:#172033; --muted:#667085; --line:#e5e7ef; --good:#12805c; --bad:#c62828; --warn:#b26a00; --brand:#2947d3; --soft:#eef2ff; --radius:18px; --shadow:0 10px 30px rgba(20,30,60,.08); } *{box-sizing:border-box} body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;background:var(--bg);color:var(--ink)} header{position:sticky;top:0;z-index:20;background:rgba(245,247,251,.94);backdrop-filter:blur(14px);border-bottom:1px solid var(--line)} .top{max-width:1160px;margin:auto;padding:14px 16px;display:flex;gap:10px;align-items:center;justify-content:space-between} .brand{font-weight:900;font-size:20px} .small{font-size:12px;color:var(--muted)} main{max-width:1160px;margin:auto;padding:16px 14px 92px} .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px} .grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:12px} .card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:16px} .stat .label{font-size:13px;color:var(--muted);margin-bottom:6px} .stat .value{font-size:24px;font-weight:900;direction:ltr;text-align:right} .good{color:var(--good)} .bad{color:var(--bad)} .warn{color:var(--warn)} .toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:12px 0} button,select,input{font:inherit;border-radius:12px;border:1px solid var(--line);background:#fff;color:var(--ink)} button{padding:10px 13px;font-weight:800;cursor:pointer} button.primary{background:var(--brand);color:#fff;border-color:var(--brand)} button.ghost{background:var(--soft);color:var(--brand);border-color:#dbe3ff} button.danger{background:#fff2f2;color:var(--bad);border-color:#ffd0d0} button.goodbtn{background:#ecfdf3;color:var(--good);border-color:#b7ebc7} input,select{padding:10px 11px;min-width:0} .form{display:grid;grid-template-columns:2fr 1fr 1fr 1fr 1fr auto;gap:8px;align-items:end} .form.compact{grid-template-columns:2fr 1fr 1fr auto} .field label{display:block;font-size:12px;color:var(--muted);margin:0 0 5px} .tabs{display:flex;gap:8px;overflow:auto;padding-bottom:2px} .tab{white-space:nowrap} .tab.active{background:var(--brand);color:white;border-color:var(--brand)} table{width:100%;border-collapse:separate;border-spacing:0 8px} th{font-size:12px;color:var(--muted);text-align:right;font-weight:800;padding:0 8px} td{background:#fff;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:10px 8px;vertical-align:middle} tr td:first-child{border-right:1px solid var(--line);border-radius:0 12px 12px 0} tr td:last-child{border-left:1px solid var(--line);border-radius:12px 0 0 12px} .amount{direction:ltr;text-align:right;font-weight:900} .pill{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);border-radius:999px;padding:5px 9px;background:#fafafa;font-size:12px;color:var(--muted)} .pill.good{background:#ecfdf3;border-color:#b7ebc7;color:var(--good)} .pill.bad{background:#fff2f2;border-color:#ffd0d0;color:var(--bad)} .pill.warn{background:#fff8e1;border-color:#ffe2a8;color:var(--warn)} .section-title{display:flex;justify-content:space-between;gap:10px;align-items:center;margin-bottom:8px} h2{font-size:18px;margin:0} .muted{color:var(--muted)} .row-actions{display:flex;gap:6px;justify-content:flex-end} .notice{border-radius:14px;padding:12px 14px;background:#fff8e1;border:1px solid #ffe2a8;color:#6b4300;margin:10px 0} .hidden{display:none!important} .bottom-nav{position:fixed;bottom:0;right:0;left:0;background:rgba(255,255,255,.94);backdrop-filter:blur(12px);border-top:1px solid var(--line);z-index:30} .bottom-nav .inner{max-width:1160px;margin:auto;display:grid;grid-template-columns:repeat(5,1fr);gap:6px;padding:8px} .bottom-nav button{font-size:12px;padding:9px 4px} .progress{height:10px;background:#edf0f7;border-radius:999px;overflow:hidden;margin-top:8px} .bar{height:100%;background:var(--good);width:0%} .bar.warnbg{background:var(--warn)} .bar.badbg{background:var(--bad)} .two{display:grid;grid-template-columns:1.05fr .95fr;gap:12px} .login{max-width:520px;margin:40px auto} canvas{max-width:100%;height:auto} .chartWrap{height:280px;display:flex;align-items:center;justify-content:center} .print-only{display:none} .archive-card{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;border:1px solid var(--line);border-radius:14px;padding:12px;margin:8px 0;background:#fff} .income-amount-edit,.income-name-edit, .limit-amount-edit,.limit-name-edit,.limit-group-edit, .temp-name-edit,.temp-card-edit,.temp-amount-edit,.temp-progress-edit input{ width:100%;max-width:260px;background:#f8fafc;border:1px solid var(--line);border-radius:10px;padding:9px } .income-amount-edit,.limit-amount-edit,.temp-amount-edit,.temp-progress-edit input{direction:ltr;text-align:right;font-weight:900} .income-amount-edit{color:var(--good)} .limit-amount-edit,.temp-amount-edit{color:var(--brand)} .temp-card-edit{color:#172033} .temp-progress-edit{display:flex;align-items:center;gap:6px} .temp-progress-edit input{max-width:72px} @media(max-width:860px){ .grid,.grid3{grid-template-columns:repeat(2,1fr)} .two{grid-template-columns:1fr} .form,.form.compact{grid-template-columns:1fr 1fr} .form .wide{grid-column:1/-1} table,thead,tbody,tr,td,th{display:block} thead{display:none} tr{background:#fff;border:1px solid var(--line);border-radius:14px;margin:10px 0;padding:8px} td{border:0!important;border-radius:0!important;padding:7px 4px;display:flex;justify-content:space-between;gap:12px} td:before{content:attr(data-label);font-size:12px;color:var(--muted);font-weight:800} .row-actions{justify-content:stretch} .row-actions button{flex:1} .stat .value{font-size:21px} } @media(max-width:520px){ .grid,.grid3{grid-template-columns:1fr} .top{align-items:flex-start;flex-direction:column} .bottom-nav .inner{grid-template-columns:repeat(5,1fr)} } @media print{ header,.bottom-nav,.tabs,.toolbar button,.no-print,#loginView{display:none!important} body{background:white} main{padding:0;max-width:none} .card{box-shadow:none;border:1px solid #ddd;break-inside:avoid;margin-bottom:10px} .hidden{display:none!important} .print-only{display:block} #appView{display:block!important} #tab-dashboard,#tab-limits,#tab-items,#tab-archive{display:block!important} #tab-settings{display:none!important} } .mini-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px} #tab-items .card,#tab-limits .card{font-size:inherit} #tab-items input,#tab-items select,#tab-items button,#tab-limits input,#tab-limits select,#tab-limits button{min-height:44px} @media(max-width:860px){.mini-grid{grid-template-columns:1fr}.form,.form.compact{grid-template-columns:1fr!important}.field{width:100%}} </style> <!-- EITAN_QA: dashboardTempDetails container exists and renderDashboardTempDetails is called from render() --> </head> <body> <header> <div class="top"> <div> <div class="brand">ניהול תקציב משפחתי</div> <div class="small" id="syncStatus">מצב סנכרון: לא מחובר</div> </div> <div class="toolbar"> <select id="periodPicker"></select> <button class="ghost" id="pdfBtn">דוח PDF</button> <button class="ghost" id="htmlReportBtn">דוח HTML</button> <button class="ghost" id="exportBtn">גיבוי JSON</button> <button id="signOutBtn" class="hidden">יציאה</button> </div> </div> </header> <main> <section id="loginView" class="login card"> <h2>כניסה</h2> <div class="form" style="grid-template-columns:1fr;gap:10px"> <div class="field"> <label>מייל</label> <input id="emailLoginInput" type="email" autocomplete="username" placeholder="name@gmail.com"> </div> <div class="field"> <label>סיסמה</label> <input id="passwordLoginInput" type="password" autocomplete="current-password" placeholder="סיסמה"> </div> <button class="primary" id="emailLoginBtn">כניסה עם מייל וסיסמה</button> </div> <div class="toolbar"> <button class="ghost" id="googleLoginBtn">כניסה עם Google</button> <button class="ghost" id="localModeBtn">מצב מקומי לבדיקה</button> <button class="secondary" id="cleanTestBtn">מצב בדיקה נקי</button> </div> <p class="small">מומלץ להשתמש בכניסה עם מייל וסיסמה. Google נשאר כאפשרות גיבוי.</p> </section> <section id="appView" class="hidden"> <div class="print-only"> <h1>דוח תקציב חודשי</h1> <p id="printPeriod"></p> </div> <div class="grid"> <div class="card stat"> <div class="label">מסגרת הכנסות חודשית</div> <div class="value" id="incomeTotal">₪0</div> </div> <div class="card stat"> <div class="label">תקרת תקציב</div> <div class="value" id="limitTotal">₪0</div> </div> <div class="card stat"> <div class="label">הוצאות בפועל</div> <div class="value" id="spentTotal">₪0</div> </div> <div class="card stat"> <div class="label">יתרה מול הכנסות</div> <div class="value" id="balanceTotal">₪0</div> </div> </div> <div class="card" style="margin-top:12px"> <div class="section-title"><h2>פירוט הכנסות החודש</h2></div> <div id="dashboardIncomeBreakdown" class="mini-grid"></div> </div> <div class="card" style="margin-top:12px"> <div class="section-title"> <h2>מצב החודש</h2> <span class="pill" id="monthHealth">—</span> </div> <div class="progress"><div class="bar" id="spendBar"></div></div> <p class="small" id="monthHint"></p> </div> <div class="tabs toolbar no-print"> <button class="tab active" data-tab="dashboard">דשבורד</button> <button class="tab" data-tab="items">הזנת הוצאות / הכנסות</button> <button class="tab" data-tab="limits">מסגרות תקציב</button> <button class="tab" data-tab="archive">ארכיון</button> <button class="tab" data-tab="settings">הגדרות</button> </div> <section id="tab-dashboard"> <div class="two"> <div class="card"> <div class="section-title"><h2>מצב לפי קטגוריה</h2></div> <div id="categorySummary"></div> </div> <div class="card"> <div class="section-title"><h2>תרשים עוגה לפי קטגוריות</h2></div> <div class="chartWrap"><canvas id="pieChart" width="300" height="260"></canvas></div> </div> </div> <div class="card" style="margin-top:12px"> <div class="section-title"><h2>גרף הוצאות מול מסגרת</h2></div> <div class="chartWrap"><canvas id="barChart" width="760" height="260"></canvas></div> </div> <div class="card" style="margin-top:12px"> <div class="section-title"><h2>פירוט הוצאות זמניות / הלוואות</h2></div> <p class="small">פירוט נפרד לכל התחייבות זמנית: שם, מקור חיוב, סכום חודשי, כמה מתוך כמה ויתרה עתידית.</p> <div id="dashboardTempDetails" class="grid3" style="margin-top:10px"></div> </div> <div class="card" style="margin-top:12px"> <div class="section-title"><h2>סיכום לפי מקור חיוב</h2></div> <div id="paymentSourceSummary" class="grid3" style="margin-top:10px"></div> </div> </section> <section id="tab-items" class="hidden"> <div class="card"> <div class="section-title"><h2>הוספת הוצאה בפועל</h2></div> <div class="form"> <div class="field wide"><label>פירוט</label><input id="nameInput" placeholder="לדוגמה: שופרסל / חשמל / מסעדה"></div> <div class="field"><label>סכום ₪</label><input id="amountInput" type="number" inputmode="decimal" placeholder="0"></div> <div class="field"><label>קטגוריה</label><select id="categoryInput"></select></div> <div class="field"><label>תאריך</label><input id="dateInput" type="date"></div> <div class="field"><label>אמצעי</label><select id="methodInput"> <option>בנק</option> <option>אשראי</option> <option>ויזה בנק הפועלים — איתן</option> <option>ויזה חוץ בנקאית — קרין</option> <option>מזומן</option> <option>אחר</option> </select></div> <button class="primary" id="addBtn">שמירה</button> </div> </div> <div class="card" style="margin-top:12px"> <div class="section-title"><h2>עדכון הכנסות החודש</h2></div> <p class="small">כאן מעדכנים את המשכורות בפועל לאותו חודש ומוסיפים הכנסה חד־פעמית לחודש בלבד.</p> <div id="monthlyIncomeQuickEdit" class="mini-grid"></div> <div class="form compact" style="margin-top:12px"> <div class="field wide"><label>שם הכנסה חד־פעמית</label><input id="quickExtraName" placeholder="לדוגמה: בונוס / עבודה נוספת"></div> <div class="field"><label>סכום ₪</label><input id="quickExtraAmount" type="number" inputmode="decimal" placeholder="0"></div> <div class="field"><label>שייך ל</label><select id="quickExtraOwner"><option>איתן</option><option>קרין</option><option>משפחה</option></select></div> <button class="primary" id="quickAddExtraBtn">הוסף הכנסה לחודש</button> </div> </div> <div class="card" style="margin-top:12px"> <div class="section-title"> <h2>הוצאות החודש בפועל</h2> <span class="pill" id="itemsCount">0 הוצאות</span> </div> <p class="small">הוצאות קבועות ותשלומים זמניים נכנסים אוטומטית בכל חודש. הוצאות ידניות מתווספות בנוסף.</p> <table> <thead><tr><th>פירוט</th><th>קטגוריה</th><th>תאריך</th><th>אמצעי</th><th>סכום</th><th></th></tr></thead> <tbody id="itemsTable"></tbody> </table> </div> </section> <section id="tab-limits" class="hidden"> <div class="card"> <div class="section-title"><h2>הכנסות חודשיות בסיס</h2></div> <p class="small">אפשר לערוך ישירות את שם ההכנסה או הסכום. שינוי כאן משפיע על בסיס התקציב, עד שתשנה שוב.</p> <div class="form compact"> <div class="field wide"><label>שם הכנסה</label><input id="incomeName" placeholder="לדוגמה: משכורת"></div> <div class="field"><label>סכום ₪</label><input id="incomeAmount" type="number" inputmode="decimal" placeholder="0"></div> <div class="field"><label>סוג</label><select id="incomeType"><option>משכורת</option><option>הכנסה נוספת</option></select></div> <button class="primary" id="addIncomeBtn">הוספה</button> </div> <table> <thead><tr><th>שם</th><th>סוג</th><th>סכום</th><th></th></tr></thead> <tbody id="incomeTable"></tbody> </table> </div> <div class="card" style="margin-top:12px"> <div class="section-title"><h2>הכנסה נוספת לחודש הנוכחי בלבד</h2></div> <p class="small">כאן מוסיפים בונוס/עבודה נוספת/השלמה חד־פעמית. זה משפיע רק על החודש הנוכחי ולא משנה את הכנסות הבסיס.</p> <div class="form compact"> <div class="field wide"><label>שם הכנסה</label><input id="extraIncomeName" placeholder="לדוגמה: עבודה נוספת איתן / בונוס קרין"></div> <div class="field"><label>סכום ₪</label><input id="extraIncomeAmount" type="number" inputmode="decimal" placeholder="0"></div> <div class="field"><label>שייך ל</label><select id="extraIncomeOwner"><option>איתן</option><option>קרין</option><option>משפחה</option></select></div> <button class="primary" id="addExtraIncomeBtn">הוספה לחודש</button> </div> <table> <thead><tr><th>שם</th><th>שייך ל</th><th>סכום</th><th></th></tr></thead> <tbody id="extraIncomeTable"></tbody> </table> </div> <div class="card" style="margin-top:12px"> <div class="section-title"><h2>מסגרות תקציב לפי קטגוריה</h2></div> <p class="small">אפשר לערוך ישירות שם, קטגוריה ותקרה. שורת “הוצאות זמניות בכ.א” מחושבת אוטומטית מתוך התשלומים הזמניים.</p> <div class="form compact"> <div class="field wide"><label>שם מסגרת</label><input id="limitName" placeholder="לדוגמה: קניות סופר"></div> <div class="field"><label>סכום ₪</label><input id="limitAmount" type="number" inputmode="decimal" placeholder="0"></div> <div class="field"><label>קטגוריית על</label><select id="limitGroup"></select></div> <button class="primary" id="addמסגרתBtn">הוספה</button> </div> <table> <thead><tr><th>מסגרת</th><th>קטגוריה</th><th>מקור חיוב</th><th>תקרה</th><th>בפועל</th><th>מצב</th><th></th></tr></thead> <tbody id="limitsTable"></tbody> </table> </div> <div class="card" style="margin-top:12px"> <div class="section-title"><h2>הוצאות זמניות בכרטיס אשראי</h2></div> <p class="small">אפשר לערוך ישירות שם, כרטיס/מקור חיוב, סכום חודשי, תשלום נוכחי וסך תשלומים.</p> <div id="tempCards" class="grid3" style="margin:10px 0"></div> <div class="form compact"> <div class="field wide"><label>שם</label><input id="tempName" placeholder="לדוגמה: רובוט שואב"></div> <div class="field"><label>תשלום חודשי ₪</label><input id="tempAmount" type="number" inputmode="decimal"></div> <div class="field"><label>תשלום נוכחי / סה״כ</label><input id="tempProgress" placeholder="7/10"></div> <button class="primary" id="addTempBtn">הוספה</button> </div> <table> <thead><tr><th>שם</th><th>כרטיס / מקור חיוב</th><th>תשלום חודשי</th><th>כמה מתוך כמה</th><th>יתרה עתידית</th><th></th></tr></thead> <tbody id="tempTable"></tbody> </table> </div> </section> <section id="tab-archive" class="hidden"> <div class="card"> <div class="section-title"><h2>סגירת חודש והתחלה חדשה</h2></div> <p class="muted">סגירת חודש שומרת Snapshot בארכיון ואז פותחת חודש תקציבי חדש וריק להזנת הוצאות בפועל. ה־מסגרתs וההכנסות נשארים כבסיס לחודש הבא.</p> <div class="toolbar"> <button class="danger" id="closeMonthBtn">סגור חודש והתחל חדש</button> <button class="ghost" id="manualResetBtn">איפוס הוצאות החודש בלי ארכיון</button> </div> </div> <div class="card" style="margin-top:12px"> <div class="section-title"><h2>חודשים קודמים</h2></div> <div id="archiveList"></div> </div> </section> <section id="tab-settings" class="hidden"> <div class="card"> <div class="section-title"><h2>הגדרות מחזור תקציב</h2></div> <div class="form compact"> <div class="field wide"> <label>מחזור חודש</label> <select id="cycleMode"> <option value="2">2 לחודש עד 2 לחודש הבא</option> <option value="10">10 לחודש עד 10 לחודש הבא</option> <option value="15">15 לחודש עד 15 לחודש הבא</option> <option value="custom">מותאם אישית</option> </select> </div> <div class="field"><label>יום התחלה מותאם</label><input id="customCycleDay" type="number" min="1" max="28" value="1"></div> <div class="field"><label>איפוס אוטומטי</label><select id="autoReset"><option value="false">כבוי</option><option value="true">פעיל</option></select></div> <button class="primary" id="saveSettingsBtn">שמירה</button> </div> <p class="small">איפוס אוטומטי יסגור חודש לפי יום המחזור שבחרת. איפוס ידני תמיד זמין.</p> </div> </section> </section> </main> <nav class="bottom-nav hidden no-print" id="bottomNav"> <div class="inner"> <button onclick="BudgetApp.showTab('dashboard')">דשבורד</button> <button onclick="BudgetApp.showTab('items')">הוצאות / הכנסות</button> <button onclick="BudgetApp.showTab('limits')">מסגרות תקציב</button> <button onclick="BudgetApp.showTab('archive')">ארכיון</button> <button onclick="BudgetApp.showTab('settings')">הגדרות</button> </div> </nav> <script type="module"> const firebaseConfig = { apiKey: "AIzaSyDJVmdnQw6ZV3sFOc04EMkYob735XATKBM", authDomain: "vaction-budget.firebaseapp.com", projectId: "vaction-budget", storageBucket: "vaction-budget.firebasestorage.app", messagingSenderId: "1039853800467", appId: "1:1039853800467:web:6ab326be5e04fe8bbc43e6", measurementId: "G-35QMXX5D01" }; const allowedEmails = [ "eitan.fridman@gmail.com", "eitan.friedman@gmail.com", "Karin.enoshi@gmail.com", "karin.enoshi@gmail.com" ]; const APP_ID = "householdBudgetClean2026"; const HOUSEHOLD_ID = "fridmanCleanFinal"; const FIXED_LOCKED_LIMITS = new Set([ "ארנונה", "ועד בית", "נטפליקס", "מנוי פיס", "דמי כיס ילדים", "איתוראן", "iCloud+ 2TB", "Apple Music", "ChatGPT Plus", "סים מצלמת רכב", "019 סלולר", "סלקום טריפל", "חוג עידו", "חסכונות חודשיים" ,"משכנתא","ביטוח חיים מנורה","ביטוח רפואי פרטי משפחתי — כלל","ביטוח רפואי פרטי מיכאל — מנורה","ביטוח מחלות קשות ילדים — הראל", "קו סלולר קרין — סלקום"]); let firebaseReady = false, db = null, auth = null, currentUser = null, unsubscribe = null; let localMode = false; let cleanTestMode = false; const defaultState = () => ({ schemaVersion: 200, activePeriod: periodKeyForDate(new Date(), 10), expenses: [], monthlyExtraIncomes: [], incomes: [ {id: uid(), name:"משכורת איתן", type:"משכורת", amount:14500}, {id: uid(), name:"משכורת קרין — עיריית תל אביב", type:"משכורת", amount:13500}, {id: uid(), name:"משכורת קרין — קרן מחקרים", type:"משכורת", amount:8500}, {id: uid(), name:"ביטוח לאומי ילדים", type:"הכנסה נוספת", amount:276} ], groups: ["בית ודיור","ביטוחים ובריאות","רכב ותחבורה","ילדים וחינוך","תקשורת ומנויים","פנאי ושונות","מזון וסופר","רזרבה / בלתי צפוי","חסכונות חודשיים","הוצאות זמניות בכ.א"], limits: [ ["משכנתא","בית ודיור",8256,"בנק"], ["ועד בית","בית ודיור",800,"בנק"], ["ארנונה","בית ודיור",1111,"בנק"], ["גז","בית ודיור",300,"אשראי"], ["חשמל","בית ודיור",550,"אשראי"], ["מים","בית ודיור",400,"אשראי"], ["ביטוח מחלות קשות ילדים — הראל","ביטוחים ובריאות",40,"אשראי"], ["ביטוח רפואי פרטי משפחתי — כלל","ביטוחים ובריאות",422,"בנק"], ["ביטוח רפואי פרטי מיכאל — מנורה","ביטוחים ובריאות",98,"אשראי"], ["ביטוח חיים מנורה","ביטוחים ובריאות",320,"אשראי"], ["ביטוח מבנה מנורה","ביטוחים ובריאות",122,"אשראי"], ["קופת חולים","ביטוחים ובריאות",500,"בנק"], ["תרופות","ביטוחים ובריאות",700,"אשראי"], ["דלק","רכב ותחבורה",650,"אשראי"], ["איתוראן","רכב ותחבורה",52,"אשראי"], ["רב־קו ילדים","רכב ותחבורה",100,"אשראי"], ["ביטוח רכב חובה — הראל","רכב ותחבורה",126,"אשראי"], ["ביטוח מקיף — הראל","רכב ותחבורה",706,"אשראי"], ["טיפול שנתי לרכב","רכב ותחבורה",200,"אשראי"], ["טסט שנתי","רכב ותחבורה",460,"אשראי"], ["חוג עידו","ילדים וחינוך",550,"אשראי"], ["הוצאות בית ספר","ילדים וחינוך",200,"אשראי"], ["דמי כיס ילדים","ילדים וחינוך",200,"אשראי"], ["סלקום טריפל","תקשורת ומנויים",200,"אשראי"], ["קו סלולר קרין — סלקום","תקשורת ומנויים",10,"אשראי"], ["נטפליקס","תקשורת ומנויים",70,"אשראי"], ["019 סלולר","תקשורת ומנויים",71,"אשראי"], ["סים מצלמת רכב","תקשורת ומנויים",27,"אשראי"], ["Apple Music","תקשורת ומנויים",35,"אשראי"], ["iCloud+ 2TB","תקשורת ומנויים",40,"אשראי"], ["ChatGPT Plus","תקשורת ומנויים",70,"אשראי"], ["מנוי פיס","פנאי ושונות",60,"אשראי"], ["תספורות","פנאי ושונות",350,"אשראי"], ["קניות סופר","מזון וסופר",4500,"אשראי"], ["בלת״מים","רזרבה / בלתי צפוי",1000,"אשראי"], ["חסכונות חודשיים","חסכונות חודשיים",8900,"בנק"], ["הוצאות זמניות בכ.א","הוצאות זמניות בכ.א",2211.5,"אשראי"] ].map(x => ({id: uid(), name:x[0], group:x[1], amount:x[2], source:x[3] || "אשראי"})), savingsBreakdown: [ {name:"רכב", amount:3000},{name:"עידו חתונה", amount:1200},{name:"מיכאל חתונה", amount:200},{name:"מיכאל בר מצווה", amount:1000},{name:"חופשה", amount:3000},{name:"עידו חשבון בנק", amount:100},{name:"מפתח", amount:400} ], tempPayments: [ {id: uid(), name:"הלוואת רכב", card:"הוראת קבע / בנק", amount:1694, paid:8, total:36}, {id: uid(), name:"שואב רובוטי", card:"ויזה בנק הפועלים — איתן", amount:442.5, paid:7, total:10}, {id: uid(), name:"ביטוח למסך סלון", card:"ויזה חוץ בנקאית — קרין", amount:75, paid:31, total:36} ], settings: { cycleMode:"10", customCycleDay:1, autoReset:false }, archive: [] }); let state = defaultState(); const $ = id => document.getElementById(id); const money = n => new Intl.NumberFormat("he-IL",{style:"currency",currency:"ILS",maximumFractionDigits: n % 1 ? 1 : 0}).format(Number(n||0)); function uid(){ return crypto.randomUUID ? crypto.randomUUID() : String(Date.now()) + Math.random().toString(16).slice(2); } const today = () => new Date().toISOString().slice(0,10); function cycleDay(){ return state.settings.cycleMode === "custom" ? Number(state.settings.customCycleDay||1) : Number(state.settings.cycleMode||10); } function periodKeyForDate(date, startDay){ const d = new Date(date); let y = d.getFullYear(), m = d.getMonth(); if(d.getDate() < startDay) m -= 1; const s = new Date(y, m, startDay); return s.toISOString().slice(0,10); } function periodLabel(key){ const s = new Date(key+"T00:00:00"); const e = new Date(s.getFullYear(), s.getMonth()+1, s.getDate()); return `${s.toLocaleDateString("he-IL")} עד ${e.toLocaleDateString("he-IL")}`; } function setStatus(txt){ $("syncStatus").textContent = "מצב סנכרון: " + txt; } async function initFirebase(){ const appMod = await import("https://www.gstatic.com/firebasejs/10.12.5/firebase-app.js"); const authMod = await import("https://www.gstatic.com/firebasejs/10.12.5/firebase-auth.js"); const fsMod = await import("https://www.gstatic.com/firebasejs/10.12.5/firebase-firestore.js"); window.FirebaseBudget = {appMod, authMod, fsMod}; const app = appMod.initializeApp(firebaseConfig); auth = authMod.getAuth(app); db = fsMod.getFirestore(app); firebaseReady = true; authMod.onAuthStateChanged(auth, user => { const allowed = allowedEmails.map(e => e.toLowerCase()); if(user && allowed.includes((user.email || "").toLowerCase())){ currentUser = user; enterApp(false); return; } if(user){ const userEmail = user.email || "לא זוהה"; authMod.signOut(auth); alert("המייל שנכנס אינו מורשה לאפליקציה: " + userEmail); } }); } async function emailLogin(){ if(!firebaseReady){ alert("Firebase לא מוכן."); return; } const email = $("emailLoginInput").value.trim(); const password = $("passwordLoginInput").value; if(!email || !password){ alert("צריך להזין מייל וסיסמה."); return; } const btn = $("emailLoginBtn"); if(btn){ btn.disabled = true; btn.textContent = "מתחבר..."; } const {authMod} = window.FirebaseBudget; try { await authMod.signInWithEmailAndPassword(auth, email, password); } catch (err) { console.error("Email/password login error:", err); alert("שגיאת כניסה עם מייל וסיסמה: " + (err.message || err.code || err)); } finally { if(btn){ btn.disabled = false; btn.textContent = "כניסה עם מייל וסיסמה"; } } } let googleLoginRunning = false; async function googleLogin(){ if(!firebaseReady){ alert("Firebase לא מוכן."); return; } if(googleLoginRunning){ return; } googleLoginRunning = true; const btn = $("googleLoginBtn"); if(btn){ btn.disabled = true; btn.textContent = "מתחבר..."; } const {authMod} = window.FirebaseBudget; const provider = new authMod.GoogleAuthProvider(); const loginTimeout = setTimeout(() => { if(googleLoginRunning && btn){ btn.disabled = false; btn.textContent = "כניסה עם Google"; } }, 12000); try { await authMod.signInWithPopup(auth, provider); clearTimeout(loginTimeout); } catch (err) { clearTimeout(loginTimeout); console.error("Google popup login error:", err); alert("שגיאת התחברות Google: " + (err.message || err.code || err)); } finally { googleLoginRunning = false; if(btn){ btn.disabled = false; btn.textContent = "כניסה עם Google"; } } } function enterApp(isLocal){ localMode = isLocal; $("loginView").classList.add("hidden"); $("appView").classList.remove("hidden"); $("bottomNav").classList.remove("hidden"); $("signOutBtn").classList.toggle("hidden", isLocal); $("dateInput").value = today(); if(isLocal){ setStatus("מקומי במכשיר"); loadLocal(); afterLoad(); render(); } else { setStatus("מחובר לענן"); listenCloud(); } } function enterCleanTestMode(){ cleanTestMode = true; localMode = true; state = defaultState(); state.expenses = []; state.monthlyExtraIncomes = []; state.archive = []; $("loginView").classList.add("hidden"); $("appView").classList.remove("hidden"); $("bottomNav").classList.remove("hidden"); $("signOutBtn").classList.add("hidden"); setStatus("מצב בדיקה נקי"); afterLoad(); render(); } function docPath(){ return ["apps", APP_ID, "households", HOUSEHOLD_ID, "main", "state"]; } async function listenCloud(){ const {fsMod} = window.FirebaseBudget; const ref = fsMod.doc(db, ...docPath()); if(unsubscribe) unsubscribe(); unsubscribe = fsMod.onSnapshot(ref, snap => { try { if(snap.exists()){ state = mergeState(defaultState(), snap.data()); }else{ save(); } afterLoad(); render(); } catch (err) { console.error("שגיאת טעינת נתונים:", err); alert("שגיאת טעינת נתונים אחרי התחברות: " + (err.message || err)); } }, err => { console.error("Firestore error:", err); alert("שגיאת Firestore: " + (err.message || err.code || err)); }); } function normalizeBaseIncomes(){ state.incomes = asArray(state.incomes, []); const hasKarinCity = state.incomes.some(i => String(i.name || "").includes("קרין") && String(i.name || "").includes("עיריית")); const hasKarinResearch = state.incomes.some(i => String(i.name || "").includes("קרין") && String(i.name || "").includes("קרן מחקרים")); if(!hasKarinCity || !hasKarinResearch){ state.incomes = state.incomes.filter(i => { const name = String(i.name || ""); return !(name === "משכורת קרין" || (name.includes("משכורת קרין") && !name.includes("עיריית") && !name.includes("קרן מחקרים"))); }); if(!hasKarinCity){ state.incomes.push({id: uid(), name:"משכורת קרין — עיריית תל אביב", type:"משכורת", amount:13500}); } if(!hasKarinResearch){ state.incomes.push({id: uid(), name:"משכורת קרין — קרן מחקרים", type:"משכורת", amount:8500}); } } if(!state.incomes.some(i => String(i.name || "") === "משכורת איתן")){ state.incomes.unshift({id: uid(), name:"משכורת איתן", type:"משכורת", amount:14500}); } if(!state.incomes.some(i => String(i.name || "").includes("ביטוח לאומי"))){ state.incomes.push({id: uid(), name:"ביטוח לאומי ילדים", type:"הכנסה נוספת", amount:276}); } const order = [ "משכורת איתן", "משכורת קרין — עיריית תל אביב", "משכורת קרין — קרן מחקרים", "ביטוח לאומי ילדים" ]; state.incomes.sort((a,b) => { const ia = order.indexOf(a.name); const ib = order.indexOf(b.name); return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib); }); } function syncTemporaryמסגרתFromPayments(){ state.limits = asArray(state.limits, []); state.tempPayments = asArray(state.tempPayments, []); const total = state.tempPayments.reduce((sum, payment) => sum + Number(payment.amount || 0), 0); let limit = state.limits.find(l => l.name === "הוצאות זמניות בכ.א"); if(!limit){ state.limits.push({ id: uid(), name: "הוצאות זמניות בכ.א", group: "הוצאות זמניות בכ.א", amount: total }); return; } limit.amount = total; limit.group = "הוצאות זמניות בכ.א"; } function asArray(value, fallback=[]){ return Array.isArray(value) ? value : fallback; } function asObject(value, fallback={}){ return value && typeof value === "object" && !Array.isArray(value) ? value : fallback; } function mergeState(base, data){ data = asObject(data, {}); const out = {...base, ...data}; out.incomes = asArray(data.incomes, base.incomes); out.groups = asArray(data.groups, base.groups); out.limits = asArray(data.limits, base.limits); out.expenses = asArray(data.expenses, []); out.monthlyExtraIncomes = asArray(data.monthlyExtraIncomes, []); out.tempPayments = asArray(data.tempPayments, base.tempPayments); out.savingsBreakdown = asArray(data.savingsBreakdown, base.savingsBreakdown); out.settings = {...base.settings, ...asObject(data.settings, {})}; out.archive = asArray(data.archive, []); if(!out.activePeriod || typeof out.activePeriod !== "string"){ out.activePeriod = periodKeyForDate(new Date(), Number(out.settings.cycleMode || 10)); } return out; } function normalizeMasterBudgetData(){ state.limits = asArray(state.limits, []); const requiredמסגרתs = [ {name:"משכנתא", group:"בית ודיור", amount:8256, source:"בנק"}, {name:"ביטוח חיים מנורה", group:"ביטוחים ובריאות", amount:320, source:"אשראי"}, {name:"ביטוח רפואי פרטי משפחתי — כלל", group:"ביטוחים ובריאות", amount:422, source:"בנק"}, {name:"ביטוח רפואי פרטי מיכאל — מנורה", group:"ביטוחים ובריאות", amount:98, source:"אשראי"}, {name:"ביטוח מחלות קשות ילדים — הראל", group:"ביטוחים ובריאות", amount:40, source:"אשראי"}, {name:"קו סלולר קרין — סלקום", group:"תקשורת ומנויים", amount:10, source:"אשראי"} ]; requiredמסגרתs.forEach(required => { let existing = state.limits.find(l => l.name === required.name); if(existing){ existing.group = required.group; existing.amount = required.amount; existing.source = required.source; }else{ state.limits.push({id: uid(), ...required}); } }); } function normalizePaymentSources(){ const bankNames = new Set(["משכנתא","ארנונה","ועד בית","קופת חולים","ביטוח רפואי פרטי משפחתי — כלל","חסכונות חודשיים"]); state.limits = (state.limits || []).map(limit => ({ ...limit, source: bankNames.has(limit.name) ? "בנק" : (limit.source || "אשראי") })); state.tempPayments = (state.tempPayments || []).map(payment => { if(payment.name === "הלוואת רכב") return {...payment, card:"הוראת קבע / בנק"}; return payment; }); } function afterLoad(){ normalizeMasterBudgetData(); normalizePaymentSources(); normalizeBaseIncomes(); syncTemporaryמסגרתFromPayments(); const currentPeriod = periodKeyForDate(new Date(), cycleDay()); if(!state.activePeriod) state.activePeriod = currentPeriod; if(state.settings.autoReset && currentPeriod !== state.activePeriod){ closeCurrentMonth(true); state.activePeriod = currentPeriod; save(); } renderPeriodOptions(); } function loadLocal(){ const data = localStorage.getItem("householdBudget:v2"); if(data){ try{ state = mergeState(defaultState(), JSON.parse(data)); }catch(e){} } } async function save(){ if(localMode || !firebaseReady){ localStorage.setItem("householdBudget:v2", JSON.stringify(state)); render(); return; } const {fsMod} = window.FirebaseBudget; await fsMod.setDoc(fsMod.doc(db, ...docPath()), { ...state, updatedAt: fsMod.serverTimestamp(), updatedBy: currentUser?.email || null }, {merge:false}); } function renderPeriodOptions(){ const sel = $("periodPicker"); const old = sel.value || state.activePeriod; sel.innerHTML = ""; const periods = new Set([state.activePeriod, periodKeyForDate(new Date(), cycleDay()), ...state.archive.map(a=>a.periodKey)]); [...periods].sort().reverse().forEach(p=>{ const o = document.createElement("option"); o.value=p; o.textContent = periodLabel(p); sel.appendChild(o); }); sel.value = old || state.activePeriod; } function currentExpenses(){ return state.expenses.filter(e => (e.periodKey || state.activePeriod) === state.activePeriod); } function currentExtraIncomes(){ return (state.monthlyExtraIncomes || []).filter(i => (i.periodKey || state.activePeriod) === state.activePeriod); } function automaticMonthlyExpenses(){ const auto = []; state.limits.forEach(limit => { if(FIXED_LOCKED_LIMITS.has(limit.name)){ auto.push({ id: "auto-fixed-" + limit.id, periodKey: state.activePeriod, name: limit.name, amount: Number(limit.amount || 0), limitName: limit.name, date: state.activePeriod, method: "קבוע אוטומטי", auto: true }); } }); state.tempPayments.forEach(payment => { auto.push({ id: "auto-temp-" + payment.id, periodKey: state.activePeriod, name: payment.name, amount: Number(payment.amount || 0), limitName: "הוצאות זמניות בכ.א", date: state.activePeriod, method: `תשלום זמני אוטומטי — ${payment.card || "לא הוגדר"}`, auto: true }); }); return auto; } function allCurrentExpenses(){ return [...automaticMonthlyExpenses(), ...currentExpenses()]; } function baseIncomeTotal(){ return state.incomes.reduce((s,i)=>s+Number(i.amount||0),0); } function extraIncomeTotal(){ return currentExtraIncomes().reduce((s,i)=>s+Number(i.amount||0),0); } function incomeTotal(){ return baseIncomeTotal() + extraIncomeTotal(); } function limitTotal(){ return state.limits.reduce((s,l)=>s+Number(l.amount||0),0); } function spentByמסגרת(limitName){ return allCurrentExpenses().filter(e=>e.limitName===limitName).reduce((s,e)=>s+Number(e.amount||0),0); } function spentByGroup(group){ const names = state.limits.filter(l=>l.group===group).map(l=>l.name); return allCurrentExpenses().filter(e=>names.includes(e.limitName)).reduce((s,e)=>s+Number(e.amount||0),0); } function spentTotal(){ return allCurrentExpenses().reduce((s,e)=>s+Number(e.amount||0),0); } function autoSelectPaymentMethod(){ const selected = $("categoryInput") ? $("categoryInput").value : ""; const limit = state.limits.find(l=>l.name===selected); if(!limit || !$("methodInput")) return; $("methodInput").value = limit.source === "בנק" ? "בנק" : "אשראי"; } function addExpense(){ const name = $("nameInput").value.trim(); const amount = Number($("amountInput").value); if(!name || !amount){ alert("צריך פירוט וסכום."); return; } const limitName = $("categoryInput").value; state.expenses.push({id:uid(), periodKey: state.activePeriod, name, amount, limitName, date:$("dateInput").value || today(), method:$("methodInput").value}); $("nameInput").value=""; $("amountInput").value=""; $("dateInput").value=today(); save(); } function deleteExpense(id){ state.expenses = state.expenses.filter(e=>e.id!==id); save(); } function addIncome(){ const name=$("incomeName").value.trim(), amount=Number($("incomeAmount").value); if(!name || !amount){ alert("צריך שם וסכום."); return; } state.incomes.push({id:uid(), name, amount, type:$("incomeType").value}); $("incomeName").value=""; $("incomeAmount").value=""; save(); } function deleteIncome(id){ state.incomes=state.incomes.filter(i=>i.id!==id); save(); } function updateIncomeAmount(id, value){ const income = state.incomes.find(i => i.id === id); if(!income) return; const amount = Number(value); income.amount = Number.isFinite(amount) ? amount : 0; save(); } function updateIncomeName(id, value){ const income = state.incomes.find(i => i.id === id); if(!income) return; income.name = String(value || "").trim() || income.name; save(); } function addExtraIncome(){ const name=$("extraIncomeName").value.trim(), amount=Number($("extraIncomeAmount").value); if(!name || !amount){ alert("צריך שם וסכום."); return; } state.monthlyExtraIncomes = state.monthlyExtraIncomes || []; state.monthlyExtraIncomes.push({ id:uid(), periodKey:state.activePeriod, name, owner:$("extraIncomeOwner").value, amount }); $("extraIncomeName").value=""; $("extraIncomeAmount").value=""; save(); } function deleteExtraIncome(id){ state.monthlyExtraIncomes = (state.monthlyExtraIncomes || []).filter(i=>i.id!==id); save(); } function addמסגרת(){ const name=$("limitName").value.trim(), amount=Number($("limitAmount").value); if(!name || !amount){ alert("צריך שם וסכום."); return; } const group=$("limitGroup").value; state.limits.push({id:uid(), name, group, amount}); $("limitName").value=""; $("limitAmount").value=""; save(); } function deleteמסגרת(id){ const l = state.limits.find(x=>x.id===id); if(l && state.expenses.some(e=>e.limitName===l.name)){ alert("אי אפשר למחוק מסגרת שכבר יש עליה הוצאות."); return; } state.limits=state.limits.filter(l=>l.id!==id); save(); } function updateמסגרתName(id, value){ const limit = state.limits.find(l => l.id === id); if(!limit) return; const oldName = limit.name; const newName = String(value || "").trim(); if(!newName) return; limit.name = newName; state.expenses.forEach(expense => { if(expense.limitName === oldName){ expense.limitName = newName; } }); save(); } function updateמסגרתAmount(id, value){ const limit = state.limits.find(l => l.id === id); if(!limit) return; const amount = Number(value); limit.amount = Number.isFinite(amount) ? amount : 0; save(); } function updateמסגרתGroup(id, value){ const limit = state.limits.find(l => l.id === id); if(!limit) return; limit.group = String(value || "").trim() || limit.group; save(); } function addTemp(){ const name=$("tempName").value.trim(), amount=Number($("tempAmount").value), prog=$("tempProgress").value.trim(); const m = prog.match(/(\d+)\s*\/\s*(\d+)/); if(!name || !amount || !m){ alert("צריך שם, סכום והתקדמות בפורמט 7/10."); return; } state.tempPayments.push({id:uid(), name, amount, paid:Number(m[1]), total:Number(m[2])}); syncTemporaryמסגרתFromPayments(); $("tempName").value=""; $("tempAmount").value=""; $("tempProgress").value=""; save(); } function deleteTemp(id){ state.tempPayments = state.tempPayments.filter(t=>t.id!==id); syncTemporaryמסגרתFromPayments(); save(); } function updateTempPayment(id, field, value){ const payment = state.tempPayments.find(t => t.id === id); if(!payment) return; if(field === "name"){ payment.name = String(value || "").trim() || payment.name; } if(field === "card"){ payment.card = String(value || "").trim() || "לא הוגדר"; } if(field === "amount"){ const amount = Number(value); payment.amount = Number.isFinite(amount) ? amount : 0; } if(field === "paid"){ const paid = Number(value); payment.paid = Number.isFinite(paid) ? paid : 0; } if(field === "total"){ const total = Number(value); payment.total = Number.isFinite(total) ? total : 0; } syncTemporaryמסגרתFromPayments(); save(); } function saveSettings(){ state.settings.cycleMode = $("cycleMode").value; state.settings.customCycleDay = Math.min(28, Math.max(1, Number($("customCycleDay").value || 1))); state.settings.autoReset = $("autoReset").value === "true"; const newPeriod = periodKeyForDate(new Date(), cycleDay()); if(confirm("לעדכן את מחזור התקציב הנוכחי לפי ההגדרה החדשה?")){ state.activePeriod = newPeriod; } save(); } function closeCurrentMonth(auto=false){ const snapshot = buildSnapshot(); state.archive = state.archive.filter(a=>a.periodKey !== snapshot.periodKey); state.archive.push(snapshot); state.archive.sort((a,b)=>String(b.periodKey).localeCompare(String(a.periodKey))); state.expenses = state.expenses.filter(e => e.periodKey !== state.activePeriod); state.monthlyExtraIncomes = (state.monthlyExtraIncomes || []).filter(i => i.periodKey !== state.activePeriod); state.activePeriod = periodKeyForDate(new Date(new Date(state.activePeriod+"T00:00:00").setMonth(new Date(state.activePeriod+"T00:00:00").getMonth()+1)), cycleDay()); if(!auto) save(); } function manualReset(){ if(!confirm("לאפס את הוצאות החודש בלי לשמור ארכיון?")) return; state.expenses = state.expenses.filter(e => e.periodKey !== state.activePeriod); state.monthlyExtraIncomes = (state.monthlyExtraIncomes || []).filter(i => i.periodKey !== state.activePeriod); save(); } function buildSnapshot(){ const inc=incomeTotal(), lim=limitTotal(), spent=spentTotal(); const byמסגרת = state.limits.map(l=>({name:l.name, group:l.group, limit:l.amount, spent:spentByמסגרת(l.name), diff:Number(l.amount)-spentByמסגרת(l.name)})); return { id:uid(), periodKey:state.activePeriod, label:periodLabel(state.activePeriod), closedAt:new Date().toISOString(), income:inc, limits:lim, spent, balance:inc-spent, vsמסגרתs:lim-spent, byמסגרת, expenses: allCurrentExpenses(), manualExpenses: currentExpenses(), automaticExpenses: automaticMonthlyExpenses(), extraIncomes: currentExtraIncomes() }; } function render(){ renderDashboardIncomeBreakdown(); renderMonthlyIncomeQuickEdit(); renderDashboardTempDetails(); renderPaymentSourceSummary(); renderPeriodOptions(); renderOptions(); autoSelectPaymentMethod(); const inc=incomeTotal(), lim=limitTotal(), spent=spentTotal(), balance=inc-spent; $("printPeriod").textContent = periodLabel(state.activePeriod); $("incomeTotal").textContent=money(inc); $("limitTotal").textContent=money(lim); $("spentTotal").textContent=money(spent); $("balanceTotal").textContent=money(balance); $("balanceTotal").className="value "+(balance>=0?"good":"bad"); const pct = lim ? Math.min(140, spent/lim*100) : 0; $("spendBar").style.width=Math.min(100,pct)+"%"; $("spendBar").className="bar "+(pct>100?"badbg":pct>85?"warnbg":""); $("monthHealth").textContent = pct>100 ? "חריגה מהתקציב" : pct>85 ? "קרוב לתקרה" : "תקין"; $("monthHealth").className = "pill "+(pct>100?"bad":pct>85?"warn":"good"); $("monthHint").textContent = `הכנסות בסיס: ${money(baseIncomeTotal())}. הכנסה נוספת החודש: ${money(extraIncomeTotal())}. נוצלו ${Math.round(pct)}% מתקרת התקציב. יתרה מול מסגרות: ${money(lim-spent)}.`; renderSummary(); renderExpenses(); renderמסגרתs(); renderIncomes(); renderExtraIncomes(); renderTemps(); renderArchive(); renderSettings(); drawCharts(); } function renderOptions(){ const cat=$("categoryInput"); const old=cat.value; cat.innerHTML=""; state.limits.forEach(l=>{ const o=document.createElement("option"); o.value=l.name; o.textContent=`${l.name} — ${money(l.amount)}`; cat.appendChild(o); }); if(old) cat.value=old; const lg=$("limitGroup"); const oldg=lg.value; lg.innerHTML=""; state.groups.forEach(g=>{ const o=document.createElement("option"); o.value=g; o.textContent=g; lg.appendChild(o); }); if(oldg) lg.value=oldg; } function renderPaymentSourceSummary(){ const box = $("paymentSourceSummary"); if(!box) return; const bankמסגרתNames = new Set(state.limits.filter(l=>l.source==="בנק").map(l=>l.name)); const creditמסגרתNames = new Set(state.limits.filter(l=>(l.source||"אשראי")==="אשראי").map(l=>l.name)); const bankTotal = allCurrentExpenses().filter(e => bankמסגרתNames.has(e.limitName) || (e.method||"").includes("בנק")).reduce((s,e)=>s+Number(e.amount||0),0); const creditTotal = allCurrentExpenses().filter(e => creditמסגרתNames.has(e.limitName) || (e.method||"").includes("אשראי") || (e.method||"").includes("ויזה")).reduce((s,e)=>s+Number(e.amount||0),0); box.innerHTML = ` <div class="card stat"><div class="label">חיובי בנק</div><div class="value">${money(bankTotal)}</div></div> <div class="card stat"><div class="label">חיובי אשראי</div><div class="value">${money(creditTotal)}</div></div> `; } function renderDashboardIncomeBreakdown(){ const box = $("dashboardIncomeBreakdown"); if(!box) return; box.innerHTML = ""; state.incomes.forEach(i=>{ const d=document.createElement("div"); d.className="card stat"; d.innerHTML=`<div class="label">${esc(i.name)}</div><div class="value good">${money(i.amount)}</div><div class="small">${esc(i.type || "")}</div>`; box.appendChild(d); }); currentExtraIncomes().forEach(i=>{ const d=document.createElement("div"); d.className="card stat"; d.innerHTML=`<div class="label">${esc(i.name)} — ${esc(i.owner || "")}</div><div class="value good">${money(i.amount)}</div><div class="small">הכנסה חד־פעמית לחודש</div>`; box.appendChild(d); }); } function renderMonthlyIncomeQuickEdit(){ const box = $("monthlyIncomeQuickEdit"); if(!box) return; box.innerHTML = ""; state.incomes.forEach(i=>{ const d=document.createElement("div"); d.className="card"; d.innerHTML=` <div class="field"> <label>${esc(i.name)}</label> <input class="income-amount-edit" data-quick-income="${i.id}" type="number" inputmode="decimal" value="${Number(i.amount||0)}"> </div> <div class="small">${esc(i.type || "")}</div>`; box.appendChild(d); }); box.querySelectorAll("[data-quick-income]").forEach(inp=>{ inp.onchange=()=>{ const income = state.incomes.find(i=>i.id===inp.dataset.quickIncome); if(income){ income.amount = Number(inp.value||0); save(); } }; }); } function quickAddExtraIncome(){ const name = $("quickExtraName").value.trim(); const amount = Number($("quickExtraAmount").value||0); if(!name || !amount){ alert("צריך שם וסכום."); return; } state.monthlyExtraIncomes = state.monthlyExtraIncomes || []; state.monthlyExtraIncomes.push({ id: uid(), periodKey: state.activePeriod, name, owner: $("quickExtraOwner").value, amount }); $("quickExtraName").value=""; $("quickExtraAmount").value=""; save(); } function renderDashboardTempDetails(){ const box = $("dashboardTempDetails"); if(!box) return; box.innerHTML = ""; state.tempPayments.forEach(t=>{ const leftCount = Math.max(0, Number(t.total||0)-Number(t.paid||0)); const leftAmount = leftCount * Number(t.amount||0); const row = document.createElement("div"); row.className = "card"; row.style.margin = "10px 0"; row.innerHTML = ` <div style="display:flex;justify-content:space-between;gap:10px;align-items:center;flex-wrap:wrap"> <div> <div style="font-weight:900;font-size:17px">${esc(t.name)}</div> <div class="small">${esc(t.card || "לא הוגדר")}</div> </div> <div class="amount" style="font-size:20px">${money(t.amount)}</div> </div> <div style="margin-top:8px"> <span class="pill good">תשלום ${Number(t.paid||0)} מתוך ${Number(t.total||0)}</span> <span class="pill">נותרו ${leftCount} תשלומים</span> <span class="pill">יתרה עתידית ${money(leftAmount)}</span> </div> `; box.appendChild(row); }); } function renderSummary(){ const box=$("categorySummary"); box.innerHTML=""; state.limits.forEach(l=>{ const spent=spentByמסגרת(l.name), diff=Number(l.amount)-spent, pct=l.amount?spent/l.amount*100:0; const cls = pct>100?"bad":pct>85?"warn":"good"; const div=document.createElement("div"); div.style.margin="0 0 14px"; div.innerHTML=` <div style="display:flex;justify-content:space-between;gap:10px;align-items:center"> <div><b>${esc(l.name)}</b><div class="small">${esc(l.group)}</div></div> <span class="amount ${cls}">${money(spent)} / ${money(l.amount)}</span> </div> <div class="progress"><div class="bar ${pct>100?'badbg':pct>85?'warnbg':''}" style="width:${Math.min(100,pct)}%"></div></div> <div class="small ${cls}">${diff>=0 ? "יתרה: "+money(diff) : "חריגה: "+money(Math.abs(diff))}</div>`; box.appendChild(div); }); } function renderExpenses(){ const tbody=$("itemsTable"); tbody.innerHTML=""; const arr=[...allCurrentExpenses()].sort((a,b)=>String(b.date).localeCompare(String(a.date))); const manualCount = currentExpenses().length; const autoCount = automaticMonthlyExpenses().length; $("itemsCount").textContent=`${arr.length} הוצאות (${autoCount} אוטומטיות, ${manualCount} ידניות)`; arr.forEach(e=>{ const tr=document.createElement("tr"); const actionCell = e.auto ? `<span class="pill good">אוטומטי</span>` : `<div class="row-actions"><button class="danger" data-del="${e.id}">מחיקה</button></div>`; tr.innerHTML=` <td data-label="פירוט">${esc(e.name)}</td> <td data-label="קטגוריה">${esc(e.limitName)}</td> <td data-label="תאריך">${esc(e.date)}</td> <td data-label="אמצעי">${esc(e.method||"")}</td> <td data-label="סכום" class="amount bad">${money(e.amount)}</td> <td data-label="פעולות">${actionCell}</td>`; tbody.appendChild(tr); }); tbody.querySelectorAll("[data-del]").forEach(b=>b.onclick=()=>deleteExpense(b.dataset.del)); } function renderמסגרתs(){ const tbody=$("limitsTable"); tbody.innerHTML=""; state.limits.forEach(l=>{ const spent=spentByמסגרת(l.name), diff=l.amount-spent, pct=l.amount?spent/l.amount*100:0; const cls=pct>100?"bad":pct>85?"warn":"good"; const isTempמסגרת = l.name === "הוצאות זמניות בכ.א"; const isFixedLocked = FIXED_LOCKED_LIMITS.has(l.name); const isLockedמסגרת = isTempמסגרת || isFixedLocked; const amountInput = isLockedמסגרת ? `<span class="amount">${money(l.amount)}</span><div class="small">${isTempמסגרת ? "מחושב אוטומטית מהתשלומים הזמניים" : "סכום קבוע"}</div>` : `<input class="limit-amount-edit" data-limit-amount="${l.id}" type="number" inputmode="decimal" value="${Number(l.amount||0)}">`; const tr=document.createElement("tr"); tr.innerHTML=` <td data-label="מסגרת"><input class="limit-name-edit" data-limit-name="${l.id}" value="${esc(l.name)}" ${isLockedמסגרת ? "readonly" : ""}></td> <td data-label="קטגוריה"><select class="limit-group-edit" data-limit-group="${l.id}" ${isLockedמסגרת ? "disabled" : ""}>${state.groups.map(g=>`<option value="${esc(g)}" ${g===l.group?"selected":""}>${esc(g)}</option>`).join("")}</select></td> <td data-label="מקור חיוב"><span class="pill">${esc(l.source || "אשראי")}</span></td> <td data-label="תקרה">${amountInput}</td> <td data-label="בפועל" class="amount">${money(spent)}</td> <td data-label="מצב" class="${cls}">${diff>=0 ? "יתרה "+money(diff) : "חריגה "+money(Math.abs(diff))}</td> <td data-label="פעולות"><div class="row-actions"><button class="danger" data-dellimit="${l.id}" ${isLockedמסגרת ? "disabled" : ""}>מחיקה</button></div></td>`; tbody.appendChild(tr); }); tbody.querySelectorAll("[data-dellimit]").forEach(b=>{ if(!b.disabled) b.onclick=()=>deleteמסגרת(b.dataset.dellimit); }); tbody.querySelectorAll("[data-limit-name]").forEach(inp=>{ if(!inp.readOnly) inp.onchange=()=>updateמסגרתName(inp.dataset.limitName, inp.value); }); tbody.querySelectorAll("[data-limit-amount]").forEach(inp=>{ inp.onchange=()=>updateמסגרתAmount(inp.dataset.limitAmount, inp.value); }); tbody.querySelectorAll("[data-limit-group]").forEach(sel=>{ if(!sel.disabled) sel.onchange=()=>updateמסגרתGroup(sel.dataset.limitGroup, sel.value); }); } function renderIncomes(){ const tbody=$("incomeTable"); tbody.innerHTML=""; state.incomes.forEach(i=>{ const tr=document.createElement("tr"); tr.innerHTML=` <td data-label="שם"><input class="income-name-edit" data-income-name="${i.id}" value="${esc(i.name)}"></td> <td data-label="סוג">${esc(i.type)}</td> <td data-label="סכום"><input class="income-amount-edit" data-income-amount="${i.id}" type="number" inputmode="decimal" value="${Number(i.amount||0)}"></td> <td data-label="פעולות"><div class="row-actions"><button class="danger" data-delincome="${i.id}">מחיקה</button></div></td>`; tbody.appendChild(tr); }); tbody.querySelectorAll("[data-delincome]").forEach(b=>b.onclick=()=>deleteIncome(b.dataset.delincome)); tbody.querySelectorAll("[data-income-amount]").forEach(inp=>{ inp.onchange=()=>updateIncomeAmount(inp.dataset.incomeAmount, inp.value); }); tbody.querySelectorAll("[data-income-name]").forEach(inp=>{ inp.onchange=()=>updateIncomeName(inp.dataset.incomeName, inp.value); }); } function renderExtraIncomes(){ const tbody=$("extraIncomeTable"); if(!tbody) return; tbody.innerHTML=""; currentExtraIncomes().forEach(i=>{ const tr=document.createElement("tr"); tr.innerHTML=` <td data-label="שם">${esc(i.name)}</td> <td data-label="שייך ל">${esc(i.owner || "")}</td> <td data-label="סכום" class="amount good">${money(i.amount)}</td> <td data-label="פעולות"><div class="row-actions"><button class="danger" data-delextraincome="${i.id}">מחיקה</button></div></td>`; tbody.appendChild(tr); }); tbody.querySelectorAll("[data-delextraincome]").forEach(b=>b.onclick=()=>deleteExtraIncome(b.dataset.delextraincome)); } function renderTempCards(){ const box = $("tempCards"); if(!box) return; box.innerHTML = ""; state.tempPayments.forEach(t=>{ const leftCount = Math.max(0, Number(t.total||0)-Number(t.paid||0)); const d = document.createElement("div"); d.className = "card stat"; d.innerHTML = ` <div class="label">${esc(t.name)}</div> <div class="value">${money(t.amount)}</div> <div class="small">${esc(t.card || "לא הוגדר")}</div> <div class="small">תשלום ${Number(t.paid||0)} מתוך ${Number(t.total||0)} · נותר ${leftCount} תשלומים · יתרה ${money(leftCount*Number(t.amount||0))}</div>`; box.appendChild(d); }); } function renderTemps(){ renderTempCards(); const tbody=$("tempTable"); tbody.innerHTML=""; state.tempPayments.forEach(t=>{ const left=Math.max(0, Number(t.total)-Number(t.paid)); const card = t.card || "לא הוגדר"; const tr=document.createElement("tr"); tr.innerHTML=` <td data-label="שם"><input class="temp-name-edit" data-temp-name="${t.id}" value="${esc(t.name)}"></td> <td data-label="כרטיס / מקור חיוב"><input class="temp-card-edit" data-temp-card="${t.id}" value="${esc(card)}"></td> <td data-label="תשלום חודשי"><input class="temp-amount-edit" data-temp-amount="${t.id}" type="number" inputmode="decimal" value="${Number(t.amount||0)}"></td> <td data-label="כמה מתוך כמה"> <div class="temp-progress-edit"> <input data-temp-paid="${t.id}" type="number" inputmode="numeric" value="${Number(t.paid||0)}"> <span>/</span> <input data-temp-total="${t.id}" type="number" inputmode="numeric" value="${Number(t.total||0)}"> </div> </td> <td data-label="יתרה עתידית" class="amount">${money(left*Number(t.amount||0))}</td> <td data-label="פעולות"><div class="row-actions"><button class="danger" data-deltemp="${t.id}">מחיקה</button></div></td>`; tbody.appendChild(tr); }); tbody.querySelectorAll("[data-deltemp]").forEach(b=>b.onclick=()=>deleteTemp(b.dataset.deltemp)); tbody.querySelectorAll("[data-temp-name]").forEach(inp=>inp.onchange=()=>updateTempPayment(inp.dataset.tempName, "name", inp.value)); tbody.querySelectorAll("[data-temp-card]").forEach(inp=>inp.onchange=()=>updateTempPayment(inp.dataset.tempCard, "card", inp.value)); tbody.querySelectorAll("[data-temp-amount]").forEach(inp=>inp.onchange=()=>updateTempPayment(inp.dataset.tempAmount, "amount", inp.value)); tbody.querySelectorAll("[data-temp-paid]").forEach(inp=>inp.onchange=()=>updateTempPayment(inp.dataset.tempPaid, "paid", inp.value)); tbody.querySelectorAll("[data-temp-total]").forEach(inp=>inp.onchange=()=>updateTempPayment(inp.dataset.tempTotal, "total", inp.value)); } function renderArchive(){ const box=$("archiveList"); box.innerHTML=""; if(!state.archive.length){ box.innerHTML='<p class="muted">אין עדיין חודשים סגורים.</p>'; return; } state.archive.forEach(a=>{ const div=document.createElement("div"); div.className="archive-card"; div.innerHTML=` <div> <b>${esc(a.label)}</b> <div class="small">הוצאות: ${money(a.spent)} | יתרה מול הכנסות: ${money(a.balance)} | מול מסגרות: ${money(a.vsמסגרתs)}</div> </div> <button class="ghost" data-loadarchive="${a.id}">הצג</button>`; box.appendChild(div); }); box.querySelectorAll("[data-loadarchive]").forEach(b=>b.onclick=()=>alert("החודש שמור בארכיון. בגרסה הבאה אוסיף מסך צפייה מלא בחודש סגור.")); } function renderSettings(){ $("cycleMode").value=state.settings.cycleMode; $("customCycleDay").value=state.settings.customCycleDay; $("autoReset").value=String(!!state.settings.autoReset); } function drawCharts(){ drawPie(); drawBar(); } function drawPie(){ const c=$("pieChart"), ctx=c.getContext("2d"); ctx.clearRect(0,0,c.width,c.height); const data=state.groups.map(g=>({g, v:spentByGroup(g)})).filter(x=>x.v>0); if(!data.length){ ctx.fillStyle="#667085"; ctx.font="16px Arial"; ctx.fillText("אין עדיין הוצאות",100,130); return; } const total=data.reduce((s,x)=>s+x.v,0); let start=-Math.PI/2; const colors=["#2947d3","#12805c","#b26a00","#c62828","#6f42c1","#00838f","#ad1457","#455a64","#7cb342","#ef6c00"]; data.forEach((x,i)=>{ const ang=x.v/total*Math.PI*2; ctx.beginPath(); ctx.moveTo(130,120); ctx.arc(130,120,100,start,start+ang); ctx.closePath(); ctx.fillStyle=colors[i%colors.length]; ctx.fill(); start+=ang; }); let y=18; ctx.font="12px Arial"; ctx.textAlign="right"; data.slice(0,8).forEach((x,i)=>{ctx.fillStyle=colors[i%colors.length];ctx.fillRect(285,y-10,10,10);ctx.fillStyle="#172033";ctx.fillText(`${x.g}: ${money(x.v)}`,275,y);y+=18;}); } function drawBar(){ const c=$("barChart"), ctx=c.getContext("2d"); ctx.clearRect(0,0,c.width,c.height); const data=state.limits.map(l=>({name:l.name, limit:l.amount, spent:spentByמסגרת(l.name)})).filter(x=>x.limit>0).slice(0,14); if(!data.length){ return; } const max=Math.max(...data.map(x=>Math.max(x.limit,x.spent)),1); const left=120, top=20, h=14, gap=4; ctx.font="11px Arial"; ctx.textAlign="right"; data.forEach((x,i)=>{ const y=top+i*(h*2+gap); ctx.fillStyle="#172033"; ctx.fillText(x.name,left-8,y+h); ctx.fillStyle="#edf0f7"; ctx.fillRect(left,y,560,h); ctx.fillStyle=x.spent>x.limit?"#c62828":"#12805c"; ctx.fillRect(left,y,560*(x.spent/max),h); ctx.strokeStyle="#2947d3"; const lx=left+560*(x.limit/max); ctx.beginPath(); ctx.moveTo(lx,y-2); ctx.lineTo(lx,y+h+2); ctx.stroke(); ctx.fillStyle="#667085"; ctx.textAlign="left"; ctx.fillText(`${money(x.spent)} / ${money(x.limit)}`, left+570, y+h); ctx.textAlign="right"; }); } function downloadTextFile(filename, content, mime){ const blob = new Blob([content], {type:mime}); const a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=filename; document.body.appendChild(a); a.click(); setTimeout(()=>{ URL.revokeObjectURL(a.href); a.remove(); }, 1000); } function buildReportHtml(){ const inc=incomeTotal(), lim=limitTotal(), spent=spentTotal(), balance=inc-spent; const now = new Date().toLocaleString("he-IL"); const limitRows = state.limits.map(l=>{ const spentמסגרת = spentByמסגרת(l.name); const diff = Number(l.amount||0) - spentמסגרת; const status = diff >= 0 ? `יתרה ${money(diff)}` : `חריגה ${money(Math.abs(diff))}`; const cls = diff >= 0 ? "good" : "bad"; return `<tr><td>${esc(l.name)}</td><td>${esc(l.group)}</td><td>${esc(l.source || "אשראי")}</td><td>${money(l.amount)}</td><td>${money(spentמסגרת)}</td><td class="${cls}">${status}</td></tr>`; }).join(""); const incomeRows = state.incomes.map(i=>`<tr><td>${esc(i.name)}</td><td>${esc(i.type)}</td><td>${money(i.amount)}</td></tr>`).join(""); const extraRows = currentExtraIncomes().map(i=>`<tr><td>${esc(i.name)}</td><td>${esc(i.owner || "")}</td><td>${money(i.amount)}</td></tr>`).join("") || `<tr><td colspan="3">אין הכנסות נוספות החודש</td></tr>`; const expenseRows = allCurrentExpenses().map(e=>`<tr><td>${esc(e.date)}</td><td>${esc(e.name)}</td><td>${esc(e.limitName)}</td><td>${esc(e.method || "")}${e.auto ? " — אוטומטי" : ""}</td><td>${money(e.amount)}</td></tr>`).join("") || `<tr><td colspan="5">אין הוצאות בפועל</td></tr>`; const tempRows = state.tempPayments.map(t=>{ const left = Math.max(0, Number(t.total||0)-Number(t.paid||0)); return `<tr><td>${esc(t.name)}</td><td>${esc(t.card || "לא הוגדר")}</td><td>${money(t.amount)}</td><td>${Number(t.paid||0)}/${Number(t.total||0)}</td><td>${money(left*Number(t.amount||0))}</td></tr>`; }).join(""); return `<!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> body{font-family:Arial,sans-serif;color:#172033;background:#fff;margin:24px;line-height:1.5} h1,h2{margin:0 0 12px} h2{margin-top:28px} .summary{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:18px 0} .card{border:1px solid #e5e7ef;border-radius:14px;padding:14px;background:#f8fafc} .label{color:#667085;font-size:13px} .value{font-size:22px;font-weight:900;direction:ltr;text-align:right} .good{color:#12805c;font-weight:800}.bad{color:#c62828;font-weight:800} table{width:100%;border-collapse:collapse;margin-top:10px} th,td{border:1px solid #e5e7ef;padding:8px;text-align:right} th{background:#eef2ff} .note{color:#667085;font-size:13px} @media print{button{display:none}.summary{grid-template-columns:repeat(4,1fr)}body{margin:12mm}} @media(max-width:700px){.summary{grid-template-columns:1fr 1fr}body{margin:14px;font-size:14px}} </style> </head> <body> <div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px"> <button onclick="window.print()" style="padding:13px 20px;border-radius:14px;border:0;background:#2947d3;color:white;font-weight:900;box-shadow:0 6px 18px rgba(41,71,211,.25)">שמירה / הדפסה ל־PDF</button> <button onclick="if(history.length>1){history.back()}else{window.close()}" style="padding:13px 20px;border-radius:14px;border:1px solid #d0d5dd;background:#fff;color:#172033;font-weight:900">חזרה לאפליקציה</button> </div> <h1>דוח תקציב משפחתי</h1> <div class="note">תקופה: ${periodLabel(state.activePeriod)} | הופק: ${now}</div> <div class="summary"> <div class="card"><div class="label">הכנסות</div><div class="value">${money(inc)}</div></div> <div class="card"><div class="label">תקרת תקציב</div><div class="value">${money(lim)}</div></div> <div class="card"><div class="label">הוצאות בפועל</div><div class="value">${money(spent)}</div></div> <div class="card"><div class="label">יתרה מול הכנסות</div><div class="value ${balance>=0?'good':'bad'}">${money(balance)}</div></div> </div> <h2>הכנסות בסיס</h2> <table><thead><tr><th>שם</th><th>סוג</th><th>סכום</th></tr></thead><tbody>${incomeRows}</tbody></table> <h2>הכנסות נוספות לחודש</h2> <table><thead><tr><th>שם</th><th>שייך ל</th><th>סכום</th></tr></thead><tbody>${extraRows}</tbody></table> <h2>מסגרות תקציב</h2> <table><thead><tr><th>שם</th><th>קבוצה</th><th>מקור חיוב</th><th>תקרה</th><th>בפועל</th><th>מצב</th></tr></thead><tbody>${limitRows}</tbody></table> <h2>הוצאות בפועל</h2> <table><thead><tr><th>תאריך</th><th>פירוט</th><th>מסגרת</th><th>אמצעי</th><th>סכום</th></tr></thead><tbody>${expenseRows}</tbody></table> <h2>הוצאות זמניות בכרטיס אשראי</h2> <table><thead><tr><th>שם</th><th>כרטיס / מקור חיוב</th><th>תשלום חודשי</th><th>כמה מתוך כמה</th><th>יתרה עתידית</th></tr></thead><tbody>${tempRows}</tbody></table> </body> </html>`; } function exportData(){ const data = JSON.stringify(state, null, 2); downloadTextFile(`household-budget-${state.activePeriod}.json`, "\ufeff" + data, "application/json;charset=utf-8"); } function exportHtmlReport(){ const report = buildReportHtml(); downloadTextFile(`household-budget-report-${state.activePeriod}.html`, "\ufeff" + report, "text/html;charset=utf-8"); } function printPdf(){ const report = buildReportHtml(); const win = window.open("", "_blank"); if(!win){ exportHtmlReport(); alert("הדפדפן חסם פתיחת חלון. הורדתי במקום זה דוח HTML; פתח אותו ובחר שיתוף/הדפסה ל־PDF."); return; } win.document.open(); win.document.write(report); win.document.close(); setTimeout(()=>{ try{ win.print(); }catch(e){} }, 600); } function esc(s){ return String(s??"").replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } function showTab(tab){ document.querySelectorAll(".tab").forEach(b=>b.classList.toggle("active", b.dataset.tab===tab)); ["dashboard","items","limits","archive","settings"].forEach(t=>$("tab-"+t).classList.toggle("hidden", t!==tab)); } $("emailLoginBtn").onclick=emailLogin; $("googleLoginBtn").onclick=googleLogin; $("localModeBtn").onclick=()=>enterApp(true); $("cleanTestBtn").onclick=enterCleanTestMode; $("addBtn").onclick=addExpense; $("categoryInput").onchange=autoSelectPaymentMethod; $("addIncomeBtn").onclick=addIncome; $("quickAddExtraBtn").onclick=quickAddExtraIncome; $("addExtraIncomeBtn").onclick=addExtraIncome; $("addמסגרתBtn").onclick=addמסגרת; $("addTempBtn").onclick=addTemp; $("saveSettingsBtn").onclick=saveSettings; $("exportBtn").onclick=exportData; $("pdfBtn").onclick=printPdf; $("htmlReportBtn").onclick=exportHtmlReport; $("closeMonthBtn").onclick=()=>{ if(confirm("לסגור את החודש, לשמור ארכיון ולהתחיל חודש חדש?")) closeCurrentMonth(false); }; $("manualResetBtn").onclick=manualReset; $("periodPicker").onchange=()=>{ state.activePeriod=$("periodPicker").value; render(); }; $("signOutBtn").onclick=async()=>{ if(unsubscribe) unsubscribe(); const {authMod}=window.FirebaseBudget; await authMod.signOut(auth); location.reload(); }; document.querySelectorAll(".tab").forEach(b=>b.onclick=()=>showTab(b.dataset.tab)); window.BudgetApp={showTab}; initFirebase(); </script> </body> </html>
שמור קובץ