Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -50,6 +50,8 @@ BOTTOM_MARGIN_MM = 5
|
|
| 50 |
PRINTABLE_HEIGHT_MM = PAGE_HEIGHT_MM - TOP_MARGIN_MM - BOTTOM_MARGIN_MM # ≈ 290 mm
|
| 51 |
# Approximate height of a single text line in the calendar layout (in mm)
|
| 52 |
LINE_HEIGHT_MM = 0.90 # 6.8 pt ≈ 0.9 mm
|
|
|
|
|
|
|
| 53 |
# ========================================== Shared CSS (Used in both HTML preview and PDF generation) ==========================================
|
| 54 |
# CSS styles shared between the HTML preview and the PDF output.
|
| 55 |
# Uses placeholders {{locations}}, {{start}}, {{end}}, {{time}} that are replaced at generation time.
|
|
@@ -348,7 +350,7 @@ def estimate_week_height(week_content, display_locs):
|
|
| 348 |
max_lines = 0
|
| 349 |
for day_html in week_content:
|
| 350 |
content = day_html.split(' ', 1)[1] if ' ' in day_html else day_html
|
| 351 |
-
lines = content.count(' ') + 1
|
| 352 |
if lines > max_lines:
|
| 353 |
max_lines = lines
|
| 354 |
base_height = 4.0
|
|
@@ -923,9 +925,8 @@ def combine_schedules(provider_info_file, provider_files, ma_files, start_date,
|
|
| 923 |
page_content = []
|
| 924 |
week_content = []
|
| 925 |
current_page_height = 0
|
| 926 |
-
is_first_page = True
|
| 927 |
|
| 928 |
-
# Static day headers (Mon-Sat)
|
| 929 |
day_headers = """
|
| 930 |
<div class="week day-headers">
|
| 931 |
<div class="week-number"></div>
|
|
@@ -949,14 +950,17 @@ def combine_schedules(provider_info_file, provider_files, ma_files, start_date,
|
|
| 949 |
if current.weekday() == 0 and len(week_content) > 0:
|
| 950 |
finished_week = f'<div class="week"><div class="week-number">{week_num}</div>{"".join(week_content)}</div>'
|
| 951 |
week_height = estimate_week_height(week_content, display_locs)
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
|
|
|
|
|
|
|
|
|
| 955 |
if current_page_height + needed > PRINTABLE_HEIGHT_MM:
|
| 956 |
-
|
|
|
|
| 957 |
page_content = []
|
| 958 |
current_page_height = 0
|
| 959 |
-
is_first_page = False
|
| 960 |
|
| 961 |
page_content.append(finished_week)
|
| 962 |
current_page_height += needed
|
|
@@ -964,22 +968,21 @@ def combine_schedules(provider_info_file, provider_files, ma_files, start_date,
|
|
| 964 |
week_num += 1
|
| 965 |
|
| 966 |
# Build the HTML for the current day
|
| 967 |
-
day_num = current.day
|
| 968 |
day_html = f'<div class="day">{current.strftime("%m/%d")}'
|
| 969 |
|
| 970 |
-
# Global conflict warnings
|
| 971 |
if perform_conflict:
|
| 972 |
for p, locs, cls, msg in check_provider_location_conflicts(providers_df, current, display_locs):
|
| 973 |
day_html += f'<div class="{cls}"><span class="warning-details">{msg}</span></div>'
|
| 974 |
|
| 975 |
-
#
|
| 976 |
has_clinic_close_anywhere = False
|
| 977 |
if 'Note' in providers_df.columns:
|
| 978 |
day_notes = providers_df[providers_df['Date'] == current]['Note'].dropna().astype(str).str.upper()
|
| 979 |
if day_notes.str.contains('CLINIC_CLOSE').any():
|
| 980 |
has_clinic_close_anywhere = True
|
| 981 |
|
| 982 |
-
# Overall age coverage
|
| 983 |
if perform_overall and not has_clinic_close_anywhere:
|
| 984 |
missing, _ = check_overall_age_coverage(providers_df, info_df, current, display_locs)
|
| 985 |
if missing:
|
|
@@ -990,7 +993,7 @@ def combine_schedules(provider_info_file, provider_files, ma_files, start_date,
|
|
| 990 |
loc_df = providers_df[(providers_df['Date'] == current) & (providers_df['Display_Location'] == loc)]
|
| 991 |
loc_info = info_df[info_df['Location'] == loc]
|
| 992 |
|
| 993 |
-
# Detect holiday or school-closed
|
| 994 |
is_holiday = is_school = False
|
| 995 |
if not loc_df.empty:
|
| 996 |
notes = loc_df['Note'].dropna().str.strip().str.upper().tolist()
|
|
@@ -999,7 +1002,6 @@ def combine_schedules(provider_info_file, provider_files, ma_files, start_date,
|
|
| 999 |
elif notes and all(n == 'SCHOOL CLOSED' for n in notes) and loc in NO_AGE_CHECK_LOCATIONS:
|
| 1000 |
is_school = True
|
| 1001 |
|
| 1002 |
-
# Remove completely empty rows
|
| 1003 |
loc_df = loc_df[~((loc_df['Start_Time'].isna()) & (loc_df['End_Time'].isna()) & (loc_df['Note'].isna() | (loc_df['Note'] == '')))]
|
| 1004 |
|
| 1005 |
if not loc_df.empty or is_holiday or is_school:
|
|
@@ -1009,7 +1011,6 @@ def combine_schedules(provider_info_file, provider_files, ma_files, start_date,
|
|
| 1009 |
elif is_school:
|
| 1010 |
day_html += '<div class="holiday-message">School Closed!</div>'
|
| 1011 |
else:
|
| 1012 |
-
# === NEW: Check for clinic closure ===
|
| 1013 |
if is_clinic_closed(providers_df, current, loc):
|
| 1014 |
day_html += '<div class="clinic-closed-warning">Clinic Closed!</div>'
|
| 1015 |
else:
|
|
@@ -1020,8 +1021,6 @@ def combine_schedules(provider_info_file, provider_files, ma_files, start_date,
|
|
| 1020 |
info_row = loc_info[loc_info['Provider'] == r['Name']]
|
| 1021 |
name = info_row['Last_Name'].iloc[0] if not info_row.empty else r['Name']
|
| 1022 |
tstr = get_time_string(r)
|
| 1023 |
-
|
| 1024 |
-
# Color coding based on age coverage
|
| 1025 |
if r['Name'] in full:
|
| 1026 |
color = "#ff6347"
|
| 1027 |
elif r['Name'] in under:
|
|
@@ -1038,17 +1037,14 @@ def combine_schedules(provider_info_file, provider_files, ma_files, start_date,
|
|
| 1038 |
day_html += f'<span style="{style}">{name}: {tstr}</span><br>'
|
| 1039 |
day_html += '</div>'
|
| 1040 |
|
| 1041 |
-
# Operational hour coverage warning
|
| 1042 |
if check_operation_coverage_flag and loc not in NO_OPERATION_CHECK_LOCATIONS:
|
| 1043 |
gaps = check_operation_time_coverage(providers_df, current, loc)
|
| 1044 |
if gaps:
|
| 1045 |
day_html += f'<div class="operation-warning"><span class="warning-details">Missing: {", ".join(gaps)}</span></div>'
|
| 1046 |
|
| 1047 |
-
# Per-location age coverage warning
|
| 1048 |
if check_age_coverage_flag and missing and loc not in NO_AGE_CHECK_LOCATIONS:
|
| 1049 |
day_html += f'<div class="warning"><span class="warning-details">Missing: {", ".join(missing)}</span></div>'
|
| 1050 |
|
| 1051 |
-
# MA scheduling and ratio check
|
| 1052 |
if check_ma_mismatch_flag:
|
| 1053 |
ma_loc_df = ma_df[(ma_df['Date'] == current) & (ma_df['Display_Location'] == loc)]
|
| 1054 |
ma_loc_df = ma_loc_df[~((ma_loc_df['Start_Time'].isna()) & (ma_loc_df['End_Time'].isna()) & (ma_loc_df['Note'].isna() | (ma_loc_df['Note'] == '')))]
|
|
@@ -1073,23 +1069,25 @@ def combine_schedules(provider_info_file, provider_files, ma_files, start_date,
|
|
| 1073 |
week_content.append(day_html)
|
| 1074 |
current += pd.Timedelta(days=1)
|
| 1075 |
|
| 1076 |
-
# Finalize
|
| 1077 |
if week_content:
|
| 1078 |
finished_week = f'<div class="week"><div class="week-number">{week_num}</div>{"".join(week_content)}</div>'
|
| 1079 |
week_height = estimate_week_height(week_content, display_locs)
|
| 1080 |
-
|
|
|
|
|
|
|
| 1081 |
if current_page_height + needed > PRINTABLE_HEIGHT_MM:
|
| 1082 |
-
html += f'<div class="page-group"><div class="week-group">{day_headers
|
| 1083 |
page_content = []
|
| 1084 |
current_page_height = 0
|
| 1085 |
-
|
| 1086 |
page_content.append(finished_week)
|
| 1087 |
|
| 1088 |
-
# Close
|
| 1089 |
if page_content:
|
| 1090 |
-
html += f'<div class="page-group"><div class="week-group">{day_headers
|
| 1091 |
|
| 1092 |
-
# Add weekly
|
| 1093 |
if show_weekly_hours:
|
| 1094 |
wh, wt = calculate_weekly_hours(providers_df, info_df, start_obj, end_obj, display_locs)
|
| 1095 |
html += '<div class="hours-table-section" style="break-before: page;">'
|
|
|
|
| 50 |
PRINTABLE_HEIGHT_MM = PAGE_HEIGHT_MM - TOP_MARGIN_MM - BOTTOM_MARGIN_MM # ≈ 290 mm
|
| 51 |
# Approximate height of a single text line in the calendar layout (in mm)
|
| 52 |
LINE_HEIGHT_MM = 0.90 # 6.8 pt ≈ 0.9 mm
|
| 53 |
+
# Estimated height of the day headers block (in mm) — measured empirically
|
| 54 |
+
DAY_HEADERS_HEIGHT_MM = 5.0
|
| 55 |
# ========================================== Shared CSS (Used in both HTML preview and PDF generation) ==========================================
|
| 56 |
# CSS styles shared between the HTML preview and the PDF output.
|
| 57 |
# Uses placeholders {{locations}}, {{start}}, {{end}}, {{time}} that are replaced at generation time.
|
|
|
|
| 350 |
max_lines = 0
|
| 351 |
for day_html in week_content:
|
| 352 |
content = day_html.split(' ', 1)[1] if ' ' in day_html else day_html
|
| 353 |
+
lines = content.count('<br>') + content.count('</div>') + 1 # Better estimate using tags
|
| 354 |
if lines > max_lines:
|
| 355 |
max_lines = lines
|
| 356 |
base_height = 4.0
|
|
|
|
| 925 |
page_content = []
|
| 926 |
week_content = []
|
| 927 |
current_page_height = 0
|
|
|
|
| 928 |
|
| 929 |
+
# Static day headers (Mon-Sat) — now included on EVERY page
|
| 930 |
day_headers = """
|
| 931 |
<div class="week day-headers">
|
| 932 |
<div class="week-number"></div>
|
|
|
|
| 950 |
if current.weekday() == 0 and len(week_content) > 0:
|
| 951 |
finished_week = f'<div class="week"><div class="week-number">{week_num}</div>{"".join(week_content)}</div>'
|
| 952 |
week_height = estimate_week_height(week_content, display_locs)
|
| 953 |
+
|
| 954 |
+
# Add day headers height only if this is the first week on the current page
|
| 955 |
+
extra_header = DAY_HEADERS_HEIGHT_MM if len(page_content) == 0 else 0
|
| 956 |
+
needed = week_height + extra_header + 1 # +1 mm buffer between weeks
|
| 957 |
+
|
| 958 |
+
# Page break logic
|
| 959 |
if current_page_height + needed > PRINTABLE_HEIGHT_MM:
|
| 960 |
+
# Close current page (always include headers)
|
| 961 |
+
html += f'<div class="page-group"><div class="week-group">{day_headers}{"".join(page_content)}</div></div>'
|
| 962 |
page_content = []
|
| 963 |
current_page_height = 0
|
|
|
|
| 964 |
|
| 965 |
page_content.append(finished_week)
|
| 966 |
current_page_height += needed
|
|
|
|
| 968 |
week_num += 1
|
| 969 |
|
| 970 |
# Build the HTML for the current day
|
|
|
|
| 971 |
day_html = f'<div class="day">{current.strftime("%m/%d")}'
|
| 972 |
|
| 973 |
+
# Global conflict warnings
|
| 974 |
if perform_conflict:
|
| 975 |
for p, locs, cls, msg in check_provider_location_conflicts(providers_df, current, display_locs):
|
| 976 |
day_html += f'<div class="{cls}"><span class="warning-details">{msg}</span></div>'
|
| 977 |
|
| 978 |
+
# Check for CLINIC_CLOSE note anywhere
|
| 979 |
has_clinic_close_anywhere = False
|
| 980 |
if 'Note' in providers_df.columns:
|
| 981 |
day_notes = providers_df[providers_df['Date'] == current]['Note'].dropna().astype(str).str.upper()
|
| 982 |
if day_notes.str.contains('CLINIC_CLOSE').any():
|
| 983 |
has_clinic_close_anywhere = True
|
| 984 |
|
| 985 |
+
# Overall age coverage
|
| 986 |
if perform_overall and not has_clinic_close_anywhere:
|
| 987 |
missing, _ = check_overall_age_coverage(providers_df, info_df, current, display_locs)
|
| 988 |
if missing:
|
|
|
|
| 993 |
loc_df = providers_df[(providers_df['Date'] == current) & (providers_df['Display_Location'] == loc)]
|
| 994 |
loc_info = info_df[info_df['Location'] == loc]
|
| 995 |
|
| 996 |
+
# Detect holiday or school-closed
|
| 997 |
is_holiday = is_school = False
|
| 998 |
if not loc_df.empty:
|
| 999 |
notes = loc_df['Note'].dropna().str.strip().str.upper().tolist()
|
|
|
|
| 1002 |
elif notes and all(n == 'SCHOOL CLOSED' for n in notes) and loc in NO_AGE_CHECK_LOCATIONS:
|
| 1003 |
is_school = True
|
| 1004 |
|
|
|
|
| 1005 |
loc_df = loc_df[~((loc_df['Start_Time'].isna()) & (loc_df['End_Time'].isna()) & (loc_df['Note'].isna() | (loc_df['Note'] == '')))]
|
| 1006 |
|
| 1007 |
if not loc_df.empty or is_holiday or is_school:
|
|
|
|
| 1011 |
elif is_school:
|
| 1012 |
day_html += '<div class="holiday-message">School Closed!</div>'
|
| 1013 |
else:
|
|
|
|
| 1014 |
if is_clinic_closed(providers_df, current, loc):
|
| 1015 |
day_html += '<div class="clinic-closed-warning">Clinic Closed!</div>'
|
| 1016 |
else:
|
|
|
|
| 1021 |
info_row = loc_info[loc_info['Provider'] == r['Name']]
|
| 1022 |
name = info_row['Last_Name'].iloc[0] if not info_row.empty else r['Name']
|
| 1023 |
tstr = get_time_string(r)
|
|
|
|
|
|
|
| 1024 |
if r['Name'] in full:
|
| 1025 |
color = "#ff6347"
|
| 1026 |
elif r['Name'] in under:
|
|
|
|
| 1037 |
day_html += f'<span style="{style}">{name}: {tstr}</span><br>'
|
| 1038 |
day_html += '</div>'
|
| 1039 |
|
|
|
|
| 1040 |
if check_operation_coverage_flag and loc not in NO_OPERATION_CHECK_LOCATIONS:
|
| 1041 |
gaps = check_operation_time_coverage(providers_df, current, loc)
|
| 1042 |
if gaps:
|
| 1043 |
day_html += f'<div class="operation-warning"><span class="warning-details">Missing: {", ".join(gaps)}</span></div>'
|
| 1044 |
|
|
|
|
| 1045 |
if check_age_coverage_flag and missing and loc not in NO_AGE_CHECK_LOCATIONS:
|
| 1046 |
day_html += f'<div class="warning"><span class="warning-details">Missing: {", ".join(missing)}</span></div>'
|
| 1047 |
|
|
|
|
| 1048 |
if check_ma_mismatch_flag:
|
| 1049 |
ma_loc_df = ma_df[(ma_df['Date'] == current) & (ma_df['Display_Location'] == loc)]
|
| 1050 |
ma_loc_df = ma_loc_df[~((ma_loc_df['Start_Time'].isna()) & (ma_loc_df['End_Time'].isna()) & (ma_loc_df['Note'].isna() | (ma_loc_df['Note'] == '')))]
|
|
|
|
| 1069 |
week_content.append(day_html)
|
| 1070 |
current += pd.Timedelta(days=1)
|
| 1071 |
|
| 1072 |
+
# Finalize remaining week
|
| 1073 |
if week_content:
|
| 1074 |
finished_week = f'<div class="week"><div class="week-number">{week_num}</div>{"".join(week_content)}</div>'
|
| 1075 |
week_height = estimate_week_height(week_content, display_locs)
|
| 1076 |
+
extra_header = DAY_HEADERS_HEIGHT_MM if len(page_content) == 0 else 0
|
| 1077 |
+
needed = week_height + extra_header + 1
|
| 1078 |
+
|
| 1079 |
if current_page_height + needed > PRINTABLE_HEIGHT_MM:
|
| 1080 |
+
html += f'<div class="page-group"><div class="week-group">{day_headers}{"".join(page_content)}</div></div>'
|
| 1081 |
page_content = []
|
| 1082 |
current_page_height = 0
|
| 1083 |
+
|
| 1084 |
page_content.append(finished_week)
|
| 1085 |
|
| 1086 |
+
# Close final page
|
| 1087 |
if page_content:
|
| 1088 |
+
html += f'<div class="page-group"><div class="week-group">{day_headers}{"".join(page_content)}</div></div>'
|
| 1089 |
|
| 1090 |
+
# Add weekly hours table
|
| 1091 |
if show_weekly_hours:
|
| 1092 |
wh, wt = calculate_weekly_hours(providers_df, info_df, start_obj, end_obj, display_locs)
|
| 1093 |
html += '<div class="hours-table-section" style="break-before: page;">'
|