C:\TripServer\Moneyapp\index.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>
<meta name="eitan-tripserver-artifact-contract" content="eitan-tripserver-artifact/v1">
<meta name="eitan-artifact-id" content="Moneyapp">
<meta name="eitan-artifact-type" content="vacation-budget-app">
<meta name="eitan-artifact-version" content="2026.07.17-GOLD-AUDIT-PDF-COMPAT-v11">
<script id="eitan-platform-artifact" type="application/json">
{
"schema": "eitan-tripserver-artifact/v1",
"artifact_id": "Moneyapp",
"artifact_type": "vacation-budget-app",
"version": "2026.07.17-GOLD-AUDIT-PDF-COMPAT-v11",
"features": [
"foreign_budget_sources",
"ido_private_budget",
"michael_private_budget",
"expense_budget_owner",
"visible_version_badge"
]
}
</script>
<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, waitForPendingWrites } 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;
let dataUnsubscribers = [];
let cloudRefreshPromise = null;
let pendingWriteSequence = pendingOfflineWrites;
let acknowledgedWriteSequence = 0;
let pendingWriteAckPromise = null;
let legacyFreezeCompleted = false;
let tripNameSaveTimer = null;
let tripSettingsDirty = false;
let dataSessionId = 0;
let renderFramePending = false;
const BUDGET_APP_VERSION = "2026.07.17-GOLD-AUDIT-PDF-COMPAT-v11";
function persistPendingWriteCount(){
pendingOfflineWrites = Math.max(0, pendingWriteSequence - acknowledgedWriteSequence);
localStorage.setItem("pendingOfflineWrites", String(pendingOfflineWrites));
}
function clearDataListeners(){
dataUnsubscribers.forEach(unsubscribe => {
try{ unsubscribe(); }catch(err){ console.warn("Listener cleanup skipped", err); }
});
dataUnsubscribers = [];
}
function stopBudgetAppData(){
dataSessionId += 1;
clearDataListeners();
appDataStarted = false;
}
function schedulePendingWriteConfirmation(){
if(!navigator.onLine || pendingWriteAckPromise || !auth.currentUser || !isAllowedUser(auth.currentUser)) return pendingWriteAckPromise;
const targetSequence = pendingWriteSequence;
let confirmed = false;
pendingWriteAckPromise = waitForPendingWrites(db)
.then(() => {
confirmed = true;
acknowledgedWriteSequence = Math.max(acknowledgedWriteSequence, targetSequence);
persistPendingWriteCount();
updateConnectionStatus();
})
.catch(err => {
console.warn("Pending writes confirmation postponed", err);
updateConnectionStatus();
})
.finally(() => {
pendingWriteAckPromise = null;
if(confirmed && navigator.onLine && pendingWriteSequence > acknowledgedWriteSequence){
setTimeout(schedulePendingWriteConfirmation, 250);
}
});
return pendingWriteAckPromise;
}
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", "מסנכרן נתונים...", "יש שינויים שממתינים לאישור מהענן.");
return;
}
setSyncStatus("online", "מחובר ומסונכרן", "כל שינוי נשמר בענן ומסתנכרן בין המכשירים.");
}
window.addEventListener("online", () => {
updateConnectionStatus();
schedulePendingWriteConfirmation();
if(typeof renderAll === "function") setTimeout(renderAll, 100);
});
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(){
pendingWriteSequence += 1;
persistPendingWriteCount();
updateConnectionStatus();
}
async function trackedWrite(operation){
markLocalWriteAttempt();
try{
const result = await operation();
schedulePendingWriteConfirmation();
return result;
}catch(err){
acknowledgedWriteSequence = Math.min(pendingWriteSequence, acknowledgedWriteSequence + 1);
persistPendingWriteCount();
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){
stopBudgetAppData();
showAuthScreen("");
return;
}
if(!isAllowedUser(user)){
stopBudgetAppData();
await signOut(auth);
showAuthScreen("המשתמש הזה אינו מורשה להיכנס לאפליקציה.");
return;
}
showAppForUser(user);
if(!appDataStarted){
appDataStarted = true;
const sessionId = ++dataSessionId;
startBudgetAppData(sessionId).catch(err => {
console.error("Budget data startup failed", err);
if(sessionId === dataSessionId) appDataStarted = false;
setSyncStatus("pending", "טעינת הנתונים נכשלה", "בדוק חיבור אינטרנט ורענן את האפליקציה.");
});
}
});
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 roundILS(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){
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);
const isPreview = e.id === "__preview__";
const storedBaseILS = roundILS(Number.isFinite(Number(e.baseIlsAtEntry)) ? Number(e.baseIlsAtEntry) : amount * baseRate);
const storedFeeILS = roundILS(Number.isFinite(Number(e.foreignCreditFeeILS)) ? Number(e.foreignCreditFeeILS) : 0);
const storedTotalILS = roundILS(Number.isFinite(Number(e.ilsAtEntry)) ? Number(e.ilsAtEntry) : storedBaseILS + storedFeeILS);
let coveredNative = 0, uncoveredNative = 0, feeILS = 0, effectiveILS = 0;
if(owner !== "home"){
feeILS = isPreview && currency !== "ILS" && e.paymentMethod === "credit"
? roundILS(storedBaseILS * FOREIGN_CREDIT_FEE_RATE)
: storedFeeILS;
effectiveILS = isPreview ? roundILS(storedBaseILS + feeILS) : storedTotalILS;
}else if(currency === "ILS"){
const preferredType = ["bank","cash"].includes(e.fundingTypeAtEntry) ? e.fundingTypeAtEntry : expenseFundingType(e);
const fallbackType = preferredType === "cash" ? "bank" : "cash";
const fromPreferred = consume(`ILS::${preferredType}`, amount);
const fromFallback = fromPreferred.uncovered > 0 ? consume(`ILS::${fallbackType}`, fromPreferred.uncovered) : {consumed:0, uncovered:0};
coveredNative = fromPreferred.consumed + fromFallback.consumed;
uncoveredNative = Math.max(0, amount - coveredNative);
effectiveILS = isPreview ? roundILS(amount) : storedTotalILS;
}else{
const type = ["bank","cash"].includes(e.fundingTypeAtEntry) ? e.fundingTypeAtEntry : expenseFundingType(e);
const result = consume(`${currency}::${type}`, amount);
coveredNative = result.consumed;
uncoveredNative = result.uncovered;
const uncoveredBaseILS = roundILS(uncoveredNative * baseRate);
feeILS = isPreview ? roundILS(uncoveredBaseILS * FOREIGN_CREDIT_FEE_RATE) : storedFeeILS;
effectiveILS = isPreview ? roundILS(storedBaseILS + feeILS) : storedTotalILS;
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 = roundILS(homeSpentILS + effectiveILS);
totalFeeILS = roundILS(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: roundILS(convertToILS(src.remaining, src.currency, rates))}));
const walletRemainingILS = roundILS(balances.reduce((sum,src)=>sum+src.valueILS,0));
const totalBudgetILS = getTripBudgetSourcesILS(trip);
const remainingTotalILS = roundILS(totalBudgetILS - homeSpentILS);
return {sources:balances, allocations, homeSpentILS:roundILS(homeSpentILS), remainingTotalILS, walletRemainingILS, uncoveredForeignILS:roundILS(uncoveredForeignILS), totalFeeILS:roundILS(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 isShoppingCategory(category){
const cat = String(category || "").trim();
return cat.includes("קניות") || cat.includes("ביגוד") || cat.includes("הנעלה") || cat.includes("אלקטרוניקה");
}
function getTripShoppingILS(tripId){
return getTripExpenses(tripId)
.filter(e => budgetOwnerKey(e) === "home")
.filter(e => isShoppingCategory(e.category))
.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(tabId === "tabCompare" && typeof renderTripComparison === "function") setTimeout(() => renderTripComparison(true), 100);
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;
function finitePositive(value, fallback){
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number : Number(fallback);
}
function normalizeRatesData(input = {}, base = rates){
return {
...base,
USD: finitePositive(input.USD, base.USD || defaultRates.USD),
JPY: finitePositive(input.JPY, base.JPY || defaultRates.JPY),
EUR: finitePositive(input.EUR, base.EUR || defaultRates.EUR),
GBP: finitePositive(input.GBP, base.GBP || defaultRates.GBP),
CAD: finitePositive(input.CAD, base.CAD || defaultRates.CAD),
source: String(input.source || base.source || defaultRates.source),
updatedAtText: String(input.updatedAtText || base.updatedAtText || defaultRates.updatedAtText),
note: String(input.note || base.note || "")
};
}
function withTimeout(promise, milliseconds, message){
let timeoutId;
const timeout = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(message || "הפעולה נמשכה זמן רב מדי")), milliseconds);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timeoutId));
}
function showActionError(userMessage, err){
console.error(userMessage, err);
alert(userMessage + " הנתונים הקיימים לא שונו.");
}
const FOREIGN_CREDIT_FEE_RATE = 0.01;
function paymentMethodLabel(method){
return method === "credit" ? "אשראי" : "מזומן";
}
function resolvedExpensePaymentMethod(e){
if(e?.paymentMethod === "cash" || e?.paymentMethod === "credit") return e.paymentMethod;
return Number(e?.foreignCreditFeeILS || 0) > 0 ? "credit" : "cash";
}
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 currentAllocation = getFundingAllocation(e);
const hasCoveredSnapshot = Number.isFinite(Number(e?.coveredForeignAmountAtEntry));
const hasUncoveredSnapshot = Number.isFinite(Number(e?.uncoveredForeignAmountAtEntry));
const coveredNative = hasCoveredSnapshot ? Number(e.coveredForeignAmountAtEntry) : Number(currentAllocation?.coveredNative || 0);
const uncoveredNative = hasUncoveredSnapshot ? Number(e.uncoveredForeignAmountAtEntry) : Number(currentAllocation?.uncoveredNative || 0);
const hasFundingData = currentAllocation || hasCoveredSnapshot || hasUncoveredSnapshot;
const fundingLine = hasFundingData && e.currency !== "ILS" && budgetOwnerKey(e)==="home"
? `<br><span class="expenseSubLine">כוסה מהקופה בעת ההזנה: ${fmtCurrencyByCode(coveredNative,e.currency)}${uncoveredNative>0?` · הומר לשקלים: ${fmtCurrencyByCode(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"){
const sourceLabel = paymentMethod === "cash" ? "קופת השקלים במזומן" : "קופת השקלים בחשבון הבנק";
box.innerHTML = `<b>${sourceLabel}:</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 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 roundILS(value){
const number = Number(value || 0);
if(!Number.isFinite(number)) return 0;
return Math.round((number + Number.EPSILON) * 100) / 100;
}
function fmtILS(n){ return "₪" + roundILS(n).toLocaleString("he-IL", {minimumFractionDigits:2, maximumFractionDigits:2}); }
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){
if(Number.isFinite(Number(e?.ilsAtEntry))) return roundILS(e.ilsAtEntry);
const allocation = getFundingAllocation(e);
if(allocation && budgetOwnerKey(e) === "home") return roundILS(allocation.effectiveILS);
return roundILS(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.scheduleTripNameSave = function(){
clearTimeout(tripNameSaveTimer);
tripNameSaveTimer = setTimeout(() => window.saveTripName(), 700);
};
window.saveTripName = async function(){
clearTimeout(tripNameSaveTimer);
tripNameSaveTimer = null;
const input = document.getElementById("tripName");
const name = input?.value.trim() || "";
const trip = getActiveTrip();
const finalName = name || trip.name || "טיול ללא שם";
tripName = finalName;
if(finalName === (trip.name || "")) return;
const updatedTrip = {...trip, id:activeTripId, name:finalName};
try{
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();
}catch(err){
console.error("trip name save failed", err);
setSyncStatus("pending", "שם הטיול טרם נשמר", "המשך לעבוד כרגיל ונסה שוב כשהחיבור יציב.");
}
};
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].every(value => Number.isFinite(value) && value > 0)){
alert("יש להזין שער חיובי ותקין לכל מטבע.");
return;
}
const manualRates = {
USD: usd,
JPY: jpy,
EUR: eur,
GBP: gbp,
CAD: cad,
source: "עדכון ידני",
updatedAtText: new Date().toLocaleString("he-IL"),
note: "שערים ידניים"
};
try{
await trackedSetDoc(doc(db, "settings", "rates"), manualRates, {merge:true});
rates = normalizeRatesData(manualRates, rates);
renderRates();
renderAll();
alert("השערים נשמרו בהצלחה. הוצאות שכבר נוספו לא ישתנו.");
}catch(err){
showActionError("לא ניתן היה לשמור את שערי המטבע.", err);
}
};
window.updateRatesLive = async function(){
try{
document.getElementById("ratesStatus").innerText = "מעדכן שערים בזמן אמת...";
const controller = new AbortController();
const abortTimer = setTimeout(() => controller.abort(), 10000);
let res;
try{
res = await fetch("https://open.er-api.com/v6/latest/ILS", {cache:"no-store", signal:controller.signal});
}finally{
clearTimeout(abortTimer);
}
if(!res.ok) throw new Error("Rates request failed: " + res.status);
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: "שער עדכני ממקור מקוון"
};
await trackedSetDoc(doc(db, "settings", "rates"), liveRates, {merge:true});
rates = normalizeRatesData(liveRates, rates);
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 || !Number.isFinite(amount) || amount <= 0){
alert("נא לבחור קטגוריה ולהזין סכום חיובי ותקין.");
return null;
}
const isExistingExpense = !!editingExpenseId && !!existing && Object.keys(existing).length > 0;
const financialInputsUnchanged = isExistingExpense
&& Number(existing.amount || 0) === amount
&& (existing.currency || "ILS") === currency
&& resolvedExpensePaymentMethod(existing) === paymentMethod
&& budgetOwnerKey(existing) === budgetOwner
&& Number.isFinite(Number(existing.ilsAtEntry));
if(financialInputsUnchanged){
return {
...existing,
tripId: activeTripId,
tripName: getActiveTrip().name || tripName || "",
category,
note,
amount,
currency,
paymentMethod,
budgetOwner,
date,
created: existing.created || Date.now(),
updated: Date.now(),
fundingTypeAtEntry: existing.fundingTypeAtEntry || expenseFundingType({currency,paymentMethod})
};
}
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 = roundILS(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 = roundILS(previewAllocation.feeILS || 0);
const ilsAtEntry = roundILS(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 = resolvedExpensePaymentMethod(e);
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;
try{
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);
}catch(err){
showActionError("לא ניתן היה לשמור את ההוצאה.", err);
}
};
async function freezeLegacyExpensesOnce(){
if(legacyFreezeRunning || legacyFreezeCompleted) 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 || typeof e.baseIlsAtEntry !== "number"){
const rateSnapshot = {USD: Number(rates.USD), JPY: Number(rates.JPY), EUR: Number(rates.EUR), GBP: Number(rates.GBP), CAD: Number(rates.CAD)};
const baseIlsAtEntry = typeof e.baseIlsAtEntry === "number" ? e.baseIlsAtEntry : convertToILS(e.amount, e.currency, rateSnapshot);
const foreignCreditFeeILS = typeof e.foreignCreditFeeILS === "number" ? e.foreignCreditFeeILS : 0;
updates.push(() => trackedSetDoc(doc(db, "expenses", d.id), {
tripId: e.tripId || defaultTrip.id,
tripName: e.tripName || "יפן 2026",
baseIlsAtEntry,
foreignCreditFeeILS,
ilsAtEntry: typeof e.ilsAtEntry === "number" ? e.ilsAtEntry : baseIlsAtEntry + foreignCreditFeeILS,
rateUSDAtEntry: Number(e.rateUSDAtEntry || rateSnapshot.USD),
rateJPYAtEntry: Number(e.rateJPYAtEntry || rateSnapshot.JPY),
rateEURAtEntry: Number(e.rateEURAtEntry || rateSnapshot.EUR),
rateGBPAtEntry: Number(e.rateGBPAtEntry || rateSnapshot.GBP),
rateCADAtEntry: Number(e.rateCADAtEntry || rateSnapshot.CAD),
rateSourceAtEntry: e.rateSourceAtEntry || "נעילת שער להוצאה ישנה",
rateUpdatedAtEntry: e.rateUpdatedAtEntry || new Date().toLocaleString("he-IL")
}, {merge:true}));
}
});
if(updates.length) await runInChunks(updates, 100);
legacyFreezeCompleted = true;
}catch(err){
console.error("legacy expense normalization failed", err);
}finally{
legacyFreezeRunning = false;
}
}
async function runInChunks(tasks, chunkSize = 100){
for(let index = 0; index < tasks.length; index += chunkSize){
await Promise.all(tasks.slice(index, index + chunkSize).map(task => task()));
}
}
window.deleteExpense = async function(id){
if(!confirm("למחוק את ההוצאה הזו?")) return;
try{
await trackedDeleteDoc(doc(db, "expenses", id));
}catch(err){
showActionError("לא ניתן היה למחוק את ההוצאה.", err);
}
};
window.clearTrip = async function(){
if(!confirm("לפני האיפוס מומלץ לשמור דוח PDF או HTML. למחוק את כל הוצאות הטיול הפעיל?")) return;
let snap;
try{
snap = navigator.onLine
? await withTimeout(getDocsFromServer(collection(db, "expenses")), 15000, "טעינת ההוצאות נמשכה זמן רב מדי")
: await getDocs(collection(db, "expenses"));
}catch(err){
console.warn("Server expense list unavailable; using local cache", err);
snap = await getDocs(collection(db, "expenses"));
}
const deleteTasks = [];
snap.forEach(d => {
const e = d.data();
if((e.tripId || defaultTrip.id) === activeTripId){
deleteTasks.push(() => trackedDeleteDoc(doc(db, "expenses", d.id)));
}
});
try{
await runInChunks(deleteTasks, 100);
alert("הוצאות הטיול הפעיל נמחקו בהצלחה.");
}catch(err){
showActionError("לא ניתן היה להשלים את איפוס הטיול.", err);
}
};
function checkWarnings(category, date){
const clothingTotal = getTripShoppingILS(activeTripId);
const foodTodayILS = activeExpenses().filter(e => budgetOwnerKey(e) === "home" && e.category === "אוכל" && e.date === date)
.reduce((s,e)=>s + expenseILS(e), 0);
const messages = [];
if(isShoppingCategory(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";
tripSettingsDirty = false;
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 && document.activeElement !== 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");
const shouldRefreshSettings = !tripSettingsDirty || budgetSourcesRenderedTripId !== activeTripId;
if(shouldRefreshSettings){
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();
}else{
updateBudgetSourcesPreview();
}
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";
tripSettingsDirty = false;
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()
};
try{
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);
tripSettingsDirty = false;
budgetSourcesRenderedTripId = null;
renderTripSelector();
renderHomeTripSelector();
renderAll();
}catch(err){
showActionError("לא ניתן היה ליצור את הטיול החדש.", err);
}
};
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)}" aria-label="סכום מקור תקציב">
<select class="budgetSourceCurrency" 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" 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(){
tripSettingsDirty = true;
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" aria-label="סכום מקור תקציב">
<select class="budgetSourceCurrency" aria-label="מטבע מקור תקציב">
${["ILS","USD","JPY","EUR","GBP","CAD"].map(code => `<option value="${code}">${currencyLabel(code)}</option>`).join("")}
</select>
<select class="budgetSourceHolding" 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){
tripSettingsDirty = true;
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("#tripSettingsPanel input:not(#tripName),#tripSettingsPanel select")) tripSettingsDirty = true;
if(e.target.matches(".budgetSourceAmount")) updateBudgetSourcesPreview();
});
document.addEventListener("change", e => {
if(e.target.matches("#tripSettingsPanel input:not(#tripName),#tripSettingsPanel select")) tripSettingsDirty = true;
if(e.target.matches(".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
};
try{
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];
tripSettingsDirty = false;
budgetSourcesRenderedTripId = null;
renderTripSelector();
renderBudgetSourceRows(true);
renderHomeTripSelector();
renderAll();
alert("הגדרות הטיול נשמרו בהצלחה.");
}catch(err){
showActionError("לא ניתן היה לשמור את הגדרות הטיול.", err);
}
};
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(isShoppingCategory(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 = getTripShoppingILS(activeTripId);
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="${usedCats.length + 2}">אין עדיין הוצאות</td></tr>`;
return;
}
tbody.innerHTML = keys.map(date => {
const d = days[date];
return `
<tr>
<td>${escapeHtml(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>
`;
}).join("");
}
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) table.style.width = "max-content";
}
function renderExpenses(){
const tbody = document.getElementById("expensesBody");
if(!tbody) return;
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;
const rows = list.map(e => {
const ils = expenseILS(e);
total += ils;
return `
<tr>
<td>${escapeHtml(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(resolvedExpensePaymentMethod(e))}</span></td>
<td>${formatExpenseTotalCell(e)}</td>
<td>${compactRateLabel(e)}</td>
<td>
<div class="rowActions">
<button type="button" class="secondaryBtn smallBtn" data-expense-action="edit" data-expense-id="${escapeHtml(e.id)}">ערוך</button>
<button type="button" class="danger smallBtn" aria-label="מחיקת הוצאה" data-expense-action="delete" data-expense-id="${escapeHtml(e.id)}">×</button>
</div>
</td>
</tr>
`;
});
rows.push(`
<tr class="expensesTotalRow">
<td colspan="6"><b>סה״כ כל ההוצאות המוצגות</b></td>
<td><b>${fmtILS(total)}</b></td>
<td colspan="2"></td>
</tr>
`);
tbody.innerHTML = rows.join("");
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();
}
document.getElementById("expensesBody")?.addEventListener("click", event => {
const button = event.target.closest("button[data-expense-action]");
if(!button) return;
const id = button.dataset.expenseId || "";
if(button.dataset.expenseAction === "edit") window.startEditExpense(id);
if(button.dataset.expenseAction === "delete") window.deleteExpense(id);
});
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.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 = document.body.classList.contains("darkMode") ? "#cbd5e1" : "#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 = document.body.classList.contains("darkMode") ? "#64748b" : "#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 = document.body.classList.contains("darkMode") ? "#e2e8f0" : "#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 clothing = getTripShoppingILS(activeTripId);
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(foodTodayILS);
if(elClothing) elClothing.innerText = fmtILS(clothing);
if(elCount) elCount.innerText = String(activeExpenses().length);
setStatusClass(elRemaining, remaining < 0 ? "bad" : status);
setStatusClass(elPercent, status);
setStatusClass(elFood, foodTodayILS > getActiveFoodDailyBudgetILS() ? "bad" : foodTodayILS >= 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();
renderTripBudgetSummary();
renderHomeSummary();
renderHomeTripSelector();
if(document.body.classList.contains("tab-charts")) renderDashboard();
if(document.body.classList.contains("tab-compare")) renderTripComparison();
setTimeout(applyMobileTableLabels, 50);
}
function scheduleRenderAll(){
if(renderFramePending) return;
renderFramePending = true;
requestAnimationFrame(() => {
renderFramePending = false;
renderAll();
});
}
function sortedActiveExpenses(){
return activeExpenses().slice().sort((a,b) =>
dateSortKey(a.date).localeCompare(dateSortKey(b.date)) || (a.created||0)-(b.created||0)
);
}
function safeDownloadFilename(value){
return String(value || "חופשה")
.trim()
.replace(/[\/:*?"<>|]+/g, "_")
.replace(/\s+/g, " ")
.slice(0, 80) || "חופשה";
}
window.exportHTML = function(){
try{
refreshFundingLedger();
renderSummary();
const generated = new Date().toLocaleString("he-IL");
const activeTripTitle = getActiveTrip().name || tripName || "חופשה";
const list = sortedActiveExpenses();
const reportTotal = list.reduce((sum,e) => sum + expenseILS(e), 0);
const expensesRows = list.map(e => `
<tr>
<td>${escapeHtml(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(resolvedExpensePaymentMethod(e))}</td>
<td>${formatExpenseTotalCell(e)}</td>
<td>${compactRateLabel(e)}</td>
</tr>
`).join("");
const keepTogetherClass = list.length <= 12 && list.reduce((n,e)=>n+String(e.note||"").length,0) < 900 ? " fitOnePage" : "";
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>
*{box-sizing:border-box}
body{font-family:Arial,"Noto Sans Hebrew",sans-serif;direction:rtl;padding:20px;color:#111827;background:#fff;margin:0}
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}
.reportTableSection{margin-top:18px}
.reportTableTitle{margin:0 0 10px;break-after:avoid-page;page-break-after:avoid}
.printTableViewport{max-width:100%;max-height:72vh;overflow:auto;border:1px solid #d7dce5;border-radius:12px}
table{width:100%;border-collapse:collapse;font-size:12px;background:#fff}
th,td{border:1px solid #d7dce5;padding:7px;text-align:center;vertical-align:middle;overflow-wrap:anywhere}
th{background:#2563eb;color:white;position:sticky;top:0}
tbody tr:nth-child(even) td{background:#fafafa}
tfoot td{font-weight:900;background:#eef6ff}
.expenseSubLine{display:block;font-size:10px;line-height:1.25;color:#475569;font-weight:700}
@page{size:A4 portrait;margin:10mm}
@media print{
body{padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact}
.printTableViewport{overflow:visible;max-height:none;border:0;border-radius:0}
.reportTableSection.fitOnePage{break-inside:avoid-page;page-break-inside:avoid}
.reportTableTitle{break-after:avoid-page;page-break-after:avoid}
table{font-size:9px;table-layout:fixed;break-before:avoid-page;page-break-before:avoid}
thead{display:table-header-group}
tfoot{display:table-row-group}
tr{break-inside:avoid-page;page-break-inside:avoid}
th,td{padding:4px 3px;word-break:break-word;overflow-wrap:anywhere}
th:nth-child(1),td:nth-child(1){width:8%}
th:nth-child(2),td:nth-child(2){width:12%}
th:nth-child(3),td:nth-child(3){width:12%}
th:nth-child(4),td:nth-child(4){width:22%}
th:nth-child(5),td:nth-child(5){width:9%}
th:nth-child(6),td:nth-child(6){width:12%}
th:nth-child(7),td:nth-child(7){width:17%}
th:nth-child(8),td:nth-child(8){width:8%}
}
</style>
</head>
<body>
<h1>דוח תקציב חופשה</h1>
<h2>${escapeHtml(activeTripTitle)}</h2>
<div class="meta">נוצר: ${generated}</div>
<div class="summary">${document.getElementById("summaryFinal")?.innerHTML || ""}</div>
<section class="reportDailySection">
<h2 class="reportTableTitle">סיכום יומי</h2>
<div class="printTableViewport">${document.getElementById("dailyTable")?.outerHTML || ""}</div>
</section>
<section class="reportTableSection${keepTogetherClass}">
<h2 class="reportTableTitle">פירוט הוצאות</h2>
<div class="printTableViewport">
<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>
<tfoot><tr><td colspan="6">סה״כ הוצאות</td><td>${fmtILS(reportTotal)}</td><td></td></tr></tfoot>
</table>
</div>
</section>
</body>
</html>`;
const blob = new Blob([report], {type:"text/html;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "דוח_תקציב_" + safeDownloadFilename(activeTripTitle) + ".html";
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1500);
}catch(err){
console.error("exportHTML failed", err);
alert("ייצוא הדוח ל־HTML נכשל. רענן את האפליקציה ונסה שוב.");
}
};
function buildPrintableBudgetReport(){
refreshFundingLedger();
renderSummary();
const activeTripTitle = getActiveTrip().name || tripName || "חופשה";
const generated = new Date().toLocaleString("he-IL");
const list = sortedActiveExpenses();
const reportTotal = list.reduce((sum,e) => sum + expenseILS(e), 0);
const keepTogetherClass = list.length <= 12 && list.reduce((n,e)=>n+String(e.note||"").length,0) < 900 ? " fitOnePage" : "";
const rows = list.map(e => `
<tr>
<td>${escapeHtml(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(resolvedExpensePaymentMethod(e))}</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>
<section class="reportTableSection${keepTogetherClass}">
<h2 class="reportTableTitle">פירוט הוצאות</h2>
<div class="printTableViewport">
<table class="reportExpensesTable">
<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>
<tfoot><tr><td colspan="6">סה״כ הוצאות</td><td>${fmtILS(reportTotal)}</td><td></td></tr></tfoot>
</table>
</div>
</section>
</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>
*{box-sizing:border-box}
@page{size:A4 portrait;margin:10mm}
html,body{background:#fff!important;color:#111827!important;margin:0}
body{font-family:Arial,"Noto Sans Hebrew",sans-serif;direction:rtl;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact}
h1,h2{text-align:center;margin:8px 0}
.meta{text-align:center;color:#475569;margin-bottom:14px}
.summary{border:1px solid #d7dce5;border-radius:12px;padding:10px;margin:10px 0;line-height:1.55;font-size:11px}
.reportTableSection{margin-top:14px}
.reportTableSection.fitOnePage{break-inside:avoid-page;page-break-inside:avoid}
.reportTableTitle{margin:0 0 8px;break-after:avoid-page;page-break-after:avoid}
.printTableViewport{overflow:visible;max-height:none}
table{width:100%;border-collapse:collapse;font-size:9px;table-layout:fixed;break-before:avoid-page;page-break-before:avoid}
thead{display:table-header-group}
tfoot{display:table-row-group}
tr{break-inside:avoid-page;page-break-inside:avoid}
th,td{border:1px solid #d7dce5;padding:4px 3px;text-align:center;vertical-align:middle;word-break:break-word;overflow-wrap:anywhere}
th{background:#0b84f3!important;color:white!important;font-weight:900}
tbody tr:nth-child(even) td{background:#fafafa!important}
tfoot td{background:#eef6ff!important;font-weight:900}
.expenseSubLine{display:block;font-size:7.5px;line-height:1.2;color:#475569;font-weight:700}
th:nth-child(1),td:nth-child(1){width:8%}
th:nth-child(2),td:nth-child(2){width:12%}
th:nth-child(3),td:nth-child(3){width:12%}
th:nth-child(4),td:nth-child(4){width:22%}
th:nth-child(5),td:nth-child(5){width:9%}
th:nth-child(6),td:nth-child(6){width:12%}
th:nth-child(7),td:nth-child(7){width:17%}
th:nth-child(8),td:nth-child(8){width:8%}
</style>
</head>
<body>${buildPrintableBudgetReport()}</body>
</html>`;
let iframe = document.getElementById("pdfPrintFrame");
if(iframe) iframe.remove();
iframe = document.createElement("iframe");
iframe.id = "pdfPrintFrame";
iframe.setAttribute("title", "דוח תקציב להדפסה");
iframe.style.position = "fixed";
iframe.style.left = "-2400px";
iframe.style.bottom = "0";
iframe.style.width = "794px";
iframe.style.height = "1123px";
iframe.style.opacity = "0";
iframe.style.border = "0";
iframe.style.pointerEvents = "none";
document.body.appendChild(iframe);
const printDoc = iframe.contentWindow.document;
printDoc.open();
printDoc.write(reportHtml);
printDoc.close();
const doPrint = () => {
try{
iframe.contentWindow.focus();
iframe.contentWindow.print();
}catch(err){
console.error("iframe print failed", err);
alert("ההדפסה נכשלה. נסה לפתוח את הדוח שוב או להשתמש בייצוא HTML.");
}
};
if(printDoc.readyState === "complete") setTimeout(doPrint, 250);
else iframe.addEventListener("load", () => setTimeout(doPrint, 250), {once:true});
}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","budgetOwner"].forEach(id => {
const el = document.getElementById(id);
if(el){
el.addEventListener("input", renderExpenseFeePreview);
el.addEventListener("change", renderExpenseFeePreview);
}
});
["authEmail","authPassword"].forEach(id => {
document.getElementById(id)?.addEventListener("keydown", event => {
if(event.key === "Enter") window.loginBudgetApp();
});
});
const versionBadge = document.getElementById("budgetAppVersionBadge");
if(versionBadge) versionBadge.innerText = "גרסת GOLD Audit · " + BUDGET_APP_VERSION;
setTimeout(renderExpenseFeePreview, 80);
setTimeout(initAppTabs, 50);
async function forceFirebaseServerRefresh(reason = "כניסה לאפליקציה"){
if(cloudRefreshPromise) return cloudRefreshPromise;
cloudRefreshPromise = (async () => {
const authEmailAtStart = String(auth.currentUser?.email || "").toLowerCase();
try{
setSyncStatus("pending", "מרענן נתונים מהענן...", "מבצע קריאה ישירה מהענן כדי לוודא שהמידע מעודכן.");
const [ratesSnap, tripsSnap, expensesSnap] = await withTimeout(Promise.all([
getDocFromServer(doc(db, "settings", "rates")),
getDocsFromServer(collection(db, "trips")),
getDocsFromServer(collection(db, "expenses"))
]), 15000, "רענון הנתונים נמשך זמן רב מדי");
if(!auth.currentUser || String(auth.currentUser.email || "").toLowerCase() !== authEmailAtStart) return;
if(ratesSnap.exists()) rates = normalizeRatesData(ratesSnap.data(), rates);
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);
schedulePendingWriteConfirmation();
}catch(err){
console.error("forceFirebaseServerRefresh failed", err);
setSyncStatus("pending", "הרענון הישיר נכשל", "ממשיך לעבוד עם הנתונים המקומיים והסנכרון הרגיל. בדוק חיבור אינטרנט ונסה שוב.");
}
})();
try{
return await cloudRefreshPromise;
}finally{
cloudRefreshPromise = null;
}
}
window.forceFirebaseServerRefresh = forceFirebaseServerRefresh;
function handleSnapshotError(context, err){
console.error(context + " snapshot failed", err);
setSyncStatus("pending", "הסנכרון הושהה", "לא ניתן לעדכן כרגע את " + context + ". הנתונים המקומיים נשמרו וייעשה ניסיון נוסף כשיחזור החיבור.");
}
async function startBudgetAppData(sessionId){
clearDataListeners();
await withTimeout(ensureDefaultSettings(), 15000, "בדיקת הגדרות ברירת המחדל נמשכה זמן רב מדי");
if(sessionId !== dataSessionId || !auth.currentUser) return;
await forceFirebaseServerRefresh("כניסה לאפליקציה");
if(sessionId !== dataSessionId || !auth.currentUser) return;
dataUnsubscribers.push(onSnapshot(doc(db, "settings", "rates"), async (snap) => {
if(snap.exists()) rates = normalizeRatesData(snap.data(), rates);
scheduleRenderAll();
await freezeLegacyExpensesOnce();
}, err => handleSnapshotError("שערי המטבע", err)));
dataUnsubscribers.push(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);
scheduleRenderAll();
}, err => handleSnapshotError("רשימת הטיולים", err)));
dataUnsubscribers.push(onSnapshot(collection(db, "expenses"), (snap) => {
expenses = [];
snap.forEach(d => expenses.push({id:d.id, ...d.data()}));
scheduleRenderAll();
}, err => handleSnapshotError("ההוצאות", err)));
}
</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;
}
*{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:#2563eb;
color:white;
border:none;
font-weight:900;
cursor:pointer;
}
button:disabled{opacity:.55;cursor:not-allowed}
@media(hover:hover){button:not(:disabled):hover{filter:brightness(.96);transform:translateY(-1px)}}
:is(button,input,select):focus-visible{outline:3px solid rgba(37,99,235,.35);outline-offset:2px}
button.export{background:#2e7d32}
button.orange{background:#c2410c}
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}
.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:#2563eb;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:#2563eb;
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(100px,1fr) minmax(120px,.8fr) minmax(115px,.8fr) 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 .budgetSourceHolding,.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:auto;
-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}
.reportTableSection{margin-top:18px}
.reportTableTitle{margin:0 0 10px!important;break-after:avoid-page;page-break-after:avoid}
.printTableViewport{max-width:100%;overflow:auto;border:1px solid #d7dce5;border-radius:12px;-webkit-overflow-scrolling:touch}
.printReport table{width:100%;min-width:760px;border-collapse:collapse;margin:0;font-size:12px;background:#fff}
.printReport thead th{position:sticky;top:0;z-index:2}
.printReport th,.printReport td{border:1px solid #d7dce5;padding:8px;text-align:center;vertical-align:middle;color:#111827;overflow-wrap:anywhere}
.printReport th{background:#0b84f3!important;color:#fff!important}
.printReport tbody tr:nth-child(even) td{background:#fafafa}
.printReport tfoot td{background:#eef6ff;font-weight:900}
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}
body.darkMode label{color:#e5e7eb}
body.darkMode .filterPanel,
body.darkMode .compareControls,
body.darkMode .tripBudgetSummaryBox,
body.darkMode .homeMiniNote,
body.darkMode .fxFeePreview,
body.darkMode .syncStatusBar,
body.darkMode .featureVerificationBox{
background:#1f2937;
color:#e5e7eb;
border-color:#475569;
}
body.darkMode .ratesHeadline{background:#292524;border-color:#a16207;color:#fef3c7}
body.darkMode .rateLine{border-color:#475569}
body.darkMode .tripBudgetSub,
body.darkMode .expenseSubLine,
body.darkMode .walletBalances .small{color:#cbd5e1}
@media(max-width:700px){
body.darkMode table.mobileCards tr{background:#1f2937;border-color:#475569;color:#e5e7eb}
body.darkMode table.mobileCards td::before{color:#cbd5e1}
}
@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}
}
@page{size:A4 portrait;margin:10mm}
@media print{
*{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
.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{max-width:none!important;border:0!important;border-radius:0!important;padding:0!important}
body.pdfPreviewOpen .printTableViewport{overflow:visible!important;border:0!important;border-radius:0!important}
body.pdfPreviewOpen .reportTableSection.fitOnePage{break-inside:avoid-page!important;page-break-inside:avoid!important}
body.pdfPreviewOpen .reportTableTitle{break-after:avoid-page!important;page-break-after:avoid!important}
body.pdfPreviewOpen .reportExpensesTable{min-width:0!important;width:100%!important;table-layout:fixed!important;font-size:9px!important;break-before:avoid-page!important;page-break-before:avoid!important}
body.pdfPreviewOpen .reportExpensesTable thead{display:table-header-group!important}
body.pdfPreviewOpen .reportExpensesTable tfoot{display:table-row-group!important}
body.pdfPreviewOpen .reportExpensesTable tr{break-inside:avoid-page!important;page-break-inside:avoid!important}
body.pdfPreviewOpen .reportExpensesTable th,body.pdfPreviewOpen .reportExpensesTable td{padding:4px 3px!important;word-break:break-word!important;overflow-wrap:anywhere!important}
body.pdfPreviewOpen .reportExpensesTable th:nth-child(1),body.pdfPreviewOpen .reportExpensesTable td:nth-child(1){width:8%}
body.pdfPreviewOpen .reportExpensesTable th:nth-child(2),body.pdfPreviewOpen .reportExpensesTable td:nth-child(2){width:12%}
body.pdfPreviewOpen .reportExpensesTable th:nth-child(3),body.pdfPreviewOpen .reportExpensesTable td:nth-child(3){width:12%}
body.pdfPreviewOpen .reportExpensesTable th:nth-child(4),body.pdfPreviewOpen .reportExpensesTable td:nth-child(4){width:22%}
body.pdfPreviewOpen .reportExpensesTable th:nth-child(5),body.pdfPreviewOpen .reportExpensesTable td:nth-child(5){width:9%}
body.pdfPreviewOpen .reportExpensesTable th:nth-child(6),body.pdfPreviewOpen .reportExpensesTable td:nth-child(6){width:12%}
body.pdfPreviewOpen .reportExpensesTable th:nth-child(7),body.pdfPreviewOpen .reportExpensesTable td:nth-child(7){width:17%}
body.pdfPreviewOpen .reportExpensesTable th:nth-child(8),body.pdfPreviewOpen .reportExpensesTable td:nth-child(8){width:8%}
.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" role="status" aria-live="polite">
<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">גרסת GOLD Audit · 2026.07.17-GOLD-AUDIT-PDF-COMPAT-v11</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 for="homeTripSelector"><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" maxlength="300" 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" role="status" aria-live="polite">הזן סכום כדי לראות מאיזו קופה תחויב ההוצאה והאם תחול עמלת המרה.</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 for="summaryCurrency"><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" role="img" aria-label="תרשים התפלגות הוצאות לפי קטגוריה">תרשים התפלגות הוצאות לפי קטגוריה.</canvas>
<div id="categoryLegend"></div>
</div>
<div class="chartBox">
<h3>📈 הוצאות לפי יום</h3>
<canvas id="dailyBar" class="chartCanvas" width="420" height="240" role="img" aria-label="תרשים הוצאות לפי יום">תרשים הוצאות לפי יום.</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" maxlength="80" placeholder="לדוגמה: יפן 2026 / ארה״ב 2027" oninput="scheduleTripNameSave()" onblur="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" role="status" aria-live="polite"></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>