Skip to content

Instantly share code, notes, and snippets.

@ey-ron
Created January 2, 2026 15:13
Show Gist options
  • Select an option

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

Select an option

Save ey-ron/70aa103a40c474efc11f43d2da0c2256 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<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;
}
.app-container {
height: 100%;
width: 100%;
max-width: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
position: relative;
}
/* --- 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 {
flex-shrink: 0;
padding: 40px 20px 10px 20px;
text-align: center;
background-color: #f4f6f8;
}
.header h1 {
margin: 0;
font-size: 28px;
color: #1e293b;
font-weight: 800;
letter-spacing: -0.5px;
cursor: pointer;
}
.header h1:active { opacity: 0.7; }
.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;
padding: 5px;
cursor: pointer;
}
/* --- 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: 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: 50px; 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; }
.version-footer { flex-shrink: 0; text-align: center; font-size: 10px; color: #cbd5e1; padding: 2px 0 15px 0; font-weight: 500; letter-spacing: 1px; line-height: 1.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); /* Dark Blue/Black */
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);
}
}
.animate-slide-in {
animation: slideInUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
opacity: 0; /* Start hidden */
}
/* --- 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 */
}
</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 class="app-container">
<div id="dashboard-view">
<div class="header">
<h1 onclick="checkPin(openNetWorthView)">Financial Dashboard</h1>
<a id="appLink" onclick="checkPin(togglePrivacy)">Tap to show hidden amounts</a>
</div>
<div class="dashboard-wrapper">
<div class="swipe-wrapper" id="budgetSwipeContainer">
<div class="stacked-card card-front" id="cardBudgetMain" onclick="checkPin(openBudgetView, event)">
<h2>This Month's Budget</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: right;"><span>Total</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>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>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" id="version-footer">v1.3.8 - Final</div>
</div>
<div id="emergency-view">
<div class="header">
<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">
<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">
<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">
<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">
<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">
<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">
<h2>Total Net Worth</h2>
<div class="amount" id="networth-val">--</div>
</div>
<div class="nw-list-card">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<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="scrollable-middle">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<div id="ret-tab-plan-content">
<div class="nw-total-card" style="padding: 12px 20px; margin-bottom: 8px; 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;">
<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;">
<div>
<span style="font-size:10px; color:#94a3b8; text-transform:uppercase; display:block;">Current Monthly Exp.</span>
<input type="text" id="ret-exp-now" disabled style="background:transparent; border:none; padding:0; font-size:15px; font-weight:700; color:#334155; width:100%;">
</div>
<div style="text-align:right;">
<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;">--</strong>
</div>
</div>
<div style="border-top:1px solid #e2e8f0; padding-top:6px; 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="investment-view">
<div class="header">
<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">
<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">
<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">
<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">
<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">
<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 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";
const TRUSTED_DEVICES = [
"dev_ackwzk9xnmjwsyqz9",
"dev_5idkuh27ymjwsyuib",
"dev_s6osa0gjdmjwszst5",
"dev_jjwvo6lj1mjwu6ro9",
"dev_r5bmgp3vfmjwxqrx2"
];
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)); // 10 years
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();
if (TRUSTED_DEVICES.indexOf(myDeviceId) > -1) {
isPinAuthenticated = true;
}
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');
}
document.getElementById('appLink').addEventListener('click', function(e) { e.stopPropagation(); });
window.onload = function() { loadDashboard(); };
let userDeviceName = "Unknown";
function loadDashboard() {
document.getElementById('loading-overlay').style.display = 'flex';
document.getElementById('loading-text').innerText = "Loading Finances...";
const footer = document.getElementById('version-footer');
const subStyle = "font-size: 9px; margin-top: 1px; opacity: 0.8;";
if (isPinAuthenticated) {
footer.innerHTML = `
<div>v1.3.8</div>
<div style="${subStyle} color:#10b981;">Owner Device | Security Bypassed</div>
`;
} else {
footer.innerHTML = `
<div>v1.3.8</div>
<div style="${subStyle}"><span style="user-select:all; color:#3b82f6;">Final Version</span></div>
`;
}
google.script.run.withSuccessHandler(initDashboard).withFailureHandler(showError).db_getFinancialData();
}
function initDashboard(data) {
if (!data.success) { showError(data.error); return; }
globalData = data;
invDataGlobal = data.investment;
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, '#10b981');
drawChart('chartEmergency', data.emergency.current, data.emergency.target, false, '#8b5cf6');
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';
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) {
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) {
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'; // Standard styling
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';
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);
content.style.transform = `translateX(0)`; // Close swipe
};
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>`;
attachDoubleSwipe(content);
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';
const currentAccount = document.getElementById('nw-detail-title').innerText;
const currentCategory = document.getElementById('nw-detail-subtitle').innerText;
openNetWorthDetail(currentAccount, currentCategory);
google.script.run.withSuccessHandler(initDashboard).db_getFinancialData();
}
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...";
google.script.run.withSuccessHandler(function() {
// Refresh the list
const currentAccount = document.getElementById('nw-detail-title').innerText;
const currentCategory = document.getElementById('nw-detail-subtitle').innerText;
openNetWorthDetail(currentAccount, currentCategory);
// REFRESH MAIN DASHBOARD
refreshMainDashboard();
}).withFailureHandler(showError).db_deleteNetWorthItem(row);
}
}
// --- 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';
// Refresh the detail list
openNetWorthDetail(form.categoryVal, form.classVal);
// REFRESH MAIN DASHBOARD
refreshMainDashboard();
document.getElementById('loading-overlay').style.display = 'none';
}).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';
const container = document.getElementById('budget-breakdown-container');
container.innerHTML = '';
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;
const left = item.left;
let leftClass = (typeof left === 'number') ? (left < 0 ? "b-val b-val-neg" : "b-val b-val-left") : "b-val";
// 1. Wrapper (Blue Background)
const wrapper = document.createElement('div');
wrapper.className = 'budget-swipe-wrapper';
// 2. Edit Action (Behind)
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</span>
`;
// CLICK: Open Edit for THIS category
editBtn.onclick = function() {
openCategoryEdit(cat);
// Reset swipe
wrapper.querySelector('.budget-swipe-content').style.transform = `translateX(0)`;
};
// 3. Content (The Row)
const content = document.createElement('div');
content.className = 'budget-swipe-content';
// Render Row HTML
const rowInner = document.createElement('div');
rowInner.className = 'budget-row'; // Keep original styling for inner layout
// IMPORTANT: Remove border-bottom from .budget-row via inline style or CSS to avoid double borders
// if the wrapper handles grouping. For now, we keep it as is.
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);
// 4. Attach Swipe Logic
attachAppleSwipe(content); // Reusing the helper we made earlier!
wrapper.appendChild(editBtn);
wrapper.appendChild(content);
container.appendChild(wrapper);
});
}
// --- 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';
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.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';
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';
// If you came from detail view, you might want to refresh that too,
// but importantly, we refresh the MAIN dashboard now:
refreshMainDashboard();
document.getElementById('loading-overlay').style.display = 'none';
}).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 = "Fetching Information...";
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';
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';
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 = "20px";
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)
attachDoubleSwipe(content);
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...";
// ... inside deleteAsset success handler ...
google.script.run.withSuccessHandler(function() {
if (isInvestment) {
google.script.run.withSuccessHandler(renderInvestmentDetails).db_getInvestmentDetails();
} else {
google.script.run.withSuccessHandler(renderRetirementData).db_getRetirementAssets();
}
// REFRESH MAIN DASHBOARD
refreshMainDashboard();
document.getElementById('loading-overlay').style.display = 'none';
}).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
};
// ... inside saveAssetData ...
google.script.run.withSuccessHandler(function() {
document.getElementById('asset-edit-view').style.display = 'none';
if (isEditingInvestment) {
google.script.run.withSuccessHandler(renderInvestmentDetails).db_getInvestmentDetails();
} else {
google.script.run.withSuccessHandler(renderRetirementData).db_getRetirementAssets();
}
// REFRESH MAIN DASHBOARD
refreshMainDashboard();
document.getElementById('loading-overlay').style.display = 'none';
}).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';
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. FORMATTERS ---
const link = document.getElementById('appLink');
link.innerText = isVisible ? "Tap to hide amounts" : "Tap to show hidden amounts";
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('₱', '₱ ');
// EXISTING: 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('$', '$');
// NEW: 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';
// --- Budget Comparison Data (YOY Card) ---
const bData = globalData.budget;
// Month Comparison
// CHANGED: Use formatCompactMain for the big number
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
// CHANGED: Use formatCompactMain for the big number
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';
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';
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...";
google.script.run.withSuccessHandler(() => {
google.script.run.withSuccessHandler(renderGoalsView).db_getSavingsGoals();
}).db_deleteSavingsGoal(g.name);
} else {
// Close if cancelled
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
attachAppleSwipe(content);
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, ''), // Clean commas
date: date
};
google.script.run.withSuccessHandler(function() {
document.getElementById('add-goal-view').style.display = 'none';
// Refresh the Goals View
google.script.run.withSuccessHandler(renderGoalsView).withFailureHandler(showError).db_getSavingsGoals();
}).withFailureHandler(showError).db_addSavingsGoal(form);
}
// --- APPLE STYLE SWIPE LOGIC ---
function attachAppleSwipe(element) {
let startX = 0;
let currentX = 0;
let isSwiping = false;
let isOpen = false;
const snapWidth = -80; // Width of the delete button
element.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
isSwiping = true;
element.style.transition = 'none'; // Follow finger instantly
}, {passive: true});
element.addEventListener('touchmove', (e) => {
if (!isSwiping) return;
const x = e.touches[0].clientX;
let diff = x - startX;
// If already open, adjust starting point
if (isOpen) diff += snapWidth;
// Constraints: Can't swipe right past 0, can't swipe left past -120 (overshoot)
if (diff > 0) diff = 0;
if (diff < -150) diff = -150; // Rubber band max
currentX = diff;
element.style.transform = `translateX(${currentX}px)`;
}, {passive: true});
element.addEventListener('touchend', (e) => {
isSwiping = false;
element.style.transition = 'transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1)'; // Smooth snap
// Threshold to snap open (halfway)
if (currentX < -40) {
// Snap Open
element.style.transform = `translateX(${snapWidth}px)`;
isOpen = true;
} else {
// Snap Closed
element.style.transform = `translateX(0)`;
isOpen = false;
}
});
// Auto-close if clicked elsewhere (optional polish)
element.addEventListener('click', () => {
if(isOpen) {
element.style.transform = `translateX(0)`;
isOpen = false;
}
});
}
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 attachDoubleSwipe(element) {
let startX = 0;
let currentX = 0;
let isSwiping = false;
let isOpen = false;
const snapWidth = -140; // Width of 2 buttons (70px + 70px)
element.addEventListener('touchstart', (e) => {
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;
if (diff < -200) diff = -200; // Max overscroll
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)';
// Threshold: if dragged more than 70px (halfway), snap open
if (currentX < -70) {
element.style.transform = `translateX(${snapWidth}px)`;
isOpen = true;
} else {
element.style.transform = `translateX(0)`;
isOpen = false;
}
});
// Auto-close on click
element.addEventListener('click', () => {
if(isOpen) {
element.style.transform = `translateX(0)`;
isOpen = false;
}
});
}
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;
// Basic validation
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,
// Column C: Stock Type (Dropdown)
colC: document.getElementById('stock-c').value,
// Column D: Broker
colD: document.getElementById('stock-d').value,
// Column F: Total Investment SGD
colF: document.getElementById('stock-f').value,
// Column G: Stock Currency
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';
// Refresh Investment List
google.script.run.withSuccessHandler(renderInvestmentDetails).db_getInvestmentDetails();
// REFRESH MAIN DASHBOARD
refreshMainDashboard();
document.getElementById('loading-overlay').style.display = 'none';
}).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";
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment