source: Wedding_Planner.html@ d8deee6

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

Add files via upload

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