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>