C:\TripServer\MoneyHome\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}
button.refundbtn{background:#ecfdf3;color:var(--good);border-color:#b7ebc7}
tr.refund-row td{background:#f7fff9}
.amount.refund{color:var(--good)}
button.secondary{background:#f8fafc;color:var(--ink);border-color:#d0d5dd}
.debug-only{display:none!important}
body.debug-tools .debug-only{display:inline-flex!important}
.archive-detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin:12px 0}
.archive-detail-table{width:100%;border-collapse:separate;border-spacing:0 8px}
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}
canvas[role="img"]{outline:0}
.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>
</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 type="button" class="ghost" id="pdfBtn">דוח PDF</button>
<button type="button" class="ghost" id="htmlReportBtn">דוח HTML</button>
<button type="button" class="ghost" id="exportBtn">גיבוי JSON</button>
<button type="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 for="emailLoginInput">מייל</label>
<input id="emailLoginInput" type="email" autocomplete="username" placeholder="name@gmail.com">
</div>
<div class="field">
<label for="passwordLoginInput">סיסמה</label>
<input id="passwordLoginInput" type="password" autocomplete="current-password" placeholder="סיסמה">
</div>
<button type="button" class="primary" id="emailLoginBtn">כניסה עם מייל וסיסמה</button>
</div>
<div class="toolbar">
<button type="button" class="ghost" id="googleLoginBtn">כניסה עם Google</button>
<button type="button" class="ghost debug-only" id="localModeBtn">מצב מקומי</button>
<button type="button" class="secondary debug-only" 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 good" id="refundTotal">₪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 type="button" class="tab active" data-tab="dashboard">דשבורד</button>
<button type="button" class="tab" data-tab="items">הוצאות / החזרים / הכנסות</button>
<button type="button" class="tab" data-tab="limits">מסגרות תקציב</button>
<button type="button" class="tab" data-tab="archive">ארכיון</button>
<button type="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" role="img" aria-label="תרשים עוגה של ההוצאות לפי קטגוריות"></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" role="img" aria-label="גרף השוואת הוצאות מול מסגרת תקציב"></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 for="nameInput">פירוט</label><input id="nameInput" placeholder="לדוגמה: שופרסל / חשמל / מסעדה"></div>
<div class="field"><label for="amountInput">סכום ₪</label><input id="amountInput" type="number" inputmode="decimal" min="0" step="0.01" placeholder="0"></div>
<div class="field"><label for="categoryInput">קטגוריה</label><select id="categoryInput"></select></div>
<div class="field"><label for="dateInput">תאריך</label><input id="dateInput" type="date"></div>
<div class="field"><label for="methodInput">אמצעי</label><select id="methodInput">
<option>בנק</option>
<option>אשראי</option>
<option>ויזה בנק הפועלים — איתן</option>
<option>ויזה חוץ בנקאית — קרין</option>
<option>מזומן</option>
<option>אחר</option>
</select></div>
<button type="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 class="form">
<div class="field wide"><label for="refundNameInput">פירוט ההחזר</label><input id="refundNameInput" placeholder="לדוגמה: החזר שופרסל / זיכוי ביטוח / ביטול עסקה"></div>
<div class="field"><label for="refundAmountInput">סכום ₪</label><input id="refundAmountInput" type="number" inputmode="decimal" min="0" step="0.01" placeholder="0"></div>
<div class="field"><label for="refundCategoryInput">מסגרת לקיזוז</label><select id="refundCategoryInput"></select></div>
<div class="field"><label for="refundDateInput">תאריך</label><input id="refundDateInput" type="date"></div>
<div class="field"><label for="refundMethodInput">מקור החזר</label><select id="refundMethodInput">
<option>בנק</option>
<option>אשראי</option>
<option>ויזה בנק הפועלים — איתן</option>
<option>ויזה חוץ בנקאית — קרין</option>
<option>מזומן</option>
<option>אחר</option>
</select></div>
<button type="button" class="refundbtn" id="addRefundBtn">שמירת החזר</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 for="quickExtraName">שם הכנסה חד־פעמית</label><input id="quickExtraName" placeholder="לדוגמה: בונוס / עבודה נוספת"></div>
<div class="field"><label for="quickExtraAmount">סכום ₪</label><input id="quickExtraAmount" type="number" inputmode="decimal" min="0" step="0.01" placeholder="0"></div>
<div class="field"><label for="quickExtraOwner">שייך ל</label><select id="quickExtraOwner"><option>איתן</option><option>קרין</option><option>משפחה</option></select></div>
<button type="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><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 for="incomeName">שם הכנסה</label><input id="incomeName" placeholder="לדוגמה: משכורת"></div>
<div class="field"><label for="incomeAmount">סכום ₪</label><input id="incomeAmount" type="number" inputmode="decimal" min="0" step="0.01" placeholder="0"></div>
<div class="field"><label for="incomeType">סוג</label><select id="incomeType"><option>משכורת</option><option>הכנסה נוספת</option></select></div>
<button type="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 for="extraIncomeName">שם הכנסה</label><input id="extraIncomeName" placeholder="לדוגמה: עבודה נוספת איתן / בונוס קרין"></div>
<div class="field"><label for="extraIncomeAmount">סכום ₪</label><input id="extraIncomeAmount" type="number" inputmode="decimal" min="0" step="0.01" placeholder="0"></div>
<div class="field"><label for="extraIncomeOwner">שייך ל</label><select id="extraIncomeOwner"><option>איתן</option><option>קרין</option><option>משפחה</option></select></div>
<button type="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 for="limitName">שם מסגרת</label><input id="limitName" placeholder="לדוגמה: קניות סופר"></div>
<div class="field"><label for="limitAmount">סכום ₪</label><input id="limitAmount" type="number" inputmode="decimal" min="0" step="0.01" placeholder="0"></div>
<div class="field"><label for="limitGroup">קטגוריית על</label><select id="limitGroup"></select></div>
<button type="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 for="tempName">שם</label><input id="tempName" placeholder="לדוגמה: רובוט שואב"></div>
<div class="field"><label for="tempAmount">תשלום חודשי ₪</label><input id="tempAmount" type="number" inputmode="decimal" min="0" step="0.01"></div>
<div class="field"><label for="tempProgress">תשלום נוכחי / סה״כ</label><input id="tempProgress" placeholder="7/10"></div>
<button type="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">סגירת חודש שומרת את סיכום החודש בארכיון, ואז פותחת חודש תקציבי חדש וריק להזנת הוצאות בפועל. מסגרות התקציב וההכנסות נשארות כבסיס לחודש הבא.</p>
<div class="toolbar">
<button type="button" class="danger" id="closeMonthBtn">סגור חודש והתחל חדש</button>
<button type="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 id="archiveDetails" style="margin-top:12px"></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 for="cycleMode">מחזור חודש</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 for="customCycleDay">יום התחלה מותאם</label><input id="customCycleDay" type="number" min="1" max="28" step="1" value="1"></div>
<div class="field"><label for="autoReset">איפוס אוטומטי</label><select id="autoReset"><option value="false">כבוי</option><option value="true">פעיל</option></select></div>
<button type="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 type="button" onclick="BudgetApp.showTab('dashboard')">דשבורד</button>
<button type="button" onclick="BudgetApp.showTab('items')">הוצאות / החזרים</button>
<button type="button" onclick="BudgetApp.showTab('limits')">מסגרות תקציב</button>
<button type="button" onclick="BudgetApp.showTab('archive')">ארכיון</button>
<button type="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 = () => localDateKey(new Date());
function cycleDay(){ return state.settings.cycleMode === "custom" ? Number(state.settings.customCycleDay||1) : Number(state.settings.cycleMode||10); }
function localDateKey(date){
const d = new Date(date);
const y = d.getFullYear();
const m = String(d.getMonth()+1).padStart(2,"0");
const day = String(d.getDate()).padStart(2,"0");
return `${y}-${m}-${day}`;
}
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 localDateKey(s);
}
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; }
function isDebugMode(){
const params = new URLSearchParams(location.search);
return params.get("debug") === "1" || params.get("test") === "1" || params.get("local") === "1";
}
function enableDebugToolsIfRequested(){
if(isDebugMode()) document.body.classList.add("debug-tools");
}
function requirePositiveAmount(value, message){
const n = Number(value);
if(!Number.isFinite(n) || n <= 0){
alert(message || "צריך להזין סכום חיובי.");
return null;
}
return n;
}
function clampNonNegative(value){
const n = Number(value);
return Number.isFinite(n) && n >= 0 ? n : 0;
}
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){
if(isLocal && !isDebugMode()){
alert("מצב מקומי זמין רק עם פרמטר debug=1 בכתובת.");
return;
}
localMode = isLocal;
$("loginView").classList.add("hidden");
$("appView").classList.remove("hidden");
$("bottomNav").classList.remove("hidden");
$("signOutBtn").classList.toggle("hidden", isLocal);
$("dateInput").value = today(); if($("refundDateInput")) $("refundDateInput").value = today();
if(isLocal){ setStatus("מקומי במכשיר"); loadLocal(); afterLoad(); render(); }
else { setStatus("מחובר לענן"); listenCloud(); }
}
function enterCleanTestMode(){
if(!isDebugMode()){
alert("מצב נתוני דוגמה זמין רק עם פרמטר debug=1 בכתובת.");
return;
}
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("נתוני דוגמה");
if($("dateInput")) $("dateInput").value = today();
if($("refundDateInput")) $("refundDateInput").value = today();
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,
source: "אשראי"
});
return;
}
limit.amount = total;
limit.group = "הוצאות זמניות בכ.א";
limit.source = "אשראי";
}
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){
try{
localStorage.setItem("householdBudget:v2", JSON.stringify(state));
}catch(err){
console.warn("שמירה מקומית לא זמינה בדפדפן הנוכחי:", err);
}
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 isRefund(expense){ return expense && expense.type === "refund"; }
function itemNetAmount(expense){ return isRefund(expense) ? -Number(expense.amount || 0) : Number(expense.amount || 0); }
function grossSpentByמסגרת(limitName){
return allCurrentExpenses().filter(e=>e.limitName===limitName && !isRefund(e)).reduce((s,e)=>s+Number(e.amount||0),0);
}
function refundByמסגרת(limitName){
return currentExpenses().filter(e=>e.limitName===limitName && isRefund(e)).reduce((s,e)=>s+Number(e.amount||0),0);
}
function effectiveRefundByמסגרת(limitName){
return Math.min(refundByמסגרת(limitName), grossSpentByמסגרת(limitName));
}
function excessRefundByמסגרת(limitName){
return Math.max(0, refundByמסגרת(limitName) - grossSpentByמסגרת(limitName));
}
function grossSpentTotal(){ return allCurrentExpenses().filter(e=>!isRefund(e)).reduce((s,e)=>s+Number(e.amount||0),0); }
function refundTotal(){ return currentExpenses().filter(isRefund).reduce((s,e)=>s+Number(e.amount||0),0); }
function effectiveRefundTotal(){
const names = new Set(allCurrentExpenses().map(e=>e.limitName).filter(Boolean));
return [...names].reduce((sum,name)=>sum+effectiveRefundByמסגרת(name),0);
}
function excessRefundTotal(){
const names = new Set(allCurrentExpenses().map(e=>e.limitName).filter(Boolean));
return [...names].reduce((sum,name)=>sum+excessRefundByמסגרת(name),0);
}
function spentByמסגרת(limitName){
return Math.max(0, grossSpentByמסגרת(limitName) - refundByמסגרת(limitName));
}
function spentByGroup(group){
const names = state.limits.filter(l=>l.group===group).map(l=>l.name);
return names.reduce((s,name)=>s+spentByמסגרת(name),0);
}
function spentTotal(){
const names = new Set(allCurrentExpenses().map(e=>e.limitName).filter(Boolean));
return [...names].reduce((sum,name)=>sum+spentByמסגרת(name),0);
}
function displayItemAmount(expense){ return isRefund(expense) ? "−" + money(expense.amount) : money(expense.amount); }
function refundableAmountForמסגרת(limitName){
return Math.max(0, grossSpentByמסגרת(limitName) - refundByמסגרת(limitName));
}
function setPaymentMethodByמסגרת(categoryId, methodId){
const category = $(categoryId);
const method = $(methodId);
if(!category || !method) return;
const limit = state.limits.find(l=>l.name===category.value);
if(!limit) return;
method.value = limit.source === "בנק" ? "בנק" : "אשראי";
}
function autoSelectPaymentMethod(){
setPaymentMethodByמסגרת("categoryInput", "methodInput");
}
function autoSelectRefundMethod(){
setPaymentMethodByמסגרת("refundCategoryInput", "refundMethodInput");
}
function addExpense(){
const name = $("nameInput").value.trim();
const amount = requirePositiveAmount($("amountInput").value, "צריך להזין הוצאה עם סכום חיובי.");
if(!name){ alert("צריך להזין פירוט להוצאה."); return; }
if(amount === null) return;
const limitName = $("categoryInput").value;
state.expenses.push({id:uid(), type:"expense", periodKey: state.activePeriod, name, amount, limitName, date:$("dateInput").value || today(), method:$("methodInput").value});
$("nameInput").value=""; $("amountInput").value=""; $("dateInput").value=today();
save();
}
function addRefund(){
const name = $("refundNameInput").value.trim();
const amount = requirePositiveAmount($("refundAmountInput").value, "צריך להזין החזר עם סכום חיובי.");
if(!name){ alert("צריך להזין פירוט להחזר."); return; }
if(amount === null) return;
const limitName = $("refundCategoryInput").value;
const available = refundableAmountForמסגרת(limitName);
if(available <= 0){
alert("לא ניתן לקלוט החזר למסגרת הזו לפני שקיימת בה הוצאה לקיזוז.");
return;
}
if(amount > available){
alert(`סכום ההחזר גבוה מהיתרה הזמינה לקיזוז במסגרת הזו. ניתן לקזז עד ${money(available)}.`);
return;
}
state.expenses.push({id:uid(), type:"refund", periodKey: state.activePeriod, name, amount, limitName, date:$("refundDateInput").value || today(), method:$("refundMethodInput").value});
$("refundNameInput").value=""; $("refundAmountInput").value=""; $("refundDateInput").value=today();
save();
}
function deleteExpense(id){
const item = state.expenses.find(e=>e.id===id);
if(item && !isRefund(item)){
const remainingGross = currentExpenses().filter(e=>e.id!==id && e.limitName===item.limitName && !isRefund(e)).reduce((s,e)=>s+Number(e.amount||0),0)
+ automaticMonthlyExpenses().filter(e=>e.limitName===item.limitName).reduce((s,e)=>s+Number(e.amount||0),0);
const refunds = currentExpenses().filter(e=>e.limitName===item.limitName && isRefund(e)).reduce((s,e)=>s+Number(e.amount||0),0);
if(refunds > remainingGross){
alert("לא ניתן למחוק את ההוצאה לפני מחיקת החזר/זיכוי שתלוי בה, כדי לא ליצור יתרת הוצאות שלילית.");
return;
}
}
state.expenses = state.expenses.filter(e=>e.id!==id);
save();
}
function addIncome(){
const name=$("incomeName").value.trim(), amount=requirePositiveAmount($("incomeAmount").value, "צריך להזין הכנסה עם סכום חיובי.");
if(!name){ alert("צריך להזין שם הכנסה."); return; }
if(amount === null) 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 = requirePositiveAmount(value, "סכום הכנסה חייב להיות חיובי.");
if(amount === null){ render(); return; }
income.amount = amount;
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=requirePositiveAmount($("extraIncomeAmount").value, "צריך להזין הכנסה חד־פעמית עם סכום חיובי.");
if(!name){ alert("צריך להזין שם הכנסה."); return; }
if(amount === null) 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=requirePositiveAmount($("limitAmount").value, "צריך להזין מסגרת עם סכום חיובי.");
if(!name){ alert("צריך להזין שם מסגרת."); return; }
if(amount === null) return;
const group=$("limitGroup").value;
state.limits.push({id:uid(), name, group, amount, source:"אשראי"});
$("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 = requirePositiveAmount(value, "סכום מסגרת חייב להיות חיובי.");
if(amount === null){ render(); return; }
limit.amount = amount;
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=requirePositiveAmount($("tempAmount").value, "צריך להזין תשלום חודשי חיובי."), prog=$("tempProgress").value.trim();
const m = prog.match(/(\d+)\s*\/\s*(\d+)/);
if(!name){ alert("צריך להזין שם תשלום זמני."); return; }
if(amount === null) return;
if(!m){ alert("צריך להזין התקדמות בפורמט 7/10."); return; }
const paid = Number(m[1]), total = Number(m[2]);
if(total <= 0 || paid < 0 || paid > total){ alert("התקדמות התשלום חייבת להיות בין 0 לבין סך התשלומים."); return; }
state.tempPayments.push({id:uid(), name, amount, paid, total, card:"לא הוגדר"});
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 = requirePositiveAmount(value, "תשלום חודשי חייב להיות חיובי.");
if(amount === null){ render(); return; }
payment.amount = amount;
}
if(field === "paid"){
const paid = clampNonNegative(value);
if(paid > Number(payment.total||0)){ alert("תשלום נוכחי לא יכול להיות גדול מסך התשלומים."); render(); return; }
payment.paid = paid;
}
if(field === "total"){
const total = requirePositiveAmount(value, "סך התשלומים חייב להיות חיובי.");
if(total === null){ render(); return; }
if(Number(payment.paid||0) > total){ alert("סך התשלומים לא יכול להיות קטן מהתשלום הנוכחי."); render(); return; }
payment.total = total;
}
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 = buildClosedMonthRecord();
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 buildClosedMonthRecord(){
const inc=incomeTotal(), lim=limitTotal(), spent=spentTotal(), refunds=effectiveRefundTotal(), grossSpent=grossSpentTotal();
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, grossSpent, refunds, 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();
autoSelectRefundMethod();
const inc=incomeTotal(), lim=limitTotal(), spent=spentTotal(), refunds=effectiveRefundTotal(), grossSpent=grossSpentTotal(), balance=inc-spent, excessRefunds=excessRefundTotal();
$("printPeriod").textContent = periodLabel(state.activePeriod);
$("incomeTotal").textContent=money(inc);
$("limitTotal").textContent=money(lim);
$("spentTotal").textContent=money(spent);
if($("refundTotal")) $("refundTotal").textContent=money(refunds);
$("balanceTotal").textContent=money(balance);
$("balanceTotal").className="value "+(balance>=0?"good":"bad");
const pct = lim ? Math.min(140, Math.max(0, 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())}. הוצאות ברוטו: ${money(grossSpent)}. החזרים/זיכויים שקוזזו: ${money(refunds)}. הוצאות נטו: ${money(spent)}. נוצלו ${Math.round(pct)}% מתקרת התקציב. יתרה מול מסגרות: ${money(lim-spent)}.${excessRefunds ? " קיימת יתרת החזר שלא קוזזה: "+money(excessRefunds)+"." : ""}`;
renderSummary(); renderExpenses(); renderמסגרתs(); renderIncomes(); renderExtraIncomes(); renderTemps(); renderArchive(); renderSettings(); drawCharts();
}
function renderOptions(){
const fillLimitSelect = (selectId) => {
const sel = $(selectId);
if(!sel) return;
const oldValue = sel.value;
sel.innerHTML="";
state.limits.forEach(l=>{ const o=document.createElement("option"); o.value=l.name; o.textContent=`${l.name} — ${money(l.amount)}`; sel.appendChild(o); });
if(oldValue) sel.value=oldValue;
};
fillLimitSelect("categoryInput");
fillLimitSelect("refundCategoryInput");
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 paymentSourceForExpense(expense){
const method = String(expense.method || "");
if(method.includes("מזומן")) return "מזומן";
if(method.includes("ויזה") || method.includes("אשראי")) return "אשראי";
if(method.includes("בנק") || method.includes("הוראת קבע")) return "בנק";
const limit = state.limits.find(l => l.name === expense.limitName);
return limit?.source || "אשראי";
}
function renderPaymentSourceSummary(){
const box = $("paymentSourceSummary");
if(!box) return;
const totals = {"בנק":0, "אשראי":0, "מזומן":0, "אחר":0};
allCurrentExpenses().forEach(expense => {
const source = paymentSourceForExpense(expense);
const key = source in totals ? source : "אחר";
totals[key] += itemNetAmount(expense);
});
Object.keys(totals).forEach(key => { totals[key] = Math.max(0, totals[key]); });
const displayedTotal = Object.values(totals).reduce((s,n)=>s+n,0);
box.innerHTML = `
<div class="card stat"><div class="label">חיובי בנק</div><div class="value">${money(totals["בנק"])}</div></div>
<div class="card stat"><div class="label">חיובי אשראי</div><div class="value">${money(totals["אשראי"])}</div></div>
<div class="card stat"><div class="label">מזומן / אחר</div><div class="value">${money(totals["מזומן"] + totals["אחר"])}</div></div>
<div class="card stat"><div class="label">בקרת התאמה לסך הוצאות נטו</div><div class="value ${Math.round(displayedTotal) === Math.round(spentTotal()) ? 'good' : 'bad'}">${money(displayedTotal)}</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 for="quick-income-${esc(i.id)}">${esc(i.name)}</label>
<input id="quick-income-${esc(i.id)}" class="income-amount-edit" data-quick-income="${i.id}" type="number" inputmode="decimal" min="0" step="0.01" aria-label="סכום הכנסה ${esc(i.name)}" 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){
const amount = requirePositiveAmount(inp.value, "סכום הכנסה חייב להיות חיובי.");
if(amount === null){ render(); return; }
income.amount = amount;
save();
}
};
});
}
function quickAddExtraIncome(){
const name = $("quickExtraName").value.trim();
const amount = requirePositiveAmount($("quickExtraAmount").value, "צריך להזין הכנסה חד־פעמית עם סכום חיובי.");
if(!name){ alert("צריך להזין שם הכנסה."); return; }
if(amount === null) 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?Math.max(0, 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 manualRows = currentExpenses();
const manualExpenseCount = manualRows.filter(e=>!isRefund(e)).length;
const refundCount = manualRows.filter(isRefund).length;
const autoCount = automaticMonthlyExpenses().length;
$("itemsCount").textContent=`${arr.length} פעולות (${autoCount} אוטומטיות, ${manualExpenseCount} הוצאות ידניות, ${refundCount} החזרים)`;
arr.forEach(e=>{
const tr=document.createElement("tr");
if(isRefund(e)) tr.className = "refund-row";
const typeLabel = e.auto ? "אוטומטי" : isRefund(e) ? "החזר / זיכוי" : "הוצאה";
const typePillClass = e.auto || isRefund(e) ? "good" : "bad";
const amountClass = isRefund(e) ? "amount refund" : "amount bad";
const actionCell = e.auto
? `<span class="pill good">אוטומטי</span>`
: `<div class="row-actions"><button type="button" class="danger" data-del="${e.id}" aria-label="מחיקת ${typeLabel} ${esc(e.name)}">מחיקה</button></div>`;
tr.innerHTML=`
<td data-label="סוג"><span class="pill ${typePillClass}">${typeLabel}</span></td>
<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="${amountClass}">${displayItemAmount(e)}</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" min="0" step="0.01" aria-label="סכום מסגרת ${esc(l.name)}" 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}" aria-label="שם מסגרת" value="${esc(l.name)}" ${isLockedמסגרת ? "readonly" : ""}></td>
<td data-label="קטגוריה"><select class="limit-group-edit" data-limit-group="${l.id}" aria-label="קטגוריית מסגרת ${esc(l.name)}" ${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 type="button" class="danger" data-dellimit="${l.id}" aria-label="מחיקת מסגרת ${esc(l.name)}" ${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}" aria-label="שם הכנסה" 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" min="0" step="0.01" aria-label="סכום הכנסה ${esc(i.name)}" value="${Number(i.amount||0)}"></td>
<td data-label="פעולות"><div class="row-actions"><button type="button" class="danger" data-delincome="${i.id}" aria-label="מחיקת הכנסה ${esc(i.name)}">מחיקה</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 type="button" class="danger" data-delextraincome="${i.id}" aria-label="מחיקת הכנסה חד־פעמית ${esc(i.name)}">מחיקה</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}" aria-label="שם תשלום זמני" value="${esc(t.name)}"></td>
<td data-label="כרטיס / מקור חיוב"><input class="temp-card-edit" data-temp-card="${t.id}" aria-label="כרטיס או מקור חיוב" value="${esc(card)}"></td>
<td data-label="תשלום חודשי"><input class="temp-amount-edit" data-temp-amount="${t.id}" type="number" inputmode="decimal" min="0" step="0.01" aria-label="סכום תשלום זמני ${esc(t.name)}" value="${Number(t.amount||0)}"></td>
<td data-label="כמה מתוך כמה">
<div class="temp-progress-edit">
<input data-temp-paid="${t.id}" type="number" inputmode="numeric" min="0" step="1" aria-label="תשלום נוכחי" value="${Number(t.paid||0)}">
<span>/</span>
<input data-temp-total="${t.id}" type="number" inputmode="numeric" min="1" step="1" aria-label="סך תשלומים" 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 type="button" class="danger" data-deltemp="${t.id}" aria-label="מחיקת תשלום זמני ${esc(t.name)}">מחיקה</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="";
const details=$("archiveDetails"); if(details) details.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.refunds || 0)} | יתרה מול הכנסות: ${money(a.balance)} | מול מסגרות: ${money(a.vsמסגרתs)}</div>
</div>
<button type="button" class="ghost" data-loadarchive="${a.id}">הצג פירוט</button>`;
box.appendChild(div);
});
box.querySelectorAll("[data-loadarchive]").forEach(b=>b.onclick=()=>renderArchiveDetails(b.dataset.loadarchive));
}
function renderArchiveDetails(id){
const details = $("archiveDetails");
if(!details) return;
const a = state.archive.find(x => x.id === id);
if(!a){ details.innerHTML = ""; return; }
const rows = (a.byמסגרת || []).map(r=>`<tr><td data-label="מסגרת">${esc(r.name)}</td><td data-label="קבוצה">${esc(r.group)}</td><td data-label="תקרה" class="amount">${money(r.limit)}</td><td data-label="בפועל" class="amount">${money(r.spent)}</td><td data-label="מצב" class="${Number(r.diff)>=0?'good':'bad'}">${Number(r.diff)>=0 ? 'יתרה ' + money(r.diff) : 'חריגה ' + money(Math.abs(r.diff))}</td></tr>`).join("") || `<tr><td colspan="5">אין נתוני מסגרות לחודש זה.</td></tr>`;
const expenseRows = (a.expenses || []).map(e=>{
const typeLabel = e.auto ? "אוטומטי" : e.type === "refund" ? "החזר / זיכוי" : "הוצאה";
const amountClass = e.type === "refund" ? "amount refund" : "amount bad";
const amountText = e.type === "refund" ? "−" + money(e.amount) : money(e.amount);
return `<tr><td data-label="סוג">${typeLabel}</td><td data-label="תאריך">${esc(e.date)}</td><td data-label="פירוט">${esc(e.name)}</td><td data-label="מסגרת">${esc(e.limitName)}</td><td data-label="אמצעי">${esc(e.method || "")}${e.auto ? " — אוטומטי" : ""}</td><td data-label="סכום" class="${amountClass}">${amountText}</td></tr>`;
}).join("") || `<tr><td colspan="6">אין פעולות לחודש זה.</td></tr>`;
const incomeRows = (a.extraIncomes || []).map(i=>`<tr><td data-label="שם">${esc(i.name)}</td><td data-label="שייך ל">${esc(i.owner || "")}</td><td data-label="סכום" class="amount good">${money(i.amount)}</td></tr>`).join("") || `<tr><td colspan="3">אין הכנסות חד־פעמיות לחודש זה.</td></tr>`;
details.innerHTML = `
<div class="card">
<div class="section-title"><h2>פירוט חודש סגור: ${esc(a.label)}</h2><button class="ghost" type="button" onclick="document.getElementById('archiveDetails').innerHTML=''">סגירה</button></div>
<div class="archive-detail-grid">
<div class="card stat"><div class="label">הכנסות</div><div class="value">${money(a.income)}</div></div>
<div class="card stat"><div class="label">מסגרות</div><div class="value">${money(a.limits)}</div></div>
<div class="card stat"><div class="label">הוצאות ברוטו</div><div class="value">${money(a.grossSpent || a.spent)}</div></div>
<div class="card stat"><div class="label">החזרים / זיכויים</div><div class="value good">${money(a.refunds || 0)}</div></div>
<div class="card stat"><div class="label">הוצאות נטו</div><div class="value">${money(a.spent)}</div></div>
<div class="card stat"><div class="label">יתרה מול הכנסות</div><div class="value ${Number(a.balance)>=0?'good':'bad'}">${money(a.balance)}</div></div>
</div>
<h2>מסגרות וחריגות</h2>
<table class="archive-detail-table"><thead><tr><th>מסגרת</th><th>קבוצה</th><th>תקרה</th><th>בפועל</th><th>מצב</th></tr></thead><tbody>${rows}</tbody></table>
<h2>הוצאות והחזרים בחודש</h2>
<table class="archive-detail-table"><thead><tr><th>סוג</th><th>תאריך</th><th>פירוט</th><th>מסגרת</th><th>אמצעי</th><th>סכום</th></tr></thead><tbody>${expenseRows}</tbody></table>
<h2>הכנסות חד־פעמיות</h2>
<table class="archive-detail-table"><thead><tr><th>שם</th><th>שייך ל</th><th>סכום</th></tr></thead><tbody>${incomeRows}</tbody></table>
</div>`;
}
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, Math.max(0,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*(Math.max(0,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(), refunds=effectiveRefundTotal(), grossSpent=grossSpentTotal(), balance=inc-spent, excessRefunds=excessRefundTotal();
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=>{
const typeLabel = e.auto ? "אוטומטי" : isRefund(e) ? "החזר / זיכוי" : "הוצאה";
const cls = isRefund(e) ? "good" : "bad";
return `<tr><td>${typeLabel}</td><td>${esc(e.date)}</td><td>${esc(e.name)}</td><td>${esc(e.limitName)}</td><td>${esc(e.method || "")}${e.auto ? " — אוטומטי" : ""}</td><td class="${cls}">${displayItemAmount(e)}</td></tr>`;
}).join("") || `<tr><td colspan="6">אין פעולות בפועל</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(auto-fit,minmax(150px,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(5,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 type="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 type="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(grossSpent)}</div></div>
<div class="card"><div class="label">החזרים / זיכויים שקוזזו</div><div class="value good">${money(refunds)}</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><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;
$("addRefundBtn").onclick=addRefund;
$("categoryInput").onchange=autoSelectPaymentMethod;
$("refundCategoryInput").onchange=autoSelectRefundMethod;
$("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=()=>{
const selected = $("periodPicker").value;
const archived = state.archive.find(a => a.periodKey === selected);
if(archived){
showTab("archive");
renderArchiveDetails(archived.id);
$("periodPicker").value = state.activePeriod;
return;
}
state.activePeriod=selected;
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, renderArchiveDetails};
enableDebugToolsIfRequested();
initFirebase().catch(err => {
console.warn("Firebase initialization failed:", err);
setStatus("ענן לא זמין");
});
</script>
</body>
</html>