source: Wedding_Planner.html@ 679113b

main
Last change on this file since 679113b was b7d8a4d, checked in by GitHub <noreply@…>, 5 weeks ago

Add files via upload

  • Property mode set to 100644
File size: 97.0 KB
RevLine 
[d8deee6]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// =============================================================
319const API = 'http://localhost:3000';
320
321// =============================================================
322// APP STATE — loaded from DB, not hardcoded
323// =============================================================
324let CURRENT_USER = null;
325let CURRENT_WEDDING = null;
[b7d8a4d]326let WEDDINGS = [];
[d8deee6]327let EVENTS = [];
328let GUESTS = [];
329let VENUES = [];
330let VENUE_BOOKINGS = [];
331let PHOTOGRAPHERS = [];
332let PHOTOGRAPHER_BOOKINGS = [];
333let BANDS = [];
334let BAND_BOOKINGS = [];
335let CHURCHES = [];
336let CHURCH_BOOKINGS = [];
337let REGISTRARS = [];
338let REGISTRAR_BOOKINGS = [];
339let ATTENDANCE = [];
[b7d8a4d]340let RSVPS = [];
[d8deee6]341
342// =============================================================
343// API HELPER
344// =============================================================
345async function api(method, path, body) {
346 const opts = {
347 method,
348 credentials: 'include',
349 headers: { 'Content-Type': 'application/json' }
350 };
351 if (body) opts.body = JSON.stringify(body);
352 const res = await fetch(`${API}${path}`, opts);
353 if (res.status === 401) { window.location.href = 'login.html'; return null; }
354 return res;
355}
356
357// =============================================================
358// BOOTSTRAP — check auth + load all data
359// =============================================================
360async function bootstrap() {
361 // 1. Check login
362 const meRes = await fetch(`${API}/api/auth/me`, { credentials: 'include' });
363 if (!meRes.ok) { window.location.href = 'login.html'; return; }
364 CURRENT_USER = (await meRes.json()).user;
365
366 // Update sidebar user info
367 document.getElementById('userName').textContent = `${CURRENT_USER.first_name} ${CURRENT_USER.last_name}`;
368 document.getElementById('userAvatar').textContent = `${CURRENT_USER.first_name[0]}${CURRENT_USER.last_name[0]}`;
369
370 // 2. Load wedding
371 const wRes = await api('GET', '/api/weddings');
372 if (wRes && wRes.ok) {
373 const weddings = await wRes.json();
[b7d8a4d]374 WEDDINGS = weddings;
[d8deee6]375 CURRENT_WEDDING = weddings[0] || null;
376 }
377
378 if (!CURRENT_WEDDING) {
379 // No wedding yet — show create wedding prompt
380 if (!location.hash || location.hash === '#/dashboard') {
381 renderApp();
382 return;
383 }
384 }
385
386 const wid = CURRENT_WEDDING ? CURRENT_WEDDING.wedding_id : null;
387
388 // 3. Load all data in parallel
389 if (wid) {
390 const [evR, guR, vbR, pbR, bbR, rbR, cbR, attR] = await Promise.all([
391 api('GET', `/api/weddings/${wid}/events`),
392 api('GET', `/api/weddings/${wid}/guests`),
393 api('GET', `/api/weddings/${wid}/venue-bookings`),
394 api('GET', `/api/weddings/${wid}/photographer-bookings`),
395 api('GET', `/api/weddings/${wid}/band-bookings`),
396 api('GET', `/api/weddings/${wid}/registrar-bookings`),
397 api('GET', `/api/weddings/${wid}/church-bookings`),
398 api('GET', `/api/weddings/${wid}/attendance`)
399 ]);
400 if (evR && evR.ok) EVENTS = await evR.json();
401 if (guR && guR.ok) GUESTS = await guR.json();
402 if (vbR && vbR.ok) VENUE_BOOKINGS = await vbR.json();
403 if (pbR && pbR.ok) PHOTOGRAPHER_BOOKINGS = await pbR.json();
404 if (bbR && bbR.ok) BAND_BOOKINGS = await bbR.json();
405 if (rbR && rbR.ok) REGISTRAR_BOOKINGS = await rbR.json();
406 if (cbR && cbR.ok) CHURCH_BOOKINGS = await cbR.json();
407 if (attR && attR.ok) ATTENDANCE = await attR.json();
408 }
409
[b7d8a4d]410 // Load RSVPs for this wedding (admin view)
411 if (wid) {
412 const rsvpR = await api('GET', `/api/weddings/${wid}/rsvp`);
413 if (rsvpR && rsvpR.ok) RSVPS = await rsvpR.json();
414 }
415
[d8deee6]416 const [venR, banR, phR, chR, regR] = await Promise.all([
417 api('GET', '/api/venues'),
418 api('GET', '/api/bands'),
419 api('GET', '/api/photographers'),
420 api('GET', '/api/churches'),
421 api('GET', '/api/registrars')
422 ]);
423 if (venR && venR.ok) VENUES = await venR.json();
424 if (banR && banR.ok) BANDS = await banR.json();
425 if (phR && phR.ok) PHOTOGRAPHERS = await phR.json();
426 if (chR && chR.ok) CHURCHES = await chR.json();
427 if (regR && regR.ok) REGISTRARS = await regR.json();
428
429 renderApp();
430}
431
432// Reload all wedding-specific data then re-render
433async function reload() {
434 if (!CURRENT_WEDDING) return;
[b7d8a4d]435 // Refresh list of weddings first (in case user created/removed/switches)
436 try {
437 const wListR = await api('GET', '/api/weddings');
438 if (wListR && wListR.ok) {
439 WEDDINGS = await wListR.json();
440 // keep CURRENT_WEDDING reference in sync
441 if (CURRENT_WEDDING) {
442 const found = WEDDINGS.find(w => String(w.wedding_id) === String(CURRENT_WEDDING.wedding_id));
443 if (found) CURRENT_WEDDING = found;
444 }
445 }
446 } catch (e) { /* ignore */ }
447
[d8deee6]448 const wid = CURRENT_WEDDING.wedding_id;
449 const [evR, guR, vbR, pbR, bbR, rbR, cbR, attR] = await Promise.all([
450 api('GET', `/api/weddings/${wid}/events`),
451 api('GET', `/api/weddings/${wid}/guests`),
452 api('GET', `/api/weddings/${wid}/venue-bookings`),
453 api('GET', `/api/weddings/${wid}/photographer-bookings`),
454 api('GET', `/api/weddings/${wid}/band-bookings`),
455 api('GET', `/api/weddings/${wid}/registrar-bookings`),
456 api('GET', `/api/weddings/${wid}/church-bookings`),
457 api('GET', `/api/weddings/${wid}/attendance`)
458 ]);
459 if (evR && evR.ok) EVENTS = await evR.json();
460 if (guR && guR.ok) GUESTS = await guR.json();
461 if (vbR && vbR.ok) VENUE_BOOKINGS = await vbR.json();
462 if (pbR && pbR.ok) PHOTOGRAPHER_BOOKINGS = await pbR.json();
463 if (bbR && bbR.ok) BAND_BOOKINGS = await bbR.json();
464 if (rbR && rbR.ok) REGISTRAR_BOOKINGS = await rbR.json();
465 if (cbR && cbR.ok) CHURCH_BOOKINGS = await cbR.json();
466 if (attR && attR.ok) ATTENDANCE = await attR.json();
[b7d8a4d]467 // Refresh RSVP list as well
468 const rsvpR = await api('GET', `/api/weddings/${wid}/rsvp`);
469 if (rsvpR && rsvpR.ok) RSVPS = await rsvpR.json();
[d8deee6]470 renderApp();
471}
472
473// =============================================================
474// ROUTER
475// =============================================================
476const ROUTE_LABELS = {
477 '/dashboard':'Dashboard','/event':'Events','/guests':'Guests',
478 '/seating':'Table Seating','/venue':'Venue','/band':'Band',
479 '/photographer':'Photographer','/church':'Church',
480 '/registrar':'Registrar','/settings':'Settings'
481};
482const ROUTE_RENDERERS = {
483 '/dashboard':renderDashboard,'/event':renderEventPage,
484 '/guests':renderGuestsPage,'/seating':renderSeatingPage,
485 '/venue':renderVenuePage,'/band':renderBandPage,
486 '/photographer':renderPhotographerPage,'/church':renderChurchPage,
487 '/registrar':renderRegistrarPage,'/settings':renderSettingsPage
488};
489
490function navigate(hash){ window.location.hash = hash; }
491
492function renderApp(){
493 const rawPath = window.location.hash.replace('#','') || '/dashboard';
494 const ctx = rawPath.split('?')[0];
495 document.getElementById('breadcrumb-page').textContent = ROUTE_LABELS[ctx] || 'Dashboard';
496 document.querySelectorAll('.nav-item').forEach(a => {
497 const page = a.getAttribute('data-page');
498 a.classList.toggle('active', page ? `/${page}` === ctx : false);
499 });
[b7d8a4d]500 // Render wedding selector in the topbar actions so user can switch between weddings
501 try {
502 const topbarActions = document.querySelector('.topbar .topbar-actions');
503 // remove old selector if present
504 const oldSel = document.getElementById('weddingSelect');
505 if (oldSel) oldSel.remove();
506 const oldBtn = document.getElementById('btnNewWedding');
507 if (oldBtn) oldBtn.remove();
508
509 // Create selector
510 const sel = document.createElement('select');
511 sel.id = 'weddingSelect';
512 sel.style.minWidth = '200px';
513 sel.style.padding = '6px 8px';
514 sel.style.borderRadius = '8px';
515 sel.style.border = '1px solid var(--border)';
516 sel.style.background = '#fff';
517 sel.style.marginRight = '8px';
518 const noneOpt = document.createElement('option');
519 noneOpt.value = '';
520 noneOpt.textContent = '— Select Wedding —';
521 sel.appendChild(noneOpt);
522 WEDDINGS.forEach(w => {
523 const o = document.createElement('option');
524 o.value = w.wedding_id;
525 o.textContent = `${w.date} ${w.notes?'- '+(w.notes||'') : ''}`;
526 if (CURRENT_WEDDING && String(CURRENT_WEDDING.wedding_id) === String(w.wedding_id)) o.selected = true;
527 sel.appendChild(o);
528 });
529 sel.addEventListener('change', async (e) => {
530 const val = e.target.value;
531 if (!val) return;
532 const found = WEDDINGS.find(x => String(x.wedding_id) === String(val));
533 if (found) {
534 CURRENT_WEDDING = found;
535 await reload();
536 renderApp();
537 }
538 });
539 topbarActions.insertBefore(sel, topbarActions.firstChild);
540
541 // New wedding button
542 const newBtn = document.createElement('button');
543 newBtn.id = 'btnNewWedding';
544 newBtn.className = 'btn btn-sm';
545 newBtn.style.marginRight = '8px';
546 newBtn.textContent = '+ Wedding';
547 newBtn.addEventListener('click', openCreateWeddingModal);
548 topbarActions.insertBefore(newBtn, topbarActions.firstChild);
549 } catch (e) { /* ignore UI insertion errors */ }
[d8deee6]550 const content = document.getElementById('page-content');
551 content.innerHTML = '';
552 (ROUTE_RENDERERS[ctx] || renderDashboard)(content);
553 syncBadges();
554}
555
556window.addEventListener('hashchange', renderApp);
557document.addEventListener('DOMContentLoaded', () => {
558 if (!location.hash) location.hash = '#/dashboard';
559 bootstrap();
560});
561
562function syncBadges(){
563 const g = document.getElementById('badge-guests');
564 if(g) g.textContent = String(GUESTS.length);
565}
566
567// Logout
568document.getElementById('btnLogout').addEventListener('click', async (e) => {
569 e.preventDefault();
570 await api('POST','/api/auth/logout');
571 window.location.href = 'login.html';
572});
573
574// =============================================================
575// TOAST
576// =============================================================
577let toastTimer = null;
578function showToast(msg, type=''){
579 const t = document.getElementById('toast');
580 t.textContent = msg;
581 t.className = 'toast show' + (type ? ` toast-${type}` : '');
582 clearTimeout(toastTimer);
583 toastTimer = setTimeout(() => t.classList.remove('show'), 3200);
584}
585
586// =============================================================
587// MODAL
588// =============================================================
589function showModal(title, bodyHtml, onSubmit){
590 document.getElementById('modalTitle').textContent = title;
591 document.getElementById('modalBody').innerHTML = bodyHtml;
592 document.getElementById('modalBackdrop').classList.add('open');
593 const btn = document.getElementById('modalSubmit');
594 btn.onclick = onSubmit;
595}
596function closeModal(){
597 document.getElementById('modalBackdrop').classList.remove('open');
598}
599document.getElementById('modalBackdrop').addEventListener('click', e => {
600 if(e.target === document.getElementById('modalBackdrop')) closeModal();
601});
602
603// =============================================================
604// DATA TABLE BUILDER
605// =============================================================
606function buildDataTable({ container, columns, rows, onEdit, onDelete }){
607 container.innerHTML = '';
608 const tableCard = document.createElement('div');
609 tableCard.className = 'table-card';
610
611 // toolbar / search
612 const toolbar = document.createElement('div');
613 toolbar.className = 'table-toolbar';
614 const searchWrap = document.createElement('div');
615 searchWrap.className = 'search-wrap';
616 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..."/>`;
617 toolbar.appendChild(searchWrap);
618 tableCard.appendChild(toolbar);
619
620 const tableWrap = document.createElement('div');
621 tableWrap.className = 'table-wrap';
622 const table = document.createElement('table');
623 const thead = document.createElement('thead');
624 const headRow = document.createElement('tr');
625
626 let sortKey = null, sortDir = 'asc';
627 let currentData = [...rows];
628 let currentPage = 1;
629 const pageSize = 8;
630
631 columns.forEach(col => {
632 const th = document.createElement('th');
633 if(col.sortable !== false) th.classList.add('sortable');
634 th.innerHTML = col.header + (col.sortable !== false ? '<span class="sort-arrow">&#8597;</span>' : '');
635 if(col.sortable !== false){
636 th.addEventListener('click', () => {
637 sortKey = col.accessor;
638 sortDir = sortKey === col.accessor && sortDir === 'asc' ? 'desc' : 'asc';
639 sortAndRender();
640 });
641 }
642 headRow.appendChild(th);
643 });
644 const thAct = document.createElement('th');
645 thAct.textContent = 'Actions'; thAct.style.textAlign = 'right';
646 headRow.appendChild(thAct);
647 thead.appendChild(headRow);
648 table.appendChild(thead);
649
650 const tbody = document.createElement('tbody');
651 table.appendChild(tbody);
652 tableWrap.appendChild(table);
653 tableCard.appendChild(tableWrap);
654
655 const pagBar = document.createElement('div');
656 pagBar.className = 'pagination-bar';
657 const pagInfo = document.createElement('span');
658 const pagCtrls = document.createElement('div');
659 pagCtrls.className = 'pag-controls';
660 pagBar.appendChild(pagInfo);
661 pagBar.appendChild(pagCtrls);
662 tableCard.appendChild(pagBar);
663 container.appendChild(tableCard);
664
665 function renderRows(){
666 tbody.innerHTML = '';
667 const total = currentData.length;
668 const totalPages = Math.max(1, Math.ceil(total / pageSize));
669 if(currentPage > totalPages) currentPage = totalPages;
670 const start = (currentPage-1)*pageSize;
671 const slice = currentData.slice(start, start+pageSize);
672
673 pagInfo.textContent = total === 0 ? 'No results' : `${start+1}–${Math.min(start+pageSize,total)} of ${total}`;
674
675 pagCtrls.innerHTML = '';
676 const mkBtn = (label, val, disabled, isActive) => {
677 const b = document.createElement('button');
678 b.className = 'pag-btn' + (isActive ? ' active' : '');
679 b.innerHTML = label; b.disabled = disabled;
680 if(!disabled) b.addEventListener('click', () => { currentPage = val; renderRows(); });
681 return b;
682 };
683 pagCtrls.appendChild(mkBtn('&laquo;',1,currentPage===1,false));
684 pagCtrls.appendChild(mkBtn('&lsaquo;',currentPage-1,currentPage===1,false));
685 for(let p=Math.max(1,currentPage-1);p<=Math.min(totalPages,currentPage+1);p++)
686 pagCtrls.appendChild(mkBtn(p,p,false,p===currentPage));
687 pagCtrls.appendChild(mkBtn('&rsaquo;',currentPage+1,currentPage===totalPages,false));
688 pagCtrls.appendChild(mkBtn('&raquo;',totalPages,currentPage===totalPages,false));
689
690 if(slice.length === 0){
691 const tr = document.createElement('tr'); tr.className = 'no-data-row';
692 const td = document.createElement('td'); td.colSpan = columns.length+1; td.textContent = 'No records found.';
693 tr.appendChild(td); tbody.appendChild(tr); return;
694 }
695
696 slice.forEach((row, idx) => {
697 const tr = document.createElement('tr');
698 tr.className = (start+idx)%2===1 ? 'row-even' : '';
699 columns.forEach(col => {
700 const td = document.createElement('td');
701 td.innerHTML = col.render ? col.render(row[col.accessor], row) : (row[col.accessor] ?? '');
702 tr.appendChild(td);
703 });
704 const tdAct = document.createElement('td');
705 tdAct.style.textAlign = 'right';
706 const actWrap = document.createElement('div');
707 actWrap.className = 'td-actions'; actWrap.style.justifyContent = 'flex-end';
708 if(onEdit){ const b = document.createElement('button'); b.className='btn btn-sm'; b.textContent='Edit'; b.addEventListener('click',()=>onEdit(row)); actWrap.appendChild(b); }
709 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); }
710 tdAct.appendChild(actWrap); tr.appendChild(tdAct); tbody.appendChild(tr);
711 });
712 }
713
714 function sortAndRender(){
715 if(sortKey){
716 currentData.sort((a,b)=>{
717 const va=(a[sortKey]??'').toString(), vb=(b[sortKey]??'').toString();
718 if(!isNaN(va)&&!isNaN(vb)) return sortDir==='asc'?Number(va)-Number(vb):Number(vb)-Number(va);
719 return sortDir==='asc'?va.localeCompare(vb):vb.localeCompare(va);
720 });
721 }
722 renderRows();
723 }
724
725 searchWrap.querySelector('input').addEventListener('input', e => {
726 const q = e.target.value.trim().toLowerCase();
727 currentData = q ? rows.filter(r=>Object.values(r).join(' ').toLowerCase().includes(q)) : [...rows];
728 currentPage = 1; sortAndRender();
729 });
730
731 sortAndRender();
732}
733
734// =============================================================
735// NO WEDDING STATE
736// =============================================================
737function renderNoWedding(container){
738 container.innerHTML = `
739 <div class="card" style="text-align:center;padding:48px 32px;">
740 <div style="font-size:48px;margin-bottom:16px;">💍</div>
741 <h2 style="font-family:'Cormorant Garamond',serif;font-size:26px;margin-bottom:8px;">No Wedding Created Yet</h2>
742 <p class="muted" style="margin-bottom:24px;">Create your wedding to start planning your perfect day.</p>
743 <button class="btn btn-primary" onclick="openCreateWeddingModal()">+ Create Wedding</button>
744 </div>`;
745}
746
747function openCreateWeddingModal(){
748 showModal('Create Your Wedding',
749 `<div class="form-grid">
750 <div class="form-group" style="grid-column:1/-1;">
751 <label class="form-label">Wedding Date</label>
752 <input id="wDate" type="date"/>
753 </div>
754 <div class="form-group">
755 <label class="form-label">Budget ($)</label>
756 <input id="wBudget" type="number" placeholder="15000"/>
757 </div>
758 <div class="form-group" style="grid-column:1/-1;">
759 <label class="form-label">Notes / Theme</label>
760 <textarea id="wNotes" rows="3" placeholder="Summer garden theme..."></textarea>
761 </div>
762 </div>`,
763 async () => {
764 const date = document.getElementById('wDate').value;
765 if(!date){ showToast('Please select a wedding date.','danger'); return; }
766 const res = await api('POST','/api/weddings',{
767 date,
768 budget: document.getElementById('wBudget').value || null,
769 notes: document.getElementById('wNotes').value || null
770 });
771 if(res && res.ok){
772 CURRENT_WEDDING = await res.json();
773 closeModal();
774 showToast('Wedding created! 🎉','success');
775 await reload();
776 } else {
777 showToast('Could not create wedding.','danger');
778 }
779 }
780 );
781}
782
783// =============================================================
784// DASHBOARD
785// =============================================================
786function renderDashboard(container){
787 if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
788 const w = CURRENT_WEDDING;
789 const daysLeft = Math.max(0, Math.ceil((new Date(w.date)-new Date())/86400000));
790
791 container.innerHTML = `
792 <div style="margin-bottom:20px;">
793 <h2 style="font-family:'Cormorant Garamond',serif;font-size:26px;font-weight:700;">
794 Welcome back, <em style="color:var(--rose);">${CURRENT_USER.first_name}</em> 👋
795 </h2>
796 <p class="muted" style="margin-top:4px;">Your wedding overview for <strong>${w.date}</strong></p>
797 </div>
798 <div class="stats-row">
799 <div class="stat-card stat-rose">
800 <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>
801 <div class="stat-label">Days Left</div>
802 <div class="stat-value">${daysLeft}</div>
803 <div class="stat-sub">Until the big day</div>
804 </div>
805 <div class="stat-card stat-gold">
806 <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>
807 <div class="stat-label">Budget</div>
808 <div class="stat-value">$${w.budget ? (w.budget/1000).toFixed(0)+'k' : '—'}</div>
809 <div class="stat-sub">Total budget</div>
810 </div>
811 <div class="stat-card stat-green">
812 <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>
813 <div class="stat-label">Guests</div>
814 <div class="stat-value">${GUESTS.length}</div>
815 <div class="stat-sub">Invited guests</div>
816 </div>
817 <div class="stat-card stat-amber">
818 <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>
819 <div class="stat-label">Venues</div>
820 <div class="stat-value">${VENUE_BOOKINGS.length}</div>
821 <div class="stat-sub">Booked</div>
822 </div>
823 <div class="stat-card stat-purple">
824 <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>
825 <div class="stat-label">Events</div>
826 <div class="stat-value">${EVENTS.length}</div>
827 <div class="stat-sub">Scheduled</div>
828 </div>
829 </div>
830 <div class="dash-grid">
831 <div class="card">
832 <div class="card-header"><h3>Wedding Overview</h3><span class="badge badge-rose"><span class="badge-dot"></span>Planned</span></div>
833 <div class="gold-rule"></div>
834 <div class="info-list">
835 <div class="info-row"><span class="info-label">Date</span><span class="info-val">${w.date}</span></div>
836 <div class="info-row"><span class="info-label">Budget</span><span class="info-val">${w.budget ? '$'+Number(w.budget).toLocaleString() : 'Not set'}</span></div>
837 <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>
838 <div class="info-row"><span class="info-label">Photographers</span><span class="info-val">${PHOTOGRAPHER_BOOKINGS.length} booked</span></div>
839 <div class="info-row"><span class="info-label">Bands</span><span class="info-val">${BAND_BOOKINGS.length} booked</span></div>
840 </div>
841 <div style="display:flex;gap:8px;margin-top:18px;flex-wrap:wrap;">
842 <button class="btn btn-primary" onclick="navigate('#/event')">+ Add Event</button>
843 <button class="btn btn-gold" onclick="navigate('#/venue')">Book Venue</button>
844 <button class="btn" onclick="navigate('#/guests')">View Guests</button>
845 </div>
846 </div>
847 <div class="card">
848 <div class="card-header"><h3>Upcoming Events</h3><button class="btn btn-ghost btn-sm" onclick="navigate('#/event')">Manage all</button></div>
849 <div class="table-wrap" style="max-height:300px;">
850 <table><thead><tr><th>Type</th><th>Date</th><th>Time</th><th>Status</th></tr></thead>
851 <tbody>
852 ${EVENTS.length === 0
853 ? `<tr class="no-data-row"><td colspan="4">No events yet.</td></tr>`
854 : EVENTS.map(e=>`<tr>
855 <td style="font-weight:500;">${e.event_type}</td>
856 <td>${e.date}</td>
857 <td><span class="badge badge-info">${e.start_time}–${e.end_time}</span></td>
858 <td><span class="badge ${e.status==='scheduled'?'badge-success':e.status==='cancelled'?'badge-danger':'badge-warn'}">${e.status}</span></td>
859 </tr>`).join('')}
860 </tbody></table>
861 </div>
862 </div>
863 <div class="card dash-full">
864 <div class="card-header"><h3>Quick Actions</h3></div>
865 <div style="display:flex;gap:10px;flex-wrap:wrap;">
866 <button class="btn btn-primary" onclick="navigate('#/event')">+ New Event</button>
867 <button class="btn btn-gold" onclick="navigate('#/venue')">+ Book Venue</button>
868 <button class="btn" onclick="navigate('#/band')">+ Book Band</button>
869 <button class="btn" onclick="navigate('#/photographer')">+ Book Photographer</button>
870 <button class="btn" onclick="navigate('#/guests')">+ Add Guest</button>
871 <button class="btn" onclick="navigate('#/seating')">🪑 Manage Seating</button>
872 <button class="btn" onclick="navigate('#/registrar')">+ Book Registrar</button>
873 </div>
874 </div>
875 </div>`;
876}
877
878// =============================================================
879// EVENTS PAGE
880// =============================================================
881function renderEventPage(container){
882 if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
883 const wrap = document.createElement('div');
884 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>`;
885 container.appendChild(wrap);
886 wrap.querySelector('#btnAddEvent').addEventListener('click', openEventModal);
887
888 buildDataTable({
889 container: wrap.querySelector('#evTableContainer'),
890 columns: [
891 { header:'Event Type', accessor:'event_type' },
892 { header:'Date', accessor:'date' },
893 { header:'Time', accessor:'time', sortable:false },
894 { header:'Status', accessor:'status', render: v=>`<span class="badge ${v==='scheduled'?'badge-success':v==='cancelled'?'badge-danger':'badge-warn'}">${v}</span>` }
895 ],
896 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 })),
897 onEdit: row => editEventModal(row.id),
898 onDelete: async row => {
899 const res = await api('DELETE',`/api/events/${row.id}`);
900 if(res && res.ok){ showToast('Event deleted.',''); await reload(); }
901 else showToast('Could not delete event.','danger');
902 }
903 });
904}
905
906function openEventModal(){
907 const w = CURRENT_WEDDING;
908 showModal('Add Event',
909 `<div class="form-grid">
910 <div class="form-group"><label class="form-label">Event Type</label><input id="ev_type" placeholder="e.g. Church Ceremony"/></div>
911 <div class="form-group"><label class="form-label">Date</label><input id="ev_date" type="date" value="${w.date}"/></div>
912 <div class="form-group"><label class="form-label">Start Time</label><input id="ev_start" type="time" value="12:00"/></div>
913 <div class="form-group"><label class="form-label">End Time</label><input id="ev_end" type="time" value="14:00"/></div>
914 <div class="form-group"><label class="form-label">Status</label>
915 <select id="ev_status"><option value="scheduled">Scheduled</option><option value="completed">Completed</option><option value="cancelled">Cancelled</option></select>
916 </div>
917 </div>`,
918 async () => {
919 const type = document.getElementById('ev_type').value.trim();
920 if(!type){ showToast('Event type is required.','danger'); return; }
921 const res = await api('POST',`/api/weddings/${CURRENT_WEDDING.wedding_id}/events`,{
922 event_type: type,
923 date: document.getElementById('ev_date').value,
924 start_time: document.getElementById('ev_start').value,
925 end_time: document.getElementById('ev_end').value,
926 status: document.getElementById('ev_status').value
927 });
928 if(res && res.ok){ closeModal(); showToast('Event added.','success'); await reload(); }
929 else { const d=await res.json(); showToast(d.error||'Error adding event.','danger'); }
930 }
931 );
932}
933
934function editEventModal(id){
935 const e = EVENTS.find(x=>x.event_id===id); if(!e) return;
936 showModal('Edit Event',
937 `<div class="form-grid">
938 <div class="form-group"><label class="form-label">Event Type</label><input id="ev_type" value="${e.event_type}"/></div>
939 <div class="form-group"><label class="form-label">Date</label><input id="ev_date" type="date" value="${e.date}"/></div>
940 <div class="form-group"><label class="form-label">Start Time</label><input id="ev_start" type="time" value="${e.start_time}"/></div>
941 <div class="form-group"><label class="form-label">End Time</label><input id="ev_end" type="time" value="${e.end_time}"/></div>
942 <div class="form-group"><label class="form-label">Status</label>
943 <select id="ev_status">
944 <option ${e.status==='scheduled'?'selected':''} value="scheduled">Scheduled</option>
945 <option ${e.status==='completed'?'selected':''} value="completed">Completed</option>
946 <option ${e.status==='cancelled'?'selected':''} value="cancelled">Cancelled</option>
947 </select>
948 </div>
949 </div>`,
950 async () => {
951 const res = await api('PUT',`/api/events/${id}`,{
952 event_type: document.getElementById('ev_type').value,
953 date: document.getElementById('ev_date').value,
954 start_time: document.getElementById('ev_start').value,
955 end_time: document.getElementById('ev_end').value,
956 status: document.getElementById('ev_status').value
957 });
958 if(res && res.ok){ closeModal(); showToast('Event updated.','success'); await reload(); }
959 else showToast('Error updating event.','danger');
960 }
961 );
962}
963
964// =============================================================
965// GUESTS PAGE
966// =============================================================
967function renderGuestsPage(container){
968 if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
969 const wrap = document.createElement('div');
[b7d8a4d]970 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><button class="btn" id="btnUploadGuests">Upload CSV</button></div></div><div id="guestTableContainer"></div></div>`;
[d8deee6]971 container.appendChild(wrap);
972 wrap.querySelector('#btnAddGuest').addEventListener('click', openAddGuestModal);
[b7d8a4d]973 wrap.querySelector('#btnUploadGuests').addEventListener('click', () => {
974 if(!CURRENT_WEDDING){ showToast('Select or create a wedding first.','danger'); return; }
975 const input = document.createElement('input');
976 input.type = 'file';
977 input.accept = '.csv,text/csv';
978 input.addEventListener('change', async (e) => {
979 const file = e.target.files && e.target.files[0];
980 if(!file) return;
981 const text = await file.text();
982 const rows = parseCSV(text);
983 if(rows.length === 0){ showToast('No guests found in CSV.','danger'); return; }
984
985 const previewRows = rows.slice(0,200).map(r=>`<tr><td>${escapeHtml(r.first_name||'')}</td><td>${escapeHtml(r.last_name||'')}</td><td>${escapeHtml(r.email||'')}</td><td>${escapeHtml(r.role||'')}</td></tr>`).join('');
986 const body = `<div style="max-height:320px;overflow:auto;"><table style="width:100%;border-collapse:collapse;"><thead><tr><th style="text-align:left;padding:6px">First</th><th style="text-align:left;padding:6px">Last</th><th style="text-align:left;padding:6px">Email</th><th style="text-align:left;padding:6px">Role</th></tr></thead><tbody>${previewRows}</tbody></table></div><div style="margin-top:12px;"><label><input type="checkbox" id="csvSendEmails"/> Send invitation emails</label></div><p class="muted" style="margin-top:10px;">${rows.length} guests parsed. Showing first ${Math.min(rows.length,200)} rows.</p>`;
987
988 showModal('Import Guests from CSV', body, async () => {
989 const sendEmails = document.getElementById('csvSendEmails').checked;
990 const res = await api('POST', `/api/weddings/${CURRENT_WEDDING.wedding_id}/guests/bulk`, { guests: rows, sendEmails });
991 if(res && res.ok){ const d = await res.json(); closeModal(); showToast(`${d.inserted} guests imported.`,'success'); await reload(); }
992 else { const d = await res.json().catch(()=>null); showToast((d&&d.error) || 'Error importing guests.','danger'); }
993 });
994 });
995 input.click();
996 });
[d8deee6]997
[b7d8a4d]998 // Build table after ensuring we have up-to-date RSVP data for this wedding
999 (async () => {
1000 // Fetch fresh RSVPs for this wedding to ensure the UI reflects guest-submitted responses
1001 try {
1002 const r = await api('GET', `/api/weddings/${CURRENT_WEDDING.wedding_id}/rsvp`);
1003 if (r && r.ok) RSVPS = await r.json();
1004 } catch (e) { /* ignore, we'll use cached RSVPS if available */ }
1005
1006 const guestRsvpMap = {};
1007 if (EVENTS.length > 0) {
1008 const firstEvent = EVENTS[0];
1009 RSVPS.forEach(rv => {
1010 if (String(rv.event_id) === String(firstEvent.event_id)) guestRsvpMap[String(rv.guest_id)] = rv.status;
1011 });
1012 }
[d8deee6]1013
[b7d8a4d]1014 buildDataTable({
1015 container: wrap.querySelector('#guestTableContainer'),
1016 columns: [
1017 { header:'First Name', accessor:'first_name' },
1018 { header:'Last Name', accessor:'last_name' },
1019 { header:'Email', accessor:'email' },
1020 { header:'Status', accessor:'status', render: v => {
1021 const color = v === 'accepted' ? 'badge-success' : v === 'declined' ? 'badge-danger' : v === 'invited' ? 'badge-warn' : 'badge-purple';
1022 return `<span class="badge ${color}">${v || 'pending'}</span>`;
1023 }},
1024 { header:'Invite', accessor:'email', sortable:false, render: (v,row) =>
1025 v ? `<span class="badge badge-success">✉ Invite sent</span>` : `<span class="badge badge-warn">No email</span>`
1026 }
1027 ],
1028 rows: GUESTS.map(g=>({ id:g.guest_id, first_name:g.first_name, last_name:g.last_name, email:g.email||'', status: guestRsvpMap[String(g.guest_id)] || 'pending' })),
1029 onEdit: row => editGuestModal(row.id),
1030 onDelete: async row => {
1031 const res = await api('DELETE',`/api/guests/${row.id}`);
1032 if(res && res.ok){ showToast('Guest removed.',''); await reload(); }
1033 else showToast('Could not remove guest.','danger');
[d8deee6]1034 }
[b7d8a4d]1035 });
1036 })();
1037}
1038
1039// CSV helpers for guest import
1040function splitCSVLine(line){
1041 const out = [];
1042 let cur = '';
1043 let inQuotes = false;
1044 for(let i=0;i<line.length;i++){
1045 const ch = line[i];
1046 if(ch === '"'){
1047 if(inQuotes && line[i+1] === '"'){ cur += '"'; i++; }
1048 else inQuotes = !inQuotes;
1049 } else if(ch === ',' && !inQuotes){ out.push(cur); cur = ''; }
1050 else cur += ch;
1051 }
1052 out.push(cur);
1053 return out.map(s=>s.trim());
1054}
1055
1056function parseCSV(text){
1057 if(!text) return [];
1058 const lines = text.split(/\r?\n/).map(l=>l.trim()).filter(l=>l.length>0);
1059 if(lines.length === 0) return [];
1060 const first = splitCSVLine(lines[0]).map(h=>h.toLowerCase());
1061 const hasHeader = first.some(h=>['first','first_name','first name','last','last_name','email','role'].includes(h));
1062 const rows = [];
1063 const start = hasHeader ? 1 : 0;
1064 let headers = first;
1065 if(!hasHeader) headers = ['first_name','last_name','email','role'];
1066 for(let i=start;i<lines.length;i++){
1067 const cols = splitCSVLine(lines[i]);
1068 const obj = {};
1069 for(let j=0;j<headers.length;j++){
1070 const key = headers[j] ? headers[j].toLowerCase().replace(/ /g,'_') : `col${j}`;
1071 obj[key] = cols[j] ? cols[j].trim().replace(/^"|"$/g,'') : '';
[d8deee6]1072 }
[b7d8a4d]1073 const out = {
1074 first_name: obj.first_name || obj.first || '',
1075 last_name: obj.last_name || obj.last || '',
1076 email: obj.email || '',
1077 role: obj.role || ''
1078 };
1079 if(out.first_name || out.last_name) rows.push(out);
1080 }
1081 return rows;
[d8deee6]1082}
1083
[b7d8a4d]1084function escapeHtml(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
1085
[d8deee6]1086function openAddGuestModal(){
1087 showModal('Add Guest',
1088 `<div class="form-grid">
1089 <div class="form-group"><label class="form-label">First Name</label><input id="gFirst" placeholder="First name"/></div>
1090 <div class="form-group"><label class="form-label">Last Name</label><input id="gLast" placeholder="Last name"/></div>
1091 <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>
1092 <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>
1093 </div>`,
1094 async () => {
1095 const first = document.getElementById('gFirst').value.trim();
1096 if(!first){ showToast('First name is required.','danger'); return; }
1097 const res = await api('POST',`/api/weddings/${CURRENT_WEDDING.wedding_id}/guests`,{
1098 first_name: first,
1099 last_name: document.getElementById('gLast').value.trim(),
1100 email: document.getElementById('gEmail').value.trim(),
1101 role: document.getElementById('gRole').value
1102 });
1103 if(res && res.ok){
1104 const d = await res.json();
1105 closeModal();
1106 showToast(d.emailSent ? 'Guest added & invitation email sent! 📧' : 'Guest added (no email provided).','success');
1107 await reload();
1108 } else showToast('Could not add guest.','danger');
1109 }
1110 );
1111}
1112
1113function editGuestModal(id){
1114 const g = GUESTS.find(x=>x.guest_id===id); if(!g) return;
1115 showModal('Edit Guest',
1116 `<div class="form-grid">
1117 <div class="form-group"><label class="form-label">First Name</label><input id="gFirst" value="${g.first_name}"/></div>
1118 <div class="form-group"><label class="form-label">Last Name</label><input id="gLast" value="${g.last_name}"/></div>
1119 <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Email</label><input id="gEmail" value="${g.email||''}"/></div>
1120 <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>
1121 </div>`,
1122 async () => {
1123 const res = await api('PUT',`/api/guests/${id}`,{
1124 first_name: document.getElementById('gFirst').value,
1125 last_name: document.getElementById('gLast').value,
1126 email: document.getElementById('gEmail').value,
1127 role: document.getElementById('gRole').value
1128 });
1129 if(res && res.ok){ closeModal(); showToast('Guest updated.','success'); await reload(); }
1130 else showToast('Error updating guest.','danger');
1131 }
1132 );
1133}
1134
1135// =============================================================
1136// SEATING PAGE (NEW — UC0011)
1137// =============================================================
1138function renderSeatingPage(container){
1139 if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
1140
1141 const wrap = document.createElement('div');
1142
1143 // Build event selector
1144 const eventOptions = EVENTS.map(e=>`<option value="${e.event_id}">${e.event_type} (${e.date})</option>`).join('');
1145
1146 wrap.innerHTML = `
1147 <div class="card" style="margin-bottom:18px;">
1148 <div class="card-header">
1149 <h3>🪑 Table Seating</h3>
1150 <div class="card-actions">
1151 <select id="seatEventSel" style="width:220px;">
1152 <option value="">— Select Event —</option>
1153 ${eventOptions}
1154 </select>
1155 <button class="btn btn-primary" id="btnAssignSeat">+ Assign Guest</button>
1156 </div>
1157 </div>
1158 <div class="gold-rule"></div>
1159 <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>
1160 <div id="seatingBoard"></div>
1161 </div>
1162 <div class="card">
1163 <div class="card-header"><h3>All Assignments</h3></div>
1164 <div id="seatingTableContainer"></div>
1165 </div>`;
1166
1167 container.appendChild(wrap);
1168
1169 const sel = wrap.querySelector('#seatEventSel');
1170 sel.addEventListener('change', () => renderSeatingBoard(wrap, sel.value));
1171 wrap.querySelector('#btnAssignSeat').addEventListener('click', () => openAssignSeatModal(sel.value));
1172
1173 // Full table view
1174 buildDataTable({
1175 container: wrap.querySelector('#seatingTableContainer'),
1176 columns: [
1177 { header:'Guest', accessor:'guest_name' },
1178 { header:'Event', accessor:'event_type' },
1179 { header:'Table #', accessor:'table_number', render: v => v ? `<span class="badge badge-gold">Table ${v}</span>` : '<span class="badge badge-warn">Unassigned</span>' },
1180 { header:'Role', accessor:'role', render: v => `<span class="badge badge-purple">${v}</span>` },
1181 { header:'Status', accessor:'status', render: v => `<span class="badge ${v==='attending'?'badge-success':'badge-danger'}">${v}</span>` }
1182 ],
1183 rows: ATTENDANCE.map(a=>({
1184 id: a.attendance_id,
1185 guest_name: `${a.first_name} ${a.last_name}`,
1186 event_type: a.event_type,
1187 table_number: a.table_number,
1188 role: a.role,
1189 status: a.status
1190 })),
1191 onEdit: row => openEditSeatModal(row.id),
1192 onDelete: async row => {
1193 const res = await api('DELETE',`/api/attendance/${row.id}`);
1194 if(res && res.ok){ showToast('Assignment removed.',''); await reload(); }
1195 else showToast('Error removing assignment.','danger');
1196 }
1197 });
1198}
1199
1200function renderSeatingBoard(wrap, eventId){
1201 const board = wrap.querySelector('#seatingBoard');
1202 if(!eventId){ board.innerHTML = '<p class="muted">Select an event above to see the seating layout.</p>'; return; }
1203
1204 const eventAtt = ATTENDANCE.filter(a => String(a.event_id) === String(eventId));
1205 const assigned = eventAtt.filter(a => a.table_number);
1206 const unassigned = eventAtt.filter(a => !a.table_number);
1207
1208 // Group by table
1209 const tables = {};
1210 assigned.forEach(a => {
1211 if(!tables[a.table_number]) tables[a.table_number] = [];
1212 tables[a.table_number].push(a);
1213 });
1214
1215 const tableNums = Object.keys(tables).map(Number).sort((a,b)=>a-b);
1216
1217 let html = '';
1218
1219 if(tableNums.length === 0 && unassigned.length === 0){
1220 html = '<p class="muted">No guests assigned to this event yet. Click "+ Assign Guest" to start.</p>';
1221 } else {
1222 if(tableNums.length > 0){
1223 html += `<h4 style="font-family:'Cormorant Garamond',serif;font-size:17px;margin-bottom:12px;color:var(--text-dark);">Seating Layout</h4>
1224 <div class="seating-grid">
1225 ${tableNums.map(tnum => `
1226 <div class="table-card-seat">
1227 <div class="table-title">🍽 Table ${tnum} <span style="font-size:12px;font-weight:400;color:var(--text-light);">(${tables[tnum].length} guests)</span></div>
1228 ${tables[tnum].map(a=>`
1229 <div class="seat-guest">
1230 <span>${a.first_name} ${a.last_name}</span>
1231 <span class="seat-role">${a.role}</span>
1232 </div>`).join('')}
1233 </div>`).join('')}
1234 </div>`;
1235 }
1236 if(unassigned.length > 0){
1237 html += `<h4 style="font-family:'Cormorant Garamond',serif;font-size:17px;margin:20px 0 10px;color:var(--amber);">⚠ Unassigned Guests (${unassigned.length})</h4>
1238 <div class="unassigned-list">
1239 ${unassigned.map(a=>`
1240 <div class="unassigned-item">
1241 <span>${a.first_name} ${a.last_name} <span style="color:var(--text-light);font-size:12px;">(${a.role})</span></span>
1242 <button class="btn btn-sm btn-gold" onclick="openEditSeatModal(${a.attendance_id})">Assign Table</button>
1243 </div>`).join('')}
1244 </div>`;
1245 }
1246 }
1247
1248 board.innerHTML = html;
1249}
1250
1251function openAssignSeatModal(preselectedEventId){
1252 const eventOptions = EVENTS.map(e=>`<option value="${e.event_id}" ${String(e.event_id)===String(preselectedEventId)?'selected':''}>${e.event_type} (${e.date})</option>`).join('');
1253 const guestOptions = GUESTS.map(g=>`<option value="${g.guest_id}">${g.first_name} ${g.last_name}</option>`).join('');
1254
1255 showModal('Assign Guest to Table',
1256 `<div class="form-grid">
1257 <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Event</label><select id="attEvent">${eventOptions}</select></div>
1258 <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Guest</label><select id="attGuest">${guestOptions}</select></div>
1259 <div class="form-group"><label class="form-label">Table Number</label><input id="attTable" type="number" min="1" placeholder="e.g. 5"/></div>
1260 <div class="form-group"><label class="form-label">Role</label>
1261 <select id="attRole">
1262 <option value="Guest">Guest</option>
1263 <option value="Best Man">Best Man</option>
1264 <option value="Maid of Honor">Maid of Honor</option>
1265 <option value="Bridesmaid">Bridesmaid</option>
1266 <option value="Groomsman">Groomsman</option>
1267 <option value="Family">Family</option>
1268 <option value="VIP">VIP</option>
1269 </select>
1270 </div>
1271 <div class="form-group" style="grid-column:1/-1;"><p class="muted" style="margin:0;">Set the attendance status manually.</p></div>
1272 </div>`,
1273 async () => {
1274 const res = await api('POST','/api/attendance',{
1275 event_id: document.getElementById('attEvent').value,
1276 guest_id: document.getElementById('attGuest').value,
1277 table_number: document.getElementById('attTable').value || null,
1278 role: document.getElementById('attRole').value
1279 });
1280 if(res && res.ok){ closeModal(); showToast('Guest assigned to table!','success'); await reload(); }
1281 else { const d=await res.json(); showToast(d.error||'Error assigning guest.','danger'); }
1282 }
1283 );
1284}
1285
1286function openEditSeatModal(id){
1287 const a = ATTENDANCE.find(x=>x.attendance_id===id); if(!a) return;
1288 showModal('Edit Seating Assignment',
1289 `<div class="form-grid">
1290 <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>
1291 <div class="form-group"><label class="form-label">Table Number</label><input id="attTable" type="number" min="1" value="${a.table_number||''}"/></div>
1292 <div class="form-group"><label class="form-label">Role</label>
1293 <select id="attRole">
1294 ${['Guest','Best Man','Maid of Honor','Bridesmaid','Groomsman','Family','VIP'].map(r=>`<option ${a.role===r?'selected':''}>${r}</option>`).join('')}
1295 </select>
1296 </div>
1297 <div class="form-group"><label class="form-label">Status</label>
1298 <select id="attStatus">
1299 <option ${a.status==='attending'?'selected':''} value="attending">Attending</option>
1300 <option ${a.status==='not_attending'?'selected':''} value="not_attending">Not Attending</option>
1301 </select>
1302 </div>
1303 <div class="form-group" style="grid-column:1/-1;"><p class="muted" style="margin:0;">Set the attendance status manually.</p></div>
1304 </div>`,
1305 async () => {
1306 const res = await api('PUT',`/api/attendance/${id}`,{
1307 table_number: document.getElementById('attTable').value || null,
1308 role: document.getElementById('attRole').value,
1309 status: document.getElementById('attStatus').value
1310 });
1311 if(res && res.ok){ closeModal(); showToast('Seating updated!','success'); await reload(); }
1312 else showToast('Error updating seating.','danger');
1313 }
1314 );
1315}
1316
1317// =============================================================
1318// VENUE PAGE
1319// =============================================================
1320function renderVenuePage(container){
1321 if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
1322 const wrap = document.createElement('div');
1323 wrap.innerHTML = `
1324 <div class="card" style="margin-bottom:18px;">
1325 <div class="card-header"><h3>Venues</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookVenue">+ Book Venue</button></div></div>
1326 <div id="venueTableContainer"></div>
1327 </div>
1328 <div class="card">
1329 <div class="card-header"><h3>Venue Bookings</h3></div>
1330 <div id="venueBookingTableContainer"></div>
1331 </div>`;
1332 container.appendChild(wrap);
1333 wrap.querySelector('#btnBookVenue').addEventListener('click', openVenueBookingModal);
1334
1335 buildDataTable({
1336 container: wrap.querySelector('#venueTableContainer'),
1337 columns: [
1338 { header:'Venue Name', accessor:'name' },
1339 { header:'City', accessor:'city' },
1340 { header:'Capacity', accessor:'capacity' },
1341 { header:'Type', accessor:'type_name' },
1342 { header:'Price/Guest', accessor:'price_per_guest', render: v=>`<strong>$${v}</strong>` }
1343 ],
1344 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 })),
1345 onEdit: row => showToast(`Viewing ${row.name}`,'')
1346 });
1347
1348 buildDataTable({
1349 container: wrap.querySelector('#venueBookingTableContainer'),
1350 columns: [
1351 { header:'Venue', accessor:'venue_name' },
1352 { header:'Date', accessor:'date' },
1353 { header:'Time', accessor:'time', sortable:false },
1354 { header:'Price', accessor:'price', render: v=>`$${Number(v).toLocaleString()}` },
1355 { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` }
1356 ],
1357 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 })),
1358 onDelete: async row => {
1359 const res = await api('DELETE',`/api/venue-bookings/${row.id}`);
1360 if(res && res.ok){ showToast('Booking cancelled.',''); await reload(); }
1361 else showToast('Could not cancel booking.','danger');
1362 }
1363 });
1364}
1365
1366function openVenueBookingModal(){
1367 const w = CURRENT_WEDDING;
1368 const venueOptions = VENUES.map(v=>`<option value="${v.venue_id}">${v.name} — $${v.price_per_guest}/guest</option>`).join('');
1369 showModal('Book a Venue',
1370 `<div class="form-grid">
1371 <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Venue</label><select id="venueSelect">${venueOptions}</select></div>
1372 <div class="form-group"><label class="form-label">Date</label><input id="venueDate" type="date" value="${w.date}"/></div>
1373 <div class="form-group"><label class="form-label">Price ($)</label><input id="venuePrice" type="number" placeholder="0"/></div>
1374 <div class="form-group"><label class="form-label">Start Time</label><input id="venueStart" type="time" value="16:00"/></div>
1375 <div class="form-group"><label class="form-label">End Time</label><input id="venueEnd" type="time" value="23:00"/></div>
1376 </div>`,
1377 async () => {
1378 const res = await api('POST',`/api/weddings/${w.wedding_id}/venue-bookings`,{
1379 venue_id: document.getElementById('venueSelect').value,
1380 date: document.getElementById('venueDate').value,
1381 start_time: document.getElementById('venueStart').value,
1382 end_time: document.getElementById('venueEnd').value,
1383 price: document.getElementById('venuePrice').value || 0,
1384 status: 'confirmed'
1385 });
1386 if(res && res.ok){ closeModal(); showToast('Venue booked!','success'); await reload(); }
1387 else { const d=await res.json(); showToast(d.error||'Error booking venue.','danger'); }
1388 }
1389 );
1390}
1391
1392// =============================================================
1393// BAND PAGE
1394// =============================================================
1395function renderBandPage(container){
1396 if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
1397 const wrap = document.createElement('div');
1398 wrap.innerHTML = `
1399 <div class="card" style="margin-bottom:18px;">
1400 <div class="card-header"><h3>Bands</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookBand">+ Book Band</button></div></div>
1401 <div id="bandTableContainer"></div>
1402 </div>
1403 <div class="card"><div class="card-header"><h3>Band Bookings</h3></div><div id="bandBookingsContainer"></div></div>`;
1404 container.appendChild(wrap);
1405 wrap.querySelector('#btnBookBand').addEventListener('click', openBandBookingModal);
1406
1407 buildDataTable({
1408 container: wrap.querySelector('#bandTableContainer'),
1409 columns: [
1410 { header:'Band Name', accessor:'band_name' },
1411 { header:'Genre', accessor:'genre' },
1412 { header:'Equipment', accessor:'equipment', sortable:false },
1413 { header:'Price/Hour', accessor:'price_per_hour', render: v=>`<strong>$${v}</strong>` }
1414 ],
1415 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 })),
1416 onEdit: row => showToast(`Viewing ${row.band_name}`,'')
1417 });
1418
1419 buildDataTable({
1420 container: wrap.querySelector('#bandBookingsContainer'),
1421 columns: [
1422 { header:'Band', accessor:'band_name' },
1423 { header:'Date', accessor:'date' },
1424 { header:'Time', accessor:'time', sortable:false },
1425 { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` }
1426 ],
1427 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 })),
1428 onDelete: async row => {
1429 const res = await api('DELETE',`/api/band-bookings/${row.id}`);
1430 if(res && res.ok){ showToast('Band booking removed.',''); await reload(); }
1431 else showToast('Error removing booking.','danger');
1432 }
1433 });
1434}
1435
1436function openBandBookingModal(){
1437 const w = CURRENT_WEDDING;
1438 const opts = BANDS.map(b=>`<option value="${b.band_id}">${b.band_name} — $${b.price_per_hour}/hr</option>`).join('');
1439 showModal('Book a Band',
1440 `<div class="form-grid">
1441 <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Band</label><select id="bandSel">${opts}</select></div>
1442 <div class="form-group"><label class="form-label">Date</label><input id="bandDate" type="date" value="${w.date}"/></div>
1443 <div class="form-group"><label class="form-label">Start Time</label><input id="bandStart" type="time" value="19:00"/></div>
1444 <div class="form-group"><label class="form-label">End Time</label><input id="bandEnd" type="time" value="23:00"/></div>
1445 </div>`,
1446 async () => {
1447 const res = await api('POST',`/api/weddings/${w.wedding_id}/band-bookings`,{
1448 band_id: document.getElementById('bandSel').value,
1449 date: document.getElementById('bandDate').value,
1450 start_time: document.getElementById('bandStart').value,
1451 end_time: document.getElementById('bandEnd').value,
1452 status: 'confirmed'
1453 });
1454 if(res && res.ok){ closeModal(); showToast('Band booked!','success'); await reload(); }
1455 else { const d=await res.json(); showToast(d.error||'Error booking band.','danger'); }
1456 }
1457 );
1458}
1459
1460// =============================================================
1461// PHOTOGRAPHER PAGE
1462// =============================================================
1463function renderPhotographerPage(container){
1464 if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
1465 const wrap = document.createElement('div');
1466 wrap.innerHTML = `
1467 <div class="card" style="margin-bottom:18px;">
1468 <div class="card-header"><h3>Photographers</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookPhotog">+ Book Photographer</button></div></div>
1469 <div id="photogTableContainer"></div>
1470 </div>
1471 <div class="card"><div class="card-header"><h3>Photographer Bookings</h3></div><div id="photogBookingContainer"></div></div>`;
1472 container.appendChild(wrap);
1473 wrap.querySelector('#btnBookPhotog').addEventListener('click', openPhotographerBookingModal);
1474
1475 buildDataTable({
1476 container: wrap.querySelector('#photogTableContainer'),
1477 columns: [
1478 { header:'Name', accessor:'name' },
1479 { header:'Email', accessor:'email', sortable:false },
1480 { header:'Phone', accessor:'phone', sortable:false },
1481 { header:'Price/Hour', accessor:'price_per_hour', render: v=>`<strong>$${v}</strong>` }
1482 ],
1483 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 })),
1484 onEdit: row => showToast(`Viewing ${row.name}`,'')
1485 });
1486
1487 buildDataTable({
1488 container: wrap.querySelector('#photogBookingContainer'),
1489 columns: [
1490 { header:'Photographer', accessor:'name' },
1491 { header:'Date', accessor:'date' },
1492 { header:'Time', accessor:'time', sortable:false },
1493 { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` }
1494 ],
1495 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 })),
1496 onDelete: async row => {
1497 const res = await api('DELETE',`/api/photographer-bookings/${row.id}`);
1498 if(res && res.ok){ showToast('Booking removed.',''); await reload(); }
1499 else showToast('Error removing booking.','danger');
1500 }
1501 });
1502}
1503
1504function openPhotographerBookingModal(){
1505 const w = CURRENT_WEDDING;
1506 const opts = PHOTOGRAPHERS.map(p=>`<option value="${p.photographer_id}">${p.name} — $${p.price_per_hour}/hr</option>`).join('');
1507 showModal('Book a Photographer',
1508 `<div class="form-grid">
1509 <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Photographer</label><select id="photogSel">${opts}</select></div>
1510 <div class="form-group"><label class="form-label">Date</label><input id="photogDate" type="date" value="${w.date}"/></div>
1511 <div class="form-group"><label class="form-label">Start Time</label><input id="photogStart" type="time" value="14:00"/></div>
1512 <div class="form-group"><label class="form-label">End Time</label><input id="photogEnd" type="time" value="22:00"/></div>
1513 </div>`,
1514 async () => {
1515 const res = await api('POST',`/api/weddings/${w.wedding_id}/photographer-bookings`,{
1516 photographer_id: document.getElementById('photogSel').value,
1517 date: document.getElementById('photogDate').value,
1518 start_time: document.getElementById('photogStart').value,
1519 end_time: document.getElementById('photogEnd').value,
1520 status: 'confirmed'
1521 });
1522 if(res && res.ok){ closeModal(); showToast('Photographer booked!','success'); await reload(); }
1523 else { const d=await res.json(); showToast(d.error||'Error booking photographer.','danger'); }
1524 }
1525 );
1526}
1527
1528// =============================================================
1529// CHURCH PAGE
1530// =============================================================
1531function renderChurchPage(container){
1532 if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
1533 const wrap = document.createElement('div');
1534 wrap.innerHTML = `
1535 <div class="card" style="margin-bottom:18px;">
1536 <div class="card-header"><h3>Churches</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookChurch">+ Book Church</button></div></div>
1537 <div id="churchTableContainer"></div>
1538 </div>
1539 <div class="card"><div class="card-header"><h3>Church Bookings</h3></div><div id="churchBookingContainer"></div></div>`;
1540 container.appendChild(wrap);
1541 wrap.querySelector('#btnBookChurch').addEventListener('click', openChurchBookingModal);
1542
1543 buildDataTable({
1544 container: wrap.querySelector('#churchTableContainer'),
1545 columns: [
1546 { header:'Church', accessor:'name' },
1547 { header:'Location', accessor:'location' },
1548 { header:'Contact', accessor:'contact', sortable:false },
1549 { header:'Priest', accessor:'priest_name', sortable:false }
1550 ],
1551 rows: CHURCHES.map(c=>({ id:c.church_id, name:c.name, location:c.location, contact:c.contact, priest_name:c.priest_name||'—' })),
1552 onEdit: row => showToast(`Viewing ${row.name}`,'')
1553 });
1554
1555 buildDataTable({
1556 container: wrap.querySelector('#churchBookingContainer'),
1557 columns: [
1558 { header:'Church', accessor:'church_name' },
1559 { header:'Date', accessor:'date' },
1560 { header:'Time', accessor:'time', sortable:false },
1561 { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` },
1562 { header:'Actions', accessor:'id', sortable:false, render: id=>`<button class="btn btn-sm btn-danger" onclick="deleteChurchBooking(${id})">Delete</button>` }
1563 ],
1564 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 })),
1565 onEdit: undefined
1566 });
1567}
1568
1569// =============================================================
1570// REGISTRAR PAGE
1571// =============================================================
1572function renderRegistrarPage(container){
1573 if(!CURRENT_WEDDING){ renderNoWedding(container); return; }
1574 const wrap = document.createElement('div');
1575 wrap.innerHTML = `
1576 <div class="card" style="margin-bottom:18px;">
1577 <div class="card-header"><h3>Registrars</h3><div class="card-actions"><button class="btn btn-primary" id="btnBookReg">+ Book Registrar</button></div></div>
1578 <div id="regTableContainer"></div>
1579 </div>
1580 <div class="card"><div class="card-header"><h3>Registrar Bookings</h3></div><div id="regBookingContainer"></div></div>`;
1581 container.appendChild(wrap);
1582 wrap.querySelector('#btnBookReg').addEventListener('click', openRegistrarBookingModal);
1583
1584 buildDataTable({
1585 container: wrap.querySelector('#regTableContainer'),
1586 columns: [
1587 { header:'Name', accessor:'name' },
1588 { header:'Location', accessor:'location' },
1589 { header:'Working Hours', accessor:'working_hours', sortable:false },
1590 { header:'Contact', accessor:'contact', sortable:false }
1591 ],
1592 rows: REGISTRARS.map(r=>({ id:r.registrar_id, name:r.name, location:r.location, working_hours:r.working_hours||'—', contact:r.contact })),
1593 onEdit: row => showToast(`Viewing ${row.name}`,'')
1594 });
1595
1596 buildDataTable({
1597 container: wrap.querySelector('#regBookingContainer'),
1598 columns: [
1599 { header:'Registrar', accessor:'registrar_name' },
1600 { header:'Date', accessor:'date' },
1601 { header:'Time', accessor:'time', sortable:false },
1602 { header:'Status', accessor:'status', render: v=>`<span class="badge badge-success">${v}</span>` }
1603 ],
1604 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 })),
1605 onDelete: async row => {
1606 const res = await api('DELETE',`/api/registrar-bookings/${row.id}`);
1607 if(res && res.ok){ showToast('Booking removed.',''); await reload(); }
1608 else showToast('Error removing booking.','danger');
1609 }
1610 });
1611}
1612
1613function openRegistrarBookingModal(){
1614 const w = CURRENT_WEDDING;
1615 const opts = REGISTRARS.map(r=>`<option value="${r.registrar_id}">${r.name}</option>`).join('');
1616 showModal('Book Registrar',
1617 `<div class="form-grid">
1618 <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Registrar</label><select id="regSel">${opts}</select></div>
1619 <div class="form-group"><label class="form-label">Date</label><input id="regDate" type="date" value="${w.date}"/></div>
1620 <div class="form-group"><label class="form-label">Start Time</label><input id="regStart" type="time" value="10:00"/></div>
1621 <div class="form-group"><label class="form-label">End Time</label><input id="regEnd" type="time" value="12:00"/></div>
1622 </div>`,
1623 async () => {
1624 const res = await api('POST',`/api/weddings/${w.wedding_id}/registrar-bookings`,{
1625 registrar_id: document.getElementById('regSel').value,
1626 date: document.getElementById('regDate').value,
1627 start_time: document.getElementById('regStart').value,
1628 end_time: document.getElementById('regEnd').value,
1629 status: 'confirmed'
1630 });
1631 if(res && res.ok){ closeModal(); showToast('Registrar booked!','success'); await reload(); }
1632 else { const d=await res.json(); showToast(d.error||'Error booking registrar.','danger'); }
1633 }
1634 );
1635}
1636
1637function openChurchBookingModal(){
1638 const w = CURRENT_WEDDING;
1639 const churchOptions = CHURCHES.map(c=>`<option value="${c.church_id}">${c.name}</option>`).join('');
1640 showModal('Book a Church',
1641 `<div class="form-grid">
1642 <div class="form-group" style="grid-column:1/-1;"><label class="form-label">Church</label><select id="churchSelect">${churchOptions}</select></div>
1643 <div class="form-group"><label class="form-label">Date</label><input id="churchDate" type="date" value="${w.date}"/></div>
1644 <div class="form-group"><label class="form-label">Start Time</label><input id="churchStart" type="time" value="14:00"/></div>
1645 <div class="form-group"><label class="form-label">End Time</label><input id="churchEnd" type="time" value="15:00"/></div>
1646 </div>`,
1647 async () => {
1648 const res = await api('POST',`/api/weddings/${w.wedding_id}/church-bookings`,{
1649 church_id: document.getElementById('churchSelect').value,
1650 date: document.getElementById('churchDate').value,
1651 start_time: document.getElementById('churchStart').value,
1652 end_time: document.getElementById('churchEnd').value,
1653 status: 'confirmed'
1654 });
1655 if(res && res.ok){ closeModal(); showToast('Church booked!','success'); await reload(); }
1656 else { const d=await res.json(); showToast(d.error||'Error booking church.','danger'); }
1657 }
1658 );
1659}
1660
1661async function deleteChurchBooking(id){
1662 if(!confirm('Delete this church booking?')) return;
1663 const res = await api('DELETE',`/api/church-bookings/${id}`);
1664 if(res && res.ok){ showToast('Booking removed.','success'); await reload(); }
1665 else showToast('Error removing booking.','danger');
1666}
1667
1668// =============================================================
1669// SETTINGS PAGE
1670// =============================================================
1671function renderSettingsPage(container){
1672 const u = CURRENT_USER;
1673 const wrap = document.createElement('div');
1674 wrap.innerHTML = `
1675 <div class="card">
1676 <div class="card-header"><h3>Profile Settings</h3><span class="badge badge-rose">User #${u.user_id}</span></div>
1677 <div class="gold-rule"></div>
1678 <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);">
1679 <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;">
1680 ${u.first_name[0]}${u.last_name[0]}
1681 </div>
1682 <div>
1683 <div style="font-family:'Cormorant Garamond',serif;font-size:20px;font-weight:700;">${u.first_name} ${u.last_name}</div>
1684 <div class="muted">${u.email}</div>
1685 </div>
1686 </div>
1687 <div class="form-grid">
1688 <div class="form-group"><label class="form-label">First Name</label><input id="sFirst" value="${u.first_name}"/></div>
1689 <div class="form-group"><label class="form-label">Last Name</label><input id="sLast" value="${u.last_name}"/></div>
1690 <div class="form-group"><label class="form-label">Email</label><input id="sEmail" type="email" value="${u.email}"/></div>
1691 <div class="form-group"><label class="form-label">Phone</label><input id="sPhone" value="${u.phone_number||''}"/></div>
1692 <div class="form-group"><label class="form-label">Gender</label>
1693 <select id="sGender">
1694 <option ${u.gender==='Female'?'selected':''}>Female</option>
1695 <option ${u.gender==='Male'?'selected':''}>Male</option>
1696 <option ${u.gender==='Other'?'selected':''}>Other</option>
1697 </select>
1698 </div>
1699 </div>
1700 <div style="display:flex;gap:10px;margin-top:20px;">
1701 <button class="btn btn-primary" id="btnSaveProfile">Save Changes</button>
1702 <button class="btn btn-ghost" onclick="renderApp()">Reset</button>
1703 </div>
1704 </div>`;
1705 container.appendChild(wrap);
1706
1707 wrap.querySelector('#btnSaveProfile').addEventListener('click', async () => {
1708 const res = await api('PUT',`/api/users/${u.user_id}`,{
1709 first_name: document.getElementById('sFirst').value,
1710 last_name: document.getElementById('sLast').value,
1711 email: document.getElementById('sEmail').value,
1712 phone_number: document.getElementById('sPhone').value,
1713 gender: document.getElementById('sGender').value
1714 });
1715 if(res && res.ok){
1716 CURRENT_USER = await res.json();
1717 document.getElementById('userName').textContent = `${CURRENT_USER.first_name} ${CURRENT_USER.last_name}`;
1718 document.getElementById('userAvatar').textContent = `${CURRENT_USER.first_name[0]}${CURRENT_USER.last_name[0]}`;
1719 showToast('Profile saved!','success');
1720 } else showToast('Error saving profile.','danger');
1721 });
1722}
1723
1724// =============================================================
1725// UTILITY
1726// =============================================================
[b7d8a4d]1727// When the window gains focus, refresh wedding-specific data so changes submitted via RSVP links
1728// (which happen outside this UI) are reflected in both Guests and Seating pages.
1729window.addEventListener('focus', () => { if (CURRENT_WEDDING) reload(); });
1730
[d8deee6]1731function focusSearch(){
1732 const s = document.querySelector('.search-input');
1733 if(s) s.focus();
1734}
1735</script>
1736</body>
1737</html>
Note: See TracBrowser for help on using the repository browser.