| 1 | <!DOCTYPE html>
|
|---|
| 2 | <html lang="en">
|
|---|
| 3 | <head>
|
|---|
| 4 | <meta charset="UTF-8" />
|
|---|
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|---|
| 6 | <title>Wedding Planner</title>
|
|---|
| 7 | <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,600&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"/>
|
|---|
| 8 | <style>
|
|---|
| 9 | :root {
|
|---|
| 10 | --cream:#fdf8f3; --cream-2:#f9f1e8; --cream-3:#f2e8db;
|
|---|
| 11 | --blush-light:#fce8e8; --blush:#f4b8b8; --blush-mid:#e8949c;
|
|---|
| 12 | --rose:#d4606a; --rose-deep:#b84550;
|
|---|
| 13 | --gold-light:#f7e6b0; --gold:#c9a84c; --gold-dark:#9e7a28;
|
|---|
| 14 | --text-dark:#2c1f1f; --text-mid:#6b4f4f; --text-light:#a08080;
|
|---|
| 15 | --border:#ecddd0; --border-light:#f4ebe2; --white:#ffffff;
|
|---|
| 16 | --green-light:#e4f5ec; --green:#3a9e6b;
|
|---|
| 17 | --amber-light:#fff4e0; --amber:#c87f2a;
|
|---|
| 18 | --red-light:#fdeaea; --red:#c94545;
|
|---|
| 19 | --purple-light:#f0eaff; --purple:#7c55c8;
|
|---|
| 20 | --sidebar-w:248px; --header-h:64px;
|
|---|
| 21 | --radius-sm:6px; --radius-md:10px; --radius-lg:16px; --radius-xl:22px;
|
|---|
| 22 | --shadow-xs:0 1px 3px rgba(44,31,31,.06),0 1px 2px rgba(44,31,31,.04);
|
|---|
| 23 | --shadow-sm:0 2px 8px rgba(44,31,31,.08),0 1px 3px rgba(44,31,31,.05);
|
|---|
| 24 | --shadow-md:0 6px 20px rgba(44,31,31,.10),0 2px 6px rgba(44,31,31,.06);
|
|---|
| 25 | --shadow-lg:0 16px 40px rgba(44,31,31,.14),0 4px 12px rgba(44,31,31,.08);
|
|---|
| 26 | --transition:200ms cubic-bezier(.4,0,.2,1);
|
|---|
| 27 | }
|
|---|
| 28 | *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
|---|
| 29 | html,body{height:100%;}
|
|---|
| 30 | body{font-family:'DM Sans',sans-serif;background:var(--cream);color:var(--text-dark);display:flex;overflow:hidden;-webkit-font-smoothing:antialiased;}
|
|---|
| 31 | ::-webkit-scrollbar{width:6px;height:6px;}
|
|---|
| 32 | ::-webkit-scrollbar-track{background:transparent;}
|
|---|
| 33 | ::-webkit-scrollbar-thumb{background:var(--blush);border-radius:99px;}
|
|---|
| 34 |
|
|---|
| 35 | /* ── Sidebar ── */
|
|---|
| 36 | .sidebar{width:var(--sidebar-w);min-height:100vh;background:#fff;border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;left:0;top:0;bottom:0;z-index:100;overflow-y:auto;overflow-x:hidden;box-shadow:var(--shadow-sm);}
|
|---|
| 37 | .sidebar-header{padding:22px 18px 18px;border-bottom:1px solid var(--border-light);display:flex;align-items:center;gap:10px;flex-shrink:0;}
|
|---|
| 38 | .logo-mark{width:36px;height:36px;flex-shrink:0;background:linear-gradient(135deg,var(--rose),var(--blush-mid));border-radius:9px;display:flex;align-items:center;justify-content:center;box-shadow:0 3px 12px rgba(212,96,106,.35);}
|
|---|
| 39 | .logo-mark svg{width:18px;height:18px;}
|
|---|
| 40 | .logo-text{display:flex;flex-direction:column;gap:1px;}
|
|---|
| 41 | .logo-text .brand{font-family:'Cormorant Garamond',serif;font-size:17px;font-weight:700;color:var(--text-dark);line-height:1.1;}
|
|---|
| 42 | .logo-text .sub{font-size:9px;font-weight:600;color:var(--text-light);text-transform:uppercase;letter-spacing:.14em;}
|
|---|
| 43 | .sidebar-section-label{font-size:9.5px;font-weight:700;color:var(--text-light);text-transform:uppercase;letter-spacing:.1em;padding:20px 20px 6px;}
|
|---|
| 44 | .nav-item{display:flex;align-items:center;gap:9px;padding:9px 14px;margin:1px 8px;border-radius:var(--radius-md);cursor:pointer;font-size:13.5px;font-weight:450;color:var(--text-mid);transition:background var(--transition),color var(--transition);text-decoration:none;white-space:nowrap;}
|
|---|
| 45 | .nav-item svg{width:16px;height:16px;flex-shrink:0;opacity:.65;transition:opacity var(--transition);}
|
|---|
| 46 | .nav-item:hover{background:var(--cream-2);color:var(--text-dark);}
|
|---|
| 47 | .nav-item:hover svg{opacity:1;}
|
|---|
| 48 | .nav-item.active{background:var(--blush-light);color:var(--rose);font-weight:600;}
|
|---|
| 49 | .nav-item.active svg{opacity:1;}
|
|---|
| 50 | .nav-badge{margin-left:auto;background:var(--blush-light);color:var(--rose);font-size:10px;font-weight:700;padding:2px 8px;border-radius:20px;flex-shrink:0;}
|
|---|
| 51 | .sidebar-footer{margin-top:auto;padding:14px 12px;border-top:1px solid var(--border-light);flex-shrink:0;}
|
|---|
| 52 | .user-card{display:flex;align-items:center;gap:10px;padding:10px;border-radius:var(--radius-md);cursor:pointer;transition:background var(--transition);}
|
|---|
| 53 | .user-card:hover{background:var(--cream-2);}
|
|---|
| 54 | .user-avatar{width:34px;height:34px;border-radius:50%;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;background:linear-gradient(135deg,var(--blush-mid),var(--rose));color:white;font-weight:700;font-size:12px;}
|
|---|
| 55 | .user-info{display:flex;flex-direction:column;gap:1px;overflow:hidden;}
|
|---|
| 56 | .user-name{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|---|
| 57 | .user-role{font-size:11px;color:var(--text-light);}
|
|---|
| 58 |
|
|---|
| 59 | /* ── Main ── */
|
|---|
| 60 | .main{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden;}
|
|---|
| 61 | .topbar{height:var(--header-h);background:#fff;border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 28px;gap:12px;position:sticky;top:0;z-index:50;flex-shrink:0;}
|
|---|
| 62 | .breadcrumb{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-light);}
|
|---|
| 63 | .breadcrumb .bc-root{font-family:'Cormorant Garamond',serif;font-size:15px;font-weight:600;color:var(--text-mid);}
|
|---|
| 64 | .breadcrumb .bc-active{color:var(--text-dark);font-weight:600;}
|
|---|
| 65 | .breadcrumb-sep{width:4px;height:4px;background:var(--border);border-radius:50%;flex-shrink:0;}
|
|---|
| 66 | .topbar-actions{margin-left:auto;display:flex;align-items:center;gap:8px;}
|
|---|
| 67 | .icon-btn{position:relative;width:36px;height:36px;border-radius:var(--radius-md);border:1px solid var(--border);background:#fff;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background var(--transition),border-color var(--transition);}
|
|---|
| 68 | .icon-btn:hover{background:var(--cream-2);border-color:var(--blush);}
|
|---|
| 69 | .icon-btn svg{width:15px;height:15px;stroke:var(--text-mid);}
|
|---|
| 70 | .page-content{padding:28px;overflow-y:auto;height:calc(100vh - var(--header-h));}
|
|---|
| 71 |
|
|---|
| 72 | /* ── Buttons ── */
|
|---|
| 73 | .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 16px;border-radius:var(--radius-md);font-family:'DM Sans',sans-serif;font-size:13px;font-weight:500;border:1px solid var(--border);background:#fff;color:var(--text-dark);cursor:pointer;white-space:nowrap;transition:background var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);text-decoration:none;line-height:1.4;}
|
|---|
| 74 | .btn:hover{background:var(--cream-2);border-color:var(--blush);}
|
|---|
| 75 | .btn:active{transform:translateY(1px);}
|
|---|
| 76 | .btn-primary{background:var(--rose);color:#fff;border-color:var(--rose);box-shadow:0 2px 8px rgba(212,96,106,.30);}
|
|---|
| 77 | .btn-primary:hover{background:var(--rose-deep);border-color:var(--rose-deep);}
|
|---|
| 78 | .btn-gold{background:var(--gold);color:#fff;border-color:var(--gold);}
|
|---|
| 79 | .btn-gold:hover{background:var(--gold-dark);border-color:var(--gold-dark);}
|
|---|
| 80 | .btn-ghost{background:transparent;color:var(--text-mid);border-color:transparent;}
|
|---|
| 81 | .btn-ghost:hover{background:var(--cream-2);border-color:var(--border);}
|
|---|
| 82 | .btn-danger{background:var(--red-light);color:var(--red);border-color:#f5c2c2;}
|
|---|
| 83 | .btn-danger:hover{background:#fbd5d5;border-color:var(--red);}
|
|---|
| 84 | .btn-sm{padding:5px 10px;font-size:12px;border-radius:var(--radius-sm);}
|
|---|
| 85 | .btn-icon{padding:6px;border-radius:var(--radius-sm);}
|
|---|
| 86 |
|
|---|
| 87 | /* ── Cards ── */
|
|---|
| 88 | .card{background:#fff;border-radius:var(--radius-xl);box-shadow:var(--shadow-md);border:1px solid var(--border-light);padding:22px;}
|
|---|
| 89 | .card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px;}
|
|---|
| 90 | .card-header h3{font-family:'Cormorant Garamond',serif;font-size:20px;font-weight:700;color:var(--text-dark);}
|
|---|
| 91 | .card-header .card-actions{display:flex;gap:8px;align-items:center;}
|
|---|
| 92 | h1,h2,h3,h4{margin:0;}
|
|---|
| 93 | .muted{color:var(--text-light);font-size:13px;}
|
|---|
| 94 | .serif{font-family:'Cormorant Garamond',serif;}
|
|---|
| 95 |
|
|---|
| 96 | /* ── Dashboard grid ── */
|
|---|
| 97 | .dash-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;}
|
|---|
| 98 | .dash-full{grid-column:1/-1;}
|
|---|
| 99 |
|
|---|
| 100 | /* ── Stats ── */
|
|---|
| 101 | .stats-row{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:20px;}
|
|---|
| 102 | .stat-card{background:#fff;border:1px solid var(--border-light);border-radius:var(--radius-lg);padding:16px 14px;display:flex;flex-direction:column;gap:5px;box-shadow:var(--shadow-xs);}
|
|---|
| 103 | .stat-icon{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;margin-bottom:4px;}
|
|---|
| 104 | .stat-icon svg{width:15px;height:15px;}
|
|---|
| 105 | .stat-label{font-size:10.5px;font-weight:700;color:var(--text-light);text-transform:uppercase;letter-spacing:.07em;}
|
|---|
| 106 | .stat-value{font-family:'Cormorant Garamond',serif;font-size:28px;font-weight:700;line-height:1;}
|
|---|
| 107 | .stat-sub{font-size:11px;color:var(--text-light);}
|
|---|
| 108 | .stat-rose .stat-icon{background:var(--blush-light);}.stat-rose .stat-icon svg{stroke:var(--rose);}.stat-rose .stat-value{color:var(--rose);}
|
|---|
| 109 | .stat-gold .stat-icon{background:var(--gold-light);}.stat-gold .stat-icon svg{stroke:var(--gold-dark);}.stat-gold .stat-value{color:var(--gold-dark);}
|
|---|
| 110 | .stat-green .stat-icon{background:var(--green-light);}.stat-green .stat-icon svg{stroke:var(--green);}.stat-green .stat-value{color:var(--green);}
|
|---|
| 111 | .stat-amber .stat-icon{background:var(--amber-light);}.stat-amber .stat-icon svg{stroke:var(--amber);}.stat-amber .stat-value{color:var(--amber);}
|
|---|
| 112 | .stat-purple .stat-icon{background:var(--purple-light);}.stat-purple .stat-icon svg{stroke:var(--purple);}.stat-purple .stat-value{color:var(--purple);}
|
|---|
| 113 |
|
|---|
| 114 | /* ── Badges ── */
|
|---|
| 115 | .badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:999px;font-weight:600;font-size:11px;line-height:1.6;}
|
|---|
| 116 | .badge-dot{width:6px;height:6px;border-radius:50%;}
|
|---|
| 117 | .badge-rose{background:var(--blush-light);color:var(--rose-deep);}
|
|---|
| 118 | .badge-gold{background:var(--gold-light);color:var(--gold-dark);}
|
|---|
| 119 | .badge-success{background:var(--green-light);color:#1e7a4f;}
|
|---|
| 120 | .badge-warn{background:var(--amber-light);color:#7a4e10;}
|
|---|
| 121 | .badge-danger{background:var(--red-light);color:#8b1f1f;}
|
|---|
| 122 | .badge-info{background:#e6f0ff;color:#1a4fa0;}
|
|---|
| 123 | .badge-purple{background:var(--purple-light);color:#4a2d9e;}
|
|---|
| 124 |
|
|---|
| 125 | /* ── Gold rule ── */
|
|---|
| 126 | .gold-rule{width:36px;height:2px;background:linear-gradient(90deg,var(--gold),transparent);border-radius:2px;margin:10px 0 14px;}
|
|---|
| 127 |
|
|---|
| 128 | /* ── Table ── */
|
|---|
| 129 | .table-card{background:#fff;border:1px solid var(--border-light);border-radius:var(--radius-xl);box-shadow:var(--shadow-xs);overflow:hidden;}
|
|---|
| 130 | .table-toolbar{padding:14px 18px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--border-light);flex-wrap:wrap;}
|
|---|
| 131 | .search-wrap{position:relative;flex:1;min-width:200px;}
|
|---|
| 132 | .search-wrap input{padding-left:32px;background:#fff;}
|
|---|
| 133 | .search-wrap svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:14px;height:14px;stroke:var(--text-light);}
|
|---|
| 134 | .table-wrap{overflow-x:auto;}
|
|---|
| 135 | table{width:100%;border-collapse:collapse;font-size:13.5px;}
|
|---|
| 136 | thead{background:var(--cream);border-bottom:1px solid var(--border-light);}
|
|---|
| 137 | th{padding:11px 14px;text-align:left;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--text-light);white-space:nowrap;cursor:default;}
|
|---|
| 138 | th.sortable{cursor:pointer;user-select:none;}
|
|---|
| 139 | th.sortable:hover{color:var(--text-dark);}
|
|---|
| 140 | .sort-arrow{margin-left:4px;opacity:.5;font-size:10px;}
|
|---|
| 141 | .sort-arrow.asc,.sort-arrow.desc{opacity:1;color:var(--rose);}
|
|---|
| 142 | td{padding:11px 14px;border-bottom:1px solid var(--border-light);vertical-align:middle;}
|
|---|
| 143 | tr:last-child td{border-bottom:none;}
|
|---|
| 144 | tr.row-even td{background:var(--cream);}
|
|---|
| 145 | tr:hover td{background:var(--blush-light);}
|
|---|
| 146 | .td-actions{display:flex;gap:6px;align-items:center;}
|
|---|
| 147 | .pagination-bar{display:flex;justify-content:space-between;align-items:center;padding:10px 18px;border-top:1px solid var(--border-light);font-size:12px;color:var(--text-light);}
|
|---|
| 148 | .pag-controls{display:flex;gap:4px;}
|
|---|
| 149 | .pag-btn{padding:4px 9px;border-radius:var(--radius-sm);border:1px solid var(--border);background:#fff;font-size:12px;cursor:pointer;}
|
|---|
| 150 | .pag-btn:hover:not(:disabled){background:var(--cream-2);}
|
|---|
| 151 | .pag-btn.active{background:var(--rose);color:#fff;border-color:var(--rose);}
|
|---|
| 152 | .pag-btn:disabled{opacity:.4;cursor:default;}
|
|---|
| 153 |
|
|---|
| 154 | /* ── Forms ── */
|
|---|
| 155 | .form-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
|
|---|
| 156 | .form-group{display:flex;flex-direction:column;gap:5px;}
|
|---|
| 157 | .form-label{font-size:11.5px;font-weight:600;color:var(--text-mid);text-transform:uppercase;letter-spacing:.06em;}
|
|---|
| 158 | input,select,textarea{width:100%;padding:9px 12px;border-radius:var(--radius-md);border:1px solid var(--border);background:var(--cream);font-family:'DM Sans',sans-serif;font-size:13.5px;color:var(--text-dark);outline:none;transition:border-color var(--transition),background var(--transition);}
|
|---|
| 159 | input:focus,select:focus,textarea:focus{border-color:var(--blush-mid);background:#fff;}
|
|---|
| 160 |
|
|---|
| 161 | /* ── Info list ── */
|
|---|
| 162 | .info-list{display:flex;flex-direction:column;gap:10px;}
|
|---|
| 163 | .info-row{display:flex;justify-content:space-between;align-items:center;}
|
|---|
| 164 | .info-row .info-label{font-size:12px;color:var(--text-light);font-weight:500;}
|
|---|
| 165 | .info-row .info-val{font-size:13.5px;font-weight:600;color:var(--text-dark);}
|
|---|
| 166 |
|
|---|
| 167 | /* ── Toast ── */
|
|---|
| 168 | .toast{position:fixed;bottom:24px;right:24px;z-index:9999;background:var(--text-dark);color:#fff;padding:12px 20px;border-radius:var(--radius-lg);font-size:13.5px;font-weight:500;box-shadow:var(--shadow-lg);display:flex;align-items:center;gap:10px;transform:translateY(10px);opacity:0;transition:transform .25s ease,opacity .25s ease;pointer-events:none;}
|
|---|
| 169 | .toast.show{transform:translateY(0);opacity:1;}
|
|---|
| 170 | .toast.toast-success{background:var(--green);}
|
|---|
| 171 | .toast.toast-danger{background:var(--red);}
|
|---|
| 172 |
|
|---|
| 173 | /* ── No data ── */
|
|---|
| 174 | .no-data-row td{text-align:center;padding:40px 20px;color:var(--text-light);font-style:italic;}
|
|---|
| 175 |
|
|---|
| 176 | /* ── Modal ── */
|
|---|
| 177 | .modal-backdrop{display:none;position:fixed;inset:0;background:rgba(44,31,31,.45);z-index:1000;align-items:center;justify-content:center;padding:20px;}
|
|---|
| 178 | .modal-backdrop.open{display:flex;}
|
|---|
| 179 | .modal{background:#fff;border-radius:var(--radius-xl);box-shadow:var(--shadow-lg);width:100%;max-width:520px;max-height:90vh;overflow-y:auto;}
|
|---|
| 180 | .modal-header{display:flex;justify-content:space-between;align-items:center;padding:20px 24px 0;}
|
|---|
| 181 | .modal-header strong{font-family:'Cormorant Garamond',serif;font-size:20px;font-weight:700;}
|
|---|
| 182 | .modal-body{padding:20px 24px;}
|
|---|
| 183 | .modal-actions{display:flex;justify-content:flex-end;gap:8px;padding:0 24px 20px;}
|
|---|
| 184 |
|
|---|
| 185 | /* ── Seating board ── */
|
|---|
| 186 | .seating-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-top:16px;}
|
|---|
| 187 | .table-card-seat{background:var(--cream);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px;transition:box-shadow var(--transition);}
|
|---|
| 188 | .table-card-seat:hover{box-shadow:var(--shadow-sm);}
|
|---|
| 189 | .table-card-seat .table-title{font-family:'Cormorant Garamond',serif;font-size:16px;font-weight:700;color:var(--rose);margin-bottom:10px;display:flex;align-items:center;gap:6px;}
|
|---|
| 190 | .table-card-seat .seat-guest{display:flex;align-items:center;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border-light);font-size:13px;}
|
|---|
| 191 | .table-card-seat .seat-guest:last-child{border-bottom:none;}
|
|---|
| 192 | .table-card-seat .seat-role{font-size:11px;color:var(--text-light);}
|
|---|
| 193 | .unassigned-list{margin-top:12px;display:flex;flex-direction:column;gap:6px;}
|
|---|
| 194 | .unassigned-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#fff;border:1px solid var(--border-light);border-radius:var(--radius-md);font-size:13px;}
|
|---|
| 195 |
|
|---|
| 196 | /* ── Responsive ── */
|
|---|
| 197 | @media(max-width:1100px){.stats-row{grid-template-columns:repeat(3,1fr);}}
|
|---|
| 198 | @media(max-width:900px){.dash-grid{grid-template-columns:1fr;}.dash-full{grid-column:1;}}
|
|---|
| 199 | @media(max-width:768px){.sidebar{width:60px;}.sidebar .logo-text,.sidebar .nav-item span:not(.nav-badge),.sidebar .sidebar-section-label,.sidebar .user-info{display:none;}.main{margin-left:60px;}.stats-row{grid-template-columns:1fr 1fr;}}
|
|---|
| 200 | @media(max-width:540px){.page-content{padding:14px;}.modal-body .form-grid{grid-template-columns:1fr;}}
|
|---|
| 201 | </style>
|
|---|
| 202 | </head>
|
|---|
| 203 | <body>
|
|---|
| 204 |
|
|---|
| 205 | <!-- SIDEBAR -->
|
|---|
| 206 | <aside class="sidebar" id="sidebar">
|
|---|
| 207 | <div class="sidebar-header">
|
|---|
| 208 | <div class="logo-mark">
|
|---|
| 209 | <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|---|
| 210 | <path d="M12 21.593c-5.63-5.539-11-10.297-11-14.402C1 3.402 4.248 1 7.5 1 9.849 1 11.271 2.007 12 3.011 12.729 2.007 14.151 1 16.5 1 19.752 1 23 3.402 23 7.191c0 4.105-5.37 8.863-11 14.402z"/>
|
|---|
| 211 | </svg>
|
|---|
| 212 | </div>
|
|---|
| 213 | <div class="logo-text">
|
|---|
| 214 | <span class="brand">Wedding Planner</span>
|
|---|
| 215 | <span class="sub">Plan Your Perfect Day</span>
|
|---|
| 216 | </div>
|
|---|
| 217 | </div>
|
|---|
| 218 |
|
|---|
| 219 | <div class="sidebar-section-label">Main</div>
|
|---|
| 220 | <a class="nav-item active" href="#/dashboard" data-page="dashboard">
|
|---|
| 221 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
|---|
| 222 | <span>Dashboard</span>
|
|---|
| 223 | </a>
|
|---|
| 224 | <a class="nav-item" href="#/event" data-page="event">
|
|---|
| 225 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
|---|
| 226 | <span>Events</span>
|
|---|
| 227 | </a>
|
|---|
| 228 | <a class="nav-item" href="#/guests" data-page="guests">
|
|---|
| 229 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|---|
| 230 | <span>Guests</span>
|
|---|
| 231 | <span class="nav-badge" id="badge-guests">0</span>
|
|---|
| 232 | </a>
|
|---|
| 233 | <a class="nav-item" href="#/seating" data-page="seating">
|
|---|
| 234 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/><line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/></svg>
|
|---|
| 235 | <span>Seating</span>
|
|---|
| 236 | </a>
|
|---|
| 237 | <a class="nav-item" href="#/venue" data-page="venue">
|
|---|
| 238 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
|---|
| 239 | <span>Venue</span>
|
|---|
| 240 | </a>
|
|---|
| 241 | <a class="nav-item" href="#/band" data-page="band">
|
|---|
| 242 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
|---|
| 243 | <span>Band</span>
|
|---|
| 244 | </a>
|
|---|
| 245 | <a class="nav-item" href="#/photographer" data-page="photographer">
|
|---|
| 246 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
|
|---|
| 247 | <span>Photographer</span>
|
|---|
| 248 | </a>
|
|---|
| 249 | <a class="nav-item" href="#/church" data-page="church">
|
|---|
| 250 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="2" x2="12" y2="8"/><line x1="9" y1="5" x2="15" y2="5"/><path d="M4 10l8 4 8-4"/><path d="M4 10v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/></svg>
|
|---|
| 251 | <span>Church</span>
|
|---|
| 252 | </a>
|
|---|
| 253 | <a class="nav-item" href="#/registrar" data-page="registrar">
|
|---|
| 254 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
|---|
| 255 | <span>Registrar</span>
|
|---|
| 256 | </a>
|
|---|
| 257 |
|
|---|
| 258 | <div class="sidebar-section-label">Account</div>
|
|---|
| 259 | <a class="nav-item" href="#/settings" data-page="settings">
|
|---|
| 260 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|---|
| 261 | <span>Settings</span>
|
|---|
| 262 | </a>
|
|---|
| 263 | <a class="nav-item" href="#" id="btnLogout" style="color:var(--red);">
|
|---|
| 264 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
|---|
| 265 | <span>Logout</span>
|
|---|
| 266 | </a>
|
|---|
| 267 |
|
|---|
| 268 | <div class="sidebar-footer">
|
|---|
| 269 | <div class="user-card">
|
|---|
| 270 | <div class="user-avatar" id="userAvatar">??</div>
|
|---|
| 271 | <div class="user-info">
|
|---|
| 272 | <div class="user-name" id="userName">Loading...</div>
|
|---|
| 273 | <div class="user-role">Wedding Owner</div>
|
|---|
| 274 | </div>
|
|---|
| 275 | </div>
|
|---|
| 276 | </div>
|
|---|
| 277 | </aside>
|
|---|
| 278 |
|
|---|
| 279 | <!-- MAIN -->
|
|---|
| 280 | <div class="main" id="app">
|
|---|
| 281 | <header class="topbar">
|
|---|
| 282 | <div class="breadcrumb" id="breadcrumb">
|
|---|
| 283 | <span class="bc-root">Wedding Planner</span>
|
|---|
| 284 | <div class="breadcrumb-sep"></div>
|
|---|
| 285 | <span id="breadcrumb-page" class="bc-active">Dashboard</span>
|
|---|
| 286 | </div>
|
|---|
| 287 | <div class="topbar-actions">
|
|---|
| 288 | <button class="icon-btn" title="Search" onclick="focusSearch()">
|
|---|
| 289 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|---|
| 290 | </button>
|
|---|
| 291 | </div>
|
|---|
| 292 | </header>
|
|---|
| 293 | <section class="page-content" id="page-content"></section>
|
|---|
| 294 | </div>
|
|---|
| 295 |
|
|---|
| 296 | <!-- MODAL -->
|
|---|
| 297 | <div class="modal-backdrop" id="modalBackdrop">
|
|---|
| 298 | <div class="modal" id="modal">
|
|---|
| 299 | <div class="modal-header">
|
|---|
| 300 | <strong id="modalTitle">Form</strong>
|
|---|
| 301 | <button class="btn btn-ghost btn-icon" onclick="closeModal()">
|
|---|
| 302 | <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|---|
| 303 | </button>
|
|---|
| 304 | </div>
|
|---|
| 305 | <div class="modal-body" id="modalBody"></div>
|
|---|
| 306 | <div class="modal-actions">
|
|---|
| 307 | <button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
|---|
| 308 | <button class="btn btn-primary" id="modalSubmit">Save</button>
|
|---|
| 309 | </div>
|
|---|
| 310 | </div>
|
|---|
| 311 | </div>
|
|---|
| 312 |
|
|---|
| 313 | <div class="toast" id="toast"></div>
|
|---|
| 314 |
|
|---|
| 315 | <script>
|
|---|
| 316 | // =============================================================
|
|---|
| 317 | // CONFIG
|
|---|
| 318 | // =============================================================
|
|---|
| 319 | const API = 'http://localhost:3000';
|
|---|
| 320 |
|
|---|
| 321 | // =============================================================
|
|---|
| 322 | // APP STATE — loaded from DB, not hardcoded
|
|---|
| 323 | // =============================================================
|
|---|
| 324 | let CURRENT_USER = null;
|
|---|
| 325 | let CURRENT_WEDDING = null;
|
|---|
| 326 | let EVENTS = [];
|
|---|
| 327 | let GUESTS = [];
|
|---|
| 328 | let VENUES = [];
|
|---|
| 329 | let VENUE_BOOKINGS = [];
|
|---|
| 330 | let PHOTOGRAPHERS = [];
|
|---|
| 331 | let PHOTOGRAPHER_BOOKINGS = [];
|
|---|
| 332 | let BANDS = [];
|
|---|
| 333 | let BAND_BOOKINGS = [];
|
|---|
| 334 | let CHURCHES = [];
|
|---|
| 335 | let CHURCH_BOOKINGS = [];
|
|---|
| 336 | let REGISTRARS = [];
|
|---|
| 337 | let REGISTRAR_BOOKINGS = [];
|
|---|
| 338 | let ATTENDANCE = [];
|
|---|
| 339 |
|
|---|
| 340 | // =============================================================
|
|---|
| 341 | // API HELPER
|
|---|
| 342 | // =============================================================
|
|---|
| 343 | async function api(method, path, body) {
|
|---|
| 344 | const opts = {
|
|---|
| 345 | method,
|
|---|
| 346 | credentials: 'include',
|
|---|
| 347 | headers: { 'Content-Type': 'application/json' }
|
|---|
| 348 | };
|
|---|
| 349 | if (body) opts.body = JSON.stringify(body);
|
|---|
| 350 | const res = await fetch(`${API}${path}`, opts);
|
|---|
| 351 | if (res.status === 401) { window.location.href = 'login.html'; return null; }
|
|---|
| 352 | return res;
|
|---|
| 353 | }
|
|---|
| 354 |
|
|---|
| 355 | // =============================================================
|
|---|
| 356 | // BOOTSTRAP — check auth + load all data
|
|---|
| 357 | // =============================================================
|
|---|
| 358 | async function bootstrap() {
|
|---|
| 359 | // 1. Check login
|
|---|
| 360 | const meRes = await fetch(`${API}/api/auth/me`, { credentials: 'include' });
|
|---|
| 361 | if (!meRes.ok) { window.location.href = 'login.html'; return; }
|
|---|
| 362 | CURRENT_USER = (await meRes.json()).user;
|
|---|
| 363 |
|
|---|
| 364 | // Update sidebar user info
|
|---|
| 365 | document.getElementById('userName').textContent = `${CURRENT_USER.first_name} ${CURRENT_USER.last_name}`;
|
|---|
| 366 | document.getElementById('userAvatar').textContent = `${CURRENT_USER.first_name[0]}${CURRENT_USER.last_name[0]}`;
|
|---|
| 367 |
|
|---|
| 368 | // 2. Load wedding
|
|---|
| 369 | const wRes = await api('GET', '/api/weddings');
|
|---|
| 370 | if (wRes && wRes.ok) {
|
|---|
| 371 | const weddings = await wRes.json();
|
|---|
| 372 | CURRENT_WEDDING = weddings[0] || null;
|
|---|
| 373 | }
|
|---|
| 374 |
|
|---|
| 375 | if (!CURRENT_WEDDING) {
|
|---|
| 376 | // No wedding yet — show create wedding prompt
|
|---|
| 377 | if (!location.hash || location.hash === '#/dashboard') {
|
|---|
| 378 | renderApp();
|
|---|
| 379 | return;
|
|---|
| 380 | }
|
|---|
| 381 | }
|
|---|
| 382 |
|
|---|
| 383 | const wid = CURRENT_WEDDING ? CURRENT_WEDDING.wedding_id : null;
|
|---|
| 384 |
|
|---|
| 385 | // 3. Load all data in parallel
|
|---|
| 386 | if (wid) {
|
|---|
| 387 | const [evR, guR, vbR, pbR, bbR, rbR, cbR, attR] = await Promise.all([
|
|---|
| 388 | api('GET', `/api/weddings/${wid}/events`),
|
|---|
| 389 | api('GET', `/api/weddings/${wid}/guests`),
|
|---|
| 390 | api('GET', `/api/weddings/${wid}/venue-bookings`),
|
|---|
| 391 | api('GET', `/api/weddings/${wid}/photographer-bookings`),
|
|---|
| 392 | api('GET', `/api/weddings/${wid}/band-bookings`),
|
|---|
| 393 | api('GET', `/api/weddings/${wid}/registrar-bookings`),
|
|---|
| 394 | api('GET', `/api/weddings/${wid}/church-bookings`),
|
|---|
| 395 | api('GET', `/api/weddings/${wid}/attendance`)
|
|---|
| 396 | ]);
|
|---|
| 397 | if (evR && evR.ok) EVENTS = await evR.json();
|
|---|
| 398 | if (guR && guR.ok) GUESTS = await guR.json();
|
|---|
| 399 | if (vbR && vbR.ok) VENUE_BOOKINGS = await vbR.json();
|
|---|
| 400 | if (pbR && pbR.ok) PHOTOGRAPHER_BOOKINGS = await pbR.json();
|
|---|
| 401 | if (bbR && bbR.ok) BAND_BOOKINGS = await bbR.json();
|
|---|
| 402 | if (rbR && rbR.ok) REGISTRAR_BOOKINGS = await rbR.json();
|
|---|
| 403 | if (cbR && cbR.ok) CHURCH_BOOKINGS = await cbR.json();
|
|---|
| 404 | if (attR && attR.ok) ATTENDANCE = await attR.json();
|
|---|
| 405 | }
|
|---|
| 406 |
|
|---|
| 407 | const [venR, banR, phR, chR, regR] = await Promise.all([
|
|---|
| 408 | api('GET', '/api/venues'),
|
|---|
| 409 | api('GET', '/api/bands'),
|
|---|
| 410 | api('GET', '/api/photographers'),
|
|---|
| 411 | api('GET', '/api/churches'),
|
|---|
| 412 | api('GET', '/api/registrars')
|
|---|
| 413 | ]);
|
|---|
| 414 | if (venR && venR.ok) VENUES = await venR.json();
|
|---|
| 415 | if (banR && banR.ok) BANDS = await banR.json();
|
|---|
| 416 | if (phR && phR.ok) PHOTOGRAPHERS = await phR.json();
|
|---|
| 417 | if (chR && chR.ok) CHURCHES = await chR.json();
|
|---|
| 418 | if (regR && regR.ok) REGISTRARS = await regR.json();
|
|---|
| 419 |
|
|---|
| 420 | renderApp();
|
|---|
| 421 | }
|
|---|
| 422 |
|
|---|
| 423 | // Reload all wedding-specific data then re-render
|
|---|
| 424 | async function reload() {
|
|---|
| 425 | if (!CURRENT_WEDDING) return;
|
|---|
| 426 | const wid = CURRENT_WEDDING.wedding_id;
|
|---|
| 427 | const [evR, guR, vbR, pbR, bbR, rbR, cbR, attR] = await Promise.all([
|
|---|
| 428 | api('GET', `/api/weddings/${wid}/events`),
|
|---|
| 429 | api('GET', `/api/weddings/${wid}/guests`),
|
|---|
| 430 | api('GET', `/api/weddings/${wid}/venue-bookings`),
|
|---|
| 431 | api('GET', `/api/weddings/${wid}/photographer-bookings`),
|
|---|
| 432 | api('GET', `/api/weddings/${wid}/band-bookings`),
|
|---|
| 433 | api('GET', `/api/weddings/${wid}/registrar-bookings`),
|
|---|
| 434 | api('GET', `/api/weddings/${wid}/church-bookings`),
|
|---|
| 435 | api('GET', `/api/weddings/${wid}/attendance`)
|
|---|
| 436 | ]);
|
|---|
| 437 | if (evR && evR.ok) EVENTS = await evR.json();
|
|---|
| 438 | if (guR && guR.ok) GUESTS = await guR.json();
|
|---|
| 439 | if (vbR && vbR.ok) VENUE_BOOKINGS = await vbR.json();
|
|---|
| 440 | if (pbR && pbR.ok) PHOTOGRAPHER_BOOKINGS = await pbR.json();
|
|---|
| 441 | if (bbR && bbR.ok) BAND_BOOKINGS = await bbR.json();
|
|---|
| 442 | if (rbR && rbR.ok) REGISTRAR_BOOKINGS = await rbR.json();
|
|---|
| 443 | if (cbR && cbR.ok) CHURCH_BOOKINGS = await cbR.json();
|
|---|
| 444 | if (attR && attR.ok) ATTENDANCE = await attR.json();
|
|---|
| 445 | renderApp();
|
|---|
| 446 | }
|
|---|
| 447 |
|
|---|
| 448 | // =============================================================
|
|---|
| 449 | // ROUTER
|
|---|
| 450 | // =============================================================
|
|---|
| 451 | const ROUTE_LABELS = {
|
|---|
| 452 | '/dashboard':'Dashboard','/event':'Events','/guests':'Guests',
|
|---|
| 453 | '/seating':'Table Seating','/venue':'Venue','/band':'Band',
|
|---|
| 454 | '/photographer':'Photographer','/church':'Church',
|
|---|
| 455 | '/registrar':'Registrar','/settings':'Settings'
|
|---|
| 456 | };
|
|---|
| 457 | const ROUTE_RENDERERS = {
|
|---|
| 458 | '/dashboard':renderDashboard,'/event':renderEventPage,
|
|---|
| 459 | '/guests':renderGuestsPage,'/seating':renderSeatingPage,
|
|---|
| 460 | '/venue':renderVenuePage,'/band':renderBandPage,
|
|---|
| 461 | '/photographer':renderPhotographerPage,'/church':renderChurchPage,
|
|---|
| 462 | '/registrar':renderRegistrarPage,'/settings':renderSettingsPage
|
|---|
| 463 | };
|
|---|
| 464 |
|
|---|
| 465 | function navigate(hash){ window.location.hash = hash; }
|
|---|
| 466 |
|
|---|
| 467 | function renderApp(){
|
|---|
| 468 | const rawPath = window.location.hash.replace('#','') || '/dashboard';
|
|---|
| 469 | const ctx = rawPath.split('?')[0];
|
|---|
| 470 | document.getElementById('breadcrumb-page').textContent = ROUTE_LABELS[ctx] || 'Dashboard';
|
|---|
| 471 | document.querySelectorAll('.nav-item').forEach(a => {
|
|---|
| 472 | const page = a.getAttribute('data-page');
|
|---|
| 473 | a.classList.toggle('active', page ? `/${page}` === ctx : false);
|
|---|
| 474 | });
|
|---|
| 475 | const content = document.getElementById('page-content');
|
|---|
| 476 | content.innerHTML = '';
|
|---|
| 477 | (ROUTE_RENDERERS[ctx] || renderDashboard)(content);
|
|---|
| 478 | syncBadges();
|
|---|
| 479 | }
|
|---|
| 480 |
|
|---|
| 481 | window.addEventListener('hashchange', renderApp);
|
|---|
| 482 | document.addEventListener('DOMContentLoaded', () => {
|
|---|
| 483 | if (!location.hash) location.hash = '#/dashboard';
|
|---|
| 484 | bootstrap();
|
|---|
| 485 | });
|
|---|
| 486 |
|
|---|
| 487 | function syncBadges(){
|
|---|
| 488 | const g = document.getElementById('badge-guests');
|
|---|
| 489 | if(g) g.textContent = String(GUESTS.length);
|
|---|
| 490 | }
|
|---|
| 491 |
|
|---|
| 492 | // Logout
|
|---|
| 493 | document.getElementById('btnLogout').addEventListener('click', async (e) => {
|
|---|
| 494 | e.preventDefault();
|
|---|
| 495 | await api('POST','/api/auth/logout');
|
|---|
| 496 | window.location.href = 'login.html';
|
|---|
| 497 | });
|
|---|
| 498 |
|
|---|
| 499 | // =============================================================
|
|---|
| 500 | // TOAST
|
|---|
| 501 | // =============================================================
|
|---|
| 502 | let toastTimer = null;
|
|---|
| 503 | function showToast(msg, type=''){
|
|---|
| 504 | const t = document.getElementById('toast');
|
|---|
| 505 | t.textContent = msg;
|
|---|
| 506 | t.className = 'toast show' + (type ? ` toast-${type}` : '');
|
|---|
| 507 | clearTimeout(toastTimer);
|
|---|
| 508 | toastTimer = setTimeout(() => t.classList.remove('show'), 3200);
|
|---|
| 509 | }
|
|---|
| 510 |
|
|---|
| 511 | // =============================================================
|
|---|
| 512 | // MODAL
|
|---|
| 513 | // =============================================================
|
|---|
| 514 | function showModal(title, bodyHtml, onSubmit){
|
|---|
| 515 | document.getElementById('modalTitle').textContent = title;
|
|---|
| 516 | document.getElementById('modalBody').innerHTML = bodyHtml;
|
|---|
| 517 | document.getElementById('modalBackdrop').classList.add('open');
|
|---|
| 518 | const btn = document.getElementById('modalSubmit');
|
|---|
| 519 | btn.onclick = onSubmit;
|
|---|
| 520 | }
|
|---|
| 521 | function closeModal(){
|
|---|
| 522 | document.getElementById('modalBackdrop').classList.remove('open');
|
|---|
| 523 | }
|
|---|
| 524 | document.getElementById('modalBackdrop').addEventListener('click', e => {
|
|---|
| 525 | if(e.target === document.getElementById('modalBackdrop')) closeModal();
|
|---|
| 526 | });
|
|---|
| 527 |
|
|---|
| 528 | // =============================================================
|
|---|
| 529 | // DATA TABLE BUILDER
|
|---|
| 530 | // =============================================================
|
|---|
| 531 | function buildDataTable({ container, columns, rows, onEdit, onDelete }){
|
|---|
| 532 | container.innerHTML = '';
|
|---|
| 533 | const tableCard = document.createElement('div');
|
|---|
| 534 | tableCard.className = 'table-card';
|
|---|
| 535 |
|
|---|
| 536 | // toolbar / search
|
|---|
| 537 | const toolbar = document.createElement('div');
|
|---|
| 538 | toolbar.className = 'table-toolbar';
|
|---|
| 539 | const searchWrap = document.createElement('div');
|
|---|
| 540 | searchWrap.className = 'search-wrap';
|
|---|
| 541 | searchWrap.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><input class="search-input" placeholder="Search..."/>`;
|
|---|
| 542 | toolbar.appendChild(searchWrap);
|
|---|
| 543 | tableCard.appendChild(toolbar);
|
|---|
| 544 |
|
|---|
| 545 | const tableWrap = document.createElement('div');
|
|---|
| 546 | tableWrap.className = 'table-wrap';
|
|---|
| 547 | const table = document.createElement('table');
|
|---|
| 548 | const thead = document.createElement('thead');
|
|---|
| 549 | const headRow = document.createElement('tr');
|
|---|
| 550 |
|
|---|
| 551 | let sortKey = null, sortDir = 'asc';
|
|---|
| 552 | let currentData = [...rows];
|
|---|
| 553 | let currentPage = 1;
|
|---|
| 554 | const pageSize = 8;
|
|---|
| 555 |
|
|---|
| 556 | columns.forEach(col => {
|
|---|
| 557 | const th = document.createElement('th');
|
|---|
| 558 | if(col.sortable !== false) th.classList.add('sortable');
|
|---|
| 559 | th.innerHTML = col.header + (col.sortable !== false ? '<span class="sort-arrow">↕</span>' : '');
|
|---|
| 560 | if(col.sortable !== false){
|
|---|
| 561 | th.addEventListener('click', () => {
|
|---|
| 562 | sortKey = col.accessor;
|
|---|
| 563 | sortDir = sortKey === col.accessor && sortDir === 'asc' ? 'desc' : 'asc';
|
|---|
| 564 | sortAndRender();
|
|---|
| 565 | });
|
|---|
| 566 | }
|
|---|
| 567 | headRow.appendChild(th);
|
|---|
| 568 | });
|
|---|
| 569 | const thAct = document.createElement('th');
|
|---|
| 570 | thAct.textContent = 'Actions'; thAct.style.textAlign = 'right';
|
|---|
| 571 | headRow.appendChild(thAct);
|
|---|
| 572 | thead.appendChild(headRow);
|
|---|
| 573 | table.appendChild(thead);
|
|---|
| 574 |
|
|---|
| 575 | const tbody = document.createElement('tbody');
|
|---|
| 576 | table.appendChild(tbody);
|
|---|
| 577 | tableWrap.appendChild(table);
|
|---|
| 578 | tableCard.appendChild(tableWrap);
|
|---|
| 579 |
|
|---|
| 580 | const pagBar = document.createElement('div');
|
|---|
| 581 | pagBar.className = 'pagination-bar';
|
|---|
| 582 | const pagInfo = document.createElement('span');
|
|---|
| 583 | const pagCtrls = document.createElement('div');
|
|---|
| 584 | pagCtrls.className = 'pag-controls';
|
|---|
| 585 | pagBar.appendChild(pagInfo);
|
|---|
| 586 | pagBar.appendChild(pagCtrls);
|
|---|
| 587 | tableCard.appendChild(pagBar);
|
|---|
| 588 | container.appendChild(tableCard);
|
|---|
| 589 |
|
|---|
| 590 | function renderRows(){
|
|---|
| 591 | tbody.innerHTML = '';
|
|---|
| 592 | const total = currentData.length;
|
|---|
| 593 | const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|---|
| 594 | if(currentPage > totalPages) currentPage = totalPages;
|
|---|
| 595 | const start = (currentPage-1)*pageSize;
|
|---|
| 596 | const slice = currentData.slice(start, start+pageSize);
|
|---|
| 597 |
|
|---|
| 598 | pagInfo.textContent = total === 0 ? 'No results' : `${start+1}–${Math.min(start+pageSize,total)} of ${total}`;
|
|---|
| 599 |
|
|---|
| 600 | pagCtrls.innerHTML = '';
|
|---|
| 601 | const mkBtn = (label, val, disabled, isActive) => {
|
|---|
| 602 | const b = document.createElement('button');
|
|---|
| 603 | b.className = 'pag-btn' + (isActive ? ' active' : '');
|
|---|
| 604 | b.innerHTML = label; b.disabled = disabled;
|
|---|
| 605 | if(!disabled) b.addEventListener('click', () => { currentPage = val; renderRows(); });
|
|---|
| 606 | return b;
|
|---|
| 607 | };
|
|---|
| 608 | pagCtrls.appendChild(mkBtn('«',1,currentPage===1,false));
|
|---|
| 609 | pagCtrls.appendChild(mkBtn('‹',currentPage-1,currentPage===1,false));
|
|---|
| 610 | for(let p=Math.max(1,currentPage-1);p<=Math.min(totalPages,currentPage+1);p++)
|
|---|
| 611 | pagCtrls.appendChild(mkBtn(p,p,false,p===currentPage));
|
|---|
| 612 | pagCtrls.appendChild(mkBtn('›',currentPage+1,currentPage===totalPages,false));
|
|---|
| 613 | pagCtrls.appendChild(mkBtn('»',totalPages,currentPage===totalPages,false));
|
|---|
| 614 |
|
|---|
| 615 | if(slice.length === 0){
|
|---|
| 616 | const tr = document.createElement('tr'); tr.className = 'no-data-row';
|
|---|
| 617 | const td = document.createElement('td'); td.colSpan = columns.length+1; td.textContent = 'No records found.';
|
|---|
| 618 | tr.appendChild(td); tbody.appendChild(tr); return;
|
|---|
| 619 | }
|
|---|
| 620 |
|
|---|
| 621 | slice.forEach((row, idx) => {
|
|---|
| 622 | const tr = document.createElement('tr');
|
|---|
| 623 | tr.className = (start+idx)%2===1 ? 'row-even' : '';
|
|---|
| 624 | columns.forEach(col => {
|
|---|
| 625 | const td = document.createElement('td');
|
|---|
| 626 | td.innerHTML = col.render ? col.render(row[col.accessor], row) : (row[col.accessor] ?? '');
|
|---|
| 627 | tr.appendChild(td);
|
|---|
| 628 | });
|
|---|
| 629 | const tdAct = document.createElement('td');
|
|---|
| 630 | tdAct.style.textAlign = 'right';
|
|---|
| 631 | const actWrap = document.createElement('div');
|
|---|
| 632 | actWrap.className = 'td-actions'; actWrap.style.justifyContent = 'flex-end';
|
|---|
| 633 | if(onEdit){ const b = document.createElement('button'); b.className='btn btn-sm'; b.textContent='Edit'; b.addEventListener('click',()=>onEdit(row)); actWrap.appendChild(b); }
|
|---|
| 634 | if(onDelete){ const b = document.createElement('button'); b.className='btn btn-sm btn-danger'; b.textContent='Delete'; b.addEventListener('click',()=>onDelete(row)); actWrap.appendChild(b); }
|
|---|
| 635 | tdAct.appendChild(actWrap); tr.appendChild(tdAct); tbody.appendChild(tr);
|
|---|
| 636 | });
|
|---|
| 637 | }
|
|---|
| 638 |
|
|---|
| 639 | function sortAndRender(){
|
|---|
| 640 | if(sortKey){
|
|---|
| 641 | currentData.sort((a,b)=>{
|
|---|
| 642 | const va=(a[sortKey]??'').toString(), vb=(b[sortKey]??'').toString();
|
|---|
| 643 | if(!isNaN(va)&&!isNaN(vb)) return sortDir==='asc'?Number(va)-Number(vb):Number(vb)-Number(va);
|
|---|
| 644 | return sortDir==='asc'?va.localeCompare(vb):vb.localeCompare(va);
|
|---|
| 645 | });
|
|---|
| 646 | }
|
|---|
| 647 | renderRows();
|
|---|
| 648 | }
|
|---|
| 649 |
|
|---|
| 650 | searchWrap.querySelector('input').addEventListener('input', e => {
|
|---|
| 651 | const q = e.target.value.trim().toLowerCase();
|
|---|
| 652 | currentData = q ? rows.filter(r=>Object.values(r).join(' ').toLowerCase().includes(q)) : [...rows];
|
|---|
| 653 | currentPage = 1; sortAndRender();
|
|---|
| 654 | });
|
|---|
| 655 |
|
|---|
| 656 | sortAndRender();
|
|---|
| 657 | }
|
|---|
| 658 |
|
|---|
| 659 | // =============================================================
|
|---|
| 660 | // NO WEDDING STATE
|
|---|
| 661 | // =============================================================
|
|---|
| 662 | function renderNoWedding(container){
|
|---|
| 663 | container.innerHTML = `
|
|---|
| 664 | <div class="card" style="text-align:center;padding:48px 32px;">
|
|---|
| 665 | <div style="font-size:48px;margin-bottom:16px;">💍</div>
|
|---|
| 666 | <h2 style="font-family:'Cormorant Garamond',serif;font-size:26px;margin-bottom:8px;">No Wedding Created Yet</h2>
|
|---|
| 667 | <p class="muted" style="margin-bottom:24px;">Create your wedding to start planning your perfect day.</p>
|
|---|
| 668 | <button class="btn btn-primary" onclick="openCreateWeddingModal()">+ Create Wedding</button>
|
|---|
| 669 | </div>`;
|
|---|
| 670 | }
|
|---|
| 671 |
|
|---|
| 672 | function openCreateWeddingModal(){
|
|---|
| 673 | showModal('Create Your Wedding',
|
|---|
| 674 | `<div class="form-grid">
|
|---|
| 675 | <div class="form-group" style="grid-column:1/-1;">
|
|---|
| 676 | <label class="form-label">Wedding Date</label>
|
|---|
| 677 | <input id="wDate" type="date"/>
|
|---|
| 678 | </div>
|
|---|
| 679 | <div class="form-group">
|
|---|
| 680 | <label class="form-label">Budget ($)</label>
|
|---|
| 681 | <input id="wBudget" type="number" placeholder="15000"/>
|
|---|
| 682 | </div>
|
|---|
| 683 | <div class="form-group" style="grid-column:1/-1;">
|
|---|
| 684 | <label class="form-label">Notes / Theme</label>
|
|---|
| 685 | <textarea id="wNotes" rows="3" placeholder="Summer garden theme..."></textarea>
|
|---|
| 686 | </div>
|
|---|
| 687 | </div>`,
|
|---|
| 688 | async () => {
|
|---|
| 689 | const date = document.getElementById('wDate').value;
|
|---|
| 690 | if(!date){ showToast('Please select a wedding date.','danger'); return; }
|
|---|
| 691 | const res = await api('POST','/api/weddings',{
|
|---|
| 692 | date,
|
|---|
| 693 | budget: document.getElementById('wBudget').value || null,
|
|---|
| 694 | notes: document.getElementById('wNotes').value || null
|
|---|
| 695 | });
|
|---|
| 696 | if(res && res.ok){
|
|---|
| 697 | CURRENT_WEDDING = await res.json();
|
|---|
| 698 | closeModal();
|
|---|
| 699 | showToast('Wedding created! 🎉','success');
|
|---|
| 700 | await reload();
|
|---|
| 701 | } else {
|
|---|
| 702 | showToast('Could not create wedding.','danger');
|
|---|
| 703 | }
|
|---|
| 704 | }
|
|---|
| 705 | );
|
|---|
| 706 | }
|
|---|
| 707 |
|
|---|
| 708 | // =============================================================
|
|---|
| 709 | // DASHBOARD
|
|---|
| 710 | // =============================================================
|
|---|
| 711 | function renderDashboard(container){
|
|---|
| 712 | if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
|
|---|
| 713 | const w = CURRENT_WEDDING;
|
|---|
| 714 | const daysLeft = Math.max(0, Math.ceil((new Date(w.date)-new Date())/86400000));
|
|---|
| 715 |
|
|---|
| 716 | container.innerHTML = `
|
|---|
| 717 | <div style="margin-bottom:20px;">
|
|---|
| 718 | <h2 style="font-family:'Cormorant Garamond',serif;font-size:26px;font-weight:700;">
|
|---|
| 719 | Welcome back, <em style="color:var(--rose);">${CURRENT_USER.first_name}</em> 👋
|
|---|
| 720 | </h2>
|
|---|
| 721 | <p class="muted" style="margin-top:4px;">Your wedding overview for <strong>${w.date}</strong></p>
|
|---|
| 722 | </div>
|
|---|
| 723 | <div class="stats-row">
|
|---|
| 724 | <div class="stat-card stat-rose">
|
|---|
| 725 | <div class="stat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/></svg></div>
|
|---|
| 726 | <div class="stat-label">Days Left</div>
|
|---|
| 727 | <div class="stat-value">${daysLeft}</div>
|
|---|
| 728 | <div class="stat-sub">Until the big day</div>
|
|---|
| 729 | </div>
|
|---|
| 730 | <div class="stat-card stat-gold">
|
|---|
| 731 | <div class="stat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 2l3 7H21l-5.5 4 2 7L12 16l-5.5 4 2-7L3 9h6z"/></svg></div>
|
|---|
| 732 | <div class="stat-label">Budget</div>
|
|---|
| 733 | <div class="stat-value">$${w.budget ? (w.budget/1000).toFixed(0)+'k' : '—'}</div>
|
|---|
| 734 | <div class="stat-sub">Total budget</div>
|
|---|
| 735 | </div>
|
|---|
| 736 | <div class="stat-card stat-green">
|
|---|
| 737 | <div class="stat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></div>
|
|---|
| 738 | <div class="stat-label">Guests</div>
|
|---|
| 739 | <div class="stat-value">${GUESTS.length}</div>
|
|---|
| 740 | <div class="stat-sub">Invited guests</div>
|
|---|
| 741 | </div>
|
|---|
| 742 | <div class="stat-card stat-amber">
|
|---|
| 743 | <div class="stat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></div>
|
|---|
| 744 | <div class="stat-label">Venues</div>
|
|---|
| 745 | <div class="stat-value">${VENUE_BOOKINGS.length}</div>
|
|---|
| 746 | <div class="stat-sub">Booked</div>
|
|---|
| 747 | </div>
|
|---|
| 748 | <div class="stat-card stat-purple">
|
|---|
| 749 | <div class="stat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/></svg></div>
|
|---|
| 750 | <div class="stat-label">Events</div>
|
|---|
| 751 | <div class="stat-value">${EVENTS.length}</div>
|
|---|
| 752 | <div class="stat-sub">Scheduled</div>
|
|---|
| 753 | </div>
|
|---|
| 754 | </div>
|
|---|
| 755 | <div class="dash-grid">
|
|---|
| 756 | <div class="card">
|
|---|
| 757 | <div class="card-header"><h3>Wedding Overview</h3><span class="badge badge-rose"><span class="badge-dot"></span>Planned</span></div>
|
|---|
| 758 | <div class="gold-rule"></div>
|
|---|
| 759 | <div class="info-list">
|
|---|
| 760 | <div class="info-row"><span class="info-label">Date</span><span class="info-val">${w.date}</span></div>
|
|---|
| 761 | <div class="info-row"><span class="info-label">Budget</span><span class="info-val">${w.budget ? '$'+Number(w.budget).toLocaleString() : 'Not set'}</span></div>
|
|---|
| 762 | <div class="info-row"><span class="info-label">Notes</span><span class="info-val" style="font-size:12px;color:var(--text-mid);">${w.notes || '—'}</span></div>
|
|---|
| 763 | <div class="info-row"><span class="info-label">Photographers</span><span class="info-val">${PHOTOGRAPHER_BOOKINGS.length} booked</span></div>
|
|---|
| 764 | <div class="info-row"><span class="info-label">Bands</span><span class="info-val">${BAND_BOOKINGS.length} booked</span></div>
|
|---|
| 765 | </div>
|
|---|
| 766 | <div style="display:flex;gap:8px;margin-top:18px;flex-wrap:wrap;">
|
|---|
| 767 | <button class="btn btn-primary" onclick="navigate('#/event')">+ Add Event</button>
|
|---|
| 768 | <button class="btn btn-gold" onclick="navigate('#/venue')">Book Venue</button>
|
|---|
| 769 | <button class="btn" onclick="navigate('#/guests')">View Guests</button>
|
|---|
| 770 | </div>
|
|---|
| 771 | </div>
|
|---|
| 772 | <div class="card">
|
|---|
| 773 | <div class="card-header"><h3>Upcoming Events</h3><button class="btn btn-ghost btn-sm" onclick="navigate('#/event')">Manage all</button></div>
|
|---|
| 774 | <div class="table-wrap" style="max-height:300px;">
|
|---|
| 775 | <table><thead><tr><th>Type</th><th>Date</th><th>Time</th><th>Status</th></tr></thead>
|
|---|
| 776 | <tbody>
|
|---|
| 777 | ${EVENTS.length === 0
|
|---|
| 778 | ? `<tr class="no-data-row"><td colspan="4">No events yet.</td></tr>`
|
|---|
| 779 | : EVENTS.map(e=>`<tr>
|
|---|
| 780 | <td style="font-weight:500;">${e.event_type}</td>
|
|---|
| 781 | <td>${e.date}</td>
|
|---|
| 782 | <td><span class="badge badge-info">${e.start_time}–${e.end_time}</span></td>
|
|---|
| 783 | <td><span class="badge ${e.status==='scheduled'?'badge-success':e.status==='cancelled'?'badge-danger':'badge-warn'}">${e.status}</span></td>
|
|---|
| 784 | </tr>`).join('')}
|
|---|
| 785 | </tbody></table>
|
|---|
| 786 | </div>
|
|---|
| 787 | </div>
|
|---|
| 788 | <div class="card dash-full">
|
|---|
| 789 | <div class="card-header"><h3>Quick Actions</h3></div>
|
|---|
| 790 | <div style="display:flex;gap:10px;flex-wrap:wrap;">
|
|---|
| 791 | <button class="btn btn-primary" onclick="navigate('#/event')">+ New Event</button>
|
|---|
| 792 | <button class="btn btn-gold" onclick="navigate('#/venue')">+ Book Venue</button>
|
|---|
| 793 | <button class="btn" onclick="navigate('#/band')">+ Book Band</button>
|
|---|
| 794 | <button class="btn" onclick="navigate('#/photographer')">+ Book Photographer</button>
|
|---|
| 795 | <button class="btn" onclick="navigate('#/guests')">+ Add Guest</button>
|
|---|
| 796 | <button class="btn" onclick="navigate('#/seating')">🪑 Manage Seating</button>
|
|---|
| 797 | <button class="btn" onclick="navigate('#/registrar')">+ Book Registrar</button>
|
|---|
| 798 | </div>
|
|---|
| 799 | </div>
|
|---|
| 800 | </div>`;
|
|---|
| 801 | }
|
|---|
| 802 |
|
|---|
| 803 | // =============================================================
|
|---|
| 804 | // EVENTS PAGE
|
|---|
| 805 | // =============================================================
|
|---|
| 806 | function renderEventPage(container){
|
|---|
| 807 | if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
|
|---|
| 808 | const wrap = document.createElement('div');
|
|---|
| 809 | wrap.innerHTML = `<div class="card"><div class="card-header"><h3>Events</h3><div class="card-actions"><button class="btn btn-primary" id="btnAddEvent">+ Add Event</button></div></div><div id="evTableContainer"></div></div>`;
|
|---|
| 810 | container.appendChild(wrap);
|
|---|
| 811 | wrap.querySelector('#btnAddEvent').addEventListener('click', openEventModal);
|
|---|
| 812 |
|
|---|
| 813 | buildDataTable({
|
|---|
| 814 | container: wrap.querySelector('#evTableContainer'),
|
|---|
| 815 | columns: [
|
|---|
| 816 | { header:'Event Type', accessor:'event_type' },
|
|---|
| 817 | { header:'Date', accessor:'date' },
|
|---|
| 818 | { header:'Time', accessor:'time', sortable:false },
|
|---|
| 819 | { header:'Status', accessor:'status', render: v=>`<span class="badge ${v==='scheduled'?'badge-success':v==='cancelled'?'badge-danger':'badge-warn'}">${v}</span>` }
|
|---|
| 820 | ],
|
|---|
| 821 | rows: EVENTS.map(e=>({ id:e.event_id, event_type:e.event_type, date:e.date, time:`${e.start_time}–${e.end_time}`, status:e.status })),
|
|---|
| 822 | onEdit: row => editEventModal(row.id),
|
|---|
| 823 | onDelete: async row => {
|
|---|
| 824 | const res = await api('DELETE',`/api/events/${row.id}`);
|
|---|
| 825 | if(res && res.ok){ showToast('Event deleted.',''); await reload(); }
|
|---|
| 826 | else showToast('Could not delete event.','danger');
|
|---|
| 827 | }
|
|---|
| 828 | });
|
|---|
| 829 | }
|
|---|
| 830 |
|
|---|
| 831 | function openEventModal(){
|
|---|
| 832 | const w = CURRENT_WEDDING;
|
|---|
| 833 | showModal('Add Event',
|
|---|
| 834 | `<div class="form-grid">
|
|---|
| 835 | <div class="form-group"><label class="form-label">Event Type</label><input id="ev_type" placeholder="e.g. Church Ceremony"/></div>
|
|---|
| 836 | <div class="form-group"><label class="form-label">Date</label><input id="ev_date" type="date" value="${w.date}"/></div>
|
|---|
| 837 | <div class="form-group"><label class="form-label">Start Time</label><input id="ev_start" type="time" value="12:00"/></div>
|
|---|
| 838 | <div class="form-group"><label class="form-label">End Time</label><input id="ev_end" type="time" value="14:00"/></div>
|
|---|
| 839 | <div class="form-group"><label class="form-label">Status</label>
|
|---|
| 840 | <select id="ev_status"><option value="scheduled">Scheduled</option><option value="completed">Completed</option><option value="cancelled">Cancelled</option></select>
|
|---|
| 841 | </div>
|
|---|
| 842 | </div>`,
|
|---|
| 843 | async () => {
|
|---|
| 844 | const type = document.getElementById('ev_type').value.trim();
|
|---|
| 845 | if(!type){ showToast('Event type is required.','danger'); return; }
|
|---|
| 846 | const res = await api('POST',`/api/weddings/${CURRENT_WEDDING.wedding_id}/events`,{
|
|---|
| 847 | event_type: type,
|
|---|
| 848 | date: document.getElementById('ev_date').value,
|
|---|
| 849 | start_time: document.getElementById('ev_start').value,
|
|---|
| 850 | end_time: document.getElementById('ev_end').value,
|
|---|
| 851 | status: document.getElementById('ev_status').value
|
|---|
| 852 | });
|
|---|
| 853 | if(res && res.ok){ closeModal(); showToast('Event added.','success'); await reload(); }
|
|---|
| 854 | else { const d=await res.json(); showToast(d.error||'Error adding event.','danger'); }
|
|---|
| 855 | }
|
|---|
| 856 | );
|
|---|
| 857 | }
|
|---|
| 858 |
|
|---|
| 859 | function editEventModal(id){
|
|---|
| 860 | const e = EVENTS.find(x=>x.event_id===id); if(!e) return;
|
|---|
| 861 | showModal('Edit Event',
|
|---|
| 862 | `<div class="form-grid">
|
|---|
| 863 | <div class="form-group"><label class="form-label">Event Type</label><input id="ev_type" value="${e.event_type}"/></div>
|
|---|
| 864 | <div class="form-group"><label class="form-label">Date</label><input id="ev_date" type="date" value="${e.date}"/></div>
|
|---|
| 865 | <div class="form-group"><label class="form-label">Start Time</label><input id="ev_start" type="time" value="${e.start_time}"/></div>
|
|---|
| 866 | <div class="form-group"><label class="form-label">End Time</label><input id="ev_end" type="time" value="${e.end_time}"/></div>
|
|---|
| 867 | <div class="form-group"><label class="form-label">Status</label>
|
|---|
| 868 | <select id="ev_status">
|
|---|
| 869 | <option ${e.status==='scheduled'?'selected':''} value="scheduled">Scheduled</option>
|
|---|
| 870 | <option ${e.status==='completed'?'selected':''} value="completed">Completed</option>
|
|---|
| 871 | <option ${e.status==='cancelled'?'selected':''} value="cancelled">Cancelled</option>
|
|---|
| 872 | </select>
|
|---|
| 873 | </div>
|
|---|
| 874 | </div>`,
|
|---|
| 875 | async () => {
|
|---|
| 876 | const res = await api('PUT',`/api/events/${id}`,{
|
|---|
| 877 | event_type: document.getElementById('ev_type').value,
|
|---|
| 878 | date: document.getElementById('ev_date').value,
|
|---|
| 879 | start_time: document.getElementById('ev_start').value,
|
|---|
| 880 | end_time: document.getElementById('ev_end').value,
|
|---|
| 881 | status: document.getElementById('ev_status').value
|
|---|
| 882 | });
|
|---|
| 883 | if(res && res.ok){ closeModal(); showToast('Event updated.','success'); await reload(); }
|
|---|
| 884 | else showToast('Error updating event.','danger');
|
|---|
| 885 | }
|
|---|
| 886 | );
|
|---|
| 887 | }
|
|---|
| 888 |
|
|---|
| 889 | // =============================================================
|
|---|
| 890 | // GUESTS PAGE
|
|---|
| 891 | // =============================================================
|
|---|
| 892 | function renderGuestsPage(container){
|
|---|
| 893 | if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
|
|---|
| 894 | const wrap = document.createElement('div');
|
|---|
| 895 | wrap.innerHTML = `<div class="card"><div class="card-header"><h3>Guest List</h3><div class="card-actions"><button class="btn btn-primary" id="btnAddGuest">+ Add Guest</button></div></div><div id="guestTableContainer"></div></div>`;
|
|---|
| 896 | container.appendChild(wrap);
|
|---|
| 897 | wrap.querySelector('#btnAddGuest').addEventListener('click', openAddGuestModal);
|
|---|
| 898 |
|
|---|
| 899 | const guestRsvpMap = {};
|
|---|
| 900 | if(EVENTS.length > 0){
|
|---|
| 901 | const firstEvent = EVENTS[0];
|
|---|
| 902 | // RSVP mapping removed for revert
|
|---|
| 903 | }
|
|---|
| 904 |
|
|---|
| 905 | buildDataTable({
|
|---|
| 906 | container: wrap.querySelector('#guestTableContainer'),
|
|---|
| 907 | columns: [
|
|---|
| 908 | { header:'First Name', accessor:'first_name' },
|
|---|
| 909 | { header:'Last Name', accessor:'last_name' },
|
|---|
| 910 | { header:'Email', accessor:'email' },
|
|---|
| 911 | { header:'Status', accessor:'status', render: v => {
|
|---|
| 912 | const color = v === 'accepted' ? 'badge-success' : v === 'declined' ? 'badge-danger' : v === 'invited' ? 'badge-warn' : 'badge-purple';
|
|---|
| 913 | return `<span class="badge ${color}">${v || 'pending'}</span>`;
|
|---|
| 914 | }},
|
|---|
| 915 | { header:'Invite', accessor:'email', sortable:false, render: (v,row) =>
|
|---|
| 916 | v ? `<span class="badge badge-success">✉ Invite sent</span>` : `<span class="badge badge-warn">No email</span>`
|
|---|
| 917 | }
|
|---|
| 918 | ],
|
|---|
| 919 | rows: GUESTS.map(g=>({ id:g.guest_id, first_name:g.first_name, last_name:g.last_name, email:g.email||'', status: 'pending' })),
|
|---|
| 920 | onEdit: row => editGuestModal(row.id),
|
|---|
| 921 | onDelete: async row => {
|
|---|
| 922 | const res = await api('DELETE',`/api/guests/${row.id}`);
|
|---|
| 923 | if(res && res.ok){ showToast('Guest removed.',''); await reload(); }
|
|---|
| 924 | else showToast('Could not remove guest.','danger');
|
|---|
| 925 | }
|
|---|
| 926 | });
|
|---|
| 927 | }
|
|---|
| 928 |
|
|---|
| 929 | function openAddGuestModal(){
|
|---|
| 930 | showModal('Add Guest',
|
|---|
| 931 | `<div class="form-grid">
|
|---|
| 932 | <div class="form-group"><label class="form-label">First Name</label><input id="gFirst" placeholder="First name"/></div>
|
|---|
| 933 | <div class="form-group"><label class="form-label">Last Name</label><input id="gLast" placeholder="Last name"/></div>
|
|---|
| 934 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Email (invitation will be sent)</label><input id="gEmail" type="email" placeholder="guest@email.com"/></div>
|
|---|
| 935 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Role</label><select id="gRole"><option>Guest</option><option>Best Man</option><option>Maid of Honor</option><option>Bridesmaid</option><option>Groomsman</option><option>Family</option><option>VIP</option></select></div>
|
|---|
| 936 | </div>`,
|
|---|
| 937 | async () => {
|
|---|
| 938 | const first = document.getElementById('gFirst').value.trim();
|
|---|
| 939 | if(!first){ showToast('First name is required.','danger'); return; }
|
|---|
| 940 | const res = await api('POST',`/api/weddings/${CURRENT_WEDDING.wedding_id}/guests`,{
|
|---|
| 941 | first_name: first,
|
|---|
| 942 | last_name: document.getElementById('gLast').value.trim(),
|
|---|
| 943 | email: document.getElementById('gEmail').value.trim(),
|
|---|
| 944 | role: document.getElementById('gRole').value
|
|---|
| 945 | });
|
|---|
| 946 | if(res && res.ok){
|
|---|
| 947 | const d = await res.json();
|
|---|
| 948 | closeModal();
|
|---|
| 949 | showToast(d.emailSent ? 'Guest added & invitation email sent! 📧' : 'Guest added (no email provided).','success');
|
|---|
| 950 | await reload();
|
|---|
| 951 | } else showToast('Could not add guest.','danger');
|
|---|
| 952 | }
|
|---|
| 953 | );
|
|---|
| 954 | }
|
|---|
| 955 |
|
|---|
| 956 | function editGuestModal(id){
|
|---|
| 957 | const g = GUESTS.find(x=>x.guest_id===id); if(!g) return;
|
|---|
| 958 | showModal('Edit Guest',
|
|---|
| 959 | `<div class="form-grid">
|
|---|
| 960 | <div class="form-group"><label class="form-label">First Name</label><input id="gFirst" value="${g.first_name}"/></div>
|
|---|
| 961 | <div class="form-group"><label class="form-label">Last Name</label><input id="gLast" value="${g.last_name}"/></div>
|
|---|
| 962 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Email</label><input id="gEmail" value="${g.email||''}"/></div>
|
|---|
| 963 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Role</label><select id="gRole"><option ${g.role==='Guest'?'selected':''}>Guest</option><option ${g.role==='Best Man'?'selected':''}>Best Man</option><option ${g.role==='Maid of Honor'?'selected':''}>Maid of Honor</option><option ${g.role==='Bridesmaid'?'selected':''}>Bridesmaid</option><option ${g.role==='Groomsman'?'selected':''}>Groomsman</option><option ${g.role==='Family'?'selected':''}>Family</option><option ${g.role==='VIP'?'selected':''}>VIP</option></select></div>
|
|---|
| 964 | </div>`,
|
|---|
| 965 | async () => {
|
|---|
| 966 | const res = await api('PUT',`/api/guests/${id}`,{
|
|---|
| 967 | first_name: document.getElementById('gFirst').value,
|
|---|
| 968 | last_name: document.getElementById('gLast').value,
|
|---|
| 969 | email: document.getElementById('gEmail').value,
|
|---|
| 970 | role: document.getElementById('gRole').value
|
|---|
| 971 | });
|
|---|
| 972 | if(res && res.ok){ closeModal(); showToast('Guest updated.','success'); await reload(); }
|
|---|
| 973 | else showToast('Error updating guest.','danger');
|
|---|
| 974 | }
|
|---|
| 975 | );
|
|---|
| 976 | }
|
|---|
| 977 |
|
|---|
| 978 | // =============================================================
|
|---|
| 979 | // SEATING PAGE (NEW — UC0011)
|
|---|
| 980 | // =============================================================
|
|---|
| 981 | function renderSeatingPage(container){
|
|---|
| 982 | if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
|
|---|
| 983 |
|
|---|
| 984 | const wrap = document.createElement('div');
|
|---|
| 985 |
|
|---|
| 986 | // Build event selector
|
|---|
| 987 | const eventOptions = EVENTS.map(e=>`<option value="${e.event_id}">${e.event_type} (${e.date})</option>`).join('');
|
|---|
| 988 |
|
|---|
| 989 | wrap.innerHTML = `
|
|---|
| 990 | <div class="card" style="margin-bottom:18px;">
|
|---|
| 991 | <div class="card-header">
|
|---|
| 992 | <h3>🪑 Table Seating</h3>
|
|---|
| 993 | <div class="card-actions">
|
|---|
| 994 | <select id="seatEventSel" style="width:220px;">
|
|---|
| 995 | <option value="">— Select Event —</option>
|
|---|
| 996 | ${eventOptions}
|
|---|
| 997 | </select>
|
|---|
| 998 | <button class="btn btn-primary" id="btnAssignSeat">+ Assign Guest</button>
|
|---|
| 999 | </div>
|
|---|
| 1000 | </div>
|
|---|
| 1001 | <div class="gold-rule"></div>
|
|---|
| 1002 | <p class="muted" style="margin-bottom:16px;">Select an event to view and manage table assignments. Tables are created dynamically based on assigned guests.</p>
|
|---|
| 1003 | <div id="seatingBoard"></div>
|
|---|
| 1004 | </div>
|
|---|
| 1005 | <div class="card">
|
|---|
| 1006 | <div class="card-header"><h3>All Assignments</h3></div>
|
|---|
| 1007 | <div id="seatingTableContainer"></div>
|
|---|
| 1008 | </div>`;
|
|---|
| 1009 |
|
|---|
| 1010 | container.appendChild(wrap);
|
|---|
| 1011 |
|
|---|
| 1012 | const sel = wrap.querySelector('#seatEventSel');
|
|---|
| 1013 | sel.addEventListener('change', () => renderSeatingBoard(wrap, sel.value));
|
|---|
| 1014 | wrap.querySelector('#btnAssignSeat').addEventListener('click', () => openAssignSeatModal(sel.value));
|
|---|
| 1015 |
|
|---|
| 1016 | // Full table view
|
|---|
| 1017 | buildDataTable({
|
|---|
| 1018 | container: wrap.querySelector('#seatingTableContainer'),
|
|---|
| 1019 | columns: [
|
|---|
| 1020 | { header:'Guest', accessor:'guest_name' },
|
|---|
| 1021 | { header:'Event', accessor:'event_type' },
|
|---|
| 1022 | { header:'Table #', accessor:'table_number', render: v => v ? `<span class="badge badge-gold">Table ${v}</span>` : '<span class="badge badge-warn">Unassigned</span>' },
|
|---|
| 1023 | { header:'Role', accessor:'role', render: v => `<span class="badge badge-purple">${v}</span>` },
|
|---|
| 1024 | { header:'Status', accessor:'status', render: v => `<span class="badge ${v==='attending'?'badge-success':'badge-danger'}">${v}</span>` }
|
|---|
| 1025 | ],
|
|---|
| 1026 | rows: ATTENDANCE.map(a=>({
|
|---|
| 1027 | id: a.attendance_id,
|
|---|
| 1028 | guest_name: `${a.first_name} ${a.last_name}`,
|
|---|
| 1029 | event_type: a.event_type,
|
|---|
| 1030 | table_number: a.table_number,
|
|---|
| 1031 | role: a.role,
|
|---|
| 1032 | status: a.status
|
|---|
| 1033 | })),
|
|---|
| 1034 | onEdit: row => openEditSeatModal(row.id),
|
|---|
| 1035 | onDelete: async row => {
|
|---|
| 1036 | const res = await api('DELETE',`/api/attendance/${row.id}`);
|
|---|
| 1037 | if(res && res.ok){ showToast('Assignment removed.',''); await reload(); }
|
|---|
| 1038 | else showToast('Error removing assignment.','danger');
|
|---|
| 1039 | }
|
|---|
| 1040 | });
|
|---|
| 1041 | }
|
|---|
| 1042 |
|
|---|
| 1043 | function renderSeatingBoard(wrap, eventId){
|
|---|
| 1044 | const board = wrap.querySelector('#seatingBoard');
|
|---|
| 1045 | if(!eventId){ board.innerHTML = '<p class="muted">Select an event above to see the seating layout.</p>'; return; }
|
|---|
| 1046 |
|
|---|
| 1047 | const eventAtt = ATTENDANCE.filter(a => String(a.event_id) === String(eventId));
|
|---|
| 1048 | const assigned = eventAtt.filter(a => a.table_number);
|
|---|
| 1049 | const unassigned = eventAtt.filter(a => !a.table_number);
|
|---|
| 1050 |
|
|---|
| 1051 | // Group by table
|
|---|
| 1052 | const tables = {};
|
|---|
| 1053 | assigned.forEach(a => {
|
|---|
| 1054 | if(!tables[a.table_number]) tables[a.table_number] = [];
|
|---|
| 1055 | tables[a.table_number].push(a);
|
|---|
| 1056 | });
|
|---|
| 1057 |
|
|---|
| 1058 | const tableNums = Object.keys(tables).map(Number).sort((a,b)=>a-b);
|
|---|
| 1059 |
|
|---|
| 1060 | let html = '';
|
|---|
| 1061 |
|
|---|
| 1062 | if(tableNums.length === 0 && unassigned.length === 0){
|
|---|
| 1063 | html = '<p class="muted">No guests assigned to this event yet. Click "+ Assign Guest" to start.</p>';
|
|---|
| 1064 | } else {
|
|---|
| 1065 | if(tableNums.length > 0){
|
|---|
| 1066 | html += `<h4 style="font-family:'Cormorant Garamond',serif;font-size:17px;margin-bottom:12px;color:var(--text-dark);">Seating Layout</h4>
|
|---|
| 1067 | <div class="seating-grid">
|
|---|
| 1068 | ${tableNums.map(tnum => `
|
|---|
| 1069 | <div class="table-card-seat">
|
|---|
| 1070 | <div class="table-title">🍽 Table ${tnum} <span style="font-size:12px;font-weight:400;color:var(--text-light);">(${tables[tnum].length} guests)</span></div>
|
|---|
| 1071 | ${tables[tnum].map(a=>`
|
|---|
| 1072 | <div class="seat-guest">
|
|---|
| 1073 | <span>${a.first_name} ${a.last_name}</span>
|
|---|
| 1074 | <span class="seat-role">${a.role}</span>
|
|---|
| 1075 | </div>`).join('')}
|
|---|
| 1076 | </div>`).join('')}
|
|---|
| 1077 | </div>`;
|
|---|
| 1078 | }
|
|---|
| 1079 | if(unassigned.length > 0){
|
|---|
| 1080 | html += `<h4 style="font-family:'Cormorant Garamond',serif;font-size:17px;margin:20px 0 10px;color:var(--amber);">⚠ Unassigned Guests (${unassigned.length})</h4>
|
|---|
| 1081 | <div class="unassigned-list">
|
|---|
| 1082 | ${unassigned.map(a=>`
|
|---|
| 1083 | <div class="unassigned-item">
|
|---|
| 1084 | <span>${a.first_name} ${a.last_name} <span style="color:var(--text-light);font-size:12px;">(${a.role})</span></span>
|
|---|
| 1085 | <button class="btn btn-sm btn-gold" onclick="openEditSeatModal(${a.attendance_id})">Assign Table</button>
|
|---|
| 1086 | </div>`).join('')}
|
|---|
| 1087 | </div>`;
|
|---|
| 1088 | }
|
|---|
| 1089 | }
|
|---|
| 1090 |
|
|---|
| 1091 | board.innerHTML = html;
|
|---|
| 1092 | }
|
|---|
| 1093 |
|
|---|
| 1094 | function openAssignSeatModal(preselectedEventId){
|
|---|
| 1095 | const eventOptions = EVENTS.map(e=>`<option value="${e.event_id}" ${String(e.event_id)===String(preselectedEventId)?'selected':''}>${e.event_type} (${e.date})</option>`).join('');
|
|---|
| 1096 | const guestOptions = GUESTS.map(g=>`<option value="${g.guest_id}">${g.first_name} ${g.last_name}</option>`).join('');
|
|---|
| 1097 |
|
|---|
| 1098 | showModal('Assign Guest to Table',
|
|---|
| 1099 | `<div class="form-grid">
|
|---|
| 1100 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Event</label><select id="attEvent">${eventOptions}</select></div>
|
|---|
| 1101 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Guest</label><select id="attGuest">${guestOptions}</select></div>
|
|---|
| 1102 | <div class="form-group"><label class="form-label">Table Number</label><input id="attTable" type="number" min="1" placeholder="e.g. 5"/></div>
|
|---|
| 1103 | <div class="form-group"><label class="form-label">Role</label>
|
|---|
| 1104 | <select id="attRole">
|
|---|
| 1105 | <option value="Guest">Guest</option>
|
|---|
| 1106 | <option value="Best Man">Best Man</option>
|
|---|
| 1107 | <option value="Maid of Honor">Maid of Honor</option>
|
|---|
| 1108 | <option value="Bridesmaid">Bridesmaid</option>
|
|---|
| 1109 | <option value="Groomsman">Groomsman</option>
|
|---|
| 1110 | <option value="Family">Family</option>
|
|---|
| 1111 | <option value="VIP">VIP</option>
|
|---|
| 1112 | </select>
|
|---|
| 1113 | </div>
|
|---|
| 1114 | <div class="form-group" style="grid-column:1/-1;"><p class="muted" style="margin:0;">Set the attendance status manually.</p></div>
|
|---|
| 1115 | </div>`,
|
|---|
| 1116 | async () => {
|
|---|
| 1117 | const res = await api('POST','/api/attendance',{
|
|---|
| 1118 | event_id: document.getElementById('attEvent').value,
|
|---|
| 1119 | guest_id: document.getElementById('attGuest').value,
|
|---|
| 1120 | table_number: document.getElementById('attTable').value || null,
|
|---|
| 1121 | role: document.getElementById('attRole').value
|
|---|
| 1122 | });
|
|---|
| 1123 | if(res && res.ok){ closeModal(); showToast('Guest assigned to table!','success'); await reload(); }
|
|---|
| 1124 | else { const d=await res.json(); showToast(d.error||'Error assigning guest.','danger'); }
|
|---|
| 1125 | }
|
|---|
| 1126 | );
|
|---|
| 1127 | }
|
|---|
| 1128 |
|
|---|
| 1129 | function openEditSeatModal(id){
|
|---|
| 1130 | const a = ATTENDANCE.find(x=>x.attendance_id===id); if(!a) return;
|
|---|
| 1131 | showModal('Edit Seating Assignment',
|
|---|
| 1132 | `<div class="form-grid">
|
|---|
| 1133 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Guest</label><input disabled value="${a.first_name} ${a.last_name}"/></div>
|
|---|
| 1134 | <div class="form-group"><label class="form-label">Table Number</label><input id="attTable" type="number" min="1" value="${a.table_number||''}"/></div>
|
|---|
| 1135 | <div class="form-group"><label class="form-label">Role</label>
|
|---|
| 1136 | <select id="attRole">
|
|---|
| 1137 | ${['Guest','Best Man','Maid of Honor','Bridesmaid','Groomsman','Family','VIP'].map(r=>`<option ${a.role===r?'selected':''}>${r}</option>`).join('')}
|
|---|
| 1138 | </select>
|
|---|
| 1139 | </div>
|
|---|
| 1140 | <div class="form-group"><label class="form-label">Status</label>
|
|---|
| 1141 | <select id="attStatus">
|
|---|
| 1142 | <option ${a.status==='attending'?'selected':''} value="attending">Attending</option>
|
|---|
| 1143 | <option ${a.status==='not_attending'?'selected':''} value="not_attending">Not Attending</option>
|
|---|
| 1144 | </select>
|
|---|
| 1145 | </div>
|
|---|
| 1146 | <div class="form-group" style="grid-column:1/-1;"><p class="muted" style="margin:0;">Set the attendance status manually.</p></div>
|
|---|
| 1147 | </div>`,
|
|---|
| 1148 | async () => {
|
|---|
| 1149 | const res = await api('PUT',`/api/attendance/${id}`,{
|
|---|
| 1150 | table_number: document.getElementById('attTable').value || null,
|
|---|
| 1151 | role: document.getElementById('attRole').value,
|
|---|
| 1152 | status: document.getElementById('attStatus').value
|
|---|
| 1153 | });
|
|---|
| 1154 | if(res && res.ok){ closeModal(); showToast('Seating updated!','success'); await reload(); }
|
|---|
| 1155 | else showToast('Error updating seating.','danger');
|
|---|
| 1156 | }
|
|---|
| 1157 | );
|
|---|
| 1158 | }
|
|---|
| 1159 |
|
|---|
| 1160 | // =============================================================
|
|---|
| 1161 | // VENUE PAGE
|
|---|
| 1162 | // =============================================================
|
|---|
| 1163 | function renderVenuePage(container){
|
|---|
| 1164 | if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
|
|---|
| 1165 | const wrap = document.createElement('div');
|
|---|
| 1166 | wrap.innerHTML = `
|
|---|
| 1167 | <div class="card" style="margin-bottom:18px;">
|
|---|
| 1168 | <div class="card-header"><h3>Venues</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookVenue">+ Book Venue</button></div></div>
|
|---|
| 1169 | <div id="venueTableContainer"></div>
|
|---|
| 1170 | </div>
|
|---|
| 1171 | <div class="card">
|
|---|
| 1172 | <div class="card-header"><h3>Venue Bookings</h3></div>
|
|---|
| 1173 | <div id="venueBookingTableContainer"></div>
|
|---|
| 1174 | </div>`;
|
|---|
| 1175 | container.appendChild(wrap);
|
|---|
| 1176 | wrap.querySelector('#btnBookVenue').addEventListener('click', openVenueBookingModal);
|
|---|
| 1177 |
|
|---|
| 1178 | buildDataTable({
|
|---|
| 1179 | container: wrap.querySelector('#venueTableContainer'),
|
|---|
| 1180 | columns: [
|
|---|
| 1181 | { header:'Venue Name', accessor:'name' },
|
|---|
| 1182 | { header:'City', accessor:'city' },
|
|---|
| 1183 | { header:'Capacity', accessor:'capacity' },
|
|---|
| 1184 | { header:'Type', accessor:'type_name' },
|
|---|
| 1185 | { header:'Price/Guest', accessor:'price_per_guest', render: v=>`<strong>$${v}</strong>` }
|
|---|
| 1186 | ],
|
|---|
| 1187 | rows: VENUES.map(v=>({ id:v.venue_id, name:v.name, city:v.city, capacity:v.capacity, type_name:v.type_name, price_per_guest:v.price_per_guest })),
|
|---|
| 1188 | onEdit: row => showToast(`Viewing ${row.name}`,'')
|
|---|
| 1189 | });
|
|---|
| 1190 |
|
|---|
| 1191 | buildDataTable({
|
|---|
| 1192 | container: wrap.querySelector('#venueBookingTableContainer'),
|
|---|
| 1193 | columns: [
|
|---|
| 1194 | { header:'Venue', accessor:'venue_name' },
|
|---|
| 1195 | { header:'Date', accessor:'date' },
|
|---|
| 1196 | { header:'Time', accessor:'time', sortable:false },
|
|---|
| 1197 | { header:'Price', accessor:'price', render: v=>`$${Number(v).toLocaleString()}` },
|
|---|
| 1198 | { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` }
|
|---|
| 1199 | ],
|
|---|
| 1200 | rows: VENUE_BOOKINGS.map(b=>({ id:b.booking_id, venue_name:b.venue_name, date:b.date, time:`${b.start_time}–${b.end_time}`, price:b.price, status:b.status })),
|
|---|
| 1201 | onDelete: async row => {
|
|---|
| 1202 | const res = await api('DELETE',`/api/venue-bookings/${row.id}`);
|
|---|
| 1203 | if(res && res.ok){ showToast('Booking cancelled.',''); await reload(); }
|
|---|
| 1204 | else showToast('Could not cancel booking.','danger');
|
|---|
| 1205 | }
|
|---|
| 1206 | });
|
|---|
| 1207 | }
|
|---|
| 1208 |
|
|---|
| 1209 | function openVenueBookingModal(){
|
|---|
| 1210 | const w = CURRENT_WEDDING;
|
|---|
| 1211 | const venueOptions = VENUES.map(v=>`<option value="${v.venue_id}">${v.name} — $${v.price_per_guest}/guest</option>`).join('');
|
|---|
| 1212 | showModal('Book a Venue',
|
|---|
| 1213 | `<div class="form-grid">
|
|---|
| 1214 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Venue</label><select id="venueSelect">${venueOptions}</select></div>
|
|---|
| 1215 | <div class="form-group"><label class="form-label">Date</label><input id="venueDate" type="date" value="${w.date}"/></div>
|
|---|
| 1216 | <div class="form-group"><label class="form-label">Price ($)</label><input id="venuePrice" type="number" placeholder="0"/></div>
|
|---|
| 1217 | <div class="form-group"><label class="form-label">Start Time</label><input id="venueStart" type="time" value="16:00"/></div>
|
|---|
| 1218 | <div class="form-group"><label class="form-label">End Time</label><input id="venueEnd" type="time" value="23:00"/></div>
|
|---|
| 1219 | </div>`,
|
|---|
| 1220 | async () => {
|
|---|
| 1221 | const res = await api('POST',`/api/weddings/${w.wedding_id}/venue-bookings`,{
|
|---|
| 1222 | venue_id: document.getElementById('venueSelect').value,
|
|---|
| 1223 | date: document.getElementById('venueDate').value,
|
|---|
| 1224 | start_time: document.getElementById('venueStart').value,
|
|---|
| 1225 | end_time: document.getElementById('venueEnd').value,
|
|---|
| 1226 | price: document.getElementById('venuePrice').value || 0,
|
|---|
| 1227 | status: 'confirmed'
|
|---|
| 1228 | });
|
|---|
| 1229 | if(res && res.ok){ closeModal(); showToast('Venue booked!','success'); await reload(); }
|
|---|
| 1230 | else { const d=await res.json(); showToast(d.error||'Error booking venue.','danger'); }
|
|---|
| 1231 | }
|
|---|
| 1232 | );
|
|---|
| 1233 | }
|
|---|
| 1234 |
|
|---|
| 1235 | // =============================================================
|
|---|
| 1236 | // BAND PAGE
|
|---|
| 1237 | // =============================================================
|
|---|
| 1238 | function renderBandPage(container){
|
|---|
| 1239 | if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
|
|---|
| 1240 | const wrap = document.createElement('div');
|
|---|
| 1241 | wrap.innerHTML = `
|
|---|
| 1242 | <div class="card" style="margin-bottom:18px;">
|
|---|
| 1243 | <div class="card-header"><h3>Bands</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookBand">+ Book Band</button></div></div>
|
|---|
| 1244 | <div id="bandTableContainer"></div>
|
|---|
| 1245 | </div>
|
|---|
| 1246 | <div class="card"><div class="card-header"><h3>Band Bookings</h3></div><div id="bandBookingsContainer"></div></div>`;
|
|---|
| 1247 | container.appendChild(wrap);
|
|---|
| 1248 | wrap.querySelector('#btnBookBand').addEventListener('click', openBandBookingModal);
|
|---|
| 1249 |
|
|---|
| 1250 | buildDataTable({
|
|---|
| 1251 | container: wrap.querySelector('#bandTableContainer'),
|
|---|
| 1252 | columns: [
|
|---|
| 1253 | { header:'Band Name', accessor:'band_name' },
|
|---|
| 1254 | { header:'Genre', accessor:'genre' },
|
|---|
| 1255 | { header:'Equipment', accessor:'equipment', sortable:false },
|
|---|
| 1256 | { header:'Price/Hour', accessor:'price_per_hour', render: v=>`<strong>$${v}</strong>` }
|
|---|
| 1257 | ],
|
|---|
| 1258 | rows: BANDS.map(b=>({ id:b.band_id, band_name:b.band_name, genre:b.genre, equipment:b.equipment||'—', price_per_hour:b.price_per_hour })),
|
|---|
| 1259 | onEdit: row => showToast(`Viewing ${row.band_name}`,'')
|
|---|
| 1260 | });
|
|---|
| 1261 |
|
|---|
| 1262 | buildDataTable({
|
|---|
| 1263 | container: wrap.querySelector('#bandBookingsContainer'),
|
|---|
| 1264 | columns: [
|
|---|
| 1265 | { header:'Band', accessor:'band_name' },
|
|---|
| 1266 | { header:'Date', accessor:'date' },
|
|---|
| 1267 | { header:'Time', accessor:'time', sortable:false },
|
|---|
| 1268 | { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` }
|
|---|
| 1269 | ],
|
|---|
| 1270 | rows: BAND_BOOKINGS.map(b=>({ id:b.booking_id, band_name:b.band_name, date:b.date, time:`${b.start_time}–${b.end_time}`, status:b.status })),
|
|---|
| 1271 | onDelete: async row => {
|
|---|
| 1272 | const res = await api('DELETE',`/api/band-bookings/${row.id}`);
|
|---|
| 1273 | if(res && res.ok){ showToast('Band booking removed.',''); await reload(); }
|
|---|
| 1274 | else showToast('Error removing booking.','danger');
|
|---|
| 1275 | }
|
|---|
| 1276 | });
|
|---|
| 1277 | }
|
|---|
| 1278 |
|
|---|
| 1279 | function openBandBookingModal(){
|
|---|
| 1280 | const w = CURRENT_WEDDING;
|
|---|
| 1281 | const opts = BANDS.map(b=>`<option value="${b.band_id}">${b.band_name} — $${b.price_per_hour}/hr</option>`).join('');
|
|---|
| 1282 | showModal('Book a Band',
|
|---|
| 1283 | `<div class="form-grid">
|
|---|
| 1284 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Band</label><select id="bandSel">${opts}</select></div>
|
|---|
| 1285 | <div class="form-group"><label class="form-label">Date</label><input id="bandDate" type="date" value="${w.date}"/></div>
|
|---|
| 1286 | <div class="form-group"><label class="form-label">Start Time</label><input id="bandStart" type="time" value="19:00"/></div>
|
|---|
| 1287 | <div class="form-group"><label class="form-label">End Time</label><input id="bandEnd" type="time" value="23:00"/></div>
|
|---|
| 1288 | </div>`,
|
|---|
| 1289 | async () => {
|
|---|
| 1290 | const res = await api('POST',`/api/weddings/${w.wedding_id}/band-bookings`,{
|
|---|
| 1291 | band_id: document.getElementById('bandSel').value,
|
|---|
| 1292 | date: document.getElementById('bandDate').value,
|
|---|
| 1293 | start_time: document.getElementById('bandStart').value,
|
|---|
| 1294 | end_time: document.getElementById('bandEnd').value,
|
|---|
| 1295 | status: 'confirmed'
|
|---|
| 1296 | });
|
|---|
| 1297 | if(res && res.ok){ closeModal(); showToast('Band booked!','success'); await reload(); }
|
|---|
| 1298 | else { const d=await res.json(); showToast(d.error||'Error booking band.','danger'); }
|
|---|
| 1299 | }
|
|---|
| 1300 | );
|
|---|
| 1301 | }
|
|---|
| 1302 |
|
|---|
| 1303 | // =============================================================
|
|---|
| 1304 | // PHOTOGRAPHER PAGE
|
|---|
| 1305 | // =============================================================
|
|---|
| 1306 | function renderPhotographerPage(container){
|
|---|
| 1307 | if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
|
|---|
| 1308 | const wrap = document.createElement('div');
|
|---|
| 1309 | wrap.innerHTML = `
|
|---|
| 1310 | <div class="card" style="margin-bottom:18px;">
|
|---|
| 1311 | <div class="card-header"><h3>Photographers</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookPhotog">+ Book Photographer</button></div></div>
|
|---|
| 1312 | <div id="photogTableContainer"></div>
|
|---|
| 1313 | </div>
|
|---|
| 1314 | <div class="card"><div class="card-header"><h3>Photographer Bookings</h3></div><div id="photogBookingContainer"></div></div>`;
|
|---|
| 1315 | container.appendChild(wrap);
|
|---|
| 1316 | wrap.querySelector('#btnBookPhotog').addEventListener('click', openPhotographerBookingModal);
|
|---|
| 1317 |
|
|---|
| 1318 | buildDataTable({
|
|---|
| 1319 | container: wrap.querySelector('#photogTableContainer'),
|
|---|
| 1320 | columns: [
|
|---|
| 1321 | { header:'Name', accessor:'name' },
|
|---|
| 1322 | { header:'Email', accessor:'email', sortable:false },
|
|---|
| 1323 | { header:'Phone', accessor:'phone', sortable:false },
|
|---|
| 1324 | { header:'Price/Hour', accessor:'price_per_hour', render: v=>`<strong>$${v}</strong>` }
|
|---|
| 1325 | ],
|
|---|
| 1326 | rows: PHOTOGRAPHERS.map(p=>({ id:p.photographer_id, name:p.name, email:p.email, phone:p.phone_number, price_per_hour:p.price_per_hour })),
|
|---|
| 1327 | onEdit: row => showToast(`Viewing ${row.name}`,'')
|
|---|
| 1328 | });
|
|---|
| 1329 |
|
|---|
| 1330 | buildDataTable({
|
|---|
| 1331 | container: wrap.querySelector('#photogBookingContainer'),
|
|---|
| 1332 | columns: [
|
|---|
| 1333 | { header:'Photographer', accessor:'name' },
|
|---|
| 1334 | { header:'Date', accessor:'date' },
|
|---|
| 1335 | { header:'Time', accessor:'time', sortable:false },
|
|---|
| 1336 | { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` }
|
|---|
| 1337 | ],
|
|---|
| 1338 | rows: PHOTOGRAPHER_BOOKINGS.map(b=>({ id:b.booking_id, name:b.photographer_name, date:b.date, time:`${b.start_time}–${b.end_time}`, status:b.status })),
|
|---|
| 1339 | onDelete: async row => {
|
|---|
| 1340 | const res = await api('DELETE',`/api/photographer-bookings/${row.id}`);
|
|---|
| 1341 | if(res && res.ok){ showToast('Booking removed.',''); await reload(); }
|
|---|
| 1342 | else showToast('Error removing booking.','danger');
|
|---|
| 1343 | }
|
|---|
| 1344 | });
|
|---|
| 1345 | }
|
|---|
| 1346 |
|
|---|
| 1347 | function openPhotographerBookingModal(){
|
|---|
| 1348 | const w = CURRENT_WEDDING;
|
|---|
| 1349 | const opts = PHOTOGRAPHERS.map(p=>`<option value="${p.photographer_id}">${p.name} — $${p.price_per_hour}/hr</option>`).join('');
|
|---|
| 1350 | showModal('Book a Photographer',
|
|---|
| 1351 | `<div class="form-grid">
|
|---|
| 1352 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Photographer</label><select id="photogSel">${opts}</select></div>
|
|---|
| 1353 | <div class="form-group"><label class="form-label">Date</label><input id="photogDate" type="date" value="${w.date}"/></div>
|
|---|
| 1354 | <div class="form-group"><label class="form-label">Start Time</label><input id="photogStart" type="time" value="14:00"/></div>
|
|---|
| 1355 | <div class="form-group"><label class="form-label">End Time</label><input id="photogEnd" type="time" value="22:00"/></div>
|
|---|
| 1356 | </div>`,
|
|---|
| 1357 | async () => {
|
|---|
| 1358 | const res = await api('POST',`/api/weddings/${w.wedding_id}/photographer-bookings`,{
|
|---|
| 1359 | photographer_id: document.getElementById('photogSel').value,
|
|---|
| 1360 | date: document.getElementById('photogDate').value,
|
|---|
| 1361 | start_time: document.getElementById('photogStart').value,
|
|---|
| 1362 | end_time: document.getElementById('photogEnd').value,
|
|---|
| 1363 | status: 'confirmed'
|
|---|
| 1364 | });
|
|---|
| 1365 | if(res && res.ok){ closeModal(); showToast('Photographer booked!','success'); await reload(); }
|
|---|
| 1366 | else { const d=await res.json(); showToast(d.error||'Error booking photographer.','danger'); }
|
|---|
| 1367 | }
|
|---|
| 1368 | );
|
|---|
| 1369 | }
|
|---|
| 1370 |
|
|---|
| 1371 | // =============================================================
|
|---|
| 1372 | // CHURCH PAGE
|
|---|
| 1373 | // =============================================================
|
|---|
| 1374 | function renderChurchPage(container){
|
|---|
| 1375 | if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
|
|---|
| 1376 | const wrap = document.createElement('div');
|
|---|
| 1377 | wrap.innerHTML = `
|
|---|
| 1378 | <div class="card" style="margin-bottom:18px;">
|
|---|
| 1379 | <div class="card-header"><h3>Churches</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookChurch">+ Book Church</button></div></div>
|
|---|
| 1380 | <div id="churchTableContainer"></div>
|
|---|
| 1381 | </div>
|
|---|
| 1382 | <div class="card"><div class="card-header"><h3>Church Bookings</h3></div><div id="churchBookingContainer"></div></div>`;
|
|---|
| 1383 | container.appendChild(wrap);
|
|---|
| 1384 | wrap.querySelector('#btnBookChurch').addEventListener('click', openChurchBookingModal);
|
|---|
| 1385 |
|
|---|
| 1386 | buildDataTable({
|
|---|
| 1387 | container: wrap.querySelector('#churchTableContainer'),
|
|---|
| 1388 | columns: [
|
|---|
| 1389 | { header:'Church', accessor:'name' },
|
|---|
| 1390 | { header:'Location', accessor:'location' },
|
|---|
| 1391 | { header:'Contact', accessor:'contact', sortable:false },
|
|---|
| 1392 | { header:'Priest', accessor:'priest_name', sortable:false }
|
|---|
| 1393 | ],
|
|---|
| 1394 | rows: CHURCHES.map(c=>({ id:c.church_id, name:c.name, location:c.location, contact:c.contact, priest_name:c.priest_name||'—' })),
|
|---|
| 1395 | onEdit: row => showToast(`Viewing ${row.name}`,'')
|
|---|
| 1396 | });
|
|---|
| 1397 |
|
|---|
| 1398 | buildDataTable({
|
|---|
| 1399 | container: wrap.querySelector('#churchBookingContainer'),
|
|---|
| 1400 | columns: [
|
|---|
| 1401 | { header:'Church', accessor:'church_name' },
|
|---|
| 1402 | { header:'Date', accessor:'date' },
|
|---|
| 1403 | { header:'Time', accessor:'time', sortable:false },
|
|---|
| 1404 | { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` },
|
|---|
| 1405 | { header:'Actions', accessor:'id', sortable:false, render: id=>`<button class="btn btn-sm btn-danger" onclick="deleteChurchBooking(${id})">Delete</button>` }
|
|---|
| 1406 | ],
|
|---|
| 1407 | rows: CHURCH_BOOKINGS.map(b=>({ id:b.church_booking_id, church_name: b.church_name||'—', date:b.date, time:`${b.start_time||'—'}–${b.end_time||'—'}`, status:b.status })),
|
|---|
| 1408 | onEdit: undefined
|
|---|
| 1409 | });
|
|---|
| 1410 | }
|
|---|
| 1411 |
|
|---|
| 1412 | // =============================================================
|
|---|
| 1413 | // REGISTRAR PAGE
|
|---|
| 1414 | // =============================================================
|
|---|
| 1415 | function renderRegistrarPage(container){
|
|---|
| 1416 | if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
|
|---|
| 1417 | const wrap = document.createElement('div');
|
|---|
| 1418 | wrap.innerHTML = `
|
|---|
| 1419 | <div class="card" style="margin-bottom:18px;">
|
|---|
| 1420 | <div class="card-header"><h3>Registrars</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookReg">+ Book Registrar</button></div></div>
|
|---|
| 1421 | <div id="regTableContainer"></div>
|
|---|
| 1422 | </div>
|
|---|
| 1423 | <div class="card"><div class="card-header"><h3>Registrar Bookings</h3></div><div id="regBookingContainer"></div></div>`;
|
|---|
| 1424 | container.appendChild(wrap);
|
|---|
| 1425 | wrap.querySelector('#btnBookReg').addEventListener('click', openRegistrarBookingModal);
|
|---|
| 1426 |
|
|---|
| 1427 | buildDataTable({
|
|---|
| 1428 | container: wrap.querySelector('#regTableContainer'),
|
|---|
| 1429 | columns: [
|
|---|
| 1430 | { header:'Name', accessor:'name' },
|
|---|
| 1431 | { header:'Location', accessor:'location' },
|
|---|
| 1432 | { header:'Working Hours', accessor:'working_hours', sortable:false },
|
|---|
| 1433 | { header:'Contact', accessor:'contact', sortable:false }
|
|---|
| 1434 | ],
|
|---|
| 1435 | rows: REGISTRARS.map(r=>({ id:r.registrar_id, name:r.name, location:r.location, working_hours:r.working_hours||'—', contact:r.contact })),
|
|---|
| 1436 | onEdit: row => showToast(`Viewing ${row.name}`,'')
|
|---|
| 1437 | });
|
|---|
| 1438 |
|
|---|
| 1439 | buildDataTable({
|
|---|
| 1440 | container: wrap.querySelector('#regBookingContainer'),
|
|---|
| 1441 | columns: [
|
|---|
| 1442 | { header:'Registrar', accessor:'registrar_name' },
|
|---|
| 1443 | { header:'Date', accessor:'date' },
|
|---|
| 1444 | { header:'Time', accessor:'time', sortable:false },
|
|---|
| 1445 | { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` }
|
|---|
| 1446 | ],
|
|---|
| 1447 | rows: REGISTRAR_BOOKINGS.map(b=>({ id:b.booking_id, registrar_name:b.registrar_name, date:b.date, time:`${b.start_time}–${b.end_time}`, status:b.status })),
|
|---|
| 1448 | onDelete: async row => {
|
|---|
| 1449 | const res = await api('DELETE',`/api/registrar-bookings/${row.id}`);
|
|---|
| 1450 | if(res && res.ok){ showToast('Booking removed.',''); await reload(); }
|
|---|
| 1451 | else showToast('Error removing booking.','danger');
|
|---|
| 1452 | }
|
|---|
| 1453 | });
|
|---|
| 1454 | }
|
|---|
| 1455 |
|
|---|
| 1456 | function openRegistrarBookingModal(){
|
|---|
| 1457 | const w = CURRENT_WEDDING;
|
|---|
| 1458 | const opts = REGISTRARS.map(r=>`<option value="${r.registrar_id}">${r.name}</option>`).join('');
|
|---|
| 1459 | showModal('Book Registrar',
|
|---|
| 1460 | `<div class="form-grid">
|
|---|
| 1461 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Registrar</label><select id="regSel">${opts}</select></div>
|
|---|
| 1462 | <div class="form-group"><label class="form-label">Date</label><input id="regDate" type="date" value="${w.date}"/></div>
|
|---|
| 1463 | <div class="form-group"><label class="form-label">Start Time</label><input id="regStart" type="time" value="10:00"/></div>
|
|---|
| 1464 | <div class="form-group"><label class="form-label">End Time</label><input id="regEnd" type="time" value="12:00"/></div>
|
|---|
| 1465 | </div>`,
|
|---|
| 1466 | async () => {
|
|---|
| 1467 | const res = await api('POST',`/api/weddings/${w.wedding_id}/registrar-bookings`,{
|
|---|
| 1468 | registrar_id: document.getElementById('regSel').value,
|
|---|
| 1469 | date: document.getElementById('regDate').value,
|
|---|
| 1470 | start_time: document.getElementById('regStart').value,
|
|---|
| 1471 | end_time: document.getElementById('regEnd').value,
|
|---|
| 1472 | status: 'confirmed'
|
|---|
| 1473 | });
|
|---|
| 1474 | if(res && res.ok){ closeModal(); showToast('Registrar booked!','success'); await reload(); }
|
|---|
| 1475 | else { const d=await res.json(); showToast(d.error||'Error booking registrar.','danger'); }
|
|---|
| 1476 | }
|
|---|
| 1477 | );
|
|---|
| 1478 | }
|
|---|
| 1479 |
|
|---|
| 1480 | function openChurchBookingModal(){
|
|---|
| 1481 | const w = CURRENT_WEDDING;
|
|---|
| 1482 | const churchOptions = CHURCHES.map(c=>`<option value="${c.church_id}">${c.name}</option>`).join('');
|
|---|
| 1483 | showModal('Book a Church',
|
|---|
| 1484 | `<div class="form-grid">
|
|---|
| 1485 | <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Church</label><select id="churchSelect">${churchOptions}</select></div>
|
|---|
| 1486 | <div class="form-group"><label class="form-label">Date</label><input id="churchDate" type="date" value="${w.date}"/></div>
|
|---|
| 1487 | <div class="form-group"><label class="form-label">Start Time</label><input id="churchStart" type="time" value="14:00"/></div>
|
|---|
| 1488 | <div class="form-group"><label class="form-label">End Time</label><input id="churchEnd" type="time" value="15:00"/></div>
|
|---|
| 1489 | </div>`,
|
|---|
| 1490 | async () => {
|
|---|
| 1491 | const res = await api('POST',`/api/weddings/${w.wedding_id}/church-bookings`,{
|
|---|
| 1492 | church_id: document.getElementById('churchSelect').value,
|
|---|
| 1493 | date: document.getElementById('churchDate').value,
|
|---|
| 1494 | start_time: document.getElementById('churchStart').value,
|
|---|
| 1495 | end_time: document.getElementById('churchEnd').value,
|
|---|
| 1496 | status: 'confirmed'
|
|---|
| 1497 | });
|
|---|
| 1498 | if(res && res.ok){ closeModal(); showToast('Church booked!','success'); await reload(); }
|
|---|
| 1499 | else { const d=await res.json(); showToast(d.error||'Error booking church.','danger'); }
|
|---|
| 1500 | }
|
|---|
| 1501 | );
|
|---|
| 1502 | }
|
|---|
| 1503 |
|
|---|
| 1504 | async function deleteChurchBooking(id){
|
|---|
| 1505 | if(!confirm('Delete this church booking?')) return;
|
|---|
| 1506 | const res = await api('DELETE',`/api/church-bookings/${id}`);
|
|---|
| 1507 | if(res && res.ok){ showToast('Booking removed.','success'); await reload(); }
|
|---|
| 1508 | else showToast('Error removing booking.','danger');
|
|---|
| 1509 | }
|
|---|
| 1510 |
|
|---|
| 1511 | // =============================================================
|
|---|
| 1512 | // SETTINGS PAGE
|
|---|
| 1513 | // =============================================================
|
|---|
| 1514 | function renderSettingsPage(container){
|
|---|
| 1515 | const u = CURRENT_USER;
|
|---|
| 1516 | const wrap = document.createElement('div');
|
|---|
| 1517 | wrap.innerHTML = `
|
|---|
| 1518 | <div class="card">
|
|---|
| 1519 | <div class="card-header"><h3>Profile Settings</h3><span class="badge badge-rose">User #${u.user_id}</span></div>
|
|---|
| 1520 | <div class="gold-rule"></div>
|
|---|
| 1521 | <div style="display:flex;align-items:center;gap:16px;margin-bottom:24px;padding:16px;background:var(--cream);border-radius:var(--radius-lg);border:1px solid var(--border-light);">
|
|---|
| 1522 | <div style="width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,var(--blush-mid),var(--rose));display:flex;align-items:center;justify-content:center;color:white;font-weight:700;font-size:20px;flex-shrink:0;">
|
|---|
| 1523 | ${u.first_name[0]}${u.last_name[0]}
|
|---|
| 1524 | </div>
|
|---|
| 1525 | <div>
|
|---|
| 1526 | <div style="font-family:'Cormorant Garamond',serif;font-size:20px;font-weight:700;">${u.first_name} ${u.last_name}</div>
|
|---|
| 1527 | <div class="muted">${u.email}</div>
|
|---|
| 1528 | </div>
|
|---|
| 1529 | </div>
|
|---|
| 1530 | <div class="form-grid">
|
|---|
| 1531 | <div class="form-group"><label class="form-label">First Name</label><input id="sFirst" value="${u.first_name}"/></div>
|
|---|
| 1532 | <div class="form-group"><label class="form-label">Last Name</label><input id="sLast" value="${u.last_name}"/></div>
|
|---|
| 1533 | <div class="form-group"><label class="form-label">Email</label><input id="sEmail" type="email" value="${u.email}"/></div>
|
|---|
| 1534 | <div class="form-group"><label class="form-label">Phone</label><input id="sPhone" value="${u.phone_number||''}"/></div>
|
|---|
| 1535 | <div class="form-group"><label class="form-label">Gender</label>
|
|---|
| 1536 | <select id="sGender">
|
|---|
| 1537 | <option ${u.gender==='Female'?'selected':''}>Female</option>
|
|---|
| 1538 | <option ${u.gender==='Male'?'selected':''}>Male</option>
|
|---|
| 1539 | <option ${u.gender==='Other'?'selected':''}>Other</option>
|
|---|
| 1540 | </select>
|
|---|
| 1541 | </div>
|
|---|
| 1542 | </div>
|
|---|
| 1543 | <div style="display:flex;gap:10px;margin-top:20px;">
|
|---|
| 1544 | <button class="btn btn-primary" id="btnSaveProfile">Save Changes</button>
|
|---|
| 1545 | <button class="btn btn-ghost" onclick="renderApp()">Reset</button>
|
|---|
| 1546 | </div>
|
|---|
| 1547 | </div>`;
|
|---|
| 1548 | container.appendChild(wrap);
|
|---|
| 1549 |
|
|---|
| 1550 | wrap.querySelector('#btnSaveProfile').addEventListener('click', async () => {
|
|---|
| 1551 | const res = await api('PUT',`/api/users/${u.user_id}`,{
|
|---|
| 1552 | first_name: document.getElementById('sFirst').value,
|
|---|
| 1553 | last_name: document.getElementById('sLast').value,
|
|---|
| 1554 | email: document.getElementById('sEmail').value,
|
|---|
| 1555 | phone_number: document.getElementById('sPhone').value,
|
|---|
| 1556 | gender: document.getElementById('sGender').value
|
|---|
| 1557 | });
|
|---|
| 1558 | if(res && res.ok){
|
|---|
| 1559 | CURRENT_USER = await res.json();
|
|---|
| 1560 | document.getElementById('userName').textContent = `${CURRENT_USER.first_name} ${CURRENT_USER.last_name}`;
|
|---|
| 1561 | document.getElementById('userAvatar').textContent = `${CURRENT_USER.first_name[0]}${CURRENT_USER.last_name[0]}`;
|
|---|
| 1562 | showToast('Profile saved!','success');
|
|---|
| 1563 | } else showToast('Error saving profile.','danger');
|
|---|
| 1564 | });
|
|---|
| 1565 | }
|
|---|
| 1566 |
|
|---|
| 1567 | // =============================================================
|
|---|
| 1568 | // UTILITY
|
|---|
| 1569 | // =============================================================
|
|---|
| 1570 | function focusSearch(){
|
|---|
| 1571 | const s = document.querySelector('.search-input');
|
|---|
| 1572 | if(s) s.focus();
|
|---|
| 1573 | }
|
|---|
| 1574 | </script>
|
|---|
| 1575 | </body>
|
|---|
| 1576 | </html>
|
|---|