Created
January 2, 2026 15:13
-
-
Save ey-ron/70aa103a40c474efc11f43d2da0c2256 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 →</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