-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstatrep_flet_app_v3_prod.py
More file actions
1321 lines (1143 loc) · 51.6 KB
/
statrep_flet_app_v3_prod.py
File metadata and controls
1321 lines (1143 loc) · 51.6 KB
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
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import flet as ft
from flet import Colors
from statrep_db_v3_prod import StatrepDatabase
from manage_handles_v3_prod import HandlesDatabase
from manage_locations_v3_prod import LocationDatabase
from datetime import datetime
from zoneinfo import ZoneInfo
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def get_central_time():
"""Get current time in US Central timezone (handles DST automatically)"""
try:
# Use America/Chicago which automatically handles CST/CDT
central_tz = ZoneInfo("America/Chicago")
return datetime.now(central_tz)
except Exception as e:
logger.warning(f"Could not use zoneinfo, falling back to UTC-6: {e}")
# Fallback: simple UTC-6 offset (doesn't handle DST)
from datetime import timezone, timedelta
central_tz = timezone(timedelta(hours=-6))
return datetime.now(central_tz)
class StatrepApp:
def __init__(self):
self.db = None
self.handles_db = None
self.locations_db = None
def main(self, page: ft.Page):
page.title = "ReadyCorps STATREP Submission"
page.theme_mode = "light"
page.padding = 20
# Scrolling is handled by Container, not page level
page.horizontal_alignment = ft.CrossAxisAlignment.START
# Status message (for connection errors, etc.)
connection_status = ft.Text(value="", size=14)
# Initialize Oracle databases with error handling
logger.info("Initializing database connections...")
self.db = StatrepDatabase()
success, error = self.db.connect()
if not success:
connection_status.value = f"❌ Database Error: {error}"
connection_status.color = Colors.RED
page.add(
ft.Column([
ft.Text("ReadyCorps STATREP Submission", size=32, weight="bold"),
ft.Divider(height=20),
connection_status,
ft.Text("\nPlease contact your administrator.", size=14, color=Colors.GREY_700)
])
)
return
self.handles_db = HandlesDatabase()
success, error = self.handles_db.connect()
if not success:
connection_status.value = f"❌ Handles Database Error: {error}"
connection_status.color = Colors.RED
page.add(
ft.Column([
ft.Text("ReadyCorps STATREP Submission", size=32, weight="bold"),
ft.Divider(height=20),
connection_status,
ft.Text("\nPlease contact your administrator.", size=14, color=Colors.GREY_700)
])
)
return
self.locations_db = LocationDatabase()
success, error = self.locations_db.connect()
if not success:
connection_status.value = f"❌ Locations Database Error: {error}"
connection_status.color = Colors.RED
page.add(
ft.Column([
ft.Text("ReadyCorps STATREP Submission", size=32, weight="bold"),
ft.Divider(height=20),
connection_status,
ft.Text("\nPlease contact your administrator.", size=14, color=Colors.GREY_700)
])
)
return
# Get lists of valid options
success, valid_handles = self.handles_db.get_all_handles()
if not success:
valid_handles = []
success, valid_states = self.locations_db.get_all_states()
if not success:
valid_states = []
success, valid_neighborhoods = self.locations_db.get_all_neighborhoods()
if not success:
valid_neighborhoods = []
# Store valid options
self.valid_handles = valid_handles
self.valid_states = valid_states
self.valid_neighborhoods = valid_neighborhoods
logger.info(f"Data loaded - Handles: {len(valid_handles)}, States: {len(valid_states)}, Neighborhoods: {len(valid_neighborhoods)}")
# Status message for user feedback
self.status_message = ft.Text(value="", color=Colors.GREEN, size=16, weight="bold")
# ===== HANDLE FIELD =====
self.handle_field = ft.TextField(
label="Your ReadyCore Handle",
hint_text="Start typing to search handles...",
width=400,
autofocus=True,
on_change=lambda e: self.filter_handles(e, page)
)
self.handle_suggestions = ft.Column(
controls=[],
visible=False,
scroll="auto",
height=200,
width=400
)
# ===== PIN FIELD =====
# Will set on_submit after verify_pin_clicked is defined
self.pin_field = ft.TextField(
label="PIN",
hint_text="Enter your 4-digit PIN",
password=True,
can_reveal_password=True,
width=250,
max_length=10
)
# Verify PIN button
verify_pin_button = ft.ElevatedButton(
text="Verify PIN",
on_click=lambda e: self.verify_pin_clicked(page),
bgcolor=Colors.BLUE_700,
color=Colors.WHITE,
height=40
)
# Change PIN button (always visible alongside Verify)
def change_pin_button_clicked(e):
logger.info("Change PIN button clicked!")
logger.info(f"Event page: {e.page}, Control page: {e.control.page}")
self.show_voluntary_pin_change(e.control.page)
change_pin_button = ft.ElevatedButton(
text="Change PIN",
on_click=change_pin_button_clicked,
bgcolor=Colors.ORANGE_700,
color=Colors.WHITE,
height=40
)
self.pin_row = ft.Row(
controls=[self.pin_field, verify_pin_button, change_pin_button],
spacing=10,
alignment=ft.MainAxisAlignment.START,
wrap=True # Allow wrapping on small screens
)
self.verify_pin_button = verify_pin_button # Store reference
self.change_pin_button = change_pin_button # Store reference
self.pin_verified = False # Track if PIN has been verified
# ===== DATETIME FIELD =====
current_dt = get_central_time().strftime("%Y-%m-%d %H:%M")
self.datetime_field = ft.TextField(
label="Report as of Date/Time",
hint_text="YYYY-MM-DD HH:MM",
value=current_dt,
width=400,
helper_text="US Central Time (CST/CDT)"
)
# ===== STATE FIELD =====
self.state_field = ft.TextField(
label="State / Territory",
hint_text="Start typing to search states...",
width=400,
on_change=lambda e: self.filter_states(e, page)
)
self.state_suggestions = ft.Column(
controls=[],
visible=False,
scroll="auto",
height=200,
width=400
)
# ===== NEIGHBORHOOD FIELD =====
self.neighborhood_field = ft.TextField(
label="Neighborhood",
hint_text="Start typing to search neighborhoods...",
width=400,
on_change=lambda e: self.filter_neighborhoods(e, page)
)
self.neighborhood_suggestions = ft.Column(
controls=[],
visible=False,
scroll="auto",
height=200,
width=400
)
# ===== LOCATION FIELD =====
self.location_field = ft.TextField(
label="Your Location",
hint_text="Grid Square (e.g., FN20xb) or City, State",
width=400,
multiline=True,
min_lines=2,
max_lines=4,
helper_text="Grid Square Preferred. Include any additional details to help \nlocate you (e.g., near downtown, 2nd floor, etc.)"
)
# Radio button groups
self.conditions_group = ft.RadioGroup(
content=ft.Column([
ft.Radio(value="A", label="A - All Stable"),
ft.Radio(value="B", label="B - Moderate Disruptions"),
ft.Radio(value="C", label="C - Severe Disruptions"),
]),
value="A"
)
self.position_group = ft.RadioGroup(
content=ft.Column([
ft.Radio(value="H", label="H - Home"),
ft.Radio(value="M", label="M - Mobile"),
ft.Radio(value="P", label="P - Portable"),
])
)
self.power_group = ft.RadioGroup(
content=ft.Column([
ft.Radio(value="Y", label="Y - Up and Running"),
ft.Radio(value="I", label="I - Intermittent / Brown-outs"),
ft.Radio(value="N", label="N - No, Commercial Power is down"),
])
)
self.water_group = ft.RadioGroup(
content=ft.Column([
ft.Radio(value="Y", label="Y - Yes"),
ft.Radio(value="C", label="C - Contaminated"),
ft.Radio(value="N", label="N - No"),
])
)
self.sanitation_group = ft.RadioGroup(
content=ft.Column([
ft.Radio(value="Y", label="Y - Yes"),
ft.Radio(value="N", label="N - No"),
])
)
self.grid_comms_group = ft.RadioGroup(
content=ft.Column([
ft.Radio(value="Y", label="Y - Yes"),
ft.Radio(value="N", label="N - No"),
])
)
self.transport_group = ft.RadioGroup(
content=ft.Column([
ft.Radio(value="Y", label="Y - Yes"),
ft.Radio(value="N", label="N - No"),
])
)
self.comments_field = ft.TextField(
label="Comments",
multiline=True,
min_lines=3,
max_lines=5,
width=400
)
# Optional fields container (shown when conditions != 'A')
self.optional_fields = ft.Column(
controls=[
ft.Divider(height=20),
ft.Text("Additional Information (Optional for 'All Stable' reports)",
size=16, weight="bold"),
ft.Text("Your Position:", weight="bold"),
self.position_group,
ft.Text("Status of Commercial Power:", weight="bold"),
self.power_group,
ft.Text("Water Status:", weight="bold"),
self.water_group,
ft.Text("Sanitation Status:", weight="bold"),
self.sanitation_group,
ft.Text("Grid Communications:", weight="bold"),
self.grid_comms_group,
ft.Text("Transportation Status:", weight="bold"),
self.transport_group,
ft.Divider(height=10),
self.comments_field,
],
visible=False
)
# Add listener to conditions radio group to show/hide optional fields
def conditions_changed(e):
if self.conditions_group.value != "A":
self.optional_fields.visible = True
else:
self.optional_fields.visible = False
page.update()
self.conditions_group.on_change = conditions_changed
# Verify PIN handler (acts as "login")
def verify_pin_clicked(page):
# Validate inputs
if not self.handle_field.value:
self.status_message.value = "✗ Please select a handle first"
self.status_message.color = Colors.RED
page.update()
return
if not self.pin_field.value:
self.status_message.value = "✗ Please enter your PIN"
self.status_message.color = Colors.RED
page.update()
return
# Verify PIN
if not self.handles_db.verify_pin(self.handle_field.value, self.pin_field.value):
self.status_message.value = "✗ Invalid handle or PIN"
self.status_message.color = Colors.RED
self.pin_verified = False
page.update()
return
# PIN is valid!
self.pin_verified = True
logger.info("PIN verified successfully")
# Check if PIN needs to be changed (starts with 'z')
if self.handles_db.pin_needs_change(self.handle_field.value, self.pin_field.value):
self.show_pin_change_dialog(self.handle_field.value, page)
return # Don't continue until PIN is changed
# Look up the last STATREP for this handle (pre-fill convenience)
success, last_statrep = self.db.get_last_statrep_for_handle(self.handle_field.value)
if success and last_statrep:
# Pre-populate state, neighborhood, and location from last report
self.state_field.value = last_statrep[3] # state
self.neighborhood_field.value = last_statrep[4] # neighborhood
self.location_field.value = last_statrep[5] # location
# Show success message with pre-fill info
self.status_message.value = f"✓ Verified! Pre-filled from your last report ({last_statrep[2]})"
self.status_message.color = Colors.GREEN
else:
# First time for this handle
self.status_message.value = f"✓ Verified! Welcome, {self.handle_field.value}"
self.status_message.color = Colors.GREEN
page.update()
self.verify_pin_clicked = verify_pin_clicked
# Now set the on_submit handler for the PIN field
self.pin_field.on_submit = lambda e: verify_pin_clicked(page)
# Submit button handler
def submit_clicked(e):
# Validate required fields
if not self.handle_field.value:
self.status_message.value = "✗ Please select your handle"
self.status_message.color = Colors.RED
page.update()
return
if not self.pin_field.value:
self.status_message.value = "✗ Please enter your PIN"
self.status_message.color = Colors.RED
page.update()
return
# Verify PIN inline (unless already verified)
if not self.pin_verified:
if not self.handles_db.verify_pin(self.handle_field.value, self.pin_field.value):
self.status_message.value = "✗ Invalid handle or PIN"
self.status_message.color = Colors.RED
page.update()
return
# Check if PIN needs to be changed (starts with 'z')
if self.handles_db.pin_needs_change(self.handle_field.value, self.pin_field.value):
self.show_pin_change_dialog(self.handle_field.value, page)
return
if not self.datetime_field.value:
self.status_message.value = "✗ Please enter date/time"
self.status_message.color = Colors.RED
page.update()
return
if not self.state_field.value:
self.status_message.value = "✗ Please enter state"
self.status_message.color = Colors.RED
page.update()
return
if not self.neighborhood_field.value:
self.status_message.value = "✗ Please enter neighborhood"
self.status_message.color = Colors.RED
page.update()
return
if not self.location_field.value:
self.status_message.value = "✗ Please enter location"
self.status_message.color = Colors.RED
page.update()
return
# Insert the STATREP
success, result = self.db.insert_statrep(
amcon_handle=self.handle_field.value,
datetime_group=self.datetime_field.value,
state=self.state_field.value,
neighborhood=self.neighborhood_field.value,
location=self.location_field.value,
conditions=self.conditions_group.value,
position=self.position_group.value if self.conditions_group.value != "A" else None,
commercial_power=self.power_group.value if self.conditions_group.value != "A" else None,
water=self.water_group.value if self.conditions_group.value != "A" else None,
sanitation=self.sanitation_group.value if self.conditions_group.value != "A" else None,
grid_comms=self.grid_comms_group.value if self.conditions_group.value != "A" else None,
transportation=self.transport_group.value if self.conditions_group.value != "A" else None,
comments=self.comments_field.value if self.comments_field.value else None
)
if success:
# Update last_used timestamp for the handle
self.handles_db.update_last_used(self.handle_field.value)
# Save handle for re-population after clear
submitted_handle = self.handle_field.value
# Mark as verified (for next time)
self.pin_verified = True
self.status_message.value = f"✓ STATREP submitted successfully! (ID: {result})"
self.status_message.color = Colors.GREEN
# Clear form except handle (for quick re-submissions)
clear_form(None)
# Re-populate handle and keep verified state for convenience
self.handle_field.value = submitted_handle
self.pin_verified = True
else:
self.status_message.value = f"✗ Error: {result}"
self.status_message.color = Colors.RED
page.update()
def clear_form(e):
self.handle_field.value = ""
self.pin_field.value = ""
self.datetime_field.value = get_central_time().strftime("%Y-%m-%d %H:%M")
self.state_field.value = ""
self.neighborhood_field.value = ""
self.location_field.value = ""
self.conditions_group.value = "A"
self.position_group.value = None
self.power_group.value = None
self.water_group.value = None
self.sanitation_group.value = None
self.grid_comms_group.value = None
self.transport_group.value = None
self.comments_field.value = ""
self.optional_fields.visible = False
self.handle_suggestions.visible = False
self.handle_suggestions.controls.clear()
self.state_suggestions.visible = False
self.state_suggestions.controls.clear()
self.neighborhood_suggestions.visible = False
self.neighborhood_suggestions.controls.clear()
# Reset PIN verification state
self.pin_verified = False
if e: # Only clear status message if user clicked clear button
self.status_message.value = ""
page.update()
def show_statreps_clicked(e):
"""Show recent STATREPs for the same state/neighborhood"""
# Validate that state and neighborhood are filled
if not self.state_field.value:
self.status_message.value = "✗ Please enter a state first"
self.status_message.color = Colors.RED
page.update()
return
if not self.neighborhood_field.value:
self.status_message.value = "✗ Please enter a neighborhood first"
self.status_message.color = Colors.RED
page.update()
return
state = self.state_field.value
neighborhood = self.neighborhood_field.value
logger.info(f"Fetching STATREPs for {state}/{neighborhood}")
# Query the database
success, results = self.db.get_latest_statreps_by_location(state, neighborhood)
if not success:
self.status_message.value = f"✗ Error fetching STATREPs: {results}"
self.status_message.color = Colors.RED
page.update()
return
if not results or len(results) == 0:
self.status_message.value = f"ℹ No STATREPs found for {state}/{neighborhood}"
self.status_message.color = Colors.BLUE
page.update()
return
# Build the table
show_statreps_dialog(page, results, state, neighborhood)
def show_statreps_dialog(page, results, state, neighborhood):
"""Display STATREPs in a scrollable dialog with table"""
import csv
import io
import base64
from datetime import datetime
# Map condition codes to descriptions
condition_map = {
"A": "All Stable",
"B": "Moderate Disruptions",
"C": "Severe Disruptions"
}
def copy_csv_to_clipboard(e):
"""Generate CSV and copy to clipboard"""
# Create CSV in memory
output = io.StringIO()
csv_writer = csv.writer(output)
# Write header
csv_writer.writerow([
"Handle", "Date/Time", "State", "Neighborhood", "Location",
"Status", "Position", "Power", "Water", "Sanitation",
"Grid/Comms", "Transport", "Comments"
])
# Write data rows
for row in results:
# row structure: id, amcon_handle, datetime_group, state, neighborhood, location,
# conditions, position, commercial_power, water, sanitation,
# grid_comms, transportation, comments
condition_desc = condition_map.get(row[6], row[6])
csv_writer.writerow([
row[1], # handle
str(row[2]), # datetime
row[3], # state
row[4], # neighborhood
row[5], # location
condition_desc, # conditions
row[7] or "", # position
row[8] or "", # power
row[9] or "", # water
row[10] or "", # sanitation
row[11] or "", # grid_comms
row[12] or "", # transportation
row[13] or "", # comments
])
# Get CSV content
csv_content = output.getvalue()
output.close()
# Copy to clipboard
page.set_clipboard(csv_content)
# Show success message
page.snack_bar = ft.SnackBar(
content=ft.Text("✓ CSV data copied to clipboard! Paste into Excel or text editor."),
bgcolor=Colors.GREEN_700,
duration=3000
)
page.snack_bar.open = True
page.update()
# Create table rows
table_rows = []
for row in results:
# row structure: id, amcon_handle, datetime_group, state, neighborhood, location,
# conditions, position, commercial_power, water, sanitation,
# grid_comms, transportation, comments
handle = row[1]
datetime_str = str(row[2])
conditions = row[6]
condition_desc = condition_map.get(conditions, conditions)
if conditions == "A":
# All Stable - just show that
table_rows.append(
ft.DataRow(
cells=[
ft.DataCell(ft.Text(handle, size=12)),
ft.DataCell(ft.Text(datetime_str, size=12)),
ft.DataCell(ft.Text(condition_desc, size=12, weight="bold", color=Colors.GREEN)),
ft.DataCell(ft.Text("")),
ft.DataCell(ft.Text("")),
ft.DataCell(ft.Text("")),
ft.DataCell(ft.Text("")),
ft.DataCell(ft.Text("")),
ft.DataCell(ft.Text("")),
ft.DataCell(ft.Text("")),
]
)
)
else:
# Moderate or Severe - show all fields
position = row[7] or ""
power = row[8] or ""
water = row[9] or ""
sanitation = row[10] or ""
grid_comms = row[11] or ""
transportation = row[12] or ""
comments = row[13] or ""
color = Colors.ORANGE if conditions == "B" else Colors.RED
# Show full comments without truncation
# Let the row height expand to fit all text
table_rows.append(
ft.DataRow(
cells=[
ft.DataCell(ft.Text(handle, size=12)),
ft.DataCell(ft.Text(datetime_str, size=12)),
ft.DataCell(ft.Text(condition_desc, size=12, weight="bold", color=color)),
ft.DataCell(ft.Text(position, size=11)),
ft.DataCell(ft.Text(power, size=11)),
ft.DataCell(ft.Text(water, size=11)),
ft.DataCell(ft.Text(sanitation, size=11)),
ft.DataCell(ft.Text(grid_comms, size=11)),
ft.DataCell(ft.Text(transportation, size=11)),
ft.DataCell(
ft.Container(
content=ft.Text(
comments, # Full comments, no truncation
size=11,
selectable=True,
no_wrap=False, # Allow text wrapping
),
width=400, # Wide enough for comments
)
),
]
)
)
# Create the data table
data_table = ft.DataTable(
columns=[
ft.DataColumn(ft.Text("Handle", weight="bold", size=13)),
ft.DataColumn(ft.Text("Date/Time", weight="bold", size=13)),
ft.DataColumn(ft.Text("Status", weight="bold", size=13)),
ft.DataColumn(ft.Text("Position", weight="bold", size=13)),
ft.DataColumn(ft.Text("Power", weight="bold", size=13)),
ft.DataColumn(ft.Text("Water", weight="bold", size=13)),
ft.DataColumn(ft.Text("Sanitation", weight="bold", size=13)),
ft.DataColumn(ft.Text("Grid/Comms", weight="bold", size=13)),
ft.DataColumn(ft.Text("Transport", weight="bold", size=13)),
ft.DataColumn(ft.Text("Comments", weight="bold", size=13)),
],
rows=table_rows,
border=ft.border.all(1, Colors.GREY_400),
border_radius=10,
horizontal_lines=ft.border.BorderSide(1, Colors.GREY_300),
heading_row_color=Colors.BLUE_GREY_100,
)
def close_dialog(e):
page.close(statreps_dialog)
# Use the same scrolling pattern that works on the main screen
# Step 1: Put table in a Row for horizontal scrolling
horizontal_table_row = ft.Row(
controls=[data_table],
scroll=ft.ScrollMode.ALWAYS,
height=600, # Increased to accommodate taller rows
)
# Step 2: Put the Row in a Column for vertical scrolling
dialog_content_column = ft.Column(
controls=[
ft.Text(
f"Showing {len(results)} most recent report(s)",
size=14,
color=Colors.GREY_700
),
ft.Divider(height=10),
horizontal_table_row, # The horizontally scrollable row
],
spacing=10,
scroll=ft.ScrollMode.ALWAYS, # Vertical scrolling
height=650, # Increased Column height
)
# Step 3: Wrap Column in another Row for the outer container
dialog_outer_row = ft.Row(
controls=[dialog_content_column],
scroll=ft.ScrollMode.ALWAYS,
height=700, # Larger than Column
)
# Step 4: Wrap in Container with explicit dimensions
scrollable_container = ft.Container(
content=dialog_outer_row,
width=1000, # Wide container
height=750, # Increased to show more content
padding=10,
border=ft.border.all(1, Colors.GREY_400),
border_radius=10,
)
# Create AlertDialog
statreps_dialog = ft.AlertDialog(
modal=True,
title=ft.Text(f"Recent STATREPs - {state} / {neighborhood}", size=20, weight="bold"),
content=scrollable_container,
actions=[
ft.ElevatedButton(
text="Copy CSV",
icon=ft.Icons.CONTENT_COPY,
on_click=copy_csv_to_clipboard,
bgcolor=Colors.GREEN_700,
color=Colors.WHITE
),
ft.ElevatedButton(
text="Close",
on_click=close_dialog,
bgcolor=Colors.BLUE_700,
color=Colors.WHITE
)
],
actions_alignment=ft.MainAxisAlignment.END,
)
page.open(statreps_dialog)
submit_button = ft.ElevatedButton(
text="Submit STATREP",
on_click=submit_clicked,
width=200,
bgcolor=Colors.GREEN_700,
color=Colors.WHITE,
height=50
)
show_statreps_button = ft.ElevatedButton(
text="Show STATREPs",
on_click=show_statreps_clicked,
width=200,
bgcolor=Colors.BLUE_700,
color=Colors.WHITE,
height=50
)
# Build the page with improved mobile scrollability
# Create the main content column with vertical scrolling
# Create the ReadyCorps styled header
readycorps_header = ft.Column(
controls=[
# Top part: Ready (green/teal) + Corps (orange)
ft.Row([
ft.Text(
"Ready",
size=58,
weight="bold",
color="#2D5F5D" # Dark teal/green color
),
ft.Text(
"Corps",
size=58,
weight="bold",
color="#8B3A1F" # Darker red/rust color
),
], spacing=0, alignment=ft.MainAxisAlignment.START),
# Bottom part: BY AMERICAN CONTINGENCY with flag
ft.Row([
ft.Text(
" BY AMERICAN CONTINGENCY",
size=11,
weight="w400",
color="#000000",
font_family="sans-serif"
),
# Stars (represented with ★ symbols)
ft.Container(
content=ft.Row([
ft.Text("★", size=10, color="#FFFFFF"),
ft.Text("★", size=10, color="#FFFFFF"),
ft.Text("★", size=10, color="#FFFFFF"),
], spacing=2),
bgcolor="#3C3B6E", # Blue field color
padding=ft.padding.only(left=5, right=5, top=2, bottom=2),
border_radius=2
),
# Two thick red stripes
ft.Container(
content=ft.Column([
ft.Container(height=4, bgcolor="#B85C38"), # Red stripe
ft.Container(height=2, bgcolor="#FFFFFF"), # White gap
ft.Container(height=4, bgcolor="#B85C38"), # Red stripe
], spacing=0),
width=80, # 3x longer than before (was 40)
border_radius=2
),
], spacing=5, alignment=ft.MainAxisAlignment.START),
],
spacing=2
)
main_content_column = ft.Column(
controls=[
readycorps_header,
ft.Row([
ft.Text("STATREP Submission", size=20, weight="w500", color=Colors.GREY_700),
ft.Text("v3.20", size=14, color=Colors.GREY_600, italic=True),
], alignment=ft.MainAxisAlignment.START, spacing=10),
ft.Text("Use this form to submit your Status Report", size=16),
ft.Divider(height=5),
ft.Container(
content=ft.Text(
"📝 Start typing your handle, when you see it press tab. \n Arrow up and down, and hit enter.\n"
"🔐 Fill in your pin, and hit enter or click verify pin. \n\nFrom there you can submit, or see recent statreps.\nScroll to bottom to see the buttons.",
size=13,
color=Colors.GREY_700,
italic=True
),
padding=ft.padding.only(left=10, bottom=5, top=5)
),
self.status_message,
ft.Divider(height=5),
# Required fields
self.handle_field,
self.handle_suggestions,
self.pin_row,
self.datetime_field,
self.state_field,
self.state_suggestions,
self.neighborhood_field,
self.neighborhood_suggestions,
self.location_field,
ft.Text("Current Conditions:", size=16, weight="bold"),
self.conditions_group,
# Optional fields (hidden by default)
self.optional_fields,
# Buttons
ft.Divider(height=20),
ft.Row(
controls=[submit_button, show_statreps_button],
spacing=20,
wrap=True # Allow wrapping on small screens
),
],
spacing=5,
scroll=ft.ScrollMode.ALWAYS, # Enable vertical scrolling
height=1000, # Increased height to show more content when space available
horizontal_alignment=ft.CrossAxisAlignment.START,
)
# Wrap in a Row for horizontal scrolling
horizontal_scroll_row = ft.Row(
controls=[main_content_column],
scroll=ft.ScrollMode.ALWAYS, # Enable horizontal scrolling
height=1050, # Larger than Column height
)
# Wrap in a container with dimensions
scrollable_main = ft.Container(
content=horizontal_scroll_row,
padding=10,
expand=True, # Fill available space
height=1100, # Larger than Row height to enable scrolling
border=ft.border.all(1, Colors.GREY_400),
)
# Add the scrollable content to the page
page.add(scrollable_main)
# Cleanup on close
def on_close(e):
logger.info("Application closing - cleaning up database connections")
if self.db:
self.db.close()
if self.handles_db:
self.handles_db.close()
if self.locations_db:
self.locations_db.close()
page.on_close = on_close
def show_pin_change_dialog(self, handle, page):
"""Show modal dialog to force PIN change for temporary PINs"""
logger.info(f"Showing PIN change dialog for handle: {handle}")
# Create dialog fields
dialog_status = ft.Text(
value="Your temporary PIN must be changed before you can submit.",
color=Colors.ORANGE,
size=14,
weight="bold"
)
new_pin_field = ft.TextField(
label="New PIN",
hint_text="At least 4 characters",
password=True,
can_reveal_password=True,
autofocus=True,
width=300
)
confirm_pin_field = ft.TextField(
label="Confirm New PIN",
hint_text="Re-enter your new PIN",
password=True,
can_reveal_password=True,
width=300
)
def change_pin_clicked(e):
# Validate inputs
new_pin = new_pin_field.value
confirm_pin = confirm_pin_field.value
if not new_pin or not confirm_pin:
dialog_status.value = "Please enter PIN in both fields"
page.update()
return
if len(new_pin) < 4:
dialog_status.value = "PIN must be at least 4 characters"
page.update()
return
if new_pin != confirm_pin:
dialog_status.value = "New PINs do not match"
page.update()
return
if new_pin.lower().startswith('z'):
dialog_status.value = "PIN cannot start with 'z' (reserved for temporary PINs)"
page.update()
return
# Change the PIN
success, error = self.handles_db.change_pin(handle, new_pin)
if success:
# Close dialog using correct Flet API
page.close(pin_change_dialog)
# Update the PIN field with new PIN
self.pin_field.value = new_pin
# Mark as verified since we just changed it
self.pin_verified = True
# Show success message