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