⬅ חזרה לתיקייה
✏️ עריכת index_before_budget_20260715_174732.html
C:\TripServer\backups\Moneyapp\index_before_budget_20260715_174732.html
<!DOCTYPE html> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ניהול תקציב חופשה</title> <script type="module"> import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js"; import { getAuth, onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js"; import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager, collection, addDoc, setDoc, doc, getDocs, getDocFromServer, getDocsFromServer, deleteDoc, onSnapshot } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js"; 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 app = initializeApp(firebaseConfig); const db = initializeFirestore(app, { localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager() }) }); const auth = getAuth(app); const budgetILS = 93063; const clothingBudgetILS = 20000; const foodDailyBudgetILS = 750; const allowedEmails = [ "eitan.fridman@gmail.com", "eitan.friedman@gmail.com", "karin.enoshi@gmail.com" ].map(e => e.toLowerCase()); let pendingOfflineWrites = Number(localStorage.getItem("pendingOfflineWrites") || "0"); let appDataStarted = false; const BUDGET_APP_VERSION = "2026.07.15-FX-WALLETS-CHILDREN-v6"; function authMsg(text){ const el = document.getElementById("authMessage"); if(el) el.innerText = text || ""; } function isAllowedUser(user){ return !!(user && user.email && allowedEmails.includes(user.email.toLowerCase())); } window.loginBudgetApp = async function(){ const email = document.getElementById("authEmail")?.value.trim(); const password = document.getElementById("authPassword")?.value; if(!email || !password){ authMsg("יש להזין אימייל וסיסמה."); return; } try{ authMsg("מתחבר..."); await signInWithEmailAndPassword(auth, email, password); }catch(err){ authMsg("כניסה נכשלה. בדוק אימייל/סיסמה."); } }; window.createBudgetUser = async function(){ const email = document.getElementById("authEmail")?.value.trim(); const password = document.getElementById("authPassword")?.value; if(!email || !password){ authMsg("יש להזין אימייל וסיסמה ליצירת משתמש."); return; } if(!allowedEmails.includes(email.toLowerCase())){ authMsg("המייל הזה לא נמצא ברשימת המורשים בקובץ."); return; } try{ authMsg("יוצר משתמש..."); await createUserWithEmailAndPassword(auth, email, password); }catch(err){ if(String(err.code || "").includes("email-already-in-use")) authMsg("המשתמש כבר קיים. אפשר ללחוץ כניסה."); else if(String(err.code || "").includes("weak-password")) authMsg("הסיסמה חלשה מדי. צריך לפחות 6 תווים."); else authMsg("יצירת המשתמש נכשלה."); } }; window.logoutBudgetApp = async function(){ await signOut(auth); }; function showAppForUser(user){ document.body.classList.remove("authLocked"); const overlay = document.getElementById("authOverlay"); if(overlay) overlay.style.display = "none"; const topBar = document.getElementById("authTopBar"); if(topBar) topBar.style.display = "flex"; const label = document.getElementById("authUserLabel"); if(label) label.innerText = "מחובר: " + (user.email || ""); updateConnectionStatus(); if(typeof initAppTabs === 'function') setTimeout(initAppTabs, 80); } function showAuthScreen(message){ document.body.classList.add("authLocked"); const overlay = document.getElementById("authOverlay"); if(overlay) overlay.style.display = "flex"; const topBar = document.getElementById("authTopBar"); if(topBar) topBar.style.display = "none"; authMsg(message || ""); } function setSyncStatus(mode, text, sub){ const bar = document.getElementById("syncStatusBar"); const txt = document.getElementById("syncStatusText"); const subEl = document.getElementById("syncStatusSub"); if(!bar || !txt || !subEl) return; bar.classList.remove("online","offline","pending"); bar.classList.add(mode); txt.innerText = text; subEl.innerText = sub || ""; } function updateConnectionStatus(){ if(!navigator.onLine){ setSyncStatus("offline", "מצב אופליין", "אפשר להזין הוצאות. הן יישמרו במכשיר ויסונכרנו כשיחזור אינטרנט."); return; } if(pendingOfflineWrites > 0){ setSyncStatus("pending", "חזרת לרשת — מסנכרן נתונים...", "יש פעולות שהוזנו אופליין וממתינות לאישור Firebase."); return; } setSyncStatus("online", "מחובר ומסונכרן", "כל שינוי נשמר מול Firebase ומסתנכרן עם המכשיר השני."); } window.addEventListener("online", () => { updateConnectionStatus(); setTimeout(() => { pendingOfflineWrites = 0; localStorage.setItem("pendingOfflineWrites", "0"); updateConnectionStatus(); if(typeof renderAll === "function") renderAll(); }, 2500); }); window.addEventListener("offline", updateConnectionStatus); let lastCloudRefreshAt = 0; document.addEventListener("visibilitychange", () => { if(!document.hidden && auth.currentUser && isAllowedUser(auth.currentUser)){ const now = Date.now(); if(now - lastCloudRefreshAt > 10000){ lastCloudRefreshAt = now; forceFirebaseServerRefresh("חזרה לאפליקציה"); } } }); window.addEventListener("focus", () => { if(auth.currentUser && isAllowedUser(auth.currentUser)){ const now = Date.now(); if(now - lastCloudRefreshAt > 10000){ lastCloudRefreshAt = now; forceFirebaseServerRefresh("חזרה לחלון"); } } }); function markLocalWriteAttempt(){ if(!navigator.onLine){ pendingOfflineWrites += 1; localStorage.setItem("pendingOfflineWrites", String(pendingOfflineWrites)); } updateConnectionStatus(); } function markWriteConfirmed(){ if(navigator.onLine && pendingOfflineWrites > 0){ pendingOfflineWrites = Math.max(0, pendingOfflineWrites - 1); localStorage.setItem("pendingOfflineWrites", String(pendingOfflineWrites)); } updateConnectionStatus(); } async function trackedWrite(operation){ markLocalWriteAttempt(); try{ const result = await operation(); markWriteConfirmed(); return result; }catch(err){ updateConnectionStatus(); throw err; } } function trackedSetDoc(ref, data, options){ return trackedWrite(() => setDoc(ref, data, options)); } function trackedAddDoc(ref, data){ return trackedWrite(() => addDoc(ref, data)); } function trackedDeleteDoc(ref){ return trackedWrite(() => deleteDoc(ref)); } onAuthStateChanged(auth, async (user) => { if(!user){ showAuthScreen(""); return; } if(!isAllowedUser(user)){ await signOut(auth); showAuthScreen("המשתמש הזה לא מורשה להיכנס לאפליקציה."); return; } showAppForUser(user); if(!appDataStarted){ appDataStarted = true; startBudgetAppData(); } }); const tripBudgetDefaults = { japan_2026: { aliases: ["japan_2026", "יפן 2026", "Japan 2026"], dailyFoodBudget: 750, totalFoodBudget: 0, shoppingBudget: 20000, totalBudget: 93063 }, usa_2027: { aliases: [ "usa_2027", "ארה״ב 2027", "ארהב 2027", "ארצות הברית 2027", "טיול ארה״ב 2027", "טיול ארהב 2027", "USA 2027", "US 2027", "United States 2027" ], dailyFoodBudget: 900, totalFoodBudget: 27000, shoppingBudget: 30000, totalBudget: 246000 }, custom: { aliases: [], dailyFoodBudget: foodDailyBudgetILS, totalFoodBudget: 0, shoppingBudget: clothingBudgetILS, totalBudget: budgetILS } }; function normalizeTripText(v){ return String(v || "") .trim() .replace(/"/g, "״") .replace(/\s+/g, " ") .toLowerCase(); } function getTripDefaultBudgetProfile(trip = {}){ const rawKeys = [trip.id, trip.name, trip.tripName, activeTripId].filter(Boolean); const keysToCheck = rawKeys.map(normalizeTripText); const joined = keysToCheck.join(" "); if(joined.includes("usa") || joined.includes("us 2027") || joined.includes("ארהב") || joined.includes("ארה״ב") || joined.includes("ארצות הברית")){ return tripBudgetDefaults.usa_2027; } if(joined.includes("japan") || joined.includes("יפן")){ return tripBudgetDefaults.japan_2026; } for(const key of Object.keys(tripBudgetDefaults)){ const cfg = tripBudgetDefaults[key]; const aliases = [key, ...(cfg.aliases || [])].map(normalizeTripText); if(keysToCheck.some(k => aliases.includes(k))){ return cfg; } } return tripBudgetDefaults.custom; } function cleanBudgetNumber(value, fallback = 0){ const n = Number(value); return Number.isFinite(n) && n >= 0 ? n : Number(fallback || 0); } function normalizeBudgetSources(trip = getActiveTrip()){ const defaults = getTripDefaultBudgetProfile(trip); const raw = Array.isArray(trip.budgetSources) ? trip.budgetSources : []; const sources = raw .map((src, index) => ({ id: String(src?.id || `source_${index}`), amount: cleanBudgetNumber(src?.amount, 0), currency: ["ILS","USD","JPY","EUR","GBP","CAD"].includes(src?.currency) ? src.currency : "ILS", holdingType: src?.holdingType === "cash" ? "cash" : "bank" })) .filter(src => src.amount > 0); if(!sources.length){ sources.push({id:"legacy_ils", amount:cleanBudgetNumber(trip.budgetILS, defaults.totalBudget), currency:"ILS", holdingType:"bank"}); } return sources; } function getTripBudgetSourcesILS(trip = getActiveTrip()){ return normalizeBudgetSources(trip).reduce((sum, src) => sum + convertToILS(src.amount, src.currency, rates), 0); } function getTripBudgetConfig(trip = getActiveTrip()){ const defaults = getTripDefaultBudgetProfile(trip); return { totalBudget: getTripBudgetSourcesILS(trip), budgetSources: normalizeBudgetSources(trip), idoPrivateBudget: cleanBudgetNumber(trip.idoPrivateBudgetILS, 0), michaelPrivateBudget: cleanBudgetNumber(trip.michaelPrivateBudgetILS, 0), shoppingBudget: cleanBudgetNumber(trip.clothingBudgetILS, defaults.shoppingBudget), dailyFoodBudget: cleanBudgetNumber(trip.foodDailyBudgetILS, defaults.dailyFoodBudget), totalFoodBudget: cleanBudgetNumber(trip.totalFoodBudgetILS, defaults.totalFoodBudget || 0) }; } function budgetOwnerKey(e){ return ["home","ido","michael"].includes(e?.budgetOwner) ? e.budgetOwner : "home"; } function budgetOwnerLabel(owner){ return owner === "ido" ? "תקציב עידו פרטי" : owner === "michael" ? "תקציב מיכאל פרטי" : "תקציב הבית"; } function holdingTypeLabel(type){ return type === "cash" ? "מזומן" : "חשבון בנק"; } function expenseFundingType(e){ if(e.currency === "ILS") return "bank"; return e.paymentMethod === "cash" ? "cash" : "bank"; } function expenseRateAtEntry(e){ if(e.currency === "ILS") return 1; const map = {USD:"rateUSDAtEntry",JPY:"rateJPYAtEntry",EUR:"rateEURAtEntry",GBP:"rateGBPAtEntry",CAD:"rateCADAtEntry"}; return Number(e[map[e.currency]] || rates[e.currency] || 1); } let fundingLedgerCache = null; function buildFundingLedger(trip = getActiveTrip(), tripId = activeTripId, extraExpense = null, excludeExpenseId = null){ const sources = normalizeBudgetSources(trip).map(src => ({...src, remaining: Number(src.amount || 0)})); const sourceGroups = {}; sources.forEach(src => { const key = `${src.currency}::${src.holdingType}`; (sourceGroups[key] ||= []).push(src); }); const items = getTripExpenses(tripId) .filter(e => !excludeExpenseId || e.id !== excludeExpenseId) .map(e => ({...e})); if(extraExpense) items.push({...extraExpense, id: extraExpense.id || "__preview__"}); items.sort((a,b)=>(a.created||0)-(b.created||0) || (a.updated||0)-(b.updated||0)); const allocations = {}; let homeSpentILS = 0; let uncoveredForeignILS = 0; let totalFeeILS = 0; function consume(groupKey, amount){ let left = Math.max(0, Number(amount || 0)); let consumed = 0; for(const src of (sourceGroups[groupKey] || [])){ if(left <= 0) break; const take = Math.min(src.remaining, left); src.remaining -= take; left -= take; consumed += take; } return {consumed, uncovered:left}; } for(const e of items){ const owner = budgetOwnerKey(e); const amount = Math.max(0, Number(e.amount || 0)); const currency = e.currency || "ILS"; const baseRate = expenseRateAtEntry(e); let coveredNative = 0, uncoveredNative = 0, feeILS = 0, effectiveILS = 0; if(owner !== "home"){ const base = typeof e.baseIlsAtEntry === "number" ? e.baseIlsAtEntry : amount * baseRate; feeILS = typeof e.foreignCreditFeeILS === "number" ? e.foreignCreditFeeILS : (currency !== "ILS" && e.paymentMethod === "credit" ? base * FOREIGN_CREDIT_FEE_RATE : 0); effectiveILS = base + feeILS; }else if(currency === "ILS"){ const result = consume("ILS::bank", amount); if(result.uncovered > 0) consume("ILS::cash", result.uncovered); coveredNative = amount; effectiveILS = amount; }else{ const type = expenseFundingType(e); const result = consume(`${currency}::${type}`, amount); coveredNative = result.consumed; uncoveredNative = result.uncovered; const uncoveredBaseILS = uncoveredNative * baseRate; feeILS = uncoveredBaseILS * FOREIGN_CREDIT_FEE_RATE; effectiveILS = amount * baseRate + feeILS; if(uncoveredBaseILS + feeILS > 0){ const ilsCharge = uncoveredBaseILS + feeILS; const fromBank = consume("ILS::bank", ilsCharge); if(fromBank.uncovered > 0) consume("ILS::cash", fromBank.uncovered); } uncoveredForeignILS += uncoveredBaseILS; } if(owner === "home") homeSpentILS += effectiveILS; totalFeeILS += feeILS; allocations[e.id || "__preview__"] = {coveredNative, uncoveredNative, feeILS, effectiveILS, fundingType:expenseFundingType(e)}; } const balances = sources.map(src => ({...src, used: Math.max(0, src.amount-src.remaining), valueILS: convertToILS(src.remaining, src.currency, rates)})); const remainingTotalILS = balances.reduce((sum,src)=>sum+src.valueILS,0); return {sources:balances, allocations, homeSpentILS, remainingTotalILS, uncoveredForeignILS, totalFeeILS}; } function refreshFundingLedger(){ fundingLedgerCache = buildFundingLedger(); return fundingLedgerCache; } function getFundingAllocation(e){ return fundingLedgerCache?.allocations?.[e.id] || null; } function walletBalancesHtml(ledger = fundingLedgerCache){ if(!ledger) ledger = buildFundingLedger(); const rows = ledger.sources.filter(src => src.currency !== "ILS" || src.amount > 0).map(src => { const cls = src.remaining < 0 ? "red" : ""; return `<div class="walletBalanceLine ${cls}"><b>${currencyLabel(src.currency)} · ${holdingTypeLabel(src.holdingType)}:</b> ${fmtCurrencyByCode(src.remaining,src.currency)} <span class="small">מתוך ${fmtCurrencyByCode(src.amount,src.currency)} · שווי נוכחי ${fmtILS(src.valueILS)}</span></div>`; }); return rows.length ? rows.join("") : '<div class="small">לא הוגדרו קופות תקציב.</div>'; } function getOwnerExpenses(owner, tripId = activeTripId){ return getTripExpenses(tripId).filter(e => budgetOwnerKey(e) === owner); } function getOwnerSpentILS(owner, tripId = activeTripId){ return getOwnerExpenses(owner, tripId).reduce((sum,e) => sum + expenseILS(e), 0); } function getOwnerBudgetILS(owner, trip = getActiveTrip()){ const cfg = getTripBudgetConfig(trip); if(owner === "ido") return cfg.idoPrivateBudget; if(owner === "michael") return cfg.michaelPrivateBudget; return cfg.totalBudget; } function getCurrentTripBudgetConfig(){ return getTripBudgetConfig(getActiveTrip()); } function getTripExpenses(tripId){ return expenses.filter(e => (e.tripId || defaultTrip.id) === tripId); } function getTripTotalILS(tripId){ return getTripExpenses(tripId).reduce((s,e)=>s + expenseILS(e), 0); } function getTripShoppingILS(tripId){ return getTripExpenses(tripId) .filter(e => budgetOwnerKey(e) === "home") .filter(e => { const cat = (e.category || "").trim(); return cat.includes("קניות") || cat.includes("ביגוד"); }) .reduce((s,e)=>s + expenseILS(e), 0); } window.showAppTab = function(tabId, shouldScroll = true){ const map = { tabMain: "tab-main", tabSummary: "tab-summary", tabCharts: "tab-charts", tabCompare: "tab-compare", tabSettings: "tab-settings" }; const scrollMap = { tabMain: "tabMain", tabSummary: "summaryAnchor", tabCharts: "chartsInfoAnchor", tabCompare: "compareAnchor", tabSettings: "settingsExportAnchor" }; const cls = map[tabId] || "tab-main"; document.body.classList.remove("tab-main","tab-summary","tab-charts","tab-compare","tab-settings"); document.body.classList.add(cls); document.querySelectorAll(".tabPanel").forEach(p => p.classList.remove("active")); const panel = document.getElementById(tabId); if(panel) panel.classList.add("active"); document.querySelectorAll(".appTabs button").forEach(b => b.classList.remove("active")); const btn = document.querySelector(`[data-tab-btn="${tabId}"]`); if(btn) btn.classList.add("active"); localStorage.setItem("activeBudgetTab", tabId); if(typeof renderExpenses === "function") setTimeout(renderExpenses, 60); if(tabId === "tabCharts" && typeof renderDashboard === "function") setTimeout(renderDashboard, 120); if(typeof applyMobileTableLabels === "function") setTimeout(applyMobileTableLabels, 90); if(shouldScroll){ const target = document.getElementById(scrollMap[tabId] || tabId) || panel; if(target){ setTimeout(() => { try{ target.scrollIntoView({behavior:"smooth", block:"start"}); }catch(err){ target.scrollIntoView(true); } }, 60); } } }; function initAppTabs(){ const saved = localStorage.getItem("activeBudgetTab") || "tabMain"; showAppTab(saved, false); } const defaultRates = { USD: 2.9090, JPY: 0.018511, EUR: 3.4225, GBP: 3.9572, CAD: 2.1282, source: "בנק ישראל", updatedAtText: "11/05/2026", note: "USD 2.9090, JPY 0.018511, EUR 3.4225, GBP 3.9572, CAD 2.1282" }; let rates = {...defaultRates}; let expenses = []; let tripName = ""; let legacyFreezeRunning = false; const FOREIGN_CREDIT_FEE_RATE = 0.01; function paymentMethodLabel(method){ return method === "credit" ? "אשראי" : "מזומן"; } function isForeignCreditExpense(currency, paymentMethod){ return currency !== "ILS" && paymentMethod === "credit"; } function baseExpenseILSValue(e){ if(typeof e.baseIlsAtEntry === "number") return e.baseIlsAtEntry; if(typeof e.ilsAtEntry === "number" && typeof e.foreignCreditFeeILS === "number") return Math.max(0, e.ilsAtEntry - e.foreignCreditFeeILS); return convertToILS(e.amount, e.currency, rates); } function foreignCreditFeeILSValue(e){ if(typeof e.foreignCreditFeeILS === "number") return e.foreignCreditFeeILS; return 0; } function formatExpenseTotalCell(e){ const total = expenseILS(e); const fee = foreignCreditFeeILSValue(e); const base = baseExpenseILSValue(e); const a = getFundingAllocation(e); const fundingLine = a && e.currency !== "ILS" && budgetOwnerKey(e)==="home" ? `<br><span class="expenseSubLine">כוסה מהקופה: ${fmtCurrencyByCode(a.coveredNative,e.currency)}${a.uncoveredNative>0?` · המרה: ${fmtCurrencyByCode(a.uncoveredNative,e.currency)}`:""}</span>` : ""; if(fee > 0){ return `<b>${fmtILS(total)}</b><br><span class="expenseSubLine">בסיס: ${fmtILS(base)}<br>עמלת המרה 1%: ${fmtILS(fee)}</span>${fundingLine}`; } return `<b>${fmtILS(total)}</b>${fundingLine}`; } function renderExpenseFeePreview(){ const box = document.getElementById("fxFeePreview"); if(!box) return; const amount = Number(document.getElementById("amount")?.value || 0); const currency = document.getElementById("currency")?.value || "ILS"; const paymentMethod = document.getElementById("paymentMethod")?.value || "credit"; const budgetOwner = document.getElementById("budgetOwner")?.value || "home"; if(!amount || amount <= 0){ box.innerHTML = "הזן סכום כדי לראות מאיזו קופה תחויב ההוצאה והאם תחול עמלת המרה."; box.classList.remove("feeActive"); return; } const rateSnapshot = {USD:Number(rates.USD),JPY:Number(rates.JPY),EUR:Number(rates.EUR),GBP:Number(rates.GBP),CAD:Number(rates.CAD)}; const baseILS = convertToILS(amount,currency,rateSnapshot); if(budgetOwner !== "home"){ const feeILS = currency !== "ILS" && paymentMethod === "credit" ? baseILS*FOREIGN_CREDIT_FEE_RATE : 0; box.innerHTML = `<b>${budgetOwnerLabel(budgetOwner)}:</b> ${fmtILS(baseILS+feeILS)}${feeILS?`<br>עמלת המרה 1%: ${fmtILS(feeILS)}`:""}`; box.classList.toggle("feeActive",feeILS>0); return; } const previewExpense = {id:"__preview__",tripId:activeTripId,amount,currency,paymentMethod,budgetOwner:"home",created:Date.now(),baseIlsAtEntry:baseILS, rateUSDAtEntry:rateSnapshot.USD,rateJPYAtEntry:rateSnapshot.JPY,rateEURAtEntry:rateSnapshot.EUR,rateGBPAtEntry:rateSnapshot.GBP,rateCADAtEntry:rateSnapshot.CAD}; const ledger = buildFundingLedger(getActiveTrip(),activeTripId,previewExpense,editingExpenseId); const a = ledger.allocations.__preview__; if(currency === "ILS"){ box.innerHTML = `<b>חיוב מתקציב השקלים:</b> ${fmtILS(a.effectiveILS)}.`; box.classList.remove("feeActive"); return; } const type = paymentMethod === "cash" ? "מזומן" : "חשבון בנק"; const covered = fmtCurrencyByCode(a.coveredNative,currency); const uncovered = fmtCurrencyByCode(a.uncoveredNative,currency); box.innerHTML = `<b>${currencyLabel(currency)} · ${type}</b><br>מכוסה מהקופה: ${covered}<br>${a.uncoveredNative>0?`לא מכוסה בקופה: ${uncovered}<br>עמלת המרה 1% על החלק הלא מכוסה: ${fmtILS(a.feeILS)}<br>`:"אין עמלת המרה — קיימת יתרה מספקת בקופה.<br>"}<b>עלות ההוצאה לצורכי מעקב: ${fmtILS(a.effectiveILS)}</b>`; box.classList.toggle("feeActive",a.feeILS>0); } const fixedCategories = [ "כרטיסי סים", "אוכל", "קניות עידו", "קניות מיכאל", "ביגוד והנעלה", "תחבורה ציבורית", "אטרקציות", "תשלומים במזומן", "מלונות", "ביטוח נסיעות", "טיסות", "השכרת רכב", "דלק", "טיפים", "כביסות", "קרוז", "שונות" ]; let currentFilterType = "category"; let currentCategoryFilter = "ALL"; let currentDateFilter = "ALL"; let summaryCurrencyCode = "ILS"; let currentCompareMetric = "TOTAL"; let currentCompareDate = "ALL"; let trips = []; let activeTripId = localStorage.getItem("activeTripId") || "japan_2026"; const defaultTrip = { id: "japan_2026", name: "יפן 2026", budgetILS: 93063, clothingBudgetILS: 20000, foodDailyBudgetILS: 750, budgetSources: [{id:"legacy_ils", amount:93063, currency:"ILS", holdingType:"bank"}], idoPrivateBudgetILS: 0, michaelPrivateBudgetILS: 0, created: 0 }; function safeTripId(name){ const base = String(name || "trip").trim() .replace(/[\s]+/g, "_") .replace(/[^-a-zA-Z0-9_\-]/g, "") .slice(0, 40); return (base || "trip") + "_" + Date.now(); } function getActiveTrip(){ return trips.find(t => t.id === activeTripId) || defaultTrip; } function getActiveBudgetILS(){ return Number(getCurrentTripBudgetConfig().totalBudget || 0); } function getActiveClothingBudgetILS(){ return Number(getCurrentTripBudgetConfig().shoppingBudget || 0); } function getActiveFoodDailyBudgetILS(){ return Number(getCurrentTripBudgetConfig().dailyFoodBudget || 0); } function allExpenses(){ return expenses; } function activeExpenses(){ return expenses.filter(e => (e.tripId || "japan_2026") === activeTripId); } function visibleExpenses(){ let list = activeExpenses(); if(currentFilterType === "category" && currentCategoryFilter !== "ALL"){ list = list.filter(e => e.category === currentCategoryFilter); } if(currentFilterType === "date" && currentDateFilter !== "ALL"){ list = list.filter(e => e.date === currentDateFilter); } return list; } function activeFilterDescription(){ if(currentFilterType === "category" && currentCategoryFilter !== "ALL"){ return "קטגוריה: " + currentCategoryFilter; } if(currentFilterType === "date" && currentDateFilter !== "ALL"){ return "תאריך: " + currentDateFilter; } return "כל ההוצאות בטיול הפעיל"; } function fmtILS(n){ return "₪" + Number(n || 0).toLocaleString("he-IL", {minimumFractionDigits:2, maximumFractionDigits:2}); } function fmtUSD(n){ return "$" + Number(n || 0).toLocaleString("en-US", {minimumFractionDigits:2, maximumFractionDigits:2}); } function fmtJPY(n){ return "¥" + Number(n || 0).toLocaleString("ja-JP", {maximumFractionDigits:0}); } function fmtCurrencyByCode(n, code){ n = Number(n || 0); if(code === "ILS") return fmtILS(n); if(code === "USD") return "$" + n.toLocaleString("en-US", {minimumFractionDigits:2, maximumFractionDigits:2}); if(code === "JPY") return "¥" + n.toLocaleString("ja-JP", {maximumFractionDigits:0}); if(code === "EUR") return "€" + n.toLocaleString("de-DE", {minimumFractionDigits:2, maximumFractionDigits:2}); if(code === "GBP") return "£" + n.toLocaleString("en-GB", {minimumFractionDigits:2, maximumFractionDigits:2}); if(code === "CAD") return "C$" + n.toLocaleString("en-CA", {minimumFractionDigits:2, maximumFractionDigits:2}); return fmtILS(n); } function convertFromILS(amountILS, code){ amountILS = Number(amountILS || 0); if(code === "ILS") return amountILS; return amountILS / Number(rates[code] || 1); } function currencyLabel(code){ const labels = { ILS: "שקל ישראלי", USD: "דולר אמריקאי", JPY: "ין יפני", EUR: "יורו", GBP: "לירה סטרלינג", CAD: "דולר קנדי" }; return labels[code] || "שקל ישראלי"; } function todayText(){ return new Date().toLocaleDateString("he-IL"); } function isoToday(){ const d = new Date(); const offset = d.getTimezoneOffset(); const local = new Date(d.getTime() - offset * 60000); return local.toISOString().slice(0,10); } function heDateToISO(dateText){ const s = String(dateText || "").trim(); const m = s.match(/^(\d{1,2})[./-](\d{1,2})[./-](\d{2,4})$/); if(!m) return ""; const day = m[1].padStart(2,"0"); const month = m[2].padStart(2,"0"); const year = m[3].length === 2 ? "20" + m[3] : m[3]; return `${year}-${month}-${day}`; } function isoToHeDate(iso){ const s = String(iso || "").trim(); const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/); if(!m) return todayText(); return `${Number(m[3])}.${Number(m[2])}.${m[1]}`; } function dateSortKey(dateText){ return heDateToISO(dateText) || String(dateText || ""); } function expenseDateTextFromInput(){ const input = document.getElementById("expenseDate"); return input?.value ? isoToHeDate(input.value) : todayText(); } function ensureExpenseDateDefault(){ const input = document.getElementById("expenseDate"); if(input && !input.value) input.value = isoToday(); } function setExpenseDateInput(dateText){ const input = document.getElementById("expenseDate"); if(input) input.value = heDateToISO(dateText) || isoToday(); } function escapeHtml(text){ return String(text || "") .replaceAll("&","&") .replaceAll("<","<") .replaceAll(">",">") .replaceAll('"',""") .replaceAll("'","'"); } function convertToILS(amount, currency, rateSnapshot = rates){ amount = Number(amount || 0); if(currency === "ILS") return amount; if(currency === "USD") return amount * Number(rateSnapshot.USD); if(currency === "JPY") return amount * Number(rateSnapshot.JPY); if(currency === "EUR") return amount * Number(rateSnapshot.EUR); if(currency === "GBP") return amount * Number(rateSnapshot.GBP); if(currency === "CAD") return amount * Number(rateSnapshot.CAD); return amount; } function expenseILS(e){ const allocation = getFundingAllocation(e); if(allocation && budgetOwnerKey(e) === "home") return allocation.effectiveILS; if(typeof e.ilsAtEntry === "number") return e.ilsAtEntry; return convertToILS(e.amount, e.currency, rates); } async function ensureDefaultSettings(){ const ratesRef = doc(db, "settings", "rates"); const defaultTripRef = doc(db, "trips", "japan_2026"); try{ const [ratesSnap, tripSnap] = await Promise.all([ getDocFromServer(ratesRef), getDocFromServer(defaultTripRef) ]); if(!ratesSnap.exists()){ await trackedSetDoc(ratesRef, defaultRates, {merge:true}); } if(!tripSnap.exists()){ await trackedSetDoc(defaultTripRef, {...defaultTrip, id:"japan_2026", name:"יפן 2026"}, {merge:true}); } }catch(err){ console.warn("Default settings check skipped", err); } } window.saveTripName = async function(){ const name = document.getElementById("tripName").value.trim(); tripName = name; const trip = getActiveTrip(); const updatedTrip = { ...trip, id: activeTripId, name: name || trip.name || "טיול ללא שם" }; await trackedSetDoc(doc(db, "trips", activeTripId), updatedTrip, {merge:true}); trips = trips.some(t => t.id === activeTripId) ? trips.map(t => t.id === activeTripId ? {...t, ...updatedTrip} : t) : [...trips, updatedTrip]; renderTripSelector(); renderHomeTripSelector(); renderHomeSummary(); }; window.saveManualRates = async function(){ const usd = Number(document.getElementById("usdRateInput").value); const jpy = Number(document.getElementById("jpyRateInput").value); const eur = Number(document.getElementById("eurRateInput").value); const gbp = Number(document.getElementById("gbpRateInput").value); const cad = Number(document.getElementById("cadRateInput").value); if(!usd || !jpy || !eur || !gbp || !cad || usd <= 0 || jpy <= 0 || eur <= 0 || gbp <= 0 || cad <= 0){ alert("הכנס שערים תקינים לכל המטבעות"); return; } const manualRates = { USD: usd, JPY: jpy, EUR: eur, GBP: gbp, CAD: cad, source: "עדכון ידני", updatedAtText: new Date().toLocaleString("he-IL"), note: "שערים ידניים" }; await trackedSetDoc(doc(db, "settings", "rates"), manualRates, {merge:true}); rates = {...rates, ...manualRates}; renderRates(); renderAll(); alert("השערים נשמרו בהצלחה. הוצאות שכבר נוספו לא ישתנו."); }; async function persistActiveRates(newRates, statusText){ rates = {...rates, ...newRates}; await trackedSetDoc(doc(db, "settings", "rates"), { ...rates, source: newRates.source || "עדכון אינטרנט", updatedAtText: newRates.updatedAtText || new Date().toLocaleString("he-IL"), updatedAt: Date.now() }, {merge:true}); renderRates(); renderAll(); } window.updateRatesLive = async function(){ try{ document.getElementById("ratesStatus").innerText = "מעדכן שערים בזמן אמת..."; const res = await fetch("https://open.er-api.com/v6/latest/ILS", {cache:"no-store"}); const data = await res.json(); if(data.result !== "success" || !data.rates || !data.rates.USD || !data.rates.JPY || !data.rates.EUR || !data.rates.GBP || !data.rates.CAD){ throw new Error("Invalid rates response"); } const usdIls = 1 / Number(data.rates.USD); const jpyIls = 1 / Number(data.rates.JPY); const eurIls = 1 / Number(data.rates.EUR); const gbpIls = 1 / Number(data.rates.GBP); const cadIls = 1 / Number(data.rates.CAD); const liveRates = { USD: usdIls, JPY: jpyIls, EUR: eurIls, GBP: gbpIls, CAD: cadIls, source: "שער חי מהאינטרנט", updatedAtText: new Date().toLocaleString("he-IL"), note: "שער חי דרך API ציבורי" }; await trackedSetDoc(doc(db, "settings", "rates"), liveRates, {merge:true}); rates = {...rates, ...liveRates}; renderRates(); renderAll(); document.getElementById("ratesStatus").innerText = "השערים נשמרו. הוצאות קיימות נשארות לפי השער שבו הוזנו."; alert("השערים נשמרו בהצלחה. הוצאות שכבר נוספו לא השתנו."); }catch(err){ console.error(err); document.getElementById("ratesStatus").innerText = "עדכון חי נכשל. נשארים עם השערים האחרונים."; alert("לא הצלחתי לעדכן שערים בזמן אמת. בדוק חיבור אינטרנט או נסה שוב."); } }; let editingExpenseId = null; function buildExpensePayload(existing = {}){ const category = document.getElementById("category")?.value || ""; const note = document.getElementById("note")?.value.trim() || ""; const amount = Number(document.getElementById("amount")?.value); const currency = document.getElementById("currency")?.value || "ILS"; const paymentMethod = document.getElementById("paymentMethod")?.value || "credit"; const budgetOwner = document.getElementById("budgetOwner")?.value || "home"; const date = expenseDateTextFromInput(); if(!category || !amount || amount <= 0){ alert("נא לבחור קטגוריה ולהכניס סכום תקין."); return null; } const rateSnapshot = { USD: Number(rates.USD), JPY: Number(rates.JPY), EUR: Number(rates.EUR), GBP: Number(rates.GBP), CAD: Number(rates.CAD), source: rates.source || "", updatedAtText: rates.updatedAtText || "" }; const baseIlsAtEntry = convertToILS(amount, currency, rateSnapshot); const previewExpense = {id:"__preview__",tripId:activeTripId,amount,currency,paymentMethod,budgetOwner,created:existing.created||Date.now(),baseIlsAtEntry, rateUSDAtEntry:rateSnapshot.USD,rateJPYAtEntry:rateSnapshot.JPY,rateEURAtEntry:rateSnapshot.EUR,rateGBPAtEntry:rateSnapshot.GBP,rateCADAtEntry:rateSnapshot.CAD}; const previewLedger = buildFundingLedger(getActiveTrip(),activeTripId,previewExpense,editingExpenseId); const previewAllocation = previewLedger.allocations.__preview__ || {}; const foreignCreditFeeILS = Number(previewAllocation.feeILS || 0); const ilsAtEntry = Number(previewAllocation.effectiveILS || baseIlsAtEntry); return { ...existing, tripId: activeTripId, tripName: getActiveTrip().name || tripName || "", category, note, amount, currency, paymentMethod, fundingTypeAtEntry: expenseFundingType(previewExpense), coveredForeignAmountAtEntry: Number(previewAllocation.coveredNative || 0), uncoveredForeignAmountAtEntry: Number(previewAllocation.uncoveredNative || 0), budgetOwner, date, created: existing.created || Date.now(), updated: Date.now(), baseIlsAtEntry, foreignCreditFeeILS, foreignCreditFeeRateAtEntry: foreignCreditFeeILS > 0 ? FOREIGN_CREDIT_FEE_RATE : 0, ilsAtEntry, rateUSDAtEntry: rateSnapshot.USD, rateJPYAtEntry: rateSnapshot.JPY, rateEURAtEntry: rateSnapshot.EUR, rateGBPAtEntry: rateSnapshot.GBP, rateCADAtEntry: rateSnapshot.CAD, rateSourceAtEntry: rateSnapshot.source, rateUpdatedAtEntry: rateSnapshot.updatedAtText }; } function resetExpenseForm(){ editingExpenseId = null; const note = document.getElementById("note"); const amount = document.getElementById("amount"); const category = document.getElementById("category"); const currency = document.getElementById("currency"); const paymentMethod = document.getElementById("paymentMethod"); const budgetOwner = document.getElementById("budgetOwner"); const button = document.getElementById("expenseSubmitButton"); const cancel = document.getElementById("expenseCancelEditButton"); const title = document.getElementById("expenseFormTitle"); if(note) note.value = ""; if(amount) amount.value = ""; if(category) category.selectedIndex = 0; if(currency) currency.value = "ILS"; if(paymentMethod) paymentMethod.value = "credit"; if(budgetOwner) budgetOwner.value = "home"; ensureExpenseDateDefault(); renderExpenseFeePreview(); if(button) button.innerText = "הוסף הוצאה"; if(cancel) cancel.style.display = "none"; if(title) title.innerText = "➕ הוספת הוצאה"; } window.cancelExpenseEdit = function(){ resetExpenseForm(); }; window.startEditExpense = function(id){ const e = expenses.find(x => x.id === id); if(!e) return; editingExpenseId = id; const category = document.getElementById("category"); const note = document.getElementById("note"); const amount = document.getElementById("amount"); const currency = document.getElementById("currency"); const paymentMethod = document.getElementById("paymentMethod"); const budgetOwner = document.getElementById("budgetOwner"); const button = document.getElementById("expenseSubmitButton"); const cancel = document.getElementById("expenseCancelEditButton"); const title = document.getElementById("expenseFormTitle"); if(category) category.value = e.category || ""; if(note) note.value = e.note || ""; if(amount) amount.value = Number(e.amount || 0); if(currency) currency.value = e.currency || "ILS"; if(paymentMethod) paymentMethod.value = e.paymentMethod || (Number(e.foreignCreditFeeILS || 0) > 0 ? "credit" : "cash"); if(budgetOwner) budgetOwner.value = budgetOwnerKey(e); setExpenseDateInput(e.date); renderExpenseFeePreview(); if(button) button.innerText = "שמירת הוצאה"; if(cancel) cancel.style.display = "block"; if(title) title.innerText = "✏️ עריכת הוצאה"; document.getElementById("quickExpenseCard")?.scrollIntoView({behavior:"smooth", block:"start"}); }; window.addExpense = async function(){ const existing = editingExpenseId ? (expenses.find(e => e.id === editingExpenseId) || {}) : {}; const payload = buildExpensePayload(existing); if(!payload) return; if(editingExpenseId){ await trackedSetDoc(doc(db, "expenses", editingExpenseId), payload, {merge:true}); }else{ await trackedAddDoc(collection(db, "expenses"), payload); } const category = payload.category; const date = payload.date; resetExpenseForm(); setTimeout(() => checkWarnings(category, date), 800); }; async function freezeLegacyExpensesOnce(){ if(legacyFreezeRunning) return; legacyFreezeRunning = true; try{ const snap = await getDocs(collection(db, "expenses")); const updates = []; snap.forEach(d => { const e = d.data(); if(typeof e.ilsAtEntry !== "number" || !e.tripId){ const rateSnapshot = {USD: Number(rates.USD), JPY: Number(rates.JPY), EUR: Number(rates.EUR), GBP: Number(rates.GBP), CAD: Number(rates.CAD)}; updates.push(trackedSetDoc(doc(db, "expenses", d.id), { tripId: e.tripId || defaultTrip.id, tripName: e.tripName || "יפן 2026", ilsAtEntry: typeof e.ilsAtEntry === "number" ? e.ilsAtEntry : convertToILS(e.amount, e.currency, rateSnapshot), rateUSDAtEntry: rateSnapshot.USD, rateJPYAtEntry: rateSnapshot.JPY, rateEURAtEntry: rateSnapshot.EUR, rateGBPAtEntry: rateSnapshot.GBP, rateCADAtEntry: rateSnapshot.CAD, rateSourceAtEntry: "נעילה אוטומטית להוצאה ישנה", rateUpdatedAtEntry: new Date().toLocaleString("he-IL") }, {merge:true})); } }); if(updates.length) await Promise.all(updates); }catch(err){ console.error("legacy freeze failed", err); }finally{ legacyFreezeRunning = false; } } window.deleteExpense = async function(id){ if(confirm("למחוק את ההוצאה הזו?")){ await trackedDeleteDoc(doc(db, "expenses", id)); } }; window.clearTrip = async function(){ if(!confirm("לפני איפוס מומלץ לייצא PDF או HTML. למחוק את כל ההוצאות של הטיול הפעיל בלבד?")) return; const snap = await getDocs(collection(db, "expenses")); const deletes = []; snap.forEach(d => { const e = d.data(); if((e.tripId || defaultTrip.id) === activeTripId){ deletes.push(trackedDeleteDoc(doc(db, "expenses", d.id))); } }); await Promise.all(deletes); alert("הטיול הפעיל אופס בהצלחה."); }; function checkWarnings(category, date){ const clothingTotal = activeExpenses().filter(e => budgetOwnerKey(e) === "home" && e.category === "ביגוד והנעלה") .reduce((s,e)=>s + expenseILS(e), 0); const foodTodayILS = activeExpenses().filter(e => budgetOwnerKey(e) === "home" && e.category === "אוכל" && e.date === date) .reduce((s,e)=>s + expenseILS(e), 0); const messages = []; if(category === "ביגוד והנעלה" && clothingTotal > getActiveClothingBudgetILS()){ messages.push("חריגה מתקציב ביגוד והנעלה: " + fmtILS(clothingTotal) + " מתוך " + fmtILS(getActiveClothingBudgetILS())); } if(category === "אוכל" && foodTodayILS > getActiveFoodDailyBudgetILS()){ messages.push("חריגה מתקציב אוכל יומי: " + fmtILS(foodTodayILS) + " מתוך " + fmtILS(getActiveFoodDailyBudgetILS())); } if(messages.length) alert(messages.join("\\n")); } function renderHomeTripSelector(){ const homeSelect = document.getElementById("homeTripSelector"); if(!homeSelect) return; const sorted = trips.slice().sort((a,b)=>(a.created||0)-(b.created||0)); homeSelect.innerHTML = sorted.map(t => `<option value="${escapeHtml(t.id)}">${escapeHtml(t.name || t.id)}</option>`).join(""); homeSelect.value = activeTripId; } window.switchTripFromHome = function(){ const homeSelect = document.getElementById("homeTripSelector"); if(!homeSelect) return; activeTripId = homeSelect.value || defaultTrip.id; localStorage.setItem("activeTripId", activeTripId); const mainSelect = document.getElementById("tripSelector"); if(mainSelect) mainSelect.value = activeTripId; currentFilterType = "category"; currentCategoryFilter = "ALL"; currentDateFilter = "ALL"; renderTripSelector(); renderHomeTripSelector(); renderAll(); }; function renderTripSelector(){ const select = document.getElementById("tripSelector"); if(!select) return; const sorted = trips.slice().sort((a,b)=>(a.created||0)-(b.created||0)); select.innerHTML = sorted.map(t => `<option value="${escapeHtml(t.id)}">${escapeHtml(t.name || t.id)}</option>`).join(""); if(!sorted.find(t => t.id === activeTripId)){ activeTripId = "japan_2026"; localStorage.setItem("activeTripId", activeTripId); } select.value = activeTripId; const trip = getActiveTrip(); const tripNameInput = document.getElementById("tripName"); if(tripNameInput) tripNameInput.value = trip.name || ""; tripName = trip.name || ""; const cfg = getTripBudgetConfig(trip); const clothingInput = document.getElementById("tripClothingInput"); const foodInput = document.getElementById("tripFoodInput"); const totalFoodInput = document.getElementById("tripTotalFoodInput"); const idoBudgetInput = document.getElementById("tripIdoBudgetInput"); const michaelBudgetInput = document.getElementById("tripMichaelBudgetInput"); if(clothingInput) clothingInput.value = Number(cfg.shoppingBudget || 0); if(idoBudgetInput) idoBudgetInput.value = Number(cfg.idoPrivateBudget || 0); if(michaelBudgetInput) michaelBudgetInput.value = Number(cfg.michaelPrivateBudget || 0); if(foodInput) foodInput.value = Number(cfg.dailyFoodBudget || 0); if(totalFoodInput) totalFoodInput.value = Number(cfg.totalFoodBudget || 0); renderBudgetSourceRows(); const activeLabel = document.getElementById("activeTripLabel"); if(activeLabel){ activeLabel.innerHTML = `טיול פעיל: <b>${escapeHtml(trip.name || activeTripId)}</b>`; } const topBudget = document.getElementById("topBudgetLabel"); const topClothing = document.getElementById("topClothingBudgetLabel"); const topFood = document.getElementById("topFoodBudgetLabel"); if(topBudget) topBudget.innerText = fmtILS(getActiveBudgetILS()); if(topClothing) topClothing.innerText = fmtILS(getActiveClothingBudgetILS()); if(topFood) topFood.innerText = fmtILS(getActiveFoodDailyBudgetILS()); } window.toggleTripSettings = function(forceOpen){ const panel = document.getElementById("tripSettingsPanel"); if(!panel) return; if(forceOpen === true){ panel.classList.add("open"); }else{ panel.classList.toggle("open"); } }; window.switchTrip = function(){ const select = document.getElementById("tripSelector"); activeTripId = select?.value || defaultTrip.id; localStorage.setItem("activeTripId", activeTripId); currentFilterType = "category"; currentCategoryFilter = "ALL"; currentDateFilter = "ALL"; budgetSourcesRenderedTripId = null; renderTripSelector(); renderBudgetSourceRows(true); toggleTripSettings(true); renderAll(); }; window.createNewTrip = async function(){ const name = prompt("שם הטיול החדש:"); if(!name || !name.trim()) return; const id = safeTripId(name); const preset = getTripDefaultBudgetProfile({id, name:name.trim()}); const newTrip = { id, name: name.trim(), budgetILS: preset.totalBudget, budgetSources: [{id:"source_" + Date.now(), amount:preset.totalBudget, currency:"ILS", holdingType:"bank"}], idoPrivateBudgetILS: 0, michaelPrivateBudgetILS: 0, clothingBudgetILS: preset.shoppingBudget, foodDailyBudgetILS: preset.dailyFoodBudget, totalFoodBudgetILS: preset.totalFoodBudget || 0, created: Date.now() }; await trackedSetDoc(doc(db, "trips", id), newTrip, {merge:true}); trips = [...trips.filter(t => t.id !== id), newTrip].sort((a,b)=>(a.created||0)-(b.created||0)); activeTripId = id; localStorage.setItem("activeTripId", activeTripId); renderTripSelector(); renderHomeTripSelector(); renderAll(); }; let budgetSourcesRenderedTripId = null; function renderBudgetSourceRows(force = false){ const box = document.getElementById("budgetSourcesRows"); if(!box) return; if(!force && budgetSourcesRenderedTripId === activeTripId && box.children.length) { updateBudgetSourcesPreview(); return; } const trip = getActiveTrip(); const sources = normalizeBudgetSources(trip); box.innerHTML = sources.map((src,index) => ` <div class="budgetSourceRow" data-source-id="${escapeHtml(src.id)}"> <input class="budgetSourceAmount" type="number" inputmode="decimal" min="0" step="0.01" value="${Number(src.amount || 0)}" oninput="renderBudgetSourcesPreview()" aria-label="סכום מקור תקציב"> <select class="budgetSourceCurrency" onchange="renderBudgetSourcesPreview()" aria-label="מטבע מקור תקציב"> ${["ILS","USD","JPY","EUR","GBP","CAD"].map(code => `<option value="${code}" ${src.currency===code?"selected":""}>${currencyLabel(code)}</option>`).join("")} </select> <select class="budgetSourceHolding" onchange="renderBudgetSourcesPreview()" aria-label="סוג אחזקת מקור התקציב"> <option value="bank" ${src.holdingType!=="cash"?"selected":""}>חשבון בנק</option> <option value="cash" ${src.holdingType==="cash"?"selected":""}>מזומן</option> </select> <button type="button" class="danger smallBtn budgetSourceRemove" onclick="removeBudgetSourceRow(this)" ${sources.length===1?"disabled":""}>הסר</button> </div> `).join(""); budgetSourcesRenderedTripId = activeTripId; updateBudgetSourcesPreview(); } window.addBudgetSourceRow = function(){ const box = document.getElementById("budgetSourcesRows"); if(!box) return; const row = document.createElement("div"); row.className = "budgetSourceRow"; row.dataset.sourceId = "source_" + Date.now() + "_" + Math.random().toString(36).slice(2,7); row.innerHTML = ` <input class="budgetSourceAmount" type="number" inputmode="decimal" min="0" step="0.01" value="0" oninput="renderBudgetSourcesPreview()" aria-label="סכום מקור תקציב"> <select class="budgetSourceCurrency" onchange="renderBudgetSourcesPreview()" aria-label="מטבע מקור תקציב"> ${["ILS","USD","JPY","EUR","GBP","CAD"].map(code => `<option value="${code}">${currencyLabel(code)}</option>`).join("")} </select> <select class="budgetSourceHolding" onchange="renderBudgetSourcesPreview()" aria-label="סוג אחזקת מקור התקציב"> <option value="bank">חשבון בנק</option><option value="cash">מזומן</option> </select> <button type="button" class="danger smallBtn budgetSourceRemove" onclick="removeBudgetSourceRow(this)">הסר</button>`; box.appendChild(row); box.querySelectorAll(".budgetSourceRemove").forEach(b => b.disabled = false); updateBudgetSourcesPreview(); }; window.removeBudgetSourceRow = function(button){ const box = document.getElementById("budgetSourcesRows"); if(!box || box.children.length <= 1) return; button.closest(".budgetSourceRow")?.remove(); if(box.children.length === 1) box.querySelector(".budgetSourceRemove").disabled = true; updateBudgetSourcesPreview(); }; function readBudgetSourcesFromUI(){ return Array.from(document.querySelectorAll("#budgetSourcesRows .budgetSourceRow")).map((row,index) => ({ id: row.dataset.sourceId || `source_${Date.now()}_${index}`, amount: cleanBudgetNumber(row.querySelector(".budgetSourceAmount")?.value, 0), currency: row.querySelector(".budgetSourceCurrency")?.value || "ILS", holdingType: row.querySelector(".budgetSourceHolding")?.value === "cash" ? "cash" : "bank" })).filter(src => src.amount > 0); } window.updateBudgetSourcesPreview = function(){ const preview = document.getElementById("budgetSourcesPreview"); if(!preview) return; const sources = readBudgetSourcesFromUI(); const total = sources.reduce((sum,src) => sum + convertToILS(src.amount, src.currency, rates), 0); preview.innerHTML = sources.length ? `<b>שווי תקציב הבית כעת:</b> ${fmtILS(total)}<br>${sources.map(src=>`${currencyLabel(src.currency)} · ${holdingTypeLabel(src.holdingType)}: <b>${fmtCurrencyByCode(src.amount,src.currency)}</b>`).join("<br>")}<br><span class="small">השווי בשקלים מתעדכן לפי שערי המטבע הפעילים.</span>` : `<span class="red">יש להזין לפחות מקור תקציב אחד עם סכום גדול מאפס.</span>`; }; document.addEventListener("input", e => { if(e.target.matches(".budgetSourceAmount,.budgetSourceCurrency,.budgetSourceHolding")) updateBudgetSourcesPreview(); }); document.addEventListener("change", e => { if(e.target.matches(".budgetSourceAmount,.budgetSourceCurrency,.budgetSourceHolding")) updateBudgetSourcesPreview(); }); window.saveTripSettings = async function(){ const trip = getActiveTrip(); const name = document.getElementById("tripName")?.value.trim() || trip.name || "טיול ללא שם"; const cfg = getTripBudgetConfig(trip); const budgetSources = readBudgetSourcesFromUI(); if(!budgetSources.length){ alert("יש להזין לפחות מקור תקציב אחד עם סכום גדול מאפס."); return; } const budget = budgetSources.reduce((sum,src) => sum + convertToILS(src.amount, src.currency, rates), 0); const idoPrivateBudget = cleanBudgetNumber(document.getElementById("tripIdoBudgetInput")?.value, cfg.idoPrivateBudget); const michaelPrivateBudget = cleanBudgetNumber(document.getElementById("tripMichaelBudgetInput")?.value, cfg.michaelPrivateBudget); const clothing = cleanBudgetNumber(document.getElementById("tripClothingInput")?.value, cfg.shoppingBudget); const food = cleanBudgetNumber(document.getElementById("tripFoodInput")?.value, cfg.dailyFoodBudget); const totalFood = cleanBudgetNumber(document.getElementById("tripTotalFoodInput")?.value, cfg.totalFoodBudget || 0); const updatedTrip = { ...trip, id: activeTripId, name, budgetILS: budget, budgetSources, idoPrivateBudgetILS: idoPrivateBudget, michaelPrivateBudgetILS: michaelPrivateBudget, clothingBudgetILS: clothing, foodDailyBudgetILS: food, totalFoodBudgetILS: totalFood }; await trackedSetDoc(doc(db, "trips", activeTripId), updatedTrip, {merge:true}); trips = trips.some(t => t.id === activeTripId) ? trips.map(t => t.id === activeTripId ? {...t, ...updatedTrip} : t) : [...trips, updatedTrip]; budgetSourcesRenderedTripId = null; renderTripSelector(); renderBudgetSourceRows(true); renderHomeTripSelector(); renderAll(); alert("הגדרות הטיול נשמרו בהצלחה."); }; let compareTripsPanelOpen = false; window.toggleCompareTripsPanel = function(){ compareTripsPanelOpen = !compareTripsPanelOpen; const panel = document.getElementById("compareTripsPanel"); if(panel){ panel.classList.toggle("open", compareTripsPanelOpen); } }; function getSelectedCompareTripIds(){ const checked = Array.from(document.querySelectorAll(".compareTripCheck:checked")).map(i => i.value); if(checked.length) return checked.slice(0,5); return trips.slice(0, Math.min(2, trips.length)).map(t => t.id); } function getTripExpensesById(tripId){ return expenses.filter(e => (e.tripId || defaultTrip.id) === tripId); } function getAllComparisonCategories(){ const cats = new Set(fixedCategories); expenses.forEach(e => { if(e.category) cats.add(e.category); }); return Array.from(cats).sort(); } function getAllComparisonDates(){ const dates = new Set(); expenses.forEach(e => { if(e.date) dates.add(e.date); }); return Array.from(dates).sort((a,b)=>dateSortKey(a).localeCompare(dateSortKey(b))); } function renderComparisonTripChecks(){ const box = document.getElementById("compareTripsBox"); if(!box) return; const existing = new Set(Array.from(document.querySelectorAll(".compareTripCheck:checked")).map(i => i.value)); const sortedTrips = trips.slice().sort((a,b)=>(a.created||0)-(b.created||0)); if(existing.size < 2){ sortedTrips.forEach(t => { if(existing.size < 2) existing.add(t.id); }); } box.innerHTML = sortedTrips.map(t => { const checked = existing.has(t.id) ? "checked" : ""; return `<label class="tripCheckItem"><input class="compareTripCheck" type="checkbox" value="${escapeHtml(t.id)}" ${checked} onchange="handleCompareTripCheck(this)"> ${escapeHtml(t.name || t.id)}</label>`; }).join(""); const panel = document.getElementById("compareTripsPanel"); if(panel){ panel.classList.toggle("open", compareTripsPanelOpen); } } window.handleCompareTripCheck = function(input){ const checked = Array.from(document.querySelectorAll(".compareTripCheck:checked")); if(checked.length > 5){ input.checked = false; alert("אפשר לבחור עד 5 טיולים להשוואה."); return; } renderTripComparison(false); }; function renderCompareMetricOptions(){ const metricSelect = document.getElementById("compareMetric"); if(!metricSelect) return; const current = currentCompareMetric || metricSelect.value || "TOTAL"; const cats = getAllComparisonCategories(); metricSelect.innerHTML = `<option value="TOTAL">סה״כ הוצאות</option>` + `<option value="DATE">תאריך מסוים</option>` + `<option value="MOST_EXPENSIVE_DAY">יום הכי יקר</option>` + `<option value="CHEAPEST_DAY">יום הכי זול</option>` + cats.map(c => `<option value="CAT::${escapeHtml(c)}">${escapeHtml(c)}</option>`).join(""); const validValues = ["TOTAL","DATE","MOST_EXPENSIVE_DAY","CHEAPEST_DAY", ...cats.map(c => "CAT::" + c)]; currentCompareMetric = validValues.includes(current) ? current : "TOTAL"; metricSelect.value = currentCompareMetric; } function renderCompareDateOptions(){ const dateSelect = document.getElementById("compareDate"); if(!dateSelect) return; const current = currentCompareDate || dateSelect.value || "ALL"; const dates = getAllComparisonDates(); dateSelect.innerHTML = `<option value="ALL">כל התאריכים יחד</option>` + dates.map(d => `<option value="${escapeHtml(d)}">${escapeHtml(d)}</option>`).join(""); currentCompareDate = dates.includes(current) ? current : "ALL"; dateSelect.value = currentCompareDate; } function renderCompareSelectors(){ renderCompareMetricOptions(); renderCompareDateOptions(); const metric = document.getElementById("compareMetric")?.value || "TOTAL"; const dateBox = document.getElementById("compareDateBox"); if(dateBox) dateBox.style.display = metric === "DATE" ? "block" : "none"; } function metricLabel(metric, date){ if(metric === "TOTAL") return "סה״כ הוצאות"; if(metric === "DATE") return date && date !== "ALL" ? "תאריך " + date : "כל התאריכים"; if(metric === "MOST_EXPENSIVE_DAY") return "יום הכי יקר"; if(metric === "CHEAPEST_DAY") return "יום הכי זול"; if(metric.startsWith("CAT::")) return metric.replace("CAT::", ""); return metric; } function tripMetricTotal(tripId, metric, date){ let list = getTripExpensesById(tripId); if(metric === "DATE" && date !== "ALL"){ list = list.filter(e => e.date === date); } if(metric.startsWith("CAT::")){ const cat = metric.replace("CAT::", ""); list = list.filter(e => e.category === cat); } if(metric === "MOST_EXPENSIVE_DAY" || metric === "CHEAPEST_DAY"){ const byDate = {}; list.forEach(e => { if(!e.date) return; byDate[e.date] = (byDate[e.date] || 0) + expenseILS(e); }); const entries = Object.entries(byDate); if(!entries.length){ return { total:0, count:0, extra:"אין נתונים" }; } entries.sort((a,b) => metric === "MOST_EXPENSIVE_DAY" ? b[1]-a[1] : a[1]-b[1]); const [chosenDate, total] = entries[0]; const count = list.filter(e => e.date === chosenDate).length; return { total, count, extra: chosenDate }; } return { total: list.reduce((s,e)=>s + expenseILS(e), 0), count: list.length, extra: "" }; } window.setCompareMetricFromUI = function(){ const metricSelect = document.getElementById("compareMetric"); currentCompareMetric = metricSelect?.value || "TOTAL"; if(currentCompareMetric !== "DATE"){ currentCompareDate = "ALL"; } renderTripComparison(false); }; window.setCompareDateFromUI = function(){ const dateSelect = document.getElementById("compareDate"); currentCompareDate = dateSelect?.value || "ALL"; renderTripComparison(false); }; function renderTripComparison(rebuildChecks = true){ const head = document.getElementById("tripComparisonHead"); const body = document.getElementById("tripComparisonBody"); const status = document.getElementById("compareStatus"); if(!head || !body) return; if(rebuildChecks) renderComparisonTripChecks(); renderCompareSelectors(); const selectedIds = getSelectedCompareTripIds(); const selectedTrips = trips.filter(t => selectedIds.includes(t.id)); const metric = currentCompareMetric || document.getElementById("compareMetric")?.value || "TOTAL"; const date = currentCompareDate || document.getElementById("compareDate")?.value || "ALL"; if(selectedTrips.length < 2){ head.innerHTML = ""; body.innerHTML = `<tr><td>בחר לפחות 2 טיולים להשוואה.</td></tr>`; if(status) status.innerHTML = `<span class="red">צריך לבחור לפחות 2 טיולים ועד 5.</span>`; return; } if(selectedTrips.length > 5){ head.innerHTML = ""; body.innerHTML = `<tr><td>אפשר לבחור עד 5 טיולים בלבד.</td></tr>`; if(status) status.innerHTML = `<span class="red">בחרת יותר מדי טיולים.</span>`; return; } const label = metricLabel(metric, date); if(status){ status.innerHTML = `משווה: <b>${escapeHtml(label)}</b> בין <b>${selectedTrips.length}</b> טיולים.`; } if(metric === "MOST_EXPENSIVE_DAY" || metric === "CHEAPEST_DAY"){ head.innerHTML = `<tr><th>טיול</th><th>${escapeHtml(label)}</th><th>תאריך</th><th>מס׳ הוצאות</th></tr>`; body.innerHTML = selectedTrips.map(t => { const result = tripMetricTotal(t.id, metric, date); return ` <tr> <td><b>${escapeHtml(t.name || t.id)}</b></td> <td>${fmtILS(result.total)}</td> <td>${escapeHtml(result.extra || "")}</td> <td>${result.count}</td> </tr> `; }).join(""); return; } head.innerHTML = `<tr><th>טיול</th><th>${escapeHtml(label)}</th><th>מס׳ הוצאות</th></tr>`; body.innerHTML = selectedTrips.map(t => { const result = tripMetricTotal(t.id, metric, date); return ` <tr> <td><b>${escapeHtml(t.name || t.id)}</b></td> <td>${fmtILS(result.total)}</td> <td>${result.count}</td> </tr> `; }).join(""); } function renderRates(){ document.getElementById("usdRateTop").innerText = Number(rates.USD).toFixed(4); document.getElementById("jpyRateTop").innerText = Number(rates.JPY).toFixed(6); if(document.getElementById("jpy100RateTop")) document.getElementById("jpy100RateTop").innerText = (Number(rates.JPY) * 100).toFixed(4); if(document.getElementById("eurRateTop")) document.getElementById("eurRateTop").innerText = Number(rates.EUR).toFixed(4); if(document.getElementById("gbpRateTop")) document.getElementById("gbpRateTop").innerText = Number(rates.GBP).toFixed(4); if(document.getElementById("cadRateTop")) document.getElementById("cadRateTop").innerText = Number(rates.CAD).toFixed(4); document.getElementById("rateSource").innerText = rates.source || ""; document.getElementById("rateUpdated").innerText = rates.updatedAtText || ""; document.getElementById("usdRateInput").value = Number(rates.USD); document.getElementById("jpyRateInput").value = Number(rates.JPY); if(document.getElementById("eurRateInput")) document.getElementById("eurRateInput").value = Number(rates.EUR); if(document.getElementById("gbpRateInput")) document.getElementById("gbpRateInput").value = Number(rates.GBP); if(document.getElementById("cadRateInput")) document.getElementById("cadRateInput").value = Number(rates.CAD); renderExpenseFeePreview(); } function getCategoryTotals(){ const totals = {}; activeExpenses().forEach(e => { totals[e.category] = (totals[e.category] || 0) + expenseILS(e); }); return totals; } function getDailyTotals(){ const days = {}; activeExpenses().forEach(e => { if(!days[e.date]){ days[e.date] = { total:0, food:0, transport:0, attractions:0, shopping:0, hotels:0, insurance:0, flights:0, carRental:0, fuel:0, other:0, categories:{} }; } const ils = expenseILS(e); days[e.date].total += ils; days[e.date].categories[e.category] = (days[e.date].categories[e.category] || 0) + ils; if(e.category === "אוכל") days[e.date].food += ils; if(e.category === "תחבורה" || e.category === "תחבורה ציבורית") days[e.date].transport += ils; if(e.category === "אטרקציות") days[e.date].attractions += ils; if(e.category === "מלונות") days[e.date].hotels += ils; if(e.category === "ביטוח נסיעות") days[e.date].insurance += ils; if(e.category === "טיסות") days[e.date].flights += ils; if(e.category === "השכרת רכב") days[e.date].carRental += ils; if(e.category === "דלק") days[e.date].fuel += ils; if(["טיפים","כביסות", "קרוז", "שונות"].includes(e.category)) days[e.date].other += ils; if(["קניות עידו","קניות מיכאל","ביגוד והנעלה"].includes(e.category)) days[e.date].shopping += ils; }); return days; } window.setSummaryCurrencyFromUI = function(){ summaryCurrencyCode = document.getElementById("summaryCurrency")?.value || "ILS"; renderSummary(); }; function renderSummary(){ const totalILS = activeExpenses().reduce((s,e)=>s + expenseILS(e), 0); const homeSpentILS = getOwnerSpentILS("home"); const idoSpentILS = getOwnerSpentILS("ido"); const michaelSpentILS = getOwnerSpentILS("michael"); const remainingILS = getActiveBudgetILS() - homeSpentILS; const totalsILS = getCategoryTotals(); const clothingILS = totalsILS["ביגוד והנעלה"] || 0; const select = document.getElementById("summaryCurrency"); const selectedCurrency = select ? select.value : summaryCurrencyCode; summaryCurrencyCode = selectedCurrency || "ILS"; const totalDisplay = convertFromILS(totalILS, summaryCurrencyCode); const remainingDisplay = convertFromILS(remainingILS, summaryCurrencyCode); const clothingDisplay = convertFromILS(clothingILS, summaryCurrencyCode); const clothingBudgetDisplay = convertFromILS(getActiveClothingBudgetILS(), summaryCurrencyCode); let categoryHtml = ""; Object.keys(totalsILS).sort().forEach(cat => { const totalCatILS = totalsILS[cat]; const red = cat === "ביגוד והנעלה" && totalCatILS > getActiveClothingBudgetILS(); const converted = convertFromILS(totalCatILS, summaryCurrencyCode); categoryHtml += `<div class="${red ? "red" : ""}">${escapeHtml(cat)}: <b>${fmtCurrencyByCode(converted, summaryCurrencyCode)}</b></div>`; }); const box = document.getElementById("summaryFinal"); if(!box) return; box.innerHTML = ` <b>מטבע תצוגה:</b> ${currencyLabel(summaryCurrencyCode)}<br><br> <b>סה״כ הוצאות בכל התקציבים:</b> ${fmtCurrencyByCode(totalDisplay, summaryCurrencyCode)}<br> <b>הוצאות מתקציב הבית:</b> ${fmtCurrencyByCode(convertFromILS(homeSpentILS, summaryCurrencyCode), summaryCurrencyCode)}<br> <b>יתרת תקציב הבית:</b> <span class="${remainingILS < 0 ? "red" : "green"}">${fmtCurrencyByCode(remainingDisplay, summaryCurrencyCode)}</span><br><br> <b>תקציבים פרטיים:</b><br> עידו — נוצל ${fmtCurrencyByCode(convertFromILS(idoSpentILS, summaryCurrencyCode), summaryCurrencyCode)} מתוך ${fmtCurrencyByCode(convertFromILS(getOwnerBudgetILS("ido"), summaryCurrencyCode), summaryCurrencyCode)}; יתרה ${fmtCurrencyByCode(convertFromILS(getOwnerBudgetILS("ido")-idoSpentILS, summaryCurrencyCode), summaryCurrencyCode)}<br> מיכאל — נוצל ${fmtCurrencyByCode(convertFromILS(michaelSpentILS, summaryCurrencyCode), summaryCurrencyCode)} מתוך ${fmtCurrencyByCode(convertFromILS(getOwnerBudgetILS("michael"), summaryCurrencyCode), summaryCurrencyCode)}; יתרה ${fmtCurrencyByCode(convertFromILS(getOwnerBudgetILS("michael")-michaelSpentILS, summaryCurrencyCode), summaryCurrencyCode)}<br><br> <b>בקרת ביגוד, הנעלה, ואלקטרוניקה:</b><br> נוצל: <span class="${clothingILS > getActiveClothingBudgetILS() ? "red" : ""}">${fmtCurrencyByCode(clothingDisplay, summaryCurrencyCode)}</span> מתוך ${fmtCurrencyByCode(clothingBudgetDisplay, summaryCurrencyCode)}<br><br> <b>סיכום לפי קטגוריות:</b><br> ${categoryHtml || "אין עדיין הוצאות"} `; } function renderDaily(){ const tbody = document.getElementById("dailyBody"); const thead = document.querySelector("#dailyTable thead"); tbody.innerHTML = ""; const days = getDailyTotals(); const keys = Object.keys(days).sort((a,b)=>dateSortKey(a).localeCompare(dateSortKey(b))); const usedCats = Array.from(new Set(activeExpenses().map(e => e.category))).sort(); if(thead){ thead.innerHTML = `<tr><th>תאריך</th><th>סה״כ יומי</th>${usedCats.map(c => `<th>${escapeHtml(c)}</th>`).join("")}</tr>`; } if(!keys.length){ tbody.innerHTML = `<tr><td colspan="2">אין עדיין הוצאות</td></tr>`; return; } keys.forEach(date => { const d = days[date]; tbody.innerHTML += ` <tr> <td>${date}</td> <td><b>${fmtILS(d.total)}</b></td> ${usedCats.map(cat => { const val = d.categories[cat] || 0; const cls = cat === "אוכל" && val > getActiveFoodDailyBudgetILS() ? "red-cell" : ""; return `<td class="${cls}">${val ? fmtILS(val) : "—"}</td>`; }).join("")} </tr> `; }); } function compactDateForTable(dateText){ const s = String(dateText || ""); const m = s.match(/^(\d{1,2})[./-](\d{1,2})[./-](\d{2,4})$/); if(!m) return s; return `${m[1]}.${m[2]}`; } function compactCurrencyLabel(currency){ if(currency === "ILS") return "₪"; if(currency === "USD") return "$"; if(currency === "JPY") return "¥"; if(currency === "EUR") return "€"; if(currency === "GBP") return "£"; if(currency === "CAD") return "C$"; return currency || ""; } function compactRateLabel(e){ if(e.currency === "ILS") return "שקל"; if(e.currency === "USD") return Number(e.rateUSDAtEntry || rates.USD).toFixed(2); if(e.currency === "JPY") return Number(e.rateJPYAtEntry || rates.JPY).toFixed(4); if(e.currency === "EUR") return Number(e.rateEURAtEntry || rates.EUR).toFixed(2); if(e.currency === "GBP") return Number(e.rateGBPAtEntry || rates.GBP).toFixed(2); if(e.currency === "CAD") return Number(e.rateCADAtEntry || rates.CAD).toFixed(2); return ""; } function refreshDynamicExpensesTableWidth(){ const table = document.getElementById("expensesTable"); if(!table) return; table.style.width = "auto"; requestAnimationFrame(() => { table.style.width = "max-content"; }); } function renderExpenses(){ const tbody = document.getElementById("expensesBody"); if(!tbody) return; tbody.innerHTML = ""; updateFilterOptions(); renderCategoryTotals(); const list = visibleExpenses() .slice() .sort((a,b)=> dateSortKey(a.date).localeCompare(dateSortKey(b.date)) || (a.created||0)-(b.created||0)); if(!list.length){ const desc = activeFilterDescription(); tbody.innerHTML = `<tr><td colspan="9">אין הוצאות להצגה לפי הסינון הנוכחי</td></tr>`; const summary = document.getElementById("selectedCategorySummary"); if(summary) summary.innerHTML = `אין הוצאות להצגה — ${escapeHtml(desc)}.`; refreshDynamicExpensesTableWidth(); return; } let total = 0; list.forEach(e => { const ils = expenseILS(e); total += ils; const rateText = e.currency === "USD" ? "USD " + Number(e.rateUSDAtEntry || rates.USD).toFixed(4) : e.currency === "JPY" ? "JPY " + Number(e.rateJPYAtEntry || rates.JPY).toFixed(6) : e.currency === "EUR" ? "EUR " + Number(e.rateEURAtEntry || rates.EUR).toFixed(4) : e.currency === "GBP" ? "GBP " + Number(e.rateGBPAtEntry || rates.GBP).toFixed(4) : e.currency === "CAD" ? "CAD " + Number(e.rateCADAtEntry || rates.CAD).toFixed(4) : "שקל"; tbody.innerHTML += ` <tr> <td>${compactDateForTable(e.date)}</td> <td>${escapeHtml(budgetOwnerLabel(budgetOwnerKey(e)))}</td> <td>${escapeHtml(e.category)}</td> <td>${escapeHtml(e.note || "")}</td> <td>${Number(e.amount).toFixed(2)}</td> <td>${compactCurrencyLabel(e.currency)}<br><span class="expenseSubLine">${paymentMethodLabel(e.paymentMethod || (Number(e.foreignCreditFeeILS || 0) > 0 ? "credit" : "cash"))}</span></td> <td>${formatExpenseTotalCell(e)}</td> <td>${compactRateLabel(e)}</td> <td> <div class="rowActions"> <button class="secondaryBtn smallBtn" onclick="startEditExpense('${e.id}')">ערוך</button> <button class="danger smallBtn" onclick="deleteExpense('${e.id}')">×</button> </div> </td> </tr> `; }); tbody.innerHTML += ` <tr> <td colspan="6"><b>סה״כ כל ההוצאות המוצגות</b></td> <td><b>${fmtILS(typeof total !== "undefined" ? total : filteredTotal)}</b></td> <td colspan="2"></td> </tr> `; const summary = document.getElementById("selectedCategorySummary"); if(summary){ const desc = activeFilterDescription(); const totalActive = activeExpenses().reduce((s,e)=>s + expenseILS(e), 0); if(desc === "כל ההוצאות בטיול הפעיל"){ summary.innerHTML = `סה״כ הוצאות בטיול הפעיל: <b>${fmtILS(total)}</b>`; }else{ summary.innerHTML = `סה״כ מוצג לפי ${escapeHtml(desc)}: <b>${fmtILS(total)}</b><br><span class="small">סה״כ כל הטיול הפעיל: ${fmtILS(totalActive)}</span>`; } } if(typeof applyMobileTableLabels === "function"){ setTimeout(applyMobileTableLabels, 30); } refreshDynamicExpensesTableWidth(); } function updateFilterOptions(){ updateCategoryFilterOptions(); updateDateFilterOptions(); const typeSelect = document.getElementById("filterType"); if(typeSelect) typeSelect.value = currentFilterType; const catBox = document.getElementById("categoryFilterBox"); const dateBox = document.getElementById("dateFilterBox"); if(catBox) catBox.style.display = currentFilterType === "category" ? "block" : "none"; if(dateBox) dateBox.style.display = currentFilterType === "date" ? "block" : "none"; } function updateCategoryFilterOptions(){ const select = document.getElementById("categoryFilter"); if(!select) return; const fromExpenses = Array.from(new Set(activeExpenses().map(e => e.category))).filter(Boolean); const cats = Array.from(new Set([...fixedCategories, ...fromExpenses])).sort(); select.innerHTML = `<option value="ALL">כל הקטגוריות</option>` + cats.map(c => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join(""); if(currentCategoryFilter !== "ALL" && !cats.includes(currentCategoryFilter)){ currentCategoryFilter = "ALL"; } select.value = currentCategoryFilter; } function updateDateFilterOptions(){ const select = document.getElementById("dateFilter"); const chips = document.getElementById("activeDatesChips"); if(!select) return; const dates = Array.from(new Set(activeExpenses().map(e => e.date).filter(Boolean))).sort((a,b)=>dateSortKey(a).localeCompare(dateSortKey(b))); select.innerHTML = `<option value="ALL">כל התאריכים הפעילים</option>` + dates.map(d => `<option value="${escapeHtml(d)}">${escapeHtml(d)}</option>`).join(""); if(currentDateFilter !== "ALL" && !dates.includes(currentDateFilter)){ currentDateFilter = "ALL"; } select.value = currentDateFilter; if(chips){ if(!dates.length){ chips.innerHTML = `<span class="small">אין עדיין תאריכים עם פעילות.</span>`; }else{ chips.innerHTML = dates.map(d => `<button class="dateChip ${currentDateFilter === d ? "active" : ""}" onclick="selectDateFilter('${escapeHtml(d)}')">${escapeHtml(d)}</button>`).join(""); } } } window.setFilterTypeFromUI = function(){ const type = document.getElementById("filterType")?.value || "category"; currentFilterType = type; renderAll(); }; window.setCategoryFilterFromUI = function(){ const val = document.getElementById("categoryFilter")?.value || "ALL"; currentFilterType = "category"; currentCategoryFilter = val; renderAll(); }; window.setDateFilterFromUI = function(){ const val = document.getElementById("dateFilter")?.value || "ALL"; currentFilterType = "date"; currentDateFilter = val; renderAll(); }; window.selectDateFilter = function(dateValue){ currentFilterType = "date"; currentDateFilter = dateValue; renderAll(); }; window.onFilterTypeChange = function(){ currentFilterType = document.getElementById("filterType")?.value || "category"; renderAll(); }; window.clearCategoryFilter = function(){ currentFilterType = "category"; currentCategoryFilter = "ALL"; currentDateFilter = "ALL"; renderAll(); }; function renderCategoryTotals(){ const box = document.getElementById("categoryTotalsGrid"); if(!box) return; const totals = getCategoryTotals(); const entries = Object.entries(totals).sort((a,b)=>b[1]-a[1]); if(!entries.length){ box.innerHTML = `<div class="categoryTotalItem">אין עדיין הוצאות</div>`; return; } box.innerHTML = entries.map(([cat,total]) => ` <div class="categoryTotalItem"> ${escapeHtml(cat)}<br> <span>${fmtILS(total)}</span> </div> `).join(""); } window.toggleTheme = function(){ document.body.classList.toggle("darkMode"); localStorage.setItem("budgetTheme", document.body.classList.contains("darkMode") ? "dark" : "light"); }; function applySavedTheme(){ if(localStorage.getItem("budgetTheme") === "dark"){ document.body.classList.add("darkMode"); } } const chartPalette = [ "#2563eb","#16a34a","#f59e0b","#dc2626","#7c3aed","#0891b2", "#db2777","#65a30d","#ea580c","#475569","#0f766e","#9333ea", "#b45309","#1d4ed8","#be123c","#4d7c0f" ]; function setStatusClass(el, status){ if(!el) return; el.classList.remove("statusGood","statusWarn","statusBad"); if(status === "bad") el.classList.add("statusBad"); else if(status === "warn") el.classList.add("statusWarn"); else el.classList.add("statusGood"); } function getStatusByPercent(percent){ if(percent >= 100) return "bad"; if(percent >= 80) return "warn"; return "good"; } function drawEmptyCanvas(canvas, text){ if(!canvas) return; const ctx = canvas.getContext("2d"); ctx.clearRect(0,0,canvas.width,canvas.height); ctx.fillStyle = "#64748b"; ctx.font = "18px Arial"; ctx.textAlign = "center"; ctx.fillText(text, canvas.width/2, canvas.height/2); } function drawPieChart(totals){ const canvas = document.getElementById("categoryPie"); const legend = document.getElementById("categoryLegend"); if(!canvas || !legend) return; const entries = Object.entries(totals).filter(([k,v]) => v > 0).sort((a,b)=>b[1]-a[1]); const sum = entries.reduce((s,[k,v])=>s+v,0); const ctx = canvas.getContext("2d"); ctx.clearRect(0,0,canvas.width,canvas.height); legend.innerHTML = ""; if(!entries.length || sum <= 0){ drawEmptyCanvas(canvas, "אין עדיין נתונים לגרף"); return; } const cx = canvas.width / 2; const cy = canvas.height / 2; const radius = Math.min(canvas.width, canvas.height) * 0.34; let start = -Math.PI / 2; entries.forEach(([cat,val], i) => { const slice = (val / sum) * Math.PI * 2; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, radius, start, start + slice); ctx.closePath(); ctx.fillStyle = chartPalette[i % chartPalette.length]; ctx.fill(); start += slice; }); entries.slice(0,10).forEach(([cat,val], i) => { const pct = ((val/sum)*100).toFixed(1); const div = document.createElement("div"); div.className = "legendItem"; div.innerHTML = `<span class="legendColor" style="background:${chartPalette[i % chartPalette.length]}"></span> <span>${escapeHtml(cat)} — ${fmtILS(val)} (${pct}%)</span>`; legend.appendChild(div); }); } function drawDailyBarChart(days){ const canvas = document.getElementById("dailyBar"); if(!canvas) return; const ctx = canvas.getContext("2d"); ctx.clearRect(0,0,canvas.width,canvas.height); const entries = Object.keys(days).sort((a,b)=>dateSortKey(a).localeCompare(dateSortKey(b))).map(d => [d, days[d].total]); if(!entries.length){ drawEmptyCanvas(canvas, "אין עדיין נתונים לגרף"); return; } const padding = 38; const chartW = canvas.width - padding * 2; const chartH = canvas.height - padding * 2; const maxVal = Math.max(...entries.map(e=>e[1]), 1); const barW = Math.max(8, chartW / entries.length * 0.65); const gap = chartW / entries.length; ctx.strokeStyle = "#cbd5e1"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(padding, padding); ctx.lineTo(padding, padding + chartH); ctx.lineTo(padding + chartW, padding + chartH); ctx.stroke(); entries.forEach(([date,val], i) => { const h = (val / maxVal) * chartH; const x = padding + i * gap + (gap - barW)/2; const y = padding + chartH - h; ctx.fillStyle = val >= 3000 ? "#dc2626" : val >= 2000 ? "#f59e0b" : "#16a34a"; ctx.fillRect(x, y, barW, h); }); ctx.fillStyle = "#334155"; ctx.font = "13px Arial"; ctx.textAlign = "center"; ctx.fillText("שיא יומי: " + fmtILS(maxVal), canvas.width/2, 20); } function renderDashboard(){ const totalILS = activeExpenses().reduce((s,e)=>s + expenseILS(e), 0); const homeSpentILS = getOwnerSpentILS("home"); const remaining = getActiveBudgetILS() - homeSpentILS; const percent = getActiveBudgetILS() > 0 ? (homeSpentILS / getActiveBudgetILS()) * 100 : 0; const status = getStatusByPercent(percent); const days = getDailyTotals(); const totals = getCategoryTotals(); const today = todayText(); const todayTotal = days[today]?.total || 0; const foodTodayILS = days[today]?.food || 0; const foodTodayILSDisplay = foodTodayILS; const clothing = totals["ביגוד והנעלה"] || 0; const activeDays = Object.keys(days).length || 1; const avgDaily = totalILS / activeDays; const elTotal = document.getElementById("dashTotal"); const elRemaining = document.getElementById("dashRemaining"); const elPercent = document.getElementById("dashPercent"); const elAvg = document.getElementById("dashAvgDaily"); const elToday = document.getElementById("dashToday"); const elFood = document.getElementById("dashFoodToday"); const elClothing = document.getElementById("dashClothing"); const elCount = document.getElementById("dashCount"); const bar = document.getElementById("budgetProgressBar"); const statusText = document.getElementById("budgetStatusText"); if(elTotal) elTotal.innerText = fmtILS(totalILS); if(elRemaining) elRemaining.innerText = fmtILS(remaining); if(elPercent) elPercent.innerText = percent.toFixed(1) + "%"; if(elAvg) elAvg.innerText = fmtILS(avgDaily); if(elToday) elToday.innerText = fmtILS(todayTotal); if(elFood) elFood.innerText = fmtILS(foodTodayILSDisplay); if(elClothing) elClothing.innerText = fmtILS(clothing); if(elCount) elCount.innerText = String(activeExpenses().length); setStatusClass(elRemaining, remaining < 0 ? "bad" : status); setStatusClass(elPercent, status); setStatusClass(elFood, foodTodayILSDisplay > getActiveFoodDailyBudgetILS() ? "bad" : foodTodayILSDisplay >= getActiveFoodDailyBudgetILS()*0.8 ? "warn" : "good"); setStatusClass(elClothing, clothing > getActiveClothingBudgetILS() ? "bad" : clothing >= getActiveClothingBudgetILS()*0.8 ? "warn" : "good"); if(bar){ const capped = Math.min(percent, 100); bar.style.width = capped.toFixed(1) + "%"; bar.style.background = status === "bad" ? "#dc2626" : status === "warn" ? "#f59e0b" : "#16a34a"; } if(statusText){ if(percent >= 100){ statusText.innerHTML = `<span class="statusBad"><b>חריגה:</b> עברתם את התקציב הכולל ב־${fmtILS(Math.abs(remaining))}.</span>`; }else if(percent >= 80){ statusText.innerHTML = `<span class="statusWarn"><b>התראה:</b> ניצלתם ${percent.toFixed(1)}% מהתקציב. כדאי להאט קצב.</span>`; }else{ statusText.innerHTML = `<span class="statusGood"><b>תקין:</b> אתם עדיין בטווח התקציב.</span>`; } } drawPieChart(totals); drawDailyBarChart(days); } function renderTripBudgetSummary(){ const box = document.getElementById("tripBudgetSummaryBox"); if(!box) return; const cfg = getCurrentTripBudgetConfig(); const activeTrip = trips.find(t => t.id === activeTripId) || {}; const activeTripName = activeTrip.name || activeTripId || "טיול פעיל"; const ledger = fundingLedgerCache || buildFundingLedger(); const totalSpent = ledger.homeSpentILS; const idoSpent = getOwnerSpentILS("ido", activeTripId); const michaelSpent = getOwnerSpentILS("michael", activeTripId); const shoppingSpent = getTripShoppingILS(activeTripId); const totalLeft = ledger.remainingTotalILS; const shoppingLeft = cfg.shoppingBudget - shoppingSpent; function cls(v){ if(v < 0) return "tripBudgetBad"; if(v < (cfg.totalBudget * 0.15)) return "tripBudgetWarn"; return "tripBudgetGood"; } box.innerHTML = ` <div class="tripBudgetSub" style="margin-bottom:12px;font-weight:800;">טיול פעיל: ${escapeHtml(activeTripName)}</div> <div class="tripBudgetSummaryGrid"> <div class="tripBudgetCard"> <h4>💰 תקציב כולל לטיול</h4> <div class="tripBudgetMain ${cls(totalLeft)}">${fmtILS(totalLeft)}</div> <div class="tripBudgetSub"> תקציב כולל: ${fmtILS(cfg.totalBudget)}<br> נוצל עד עכשיו: ${fmtILS(totalSpent)}<br> <div class="walletBalances">${walletBalancesHtml(ledger)}</div> </div> </div> <div class="tripBudgetCard"> <h4>👦 תקציב עידו פרטי</h4> <div class="tripBudgetMain ${cfg.idoPrivateBudget-idoSpent < 0 ? "tripBudgetBad" : "tripBudgetGood"}">${fmtILS(cfg.idoPrivateBudget-idoSpent)}</div> <div class="tripBudgetSub">תקציב: ${fmtILS(cfg.idoPrivateBudget)}<br>נוצל: ${fmtILS(idoSpent)}</div> </div> <div class="tripBudgetCard"> <h4>👦 תקציב מיכאל פרטי</h4> <div class="tripBudgetMain ${cfg.michaelPrivateBudget-michaelSpent < 0 ? "tripBudgetBad" : "tripBudgetGood"}">${fmtILS(cfg.michaelPrivateBudget-michaelSpent)}</div> <div class="tripBudgetSub">תקציב: ${fmtILS(cfg.michaelPrivateBudget)}<br>נוצל: ${fmtILS(michaelSpent)}</div> </div> <div class="tripBudgetCard"> <h4>🛍️ תקציב קניות</h4> <div class="tripBudgetMain ${shoppingLeft < 0 ? "tripBudgetBad" : "tripBudgetGood"}">${fmtILS(shoppingLeft)}</div> <div class="tripBudgetSub"> תקציב קניות: ${fmtILS(cfg.shoppingBudget)}<br> נוצל בקניות: ${fmtILS(shoppingSpent)} </div> </div> <div class="tripBudgetCard"> <h4>🍽️ תקציב אוכל</h4> <div class="tripBudgetMain tripBudgetGood">${fmtILS(cfg.dailyFoodBudget)}</div> <div class="tripBudgetSub"> יעד יומי: ${fmtILS(cfg.dailyFoodBudget)}<br> ${cfg.totalFoodBudget ? "מסגרת אוכל כוללת: " + fmtILS(cfg.totalFoodBudget) : "ללא מסגרת אוכל כוללת"} </div> </div> </div> `; } function applyMobileTableLabels(){ document.querySelectorAll("table.mobileCards").forEach(table => { const headers = Array.from(table.querySelectorAll("thead th")).map(th => th.innerText.trim()); table.querySelectorAll("tbody tr").forEach(tr => { Array.from(tr.children).forEach((td, i) => { if(headers[i]) td.setAttribute("data-label", headers[i]); }); }); }); } function renderHomeSummary(){ const trip = trips.find(t => t.id === activeTripId) || {}; const cfg = getCurrentTripBudgetConfig ? getCurrentTripBudgetConfig() : {totalBudget:0,shoppingBudget:0,dailyFoodBudget:0}; const ledger = fundingLedgerCache || buildFundingLedger(); const totalSpent = ledger.homeSpentILS; const shoppingSpent = getTripShoppingILS ? getTripShoppingILS(activeTripId) : 0; const idoSpent = getOwnerSpentILS("ido", activeTripId); const michaelSpent = getOwnerSpentILS("michael", activeTripId); const totalLeft = Number(ledger.remainingTotalILS || 0); const shoppingLeft = Number(cfg.shoppingBudget || 0) - shoppingSpent; const idoLeft = Number(cfg.idoPrivateBudget || 0) - idoSpent; const michaelLeft = Number(cfg.michaelPrivateBudget || 0) - michaelSpent; const set = (id, value) => { const el = document.getElementById(id); if(el) el.innerText = value; }; set("homeTripName", trip.name || activeTripId || "—"); set("homeTotalLeft", fmtILS(totalLeft)); set("homeTotalUsed", "נוצל: " + fmtILS(totalSpent) + " מתוך " + fmtILS(cfg.totalBudget || 0)); const walletBox = document.getElementById("homeWalletBalances"); if(walletBox) walletBox.innerHTML = walletBalancesHtml(ledger); set("homeShoppingLeft", fmtILS(shoppingLeft)); set("homeShoppingUsed", "נוצל בקניות: " + fmtILS(shoppingSpent) + " מתוך " + fmtILS(cfg.shoppingBudget || 0)); set("homeFoodBudget", fmtILS(cfg.dailyFoodBudget || 0)); set("homeFoodSub", cfg.totalFoodBudget ? "מסגרת אוכל כוללת: " + fmtILS(cfg.totalFoodBudget) : "יעד יומי פעיל"); set("homeIdoLeft", fmtILS(idoLeft)); set("homeIdoUsed", "נוצל: " + fmtILS(idoSpent) + " מתוך " + fmtILS(cfg.idoPrivateBudget || 0)); set("homeMichaelLeft", fmtILS(michaelLeft)); set("homeMichaelUsed", "נוצל: " + fmtILS(michaelSpent) + " מתוך " + fmtILS(cfg.michaelPrivateBudget || 0)); const totalEl = document.getElementById("homeTotalLeft"); const shopEl = document.getElementById("homeShoppingLeft"); const idoEl = document.getElementById("homeIdoLeft"); const michaelEl = document.getElementById("homeMichaelLeft"); if(totalEl) totalEl.style.color = totalLeft < 0 ? "#dc2626" : "#15803d"; if(shopEl) shopEl.style.color = shoppingLeft < 0 ? "#dc2626" : "#15803d"; if(idoEl) idoEl.style.color = idoLeft < 0 ? "#dc2626" : "#15803d"; if(michaelEl) michaelEl.style.color = michaelLeft < 0 ? "#dc2626" : "#15803d"; } function renderAll(){ refreshFundingLedger(); ensureExpenseDateDefault(); renderTripSelector(); renderRates(); renderSummary(); renderDaily(); renderExpenses(); renderDashboard(); renderTripBudgetSummary(); renderHomeSummary(); renderHomeTripSelector(); setTimeout(applyMobileTableLabels, 50); if(typeof initAppTabs === 'function') setTimeout(initAppTabs, 80); renderTripComparison(); } window.exportHTML = function(){ try{ const generated = new Date().toLocaleString("he-IL"); const activeTripTitle = getActiveTrip().name || tripName || "חופשה"; const expensesRows = activeExpenses().map(e => ` <tr> <td>${compactDateForTable(e.date)}</td> <td>${escapeHtml(budgetOwnerLabel(budgetOwnerKey(e)))}</td> <td>${escapeHtml(e.category)}</td> <td>${escapeHtml(e.note || "")}</td> <td>${Number(e.amount).toFixed(2)}</td> <td>${compactCurrencyLabel(e.currency)} / ${paymentMethodLabel(e.paymentMethod || (Number(e.foreignCreditFeeILS || 0) > 0 ? "credit" : "cash"))}</td> <td>${formatExpenseTotalCell(e)}</td> <td>${compactRateLabel(e)}</td> </tr> `).join(""); const report = `<!DOCTYPE html> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>דוח תקציב ${escapeHtml(activeTripTitle)}</title> <style> body{font-family:Arial,"Noto Sans Hebrew",sans-serif;direction:rtl;padding:20px;color:#111827;background:#fff} h1,h2{text-align:center} .meta{text-align:center;color:#475569;margin-bottom:18px} .summary{border:1px solid #d7dce5;border-radius:14px;padding:14px;margin:14px 0;line-height:1.8} table{width:100%;border-collapse:collapse;margin-top:14px;font-size:12px} th,td{border:1px solid #d7dce5;padding:8px;text-align:center;vertical-align:middle} th{background:#0b84f3;color:white} tr:nth-child(even) td{background:#fafafa} .walletBalances{margin-top:10px;padding-top:8px;border-top:1px dashed #cbd5e1;font-size:12px;line-height:1.55;text-align:right} .walletBalanceLine{margin:4px 0} .budgetSourceRow{grid-template-columns:minmax(100px,1fr) minmax(120px,1fr) minmax(115px,1fr) auto!important} @media(max-width:700px){.budgetSourceRow{grid-template-columns:1fr 1fr!important}.budgetSourceRow .budgetSourceRemove{grid-column:1/-1}} </style> </head> <body> <h1>דוח תקציב חופשה</h1> <h2>${escapeHtml(activeTripTitle)}</h2> <div class="meta">נוצר: ${generated}</div> <div class="summary">${document.getElementById("summaryFinal")?.innerHTML || ""}</div> <h2>סיכום יומי</h2> ${document.getElementById("dailyTable")?.outerHTML || ""} <h2>פירוט הוצאות</h2> <table> <thead><tr><th>תאריך</th><th>תקציב</th><th>קטגוריה</th><th>פירוט</th><th>סכום</th><th>מטבע/תשלום</th><th>סה״כ בשקלים</th><th>שער</th></tr></thead> <tbody>${expensesRows || `<tr><td colspan="8">אין הוצאות להצגה</td></tr>`}</tbody> </table> </body> </html>`; const blob = new Blob([report], {type:"text/html;charset=utf-8"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "דוח_תקציב_" + activeTripTitle + ".html"; document.body.appendChild(a); a.click(); a.remove(); }catch(err){ console.error("exportHTML failed", err); alert("ייצוא HTML נכשל. רענן את האפליקציה ונסה שוב."); } }; function buildPrintableBudgetReport(){ const activeTripTitle = getActiveTrip().name || tripName || "חופשה"; const generated = new Date().toLocaleString("he-IL"); const rows = activeExpenses().map(e => ` <tr> <td>${compactDateForTable(e.date)}</td> <td>${escapeHtml(e.category)}</td> <td>${escapeHtml(e.note || "")}</td> <td>${Number(e.amount).toFixed(2)}</td> <td>${compactCurrencyLabel(e.currency)} / ${paymentMethodLabel(e.paymentMethod || (Number(e.foreignCreditFeeILS || 0) > 0 ? "credit" : "cash"))}</td> <td>${formatExpenseTotalCell(e)}</td> <td>${compactRateLabel(e)}</td> </tr> `).join(""); return ` <div class="printReport"> <h1>דוח תקציב חופשה</h1> <h2>${escapeHtml(activeTripTitle)}</h2> <div class="meta">נוצר: ${generated}</div> <div class="summary">${document.getElementById("summaryFinal")?.innerHTML || ""}</div> <h2>פירוט הוצאות</h2> <table> <thead><tr><th>תאריך</th><th>תקציב</th><th>קטגוריה</th><th>פירוט</th><th>סכום</th><th>מטבע/תשלום</th><th>סה״כ בשקלים</th><th>שער</th></tr></thead> <tbody>${rows || `<tr><td colspan="8">אין הוצאות להצגה</td></tr>`}</tbody> </table> </div>`; } window.closePdfPreview = function(){ const overlay = document.getElementById("pdfPreviewOverlay"); if(overlay) overlay.remove(); const iframe = document.getElementById("pdfPrintFrame"); if(iframe) iframe.remove(); document.body.classList.remove("pdfPreviewOpen"); }; window.printPdfPreview = function(){ try{ const reportHtml = `<!DOCTYPE html> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>דוח תקציב</title> <style> html,body{background:#fff!important;color:#111827!important;margin:0} body{font-family:Arial,"Noto Sans Hebrew",sans-serif;direction:rtl;padding:10mm} h1,h2{text-align:center;margin:10px 0} .meta{text-align:center;color:#475569;margin-bottom:18px} .summary{border:1px solid #d7dce5;border-radius:14px;padding:14px;margin:14px 0;line-height:1.8} table{width:100%;border-collapse:collapse;margin-top:14px;font-size:12px} th,td{border:1px solid #d7dce5;padding:8px;text-align:center;vertical-align:middle} th{background:#0b84f3!important;color:white!important} tr:nth-child(even) td{background:#fafafa} </style> </head> <body>${buildPrintableBudgetReport()}</body> </html>`; let iframe = document.getElementById("pdfPrintFrame"); if(iframe) iframe.remove(); iframe = document.createElement("iframe"); iframe.id = "pdfPrintFrame"; iframe.style.position = "fixed"; iframe.style.left = "0"; iframe.style.bottom = "0"; iframe.style.width = "1px"; iframe.style.height = "1px"; iframe.style.opacity = "0"; iframe.style.border = "0"; iframe.style.pointerEvents = "none"; document.body.appendChild(iframe); const doc = iframe.contentWindow.document; doc.open(); doc.write(reportHtml); doc.close(); setTimeout(() => { try{ iframe.contentWindow.focus(); iframe.contentWindow.print(); }catch(err){ console.error("iframe print failed", err); alert("הדפסה נכשלה. נסה לפתוח את הדוח שוב או להשתמש בייצוא HTML."); } }, 800); }catch(err){ console.error("printPdfPreview failed", err); alert("הדפסה נכשלה. נסה שוב."); } }; window.exportPDF = function(){ try{ const old = document.getElementById("pdfPreviewOverlay"); if(old) old.remove(); const overlay = document.createElement("div"); overlay.id = "pdfPreviewOverlay"; overlay.innerHTML = ` <div class="pdfPreviewToolbar noPrint"> <button type="button" onclick="closePdfPreview()">⬅ חזרה לאפליקציה</button> <button type="button" class="export" onclick="printPdfPreview()">🖨️ הדפסה / שמירה כ-PDF</button> </div> <div class="pdfPreviewContent"> ${buildPrintableBudgetReport()} </div> `; document.body.appendChild(overlay); document.body.classList.add("pdfPreviewOpen"); setTimeout(() => window.scrollTo(0, 0), 50); }catch(err){ console.error("exportPDF failed", err); alert("פתיחת תצוגת PDF נכשלה. אפשר להשתמש בייצוא HTML כחלופה."); } }; applySavedTheme(); ["amount","currency","paymentMethod"].forEach(id => { const el = document.getElementById(id); if(el){ el.addEventListener("input", renderExpenseFeePreview); el.addEventListener("change", renderExpenseFeePreview); } }); setTimeout(renderExpenseFeePreview, 80); setTimeout(initAppTabs, 50); async function forceFirebaseServerRefresh(reason = "כניסה לאפליקציה"){ try{ setSyncStatus("pending", "מרענן נתונים מהענן...", "מבצע קריאה ישירה מ-Firebase ולא מסתמך רק על זיכרון מקומי."); const [ratesSnap, tripsSnap, expensesSnap] = await Promise.all([ getDocFromServer(doc(db, "settings", "rates")), getDocsFromServer(collection(db, "trips")), getDocsFromServer(collection(db, "expenses")) ]); if(ratesSnap.exists()){ rates = {...rates, ...ratesSnap.data()}; } trips = []; tripsSnap.forEach(d => trips.push({id:d.id, ...d.data()})); if(!trips.find(t => t.id === defaultTrip.id)){ trips.push(defaultTrip); } expenses = []; expensesSnap.forEach(d => expenses.push({id:d.id, ...d.data()})); renderAll(); setSyncStatus("online", "מחובר ומסונכרן", "הנתונים רועננו ישירות מהענן: " + reason); }catch(err){ console.error("forceFirebaseServerRefresh failed", err); setSyncStatus("pending", "הרענון הישיר נכשל", "ממשיך לעבוד עם הסנכרון הרגיל. בדוק חיבור אינטרנט ונסה שוב."); } } window.forceFirebaseServerRefresh = forceFirebaseServerRefresh; async function startBudgetAppData(){ await ensureDefaultSettings(); await forceFirebaseServerRefresh('כניסה לאפליקציה'); onSnapshot(doc(db, "settings", "rates"), async (snap) => { if(snap.exists()){ rates = {...rates, ...snap.data()}; } renderAll(); await freezeLegacyExpensesOnce(); }); onSnapshot(collection(db, "trips"), (snap) => { trips = []; snap.forEach(d => trips.push({id:d.id, ...d.data()})); if(!trips.find(t => t.id === defaultTrip.id)){ trips.push(defaultTrip); } renderTripSelector(); renderAll(); }); onSnapshot(collection(db, "expenses"), (snap) => { expenses = []; snap.forEach(d => expenses.push({id:d.id, ...d.data()})); renderAll(); }); } </script> <style> .budgetAppVersionBadge{ display:inline-block; margin-top:12px; padding:7px 12px; border-radius:999px; background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.35); color:#fff; font-size:12px; font-weight:900; direction:ltr; unicode-bidi:isolate; } .budgetSourceRow{display:grid;grid-template-columns:minmax(0,1fr) minmax(150px,.75fr) auto;gap:8px;align-items:end;margin-top:8px} .budgetSourceRow input,.budgetSourceRow select,.budgetSourceRow button{margin-top:0} @media(max-width:600px){.budgetSourceRow{grid-template-columns:1fr 1fr}.budgetSourceRow .budgetSourceRemove{grid-column:1/-1;width:100%}} *{box-sizing:border-box} html{direction:rtl} body{ font-family:Arial,"Noto Sans Hebrew",sans-serif; background:#f4f7fb; color:#111827; padding:15px; margin:0; } .container{ max-width:980px; margin:auto; background:#fff; padding:18px; border-radius:18px; box-shadow:0 2px 14px rgba(15,23,42,.10); } .card{ background:#eef6ff; border:1px solid #dbeafe; border-radius:16px; padding:14px; margin-top:14px; line-height:1.7; } h1,h2,h3{margin-top:0} input,select,button{ width:100%; box-sizing:border-box; padding:13px; margin-top:10px; border-radius:10px; border:1px solid #cbd5e1; font-size:16px; } button{ background:#007aff; color:white; border:none; font-weight:900; cursor:pointer; } button.export{background:#2e7d32} button.orange{background:#ef6c00} button.danger{background:#c62828} button.secondaryBtn,.secondaryBtn{background:#475569} .smallBtn{padding:6px;margin:0;font-size:12px} .small{font-size:13px;color:#555} .expenseSubLine{display:block;font-size:11px;line-height:1.25;color:#475569;font-weight:800;white-space:normal} .fxFeePreview{ margin-top:10px; padding:10px 12px; border-radius:14px; background:#f8fafc; border:1px solid #dbeafe; color:#334155; font-size:14px; line-height:1.45; font-weight:800; } .fxFeePreview.feeActive{background:#fff7ed;border-color:#fed7aa;color:#7c2d12} .rowActions{display:flex;gap:6px;justify-content:center;align-items:center;flex-wrap:wrap} .rowActions .smallBtn{width:auto;min-width:54px} label{display:block;margin-top:10px;color:#1f2937} #expenseCancelEditButton{margin-top:8px} .red{color:#b71c1c;font-weight:900} .green{color:#1b5e20;font-weight:900} .red-cell{background:#ffcdd2;color:#b71c1c;font-weight:900} .noPrint{} .authOverlay{ position:fixed; inset:0; z-index:999999; background:linear-gradient(135deg,#0f172a,#1e293b,#111827); display:flex; align-items:center; justify-content:center; padding:18px; direction:rtl; } .authCard{ width:100%; max-width:460px; background:#fff; border-radius:26px; padding:24px; box-shadow:0 22px 60px rgba(0,0,0,.32); } .authCard h1{margin:0 0 8px;font-size:30px;text-align:center} .authCard p{color:#475569;text-align:center;margin:0 0 18px;font-weight:700} .authCard label{font-weight:900;margin-top:10px;display:block} .authCard input{direction:ltr;text-align:left} .authCard button{background:linear-gradient(135deg,#2563eb,#06b6d4)} .authCard .authSecondary{background:#475569} .authMessage{margin-top:12px;text-align:center;font-weight:900;color:#991b1b;min-height:24px} .authHint{ margin-top:14px; background:#f8fafc; border:1px solid #e2e8f0; border-radius:14px; padding:12px; font-size:14px; color:#475569; line-height:1.45; } body.authLocked .container, body.authLocked #floatingBottomButton, body.authLocked #force-cloud-refresh-btn{display:none!important} .authTopBar,.syncStatusBar{ display:flex; align-items:center; justify-content:space-between; gap:10px; border-radius:16px; padding:10px 12px; margin:10px 0 14px; font-size:14px; font-weight:900; } .authTopBar{background:#ecfeff;border:1px solid #a5f3fc;color:#155e75} .authTopBar button{width:auto;padding:8px 12px;border-radius:12px;background:#0f766e} .syncStatusBar{background:#f8fafc;border:1px solid #cbd5e1;color:#334155} .syncStatusBar .dot{width:11px;height:11px;border-radius:999px;display:inline-block;margin-left:7px;background:#64748b} .syncStatusBar.online .dot{background:#16a34a} .syncStatusBar.offline .dot{background:#f97316} .syncStatusBar.pending .dot{background:#dc2626} .syncStatusBar small{display:block;font-size:12px;color:#64748b;font-weight:700;margin-top:2px} .proHeader{ padding:18px 16px; border-radius:28px; background:linear-gradient(135deg,#0f172a,#1e3a8a 58%,#0891b2); color:#fff; margin:12px 0 14px; box-shadow:0 16px 40px rgba(15,23,42,.28); } .proHeader h1{font-size:28px;margin:0;color:#fff} .proHeaderSub{margin-top:8px;font-weight:800;opacity:.9;line-height:1.35} .appVersionBadge{position:sticky;top:0;z-index:1200;display:inline-block;margin-top:10px;padding:6px 10px;border-radius:999px;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.28);font-size:12px;font-weight:900} .featureVerificationBox{margin-bottom:12px;padding:10px 12px;border-radius:12px;background:#ecfdf5;border:1px solid #86efac;color:#166534;font-size:13px;font-weight:900;line-height:1.45} .appTabs{ position:sticky; top:0; z-index:999; display:flex; gap:8px; overflow-x:auto; padding:12px 6px; margin:10px 0 16px; background:rgba(248,250,252,.96); backdrop-filter:blur(10px); border:1px solid #dbeafe; border-radius:18px; -webkit-overflow-scrolling:touch; } .appTabs button{ width:auto; min-width:max-content; flex:0 0 auto; border-radius:999px; padding:12px 18px; font-size:16px; background:#e2e8f0; color:#0f172a; margin:0; box-shadow:none; } .appTabs button.active{ background:linear-gradient(135deg,#2563eb,#06b6d4); color:#fff; } .tabPanel{display:none!important} .tabPanel.active{display:block!important} body.tab-main #tabMain, body.tab-summary #tabSummary, body.tab-charts #tabCharts, body.tab-compare #tabCompare, body.tab-settings #tabSettings{display:block!important} body.tab-main #tabSummary,body.tab-main #tabCharts,body.tab-main #tabCompare,body.tab-main #tabSettings, body.tab-summary #tabMain,body.tab-summary #tabCharts,body.tab-summary #tabCompare,body.tab-summary #tabSettings, body.tab-charts #tabMain,body.tab-charts #tabSummary,body.tab-charts #tabCompare,body.tab-charts #tabSettings, body.tab-compare #tabMain,body.tab-compare #tabSummary,body.tab-compare #tabCharts,body.tab-compare #tabSettings, body.tab-settings #tabMain,body.tab-settings #tabSummary,body.tab-settings #tabCharts,body.tab-settings #tabCompare{display:none!important} .homeKpiGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:12px 0} .homeKpi{ background:#fff; border:1px solid #e5e7eb; border-radius:20px; padding:14px; box-shadow:0 8px 22px rgba(15,23,42,.06); } .homeKpiTitle{font-size:13px;color:#64748b;font-weight:900;margin-bottom:8px} .homeKpiValue{font-size:22px;font-weight:1000;color:#0f172a} .homeKpiSub{font-size:12px;color:#64748b;margin-top:6px;line-height:1.35;font-weight:700} .homeQuickActions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px} .homeQuickActions button{border-radius:18px;padding:14px 12px;font-weight:1000;font-size:15px;background:#e0f2fe;color:#075985} .homeQuickActions button.primary{background:linear-gradient(135deg,#2563eb,#06b6d4);color:#fff} .homeMiniNote{ margin-top:12px; padding:12px; border-radius:16px; background:#f8fafc; border:1px solid #e2e8f0; color:#475569; font-size:13px; font-weight:800; line-height:1.45; } table{width:100%;border-collapse:collapse;margin-top:12px;background:white;font-size:13px} th,td{border:1px solid #ddd;padding:8px;text-align:center} th{background:#007aff;color:white} .tableWrap{overflow-x:auto} .expensesLedgerWrap{ width:100%; max-height:520px; overflow:auto; -webkit-overflow-scrolling:touch; margin-top:14px; background:#fff; border:1px solid #d7dce5; } #expensesTable.expensesLedgerTable{ width:max-content; min-width:100%; max-width:none; table-layout:auto; border-collapse:collapse; direction:rtl; background:#fff; color:#111827; font-size:clamp(9px,2.45vw,13px); } #expensesTable.expensesLedgerTable :is(thead,tbody,tr,th,td){display:revert} #expensesTable.expensesLedgerTable :is(th,td){ border:1px solid #d7dce5; padding:6px 4px; text-align:center; vertical-align:middle; line-height:1.18; white-space:nowrap; word-break:normal; overflow-wrap:normal; } #expensesTable.expensesLedgerTable thead th{ background:#0b84f3; color:#fff; font-weight:900; position:sticky; top:0; z-index:6; } #expensesTable.expensesLedgerTable tbody tr:nth-child(odd) td{background:#fff} #expensesTable.expensesLedgerTable tbody tr:nth-child(even) td{background:#fafafa} #expensesTable.expensesLedgerTable :is(th,td):nth-child(1){min-width:54px} #expensesTable.expensesLedgerTable :is(th,td):nth-child(2){min-width:64px;font-weight:800} #expensesTable.expensesLedgerTable :is(th,td):nth-child(3){min-width:120px} #expensesTable.expensesLedgerTable :is(th,td):nth-child(4){min-width:58px} #expensesTable.expensesLedgerTable :is(th,td):nth-child(5){min-width:38px} #expensesTable.expensesLedgerTable :is(th,td):nth-child(6){min-width:70px;font-weight:900} #expensesTable.expensesLedgerTable :is(th,td):nth-child(7){min-width:48px} #expensesTable.expensesLedgerTable :is(th,td):nth-child(8){min-width:42px} #expensesTable.expensesLedgerTable td::before{content:none!important;display:none!important} #expensesTable.expensesLedgerTable .danger.smallBtn{ background:#d32027; color:#fff; border:none; border-radius:10px; width:100%; max-width:42px; height:28px; padding:0; font-size:12px; line-height:28px; font-weight:900; } .dashboardGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:14px} .dashCard{ background:#fff; border:1px solid #dbeafe; border-radius:16px; padding:14px; box-shadow:0 2px 8px rgba(0,0,0,.06); } .dashTitle{font-size:13px;color:#475569;font-weight:bold} .dashValue{font-size:22px;font-weight:900;margin-top:4px} .statusGood{color:#1b5e20} .statusWarn{color:#ef6c00} .statusBad{color:#b71c1c} .progressWrap{width:100%;height:22px;background:#e5e7eb;border-radius:999px;overflow:hidden;margin-top:10px;border:1px solid #cbd5e1} .progressBar{height:100%;width:0%;background:#16a34a;transition:width .25s ease,background .25s ease} .dashboardNote{margin-top:8px;font-size:14px;color:#475569} .chartGrid{display:grid;grid-template-columns:1fr;gap:14px;margin-top:12px} .chartBox{background:#fff;border:1px solid #e5e7eb;border-radius:16px;padding:14px} .chartCanvas{width:100%;max-width:100%;height:220px} .legendItem{display:flex;align-items:center;gap:8px;margin:5px 0;font-size:14px} .legendColor{width:14px;height:14px;border-radius:4px;display:inline-block;flex:0 0 auto} .filterPanel,.compareControls{ background:#f8fafc; border:1px solid #dbeafe; border-radius:14px; padding:12px; margin-top:12px; } .categoryTotalsGrid{display:grid;grid-template-columns:1fr;gap:8px;margin-top:10px} .categoryTotalItem{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:10px;font-weight:800} .dateChips{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px} .dateChip{ width:auto; padding:8px 10px; border-radius:999px; background:#e0f2fe; color:#075985; border:1px solid #7dd3fc; font-size:13px; font-weight:800; } .dateChip.active{background:#2563eb;color:white;border-color:#2563eb} .tripCheckGrid{display:grid;grid-template-columns:1fr;gap:8px;margin-top:8px} .tripCheckItem{ background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:10px; display:flex; align-items:center; gap:8px; font-weight:800; } .tripCheckItem input{width:auto;margin:0} .compareResultCard{margin-top:12px;background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:12px} .collapsiblePanel{display:none;margin-top:12px} .collapsiblePanel.open{display:block} .tripActions{display:flex;gap:8px;flex-wrap:wrap} .tripActions button{flex:1 1 160px} .tripBudgetSummaryBox{ margin-top:14px; padding:16px; border-radius:18px; background:linear-gradient(135deg,#eff6ff 0%,#f8fafc 100%); border:1px solid #bfdbfe; } .tripBudgetSummaryGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px} .tripBudgetCard{background:white;border-radius:16px;padding:14px;border:1px solid #dbeafe} .tripBudgetCard h4{margin:0 0 10px;font-size:16px} .tripBudgetMain{font-size:22px;font-weight:900;margin-bottom:6px} .tripBudgetSub{font-size:13px;color:#475569;line-height:1.45} .tripBudgetGood{color:#15803d} .tripBudgetWarn{color:#d97706} .tripBudgetBad{color:#dc2626} .ratesHeadline{ background:#fff8e1; border:2px solid #ffca28; border-radius:12px; padding:12px; margin-top:10px; font-size:17px; } .rateLine{ display:flex; justify-content:space-between; align-items:center; gap:10px; direction:rtl; border-bottom:1px solid rgba(0,0,0,.08); padding:6px 0; } .rateLine:last-child{border-bottom:0} .rateLine span:first-child{font-weight:700} .rateLine b{white-space:nowrap} .rateInputWrap{margin-top:12px} .rateInputWrap label{display:block;font-weight:900;margin:10px 0 4px} .budgetSourceRow{display:grid;grid-template-columns:minmax(0,1fr) minmax(150px,1fr) auto;gap:8px;align-items:end;margin-top:8px} .budgetSourceRow input,.budgetSourceRow select,.budgetSourceRow button{margin-top:0} .budgetSourceRow .budgetSourceRemove{width:auto;min-width:62px;height:48px} .budgetSourceRow .budgetSourceRemove:disabled{opacity:.45;cursor:not-allowed} @media(max-width:520px){.budgetSourceRow{grid-template-columns:1fr 1fr}.budgetSourceRow .budgetSourceRemove{grid-column:1/-1;width:100%}} .appCreatorFooter{ margin-top:40px; padding:18px; border-top:1px solid #dbeafe; text-align:center; font-size:14px; color:#475569; opacity:.95; } .appCreatorFooter .creatorName{font-weight:800;font-size:16px;margin-bottom:6px} #force-cloud-refresh-btn{ position:fixed; left:14px; bottom:calc(126px + env(safe-area-inset-bottom,0px)); z-index:2147483647; min-width:118px; min-height:42px; padding:10px 14px; border:1px solid rgba(255,255,255,.35); border-radius:999px; background:linear-gradient(135deg,#0f766e,#2563eb); color:#fff; font-size:14px; font-weight:900; box-shadow:0 6px 18px rgba(0,0,0,.28); } #pdfPreviewOverlay{ position:fixed; inset:0; z-index:2147483647; display:flex; flex-direction:column; background:#f8fafc; color:#111827; direction:rtl; overflow:hidden; height:100vh; } body.pdfPreviewOpen{ overflow:hidden; height:100vh; } .pdfPreviewToolbar{ flex:0 0 auto; display:flex; gap:10px; padding:12px; background:rgba(248,250,252,.96); border-bottom:1px solid #d7dce5; backdrop-filter:blur(10px); z-index:10; } .pdfPreviewToolbar button{ width:auto; flex:1; margin:0; border-radius:14px; padding:12px; font-weight:900; } .pdfPreviewContent{ flex:1 1 auto; min-height:0; overflow-y:auto; overflow-x:hidden; -webkit-overflow-scrolling:touch; padding:18px; } .printReport{ max-width:980px; margin:0 auto; background:#fff; border:1px solid #d7dce5; border-radius:16px; padding:18px; } .printReport h1,.printReport h2{text-align:center;margin:10px 0} .printReport .meta{text-align:center;color:#475569;margin-bottom:18px} .printReport .summary{border:1px solid #d7dce5;border-radius:14px;padding:14px;margin:14px 0;line-height:1.8} .printReport table{width:100%;border-collapse:collapse;margin-top:14px;font-size:12px;background:#fff} .printReport th,.printReport td{border:1px solid #d7dce5;padding:8px;text-align:center;vertical-align:middle;color:#111827} .printReport th{background:#0b84f3!important;color:#fff!important} .printReport tr:nth-child(even) td{background:#fafafa} body.pdfPreviewOpen .container, body.pdfPreviewOpen #authOverlay, body.pdfPreviewOpen #force-cloud-refresh-btn{display:none!important} #pdfPrintFrame{display:block!important} body.darkMode{ background:#0f172a; color:#e5e7eb; } body.darkMode .container, body.darkMode .card, body.darkMode .chartBox, body.darkMode .dashCard, body.darkMode .categoryTotalItem, body.darkMode .compareResultCard, body.darkMode .tripCheckItem{ background:#111827; color:#e5e7eb; border-color:#374151; } body.darkMode input,body.darkMode select{ background:#0f172a; color:#e5e7eb; border-color:#475569; } body.darkMode .appTabs{background:rgba(15,23,42,.96);border-color:#334155} body.darkMode .appTabs button{background:#1f2937;color:#e5e7eb} body.darkMode .appTabs button.active{background:linear-gradient(135deg,#2563eb,#06b6d4);color:#fff} body.darkMode .homeKpi, body.darkMode .tripBudgetCard{background:#1f2937;border-color:#374151} body.darkMode .homeKpiValue{color:#e5e7eb} body.darkMode .homeKpiTitle,body.darkMode .homeKpiSub,body.darkMode .dashboardNote,body.darkMode .small{color:#cbd5e1} body.darkMode .expensesLedgerWrap, body.darkMode #expensesTable.expensesLedgerTable{background:#fff!important;color:#111827!important} body.darkMode #expensesTable.expensesLedgerTable th, body.darkMode #expensesTable.expensesLedgerTable td{border-color:#d7dce5!important;color:#111827!important} body.darkMode #expensesTable.expensesLedgerTable thead th{background:#0b84f3!important;color:#fff!important} @media(min-width:650px){ .categoryTotalsGrid{grid-template-columns:repeat(2,minmax(0,1fr))} } @media(min-width:700px){ .tripCheckGrid{grid-template-columns:repeat(2,minmax(0,1fr))} } @media(min-width:760px){ .dashboardGrid{grid-template-columns:repeat(4,minmax(0,1fr))} .chartGrid{grid-template-columns:1fr 1fr} } @media(max-width:700px){ body{padding:10px} .container{padding:12px;border-radius:14px} .homeKpiGrid,.homeQuickActions{grid-template-columns:1fr} .proHeader h1{font-size:24px} .appTabs button{font-size:14px;padding:10px 14px} .chartGrid{grid-template-columns:1fr} .chartCanvas{width:100%;max-width:100%} table.mobileCards,table.mobileCards thead,table.mobileCards tbody,table.mobileCards tr,table.mobileCards th,table.mobileCards td{display:block;width:100%} table.mobileCards thead{display:none} table.mobileCards tr{background:rgba(255,255,255,.92);border:1px solid #e5e7eb;border-radius:16px;margin:10px 0;padding:10px;box-shadow:0 4px 12px rgba(15,23,42,.06)} table.mobileCards td{border:0!important;padding:8px 4px!important;text-align:right!important;display:flex;justify-content:space-between;gap:12px;align-items:flex-start;white-space:normal!important;word-break:break-word} table.mobileCards td::before{content:attr(data-label);font-weight:900;color:#64748b;min-width:100px;flex:0 0 42%} } @media(max-width:430px){ .expensesLedgerWrap{max-height:500px;margin-left:-10px;margin-right:-10px} #expensesTable.expensesLedgerTable{font-size:9px} #expensesTable.expensesLedgerTable th,#expensesTable.expensesLedgerTable td{padding:5px 2px} } @media print{ .noPrint,.pdfPreviewToolbar,#force-cloud-refresh-btn{display:none!important} body.pdfPreviewOpen{overflow:visible!important;height:auto!important;background:#fff!important} body.pdfPreviewOpen #pdfPreviewOverlay{position:static!important;display:block!important;height:auto!important;overflow:visible!important;background:#fff!important} body.pdfPreviewOpen .pdfPreviewContent{overflow:visible!important;padding:0!important} body.pdfPreviewOpen .printReport{border:0!important;border-radius:0!important;padding:0!important} .container{box-shadow:none} } .tabPanel,.card[id$="Anchor"]{scroll-margin-top:92px} </style> <script> (function(){ function showTabVisual(tabId, shouldScroll){ const map = { tabMain: "tab-main", tabSummary: "tab-summary", tabCharts: "tab-charts", tabCompare: "tab-compare", tabSettings: "tab-settings" }; const scrollMap = { tabMain: "tabMain", tabSummary: "summaryAnchor", tabCharts: "chartsInfoAnchor", tabCompare: "compareAnchor", tabSettings: "settingsExportAnchor" }; const cls = map[tabId] || "tab-main"; document.body.classList.remove("tab-main","tab-summary","tab-charts","tab-compare","tab-settings"); document.body.classList.add(cls); document.querySelectorAll(".tabPanel").forEach(p => p.classList.remove("active")); const panel = document.getElementById(tabId); if(panel) panel.classList.add("active"); document.querySelectorAll(".appTabs button").forEach(b => b.classList.remove("active")); const btn = document.querySelector(`[data-tab-btn="${tabId}"]`); if(btn) btn.classList.add("active"); try{ localStorage.setItem("activeBudgetTab", tabId); }catch(err){} if(shouldScroll !== false){ const target = document.getElementById(scrollMap[tabId] || tabId) || panel; if(target){ setTimeout(() => { try{ target.scrollIntoView({behavior:"smooth", block:"start"}); } catch(err){ target.scrollIntoView(true); } }, 30); } } } if(typeof window.showAppTab !== "function"){ window.showAppTab = showTabVisual; } window.addEventListener("DOMContentLoaded", function(){ if(typeof window.showAppTab !== "function") window.showAppTab = showTabVisual; const saved = localStorage.getItem("activeBudgetTab") || "tabMain"; showTabVisual(saved, false); }); })(); </script> </head> <body class="authLocked tab-main"> <div id="authOverlay" class="authOverlay"> <div class="authCard"> <h1>🔐 כניסה מאובטחת</h1> <p>אפליקציית ניהול תקציב החופשה</p> <label for="authEmail">אימייל</label> <input id="authEmail" type="email" autocomplete="username" placeholder="example@gmail.com"> <label for="authPassword">סיסמה</label> <input id="authPassword" type="password" autocomplete="current-password" placeholder="הכנס סיסמה"> <button onclick="loginBudgetApp()">כניסה לאפליקציה</button> <button class="authSecondary" onclick="createBudgetUser()">יצירת משתמש מורשה ראשון</button> <div id="authMessage" class="authMessage"></div> <div class="authHint">ניתן לשמור את הסיסמה באייפון ולהשתמש ב־Face ID למילוי מהיר.</div> </div> </div> <div class="container"> <div id="authTopBar" class="authTopBar" style="display:none;"> <span id="authUserLabel">מחובר</span> <button onclick="logoutBudgetApp()">התנתקות</button> </div> <div id="syncStatusBar" class="syncStatusBar offline"> <div> <span class="dot"></span><span id="syncStatusText">בודק חיבור וסנכרון...</span> <small id="syncStatusSub">שינויים שתזין אופליין יישמרו מקומית ויסונכרנו כשיחזור אינטרנט.</small> </div> </div> <div class="proHeader"> <div class="proHeaderTop"> <h1>ניהול תקציב חופשה</h1> </div> <div class="proHeaderSub">תקציב חי, הוצאות, סיכומים והשוואת טיולים</div> <div id="budgetAppVersionBadge" class="budgetAppVersionBadge">גרסת קופות מטבע חוץ ותקציבי ילדים · 2026.07.15-FX-WALLETS-CHILDREN-v6</div> <div class="appVersionBadge">גרסת מטבע חוץ ותקציבים פרטיים · 2026.07.15-v3</div> </div> <div class="appTabs noPrint"> <button data-tab-btn="tabMain" onclick="showAppTab('tabMain')">🏠 ראשי</button> <button data-tab-btn="tabSummary" onclick="showAppTab('tabSummary')">💰 סיכומים</button> <button data-tab-btn="tabCharts" onclick="showAppTab('tabCharts')">📊 מידע גרפי</button> <button data-tab-btn="tabCompare" onclick="showAppTab('tabCompare')">⚖️ השוואה</button> <button data-tab-btn="tabSettings" onclick="showAppTab('tabSettings')">⚙️ הגדרות וייצוא</button> </div> <div id="tabMain" class="tabPanel active"> <div class="card section-main-home noPrint"> <h2>🧳 בחירת טיול פעיל</h2> <label><b>טיול פעיל:</b></label> <select id="homeTripSelector" onchange="switchTripFromHome()"></select> </div> <div class="card section-main-home"> <h2>🏠 מסך ראשי</h2> <div class="homeKpiGrid"> <div class="homeKpi"> <div class="homeKpiTitle">טיול פעיל</div> <div class="homeKpiValue" id="homeTripName">—</div> <div class="homeKpiSub">לפי בחירת הטיול הפעיל</div> </div> <div class="homeKpi"> <div class="homeKpiTitle">נותר בתקציב הכולל</div> <div class="homeKpiValue" id="homeTotalLeft">—</div> <div class="homeKpiSub" id="homeTotalUsed">—</div><div id="homeWalletBalances" class="walletBalances"></div> </div> <div class="homeKpi"> <div class="homeKpiTitle">נותר בקניות</div> <div class="homeKpiValue" id="homeShoppingLeft">—</div> <div class="homeKpiSub" id="homeShoppingUsed">—</div> </div> <div class="homeKpi"> <div class="homeKpiTitle">תקציב אוכל יומי</div> <div class="homeKpiValue" id="homeFoodBudget">—</div> <div class="homeKpiSub" id="homeFoodSub">יעד יומי פעיל</div> </div> <div class="homeKpi"> <div class="homeKpiTitle">נותר בתקציב עידו הפרטי</div> <div class="homeKpiValue" id="homeIdoLeft">—</div> <div class="homeKpiSub" id="homeIdoUsed">—</div> </div> <div class="homeKpi"> <div class="homeKpiTitle">נותר בתקציב מיכאל הפרטי</div> <div class="homeKpiValue" id="homeMichaelLeft">—</div> <div class="homeKpiSub" id="homeMichaelUsed">—</div> </div> </div> <div class="homeQuickActions noPrint"> <button class="primary" onclick="document.getElementById('quickExpenseCard')?.scrollIntoView({behavior:'smooth'});">➕ הוספת הוצאה</button> <button onclick="showAppTab('tabSummary')">💰 סיכום מלא</button> <button onclick="showAppTab('tabCharts')">📊 גרפים</button> <button onclick="showAppTab('tabCompare')">⚖️ השוואת טיולים</button> </div> <div class="homeMiniNote">עדכון מהיר של הוצאות ומעקב תקציב בזמן אמת.</div> </div> <div id="quickExpenseCard" class="card noPrint section-main"> <h2 id="expenseFormTitle">➕ הוספת הוצאה</h2> <label for="category"><b>קטגוריה:</b></label> <select id="category" aria-label="קטגוריה"> <option value="">בחר קטגוריה</option> <option>כרטיסי סים</option> <option>אוכל</option> <option>קניות עידו</option> <option>קניות מיכאל</option> <option>ביגוד והנעלה</option> <option>תחבורה ציבורית</option> <option>אטרקציות</option> <option>תשלומים במזומן</option> <option>מלונות</option> <option>ביטוח נסיעות</option> <option>טיסות</option> <option>השכרת רכב</option> <option>דלק</option> <option>טיפים</option> <option>כביסות</option> <option>קרוז</option> <option>שונות</option> </select> <label for="note"><b>פירוט:</b></label> <input id="note" placeholder="שם מקום / פירוט" autocomplete="off" aria-label="פירוט ההוצאה"> <label for="amount"><b>סכום:</b></label> <input id="amount" type="number" inputmode="decimal" min="0" step="0.01" placeholder="סכום" aria-label="סכום ההוצאה"> <label for="currency"><b>מטבע:</b></label> <select id="currency" aria-label="מטבע"> <option value="ILS">₪ שקל</option> <option value="USD">$ דולר אמריקאי</option> <option value="JPY">¥ ין</option> <option value="EUR">€ יורו</option> <option value="GBP">£ לירה סטרלינג</option> <option value="CAD">C$ דולר קנדי</option> </select> <label for="budgetOwner"><b>מאיזה תקציב ההוצאה?</b></label> <select id="budgetOwner" aria-label="שיוך תקציב"> <option value="home" selected>תקציב הבית</option> <option value="ido">תקציב עידו פרטי</option> <option value="michael">תקציב מיכאל פרטי</option> </select> <label for="paymentMethod"><b>אמצעי תשלום:</b></label> <select id="paymentMethod" aria-label="אמצעי תשלום"> <option value="credit" selected>אשראי</option> <option value="cash">מזומן</option> </select> <div id="fxFeePreview" class="fxFeePreview">הזן סכום כדי לראות את חישוב השקלים והאם יש עמלת אשראי מט״ח.</div> <label for="expenseDate"><b>תאריך ההוצאה:</b></label> <input id="expenseDate" type="date" aria-label="תאריך ההוצאה"> <button id="expenseSubmitButton" onclick="addExpense()">הוסף הוצאה</button> <button id="expenseCancelEditButton" class="secondaryBtn" style="display:none" onclick="cancelExpenseEdit()">ביטול עריכה</button> </div> <div class="card section-main"> <h2 id="mainExpensesAnchor">פירוט כל ההוצאות בטיול הפעיל</h2> <button id="refreshMainExpensesButton" class="noPrint" onclick="renderExpenses()">רענון טבלת הוצאות</button> <div class="expensesLedgerWrap"> <table id="expensesTable" class="expensesLedgerTable"> <thead><tr><th>תאריך</th><th>תקציב</th><th>קטגוריה</th><th>פירוט</th><th>סכום</th><th>מטבע</th><th>סה״כ ₪</th><th>שער</th><th>פעולות</th></tr></thead> <tbody id="expensesBody"></tbody> </table> </div> </div> </div> <div id="tabSummary" class="tabPanel"> <div id="summaryAnchor" class="card section-summary"> <h2>💰 סיכום סופי ותקציבים</h2> <div class="noPrint" style="margin-bottom:12px;"> <label><b>בחר מטבע להצגת הסיכום:</b></label> <select id="summaryCurrency" onchange="setSummaryCurrencyFromUI()"> <option value="ILS">₪ שקל ישראלי</option> <option value="USD">$ דולר אמריקאי</option> <option value="JPY">¥ ין יפני</option> <option value="EUR">€ יורו</option> <option value="GBP">£ לירה סטרלינג</option> <option value="CAD">C$ דולר קנדי</option> </select> </div> <div class="small">תצוגת הסיכום לפי המטבע שנבחר.</div> <div id="summaryFinal"></div> <div class="summaryUnifiedBox"><div id="tripBudgetSummaryBox" class="tripBudgetSummaryBox"></div></div> </div> <div class="card section-summary"> <h2>סיכום יומי</h2> <div class="tableWrap"> <table id="dailyTable" class="mobileCards"> <thead><tr><th>תאריך</th><th>סה״כ יומי</th></tr></thead> <tbody id="dailyBody"></tbody> </table> </div> </div> <div class="card noPrint section-summary"> <h2>🔎 סינון וסיכום לפי קטגוריה</h2> <div class="filterPanel"> <label for="filterType"><b>סוג סינון:</b></label> <select id="filterType" onchange="setFilterTypeFromUI()"> <option value="category">לפי קטגוריה</option> <option value="date">לפי תאריך</option> </select> <div id="categoryFilterBox"> <label for="categoryFilter"><b>בחר קטגוריה להצגה:</b></label> <select id="categoryFilter" onchange="setCategoryFilterFromUI()"> <option value="ALL">כל הקטגוריות</option> </select> </div> <div id="dateFilterBox" style="display:none;"> <label for="dateFilter"><b>בחר תאריך פעיל:</b></label> <select id="dateFilter" onchange="setDateFilterFromUI()"> <option value="ALL">כל התאריכים</option> </select> <div id="activeDatesChips" class="dateChips"></div> </div> <button onclick="clearCategoryFilter()">נקה סינון</button> <div id="selectedCategorySummary" class="dashboardNote"></div> </div> <h3>סה״כ לפי קטגוריות</h3> <div id="categoryTotalsGrid" class="categoryTotalsGrid"></div> </div> </div> <div id="tabCharts" class="tabPanel"> <div class="card section-charts"> <h2>📊 דשבורד תקציב מקצועי</h2> <div class="dashboardGrid"> <div class="dashCard"> <div class="dashTitle">סה״כ הוצאות בכל התקציבים</div> <div class="dashValue" id="dashTotal">₪0</div> </div> <div class="dashCard"> <div class="dashTitle">יתרת תקציב הבית</div> <div class="dashValue" id="dashRemaining">₪0</div> </div> <div class="dashCard"> <div class="dashTitle">ניצול תקציב</div> <div class="dashValue" id="dashPercent">0%</div> </div> <div class="dashCard"> <div class="dashTitle">ממוצע יומי</div> <div class="dashValue" id="dashAvgDaily">₪0</div> </div> </div> <div class="progressWrap"> <div class="progressBar" id="budgetProgressBar"></div> </div> <div class="dashboardNote" id="budgetStatusText">אין עדיין נתונים.</div> <div class="dashboardGrid"> <div class="dashCard"> <div class="dashTitle">הוצאות היום</div> <div class="dashValue" id="dashToday">₪0</div> </div> <div class="dashCard"> <div class="dashTitle">אוכל היום מול יעד יומי</div> <div class="dashValue" id="dashFoodToday">₪0</div> </div> <div class="dashCard"> <div class="dashTitle">ביגוד, הנעלה, ואלקטרוניקה</div> <div class="dashValue" id="dashClothing">₪0</div> </div> <div class="dashCard"> <div class="dashTitle">מספר הוצאות</div> <div class="dashValue" id="dashCount">0</div> </div> </div> </div> <div id="chartsInfoAnchor" class="card section-charts"> <h2>📊 מידע גרפי</h2> <div class="chartGrid section-charts"> <div class="chartBox"> <h3>🍰 פריסת הוצאות לפי קטגוריה</h3> <canvas id="categoryPie" class="chartCanvas" width="420" height="240"></canvas> <div id="categoryLegend"></div> </div> <div class="chartBox"> <h3>📈 הוצאות לפי יום</h3> <canvas id="dailyBar" class="chartCanvas" width="420" height="240"></canvas> </div> </div> </div> </div> <div id="tabCompare" class="tabPanel"> <div id="compareAnchor" class="card section-compare"> <h2>⚖️ השוואה בין טיולים</h2> <div class="compareControls"> <div class="small">בחר טיולים ומדד להשוואה.</div> <button class="secondaryBtn" onclick="toggleCompareTripsPanel()">🧳 בחירת טיולים להשוואה</button> <div id="compareTripsPanel" class="collapsiblePanel"> <label><b>סמן טיולים להשוואה:</b></label> <div id="compareTripsBox" class="tripCheckGrid"></div> </div> <label for="compareMetric"><b>בחר מדד להשוואה:</b></label> <select id="compareMetric" onchange="setCompareMetricFromUI()"> <option value="TOTAL">סה״כ הוצאות</option> <option value="DATE">תאריך מסוים</option> <option value="MOST_EXPENSIVE_DAY">יום הכי יקר</option> <option value="CHEAPEST_DAY">יום הכי זול</option> <option value="CAT::כרטיסי סים">כרטיסי סים</option> <option value="CAT::אוכל">אוכל</option> <option value="CAT::קניות עידו">קניות עידו</option> <option value="CAT::קניות מיכאל">קניות מיכאל</option> <option value="CAT::ביגוד והנעלה">ביגוד והנעלה</option> <option value="CAT::תחבורה ציבורית">תחבורה ציבורית</option> <option value="CAT::אטרקציות">אטרקציות</option> <option value="CAT::תשלומים במזומן">תשלומים במזומן</option> <option value="CAT::מלונות">מלונות</option> <option value="CAT::ביטוח נסיעות">ביטוח נסיעות</option> <option value="CAT::טיסות">טיסות</option> <option value="CAT::השכרת רכב">השכרת רכב</option> <option value="CAT::דלק">דלק</option> <option value="CAT::טיפים">טיפים</option> <option value="CAT::כביסות">כביסות</option> <option value="CAT::קרוז">קרוז</option> <option value="CAT::שונות">שונות</option> </select> <div id="compareDateBox" style="display:none;"> <label for="compareDate"><b>בחר תאריך פעיל:</b></label> <select id="compareDate" onchange="setCompareDateFromUI()"></select> </div> <div id="compareStatus" class="dashboardNote"></div> </div> <div class="compareResultCard"> <div class="tableWrap"> <table class="compareMiniTable mobileCards"> <thead id="tripComparisonHead"></thead> <tbody id="tripComparisonBody"></tbody> </table> </div> </div> </div> </div> <div id="tabSettings" class="tabPanel"> <div id="settingsExportAnchor" class="card section-settings"> <h2>⚙️ הגדרות וייצוא</h2> <h3>🧳 ניהול טיולים</h3> <div class="small">הטיול הראשון מוגדר כ־<b>יפן 2026</b>. ניתן ליצור טיולים נוספים ולבחור ביניהם.</div> <div id="activeTripLabel" class="dashboardNote">טיול פעיל: <b>יפן 2026</b></div> <label for="tripSelector"><b>בחר טיול פעיל:</b></label> <select id="tripSelector" onchange="switchTrip()"></select> <div class="tripActions noPrint"> <button onclick="createNewTrip()">➕ יצירת טיול חדש</button> <button class="secondaryBtn" onclick="toggleTripSettings()">⚙️ הגדרות הטיול הפעיל</button> </div> <div id="tripSettingsPanel" class="filterPanel collapsiblePanel"> <div class="featureVerificationBox"><b>גרסה פעילה:</b> מטבע חוץ + תקציב עידו + תקציב מיכאל + שיוך הוצאה לתקציב.</div> <label for="tripName"><b>שם הטיול:</b></label> <input id="tripName" placeholder="לדוגמה: יפן 2026 / ארה״ב 2027" oninput="saveTripName()"> <label><b>מקורות תקציב הבית:</b></label> <div class="small">אפשר לשלב שקלים ומטבעות זרים. שווי התקציב הכולל בשקלים מחושב בכל רגע לפי השערים הפעילים.</div> <div class="small">לכל קופה בחר מטבע וסוג אחזקה. הוצאה במזומן תחויב מקופת מזומן תואמת; הוצאה באשראי תחויב מחשבון בנק תואם. על חלק שאינו מכוסה במטבע המתאים תחול עמלת המרה של 1%.</div><div id="budgetSourcesRows"></div> <button type="button" class="secondaryBtn" onclick="addBudgetSourceRow()">➕ הוספת מקור תקציב במטבע נוסף</button> <div id="budgetSourcesPreview" class="fxFeePreview"></div> <label for="tripIdoBudgetInput"><b>תקציב עידו פרטי בשקלים:</b></label> <input id="tripIdoBudgetInput" type="number" inputmode="decimal" min="0" step="0.01" placeholder="0"> <label for="tripMichaelBudgetInput"><b>תקציב מיכאל פרטי בשקלים:</b></label> <input id="tripMichaelBudgetInput" type="number" inputmode="decimal" min="0" step="0.01" placeholder="0"> <label for="tripClothingInput"><b>תקציב ביגוד, הנעלה, ואלקטרוניקה בשקלים:</b></label> <input id="tripClothingInput" type="number" inputmode="decimal" placeholder="לדוגמה: 20000"> <label for="tripFoodInput"><b>תקציב אוכל יומי בשקלים:</b></label> <input id="tripFoodInput" type="number" inputmode="decimal" min="0" placeholder="לדוגמה: 750"> <label for="tripTotalFoodInput"><b>מסגרת אוכל כוללת בשקלים:</b></label> <input id="tripTotalFoodInput" type="number" inputmode="decimal" min="0" placeholder="0 אם אין מסגרת כוללת"> <button class="noPrint" onclick="saveTripSettings()">💾 שמירת הגדרות טיול</button> </div> </div> <div class="card section-settings"> <b>תקציב כולל:</b> <span id="topBudgetLabel">₪93,063</span><br> <b>תקציב ביגוד, הנעלה, ואלקטרוניקה:</b> <span id="topClothingBudgetLabel">₪20,000</span><br> <b>תקציב אוכל יומי:</b> <span id="topFoodBudgetLabel">₪750</span><br><br> <div class="ratesHeadline"> <b>שערי המרה פעילים להוצאות חדשות:</b><br> <div class="rateLine"><span>USD דולר אמריקאי</span><b><span id="usdRateTop">2.9070</span> ₪</b></div> <div class="rateLine"><span>JPY ין יפני</span><b><span id="jpyRateTop">0.018548</span> ₪</b></div> <div class="rateLine"><span>100 ין יפני</span><b><span id="jpy100RateTop">1.8548</span> ₪</b></div> <div class="rateLine"><span>EUR יורו</span><b><span id="eurRateTop">3.4225</span> ₪</b></div> <div class="rateLine"><span>GBP לירה סטרלינג</span><b><span id="gbpRateTop">3.9572</span> ₪</b></div> <div class="rateLine"><span>CAD דולר קנדי</span><b><span id="cadRateTop">2.1282</span> ₪</b></div> </div> <span class="small">מקור: <span id="rateSource"></span> | עודכן: <span id="rateUpdated"></span></span><br> <span class="small"><b>חשוב:</b> הוצאות שכבר הוזנו נשארות לפי שער הכניסה שלהן ולא משתנות אחרי עדכון שערים.</span> </div> <div class="card noPrint section-settings"> <h2>עדכון שערים</h2> <button onclick="updateRatesLive()">עדכון שערים בזמן אמת מהאינטרנט</button> <div id="ratesStatus" class="small"></div><div class="small">שערים שעודכנו מהאינטרנט או ידנית נשמרים ומשמשים לכל הוצאה חדשה עד העדכון הבא.</div> <div class="rateInputWrap"> <label for="usdRateInput">דולר אמריקאי (USD) — שער בשקלים</label> <input id="usdRateInput" type="number" step="0.0001" placeholder="לדוגמה: 2.9090"> <label for="jpyRateInput">ין יפני (JPY) — שער בשקלים ל־1 ין</label> <input id="jpyRateInput" type="number" step="0.000001" placeholder="לדוגמה: 0.018511"> <label for="eurRateInput">יורו (EUR) — שער בשקלים</label> <input id="eurRateInput" type="number" step="0.0001" placeholder="לדוגמה: 3.4225"> <label for="gbpRateInput">לירה סטרלינג (GBP) — שער בשקלים</label> <input id="gbpRateInput" type="number" step="0.0001" placeholder="לדוגמה: 3.9572"> <label for="cadRateInput">דולר קנדי (CAD) — שער בשקלים</label> <input id="cadRateInput" type="number" step="0.0001" placeholder="לדוגמה: 2.1282"> </div> <button onclick="saveManualRates()">עדכון שערים ידני וסנכרון</button> </div> <div class="card noPrint section-settings"> <h2>ייצוא ואיפוס</h2> <button class="export" onclick="exportHTML()">ייצוא HTML</button> <button class="export" type="button" onclick="exportPDF()">תצוגת PDF / הדפסה</button> <div class="small">נפתח בתוך האפליקציה עם כפתור חזרה ברור.</div> <button class="orange" onclick="clearTrip()">איפוס הוצאות הטיול הפעיל</button> </div> <div class="appCreatorFooter section-settings"> <div class="creatorName">פותח ועוצב על ידי: איתן פרידמן</div> </div> </div> </div> </body> </html>
שמור קובץ