Skip to content

Instantly share code, notes, and snippets.

@ey-ron
Created January 6, 2026 09:55
Show Gist options
  • Select an option

  • Save ey-ron/7c7156f5011b9e356b3be3b56cf5bd19 to your computer and use it in GitHub Desktop.

Select an option

Save ey-ron/7c7156f5011b9e356b3be3b56cf5bd19 to your computer and use it in GitHub Desktop.
Index.html
<!DOCTYPE html>
<html>
<head>
<script>
(function() {
var ua = window.navigator.userAgent;
var isSafari = /Safari/.test(ua) && !/Chrome/.test(ua) && !/CriOS/.test(ua) && !/FxiOS/.test(ua);
// 2. Check for iPhone Pro Screen Dimensions (Logical Resolutions)
// iPhone 14 Pro: 393 x 852
// iPhone 15 Pro: 393 x 852
// iPhone 16 Pro: 402 x 874 (Estimated/Standard scaling)
// iPhone 14 Pro Max / 15 Pro Max: 430 x 932
// iPhone 16 Pro Max: 440 x 956
var w = window.screen.width;
var h = window.screen.height;
var isProModel = (
(w === 393 && h === 852) || // 14 Pro, 15 Pro
(w === 402 && h === 874) || // 16 Pro
);
if (!isSafari || !isProModel) {
document.write(`
<style>
body {
background-color: #000;
color: #fff;
font-family: -apple-system, sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 40px;
margin: 0;
}
h1 { font-size: 20px; font-weight: 700; margin-bottom: 10px; color: #ef4444; }
p { font-size: 14px; color: #9ca3af; line-height: 1.5; }
.device-info {
margin-top: 20px;
padding: 10px 15px;
background: #111;
border-radius: 8px;
font-family: monospace;
font-size: 11px;
color: #555;
}
</style>
<body>
<h1>Unsupported Device</h1>
<p>This web application is optimized exclusively for <strong>iPhone 14 Pro, 15 Pro, or 16 Pro</strong> running on <strong>Safari</strong>.</p>
<div class="device-info">
Detected: ${w}x${h}<br>
Browser: ${isSafari ? 'Safari' : 'Other'}
</div>
</body>
`);
document.close();
window.stop();
}
})();
</script>
<base target="_top">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* --- GLOBAL RESET --- */
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
/* html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
background-color: #f4f6f8;
overflow: hidden;
user-select: none;
} */
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
/* NEW: Ambient Mesh Gradient */
background-color: #f4f6f8;
background-image:
radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.08) 0px, transparent 50%),
radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.08) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(59, 130, 246, 0.05) 0px, transparent 50%);
background-attachment: fixed; /* Keeps it still while scrolling */
overflow: hidden;
user-select: none;
}
.app-container {
height: 100%;
width: 100%;
max-width: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
position: relative;
}
.ios-date-header {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 11px;
font-weight: 600;
color: #86868b;
text-transform: uppercase;
letter-spacing: 0.8px;
line-height: 1;
text-align: left;
margin-bottom: 6px;
cursor: default;
}
/* --- VIEW WRAPPERS --- */
#dashboard-view {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
background-color: #f4f6f8;
z-index: 10;
}
#budget-detail-view,
#edit-view,
#retirement-view,
#remarks-view,
#networth-view,
#transaction-view,
#asset-edit-view,
#add-spend-view,
#investment-view,
#networth-detail-view,
#networth-edit-view,
#emergency-view,
#goals-view,
#expense-breakdown-view,
#networth-add-view,
#add-goal-view,
#add-stock-view {
display: none;
flex-direction: column;
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
background-color: #f4f6f8;
z-index: 50;
}
#transaction-view { z-index: 55; }
#asset-edit-view { z-index: 60; }
#add-spend-view { z-index: 60; }
#networth-detail-view { z-index: 65; }
#networth-edit-view { z-index: 70; }
#networth-add-view { z-index: 70; }
#add-goal-view { z-index: 70; }
#networth-view {
z-index: 60;
padding: 15px 15px 20px 15px;
justify-content: flex-start;
gap: 10px;
}
/* --- HEADER --- */
/* --- HEADER --- */
.header {
flex-shrink: 0;
padding: 20px 20px 10px 20px;
background-color: #f4f6f8;
text-align: center;
display: block;
}
.header-left {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
justify-content: flex-end; /* Align to bottom to match pills */
padding-bottom: 2px;
}
.header-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 5px; /* Reduced gap between pills */
padding-bottom: 2px;
}
/* UPDATED: Balanced Title Size */
.header h1 {
margin: 0;
font-size: 32px; /* Reduced from 40px */
color: #1e293b;
font-weight: 800;
letter-spacing: -1px;
cursor: pointer;
line-height: 1;
}
.header p {
margin: 5px 0 0 0;
font-size: 11px;
color: #94a3b8;
text-transform: uppercase;
}
.header a {
display: inline-block;
margin: 8px 0 0 0;
font-size: 10px;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
text-decoration: none;
cursor: pointer;
}
.header.main-layout {
display: flex;
justify-content: space-between;
align-items: flex-end;
text-align: left;
padding: 20px 30px 10px 30px;
}
.ios-date-header {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 11px;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 1px;
line-height: 1;
text-align: left;
margin-bottom: 6px;
cursor: default;
}
/* UPDATED: Compact Rate Pills */
.rate-pill {
background: white;
padding: 4px 10px; /* Reduced padding */
border-radius: 14px; /* Slightly tighter curves */
box-shadow: 0 2px 6px rgba(0,0,0,0.03); /* Softer shadow */
display: flex;
align-items: center;
gap: 6px;
font-size: 11px; /* Smaller font */
font-weight: 600;
color: #334155;
border: 1px solid rgba(255,255,255,0.8);
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s;
opacity: 0;
transform: translateY(10px);
min-width: 75px; /* Reduced width */
justify-content: space-between;
cursor: pointer;
user-select: none;
}
.rate-pill:active {
transform: scale(0.95) !important;
background-color: #f8fafc;
}
.rate-pill.refreshing .rp-val {
animation: pulseText 1s infinite;
opacity: 0.5;
}
@keyframes pulseText {
0% { opacity: 0.4; }
50% { opacity: 0.8; }
100% { opacity: 0.4; }
}
.rate-pill.loaded {
opacity: 1;
transform: translateY(0);
}
.rp-label { font-size: 9px; color: #94a3b8; text-transform: uppercase; font-weight: 700; letter-spacing: 0.5px; }
.rp-val { color: #1e293b; font-weight: 700; font-variant-numeric: tabular-nums; font-size: 11px; }
/* Tiny colored dots */
.rp-dot { width: 5px; height: 5px; border-radius: 50%; } /* Smaller dots */
.rp-dot.dbs { background: #ef4444; }
.rp-dot.uob { background: #003087; }
.header h1:active { opacity: 0.7; }
/* --- DASHBOARD CONTENT --- */
.dashboard-wrapper {
flex-grow: 1;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr 1fr 1fr;
width: 85%;
max-width: 380px;
margin: 0 auto;
padding-bottom: 10px;
gap: 10px;
min-height: 0;
}
/* --- CARD STYLING --- */
.card {
background: white;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.02),
0 10px 15px -3px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(0,0,0,0.02);
text-align: center;
height: 100%;
width: 100%;
min-height: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: 5px 15px;
cursor: pointer;
transition: transform 0.1s;
}
.stacked-card {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.02),
0 10px 15px -3px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(0,0,0,0.02);
transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.4s, filter 0.4s;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
padding: 5px 15px;
backface-visibility: hidden;
}
/* .card {
background: white;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
text-align: center;
height: 100%;
width: 100%;
min-height: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: 5px 15px;
cursor: pointer;
transition: transform 0.1s;
} */
.swipe-wrapper {
height: 100%;
width: 100%;
min-height: 0;
position: relative;
perspective: 1000px;
}
.card:active { transform: scale(0.98); }
/* .stacked-card {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 16px;
background: white;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.4s, filter 0.4s;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
padding: 5px 15px;
backface-visibility: hidden;
} */
.stacked-card h2 { margin-bottom: 5px !important; }
.stacked-card .stats { margin-top: 4px !important; padding-top: 8px !important; }
.card-front { z-index: 10; transform: translateX(0) scale(1); opacity: 1; }
.card-back { z-index: 5; transform: translateX(20px) scale(0.92); opacity: 0.6; filter: blur(1px); background: #f8fafc; }
.card-left { transform: translateX(-120%) scale(0.9); opacity: 0; z-index: 0; }
.card-right { transform: translateX(120%) scale(0.9); opacity: 0; z-index: 0; }
.swipe-dots { position: absolute; bottom: 8px; left: 0; width: 100%; display: flex; justify-content: center; gap: 4px; z-index: 20; pointer-events: none; }
.s-dot { width: 4px; height: 4px; border-radius: 50%; background: #e2e8f0; transition: background 0.3s; }
.s-dot.active { background: #94a3b8; }
.inv-pl-text { font-size: 24px; font-weight: 800; line-height: 1; color: #1e293b; }
.inv-pos { color: #10b981 !important; }
.inv-neg { color: #ef4444 !important; }
/* --- NET WORTH --- */
.nw-total-card {
background: white;
width: 100%;
padding: 15px 20px;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
text-align: center;
flex-shrink: 0;
}
.nw-total-card h2 { font-size: 10px; color: #64748b; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
.nw-total-card .amount {
font-size: 40px;
font-weight: 800;
color: #1e293b;
line-height: 1.1;
}
.nw-list-card {
background: white;
width: 100%;
border-radius: 20px;
box-shadow: 0 4px 15px rgba(0,0,0,0.03);
flex-grow: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.nw-list-header {
padding: 12px 20px;
border-bottom: 1px solid #f1f5f9;
font-size: 10px;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
flex-shrink: 0;
}
.nw-scroll-area { flex-grow: 1; overflow-y: auto; padding: 0; }
.nw-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 20px; border-bottom: 1px solid #f1f5f9; cursor: pointer; }
.nw-item:active { background: #f8fafc; }
.nw-item:last-child { border-bottom: none; }
.nw-info { display: flex; flex-direction: column; text-align: left;}
/* UPDATED LIST FONT SIZES */
.nw-acct { font-size: 14px; font-weight: 600; color: #1e293b; }
.nw-cat { font-size: 11px; color: #94a3b8; font-weight: 500; margin-top: 2px; }
.nw-val { font-size: 14px; font-weight: 700; color: #1e293b; }
.nw-debt { color: #ef4444 !important; }
/* --- APPLE-STYLE SWIPE ACTIONS --- */
.goal-card-wrapper {
position: relative;
overflow: hidden;
border-radius: 16px;
margin-bottom: 12px;
background-color: #ef4444;
}
.goal-card-content {
position: relative;
background: white;
z-index: 2;
padding: 15px 20px;
width: 100%;
height: 100%;
transform: translate3d(0,0,0);
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.goal-delete-action {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1;
color: white;
cursor: pointer;
}
/* SVG Icon styling */
.trash-icon { width: 24px; height: 24px; fill: white; margin-bottom: 4px; }
.trash-text { font-size: 10px; font-weight: 700; text-transform: uppercase; }
h2 { color: #64748b; margin: 0 0 5px 0; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px; flex-shrink: 0; }
.chart-wrapper { flex-grow: 1; position: relative; width: 100%; min-height: 0; display: flex; align-items: flex-end; justify-content: center; }
.chart-container { position: relative; width: 100%; height: 100%; max-width: 200px; margin: 0 auto; display: flex; align-items: flex-end; justify-content: center; }
.percentage-label { position: absolute; bottom: 0; left: 50%; transform: translate(-50%, 0); font-size: 22px; font-weight: 700; color: #1e293b; line-height: 1; }
.percentage-label .small-pct { font-size: 0.6em; vertical-align: 0.1em; opacity: 0.8; }
.stats { display: flex; justify-content: space-between; border-top: 1px solid #f1f5f9; padding-top: 8px; margin-top: 4px; flex-shrink: 0; }
.stat-box span { font-size: 10px; color: #94a3b8; text-transform: uppercase; font-weight: 600; }
.stat-box strong { display: block; color: #334155; font-size: 13px; font-weight: 600; }
.fixed-top-section { flex-shrink: 0; padding: 0 20px 5px 20px; }
.scrollable-middle { flex-grow: 1; overflow-y: auto; padding: 5px 20px; width: 100%; margin: 0 auto; }
.fixed-bottom-section { flex-shrink: 0; padding: 10px 20px 20px 20px; background: #f4f6f8; }
.budget-list-card { background: white; border-radius: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.03); padding: 0; overflow: hidden; }
.budget-row { display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; border-bottom: 1px solid #f1f5f9; cursor: pointer; }
.budget-row:active { background-color: #f8fafc; }
.budget-row:last-child { border-bottom: none; }
.b-cat { font-size: 13px; font-weight: 600; color: #1e293b; width: 40%; }
.b-data-col { text-align: right; width: 30%; }
.b-label { display: block; font-size: 9px; color: #94a3b8; text-transform: uppercase; }
.b-val { display: block; font-size: 13px; font-weight: 600; color: #334155; }
.b-val-left { color: #10b981; }
.b-val-neg { color: #ef4444; }
.trans-item { display: flex; justify-content: space-between; align-items: flex-start; padding: 12px 15px; border-bottom: 1px solid #f1f5f9; }
.trans-item:last-child { border-bottom: none; }
.trans-left { display: flex; flex-direction: column; gap: 4px; flex-grow: 1; min-width: 0; padding-right: 15px; }
.trans-date { font-size: 10px; color: #94a3b8; font-weight: 500; }
.trans-desc { font-size: 13px; color: #334155; font-weight: 600; line-height: 1.4; word-wrap: break-word; }
.trans-amt { font-size: 13px; color: #1e293b; font-weight: 700; white-space: nowrap; flex-shrink: 0; margin-top: 2px; }
#retirement-view .header { padding-top: 20px; padding-bottom: 5px; }
.prediction-box { background: white; border-radius: 16px; padding: 10px 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); text-align: center; border: 1px solid #f1f5f9; }
.pred-label { font-size: 10px; color: #64748b; text-transform: uppercase; font-weight: 600; letter-spacing: 0.5px; }
.pred-amount { font-size: 20px; font-weight: 800; margin-top: 2px; color: #1e293b; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.view-remarks-link { display: inline-block; margin-top: 4px; color: #3b82f6; font-size: 10px; font-weight: 600; cursor: pointer; text-decoration: none; padding: 2px; }
.assets-card { background: white; border-radius: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.03); padding: 0; overflow: hidden; }
.asset-item { padding: 16px 20px; border-bottom: 1px solid #f1f5f9; cursor: pointer; }
.asset-item:active { background-color: #f8fafc; }
.asset-item:last-child { border-bottom: none; }
.asset-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.asset-name { font-size: 14px; font-weight: 700; color: #1e293b; }
.asset-percent { font-size: 10px; font-weight: 600; color: white; background: #3b82f6; padding: 2px 6px; border-radius: 4px; }
.asset-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px 10px; }
.ag-cell-top { grid-column: span 2; }
.ag-cell-bottom { grid-column: span 3; }
.ag-cell span { display: block; font-size: 9px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.3px; margin-bottom: 4px; }
.ag-cell div { display: block; font-size: 13px; color: #334155; font-weight: 600; white-space: nowrap; }
.profit-val { color: #10b981 !important; font-weight: 700 !important; }
.loss-val { color: #ef4444 !important; font-weight: 700 !important; }
.detail-card { background: white; border-radius: 16px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.03); }
.form-group { margin-bottom: 15px; text-align: left; }
.form-label { display: block; font-size: 12px; color: #64748b; margin-bottom: 5px; font-weight: 600; }
.form-input {
width: 100%; box-sizing: border-box; padding: 12px;
border: 1px solid #e2e8f0; border-radius: 8px;
font-size: 16px; color: #1e293b; outline: none; background-color: white;
-webkit-appearance: none; appearance: none;
}
input[type="date"] { display: block; min-width: 0; max-width: 100%; background-color: white; }
.form-input:disabled { background-color: #f8fafc; color: #94a3b8; opacity: 1; }
.remarks-text { font-size: 15px; line-height: 1.6; color: #334155; white-space: pre-wrap; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; background: #f8fafc; border-radius: 12px; padding: 15px; margin-bottom: 20px; }
.info-box { text-align: left; }
.info-box span { display: block; font-size: 10px; color: #94a3b8; text-transform: uppercase; margin-bottom: 2px; }
.info-box strong { display: block; font-size: 13px; color: #475569; font-weight: 700; }
.btn { width: 100%; padding: 14px; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; }
.btn-primary { background-color: #10b981; color: white; }
.btn-secondary { background-color: #e2e8f0; color: #64748b; }
.btn-row { display: flex; gap: 10px; margin-top: 10px; }
.btn-row .btn { flex: 1; }
#pin-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(244, 246, 248, 0.98);
z-index: 20000; display: none;
flex-direction: column; justify-content: center; align-items: center;
backdrop-filter: blur(5px);
}
.pin-dots { display: flex; gap: 15px; margin-bottom: 40px; }
.pin-dot { width: 12px; height: 12px; border-radius: 50%; background: #cbd5e1; transition: background 0.2s; }
.pin-dot.active { background: #3b82f6; }
.pin-keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; width: 280px; justify-items: center; align-items: center; }
.pin-key {
width: 70px; height: 70px; border-radius: 50%;
background: white; border: 1px solid #e2e8f0;
display: flex; justify-content: center; align-items: center;
font-size: 24px; font-weight: 600; color: #1e293b;
cursor: pointer; user-select: none;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
padding: 0; margin: 0; line-height: 0;
}
.pin-key:active { background: #f1f5f9; transform: scale(0.95); }
.pin-title { margin-bottom: 20px; font-size: 16px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 1px; }
/* --- FOOTER: TEXT CENTER, ICON RIGHT --- */
.version-footer {
flex-shrink: 0;
position: relative;
padding: 5px 35px 20px 35px;
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
}
.footer-icon-btn:active {
opacity: 1;
transform: scale(0.9);
background: #f1f5f9;
color: #64748b;
}
.footer-icon-svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor; /* Inherits from parent color */
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
transition: stroke 0.3s;
}
.footer-icon-btn {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s, transform 0.2s;
border-radius: 50%;
background: transparent;
z-index: 2;
color: #94a3b8; /* Default icon color */
}
/* Wrapper for the Version Text (Keeps it Centered) */
.footer-text-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 1;
padding-bottom: 15px;
}
/* The Text Itself */
#footer-ver-text {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #94a3b8;
pointer-events: auto;
transition: color 0.3s, transform 0.1s;
}
/* Refresh Icon Container */
.footer-refresh-btn {
/* margin-left: auto; <--- Removed, not needed with justify-content: flex-end */
width: 30px; /* Increased touch target slightly */
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s, transform 0.2s;
border-radius: 50%;
background: transparent;
z-index: 2;
}
#footer-ver-text:active {
transform: scale(0.96);
opacity: 0.8;
}
.footer-refresh-icon {
width: 16px;
height: 16px;
fill: none;
stroke: #94a3b8;
stroke-width: 2px; /* 2px is the sweet spot for 16px size */
stroke-linecap: round;
stroke-linejoin: round;
transition: stroke 0.3s;
}
.footer-refresh-btn:active { opacity: 1; transform: scale(0.9); background: #f1f5f9; }
.footer-refresh-icon {
width: 16px; /* Slightly larger for visibility since lines are thin */
height: 16px;
/* LINE STYLING (Crucial for Elegant Look) */
fill: none; /* Transparent inside */
stroke: #94a3b8; /* Color of the lines */
stroke-width: 2px; /* Thickness (2px is perfect for this size) */
stroke-linecap: round; /* Rounds the ends of the lines */
stroke-linejoin: round; /* Rounds the corners */
transition: stroke 0.3s;
}
/* Optional: Darken slightly on press */
.footer-refresh-btn:active .footer-refresh-icon {
stroke: #64748b;
}
.card-refreshing {
animation: cardPulse 1.2s infinite ease-in-out;
pointer-events: none;
}
@keyframes cardPulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.8; transform: scale(0.98); }
100% { opacity: 1; transform: scale(1); }
}
#loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: white; z-index: 10000; display: flex; flex-direction: column; justify-content: center; align-items: center; }
.money-bag-icon { width: 80px; height: 80px; margin-bottom: 30px; animation: float 3s ease-in-out infinite; }
@keyframes float { 0% { transform: translateY(0px); } 50% { transform: translateY(-10px); } 100% { transform: translateY(0px); } }
.progress-container { width: 200px; height: 6px; background: #f1f5f9; border-radius: 10px; overflow: hidden; position: relative; }
.progress-bar { position: absolute; top: 0; left: 0; height: 100%; width: 100%; background: linear-gradient(90deg, transparent, #10b981, transparent); transform: translateX(-100%); animation: loading-slide 1.5s infinite; }
@keyframes loading-slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
#loading-text { margin-top: 15px; color: #94a3b8; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; }
/* --- TOAST NOTIFICATION --- */
#toast-msg {
visibility: hidden;
min-width: 250px;
background-color: rgba(30, 41, 59, 0.95);
color: #fff;
text-align: center;
border-radius: 50px;
padding: 12px 20px;
position: fixed;
z-index: 20000;
left: 50%;
bottom: 30px;
transform: translateX(-50%);
font-size: 13px;
font-weight: 500;
opacity: 0;
transition: opacity 0.3s, bottom 0.3s;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
#toast-msg.show {
visibility: visible;
opacity: 1;
bottom: 100px;
}
@media (max-width: 899px) and (orientation: landscape) {
.dashboard-wrapper { display: grid !important; grid-template-columns: 1fr 1fr; gap: 12px; width: 94%; max-width: none; overflow-y: auto; padding-bottom: 20px; align-content: flex-start; }
.header { padding: 10px 20px 5px 20px; }
.card, .stacked-card { min-height: 140px; padding: 10px 15px; }
.chart-container { max-width: 80px; height: 80px; }
.percentage-label { font-size: 16px; bottom: 0; }
h2 { font-size: 10px; margin-bottom: 2px; }
.stats { margin-top: 2px; padding-top: 4px; }
}
@media (min-width: 900px) {
body { display: flex; justify-content: center; align-items: center; background-color: #eef2f6; height: 100vh; overflow: hidden; }
.app-container { width: 100%; max-width: 1000px; height: 85vh; max-height: 800px; background-color: #f4f6f8; border-radius: 24px; box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.15); overflow: hidden; padding-bottom: 0; position: relative; }
#dashboard-view, #edit-view, #retirement-view, #remarks-view, #budget-detail-view, #networth-view, #transaction-view, #asset-edit-view, #add-spend-view, #investment-view, #networth-detail-view, #networth-edit-view, #networth-add-view { position: absolute; top: 0; left: 0; height: 100%; width: 100%; }
.dashboard-wrapper { display: flex; flex-direction: row; flex-wrap: nowrap; justify-content: space-between; align-items: center; gap: 20px; width: 100%; max-width: 100%; height: 100%; flex-grow: 1; padding: 0 40px; box-sizing: border-box; }
.card, .swipe-wrapper { flex: 1; min-width: 0; height: 300px; }
.card, .stacked-card { padding: 20px; border-radius: 16px; display: flex; flex-direction: column; justify-content: center; }
.chart-wrapper { flex-grow: 1; width: 100%; height: auto; margin-bottom: 10px; display: flex; align-items: flex-end; justify-content: center; }
.chart-container { max-width: 160px !important; height: 160px !important; margin: 0 auto; }
h2 { font-size: 12px; margin-bottom: 15px; }
.percentage-label { font-size: 24px; bottom: 10px; }
.stat-box span { font-size: 11px; }
.stat-box strong { font-size: 14px; }
.stats { margin-top: 15px; padding-top: 10px; }
.scrollable-middle { max-width: 700px; width: 100%; margin: 0 auto; overflow-y: auto; height: 100%; }
.fixed-top-section, .fixed-bottom-section { max-width: 700px; margin: 0 auto; width: 100%; box-sizing: border-box; }
.header { padding-top: 40px; }
#networth-view { display: none; flex-direction: column; align-items: center; justify-content: center; padding: 40px; }
.nw-total-card, .nw-list-card { max-width: 500px; }
.nw-total-card { margin-bottom: 20px; }
.dashboard-wrapper {
/* RESET to Flex for Desktop so they sit in one row */
display: flex;
flex-direction: row;
grid-template-rows: none; /* Reset grid rows */
/* ... rest of existing desktop styles ... */
justify-content: space-between;
align-items: center;
gap: 20px;
width: 100%;
max-width: 100%;
height: 100%;
flex-grow: 1;
padding: 0 40px;
box-sizing: border-box;
}
}
.comp-split {
display: flex;
flex: 1;
width: 100%;
align-items: flex-start;
position: relative;
margin-top: 8px;
}
.comp-divider {
width: 1px;
height: 60%;
background-color: #f1f5f9;
margin: 10px 5px 0 5px;
}
.comp-box {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
/* LABELS & VALUES */
.comp-label {
font-size: 10px;
color: #94a3b8;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
line-height: 1;
}
.comp-val-main {
font-size: 18px;
font-weight: 800;
color: #1e293b;
margin-bottom: 2px;
line-height: 1.1;
}
.comp-val-sub {
font-size: 11px;
color: #64748b;
font-weight: 500;
margin-bottom: 6px;
line-height: 1.1;
}
.comp-pill {
font-size: 10px;
font-weight: 700;
padding: 3px 8px;
border-radius: 12px;
display: inline-block;
}
/* HISTORY LIST (Right Side) */
.hist-container {
width: 100%;
padding: 0 8px;
margin-top: 6px;
}
.hist-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
margin-bottom: 0px;
padding-bottom: 0px;
}
.hist-row:last-child {
border-bottom: none;
}
.hist-year {
color: #94a3b8;
font-weight: 600;
margin-left: 10px;
}
.hist-val { color: #334155; font-weight: 700; }
.cp-bad { background: #fee2e2; color: #ef4444; }
.cp-good { background: #dcfce7; color: #16a34a; }
.cp-neutral { background: #f1f5f9; color: #94a3b8; }
#yoy-view {
display: none;
flex-direction: column;
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
background-color: #f4f6f8;
z-index: 60;
}
/* --- SEGMENTED CONTROL (iOS Style) --- */
.seg-control {
display: flex;
background: #e2e8f0;
padding: 4px;
border-radius: 12px;
margin-bottom: 20px;
}
.seg-btn {
flex: 1;
text-align: center;
padding: 8px 0;
font-size: 13px;
font-weight: 600;
color: #64748b;
border-radius: 9px;
cursor: pointer;
transition: all 0.2s;
}
.seg-btn.active {
background: white;
color: #1e293b;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
}
/* --- GOALS VIEW --- */
.goal-card {
background: white;
border-radius: 16px;
padding: 15px 20px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
position: relative;
overflow: hidden;
}
.goal-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; }
.goal-title { font-size: 14px; font-weight: 700; color: #1e293b; }
.goal-date { font-size: 11px; color: #64748b; font-weight: 500; margin-top: 2px; }
.goal-progress-bg { width: 100%; height: 6px; background: #f1f5f9; border-radius: 3px; overflow: hidden; margin: 10px 0 6px 0; }
.goal-progress-fill { height: 100%; border-radius: 3px; transition: width 1s; }
.goal-stats { display: flex; justify-content: space-between; align-items: flex-end; }
.gs-label { font-size: 9px; color: #94a3b8; text-transform: uppercase; font-weight: 600; display: block; }
.gs-val { font-size: 13px; font-weight: 700; color: #334155; }
/* Status Tags */
.g-tag { font-size: 9px; font-weight: 700; padding: 3px 8px; border-radius: 20px; text-transform: uppercase; letter-spacing: 0.5px; }
.gt-secured { background: #dcfce7; color: #15803d; } /* Green */
.gt-track { background: #dbeafe; color: #1e40af; } /* Blue */
.gt-risk { background: #fee2e2; color: #b91c1c; } /* Red */
/* --- ANIMATIONS --- */
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* --- UPDATED ANIMATIONS --- */
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-slide-in {
opacity: 0;
animation: slideInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
/* --- EDIT ALLOCATION STYLES --- */
.alloc-group-card {
background: white;
border-radius: 16px;
padding: 15px 20px;
margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.alloc-header {
font-size: 13px;
font-weight: 800;
color: #1e293b;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f1f5f9;
}
.alloc-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.alloc-row:last-child { margin-bottom: 0; }
.alloc-label {
font-size: 13px;
color: #64748b;
font-weight: 500;
flex-grow: 1;
}
.alloc-input-wrapper {
width: 100px;
position: relative;
}
.alloc-input {
width: 100%;
padding: 8px 10px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
color: #1e293b;
text-align: right;
outline: none;
transition: border-color 0.2s;
-webkit-appearance: none;
}
.alloc-input:focus { border-color: #3b82f6; }
/* --- BUDGET LIST SWIPE STYLES --- */
.budget-swipe-wrapper {
position: relative;
overflow: hidden;
background-color: #3b82f6; /* Blue for Edit */
}
.budget-swipe-wrapper:first-child { border-top-left-radius: 16px; border-top-right-radius: 16px; }
.budget-swipe-wrapper:last-child { border-bottom-left-radius: 16px; border-bottom-right-radius: 16px; }
.budget-swipe-content {
position: relative;
background: white;
z-index: 2;
width: 100%;
height: 100%;
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.budget-edit-action {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1;
color: white;
cursor: pointer;
}
/* Custom Edit Icon */
.edit-icon-svg { width: 20px; height: 20px; fill: white; margin-bottom: 2px; }
.edit-text { font-size: 10px; font-weight: 700; text-transform: uppercase; }
/* --- DOUBLE SWIPE ACTIONS --- */
.double-action-wrapper {
position: relative;
overflow: hidden;
border-radius: 0;
background-color: #ef4444;
}
.double-actions {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 140px;
display: flex;
z-index: 1;
}
.action-btn {
width: 70px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
}
.ab-edit { background-color: #3b82f6; } /* Blue */
.ab-del { background-color: #ef4444; } /* Red */
.swipe-content-row {
position: relative;
background: white;
z-index: 2;
transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
border-bottom: 1px solid #f1f5f9;
}
/* --- MIDDLE TOAST ANIMATION --- */
#toast-msg.show-middle {
visibility: visible;
opacity: 1;
bottom: 50%; /* Move to vertical center */
transform: translate(-50%, 50%); /* Center alignment correction */
}
#ret-expense-detail-view { display: none; flex-direction: column; height: 100%; width: 100%; position: absolute; top: 0; left: 0; background-color: #f4f6f8; z-index: 75; }
#ret-expense-edit-view { display: none; flex-direction: column; height: 100%; width: 100%; position: absolute; top: 0; left: 0; background-color: #f4f6f8; z-index: 80; }
/* Make the specific field in Retirement Plan look clickable */
.editable-field-link {
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
background: #f1f5f9;
transition: background 0.2s;
}
.editable-field-link:active { background: #e2e8f0; }
/* --- PREMIUM EXCHANGE RATE POPUP --- */
#rate-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
/* Darker, cinematic dimming */
background: rgba(0, 0, 0, 0.4);
/* Heavy iOS Blur */
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
z-index: 9000;
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s cubic-bezier(0.33, 1, 0.68, 1);
}
.rate-card {
/* Translucent Frosted White */
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(255, 255, 255, 0.6);
width: 85%;
max-width: 320px;
border-radius: 28px; /* Softer, larger corners */
padding: 32px 24px;
/* Deep, soft shadow */
box-shadow:
0 20px 60px -10px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.2) inset;
transform: scale(0.92) translateY(10px);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
text-align: center;
}
/* Typography Hierarchy */
.rate-card h3 {
margin: 0 0 6px 0;
font-size: 19px;
color: #1c1c1e;
font-weight: 800;
letter-spacing: -0.5px;
}
.rate-card p {
margin: 0 0 30px 0;
font-size: 12px;
color: #8e8e93;
font-weight: 500;
letter-spacing: 0.3px;
}
/* Row Styling */
.rate-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 0;
border-bottom: 1px solid rgba(60, 60, 67, 0.06); /* Very subtle divider */
}
.rate-row:last-of-type { border-bottom: none; margin-bottom: 15px; }
.bank-info { display: flex; align-items: center; gap: 10px; }
.bank-name {
font-size: 15px;
font-weight: 600;
color: #3a3a3c;
letter-spacing: -0.2px;
}
/* Colored Dots */
.bank-dot { width: 10px; height: 10px; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.dbs-dot { background: linear-gradient(135deg, #ff3333, #ff0000); }
.uob-dot { background: linear-gradient(135deg, #003087, #001f57); }
/* Number Styling */
.bank-val {
font-size: 22px;
font-weight: 700;
color: #1c1c1e;
letter-spacing: -0.8px;
/* Tabular nums ensure numbers don't jitter when updating */
font-variant-numeric: tabular-nums;
}
.bank-val span {
font-size: 11px;
color: #aeaeb2;
font-weight: 600;
margin-left: 4px;
transform: translateY(-2px);
display: inline-block;
}
/* Floating Action Button */
.rate-close-btn {
margin-top: 10px;
background: #1c1c1e; /* Pure black/dark gray */
color: #ffffff;
border: none;
padding: 16px;
width: 100%;
border-radius: 20px;
font-weight: 600;
font-size: 15px;
cursor: pointer;
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
transition: transform 0.1s, background-color 0.2s;
}
.rate-close-btn:active {
background: #3a3a3c;
transform: scale(0.97);
}
</style>
</head>
<body>
<div id="pin-overlay">
<div class="pin-title">Enter Access PIN</div>
<div class="pin-dots">
<div class="pin-dot" id="dot-1"></div>
<div class="pin-dot" id="dot-2"></div>
<div class="pin-dot" id="dot-3"></div>
<div class="pin-dot" id="dot-4"></div>
</div>
<div class="pin-keypad">
<div class="pin-key" ontouchstart="pressPin(1); event.preventDefault()" onclick="pressPin(1)">1</div>
<div class="pin-key" ontouchstart="pressPin(2); event.preventDefault()" onclick="pressPin(2)">2</div>
<div class="pin-key" ontouchstart="pressPin(3); event.preventDefault()" onclick="pressPin(3)">3</div>
<div class="pin-key" ontouchstart="pressPin(4); event.preventDefault()" onclick="pressPin(4)">4</div>
<div class="pin-key" ontouchstart="pressPin(5); event.preventDefault()" onclick="pressPin(5)">5</div>
<div class="pin-key" ontouchstart="pressPin(6); event.preventDefault()" onclick="pressPin(6)">6</div>
<div class="pin-key" ontouchstart="pressPin(7); event.preventDefault()" onclick="pressPin(7)">7</div>
<div class="pin-key" ontouchstart="pressPin(8); event.preventDefault()" onclick="pressPin(8)">8</div>
<div class="pin-key" ontouchstart="pressPin(9); event.preventDefault()" onclick="pressPin(9)">9</div>
</div>
<div style="margin-top:20px; color:#ef4444; font-size:12px; font-weight:600; min-height:15px;" id="pin-error"></div>
<div style="margin-top:10px; color:#94a3b8; font-size:12px; font-weight:500; cursor:pointer;" onclick="closePinPad()">Cancel</div>
</div>
<div id="loading-overlay">
<svg class="money-bag-icon" version="1.1" id="ecommerce_1_" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 115 115" style="enable-background:new 0 0 115 115" xml:space="preserve">
<style>
.st0{fill:#ffeead}
.st8{fill:#ffcc5c}
.st13{fill:#d6a041}
.st19{fill:#fff}
</style>
<path class="st13" d="m80.97 72.115-.18 3.808a8.076 8.076 0 0 0-.081.795c-.306 6.478 7.075 12.089 16.486 12.534 9.168.433 16.877-4.198 17.552-10.426h.011l.014-.308c.005-.064.013-.127.016-.191.003-.064 0-.128.002-.192l.21-4.413-34.03-1.607z"/>
<ellipse transform="rotate(-87.295 97.952 73.251)" class="st8" cx="97.951" cy="73.253" rx="11.742" ry="17.059"/>
<ellipse transform="rotate(-87.295 97.952 73.252)" class="st19" cx="97.951" cy="73.253" rx="9.14" ry="13.278"/>
<ellipse transform="rotate(-87.295 97.952 73.252)" class="st0" cx="97.951" cy="73.253" rx="8.461" ry="12.293"/>
<path class="st13" d="M38.316 75.231v-4.418H4.249v3.812a8.14 8.14 0 0 0-.044.799c0 6.485 7.638 11.742 17.059 11.742 9.178 0 16.66-4.989 17.041-11.242h.011v-.308c.002-.064.007-.127.007-.192.001-.066-.005-.129-.007-.193z"/>
<ellipse class="st8" cx="21.264" cy="71.148" rx="17.059" ry="11.742"/>
<ellipse class="st19" cx="21.264" cy="71.148" rx="13.278" ry="9.14"/>
<ellipse class="st0" cx="21.264" cy="71.148" rx="12.293" ry="8.462"/>
<path class="st13" d="M34.628 66.551v-4.419H.561v3.812a8.14 8.14 0 0 0-.044.799c0 6.485 7.638 11.742 17.059 11.742 9.178 0 16.66-4.989 17.041-11.242h.011v-.308c.001-.064.007-.127.007-.192 0-.064-.005-.128-.007-.192z"/>
<ellipse class="st8" cx="17.576" cy="62.468" rx="17.059" ry="11.742"/>
<ellipse class="st19" cx="17.576" cy="62.468" rx="13.278" ry="9.14"/>
<ellipse class="st0" cx="17.576" cy="62.468" rx="12.293" ry="8.461"/>
<path class="st13" d="M35.589 58.915v-4.419H1.521v3.812a8.14 8.14 0 0 0-.044.799c0 6.485 7.638 11.742 17.059 11.742 9.178 0 16.66-4.99 17.041-11.242h.011v-.308c.002-.064.007-.128.007-.192s-.005-.128-.006-.192z"/>
<ellipse class="st8" cx="18.537" cy="54.832" rx="17.059" ry="11.742"/>
<ellipse class="st19" cx="18.537" cy="54.832" rx="13.278" ry="9.14"/>
<ellipse class="st0" cx="18.537" cy="54.832" rx="12.293" ry="8.461"/>
<g>
<path class="st13" d="M34.111 49.192v-4.419H.044v3.812a8.14 8.14 0 0 0-.044.799c0 6.485 7.638 11.742 17.059 11.742 9.178 0 16.66-4.989 17.041-11.242h.011v-.308c.001-.064.007-.127.007-.192s-.005-.128-.007-.192z"/>
<ellipse class="st8" cx="17.059" cy="45.109" rx="17.059" ry="11.742"/>
<ellipse class="st19" cx="17.059" cy="45.109" rx="13.278" ry="9.14"/>
<ellipse class="st0" cx="17.059" cy="45.109" rx="12.293" ry="8.462"/>
</g>
<g>
<path class="st13" d="M76.554 76.059V71.64H42.486v3.812c-.026.264-.044.53-.044.798 0 6.485 7.638 11.742 17.059 11.742 9.178 0 16.66-4.989 17.041-11.242h.011v-.308c.001-.064.007-.127.007-.192s-.005-.127-.006-.191z"/>
<ellipse class="st8" cx="59.502" cy="71.976" rx="17.059" ry="11.742"/>
<ellipse class="st19" cx="59.502" cy="71.976" rx="13.278" ry="9.14"/>
<ellipse class="st0" cx="59.502" cy="71.976" rx="12.293" ry="8.462"/>
</g>
<g>
<path class="st13" d="M77.391 69.652v-4.419H43.323v3.812c-.026.264-.044.53-.044.798 0 6.485 7.638 11.742 17.059 11.742 9.178 0 16.66-4.989 17.041-11.242h.011v-.308c.001-.064.007-.128.007-.192.001-.063-.005-.127-.006-.191z"/>
<ellipse class="st8" cx="60.339" cy="65.569" rx="17.059" ry="11.742"/>
<ellipse class="st19" cx="60.339" cy="65.569" rx="13.278" ry="9.14"/>
<ellipse class="st0" cx="60.339" cy="65.569" rx="12.293" ry="8.462"/>
</g>
<g>
<path class="st13" d="M76.14 59.413v-4.419H42.073v3.812c-.026.264-.044.53-.044.798 0 6.485 7.638 11.742 17.059 11.742 9.178 0 16.66-4.989 17.041-11.242h.011v-.308c.002-.064.007-.127.007-.192 0-.064-.005-.127-.007-.191z"/>
<ellipse class="st8" cx="59.088" cy="55.33" rx="17.059" ry="11.742"/>
<ellipse class="st19" cx="59.088" cy="55.33" rx="13.278" ry="9.14"/>
<ellipse class="st0" cx="59.088" cy="55.33" rx="12.293" ry="8.462"/>
</g>
<g>
<path class="st13" d="m78.166 65.386-.18 3.808a8.076 8.076 0 0 0-.081.795c-.306 6.478 7.075 12.089 16.486 12.534 9.168.433 16.877-4.198 17.552-10.426h.011l.015-.308c.005-.064.013-.127.016-.191.003-.064 0-.128.002-.192l.208-4.414-34.029-1.606z"/>
<ellipse transform="rotate(-87.295 95.149 66.523)" class="st8" cx="95.147" cy="66.524" rx="11.742" ry="17.059"/>
<ellipse transform="rotate(-87.295 95.149 66.523)" class="st19" cx="95.147" cy="66.524" rx="9.14" ry="13.278"/>
<ellipse transform="rotate(-87.295 95.148 66.523)" class="st0" cx="95.147" cy="66.524" rx="8.461" ry="12.293"/>
</g>
<g>
<path class="st13" d="M38.75 41.38v-4.419H4.683v3.812c-.026.264-.044.53-.044.798 0 6.485 7.638 11.742 17.059 11.742 9.178 0 16.66-4.989 17.041-11.242h.011v-.308c.001-.064.007-.127.007-.192s-.005-.127-.007-.191z"/>
<ellipse class="st8" cx="21.698" cy="37.297" rx="17.059" ry="11.742"/>
<ellipse class="st19" cx="21.698" cy="37.297" rx="13.278" ry="9.14"/>
<ellipse class="st0" cx="21.698" cy="37.297" rx="12.293" ry="8.462"/>
</g>
<g>
<path class="st13" d="M35.279 33.569V29.15H1.211v3.812c-.026.264-.044.53-.044.798 0 6.485 7.638 11.742 17.059 11.742 9.178 0 16.66-4.989 17.041-11.242h.011v-.308c.002-.064.007-.127.007-.192.001-.064-.005-.127-.006-.191z"/>
<ellipse class="st8" cx="18.227" cy="29.486" rx="17.059" ry="11.742"/>
<ellipse class="st19" cx="18.227" cy="29.486" rx="13.278" ry="9.14"/>
<ellipse class="st0" cx="18.227" cy="29.486" rx="12.293" ry="8.462"/>
</g>
<g>
<circle class="st8" cx="34.116" cy="77.983" r="16.549"/>
<circle class="st13" cx="34.116" cy="80.708" r="16.549"/>
<circle class="st19" cx="34.116" cy="80.708" r="12.881"/>
<circle class="st0" cx="34.116" cy="80.708" r="11.925"/>
<path class="st13" d="M36.673 79.793c-1.655-1.26-2.471-1.878-2.471-2.471 0-.173.099-.371.321-.371.198 0 .642.939.815.939l3.385-.173c.173-.025.272-.124.272-.271 0-2.175-1.384-4.052-3.657-4.522 0-1.458 0-1.606-.198-1.606h-1.804c-.099 0-.173.099-.173.222v1.36c-2.298.47-3.533 2.446-3.533 4.324 0 1.779.914 3.237 2.322 4.25 1.952 1.408 2.545 1.952 2.545 2.594a.519.519 0 0 1-.543.544c-.569 0-.964-.519-1.137-1.384-.049-.222-.124-.271-.272-.271l-3.138.173c-.124.024-.197.098-.197.222 0 2.916 1.359 4.62 3.953 5.09 0 1.482 0 1.655.197 1.655h1.779a.213.213 0 0 0 .198-.198v-1.433c2.298-.445 3.681-2.125 3.681-4.596.002-1.532-.813-2.915-2.345-4.077z"/>
<path class="st8" d="M36.673 78.871c-1.655-1.26-2.471-1.878-2.471-2.471 0-.173.099-.371.321-.371.198 0 .642.939.815.939l3.385-.173c.173-.025.272-.124.272-.272 0-2.175-1.384-4.052-3.657-4.522 0-1.458 0-1.606-.198-1.606h-1.804c-.099 0-.173.099-.173.222v1.359c-2.298.47-3.533 2.446-3.533 4.324 0 1.779.914 3.237 2.322 4.25 1.952 1.408 2.545 1.952 2.545 2.594a.519.519 0 0 1-.543.544c-.569 0-.964-.519-1.137-1.384-.049-.222-.124-.271-.272-.271l-3.138.173c-.124.024-.197.098-.197.222 0 2.916 1.359 4.62 3.953 5.09 0 1.482 0 1.655.197 1.655h1.779a.213.213 0 0 0 .198-.198v-1.433c2.298-.445 3.681-2.125 3.681-4.596.002-1.53-.813-2.914-2.345-4.075z"/>
</g>
<g>
<path class="st13" d="M20.587 28.557c-1.988-1.012-2.967-1.508-2.967-1.984 0-.139.119-.298.386-.298.237 0 .771.754.979.754l4.065-.139c.207-.02.326-.099.326-.218 0-1.746-1.661-3.254-4.391-3.631 0-1.17 0-1.29-.237-1.29h-2.166c-.119 0-.207.079-.207.179v1.091c-2.759.377-4.243 1.964-4.243 3.472 0 1.429 1.098 2.599 2.789 3.412 2.344 1.131 3.056 1.567 3.056 2.083 0 .258-.267.437-.653.437-.683 0-1.157-.417-1.365-1.111-.059-.178-.148-.218-.326-.218l-3.768.139c-.148.02-.237.079-.237.178 0 2.341 1.632 3.71 4.747 4.087 0 1.19 0 1.329.237 1.329h2.136c.119 0 .237-.079.237-.159v-1.151c2.759-.357 4.42-1.706 4.42-3.69 0-1.229-.979-2.339-2.818-3.272z"/>
<path class="st8" d="M20.587 27.816c-1.988-1.012-2.967-1.508-2.967-1.984 0-.139.119-.298.386-.298.237 0 .771.754.979.754l4.065-.139c.207-.02.326-.099.326-.218 0-1.746-1.661-3.254-4.391-3.631 0-1.17 0-1.29-.237-1.29h-2.166c-.119 0-.207.08-.207.179v1.091c-2.759.377-4.243 1.964-4.243 3.472 0 1.429 1.098 2.599 2.789 3.412 2.344 1.131 3.056 1.567 3.056 2.083 0 .258-.267.437-.653.437-.683 0-1.157-.417-1.365-1.111-.059-.178-.148-.218-.326-.218l-3.768.139c-.148.02-.237.079-.237.178 0 2.341 1.632 3.71 4.747 4.087 0 1.19 0 1.329.237 1.329h2.136c.119 0 .237-.079.237-.159V34.78c2.759-.357 4.42-1.706 4.42-3.69 0-1.23-.979-2.341-2.818-3.274z"/>
</g>
<g>
<path class="st13" d="M62.462 54.558c-1.988-1.012-2.967-1.508-2.967-1.984 0-.139.119-.298.386-.298.237 0 .771.754.979.754l4.065-.139c.207-.02.326-.099.326-.218 0-1.746-1.661-3.254-4.391-3.631 0-1.171 0-1.29-.237-1.29h-2.166c-.119 0-.207.079-.207.179v1.091c-2.759.377-4.243 1.964-4.243 3.472 0 1.429 1.098 2.599 2.789 3.412 2.344 1.131 3.056 1.567 3.056 2.083 0 .258-.267.437-.653.437-.683 0-1.157-.417-1.365-1.111-.059-.178-.148-.218-.326-.218l-3.768.139c-.148.02-.237.079-.237.178 0 2.341 1.632 3.71 4.747 4.087 0 1.19 0 1.329.237 1.329h2.136c.119 0 .237-.08.237-.159V61.52c2.759-.357 4.42-1.706 4.42-3.69 0-1.229-.979-2.34-2.818-3.272z"/>
<path class="st8" d="M62.462 53.817c-1.988-1.012-2.967-1.508-2.967-1.984 0-.139.119-.298.386-.298.237 0 .771.754.979.754l4.065-.139c.207-.02.326-.099.326-.218 0-1.746-1.661-3.254-4.391-3.631 0-1.171 0-1.29-.237-1.29h-2.166c-.119 0-.207.079-.207.179v1.091c-2.759.377-4.243 1.964-4.243 3.472 0 1.429 1.098 2.599 2.789 3.412 2.344 1.131 3.056 1.567 3.056 2.083 0 .258-.267.437-.653.437-.683 0-1.157-.417-1.365-1.111-.059-.178-.148-.218-.326-.218l-3.768.139c-.148.02-.237.079-.237.178 0 2.341 1.632 3.71 4.747 4.087 0 1.19 0 1.329.237 1.329h2.136c.119 0 .237-.079.237-.159v-1.151c2.759-.357 4.42-1.706 4.42-3.69 0-1.229-.979-2.339-2.818-3.272z"/>
</g>
<g>
<path class="st13" d="M97.721 66.335c-1.988-1.012-2.967-1.508-2.967-1.984 0-.139.119-.298.386-.298.237 0 .771.754.979.754l4.065-.139c.207-.02.326-.099.326-.218 0-1.746-1.661-3.254-4.391-3.631 0-1.17 0-1.29-.237-1.29h-2.166c-.119 0-.207.08-.207.179v1.091c-2.759.377-4.243 1.964-4.243 3.472 0 1.429 1.098 2.599 2.789 3.412 2.344 1.131 3.056 1.567 3.056 2.083 0 .258-.267.437-.653.437-.683 0-1.157-.417-1.365-1.111-.059-.178-.148-.218-.326-.218l-3.768.139c-.148.02-.237.079-.237.178 0 2.341 1.632 3.71 4.747 4.087 0 1.19 0 1.329.237 1.329h2.136c.119 0 .237-.079.237-.159v-1.151c2.759-.357 4.42-1.706 4.42-3.69 0-1.229-.979-2.34-2.818-3.272z"/>
<path class="st8" d="M97.721 65.594c-1.988-1.012-2.967-1.508-2.967-1.984 0-.139.119-.298.386-.298.237 0 .771.754.979.754l4.065-.139c.207-.02.326-.099.326-.218 0-1.746-1.661-3.254-4.391-3.631 0-1.17 0-1.29-.237-1.29h-2.166c-.119 0-.207.08-.207.179v1.091c-2.759.377-4.243 1.964-4.243 3.472 0 1.429 1.098 2.599 2.789 3.412 2.344 1.131 3.056 1.567 3.056 2.083 0 .258-.267.437-.653.437-.683 0-1.157-.417-1.365-1.111-.059-.178-.148-.218-.326-.218l-3.768.139c-.148.02-.237.079-.237.178 0 2.341 1.632 3.71 4.747 4.087 0 1.19 0 1.329.237 1.329h2.136c.119 0 .237-.079.237-.159v-1.151c2.759-.357 4.42-1.706 4.42-3.69 0-1.229-.979-2.34-2.818-3.272z"/>
</g>
<g>
<path class="st19" d="M27.891 25.262c0 5.135-3.461 9.297-7.73 9.297 4.269 0 7.73 4.162 7.73 9.297 0-5.135 3.461-9.297 7.73-9.297-4.27-.001-7.73-4.163-7.73-9.297z"/>
</g>
<g>
<path class="st19" d="M43.514 63.018c0 5.135-3.461 9.297-7.73 9.297 4.269 0 7.73 4.162 7.73 9.297 0-5.134 3.461-9.297 7.73-9.297-4.269 0-7.73-4.163-7.73-9.297z"/>
</g>
<g>
<path class="st19" d="M102.754 63.72c0 6.218-4.191 11.259-9.361 11.259 5.17 0 9.361 5.041 9.361 11.259 0-6.218 4.191-11.259 9.361-11.259-5.17 0-9.361-5.041-9.361-11.259z"/>
</g>
</svg>
<div class="progress-container">
<div class="progress-bar"></div>
</div>
<p id="loading-text">Loading Finances...</p>
</div>
<div id="toast-msg">Notification</div>
<div id="rate-overlay" onclick="closeRatePopup()">
<div class="rate-card" onclick="event.stopPropagation()">
<h3>Live Exchange Rates</h3>
<p>SGD to Philippine Peso</p>
<div class="rate-row">
<div class="bank-info">
<div class="bank-dot dbs-dot"></div>
<span class="bank-name">DBS Bank</span>
</div>
<div class="bank-val" id="disp-dbs-rate">-- <span>PHP</span></div>
</div>
<div class="rate-row">
<div class="bank-info">
<div class="bank-dot uob-dot"></div>
<span class="bank-name">UOB Bank</span>
</div>
<div class="bank-val" id="disp-uob-rate">-- <span>PHP</span></div>
</div>
<button class="rate-close-btn" onclick="closeRatePopup()">Done</button>
</div>
</div>
<div class="app-container">
<div id="dashboard-view">
<div class="header main-layout animate-slide-in" style="animation-delay: 100ms;">
<div class="header-left">
<div id="main-date-display" class="ios-date-header"></div>
<h1 onclick="checkPin(openNetWorthView)">Dashboard</h1>
<!-- <a id="appLink" onclick="checkPin(togglePrivacy)">Tap to show hidden amounts</a> -->
</div>
<div class="header-right">
<div id="pill-dbs" class="rate-pill" onclick="refreshRate('DBS')">
<div style="display:flex; align-items:center; gap:5px;">
<div class="rp-dot dbs"></div>
<span class="rp-label">DBS</span>
</div>
<span class="rp-val" id="mini-dbs-val">...</span>
</div>
<div id="pill-uob" class="rate-pill" onclick="refreshRate('UOB')">
<div style="display:flex; align-items:center; gap:5px;">
<div class="rp-dot uob"></div>
<span class="rp-label">UOB</span>
</div>
<span class="rp-val" id="mini-uob-val">...</span>
</div>
</div>
</div>
<div class="dashboard-wrapper">
<div class="swipe-wrapper" id="budgetSwipeContainer">
<div class="stacked-card card-front" id="cardBudgetMain" onclick="checkPin(openBudgetView, event)">
<h2>Spending Velocity</h2>
<div class="chart-wrapper">
<div class="chart-container">
<canvas id="chartBudget"></canvas>
<div class="percentage-label" id="pctBudget">--</div>
</div>
</div>
<div class="stats">
<div class="stat-box" style="text-align: left;"><span>Remaining</span><strong id="curBudget">--</strong></div>
<div class="stat-box" style="text-align: center;">
<strong id="spent-month-front" style="color: #3b82f6; font-size: 14px;">--</strong>
</div>
<div class="stat-box" style="text-align: right;"><span>Budget</span><strong id="tgtBudget">--</strong></div>
</div>
</div>
<div class="stacked-card card-back" id="cardBudgetComp" onclick="checkPin(openYOYView, event)">
<h2 style="margin-bottom: 0;">Year over Year</h2>
<div class="comp-split">
<div class="comp-box">
<div class="comp-label">This Month</div>
<div class="comp-val-main" id="cmp-month-cur">--</div>
<div class="comp-val-sub">vs <span id="cmp-month-last">--</span></div>
<div class="comp-pill cp-neutral" id="cmp-month-pct">--%</div>
</div>
<div class="comp-divider"></div>
<div class="comp-box">
<div class="comp-label">YTD Trend</div>
<div class="comp-val-main" id="cmp-ytd-cur">--</div>
<div class="comp-val-sub" style="margin-bottom:-1px; font-size:9px; margin-top:2px;" id="lbl-ytd-cur">--</div>
<div class="hist-container">
<div class="hist-row">
<span class="hist-year" id="lbl-ytd-last">--</span>
<span class="hist-val" id="cmp-ytd-last">--</span>
</div>
<div class="hist-row">
<span class="hist-year" id="lbl-ytd-last2">--</span>
<span class="hist-val" id="cmp-ytd-last2">--</span>
</div>
</div>
</div>
</div>
</div>
<div class="swipe-dots">
<div class="s-dot active" id="dotBudMain"></div>
<div class="s-dot" id="dotBudComp"></div>
</div>
</div>
<div class="swipe-wrapper" id="swipeContainer">
<div class="stacked-card card-front" id="cardSavings" onclick="checkPin(openGoalsView, event)">
<h2 style="font-size: 11.5px;">Savings Goal</h2>
<div class="chart-wrapper">
<div class="chart-container" style="transform: scale(1.05);">
<canvas id="chartSavings"></canvas>
<div class="percentage-label" id="pctSavings">0%</div>
</div>
</div>
<div class="stats" style="width:100%;">
<div class="stat-box" style="text-align: left;"><span>Current</span><strong id="curSavings">*****</strong></div>
<div class="stat-box" style="text-align: right;"><span>Target</span><strong id="tgtSavings">*****</strong></div>
</div>
</div>
<div class="stacked-card card-back" id="cardInvest" onclick="checkPin(openInvestmentView, event)">
<h2>Stocks</h2>
<div class="chart-wrapper">
<div class="chart-container" style="flex-direction:column; justify-content:center; align-items:center;">
<div class="inv-pl-text" id="invMainVal">--</div>
<div style="font-size:10px; color:#94a3b8; font-weight:600; margin-top:5px;">TOTAL P/L</div>
</div>
</div>
<div class="stats" style="width:100%;">
<div class="stat-box" style="text-align: left;"><span>Market Val</span><strong id="invMktVal">*****</strong></div>
<div class="stat-box" style="text-align: right;"><span>Return</span><strong id="invRetPct">--</strong></div>
</div>
</div>
<div class="swipe-dots">
<div class="s-dot active" id="dotSav"></div>
<div class="s-dot" id="dotInv"></div>
</div>
</div>
<div class="card" onclick="checkPin(openRetirementView, event)">
<h2 style="font-size: 11.2px;">Retirement Goal</h2>
<div class="chart-wrapper">
<div class="chart-container">
<canvas id="chartRetire"></canvas>
<div class="percentage-label" id="pctRetire">0%</div>
</div>
</div>
<div class="stats">
<div class="stat-box" style="text-align: left;"><span>Current</span><strong id="curRetire">*****</strong></div>
<div class="stat-box" style="text-align: right;"><span>Target</span><strong id="tgtRetire">*****</strong></div>
</div>
</div>
<div class="card" onclick="checkPin(openEmergencyView, event)">
<h2>Emergency Fund</h2>
<div class="chart-wrapper">
<div class="chart-container">
<canvas id="chartEmergency"></canvas>
<div class="percentage-label" id="pctEmergency">0%</div>
</div>
</div>
<div class="stats">
<div class="stat-box" style="text-align: left;"><span>Current</span><strong id="curEmergency">*****</strong></div>
<div class="stat-box" style="text-align: right;"><span>Target</span><strong id="tgtEmergency">*****</strong></div>
</div>
</div>
</div>
<div class="version-footer">
<div class="footer-icon-btn" id="privacy-btn" onclick="checkPin(togglePrivacy)">
<svg class="footer-icon-svg" viewBox="0 0 24 24" id="privacy-icon">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</div>
<div class="footer-text-wrapper">
<div onclick="toggleFooterId()" style="cursor:pointer; user-select:all;" id="footer-ver-text">v1.4.3</div>
</div>
<div class="footer-icon-btn" onclick="silentRefreshDashboard()">
<svg class="footer-icon-svg footer-refresh-icon" viewBox="0 0 24 24">
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
<path d="M3 3v5h5"></path>
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path>
<path d="M16 21h5v-5"></path>
</svg>
</div>
</div>
</div>
<div id="emergency-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Safety Net</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Coverage Analysis</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card" style="margin-bottom: 15px; text-align:center; cursor: pointer;" onclick="checkPin(openExpenseBreakdown, event)">
<div class="pred-label">Monthly Requirement</div>
<div class="amount" id="emg-exp-req" style="font-size:32px; font-weight:800; color:#1e293b; margin: 10px 0;">--</div>
<div id="emg-status-box" style="background:#f0fdf4; border-radius:12px; padding:12px; margin-top:10px; border:1px solid #dcfce7;">
<div style="font-size:12px; font-weight:700; margin-bottom:4px;" id="emg-status-title">--</div>
<div style="font-size:11px; color:#475569;" id="emg-status-desc">--</div>
</div>
</div>
<div class="detail-card" style="text-align:center; margin-bottom: 15px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
<span class="pred-label" style="text-align:left;">PHP Strategic Fund</span>
<span style="font-size:10px; font-weight:700; color:#94a3b8;">TARGET: ₱ 1M</span>
</div>
<div class="amount" id="emg-php-actual" style="font-size:24px; font-weight:700; color:#1e293b; text-align:left;">--</div>
<div style="width:100%; height:8px; background:#f1f5f9; border-radius:4px; margin: 10px 0 5px 0; overflow:hidden;">
<div id="emg-php-bar" style="height:100%; width:0%; background:#3b82f6; border-radius:4px; transition:width 1s;"></div>
</div>
<div style="font-size:10px; color:#64748b; text-align:left; margin-top:5px;">Source: SEABank Account</div>
</div>
<div class="detail-card" style="text-align:center; background:#1e293b; border:1px solid #0f172a;">
<div class="pred-label" style="color:#94a3b8;">Total Survival Time</div>
<div class="amount" id="emg-total-months" style="font-size:36px; font-weight:800; color:#ffffff; margin: 10px 0; line-height:1;">--</div>
<div style="font-size:11px; color:#cbd5e1; font-weight:500;">Combined Operational + Strategic Funds</div>
</div>
</div>
<div class="fixed-bottom-section">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('emergency-view')">Back to Dashboard</button>
</div>
</div>
<div id="expense-breakdown-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Monthly Expenses</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Recurring Costs</p>
</div>
<div class="fixed-top-section">
<div class="seg-control">
<div id="tab-sg" class="seg-btn active" onclick="switchExpenseTab('SG')">Singapore (SGD)</div>
<div id="tab-ph" class="seg-btn" onclick="switchExpenseTab('PH')">Philippines (PHP)</div>
</div>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="budget-list-card" id="exp-breakdown-list"></div>
</div>
<div class="fixed-bottom-section">
<div class="nw-total-card" style="padding: 15px; background: #1e293b; color: white;">
<h2 style="color: #94a3b8;">Total Monthly Cost</h2>
<div class="amount" id="exp-breakdown-total" style="font-size: 28px; color: white; line-height:1.1;">--</div>
<div id="exp-breakdown-converted" style="font-size: 13px; color: #94a3b8; margin-top: 4px; font-weight: 600; opacity: 0.8;">--</div>
</div>
<button type="button" class="btn btn-secondary" style="margin-top:10px;" onclick="closeOverlay('expense-breakdown-view'); document.getElementById('emergency-view').style.display='flex';">Back to Safety Net</button>
</div>
</div>
<div id="goals-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Savings Goals</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Timeline & Projections</p>
</div>
<div class="fixed-top-section">
<div class="info-grid">
<div class="info-box">
<span>Current Savings</span>
<strong id="goal-total-pool" style="color:#1e293b;">--</strong>
</div>
<div class="info-box" style="text-align: right;">
<span>Projected Inflow</span>
<strong id="goal-monthly-rate" style="color:#10b981; display:block; margin-bottom:2px; line-height:1.1;">--</strong>
<span id="goal-yearly-rate" style="font-size:10px; color:#059669; font-weight:600; display:block;">--</span>
</div>
</div>
</div>
<div class="scrollable-middle animate-slide-in">
<div id="goals-list-container"></div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('goals-view')">Back</button>
<button type="button" class="btn btn-primary" onclick="openAddGoalView()">Add Goal</button>
</div>
</div>
</div>
<div id="networth-view">
<div class="nw-total-card animate-slide-in">
<h2>Total Net Worth</h2>
<div class="amount" id="networth-val">--</div>
</div>
<div class="nw-list-card animate-slide-in">
<div class="nw-list-header">Asset Breakdown</div>
<div class="nw-scroll-area" id="networth-list-container"></div>
</div>
<button type="button" class="btn btn-secondary" style="flex-shrink:0;" onclick="closeOverlay('networth-view')">Back to Dashboard</button>
</div>
<div id="networth-detail-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1 id="nw-detail-title">Asset Details</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;" id="nw-detail-subtitle">Breakdown</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="budget-list-card" id="nw-detail-container"></div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('networth-detail-view')">Back</button>
<button type="button" id="btn-add-asset" class="btn btn-primary" onclick="openAddAssetView()">Add Asset</button>
</div>
</div>
</div>
<div id="networth-edit-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Edit Asset</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Update Value</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card">
<div class="info-grid" style="grid-template-columns: 1fr;">
<div class="info-box"><span>Class</span><strong id="nwe-class">--</strong></div>
<div class="info-box" style="margin-top:10px;"><span>Category</span><strong id="nwe-category">--</strong></div>
<div class="info-box" style="margin-top:10px;"><span>Account</span><strong id="nwe-account">--</strong></div>
<div class="info-box" style="margin-top:10px;"><span>Currency</span><strong id="nwe-currency" style="color:#3b82f6;">--</strong></div>
</div>
<form id="netWorthEditForm">
<input type="hidden" id="nwe-row">
<input type="hidden" id="nwe-currency-hidden">
<div class="form-group">
<label class="form-label">Amount</label>
<input type="text" inputmode="decimal" class="form-input" id="nwe-amount"
onfocus="onAmountFocus(this)"
onblur="onAmountBlur(this)"
oninput="onAmountInput(this)"
placeholder="0.00">
</div>
</form>
<div id="nwe-sgd-remark" style="display:none; margin-top:20px; padding:12px; background:#f0fdf4; border:1px solid #bbf7d0; border-radius:8px; font-size:12px; color:#15803d; line-height:1.4;">
<strong>Note:</strong> Conversion to PHP is automatic. Please supply the amount in <strong>SGD</strong>.
</div>
</div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('networth-edit-view')">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveNetWorthItem()">Save</button>
</div>
</div>
</div>
<div id="networth-add-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Add New Asset</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Create Entry</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card">
<div class="info-grid">
<div class="info-box"><span>Group</span><strong id="nwa-class">--</strong></div>
<div class="info-box"><span>Category</span><strong id="nwa-category">--</strong></div>
</div>
<form id="netWorthAddForm">
<input type="hidden" id="nwa-class-hidden">
<input type="hidden" id="nwa-category-hidden">
<div class="form-group">
<label class="form-label">Account Name</label>
<input type="text" class="form-input" id="nwa-account" placeholder="e.g. DBS Multiplier">
</div>
<div class="form-group">
<label class="form-label">Currency</label>
<select class="form-input" id="nwa-currency" style="background-image:none;">
<option value="SGD">SGD</option>
<option value="PHP">PHP</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Amount</label>
<input type="text" inputmode="decimal" class="form-input" id="nwa-amount"
onfocus="onAmountFocus(this)"
onblur="onAmountBlur(this)"
oninput="onAmountInput(this)"
placeholder="0.00">
</div>
</form>
<div style="margin-top:20px; padding:12px; background:#f0fdf4; border:1px solid #bbf7d0; border-radius:8px; font-size:12px; color:#15803d; line-height:1.4;">
<strong>Note:</strong> Currency conversion formulas will be applied automatically in the spreadsheet.
</div>
</div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('networth-add-view')">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveNewAsset()">Create Asset</button>
</div>
</div>
</div>
<div id="budget-detail-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Budget Details</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Current Breakdown</p>
</div>
<div class="fixed-top-section animate-slide-in">
<div class="prediction-box" style="margin-bottom: 10px;">
<div class="pred-label">Total Spent This Month</div>
<div class="pred-amount" id="budget-total-spent-sum" style="color: #1e293b;">$ 0.00</div>
</div>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="budget-list-card" id="budget-breakdown-container"></div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('budget-detail-view')">Back</button>
<button type="button" class="btn btn-primary" onclick="openAddSpend()">Add Spend</button>
</div>
</div>
</div>
<div id="transaction-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1 id="trans-header-title">Transactions</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">SPENDS BREAKDOWN</p>
</div>
<div class="fixed-top-section">
<div class="prediction-box" onclick="toggleTransactionSort()" style="cursor: pointer;">
<div class="pred-label">Total Spent</div>
<div class="pred-amount" id="trans-total-amount">--</div>
<div style="font-size:9px; color:#3b82f6; margin-top:6px; font-weight:600; text-transform:uppercase; letter-spacing:0.5px;" id="trans-sort-label">Sorted by Date</div>
</div>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="budget-list-card" id="transactions-container"></div>
</div>
<div class="fixed-bottom-section">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('transaction-view')">Back to Budget</button>
</div>
</div>
<div id="edit-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Edit Allocations</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Update Budget Limits</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card">
<form id="budgetForm">
<div id="form-items-container"></div>
</form>
</div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('edit-view'); document.getElementById('budget-detail-view').style.display='flex';">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveData()">Save</button>
</div>
</div>
</div>
<div id="yoy-view" onclick="dismissChartTooltip(event)">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Trend Expedition</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">3-Year Monthly Comparison</p>
</div>
<div class="scrollable-middle" style="display:flex; flex-direction:column; justify-content:center;">
<div class="detail-card" style="height: 350px; position:relative;">
<canvas id="chartYOY"></canvas>
</div>
<div class="info-grid" style="margin-top:20px;">
<div class="info-box">
<span id="lbl-leg-1">--</span>
<strong id="val-leg-1" style="color:#3b82f6">Current</strong>
</div>
<div class="info-box">
<span id="lbl-leg-2">--</span>
<strong id="val-leg-2" style="color:#94a3b8">Last Year</strong>
</div>
<div class="info-box" style="grid-column: span 2; border-top:1px solid #e2e8f0; padding-top:10px; margin-top:5px;">
<span>Analysis Verdict</span>
<strong id="yoy-verdict" style="font-size:14px; color:#1e293b;">--</strong>
</div>
</div>
</div>
<div class="fixed-bottom-section">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('yoy-view')">Back to Dashboard</button>
</div>
</div>
<div id="add-spend-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Add Spend</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">New Transaction</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card">
<form id="addSpendForm">
<div class="form-group">
<label class="form-label">Transaction Date</label>
<input type="date" class="form-input" id="spend-date">
</div>
<div class="form-group">
<label class="form-label">Posting Date</label>
<input type="text" class="form-input" value="PENDING" disabled>
</div>
<div class="form-group">
<label class="form-label">Description</label>
<input type="text" class="form-input" id="spend-desc" placeholder="e.g., Starbucks">
</div>
<div class="form-group">
<label class="form-label">Currency</label>
<input type="text" class="form-input" value="SGD" disabled>
</div>
<div class="form-group">
<label class="form-label">Amount</label>
<input type="number" step="0.01" class="form-input" id="spend-amt" placeholder="0.00">
</div>
</form>
</div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('add-spend-view')">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveSpend()">Save</button>
</div>
</div>
</div>
<div id="add-goal-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>New Goal</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Define Target</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card">
<form id="addGoalForm">
<div class="form-group">
<label class="form-label">Goal Name</label>
<input type="text" class="form-input" id="goal-name" placeholder="e.g. Rolex Datejust">
</div>
<div class="form-group">
<label class="form-label">Target Amount (PHP)</label>
<input type="text" inputmode="decimal" class="form-input" id="goal-amt"
onfocus="onAmountFocus(this)"
onblur="onAmountBlur(this)"
oninput="onAmountInput(this)"
placeholder="0.00">
</div>
<div class="form-group">
<label class="form-label">Target Date</label>
<input type="date" class="form-input" id="goal-date">
</div>
</form>
</div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('add-goal-view')">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveNewGoal()">Save Goal</button>
</div>
</div>
</div>
<div id="asset-edit-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1 id="asset-edit-title">Edit Asset</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Update Holdings</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card">
<div class="info-grid">
<div class="info-box"><span>Current Price</span><strong id="disp-cur">--</strong></div>
<div class="info-box"><span>Market Value</span><strong id="disp-mkt">--</strong></div>
<div class="info-box"><span>Profit / Loss</span><strong id="disp-prof">--</strong></div>
<div class="info-box"><span>Return</span><strong id="disp-pct" style="color:#3b82f6;">--</strong></div>
</div>
<form id="assetForm">
<input type="hidden" id="asset-row-index">
<div class="form-group">
<label class="form-label">Average Price</label>
<input type="number" step="0.01" class="form-input" id="asset-avg-price">
</div>
<div class="form-group">
<label class="form-label">Units</label>
<input type="number" step="0.0001" class="form-input" id="asset-units">
</div>
</form>
</div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('asset-edit-view')">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveAssetData()">Save</button>
</div>
</div>
</div>
<div id="retirement-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Retirement Plan</h1>
<p style="margin:2px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Strategy & Allocation</p>
</div>
<div class="fixed-top-section">
<div class="seg-control" style="margin-bottom: 15px;">
<div id="tab-ret-plan" class="seg-btn active" onclick="switchRetirementTab('PLAN')">Plan Details</div>
<div id="tab-ret-assets" class="seg-btn" onclick="switchRetirementTab('ASSETS')">Portfolio</div>
</div>
</div>
<div class="scrollable-middle animate-slide-in">
<div id="ret-tab-plan-content">
<div class="nw-total-card" style="padding: 8px 16px; margin-bottom: 6px; display:flex; justify-content:space-between; align-items:center; text-align:left; min-height: 70px;">
<div>
<div style="font-size:14px; color:#94a3b8; text-transform:uppercase; font-weight:600;">Projected Status</div>
<div class="amount" id="ret-status-val" style="font-size: 30px; margin: 2px 0; line-height:1.1;">--</div>
<div id="ret-status-lbl" style="font-size:14px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px;">--</div>
</div>
<div style="text-align:right;">
<span class="view-remarks-link" onclick="openRemarksView()" style="background:#f1f5f9; padding:6px 10px; border-radius:20px; color:#64748b; font-size:12px;">Analysis &rarr;</span>
</div>
</div>
<div class="alloc-group-card" style="padding: 12px; margin-bottom: 0; margin-top:20px;">
<form id="retirementPlanForm">
<div style="background:#f8fafc; border-radius:12px; padding:10px 12px; border:1px solid #f1f5f9; margin-bottom:10px;">
<div style="font-size:12px; color:#94a3b8; font-weight:700; margin-bottom:8px; text-transform:uppercase;">Timeline</div>
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:10px; align-items: start;">
<div style="display:flex; flex-direction:column;">
<label class="form-label" style="font-size:10px; margin-bottom:3px; white-space:nowrap; height:14px; overflow:hidden; display:block;">Retire Year</label>
<input type="number" inputmode="numeric" class="form-input" id="ret-year" style="font-size:15px; font-weight:600; text-align:center; background:white; border:1px solid #e2e8f0; height:36px; padding:0 8px; line-height:34px; margin:0; width:100%;">
</div>
<div style="display:flex; flex-direction:column;">
<label class="form-label" style="font-size:10px; margin-bottom:3px; white-space:nowrap; height:14px; overflow:hidden; display:block;">Start Age</label>
<input type="number" disabled class="form-input" id="ret-age" style="font-size:15px; font-weight:600; text-align:center; background:#f1f5f9; color:#94a3b8; border:1px solid #e2e8f0; height:36px; padding:0 8px; line-height:34px; margin:0; width:100%;">
</div>
<div style="display:flex; flex-direction:column;">
<label class="form-label" style="font-size:10px; margin-bottom:3px; white-space:nowrap; height:14px; overflow:hidden; display:block;">End Age</label>
<input type="number" inputmode="numeric" class="form-input" id="ret-age-until" style="font-size:15px; font-weight:600; text-align:center; background:white; border:1px solid #e2e8f0; height:36px; padding:0 8px; line-height:34px; margin:0; width:100%;">
</div>
</div>
</div>
<div style="background:#f8fafc; border-radius:12px; padding:10px 12px; border:1px solid #f1f5f9; margin-bottom:10px;">
<div style="font-size:12px; color:#94a3b8; font-weight:700; margin-bottom:6px; text-transform:uppercase;">Variables</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px; margin-bottom:10px;">
<div>
<label class="form-label" style="font-size:10px;">Inflation (%)</label>
<input type="number" inputmode="decimal" step="0.01" class="form-input" id="ret-inflation" style="padding:6px; font-size:15px; color:#ef4444; font-weight:600; display: block; width: 100%; box-sizing: border-box;">
</div>
<div>
<label class="form-label" style="font-size:10px;">Inv. Return (%)</label>
<input type="number" inputmode="decimal" step="0.01" class="form-input" id="ret-roi" style="padding:6px; font-size:15px; color:#10b981; font-weight:600; display: block; width: 100%; box-sizing: border-box;">
</div>
</div>
</div>
<div style="background:#f8fafc; border-radius:12px; padding:10px 12px; border:1px solid #f1f5f9;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; gap: 10px;">
<div style="flex: 1; min-width: 0;">
<span style="font-size:10px; color:#94a3b8; text-transform:uppercase; display:block;">Current Monthly Exp.</span>
<div class="editable-field-link" onclick="openRetExpenseDetails()"
style="cursor: pointer; display: inline-flex; align-items: center; background: #f1f5f9; padding: 2px 8px; border-radius: 6px; margin-top:2px;">
<input type="text" id="ret-exp-now" readonly
style="background:transparent; border:none; padding:0; font-size:15px; font-weight:700; color:#334155; width: 80px; cursor: pointer; outline: none;">
<span style="color: #3b82f6; font-size: 12px; margin-left: 4px;">&rarr;</span>
</div>
</div>
<div style="flex: 1; text-align:right; min-width: 0;">
<span style="font-size:10px; color:#94a3b8; text-transform:uppercase; display:block; white-space: nowrap;">Future Monthly Exp.</span>
<strong id="ret-exp-future" style="font-size:15px; color:#334155; display: block; margin-top: 4px;">--</strong>
</div>
</div>
<div style="border-top:1px solid #e2e8f0; padding-top:6px; margin-top: 4px; display:flex; justify-content:space-between; align-items:center;">
<span style="font-size:12px; color:#64748b; font-weight:600;">Total Corpus Required</span>
<strong id="ret-target-corpus" style="font-size:15px; color:#1e293b;">--</strong>
</div>
</div>
</form>
</div>
</div>
<div id="ret-tab-assets-content" style="display:none;">
<div class="assets-card" id="assets-container"></div>
</div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('retirement-view')">Back</button>
<button type="button" id="btn-ret-update" class="btn btn-primary" onclick="saveRetirementPlan()">Update Strategy</button>
</div>
</div>
</div>
<div id="ret-expense-detail-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Monthly Expenses</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Retirement Requirements</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="budget-list-card" id="ret-exp-list-container"></div>
</div>
<div class="fixed-bottom-section">
<div class="nw-total-card" style="padding: 12px; background: #1e293b; color: white; margin-bottom: 15px; border-radius: 12px;">
<h2 style="color: #94a3b8; font-size: 9px; margin-bottom: 2px;">Total Breakdown Sum</h2>
<div class="amount" id="ret-exp-running-total" style="font-size: 24px; color: white; line-height:1.1;">₱ 0</div>
</div>
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeRetExpenseDetails()">Back</button>
<button type="button" class="btn btn-primary" onclick="openAddRetExpense()">Add Item</button>
</div>
</div>
</div>
<div id="ret-expense-edit-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1 id="ret-exp-edit-title">Add Expense</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Update Details</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card">
<div id="re-info-grid" class="info-grid" style="grid-template-columns: 1fr; margin-bottom: 15px; display: none;">
<div class="info-box"><span>Category</span><strong id="re-disp-cat">--</strong></div>
<div class="info-box" style="margin-top:10px;"><span>Sub-Category</span><strong id="re-disp-sub">--</strong></div>
</div>
<form id="retExpForm">
<input type="hidden" id="re-row">
<div class="form-group" id="re-cat-group">
<label class="form-label">Category</label>
<select class="form-input" id="re-category"></select>
</div>
<div class="form-group" id="re-sub-group">
<label class="form-label">Sub-Category</label>
<input type="text" class="form-input" id="re-subcategory" placeholder="e.g. Electricity">
</div>
<div class="form-group">
<label class="form-label">Amount (PHP)</label>
<input type="text" inputmode="decimal" class="form-input" id="re-amount"
onfocus="onAmountFocus(this)" onblur="onAmountBlur(this)" oninput="onAmountInput(this)"
placeholder="0.00">
</div>
</form>
</div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('ret-expense-edit-view')">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveRetExpense()">Save</button>
</div>
</div>
</div>
<div id="investment-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Stocks</h1>
<p style="margin:2px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Active Positions</p>
</div>
<div class="fixed-top-section">
<div class="prediction-box">
<div class="pred-label">Total Market Value</div>
<div class="pred-amount" id="invTotalHeader">--</div>
</div>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="assets-card" id="inv-assets-container"></div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('investment-view')">Back</button>
<button type="button" class="btn btn-primary" onclick="openAddStockView()">Add Stock</button>
</div>
</div>
</div>
<div id="remarks-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Analysis</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">Projected Outcome Details</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card">
<div class="remarks-text" id="full-remarks-text">Loading...</div>
</div>
</div>
<div class="fixed-bottom-section">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('remarks-view')">Close</button>
</div>
</div>
<div id="add-stock-view">
<div class="header animate-slide-in" style="animation-delay: 100ms;">
<h1>Add Position</h1>
<p style="margin:5px 0 0 0; font-size:11px; color:#94a3b8; text-transform:uppercase;">New Asset Details</p>
</div>
<div class="scrollable-middle animate-slide-in">
<div class="detail-card">
<form id="addStockForm">
<div class="form-group">
<label class="form-label">Portfolio Strategy</label>
<div class="seg-control" style="margin-bottom:15px;">
<div id="type-day" class="seg-btn active" onclick="setStockType('Day Trading')">Trading</div>
<div id="type-ret" class="seg-btn" onclick="setStockType('Retirement')">Retirement</div>
</div>
<input type="hidden" id="stock-type" value="Day Trading">
</div>
<div class="form-group">
<label class="form-label">Ticker (Symbol)</label>
<input type="text" class="form-input" id="stock-ticker" placeholder="e.g. AAPL" style="text-transform:uppercase; font-weight:700;">
</div>
<div class="info-grid">
<div>
<label class="form-label">Stock Type</label>
<select class="form-input" id="stock-c" style="background-image:none;">
<option value="ETF">ETF</option>
<option value="STOCK">STOCK</option>
<option value="CRYPTO">CRYPTO</option>
</select>
</div>
<div>
<label class="form-label">Broker</label>
<input type="text" class="form-input" id="stock-d" placeholder="e.g. IBKR">
</div>
</div>
<div class="info-grid" style="margin-top:15px;">
<div>
<label class="form-label">Currency</label>
<input type="text" class="form-input" id="stock-g" placeholder="e.g. USD" style="text-transform:uppercase;">
</div>
<div>
<label class="form-label">Inv. Amt (SGD)</label>
<input type="number" step="0.01" class="form-input" id="stock-f" placeholder="0.00">
</div>
</div>
<div style="margin-top:15px;">
<div class="form-group">
<label class="form-label">Avg Price</label>
<input type="number" step="0.01" class="form-input" id="stock-price" placeholder="0.00">
</div>
<div class="form-group">
<label class="form-label">Units</label>
<input type="number" step="0.0001" class="form-input" id="stock-units" placeholder="0">
</div>
</div>
<div class="form-group">
<label class="form-label">Description (Optional)</label>
<input type="text" class="form-input" id="stock-desc" placeholder="Notes...">
</div>
</form>
</div>
</div>
<div class="fixed-bottom-section">
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="closeOverlay('add-stock-view')">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveNewStock()">Add Asset</button>
</div>
</div>
</div>
</div>
<div id="error-msg" style="text-align:center; color:red; margin-top:20px;"></div>
<script>
let globalData = null;
let activeSwipe = null;
let g_currentDetailAccount = "";
let g_currentDetailCategory = "";
let currentRemarks = "";
let isVisible = false;
let invDataGlobal = null;
let isEditingInvestment = false;
let isPinAuthenticated = false;
let pendingCallback = null;
let pendingEvent = null;
let currentPin = "";
const CORRECT_PIN = "1221";
let trustedDevicesFromServer = [];
function getDeviceId() {
const key = "my_app_device_id";
function getCookie(name) {
let v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
return v ? v[2] : null;
}
function setCookie(name, value) {
let d = new Date();
d.setTime(d.getTime() + (3650 * 24 * 60 * 60 * 1000));
document.cookie = name + "=" + value + ";path=/;SameSite=Lax;expires=" + d.toGMTString();
}
let id = localStorage.getItem(key);
if (!id) {
id = getCookie(key);
if (id) localStorage.setItem(key, id);
}
if (!id) {
id = 'dev_' + Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
localStorage.setItem(key, id);
setCookie(key, id);
} else {
setCookie(key, id);
}
return id;
}
const myDeviceId = getDeviceId();
function checkPin(callback, e) {
if (e) e.stopPropagation();
if (isPinAuthenticated) {
if (callback) callback(e);
} else {
pendingCallback = callback;
pendingEvent = e;
openPinPad();
}
}
function openPinPad() {
document.getElementById('pin-overlay').style.display = 'flex';
currentPin = "";
updateDots();
document.getElementById('pin-error').innerText = "";
}
function closePinPad() {
document.getElementById('pin-overlay').style.display = 'none';
currentPin = "";
updateDots();
}
function pressPin(num) {
if (currentPin.length < 4) {
currentPin += num;
updateDots();
if (currentPin.length === 4) {
setTimeout(() => {
if (currentPin === CORRECT_PIN) {
isPinAuthenticated = true;
closePinPad();
if (pendingCallback) pendingCallback(pendingEvent);
} else {
document.getElementById('pin-error').innerText = "Incorrect PIN";
if (navigator.vibrate) navigator.vibrate(200);
setTimeout(() => {
currentPin = "";
updateDots();
document.getElementById('pin-error').innerText = "";
}, 800);
}
}, 20);
}
}
}
function updateDots() {
for(let i=1; i<=4; i++) {
const dot = document.getElementById('dot-' + i);
if (i <= currentPin.length) dot.classList.add('active');
else dot.classList.remove('active');
}
}
let touchStartX = 0;
let touchEndX = 0;
const swipeContainer = document.getElementById('swipeContainer');
const cardSav = document.getElementById('cardSavings');
const cardInv = document.getElementById('cardInvest');
let activeCard = 'savings';
swipeContainer.addEventListener('touchstart', e => {
touchStartX = e.changedTouches[0].screenX;
}, {passive: true});
swipeContainer.addEventListener('touchend', e => {
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
}, {passive: true});
swipeContainer.addEventListener('mousedown', e => { touchStartX = e.screenX; });
swipeContainer.addEventListener('mouseup', e => { touchEndX = e.screenX; handleSwipe(); });
function handleSwipe() {
const threshold = 50;
if (touchEndX < touchStartX - threshold && activeCard === 'savings') {
showInvestmentCard();
} else if (touchEndX > touchStartX + threshold && activeCard === 'invest') {
showSavingsCard();
}
}
function showInvestmentCard() {
activeCard = 'invest';
cardSav.className = 'stacked-card card-left';
cardInv.className = 'stacked-card card-front';
cardInv.style.zIndex = "";
cardSav.style.zIndex = "5";
setTimeout(() => {
if(activeCard === 'invest') cardSav.className = 'stacked-card card-back';
}, 300);
document.getElementById('dotSav').classList.remove('active');
document.getElementById('dotInv').classList.add('active');
}
function showSavingsCard() {
activeCard = 'savings';
cardInv.className = 'stacked-card card-right';
cardSav.className = 'stacked-card card-front';
cardSav.style.zIndex = "";
cardInv.style.zIndex = "5";
setTimeout(() => {
if(activeCard === 'savings') cardInv.className = 'stacked-card card-back';
}, 300);
document.getElementById('dotInv').classList.remove('active');
document.getElementById('dotSav').classList.add('active');
}
window.onload = function() {
try {
updateCurrentDate();
initMiniRates();
loadDashboard();
} catch (e) {
console.error(e);
document.getElementById('loading-overlay').style.display = 'none';
alert("Startup Error: " + e.message);
}
};
let userDeviceName = "Unknown";
function loadDashboard() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading Finances...";
window.isFooterToggled = false;
renderFooter(); // Call a helper to draw the footer
google.script.run.withSuccessHandler(initDashboard).withFailureHandler(showError).db_getFinancialData();
}
function renderFooter() {
const textEl = document.getElementById('footer-ver-text');
if(!textEl) return;
const versionText = "v1.4.2";
const deviceText = myDeviceId;
textEl.innerText = window.isFooterToggled ? deviceText : versionText;
textEl.style.color = isPinAuthenticated ? "#10b981" : "#cbd5e1";
}
function toggleFooterId() {
if (isPinAuthenticated) {
window.isFooterToggled = !window.isFooterToggled;
renderFooter();
if (navigator.vibrate) navigator.vibrate(10);
} else {
checkPin(registerCurrentDevice);
}
}
function registerCurrentDevice() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Registering Device...";
google.script.run.withSuccessHandler(function(response) {
document.getElementById('loading-overlay').style.display = 'none';
if (response.success) {
showToast("Device Trusted Successfully");
isPinAuthenticated = true;
window.isFooterToggled = true;
renderFooter();
} else {
showError(response.error);
}
}).db_trustDevice(myDeviceId);
}
function initDashboard(data) {
if (!data.success) { showError(data.error); return; }
globalData = data;
invDataGlobal = data.investment;
if (data.trustedDevices) {
trustedDevicesFromServer = data.trustedDevices;
if (trustedDevicesFromServer.indexOf(myDeviceId) > -1) {
isPinAuthenticated = true;
renderFooter();
}
}
document.getElementById('loading-overlay').style.display = 'none';
setTimeout(() => {
let charts = {};
drawChart('chartBudget', data.budget.current, data.budget.target, true);
drawChart('chartRetire', data.retirement.current, data.retirement.target, false, '#D4AF37');
drawChart('chartEmergency', data.emergency.current, data.emergency.target, false, '#F59E0B');
drawSegmentedChart('chartSavings', data.savings.segments, data.savings.colors);
document.getElementById('pctSavings').innerHTML = data.savings.activePct + '<span class="small-pct">%</span>';
const statBox = document.getElementById('curSavings').parentNode;
statBox.querySelector('span').innerText = "Next Goal";
document.getElementById('curSavings').innerText = data.savings.activeName;
}, 100);
updateTextDisplay();
}
// --- NET WORTH ---
function openNetWorthView() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Calculating...";
google.script.run.withSuccessHandler(renderNetWorth).withFailureHandler(showError).db_getNetWorthDetails();
}
function renderNetWorth(data) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('networth-view').style.display = 'flex';
playPageAnimation('networth-view');
const fmt = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
document.getElementById('networth-val').innerText = fmt(data.total);
const container = document.getElementById('networth-list-container');
container.innerHTML = '';
if(data.items.length === 0) {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#94a3b8; font-size:12px;">No assets found.</div>';
} else {
data.items.forEach(item => {
let valClass = item.isDebt ? "nw-val nw-debt" : "nw-val";
const div = document.createElement('div');
div.className = 'nw-item';
div.onclick = function() { openNetWorthDetail(item.account, item.category); };
div.innerHTML = `<div class="nw-info"><span class="nw-acct">${item.account}</span><span class="nw-cat">${item.category}</span></div><div class="${valClass}">${fmt(item.amount)}</div>`;
container.appendChild(div);
});
}
}
function openNetWorthDetail(account, category) {
g_currentDetailAccount = account;
g_currentDetailCategory = category;
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading Details...";
document.getElementById('nw-detail-title').innerText = account;
document.getElementById('nw-detail-subtitle').innerText = category;
var groupName = (category || "").toLowerCase();
var btnAdd = document.getElementById('btn-add-asset');
var isLiability = /liabilities|liability|debt|loan|credit|mortgage/i.test(groupName);
var isInvestment = (groupName === "investment" || groupName === "investments");
if (isLiability || isInvestment) {
btnAdd.style.display = 'none';
} else {
btnAdd.style.display = 'block';
}
google.script.run.withSuccessHandler(renderNetWorthDetail).withFailureHandler(showError).db_getNetWorthItemDetails(account, category);
}
function renderNetWorthDetail(items, keepLoader = false) {
// UPDATED: Only hide loader if we are NOT in a chain (like delete/add)
if (!keepLoader) {
document.getElementById('loading-overlay').style.display = 'none';
}
document.getElementById('networth-detail-view').style.display = 'flex';
const container = document.getElementById('nw-detail-container');
container.innerHTML = '';
const fmt = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
if (items.length === 0) {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#94a3b8; font-size:13px;">No details found.</div>';
} else {
items.forEach(item => {
var cVal = (item.classVal || "").toLowerCase();
var isRestricted = (cVal === "investment" || cVal === "investments" || /liability|liabilities/i.test(cVal));
const rowHTML = `
<div class="b-cat" style="flex-grow:1; width:auto; min-width:0; padding-right:10px;">
<span style="display:block; font-size:14px; font-weight:600; color:#1e293b; overflow:hidden; text-overflow:ellipsis;">${item.accountVal}</span>
<span style="display:block; font-size:11px; color:#94a3b8; margin-top:4px;">${item.classVal} • ${item.categoryVal}</span>
</div>
<div class="b-data-col" style="text-align:right; width:auto; flex-shrink:0;">
<span class="b-val" style="white-space:nowrap; font-size:14px;">${fmt(item.amount)}</span>
</div>`;
if (isRestricted) {
const staticRow = document.createElement('div');
staticRow.className = 'budget-row animate-slide-in'; // Ensure animation class is here
staticRow.style.animationDelay = `${items.indexOf(item) * 60}ms`;
staticRow.innerHTML = rowHTML;
staticRow.onclick = function() { showToastMiddle("This information is not editable."); };
container.appendChild(staticRow);
} else {
const wrapper = document.createElement('div');
wrapper.className = 'double-action-wrapper animate-slide-in'; // Ensure animation class is here
wrapper.style.animationDelay = `${items.indexOf(item) * 60}ms`;
const actions = document.createElement('div');
actions.className = 'double-actions';
const btnEdit = document.createElement('div');
btnEdit.className = 'action-btn ab-edit';
btnEdit.innerHTML = `<svg class="edit-icon-svg" viewBox="0 0 24 24" style="width:20px; height:20px; fill:white; margin-bottom:2px;"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg><span class="edit-text">Edit</span>`;
btnEdit.onclick = function() { openNetWorthEdit(item); wrapper.querySelector('.swipe-content-row').style.transform = `translateX(0)`; };
const btnDel = document.createElement('div');
btnDel.className = 'action-btn ab-del';
btnDel.innerHTML = `<svg class="trash-icon" viewBox="0 0 24 24" style="width:20px; height:20px; fill:white; margin-bottom:2px;"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg><span class="edit-text">Delete</span>`;
btnDel.onclick = function() { deleteNetWorthItem(item.row, item.accountVal); };
actions.appendChild(btnEdit);
actions.appendChild(btnDel);
const content = document.createElement('div');
content.className = 'swipe-content-row';
content.innerHTML = `<div class="budget-row" style="border-bottom:none;">${rowHTML}</div>`;
attachSwipe(content, -140);
wrapper.appendChild(actions);
wrapper.appendChild(content);
container.appendChild(wrapper);
}
});
}
}
// --- HELPER FOR MIDDLE TOAST ---
function showToastMiddle(text) {
var x = document.getElementById("toast-msg");
x.innerText = text;
// Remove standard show class if present to avoid conflict
x.classList.remove("show");
x.classList.add("show-middle");
// Vibrate for feedback if supported
if (navigator.vibrate) navigator.vibrate(50);
setTimeout(function(){
x.classList.remove("show-middle");
}, 2000);
}
function openNetWorthEdit(item) {
document.getElementById('networth-edit-view').style.display = 'flex';
document.getElementById('nwe-class').innerText = item.classVal;
document.getElementById('nwe-category').innerText = item.categoryVal;
document.getElementById('nwe-account').innerText = item.accountVal;
document.getElementById('nwe-currency').innerText = item.currency;
document.getElementById('nwe-row').value = item.row;
document.getElementById('nwe-currency-hidden').value = item.currency;
document.getElementById('nwe-amount').value = formatNumberWithCommas(item.editVal);
var remarkBox = document.getElementById('nwe-sgd-remark');
if (item.currency === 'SGD') {
remarkBox.style.display = 'block';
} else {
remarkBox.style.display = 'none';
}
}
function saveNetWorthItem() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Saving Update...";
var cleanAmt = document.getElementById('nwe-amount').value.replace(/,/g, '');
const form = {
row: document.getElementById('nwe-row').value,
currency: document.getElementById('nwe-currency-hidden').value,
amount: cleanAmt
};
google.script.run.withSuccessHandler(onNetWorthSaveSuccess).withFailureHandler(showError).db_saveNetWorthItem(form);
}
function onNetWorthSaveSuccess() {
document.getElementById('networth-edit-view').style.display = 'none';
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Syncing Records...";
const acc = g_currentDetailAccount;
const cat = g_currentDetailCategory;
google.script.run.withSuccessHandler(function(items) {
renderNetWorthDetail(items);
google.script.run.withSuccessHandler(function(data) {
const fmt = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
document.getElementById('networth-val').innerText = fmt(data.total);
const container = document.getElementById('networth-list-container');
container.innerHTML = '';
if(data.items.length > 0) {
data.items.forEach(item => {
let valClass = item.isDebt ? "nw-val nw-debt" : "nw-val";
const div = document.createElement('div');
div.className = 'nw-item';
div.onclick = function() { openNetWorthDetail(item.account, item.category); };
div.innerHTML = `<div class="nw-info"><span class="nw-acct">${item.account}</span><span class="nw-cat">${item.category}</span></div><div class="${valClass}">${fmt(item.amount)}</div>`;
container.appendChild(div);
});
}
google.script.run.withSuccessHandler(function(dashData) {
initDashboard(dashData);
document.getElementById('loading-overlay').style.display = 'none';
}).db_getFinancialData();
}).db_getNetWorthDetails();
}).db_getNetWorthItemDetails(acc, cat);
}
function deleteNetWorthItem(row, name) {
if(confirm("Are you sure you want to delete " + name + "?")) {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Deleting...";
setTimeout(() => {
google.script.run.withSuccessHandler(function() {
document.getElementById('loading-text').innerText = "Syncing Records...";
const acc = g_currentDetailAccount;
const cat = g_currentDetailCategory;
google.script.run.withSuccessHandler(function(items) {
renderNetWorthDetail(items, true);
google.script.run.withSuccessHandler(function(data) {
const fmt = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
document.getElementById('networth-val').innerText = fmt(data.total);
const container = document.getElementById('networth-list-container');
container.innerHTML = '';
if(data.items.length > 0) {
data.items.forEach(item => {
let valClass = item.isDebt ? "nw-val nw-debt" : "nw-val";
const div = document.createElement('div');
div.className = 'nw-item';
div.onclick = function() { openNetWorthDetail(item.account, item.category); };
div.innerHTML = `<div class="nw-info"><span class="nw-acct">${item.account}</span><span class="nw-cat">${item.category}</span></div><div class="${valClass}">${fmt(item.amount)}</div>`;
container.appendChild(div);
});
}
refreshMainDashboard();
}).db_getNetWorthDetails();
}).db_getNetWorthItemDetails(acc, cat);
}).withFailureHandler(showError).db_deleteNetWorthItem(row);
}, 50); // 50ms delay to ensure loader paints
}
}
// --- ADD ASSET LOGIC ---
function openAddAssetView() {
var currentClass = document.getElementById('nw-detail-subtitle').innerText;
var currentCategory = document.getElementById('nw-detail-title').innerText;
var groupName = currentClass;
var cleanGroup = groupName.toLowerCase().split(' ').map(function(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}).join(' ');
document.getElementById('nwa-class').innerText = cleanGroup;
document.getElementById('nwa-category').innerText = currentCategory;
document.getElementById('nwa-class-hidden').value = cleanGroup;
document.getElementById('nwa-category-hidden').value = currentCategory;
document.getElementById('nwa-account').value = "";
document.getElementById('nwa-amount').value = "";
document.getElementById('nwa-currency').value = "SGD";
document.getElementById('networth-add-view').style.display = 'flex';
}
function saveNewAsset() {
var acc = document.getElementById('nwa-account').value;
var amt = document.getElementById('nwa-amount').value;
if(!acc || !amt) { alert("Please fill in all fields."); return; }
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Creating Asset...";
var cleanAmt = amt.replace(/,/g, '');
const form = {
classVal: document.getElementById('nwa-class-hidden').value,
categoryVal: document.getElementById('nwa-category-hidden').value,
account: acc,
currency: document.getElementById('nwa-currency').value,
amount: cleanAmt
};
google.script.run.withSuccessHandler(function(){
document.getElementById('networth-add-view').style.display = 'none';
document.getElementById('loading-text').innerText = "Syncing Assets...";
google.script.run.withSuccessHandler(function(items) {
renderNetWorthDetail(items);
google.script.run.withSuccessHandler(renderNetWorth).db_getNetWorthDetails();
refreshMainDashboard();
}).db_getNetWorthItemDetails(form.categoryVal, form.classVal);
}).withFailureHandler(showError).db_addNewNetWorthItem(form);
}
// --- BUDGET LOGIC ---
function openBudgetView(e) {
if (e) e.stopPropagation();
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Fetching Details...";
google.script.run.withSuccessHandler(renderBudgetDetails).withFailureHandler(showError).db_getBudgetDetails();
}
function renderBudgetDetails(items) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('budget-detail-view').style.display = 'flex';
playPageAnimation('budget-detail-view');
const container = document.getElementById('budget-breakdown-container');
container.innerHTML = '';
let totalSpentSum = 0; // Initialize counter
items.forEach(item => {
const fmt = (val) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 }).format(val).replace('$', '$ ');
const cat = item.category || "";
const spent = item.spent || 0;
const left = item.left;
totalSpentSum += spent; // Add to total
let leftClass = (typeof left === 'number') ? (left < 0 ? "b-val b-val-neg" : "b-val b-val-left") : "b-val";
const wrapper = document.createElement('div');
wrapper.className = 'budget-swipe-wrapper';
wrapper.classList.add('animate-slide-in');
wrapper.style.animationDelay = `${items.indexOf(item) * 60}ms`;
const editBtn = document.createElement('div');
editBtn.className = 'budget-edit-action';
editBtn.innerHTML = `
<svg class="edit-icon-svg" viewBox="0 0 24 24">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
</svg>
<span class="edit-text">Edit Budget</span>
`;
editBtn.onclick = function() {
openCategoryEdit(cat);
wrapper.querySelector('.budget-swipe-content').style.transform = `translateX(0)`;
};
const content = document.createElement('div');
content.className = 'budget-swipe-content';
const rowInner = document.createElement('div');
rowInner.className = 'budget-row';
rowInner.onclick = function() { openTransactions(cat); };
rowInner.innerHTML = `
<div class="b-cat">${cat}</div>
<div class="b-data-col"><span class="b-label">Spent</span><span class="b-val">${fmt(spent)}</span></div>
<div class="b-data-col"><span class="b-label">Left</span><span class="${leftClass}">${fmt(left)}</span></div>
`;
content.appendChild(rowInner);
attachSwipe(content, -80);
wrapper.appendChild(editBtn);
wrapper.appendChild(content);
container.appendChild(wrapper);
});
// UPDATE THE TOTAL BOX AT THE TOP
const finalFmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 }).format(totalSpentSum).replace('$', '$ ');
document.getElementById('budget-total-spent-sum').innerText = finalFmt;
}
// --- TRANSACTIONS LOGIC ---
let currentTransData = null;
let transSortMode = 'date';
function openTransactions(category) {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading Transactions...";
document.getElementById('trans-header-title').innerText = category;
transSortMode = 'date';
google.script.run.withSuccessHandler(renderTransactions).withFailureHandler(showError).db_getTransactionsByCategory(category);
}
function renderTransactions(data) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('transaction-view').style.display = 'flex';
playPageAnimation('transaction-view');
currentTransData = data;
let list = data.list.slice();
if (transSortMode === 'amount') {
list.sort((a, b) => b.amount - a.amount);
document.getElementById('trans-sort-label').innerText = "Sorted by Amount ▼";
} else {
list.sort((a, b) => new Date(b.date) - new Date(a.date));
document.getElementById('trans-sort-label').innerText = "Sorted by Date ▼";
}
const total = data.total;
const fmtUSD = (val) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 }).format(val).replace('$', '$ ');
document.getElementById('trans-total-amount').innerText = fmtUSD(total);
const container = document.getElementById('transactions-container');
container.innerHTML = '';
if (list.length === 0) {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#94a3b8; font-size:13px;">No transactions found.</div>';
} else {
list.forEach(item => {
const div = document.createElement('div');
div.className = 'trans-item';
div.classList.add('animate-slide-in');
div.style.animationDelay = `${list.indexOf(item) * 40}ms`;
div.innerHTML = `<div class="trans-left"><span class="trans-date">${item.date}</span><span class="trans-desc">${item.desc}</span></div><div class="trans-amt">${fmtUSD(item.amount)}</div>`;
container.appendChild(div);
});
}
}
function toggleTransactionSort() {
if (!currentTransData) return;
transSortMode = (transSortMode === 'date') ? 'amount' : 'date';
renderTransactions(currentTransData);
}
function openAddSpend() {
document.getElementById('add-spend-view').style.display = 'flex';
document.getElementById('spend-date').value = new Date().toISOString().split('T')[0];
document.getElementById('spend-desc').value = "";
document.getElementById('spend-amt').value = "";
}
function saveSpend() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Saving...";
const form = {
date: document.getElementById('spend-date').value,
description: document.getElementById('spend-desc').value,
amount: document.getElementById('spend-amt').value
};
google.script.run.withSuccessHandler(function() {
document.getElementById('add-spend-view').style.display = 'none';
// Refresh the breakdown list if it's open
if(document.getElementById('budget-detail-view').style.display === 'flex') {
google.script.run.withSuccessHandler(renderBudgetDetails).db_getBudgetDetails();
}
// Refresh MAIN dashboard
refreshMainDashboard();
document.getElementById('loading-overlay').style.display = 'none';
}).withFailureHandler(showError).db_saveNewTransaction(form);
}
function transitionToEdit() {
document.getElementById('budget-detail-view').style.display = 'none';
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading Allocations...";
google.script.run.withSuccessHandler(renderEditForm).withFailureHandler(showError).db_getBudgetBreakdown();
}
function renderEditForm(data) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('edit-view').style.display = 'flex';
const container = document.getElementById('form-items-container');
container.innerHTML = '';
if (!data.order || data.order.length === 0) {
container.innerHTML = '<div style="text-align:center; padding:20px; color:#94a3b8;">No active categories found.<br><span style="font-size:10px;">(Check font colors in Sheet)</span></div>';
return;
}
// Loop through Categories (in sheet order)
data.order.forEach(category => {
const items = data.items[category];
// Create Group Card
const card = document.createElement('div');
card.className = 'alloc-group-card';
// Header
const header = document.createElement('div');
header.className = 'alloc-header';
header.innerText = category;
card.appendChild(header);
// Inputs
items.forEach(item => {
const rowDiv = document.createElement('div');
rowDiv.className = 'alloc-row';
rowDiv.innerHTML = `
<div class="alloc-label">${item.label}</div>
<div class="alloc-input-wrapper">
<input type="number"
class="alloc-input"
name="val-${item.row}"
value="${item.amount}"
inputmode="decimal"
step="0.01"
placeholder="0">
</div>
`;
card.appendChild(rowDiv);
});
container.appendChild(card);
});
}
function saveData() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Saving Allocations...";
const form = document.getElementById('budgetForm');
const formData = {};
for(let i=0; i<form.elements.length; i++){
const el = form.elements[i];
if(el.name && !el.disabled) formData[el.name] = el.value;
}
google.script.run.withSuccessHandler(function() {
document.getElementById('edit-view').style.display = 'none';
document.getElementById('loading-text').innerText = "Recalculating Limits...";
google.script.run.withSuccessHandler(function(items) {
renderBudgetDetails(items);
google.script.run.withSuccessHandler(function(dashData) {
initDashboard(dashData);
document.getElementById('loading-overlay').style.display = 'none';
}).db_getFinancialData();
}).db_getBudgetDetails();
}).withFailureHandler(showError).db_saveBudgetBreakdown(formData);
}
let currentRetTab = 'PLAN';
function openRetirementView(e) {
if (e) e.stopPropagation();
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading Projection...";
google.script.run.withSuccessHandler(renderRetirementData).withFailureHandler(showError).db_getRetirementAssets();
}
function renderRetirementData(data) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('retirement-view').style.display = 'flex';
playPageAnimation('retirement-view');
currentRemarks = data.plan.remark;
const p = data.plan;
const fmt = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
// --- TAB 1: PLAN INPUTS ---
// 1. IMPROVED STATUS PARSING
let valToFormat = 0;
let isBehind = false;
if (typeof p.statusVal === 'number') {
valToFormat = p.statusVal;
if(valToFormat < 0) isBehind = true;
} else {
// String handling: e.g. "1,000,000 (38%) Behind"
let strVal = String(p.statusVal);
isBehind = strVal.toLowerCase().includes("behind") || strVal.includes("-");
// Fix: Split by '(' to isolate the main number from the percentage
// "1,000,000 (38%)" -> ["1,000,000 ", "38%)"]
let parts = strVal.split('(');
let mainPart = parts[0];
// Clean the main part only
let cleanStr = mainPart.replace(/[^0-9.-]/g, '');
valToFormat = parseFloat(cleanStr);
if (isNaN(valToFormat)) valToFormat = 0;
// Ensure sign matches context
if (isBehind && valToFormat > 0) valToFormat = -valToFormat;
}
const statusValEl = document.getElementById('ret-status-val');
const statusLblEl = document.getElementById('ret-status-lbl');
statusValEl.innerText = fmt(Math.abs(valToFormat));
if (isBehind || valToFormat < 0) {
statusValEl.style.color = '#ef4444'; // Red
statusLblEl.innerText = "SHORTFALL";
statusLblEl.style.color = '#ef4444';
} else {
statusValEl.style.color = '#10b981'; // Green
statusLblEl.innerText = "EXTRA";
statusLblEl.style.color = '#10b981';
}
// 2. FORM INPUTS
document.getElementById('ret-year').value = p.yearRetire;
document.getElementById('ret-age').value = p.ageRetire;
document.getElementById('ret-age-until').value = p.ageUntil;
document.getElementById('ret-inflation').value = (p.inflation * 100).toFixed(2);
document.getElementById('ret-roi').value = (p.roi * 100).toFixed(2);
// Read Only Expense
document.getElementById('ret-exp-now').value = formatNumberWithCommas(p.expenseNow);
// Read Only Calculations
document.getElementById('ret-exp-future').innerText = fmt(p.expenseFuture);
document.getElementById('ret-target-corpus').innerText = fmt(p.corpusTarget);
// --- TAB 2: ASSETS ---
const container = document.getElementById('assets-container');
container.innerHTML = '';
if (!data.assets || data.assets.length === 0) {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#999; font-size:12px;">No assets found.</div>';
} else {
renderAssetsList(data.assets, container);
}
switchRetirementTab('PLAN');
}
function saveRetirementPlan() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Recalculating...";
const inflationPct = Number(document.getElementById('ret-inflation').value) / 100;
const roiPct = Number(document.getElementById('ret-roi').value) / 100;
const form = {
yearRetire: document.getElementById('ret-year').value,
ageUntil: document.getElementById('ret-age-until').value,
inflation: inflationPct,
roi: roiPct
};
google.script.run.withSuccessHandler(function() {
google.script.run.withSuccessHandler(renderRetirementData).db_getRetirementAssets();
}).withFailureHandler(showError).db_saveRetirementPlan(form);
}
function switchRetirementTab(tab) {
currentRetTab = tab;
// Buttons styling
document.getElementById('tab-ret-plan').className = (tab === 'PLAN') ? 'seg-btn active' : 'seg-btn';
document.getElementById('tab-ret-assets').className = (tab === 'ASSETS') ? 'seg-btn active' : 'seg-btn';
// Content visibility
document.getElementById('ret-tab-plan-content').style.display = (tab === 'PLAN') ? 'block' : 'none';
document.getElementById('ret-tab-assets-content').style.display = (tab === 'ASSETS') ? 'block' : 'none';
// Footer Button Logic: Only show 'Update Strategy' on the Plan tab
// When hidden, the 'Back' button will automatically stretch to fill the width (flex:1)
const updateBtn = document.getElementById('btn-ret-update');
if (tab === 'PLAN') {
updateBtn.style.display = 'block';
} else {
updateBtn.style.display = 'none';
}
}
function openInvestmentView(e) {
if (e) e.stopPropagation();
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading Assets...";
google.script.run.withSuccessHandler(renderInvestmentDetails).withFailureHandler(showError).db_getInvestmentDetails();
}
function renderInvestmentDetails(data) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('investment-view').style.display = 'flex';
playPageAnimation('investment-view');
const fmt = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
document.getElementById('invTotalHeader').innerText = fmt(data.total);
const container = document.getElementById('inv-assets-container');
container.innerHTML = '';
if (!data.assets || data.assets.length === 0) {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#999;">No active positions.</div>';
} else {
renderAssetsList(data.assets, container, true);
}
}
function renderAssetsList(assets, container, isInvestment = false) {
// Reset container
container.style.background = "transparent";
container.style.boxShadow = "none";
container.style.overflow = "visible";
const fmt = (val, type) => {
if (isNaN(parseFloat(val))) return val;
if (type === 'AVG_PRICE' || type === 'CUR_PRICE') return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 }).format(val).replace('$', '$ ');
if (type === 'MKT_VAL' || type === 'PROFIT') {
let finalVal = (type === 'PROFIT') ? Math.abs(val) : val;
return new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(finalVal).replace('₱', '₱ ');
}
if (type === 'PERCENT') return Math.abs(val * 100).toFixed(2) + '%';
return val;
};
assets.forEach(assetObj => {
let assetData = assetObj.data;
let rowIdx = assetObj.row;
let name = assetData.find(f => f.type === 'LABEL')?.value || 'Asset';
let avg = assetData.find(f => f.type === 'AVG_PRICE')?.value || 0;
let cur = assetData.find(f => f.type === 'CUR_PRICE')?.value || 0;
let unit = assetData.find(f => f.type === 'UNIT')?.value || 0;
let mkt = assetData.find(f => f.type === 'MKT_VAL')?.value || 0;
let profit = assetData.find(f => f.type === 'PROFIT')?.value || 0;
let pct = assetData.find(f => f.type === 'PERCENT')?.value || 0;
let desc = assetData.find(f => f.type === 'DESC')?.value || '';
// Color Logic
let isGain = profit >= 0;
let pColor = isGain ? '#15803d' : '#b91c1c';
let pBg = isGain ? '#dcfce7' : '#fee2e2';
let curFmt = fmt(cur, 'CUR_PRICE');
let mktFmt = fmt(mkt, 'MKT_VAL');
let profFmt = fmt(profit, 'PROFIT');
let pctFmt = fmt(pct, 'PERCENT');
let unitFmt = fmt(unit, 'UNIT');
let avgFmt = fmt(avg, 'AVG_PRICE');
// --- 1. CARD WRAPPER (This holds the shape) ---
const wrapper = document.createElement('div');
wrapper.style.position = "relative";
wrapper.style.overflow = "hidden"; // Clip the buttons
wrapper.style.borderRadius = "16px";
wrapper.style.marginBottom = "12px";
wrapper.style.background = "#ef4444"; // Red bg for overscroll
wrapper.style.boxShadow = "0 4px 12px rgba(0,0,0,0.03)";
// --- 2. ACTIONS (Hidden Behind) ---
const actions = document.createElement('div');
actions.className = 'double-actions'; // Reusing CSS from Net Worth
// Edit Button
const btnEdit = document.createElement('div');
btnEdit.className = 'action-btn ab-edit';
btnEdit.innerHTML = `
<svg class="edit-icon-svg" viewBox="0 0 24 24" style="width:20px; height:20px; fill:white; margin-bottom:2px;">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
</svg>
<span class="edit-text">Edit</span>`;
btnEdit.onclick = function() {
openAssetEdit(rowIdx, name, avg, unit, curFmt, mktFmt, profFmt, pctFmt, profit);
content.style.transform = `translateX(0)`; // Close swipe
};
// Delete Button
const btnDel = document.createElement('div');
btnDel.className = 'action-btn ab-del';
btnDel.innerHTML = `
<svg class="trash-icon" viewBox="0 0 24 24" style="width:20px; height:20px; fill:white; margin-bottom:2px;">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
<span class="edit-text">Delete</span>`;
btnDel.onclick = function() {
deleteAsset(rowIdx, name, isInvestment);
};
actions.appendChild(btnEdit);
actions.appendChild(btnDel);
// --- 3. CARD CONTENT (The White Face) ---
const content = document.createElement('div');
content.style.position = "relative";
content.style.zIndex = "2";
content.style.background = "white"; // White card face
content.style.padding = "15px";
content.style.transition = "transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1)";
content.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom: 12px;">
<div>
<div style="font-size:16px; font-weight:700; color:#1e293b; letter-spacing:-0.3px;">${name}</div>
${desc ? `<div style="font-size:11px; color:#94a3b8; margin-top:4px; font-weight:500;">${desc}</div>` : ''}
</div>
<div style="text-align:right;">
<div style="font-size:17px; font-weight:800; color:#1e293b; letter-spacing:-0.5px;">${mktFmt}</div>
<div style="display:flex; justify-content:flex-end; align-items:center; gap:6px; margin-top:5px;">
<span style="font-size:12px; font-weight:600; color:${pColor};">${profFmt}</span>
<span style="font-size:10px; font-weight:700; color:${pColor}; background:${pBg}; padding:2px 8px; border-radius:8px;">${pctFmt}</span>
</div>
</div>
</div>
<div style="background:#f8fafc; border-radius:12px; padding:12px 15px; display:grid; grid-template-columns: 1fr 1fr 1fr; gap:4px;">
<div style="text-align:left;">
<div style="font-size:9px; color:#94a3b8; text-transform:uppercase; font-weight:700; letter-spacing:0.5px;">Units</div>
<div style="font-size:13px; font-weight:600; color:#334155; margin-top:2px;">${unitFmt}</div>
</div>
<div style="text-align:center;">
<div style="font-size:9px; color:#94a3b8; text-transform:uppercase; font-weight:700; letter-spacing:0.5px;">Avg Cost</div>
<div style="font-size:13px; font-weight:600; color:#334155; margin-top:2px;">${avgFmt}</div>
</div>
<div style="text-align:right;">
<div style="font-size:9px; color:#94a3b8; text-transform:uppercase; font-weight:700; letter-spacing:0.5px;">Current</div>
<div style="font-size:13px; font-weight:600; color:#334155; margin-top:2px;">${curFmt}</div>
</div>
</div>`;
// Attach Logic (Reusing the helper you already have)
attachSwipe(content, -140);
wrapper.appendChild(actions);
wrapper.appendChild(content);
container.appendChild(wrapper);
});
}
function deleteAsset(row, name, isInvestment) {
if(confirm("Delete asset: " + name + "?")) {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Removing Asset...";
google.script.run.withSuccessHandler(function() {
document.getElementById('loading-text').innerText = "Updating Totals...";
if (isInvestment) {
// --- BRANCH A: STOCKS ---
google.script.run.withSuccessHandler(function(invData) {
renderInvestmentDetails(invData);
// Update Dashboard
google.script.run.withSuccessHandler(function(dashData) {
initDashboard(dashData);
document.getElementById('loading-overlay').style.display = 'none';
}).db_getFinancialData();
}).db_getInvestmentDetails();
} else {
// --- BRANCH B: RETIREMENT ---
google.script.run.withSuccessHandler(function(retData) {
renderRetirementData(retData);
// Update Dashboard
google.script.run.withSuccessHandler(function(dashData) {
initDashboard(dashData);
document.getElementById('loading-overlay').style.display = 'none';
}).db_getFinancialData();
}).db_getRetirementAssets();
}
}).withFailureHandler(showError).db_deleteInvestment(row);
}
}
function openAssetEdit(row, name, avg, units, cur, mkt, prof, pct, rawProf) {
document.getElementById('asset-edit-view').style.display = 'flex';
document.getElementById('asset-edit-title').innerText = name;
document.getElementById('asset-row-index').value = row;
document.getElementById('asset-avg-price').value = avg;
document.getElementById('asset-units').value = units;
document.getElementById('disp-cur').innerText = cur;
document.getElementById('disp-mkt').innerText = mkt;
document.getElementById('disp-prof').innerText = prof;
document.getElementById('disp-pct').innerText = pct;
// Dynamic Color Logic for Profit AND Return
if (rawProf >= 0) {
document.getElementById('disp-prof').style.color = '#10b981'; // Green
document.getElementById('disp-pct').style.color = '#10b981'; // Green
} else {
document.getElementById('disp-prof').style.color = '#ef4444'; // Red
document.getElementById('disp-pct').style.color = '#ef4444'; // Red
}
}
function saveAssetData() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Saving Asset...";
const formData = {
row: document.getElementById('asset-row-index').value,
avgPrice: document.getElementById('asset-avg-price').value,
units: document.getElementById('asset-units').value
};
google.script.run.withSuccessHandler(function() {
document.getElementById('asset-edit-view').style.display = 'none';
document.getElementById('loading-text').innerText = "Syncing Dashboard...";
if (isEditingInvestment) {
// --- BRANCH A: STOCKS ---
google.script.run.withSuccessHandler(function(invData) {
renderInvestmentDetails(invData);
// Update Dashboard
google.script.run.withSuccessHandler(function(dashData) {
initDashboard(dashData);
document.getElementById('loading-overlay').style.display = 'none';
}).db_getFinancialData();
}).db_getInvestmentDetails();
} else {
// --- BRANCH B: RETIREMENT ---
google.script.run.withSuccessHandler(function(retData) {
renderRetirementData(retData);
// Update Dashboard
google.script.run.withSuccessHandler(function(dashData) {
initDashboard(dashData);
document.getElementById('loading-overlay').style.display = 'none';
}).db_getFinancialData();
}).db_getRetirementAssets();
}
}).withFailureHandler(showError).db_saveRetirementAsset(formData);
}
function openRemarksView() {
document.getElementById('remarks-view').style.display = 'flex';
document.getElementById('full-remarks-text').innerText = currentRemarks || "No analysis available.";
}
function closeOverlay(id) {
document.getElementById(id).style.display = 'none';
playPageAnimation('dashboard-view');
updateTextDisplay();
}
function togglePrivacy() {
isVisible = !isVisible;
updateTextDisplay();
}
function updateTextDisplay() {
if (!globalData) return;
// --- 1. CALCULATE DYNAMIC YEARS ---
const now = new Date();
const curYear = now.getFullYear();
document.getElementById('lbl-ytd-cur').innerText = "(Current Year)";
document.getElementById('lbl-ytd-last').innerText = curYear - 1;
document.getElementById('lbl-ytd-last2').innerText = curYear - 2;
// --- 2. ICON TOGGLE LOGIC (UPDATED) ---
const privacyIcon = document.getElementById('privacy-icon');
const privacyBtn = document.getElementById('privacy-btn');
if (privacyIcon && privacyBtn) {
if (isVisible) {
// SHOWING: Eye Icon (Open)
// Clean eye path
privacyIcon.innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>';
privacyBtn.style.opacity = '1';
privacyBtn.style.color = '#3b82f6'; // Active Blue
} else {
// HIDDEN: Eye Slash Icon (Closed)
// Eye with a slash through it
privacyIcon.innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>';
privacyBtn.style.opacity = '0.6';
privacyBtn.style.color = '#94a3b8'; // Passive Grey
}
}
// --- 3. FORMATTERS ---
const formatUSD = (val) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 }).format(val).replace('$', '$ ');
const formatPHP = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
// 1 Decimal (Keep for small history items to save space)
const formatCompact = (val) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', notation: "compact", maximumFractionDigits: 1 }).format(val).replace('$', '$');
// 2 Decimals (For the Main Trend Values)
const formatCompactMain = (val) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', notation: "compact", maximumFractionDigits: 2 }).format(val).replace('$', '$');
const getPrivacyText = (val, isCompact) => {
if (!isVisible) return isCompact ? '$***' : '₱ *****';
return isCompact ? formatCompact(val) : formatPHP(val);
};
// ===============================================
// SECTION A: ALWAYS VISIBLE (Budget & Trends)
// ===============================================
// --- Budget Main ---
document.getElementById('curBudget').innerText = formatUSD(globalData.budget.current);
document.getElementById('tgtBudget').innerText = formatUSD(globalData.budget.target);
document.getElementById('curBudget').style.color = globalData.budget.current < 0 ? '#ef4444' : '#10b981';
document.getElementById('spent-month-front').innerText = formatCompactMain(globalData.budget.spentCur).replace('$', '$ ');
// --- Budget Comparison Data (YOY Card) ---
const bData = globalData.budget;
// Month Comparison
document.getElementById('cmp-month-cur').innerText = formatCompactMain(bData.spentCur);
document.getElementById('cmp-month-last').innerText = formatCompact(bData.spentLast);
let mDiff = 0;
if (bData.spentLast > 0) mDiff = ((bData.spentCur - bData.spentLast) / bData.spentLast) * 100;
const mPill = document.getElementById('cmp-month-pct');
mPill.innerText = (mDiff > 0 ? '+' : '') + mDiff.toFixed(0) + '%';
mPill.className = 'comp-pill ' + (mDiff > 0 ? 'cp-bad' : 'cp-good');
// YTD 3-Year Trend
document.getElementById('cmp-ytd-cur').innerText = formatCompactMain(bData.ytdCur);
document.getElementById('cmp-ytd-last').innerText = formatCompact(bData.ytdLast);
document.getElementById('cmp-ytd-last2').innerText = formatCompact(bData.ytdLast2);
// ===============================================
// SECTION B: HIDDEN ON TOGGLE (Assets & Goals)
// ===============================================
// Only update Target (Total Cost), keep Current as the Goal Name
document.getElementById('tgtSavings').innerText = getPrivacyText(globalData.savings.target, false);
// Ensure curSavings is set to the Active Name (always visible)
document.getElementById('curSavings').innerText = globalData.savings.activeName;
document.getElementById('curRetire').innerText = getPrivacyText(globalData.retirement.current, false);
document.getElementById('tgtRetire').innerText = getPrivacyText(globalData.retirement.target, false);
document.getElementById('curEmergency').innerText = getPrivacyText(globalData.emergency.current, false);
document.getElementById('tgtEmergency').innerText = getPrivacyText(globalData.emergency.target, false);
if (invDataGlobal) {
const profit = invDataGlobal.profit;
const mkt = invDataGlobal.current;
if (isVisible) {
// FIX: Added Math.abs() to remove negative sign
document.getElementById('invMainVal').innerText = formatPHP(Math.abs(profit));
// Color logic handles the Red/Green
document.getElementById('invMainVal').className = profit >= 0 ? 'inv-pl-text inv-pos' : 'inv-pl-text inv-neg';
document.getElementById('invMktVal').innerText = formatPHP(mkt);
let cap = invDataGlobal.capital;
let ret = cap !== 0 ? Math.abs((profit / cap) * 100).toFixed(2) + '%' : '0%';
document.getElementById('invRetPct').innerText = ret;
document.getElementById('invRetPct').style.color = profit >= 0 ? '#10b981' : '#ef4444';
} else {
document.getElementById('invMainVal').innerText = '*****';
document.getElementById('invMainVal').className = 'inv-pl-text';
document.getElementById('invMktVal').innerText = '₱ *****';
document.getElementById('invRetPct').innerText = '**%';
document.getElementById('invRetPct').style.color = '#94a3b8';
}
}
}
let charts = {};
function drawChart(canvasId, valueForGauge, totalTarget, isBudget, colorOverride) {
if (charts[canvasId]) {
charts[canvasId].destroy();
delete charts[canvasId];
}
const ctx = document.getElementById(canvasId).getContext('2d');
const current = Number(valueForGauge); // This is now Solvency for savings
const target = Number(totalTarget);
let chartColor = colorOverride;
let percent = 0;
let chartData = [];
if (isBudget) {
if (current < 0) {
chartColor = '#ef4444';
percent = 0;
chartData = [0, 100];
} else {
chartColor = '#10b981';
percent = target > 0 ? Math.min((current / target) * 100, 100).toFixed(0) : 0;
chartData = [current, Math.max(0, target - current)];
}
document.getElementById('pctBudget').innerHTML = percent + '<span class="small-pct">%</span>';
document.getElementById('pctBudget').style.color = chartColor;
} else {
// Standard Gauge Logic
percent = target > 0 ? Math.min((current / target) * 100, 100).toFixed(0) : 0;
chartData = [current, Math.max(0, target - current)];
// Find the label ID (chartSavings -> pctSavings)
const pctId = canvasId.replace('chart', 'pct');
document.getElementById(pctId).innerHTML = percent + '<span class="small-pct">%</span>';
}
charts[canvasId] = new Chart(ctx, {
type: 'doughnut',
data: {
datasets: [{
data: chartData,
backgroundColor: [chartColor, '#f1f5f9'],
borderWidth: 0,
hoverOffset: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
resizeDelay: 200,
rotation: -90,
circumference: 180,
cutout: '85%',
layout: { padding: 0 },
plugins: { legend: { display: false }, tooltip: { enabled: false } },
animation: {
duration: 1500,
easing: 'easeOutQuart',
animateRotate: true,
animateScale: false
}
}
});
}
function showToast(text) {
var x = document.getElementById("toast-msg");
x.innerText = text;
x.className = "show";
setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000);
}
// --- HELPER FORMATTERS ---
function formatNumberWithCommas(n) {
if (n === "" || n === null || isNaN(n)) return "";
return Number(n).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 2});
}
function onAmountBlur(el) {
if(el.value === "") return;
let cleanVal = el.value.replace(/,/g, '');
if(!isNaN(cleanVal) && cleanVal !== "") {
el.value = formatNumberWithCommas(cleanVal);
}
}
function onAmountFocus(el) {
el.value = el.value.replace(/,/g, '');
el.select();
}
function onAmountInput(el) {
el.value = el.value.replace(/[^0-9.]/g, '');
}
function showError(error) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('error-msg').innerText = "Error: " + error;
}
// --- NEW: BUDGET SWIPE LOGIC ---
const budSwipe = document.getElementById('budgetSwipeContainer');
const cardBudMain = document.getElementById('cardBudgetMain');
const cardBudComp = document.getElementById('cardBudgetComp');
let activeBudCard = 'main';
budSwipe.addEventListener('touchstart', e => { touchStartX = e.changedTouches[0].screenX; }, {passive: true});
budSwipe.addEventListener('touchend', e => {
touchEndX = e.changedTouches[0].screenX;
handleBudgetSwipe();
}, {passive: true});
budSwipe.addEventListener('mousedown', e => { touchStartX = e.screenX; });
budSwipe.addEventListener('mouseup', e => { touchEndX = e.screenX; handleBudgetSwipe(); });
function handleBudgetSwipe() {
const threshold = 50;
if (touchEndX < touchStartX - threshold && activeBudCard === 'main') {
// Show Compare
activeBudCard = 'comp';
cardBudMain.className = 'stacked-card card-left';
cardBudComp.className = 'stacked-card card-front';
cardBudComp.style.zIndex = "";
cardBudMain.style.zIndex = "5";
setTimeout(() => { if(activeBudCard==='comp') cardBudMain.className='stacked-card card-back'; }, 300);
document.getElementById('dotBudMain').classList.remove('active');
document.getElementById('dotBudComp').classList.add('active');
} else if (touchEndX > touchStartX + threshold && activeBudCard === 'comp') {
// Show Main
activeBudCard = 'main';
cardBudComp.className = 'stacked-card card-right';
cardBudMain.className = 'stacked-card card-front';
cardBudMain.style.zIndex = "";
cardBudComp.style.zIndex = "5";
setTimeout(() => { if(activeBudCard==='main') cardBudComp.className='stacked-card card-back'; }, 300);
document.getElementById('dotBudComp').classList.remove('active');
document.getElementById('dotBudMain').classList.add('active');
}
}
// --- YOY CHART LOGIC ---
let yoyChartInstance = null;
// 1. Triggered by clicking the card
function openYOYView(e) {
if (e) e.stopPropagation();
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Analyzing Trends...";
google.script.run.withSuccessHandler(renderYOYChart).withFailureHandler(showError).db_getYearlyTrends();
}
// 2. Render the Chart
function renderYOYChart(data) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('yoy-view').style.display = 'flex';
// Update Labels
document.getElementById('lbl-leg-1').innerText = data.years[0];
document.getElementById('lbl-leg-2').innerText = data.years[1];
// Verdict Logic
const sumCur = data.datasets[0].reduce((a, b) => a + b, 0);
const sumLast = data.datasets[1].reduce((a, b) => a + b, 0);
const monthIdx = new Date().getMonth() + 1;
const avgCur = sumCur / monthIdx;
const avgLast = sumLast / 12;
let verdict = "Spending is Stable.";
if (avgCur > avgLast * 1.1) verdict = "Caution: Lifestyle Inflation Detected.";
else if (avgCur < avgLast * 0.9) verdict = "Great! Spending is Decreasing.";
document.getElementById('yoy-verdict').innerText = verdict;
// Draw Chart
const ctx = document.getElementById('chartYOY').getContext('2d');
if (yoyChartInstance) yoyChartInstance.destroy();
yoyChartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [
{
label: data.years[0],
data: data.datasets[0],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 3,
tension: 0.4,
pointRadius: 3,
fill: true
},
{
label: data.years[1],
data: data.datasets[1],
borderColor: '#94a3b8',
borderWidth: 2,
borderDash: [5, 5],
tension: 0.4,
pointRadius: 0,
fill: false
},
{
label: data.years[2],
data: data.datasets[2],
borderColor: '#cbd5e1',
borderWidth: 1,
borderDash: [2, 2],
tension: 0.4,
pointRadius: 0,
fill: false
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false }, // Keeps "sticky" selection for easy tapping
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(30, 41, 59, 0.9)',
padding: 10,
cornerRadius: 8,
callbacks: {
label: function(context) {
return ' ' + context.dataset.label + ': ' + new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.raw);
}
}
}
},
scales: {
y: { beginAtZero: true, grid: { display: false }, ticks: { font: { size: 10 } } },
x: { grid: { display: false }, ticks: { font: { size: 10 } } }
}
}
});
}
// --- EMERGENCY FUND LOGIC ---
function openEmergencyView(e) {
if (e) e.stopPropagation();
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Checking Coverage...";
google.script.run.withSuccessHandler(renderEmergencyView).withFailureHandler(showError).db_getEmergencyDetails();
}
function renderEmergencyView(data) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('emergency-view').style.display = 'flex';
const fmt = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
// --- Card 1: Monthly Requirement ---
document.getElementById('emg-exp-req').innerText = fmt(data.expensePHP);
const statusBox = document.getElementById('emg-status-box');
const statusTitle = document.getElementById('emg-status-title');
const statusDesc = document.getElementById('emg-status-desc');
let expense = data.expensePHP > 0 ? data.expensePHP : 1;
// Logic for Card 1 (SGD Coverage only)
let monthsSGD = (data.fundSGD_PHP / expense).toFixed(1);
if (data.fundSGD_PHP >= data.expensePHP) {
statusBox.style.background = "#f0fdf4"; // Green
statusBox.style.borderColor = "#dcfce7";
statusTitle.innerText = "FULLY COVERED";
statusTitle.style.color = "#15803d";
statusDesc.innerHTML = `Your SGD Funds cover <strong>${monthsSGD} months</strong>.`;
} else {
const diff = data.expensePHP - data.fundSGD_PHP;
statusBox.style.background = "#fef2f2"; // Red
statusBox.style.borderColor = "#fee2e2";
statusTitle.innerText = "SHORTFALL DETECTED";
statusTitle.style.color = "#b91c1c";
statusDesc.innerHTML = `Your SGD Funds only cover <strong>${monthsSGD} months</strong>.<br>Short by ${fmt(diff)}.`;
}
// --- Card 2: PHP Buffer ---
document.getElementById('emg-php-actual').innerText = fmt(data.fundPHP_Actual);
let pct = 0;
if (data.fundPHP_Target > 0) {
pct = Math.min((data.fundPHP_Actual / data.fundPHP_Target) * 100, 100);
}
document.getElementById('emg-php-bar').style.width = pct + "%";
if (pct >= 100) {
document.getElementById('emg-php-bar').style.background = "#10b981";
} else {
document.getElementById('emg-php-bar').style.background = "#3b82f6";
}
// --- Card 3: Total Survival (Summary) ---
let totalFunds = data.fundSGD_PHP + data.fundPHP_Actual;
let monthsTotal = (totalFunds / expense).toFixed(1);
// Update the big number at the bottom
document.getElementById('emg-total-months').innerText = monthsTotal + " Months";
}
function dismissChartTooltip(e) {
// If the thing we clicked on is NOT the chart canvas...
if (e.target.id !== 'chartYOY' && yoyChartInstance) {
// ...tell the chart to clear all active tooltips
yoyChartInstance.tooltip.setActiveElements([], {x: 0, y: 0});
yoyChartInstance.update();
}
}
// --- EXPENSE BREAKDOWN LOGIC ---
let expenseDataCache = null; // Store data here to avoid re-fetching on toggle
let currentExpenseTab = 'SG';
function openExpenseBreakdown(e) {
if (e) e.stopPropagation();
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading Expenses...";
google.script.run.withSuccessHandler(renderExpenseBreakdown).withFailureHandler(showError).db_getExpenseBreakdown();
}
function renderExpenseBreakdown(data) {
document.getElementById('loading-overlay').style.display = 'none';
// Hide Emergency View, Show Breakdown View
document.getElementById('emergency-view').style.display = 'none';
document.getElementById('expense-breakdown-view').style.display = 'flex';
playPageAnimation('expense-breakdown-view');
expenseDataCache = data; // Save data
// Reset to SG tab by default
switchExpenseTab('SG');
}
function switchExpenseTab(tab) {
currentExpenseTab = tab;
// 1. Update Buttons
document.getElementById('tab-sg').className = (tab === 'SG') ? 'seg-btn active' : 'seg-btn';
document.getElementById('tab-ph').className = (tab === 'PH') ? 'seg-btn active' : 'seg-btn';
// 2. Formatters
const fmtSGD = (val) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(val).replace('$', 'S$ ');
const fmtPHP = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
const data = (tab === 'SG') ? expenseDataCache.sg : expenseDataCache.ph;
const formatter = (tab === 'SG') ? fmtSGD : fmtPHP;
const rate = expenseDataCache.rate;
// 3. Render List
const container = document.getElementById('exp-breakdown-list');
container.innerHTML = '';
if (data.items.length === 0) {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#94a3b8; font-size:12px;">No expenses listed.</div>';
} else {
data.items.forEach(item => {
const div = document.createElement('div');
div.className = 'budget-row';
div.innerHTML = `
<div class="b-cat" style="width:auto; flex-grow:1;">${item.category}</div>
<div class="b-data-col" style="width:auto;">
<span class="b-val">${formatter(item.amount)}</span>
</div>
`;
container.appendChild(div);
});
}
// 4. Update Footer Total & Conversion
document.getElementById('exp-breakdown-total').innerText = formatter(data.total);
// CONVERSION LOGIC
const convEl = document.getElementById('exp-breakdown-converted');
if (tab === 'SG') {
// SG View -> Show PHP
let converted = data.total * rate;
convEl.innerText = "≈ " + fmtPHP(converted);
} else {
// PH View -> Show SGD
let converted = data.total / rate;
convEl.innerText = "≈ " + fmtSGD(converted);
}
}
// --- SAVINGS GOALS LOGIC ---
function openGoalsView(e) {
if (e) e.stopPropagation();
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Analyzing Goals...";
google.script.run.withSuccessHandler(renderGoalsView).withFailureHandler(showError).db_getSavingsGoals();
}
function renderGoalsView(data) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('goals-view').style.display = 'flex';
playPageAnimation('goals-view');
const fmt = (val) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP', maximumFractionDigits: 0 }).format(val).replace('₱', '₱ ');
document.getElementById('goal-total-pool').innerText = fmt(data.totalSavings);
document.getElementById('goal-monthly-rate').innerText = fmt(data.monthlyRatePHP) + " /mo";
document.getElementById('goal-yearly-rate').innerText = fmt(data.monthlyRatePHP * 12) + " /yr";
const container = document.getElementById('goals-list-container');
container.innerHTML = '';
if (data.goals.length === 0) {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#94a3b8; font-size:12px;">No goals found.</div>';
return;
}
data.goals.forEach((g, index) => {
// 1. Wrapper
const wrapper = document.createElement('div');
wrapper.className = 'goal-card-wrapper animate-slide-in';
wrapper.style.animationDelay = `${index * 70}ms`;
// 2. Delete Action (Apple Style Button)
const delBtn = document.createElement('div');
delBtn.className = 'goal-delete-action';
// SVG Icon for Trash
delBtn.innerHTML = `
<svg class="trash-icon" viewBox="0 0 24 24">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
<span class="trash-text">Delete</span>
`;
// CLICK LISTENER: Only deletes when you tap the red button
delBtn.onclick = function() {
if(confirm("Delete goal: " + g.name + "?")) {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Deleting...";
// CHAIN START: Delete the Goal
google.script.run.withSuccessHandler(() => {
document.getElementById('loading-text').innerText = "Updating Dashboard...";
// STEP 1: Refresh the Goals List
google.script.run.withSuccessHandler(function(listData) {
renderGoalsView(listData);
// STEP 2: Refresh the Main Dashboard Card
google.script.run.withSuccessHandler(function(dashData) {
initDashboard(dashData);
// STEP 3: Only hide loader after dashboard is ready
document.getElementById('loading-overlay').style.display = 'none';
}).db_getFinancialData();
}).db_getSavingsGoals();
}).db_deleteSavingsGoal(g.name);
} else {
wrapper.querySelector('.goal-card-content').style.transform = `translateX(0)`;
}
};
// 3. Content
const content = document.createElement('div');
content.className = 'goal-card-content';
// Tag Logic
let tagHTML = '';
if (g.status === 'Secured') tagHTML = `<span class="g-tag gt-secured">Secured</span>`;
else if (g.onTrack) tagHTML = `<span class="g-tag gt-track">On Track</span>`;
else tagHTML = `<span class="g-tag gt-risk">At Risk</span>`;
let barColor = g.status === 'Secured' ? '#10b981' : (g.onTrack ? '#3b82f6' : '#ef4444');
let suggestionHTML = '';
if (!g.onTrack) {
suggestionHTML = `
<div style="margin-top:12px; background:#fef2f2; border:1px solid #fee2e2; border-radius:8px; padding:10px;">
<div style="font-size:10px; font-weight:700; color:#b91c1c; margin-bottom:6px; text-transform:uppercase;">To get back on track:</div>
<div style="display:flex; justify-content:space-between;">
<div style="text-align:left;">
<span style="display:block; font-size:9px; color:#ef4444;">LUMP SUM</span>
<strong style="font-size:12px; color:#991b1b;">+${fmt(g.fixLumpSum)}</strong>
</div>
<div style="width:1px; background:#fee2e2;"></div>
<div style="text-align:right;">
<span style="display:block; font-size:9px; color:#ef4444;">MONTHLY ADD</span>
<strong style="font-size:12px; color:#991b1b;">+${fmt(g.fixMonthly)}</strong>
</div>
</div>
</div>`;
}
content.innerHTML = `
<div class="goal-header">
<div>
<div class="goal-title">${g.name}</div>
<div class="goal-date">Target: ${g.date}</div>
</div>
${tagHTML}
</div>
<div style="display:flex; justify-content:space-between; margin-top:10px; font-size:11px;">
<span style="color:#64748b;">${g.pctFunded.toFixed(0)}% Funded</span>
<span style="color:#1e293b; font-weight:600;">${fmt(g.allocated)} / ${fmt(g.cost)}</span>
</div>
<div class="goal-progress-bg">
<div class="goal-progress-fill" style="width:${g.pctFunded}%; background:${barColor};"></div>
</div>
${g.remaining > 0 ? `<div style="text-align:right; font-size:10px; color:#94a3b8; margin-top:4px;">Remaining: ${fmt(g.remaining)}</div>` : ''}
${suggestionHTML}
`;
// Attach Apple-Style Swipe
attachSwipe(content, -80);
wrapper.appendChild(delBtn);
wrapper.appendChild(content);
container.appendChild(wrapper);
});
}
// --- ADD GOAL LOGIC ---
function openAddGoalView() {
document.getElementById('add-goal-view').style.display = 'flex';
// Reset Form
document.getElementById('goal-name').value = "";
document.getElementById('goal-amt').value = "";
document.getElementById('goal-date').value = "";
}
function saveNewGoal() {
var name = document.getElementById('goal-name').value;
var amt = document.getElementById('goal-amt').value;
var date = document.getElementById('goal-date').value;
if (!name || !amt || !date) {
alert("Please fill in all fields.");
return;
}
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Saving Goal...";
const form = {
name: name,
amount: amt.replace(/,/g, ''),
date: date
};
// CHAIN START: Save the Goal
google.script.run.withSuccessHandler(function() {
document.getElementById('add-goal-view').style.display = 'none';
document.getElementById('loading-text').innerText = "Updating Dashboard...";
// STEP 1: Refresh the Goals List
google.script.run.withSuccessHandler(function(listData) {
renderGoalsView(listData);
// STEP 2: Refresh the Main Dashboard Card (Nested inside)
google.script.run.withSuccessHandler(function(dashData) {
initDashboard(dashData);
// STEP 3: ONLY NOW do we hide the loading screen
document.getElementById('loading-overlay').style.display = 'none';
}).db_getFinancialData();
}).db_getSavingsGoals();
}).withFailureHandler(showError).db_addSavingsGoal(form);
}
function drawSegmentedChart(canvasId, dataArray, colorArray) {
if (charts[canvasId]) {
charts[canvasId].destroy();
delete charts[canvasId];
}
const ctx = document.getElementById(canvasId).getContext('2d');
charts[canvasId] = new Chart(ctx, {
type: 'doughnut',
data: {
datasets: [{
data: dataArray,
backgroundColor: colorArray,
borderWidth: 2, // Slight gap between segments for clarity
borderColor: '#ffffff',
hoverOffset: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
resizeDelay: 200,
rotation: -90,
circumference: 180,
cutout: '82%',
layout: {
padding: {
left: -5,
right: -5,
top: 0,
bottom: 0
}
},
plugins: { legend: { display: false }, tooltip: { enabled: false } },
animation: {
duration: 1500,
easing: 'easeOutQuart',
animateRotate: true,
animateScale: false
}
}
});
}
function openCategoryEdit(targetCategory) {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading " + targetCategory + "...";
// We reuse the existing backend function
google.script.run.withSuccessHandler(function(fullData) {
// FILTER THE DATA LOCALLY
// We only want the order array to contain our target
// and the items object to have that key.
// Check if category exists in data
if (fullData.items && fullData.items[targetCategory]) {
const filteredData = {
order: [targetCategory], // Only show this one
items: {}
};
filteredData.items[targetCategory] = fullData.items[targetCategory];
renderEditForm(filteredData);
} else {
showError("Configuration for '" + targetCategory + "' not found in Budget Sheet.");
}
}).withFailureHandler(showError).db_getBudgetBreakdown();
}
function openAddStockView() {
// Reset all fields
document.getElementById('stock-ticker').value = "";
document.getElementById('stock-c').value = "ETF"; // Default to ETF
document.getElementById('stock-d').value = "";
document.getElementById('stock-f').value = "";
document.getElementById('stock-g').value = "USD"; // Default to USD often helpful
document.getElementById('stock-price').value = "";
document.getElementById('stock-units').value = "";
document.getElementById('stock-desc').value = "";
setStockType('Day Trading');
document.getElementById('add-stock-view').style.display = 'flex';
}
function saveNewStock() {
var ticker = document.getElementById('stock-ticker').value;
var price = document.getElementById('stock-price').value;
var units = document.getElementById('stock-units').value;
if(!ticker || !price || !units) {
alert("Please fill in Ticker, Price, and Units.");
return;
}
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Adding Position...";
var form = {
ticker: ticker.toUpperCase(),
type: document.getElementById('stock-type').value,
colC: document.getElementById('stock-c').value,
colD: document.getElementById('stock-d').value,
colF: document.getElementById('stock-f').value,
colG: document.getElementById('stock-g').value.toUpperCase(),
avgPrice: price,
units: units,
desc: document.getElementById('stock-desc').value
};
google.script.run.withSuccessHandler(function() {
document.getElementById('add-stock-view').style.display = 'none';
document.getElementById('loading-text').innerText = "Recalculating Portfolio...";
// STEP 1: Refresh Investment List
google.script.run.withSuccessHandler(function(invData) {
renderInvestmentDetails(invData);
// STEP 2: Refresh Main Dashboard
google.script.run.withSuccessHandler(function(dashData) {
initDashboard(dashData);
// STEP 3: Hide Loader
document.getElementById('loading-overlay').style.display = 'none';
}).db_getFinancialData();
}).db_getInvestmentDetails();
}).withFailureHandler(showError).db_addStock(form);
}
function setStockType(type) {
document.getElementById('stock-type').value = type;
if(type === 'Day Trading') {
document.getElementById('type-day').className = 'seg-btn active';
document.getElementById('type-ret').className = 'seg-btn';
} else {
document.getElementById('type-day').className = 'seg-btn';
document.getElementById('type-ret').className = 'seg-btn active';
}
}
// --- HELPER: REFRESH MAIN DASHBOARD DATA ---
function refreshMainDashboard() {
google.script.run.withSuccessHandler(initDashboard).db_getFinancialData();
}
// --- DEVICE NAME DETECTOR ---
async function getPhoneName() {
const ua = navigator.userAgent;
const screenW = window.screen.width;
const screenH = window.screen.height;
const ratio = window.devicePixelRatio;
// 1. TRY MODERN ANDROID API (High Accuracy)
if (navigator.userAgentData && navigator.userAgentData.getHighEntropyValues) {
try {
const data = await navigator.userAgentData.getHighEntropyValues(["model"]);
if (data.model) return data.model;
} catch(e) { console.log(e); }
}
// 2. IOS DETECTION (Screen Size Hack)
if (/iPhone/.test(ua)) {
// Logic: Match logical screen width x height
if (screenW === 390 && screenH === 844) return "iPhone 12/13/14";
if (screenW === 428 && screenH === 926) return "iPhone 12/13/14 Pro Max";
if (screenW === 393 && screenH === 852) return "iPhone 15 Pro";
if (screenW === 430 && screenH === 932) return "iPhone 14/15/16 Pro Max";
if (screenW === 375 && screenH === 812) return "iPhone X/XS/11 Pro";
if (screenW === 414 && screenH === 896) return "iPhone XR/11";
if (screenW === 414 && screenH === 736) return "iPhone Plus";
if (screenW === 375 && screenH === 667) return "iPhone 6/7/8/SE";
return "iPhone"; // Fallback
}
// 3. LEGACY ANDROID PARSING
if (/Android/.test(ua)) {
const match = ua.match(/Android.*?; (.*?)(?:\)| Build)/);
if (match && match[1]) {
return match[1].trim();
}
return "Android Device";
}
// 4. DESKTOP / UNKNOWN
return "Web Client";
}
function openRetExpenseDetails() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading Expenses...";
google.script.run.withSuccessHandler(renderRetExpenseList).db_getRetirementExpenseDetails();
}
function renderRetExpenseList(data) {
document.getElementById('loading-overlay').style.display = 'none';
document.getElementById('ret-expense-detail-view').style.display = 'flex';
const container = document.getElementById('ret-exp-list-container');
container.innerHTML = '';
const fmt = (val) => new Intl.NumberFormat('en-PH', {
style: 'currency',
currency: 'PHP',
maximumFractionDigits: 0
}).format(val).replace('₱', '₱ ');
let runningTotal = 0;
const catSelect = document.getElementById('re-category');
catSelect.innerHTML = data.categories.map(c => `<option value="${c}">${c}</option>`).join('');
if (data.items.length === 0) {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#94a3b8; font-size:12px;">No expenses listed.</div>';
} else {
data.items.forEach(item => {
runningTotal += item.amount; // Add to sum
const wrapper = document.createElement('div');
wrapper.className = 'double-action-wrapper';
wrapper.style.marginBottom = "1px";
wrapper.style.borderRadius = "0";
const actions = document.createElement('div');
actions.className = 'double-actions';
const btnEdit = document.createElement('div');
btnEdit.className = 'action-btn ab-edit';
btnEdit.innerHTML = `<svg style="width:18px;height:18px;fill:white" viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg><span style="font-size:9px;font-weight:700;">Edit</span>`;
btnEdit.onclick = () => openEditRetExpense(item);
const btnDel = document.createElement('div');
btnDel.className = 'action-btn ab-del';
btnDel.innerHTML = `<svg style="width:18px;height:18px;fill:white" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg><span style="font-size:9px;font-weight:700;">Del</span>`;
btnDel.onclick = () => {
if(confirm(`Delete ${item.subCategory}?`)) {
document.getElementById('loading-overlay').style.display = 'flex';
google.script.run.withSuccessHandler(openRetExpenseDetails).db_deleteRetirementExpense(item.row);
}
};
const content = document.createElement('div');
content.className = 'swipe-content-row';
content.innerHTML = `
<div class="budget-row" style="border-bottom:none; padding: 12px 15px;">
<div class="b-cat" style="flex:1">
<span style="font-size:13px; font-weight:700; color:#1e293b;">${item.subCategory}</span>
<span style="display:block; font-size:10px; color:#94a3b8; margin-top:2px;">${item.category}</span>
</div>
<div class="b-val" style="font-size:14px; color:#334155; font-weight:600;">${fmt(item.amount)}</div>
</div>
`;
attachSwipe(content, -140);
actions.appendChild(btnEdit);
actions.appendChild(btnDel);
wrapper.appendChild(actions);
wrapper.appendChild(content);
container.appendChild(wrapper);
});
}
document.getElementById('ret-exp-running-total').innerText = fmt(runningTotal);
}
function openEditRetExpense(item) {
document.getElementById('ret-exp-edit-title').innerText = "Edit Amount";
document.getElementById('re-row').value = item.row;
document.getElementById('loading-text').innerText = "Loading Expenses...";
document.getElementById('re-amount').value = formatNumberWithCommas(item.amount);
// Show non-editable context
document.getElementById('re-info-grid').style.display = 'grid';
document.getElementById('re-disp-cat').innerText = item.category;
document.getElementById('re-disp-sub').innerText = item.subCategory;
// Hide input fields
document.getElementById('re-cat-group').style.display = 'none';
document.getElementById('re-sub-group').style.display = 'none';
document.getElementById('ret-expense-edit-view').style.display = 'flex';
}
function openAddRetExpense() {
document.getElementById('ret-exp-edit-title').innerText = "Add Expense";
document.getElementById('re-row').value = "";
document.getElementById('re-amount').value = "";
document.getElementById('re-subcategory').value = "";
// Hide non-editable context
document.getElementById('re-info-grid').style.display = 'none';
// Show input fields for new entry
document.getElementById('re-cat-group').style.display = 'block';
document.getElementById('re-sub-group').style.display = 'block';
document.getElementById('ret-expense-edit-view').style.display = 'flex';
}
function saveRetExpense() {
const row = document.getElementById('re-row').value;
const amt = document.getElementById('re-amount').value.replace(/,/g, '');
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Updating Expense...";
if (row) {
google.script.run.withSuccessHandler(() => {
closeOverlay('ret-expense-edit-view');
openRetExpenseDetails();
}).db_saveRetirementExpense(row, amt);
} else {
const form = {
category: document.getElementById('re-category').value,
subCategory: document.getElementById('re-subcategory').value,
amount: amt
};
google.script.run.withSuccessHandler(() => {
closeOverlay('ret-expense-edit-view');
openRetExpenseDetails();
}).db_addRetirementExpense(form);
}
}
function closeRetExpenseDetails() {
closeOverlay('ret-expense-detail-view');
document.getElementById('loading-overlay').style.display = 'flex';
google.script.run.withSuccessHandler(renderRetirementData).db_getRetirementAssets();
}
function playPageAnimation(viewId) {
const container = document.getElementById(viewId);
const animatedElements = container.querySelectorAll('.animate-slide-in');
animatedElements.forEach(el => {
el.style.animation = 'none';
el.offsetHeight; // This "magic" line forces the browser to notice the change
el.style.animation = '';
});
}
function updateCurrentDate() {
const dateEl = document.getElementById('main-date-display');
if(!dateEl) return;
const now = new Date();
const options = { weekday: 'long', month: 'long', day: 'numeric' };
dateEl.innerText = now.toLocaleDateString('en-US', options).toUpperCase();
}
/**
* Universal Swipe Handler
* @param {HTMLElement} element - The row content to move
* @param {number} snapWidth - The negative pixel value to snap to (e.g. -140 or -80)
*/
function attachSwipe(element, snapWidth) {
let startX = 0;
let currentX = 0;
let isSwiping = false;
let isOpen = false;
// Dynamic threshold: drag 40% of the width to trigger the snap
const threshold = snapWidth * 0.4;
element.addEventListener('touchstart', (e) => {
// 1. GLOBAL CLOSER: If another row is open, close it instantly
if (activeSwipe && activeSwipe.element !== element) {
activeSwipe.close();
}
startX = e.touches[0].clientX;
isSwiping = true;
element.style.transition = 'none';
}, {passive: true});
element.addEventListener('touchmove', (e) => {
if (!isSwiping) return;
const x = e.touches[0].clientX;
let diff = x - startX;
if (isOpen) diff += snapWidth;
if (diff > 0) diff = 0;
// Allow slight overscroll (rubber band) up to 60px past snap width
const limit = snapWidth - 60;
if (diff < limit) diff = limit;
currentX = diff;
element.style.transform = `translateX(${currentX}px)`;
}, {passive: true});
element.addEventListener('touchend', (e) => {
isSwiping = false;
element.style.transition = 'transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1)';
// Logic: snapWidth is negative (e.g. -140).
// If currentX is LESS than threshold (e.g. -60), we dragged far enough left.
if (currentX < threshold) {
// SNAP OPEN
element.style.transform = `translateX(${snapWidth}px)`;
isOpen = true;
activeSwipe = {
element: element,
close: () => {
element.style.transition = 'transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1)';
element.style.transform = `translateX(0)`;
isOpen = false;
activeSwipe = null;
}
};
} else {
// SNAP CLOSED
element.style.transform = `translateX(0)`;
isOpen = false;
if (activeSwipe && activeSwipe.element === element) {
activeSwipe = null;
}
}
});
element.addEventListener('click', (e) => {
if(isOpen) {
e.stopPropagation();
element.style.transform = `translateX(0)`;
isOpen = false;
activeSwipe = null;
}
});
}
function openRatePopup() {
const overlay = document.getElementById('rate-overlay');
const card = overlay.querySelector('.rate-card');
document.getElementById('disp-dbs-rate').innerHTML = '<span style="font-size:12px; color:#aeaeb2; font-weight:500;">Loading...</span>';
document.getElementById('disp-uob-rate').innerHTML = '<span style="font-size:12px; color:#aeaeb2; font-weight:500;">Loading...</span>';
overlay.style.display = 'flex';
void overlay.offsetWidth;
overlay.style.opacity = '1';
card.style.transform = 'scale(1) translateY(0)';
google.script.run
.withSuccessHandler(function(data) {
const el = document.getElementById('disp-uob-rate');
if (data.success) {
el.innerHTML = `${data.rate} <span>PHP</span>`;
el.style.transition = 'color 0.3s';
el.style.color = '#10b981';
setTimeout(() => el.style.color = '#1c1c1e', 600);
} else {
el.innerHTML = `<span style="font-size:11px; color:#ff3b30;">${data.error}</span>`;
}
})
.withFailureHandler(function(err) {
document.getElementById('disp-uob-rate').innerHTML = `<span style="font-size:11px; color:#ff3b30;">Net Err</span>`;
})
.db_fetchUOB();
google.script.run
.withSuccessHandler(function(data) {
const el = document.getElementById('disp-dbs-rate');
if (data.success) {
el.innerHTML = `${data.rate} <span>PHP</span>`;
el.style.transition = 'color 0.3s';
el.style.color = '#10b981';
setTimeout(() => el.style.color = '#1c1c1e', 600);
} else {
el.innerHTML = `<span style="font-size:11px; color:#ff3b30;">${data.error}</span>`;
}
})
.withFailureHandler(function(err) {
document.getElementById('disp-dbs-rate').innerHTML = `<span style="font-size:11px; color:#ff3b30;">Net Err</span>`;
})
.db_fetchDBS();
}
function closeRatePopup() {
const overlay = document.getElementById('rate-overlay');
const card = overlay.querySelector('.rate-card');
overlay.style.opacity = '0';
card.style.transform = 'scale(0.92) translateY(10px)';
setTimeout(() => {
overlay.style.display = 'none';
}, 300);
}
// --- BACKGROUND RATE FETCHER ---
function initMiniRates() {
// Fetch DBS
google.script.run
.withSuccessHandler(function(data) {
const pill = document.getElementById('pill-dbs');
const val = document.getElementById('mini-dbs-val');
if (data.success) {
val.innerText = data.rate;
pill.classList.add('loaded'); // Triggers the fade-in animation
} else {
val.innerText = "N/A";
val.style.color = "#cbd5e1"; // Greyed out
pill.classList.add('loaded');
}
})
.db_fetchDBS();
// Fetch UOB
google.script.run
.withSuccessHandler(function(data) {
const pill = document.getElementById('pill-uob');
const val = document.getElementById('mini-uob-val');
if (data.success) {
val.innerText = data.rate;
pill.classList.add('loaded'); // Triggers the fade-in animation
} else {
val.innerText = "N/A";
val.style.color = "#cbd5e1";
pill.classList.add('loaded');
}
})
.db_fetchUOB();
}
// --- INDIVIDUAL RATE REFRESHER ---
function refreshRate(bank) {
const pillId = bank === 'DBS' ? 'pill-dbs' : 'pill-uob';
const valId = bank === 'DBS' ? 'mini-dbs-val' : 'mini-uob-val';
const pill = document.getElementById(pillId);
const valEl = document.getElementById(valId);
// 1. Visual Feedback (Haptic & Animation)
if (navigator.vibrate) navigator.vibrate(10);
pill.classList.add('refreshing'); // Starts pulsing text
// 2. Define Handler
const handleSuccess = (data) => {
pill.classList.remove('refreshing');
if (data.success) {
valEl.innerText = data.rate;
// Flash Green to indicate "Fresh Data"
valEl.style.transition = 'color 0.3s';
valEl.style.color = '#10b981';
setTimeout(() => valEl.style.color = '#1e293b', 600);
} else {
valEl.innerText = "N/A";
valEl.style.color = "#cbd5e1";
}
};
// 3. Call Specific Backend Function
if (bank === 'DBS') {
google.script.run
.withSuccessHandler(handleSuccess)
.withFailureHandler(() => {
pill.classList.remove('refreshing');
valEl.innerText = "Err";
valEl.style.color = "#ef4444";
})
.db_fetchDBS();
} else {
google.script.run
.withSuccessHandler(handleSuccess)
.withFailureHandler(() => {
pill.classList.remove('refreshing');
valEl.innerText = "Err";
valEl.style.color = "#ef4444";
})
.db_fetchUOB();
}
}
function silentRefreshDashboard() {
const activeCards = document.querySelectorAll('.dashboard-wrapper .card, .dashboard-wrapper .stacked-card.card-front');
const backCards = document.querySelectorAll('.dashboard-wrapper .stacked-card:not(.card-front)');
const icon = document.querySelector('.footer-refresh-icon');
if(navigator.vibrate) navigator.vibrate(10);
icon.style.transition = 'transform 0.5s ease';
icon.style.transform = 'rotate(360deg)';
setTimeout(() => { icon.style.transition = 'none'; icon.style.transform = 'rotate(0deg)'; }, 500);
activeCards.forEach(c => c.classList.add('card-refreshing'));
backCards.forEach(c => {
c.style.transition = 'none';
c.style.opacity = '0';
});
google.script.run
.withSuccessHandler(function(data) {
if (data.success) {
globalData = data;
invDataGlobal = data.investment;
drawChart('chartBudget', data.budget.current, data.budget.target, true);
drawChart('chartRetire', data.retirement.current, data.retirement.target, false, '#D4AF37');
drawChart('chartEmergency', data.emergency.current, data.emergency.target, false, '#F59E0B');
drawSegmentedChart('chartSavings', data.savings.segments, data.savings.colors);
updateTextDisplay();
document.getElementById('pctSavings').innerHTML = data.savings.activePct + '<span class="small-pct">%</span>';
document.getElementById('curSavings').innerText = data.savings.activeName;
showToast("Dashboard Updated");
} else {
showToast("Refresh Failed");
}
activeCards.forEach(c => c.classList.remove('card-refreshing'));
backCards.forEach(c => {
c.style.transition = '';
c.style.opacity = '';
});
})
.withFailureHandler(function(e) {
console.error(e);
showToast("Connection Error");
activeCards.forEach(c => c.classList.remove('card-refreshing'));
backCards.forEach(c => {
c.style.transition = '';
c.style.opacity = '';
});
})
.db_getFinancialData();
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment