source: Wedding_Planner.html@ 5b2dd1d

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

Add files via upload

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