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>