File size: 37,180 Bytes
64b6fa7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
import streamlit as st
import pandas as pd
from datetime import datetime, date
import plotly.express as px
import plotly.graph_objects as go
from config_manager import ConfigManager
from vendors import VendorManager

class Dashboard:
    def __init__(self):
        self.config_manager = ConfigManager()
    
    def calculate_actualized_cost(self, vendors):
        """Calculate total cost for only booked/ordered/delivered items"""
        total_cost = 0
        
        for vendor in vendors:
            vendor_type = vendor.get('type', 'Vendor/Service')
            vendor_total_cost = vendor.get('total_cost', vendor.get('cost', 0))
            
            # Only include cost if vendor is booked or item is ordered/delivered
            should_include = False
            if vendor_type == 'Vendor/Service':
                if vendor.get('status') == 'Booked':
                    should_include = True
            elif vendor_type == 'Item/Purchase':
                status = vendor.get('status', 'Researching')
                if status in ['Ordered', 'Shipped', 'Delivered']:
                    should_include = True
            
            if should_include:
                total_cost += vendor_total_cost
        
        return total_cost
    
    def render(self, config):
        wedding_info = config.get('wedding_info', {})
        venue_city = wedding_info.get('venue_city', '')
        
        # Display header with location
        if venue_city:
            st.markdown(f"## πŸ“Š Wedding Dashboard - {venue_city}")
        else:
            st.markdown("## πŸ“Š Wedding Dashboard")
        
        # Main metrics row
        col1, col2, col3, col4 = st.columns(4)
        
        with col1:
            self.render_countdown_card(wedding_info)
        
        with col2:
            self.render_guest_count_card()
        
        with col3:
            self.render_task_progress_card()
        
        with col4:
            self.render_budget_card()
        
        # Add prominent cost breakdown section
        st.markdown("---")
        
        # Calculate costs for the breakdown section
        vendors = self.config_manager.load_json_data('vendors.json')
        total_estimated_cost = sum([v.get('total_cost', v.get('cost', 0)) for v in vendors])
        total_actualized_cost = self.calculate_actualized_cost(vendors)
        pending_cost = total_estimated_cost - total_actualized_cost
        
        col1, col2, col3 = st.columns(3)
        
        with col1:
            st.markdown("### πŸ’° Total Estimated Cost")
            st.markdown(f"**${total_estimated_cost:,.0f}**")
            st.caption("All vendors & items regardless of status")
        
        with col2:
            st.markdown("### βœ… Actualized Cost")
            st.markdown(f"**${total_actualized_cost:,.0f}**")
            st.caption("Only booked/ordered/delivered items")
        
        with col3:
            st.markdown("### ⏳ Pending Cost")
            st.markdown(f"**${pending_cost:,.0f}**")
            st.caption("Estimated costs not yet confirmed")
        
        # Charts section
        col1, col2 = st.columns(2)
        
        with col1:
            self.render_task_progress_chart()
        
        with col2:
            self.render_guest_rsvp_chart()
        
        # Upcoming payments
        self.render_upcoming_payments()
        
        # Food choices section
        self.render_food_choices_by_event()
        
        # Upcoming tasks
        self.render_upcoming_tasks()
    
    def render_countdown_card(self, wedding_info):
        st.markdown("""
        <div class="metric-card">
            <h3>⏰ Wedding Countdown</h3>
        """, unsafe_allow_html=True)
        
        wedding_start_str = wedding_info.get('wedding_start_date', '')
        wedding_end_str = wedding_info.get('wedding_end_date', '')
        
        if wedding_start_str and wedding_end_str:
            try:
                wedding_start = datetime.fromisoformat(wedding_start_str).date()
                wedding_end = datetime.fromisoformat(wedding_end_str).date()
                today = date.today()
                
                if today < wedding_start:
                    days_until = (wedding_start - today).days
                    st.markdown(f"<h1 style='margin: 0; color: white;'>{days_until}</h1>", unsafe_allow_html=True)
                    if days_until == 0:
                        st.markdown("<p style='margin: 0; color: white;'>Festivities begin today! πŸŽ‰</p>", unsafe_allow_html=True)
                    elif days_until == 1:
                        st.markdown("<p style='margin: 0; color: white;'>Festivities begin tomorrow! 🎊</p>", unsafe_allow_html=True)
                    elif days_until <= 7:
                        st.markdown("<p style='margin: 0; color: white;'>Festivities begin this week! ⚑</p>", unsafe_allow_html=True)
                    else:
                        st.markdown("<p style='margin: 0; color: white;'>days until festivities begin</p>", unsafe_allow_html=True)
                elif wedding_start <= today <= wedding_end:
                    days_remaining = (wedding_end - today).days + 1
                    st.markdown(f"<h1 style='margin: 0; color: white;'>{days_remaining}</h1>", unsafe_allow_html=True)
                    st.markdown("<p style='margin: 0; color: white;'>days of festivities remaining! πŸŽ‰</p>", unsafe_allow_html=True)
                else:
                    days_since = (today - wedding_end).days
                    st.markdown(f"<h1 style='margin: 0; color: white;'>{days_since}</h1>", unsafe_allow_html=True)
                    st.markdown("<p style='margin: 0; color: white;'>days since festivities ended</p>", unsafe_allow_html=True)
            except:
                st.markdown("<h1 style='margin: 0; color: white;'>-</h1>", unsafe_allow_html=True)
                st.markdown("<p style='margin: 0; color: white;'>Dates not set</p>", unsafe_allow_html=True)
        else:
            st.markdown("<h1 style='margin: 0; color: white;'>-</h1>", unsafe_allow_html=True)
            st.markdown("<p style='margin: 0; color: white;'>Dates not set</p>", unsafe_allow_html=True)
        
        st.markdown("</div>", unsafe_allow_html=True)
    
    def render_guest_count_card(self):
        st.markdown("""
        <div class="metric-card">
            <h3>πŸ‘₯ Confirmed Guests</h3>
        """, unsafe_allow_html=True)
        
        # Load RSVP data to get confirmed guests
        rsvp_data = self.config_manager.load_json_data('rsvp_data.json')
        confirmed_guests = 0
        
        # Handle case where rsvp_data is a list (empty file) instead of dict
        if isinstance(rsvp_data, list):
            rsvp_data = {}
        
        for group_code, group_data in rsvp_data.items():
            overall_rsvp = group_data.get('overall_rsvp', '')
            if overall_rsvp == 'Yes':
                # Count the number of attendees for this group
                group_attendees = group_data.get('group_attendees', [])
                confirmed_guests += len(group_attendees)
        
        st.markdown(f"<h1 style='margin: 0; color: white;'>{confirmed_guests}</h1>", unsafe_allow_html=True)
        st.markdown("<p style='margin: 0; color: white;'>confirmed attending</p>", unsafe_allow_html=True)
        
        st.markdown("</div>", unsafe_allow_html=True)
    
    def render_task_progress_card(self):
        st.markdown("""
        <div class="metric-card">
            <h3>βœ… Task Progress</h3>
        """, unsafe_allow_html=True)
        
        tasks = self.config_manager.load_json_data('tasks.json')
        if tasks:
            completed = len([task for task in tasks if task.get('completed', False)])
            total = len(tasks)
            percentage = int((completed / total) * 100) if total > 0 else 0
            
            st.markdown(f"<h1 style='margin: 0; color: white;'>{percentage}%</h1>", unsafe_allow_html=True)
            st.markdown(f"<p style='margin: 0; color: white;'>{completed}/{total} completed</p>", unsafe_allow_html=True)
        else:
            st.markdown("<h1 style='margin: 0; color: white;'>0%</h1>", unsafe_allow_html=True)
            st.markdown("<p style='margin: 0; color: white;'>No tasks yet</p>", unsafe_allow_html=True)
        
        st.markdown("</div>", unsafe_allow_html=True)
    
    def render_budget_card(self):
        st.markdown("""
        <div class="metric-card">
            <h3>πŸ’° Wedding Budget</h3>
        """, unsafe_allow_html=True)
        
        # Calculate both actualized and estimated costs from vendor data
        vendors = self.config_manager.load_json_data('vendors.json')
        total_estimated_cost = 0
        total_actualized_cost = self.calculate_actualized_cost(vendors)
        total_paid = 0
        
        for vendor in vendors:
            # Use total_cost field (newer format) or cost field (legacy format)
            cost = vendor.get('total_cost', vendor.get('cost', 0))
            
            # Calculate total paid from payment history instead of just deposit_paid
            payment_history = vendor.get('payment_history', [])
            total_paid_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') != 'credit')
            total_credits_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') == 'credit')
            net_paid = total_paid_from_history - total_credits_from_history
            
            total_estimated_cost += cost
            total_paid += net_paid
        
        pending_cost = total_estimated_cost - total_actualized_cost
        actualized_remaining = total_actualized_cost - total_paid
        estimated_remaining = total_estimated_cost - total_paid
        
        st.markdown(f"<h1 style='margin: 0; color: white;'>${total_actualized_cost:,.0f}</h1>", unsafe_allow_html=True)
        st.markdown(f"<p style='margin: 0; color: white;'>${total_estimated_cost:,.0f} estimated total</p>", unsafe_allow_html=True)
        st.markdown(f"<p style='margin: 0; color: white;'>${total_paid:,.0f} paid</p>", unsafe_allow_html=True)
        st.markdown(f"<p style='margin: 0; color: white;'>${actualized_remaining:,.0f} confirmed remaining</p>", unsafe_allow_html=True)
        
        st.markdown("</div>", unsafe_allow_html=True)
    
    
    def render_task_progress_chart(self):
        st.markdown("### Tasks by Event/Category")
        
        tasks = self.config_manager.load_json_data('tasks.json')
        if tasks:
            # Group tasks by category
            categories = {}
            for task in tasks:
                category = task.get('group', 'Uncategorized')
                if category not in categories:
                    categories[category] = {'completed': 0, 'not_completed': 0}
                if task.get('completed', False):
                    categories[category]['completed'] += 1
                else:
                    categories[category]['not_completed'] += 1
            
            # Create data for stacked bar chart
            category_names = list(categories.keys())
            completed_counts = [categories[cat]['completed'] for cat in category_names]
            not_completed_counts = [categories[cat]['not_completed'] for cat in category_names]
            
            if category_names:
                fig = go.Figure(data=[
                    go.Bar(name='Completed', x=category_names, y=completed_counts, marker_color='#4a7c59'),
                    go.Bar(name='Not Completed', x=category_names, y=not_completed_counts, marker_color='#d32f2f')
                ])
                
                fig.update_layout(
                    title="Number of Tasks by Event/Category",
                    barmode='stack',
                    xaxis_tickangle=-45,
                    height=400,
                    yaxis=dict(tickmode='linear', dtick=1)  # Show whole numbers only
                )
                st.plotly_chart(fig, use_container_width=True)
            else:
                st.info("No tasks to display")
        else:
            st.info("No tasks created yet")
    
    def _create_comprehensive_guest_list(self):
        """Create a comprehensive list of all guests with RSVP data - same logic as guest management"""
        all_guests = []
        
        # Load guest list and RSVP data
        guest_list_data = self.config_manager.load_json_data('guest_list_data.json')
        rsvp_data = self.config_manager.load_json_data('rsvp_data.json')
        
        # Handle case where rsvp_data is a list (empty file) instead of dict
        if isinstance(rsvp_data, list):
            rsvp_data = {}
        
        if not guest_list_data:
            return all_guests
        
        for group_name, group_data in guest_list_data.items():
            # Get RSVP data for this group
            group_rsvp_data = rsvp_data.get(group_name, {}) if rsvp_data else {}
            
            # Add named guests
            for named_guest in group_data['named_guests']:
                guest = {
                    'display_name': named_guest['full_name'],
                    'first_name': named_guest['first_name'],
                    'last_name': named_guest['last_name'],
                    'group_name': group_name,
                    'party': group_data['party'],
                    'address': group_data['address'],
                    'type': 'Named Guest',
                    'phone': group_rsvp_data.get('phone_number', ''),
                    'rsvp_by_event': {},
                    'meal_selections': {}
                }
                
                # Apply RSVP data
                self._apply_rsvp_to_guest(guest, group_rsvp_data)
                all_guests.append(guest)
            
            # Add plus one spots
            plus_one_spots = group_data['plus_one_spots']
            if plus_one_spots > 0:
                # Get plus one names from RSVP data
                group_attendees = group_rsvp_data.get('group_attendees', [])
                named_guest_names = [g['full_name'] for g in group_data['named_guests']]
                
                # Find plus one names (attendees not in named guests)
                plus_one_names = [name for name in group_attendees if name not in named_guest_names]
                
                # Create plus one entries
                for i in range(plus_one_spots):
                    if i < len(plus_one_names):
                        # Named plus one
                        plus_one_name = plus_one_names[i]
                        guest = {
                            'display_name': plus_one_name,
                            'first_name': plus_one_name.split()[0] if plus_one_name.split() else plus_one_name,
                            'last_name': ' '.join(plus_one_name.split()[1:]) if len(plus_one_name.split()) > 1 else '',
                            'group_name': group_name,
                            'party': group_data['party'],
                            'address': group_data['address'],
                            'type': 'Plus One (Named)',
                            'phone': '',
                            'rsvp_by_event': {},
                            'meal_selections': {}
                        }
                    else:
                        # Unnamed plus one
                        guest = {
                            'display_name': f'Unnamed Plus One {i+1}',
                            'first_name': '',
                            'last_name': '',
                            'group_name': group_name,
                            'party': group_data['party'],
                            'address': group_data['address'],
                            'type': 'Plus One (Unnamed)',
                            'phone': '',
                            'rsvp_by_event': {},
                            'meal_selections': {}
                        }
                    
                    # Apply RSVP data
                    self._apply_rsvp_to_guest(guest, group_rsvp_data)
                    all_guests.append(guest)
        
        return all_guests
    
    def _apply_rsvp_to_guest(self, guest, rsvp_data):
        """Apply RSVP data to a guest - same logic as guest management"""
        config = self.config_manager.load_config()
        wedding_events = config.get('wedding_events', [])
        
        # Initialize all events with "Pending" status
        for event in wedding_events:
            event_name = event.get('name', '')
            if event_name:
                guest['rsvp_by_event'][event_name] = 'Pending'
        
        if not rsvp_data:
            return
        
        event_responses = rsvp_data.get('event_responses', {})
        group_attendees = rsvp_data.get('group_attendees', [])
        dietary_restrictions = rsvp_data.get('dietary_restrictions', {})
        
        guest_name = guest['display_name']
        
        # Apply dietary restrictions
        if guest_name in dietary_restrictions:
            guest['allergies'] = dietary_restrictions[guest_name]
        
        # Apply event responses
        for event in wedding_events:
            event_name = event.get('name', '')
            if event_name in event_responses:
                event_data = event_responses[event_name]
                attendees = event_data.get('attendees', [])
                party_rsvp = event_data.get('rsvp', 'Pending')
                
                # Determine individual RSVP status
                if guest_name in attendees:
                    guest['rsvp_by_event'][event_name] = party_rsvp
                    
                    # Apply meal choice if attending and event requires meal choice
                    if party_rsvp == 'Yes' and event.get('requires_meal_choice', False):
                        meal_choice = event_data.get('meal_choice', {})
                        if guest_name in meal_choice:
                            guest['meal_selections'][event_name] = meal_choice[guest_name]
                else:
                    guest['rsvp_by_event'][event_name] = 'No'
    
    def render_guest_rsvp_chart(self):
        st.markdown("### Attendance by Event")
        
        # Get comprehensive guest list using the same logic as guest management
        all_guests = self._create_comprehensive_guest_list()
        
        if all_guests:
            # Get event names from wedding config
            config = self.config_manager.load_config()
            events = [event['name'] for event in config.get('wedding_events', [])]
            
            # Count attendance for each event
            event_attendance = {}
            for event in events:
                event_attendance[event] = {'Yes': 0, 'No': 0, 'Pending': 0}
            
            # Count attendance for each guest
            for guest in all_guests:
                rsvp_by_event = guest.get('rsvp_by_event', {})
                for event_name in event_attendance.keys():
                    rsvp_status = rsvp_by_event.get(event_name, 'Pending')
                    if rsvp_status in event_attendance[event_name]:
                        event_attendance[event_name][rsvp_status] += 1
                    else:
                        event_attendance[event_name]['Pending'] += 1
            
            # Create bar chart showing attendance by event
            event_names = list(event_attendance.keys())
            yes_counts = [event_attendance[event]['Yes'] for event in event_names]
            no_counts = [event_attendance[event]['No'] for event in event_names]
            pending_counts = [event_attendance[event]['Pending'] for event in event_names]
            
            fig = go.Figure(data=[
                go.Bar(name='Yes', x=event_names, y=yes_counts, marker_color='#4a7c59'),
                go.Bar(name='No', x=event_names, y=no_counts, marker_color='#d32f2f'),
                go.Bar(name='Pending', x=event_names, y=pending_counts, marker_color='#ffa726')
            ])
            
            # Calculate the maximum value across all events for better y-axis scaling
            max_value = max(max(yes_counts), max(no_counts), max(pending_counts))
            # Add some padding to the max value for better visualization
            y_max = max_value + 2 if max_value > 0 else 10
            
            # Calculate appropriate tick spacing based on the max value
            if y_max <= 10:
                tick_spacing = 2
            elif y_max <= 20:
                tick_spacing = 5
            elif y_max <= 50:
                tick_spacing = 10
            else:
                tick_spacing = 20
            
            fig.update_layout(
                title="RSVP Responses by Event",
                barmode='stack',
                xaxis_tickangle=-45,
                height=400,
                yaxis=dict(
                    tickmode='linear', 
                    dtick=tick_spacing,  # Show ticks at appropriate intervals
                    range=[0, y_max]  # Set max based on actual data
                )
            )
            st.plotly_chart(fig, use_container_width=True)
        else:
            st.info("No guest data available yet")
    
    def render_food_choices_by_event(self):
        st.markdown("### 🍽️ Food Choices by Event")
        
        # Load data
        rsvp_data = self.config_manager.load_json_data('rsvp_data.json')
        
        # Handle case where rsvp_data is a list (empty file) instead of dict
        if isinstance(rsvp_data, list):
            rsvp_data = {}
        
        config = self.config_manager.load_config()
        events = config.get('wedding_events', [])
        
        if not rsvp_data:
            st.info("No RSVP data available yet")
            return
        
        # Filter events that require meal choices
        meal_events = [event for event in events if event.get('requires_meal_choice', False)]
        
        if not meal_events:
            st.info("No events require meal choices.")
            return
        
        for event in meal_events:
            event_name = event['name']
            meal_options = event.get('meal_options', [])
            
            if meal_options:
                st.markdown(f"**{event_name}**")
                
                # Count meal choices for this event
                meal_counts = {option: 0 for option in meal_options}
                meal_counts['Not Selected'] = 0
                outdated_choices = {}
                
                for group_code, group_data in rsvp_data.items():
                    # Check if this group is attending this event
                    event_responses = group_data.get('event_responses', {})
                    if event_name in event_responses:
                        event_rsvp = event_responses[event_name].get('rsvp', '')
                        if event_rsvp == 'Yes':
                            attendees = event_responses[event_name].get('attendees', [])
                            
                            # Get meal selections for each attendee from the event response
                            meal_choices = event_responses[event_name].get('meal_choice', {})
                            for attendee in attendees:
                                selected_meal = meal_choices.get(attendee, 'Not Selected')
                                if selected_meal in meal_counts:
                                    meal_counts[selected_meal] += 1
                                elif selected_meal != 'Not Selected':
                                    # This is an outdated meal choice
                                    if selected_meal not in outdated_choices:
                                        outdated_choices[selected_meal] = []
                                    outdated_choices[selected_meal].append(attendee)
                                else:
                                    meal_counts['Not Selected'] += 1
                
                # Display meal choice counts
                col1, col2, col3, col4 = st.columns(4)
                cols = [col1, col2, col3, col4]
                
                for i, (meal, count) in enumerate(meal_counts.items()):
                    if i < len(cols):
                        with cols[i]:
                            st.metric(meal, count)
                
                # Show outdated meal choices warning
                if outdated_choices:
                    st.warning("⚠️ **Outdated Meal Choices Detected!**")
                    st.markdown("The following guests selected meal options that are no longer available on the current menu:")
                    
                    for outdated_meal, guests in outdated_choices.items():
                        with st.expander(f"πŸ” {outdated_meal} ({len(guests)} guests)", expanded=False):
                            st.markdown("**Guests who selected this outdated option:**")
                            
                            # Create a table for better formatting
                            guest_data = []
                            for guest_name in guests:
                                # Find the group for this guest to get contact info
                                guest_group = None
                                for group_code, group_data in rsvp_data.items():
                                    if guest_name in group_data.get('group_attendees', []):
                                        guest_group = group_data
                                        break
                                
                                # Get phone number
                                phone = 'No phone provided'
                                if guest_group:
                                    phone_number = guest_group.get('phone_number', '')
                                    if phone_number and phone_number.strip() and phone_number != 'No phone provided':
                                        phone = phone_number
                                
                                guest_data.append({
                                    'Name': guest_name,
                                    'Phone': phone
                                })
                            
                            if guest_data:
                                guest_df = pd.DataFrame(guest_data)
                                st.dataframe(guest_df, use_container_width=True, hide_index=True)
                            
                            st.markdown("**Action Required:**")
                            st.markdown(f"Please contact these {len(guests)} guests to update their meal choice from '{outdated_meal}' to one of the current options: {', '.join(meal_options)}")
                
                st.markdown("---")  # Separator between events
    
    def render_upcoming_tasks(self):
        st.markdown("### Upcoming Tasks (Next 7 Days)")
        
        tasks = self.config_manager.load_json_data('tasks.json')
        vendors = self.config_manager.load_json_data('vendors.json')
        
        # Create a vendor lookup dictionary for quick access
        vendor_lookup = {}
        if vendors:
            for vendor in vendors:
                vendor_lookup[vendor.get('id', '')] = vendor.get('name', '')
        
        if tasks:
            # Filter incomplete tasks and sort by due date
            incomplete_tasks = [task for task in tasks if not task.get('completed', False)]
            incomplete_tasks.sort(key=lambda x: x.get('due_date') or '9999-12-31')
            
            # Filter tasks within the next 7 days
            today = date.today()
            week_from_now = date(today.year, today.month, today.day + 7)
            
            upcoming_tasks = []
            for task in incomplete_tasks:
                due_date_str = task.get('due_date', '')
                if due_date_str:
                    try:
                        due_date = datetime.fromisoformat(due_date_str).date()
                        if today <= due_date <= week_from_now:
                            upcoming_tasks.append(task)
                    except:
                        # If date parsing fails, skip the task (don't include it)
                        continue
                # If no due date, skip the task (don't include it)
            
            if upcoming_tasks:
                for i, task in enumerate(upcoming_tasks):
                    with st.container():
                        # Task header with completion status and title
                        title = task.get('title', 'Untitled Task')
                        completed = task.get('completed', False)
                        status_icon = "βœ…" if completed else "⏳"
                        
                        # Check if task is associated with a vendor
                        vendor_id = task.get('vendor_id', '')
                        vendor_name = vendor_lookup.get(vendor_id, '')
                        
                        if vendor_name:
                            st.markdown(f"**{status_icon} {title}** - *{vendor_name}*")
                        else:
                            st.markdown(f"**{status_icon} {title}**")
                        
                        # Task details in columns
                        col1, col2, col3, col4 = st.columns(4)
                        
                        with col1:
                            due_date = task.get('due_date', '')
                            if due_date:
                                st.caption(f"πŸ“… Due: {due_date}")
                            else:
                                st.caption("πŸ“… No due date")
                        
                        with col2:
                            group = task.get('group', 'Uncategorized')
                            st.caption(f"πŸ“ Group: {group}")
                        
                        with col3:
                            priority = task.get('priority', 'Medium')
                            # Priority with color coding
                            if priority == "Urgent":
                                st.caption(f"πŸ”΄ Priority: {priority}")
                            elif priority == "High":
                                st.caption(f"πŸ”΄ Priority: {priority}")
                            elif priority == "Medium":
                                st.caption(f"🟑 Priority: {priority}")
                            else:
                                st.caption(f"🟒 Priority: {priority}")
                        
                        with col4:
                            assigned_to = task.get('assigned_to', '')
                            # Handle both old single assignee and new multiple assignees format
                            if isinstance(assigned_to, str):
                                assigned_to_display = assigned_to if assigned_to else "Unassigned"
                            elif isinstance(assigned_to, list):
                                if assigned_to:
                                    assigned_to_display = ", ".join(assigned_to)
                                else:
                                    assigned_to_display = "Unassigned"
                            else:
                                assigned_to_display = "Unassigned"
                            
                            if assigned_to_display and assigned_to_display != "Unassigned":
                                st.caption(f"πŸ‘€ Assigned: {assigned_to_display}")
                            else:
                                st.caption("πŸ‘€ Unassigned")
                        
                        # Add some spacing
                        st.markdown("---")
            else:
                st.info("No tasks due within the next 7 days")
        else:
            st.info("No tasks created yet")
    
    def render_upcoming_payments(self):
        st.markdown("### πŸ’³ Upcoming Payments (Next 30 Days)")
        
        # Create VendorManager instance to use its payment logic
        vendor_manager = VendorManager()
        
        # Load vendors data
        vendors = self.config_manager.load_json_data('vendors.json')
        
        if not vendors:
            st.info("No vendors added yet")
            return
        
        # Get upcoming payments using the same logic as the vendors page
        upcoming_payments = []
        today = date.today()
        
        for vendor in vendors:
            payment_installments = vendor.get('payment_installments', [])
            
            if payment_installments and len(payment_installments) > 1:
                # Handle installments
                for i, installment in enumerate(payment_installments):
                    if not installment.get('paid', False):
                        due_date_str = installment.get('due_date', '')
                        if due_date_str:
                            try:
                                due_date = datetime.fromisoformat(due_date_str).date()
                                days_until_due = (due_date - today).days
                                
                                # Only show if within next 30 days
                                if 0 <= days_until_due <= 30:
                                    upcoming_payments.append({
                                        'vendor_name': vendor.get('name', ''),
                                        'installment_num': i + 1,
                                        'amount': installment.get('amount', 0),
                                        'due_date': due_date,
                                        'days_until': days_until_due,
                                        'is_installment': True
                                    })
                            except:
                                continue
            else:
                # Handle single payment
                payment_due_date_str = vendor.get('payment_due_date')
                if payment_due_date_str:
                    try:
                        due_date = datetime.fromisoformat(payment_due_date_str).date()
                        days_until_due = (due_date - today).days
                        
                        # Only show if not fully paid and within next 30 days
                        total_cost = vendor.get('total_cost', vendor.get('cost', 0))
                        payment_history = vendor.get('payment_history', [])
                        total_paid_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') != 'credit')
                        total_credits_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') == 'credit')
                        remaining_balance = total_cost - total_paid_from_history + total_credits_from_history
                        
                        if remaining_balance > 0 and 0 <= days_until_due <= 30:
                            upcoming_payments.append({
                                'vendor_name': vendor.get('name', ''),
                                'installment_num': None,
                                'amount': remaining_balance,
                                'due_date': due_date,
                                'days_until': days_until_due,
                                'is_installment': False
                            })
                    except:
                        continue
        
        # Sort by days until due
        upcoming_payments.sort(key=lambda x: x['days_until'])
        
        if upcoming_payments:
            # Create table data
            table_data = []
            for payment in upcoming_payments:
                # Format payment description
                if payment['is_installment']:
                    payment_desc = f"{payment['vendor_name']} - Installment {payment['installment_num']}"
                else:
                    payment_desc = f"{payment['vendor_name']} - Final Payment"
                
                # Format due date
                if payment['days_until'] == 0:
                    due_text = "🟠 Today"
                elif payment['days_until'] == 1:
                    due_text = "🟑 Tomorrow"
                elif payment['days_until'] <= 3:
                    due_text = f"🟑 {payment['days_until']} days"
                else:
                    due_text = f"🟒 {payment['days_until']} days"
                
                table_data.append({
                    'Vendor & Payment': payment_desc,
                    'Amount': f"${payment['amount']:,.0f}",
                    'Due': due_text
                })
            
            # Create and display the table
            df = pd.DataFrame(table_data)
            st.dataframe(df, use_container_width=True, hide_index=True)
        else:
            st.info("No payments due within the next 30 days")