import streamlit as st
import pandas as pd
import io
import re
from datetime import datetime
# Configure the page
st.set_page_config(
page_title="CareCoordinaor Viewer",
page_icon="📅",
layout="wide"
)
# Custom CSS with hover effects and better styling
# Custom CSS with trendy, compact styling and dark borders
st.markdown("""
""", unsafe_allow_html=True)
def parse_excel_file(uploaded_file):
"""Parse the uploaded Excel file and extract schedule data"""
try:
# Read the Excel file
df = pd.read_excel(uploaded_file, header=0, engine='openpyxl')
# Extract data
times = df.iloc[:, 0].dropna().astype(str).tolist()
clients = df.columns[1:].tolist()
# Extract assignments
assignments = []
for i in range(len(times)):
row_assignments = df.iloc[i, 1:].fillna('').astype(str).tolist()
while len(row_assignments) < len(clients):
row_assignments.append('')
assignments.append(row_assignments)
return {
'times': times,
'clients': clients,
'assignments': assignments
}
except Exception as e:
st.error(f"Error parsing Excel file: {str(e)}")
return None
def render_client_modal(client, client_index, schedule_data):
"""Render a modal with hourly view for a specific client"""
with st.expander(f"📊 Hourly Schedule for {client}", expanded=True):
st.markdown(f"### 📋 Detailed Schedule for {client}")
# Create hourly view for this client
hourly_data = []
for i, time in enumerate(schedule_data['times']):
assignment = schedule_data['assignments'][i][client_index]
hourly_data.append({
'Time Slot': time,
'Assignment': assignment
})
df_hourly = pd.DataFrame(hourly_data)
# Display with styling
for _, row in df_hourly.iterrows():
col1, col2 = st.columns([2, 3])
with col1:
st.markdown(f"**{row['Time Slot']}**")
with col2:
assignment = row['Assignment']
if assignment and 'NAP TIME' in assignment.upper():
st.markdown(f'
{assignment}
', unsafe_allow_html=True)
elif assignment and assignment.strip():
st.markdown(f'{assignment}
', unsafe_allow_html=True)
else:
st.markdown("No assignment
", unsafe_allow_html=True)
def render_client_card(client, client_index, schedule_data):
"""Render a single client card with all assignments"""
# Use container for the card with hover effects
with st.container():
st.markdown(f'', unsafe_allow_html=True)
st.markdown(f'', unsafe_allow_html=True)
# Group consecutive time slots with the same assignment
assignments = group_assignments(client_index, schedule_data)
if not assignments:
st.markdown('
No assignments scheduled
', unsafe_allow_html=True)
else:
# Use a container for assignments
st.markdown('
', unsafe_allow_html=True)
for assignment in assignments:
col1, col2 = st.columns([2, 3])
with col1:
st.markdown(f'
{assignment["start"]} - {assignment["end"]}
', unsafe_allow_html=True)
with col2:
tech = assignment['tech']
if 'NAP TIME' in tech.upper():
st.markdown(f'
{tech}
', unsafe_allow_html=True)
else:
st.markdown(f'
{tech}
', unsafe_allow_html=True)
st.markdown('
', unsafe_allow_html=True)
st.markdown('
', unsafe_allow_html=True)
# Add click functionality using Streamlit's button
if st.button(f"📋 View {client}", key=f"btn_{client_index}", use_container_width=True):
render_client_modal(client, client_index, schedule_data)
def render_condensed_view(schedule_data):
"""Render the condensed view with enhanced styling"""
clients = schedule_data['clients']
# Create columns for clients (3 per row)
cols_per_row = 3
for i in range(0, len(clients), cols_per_row):
cols = st.columns(cols_per_row)
for j, col in enumerate(cols):
client_index = i + j
if client_index < len(clients):
client = clients[client_index]
with col:
render_client_card(client, client_index, schedule_data)
def group_assignments(client_index, schedule_data):
"""Group consecutive time slots with the same assignment"""
assignments = []
current_tech = None
start_time = None
for i, time in enumerate(schedule_data['times']):
tech = schedule_data['assignments'][i][client_index]
time_parts = str(time).split(' - ')
current_start = time_parts[0]
if tech != current_tech:
if current_tech is not None and str(current_tech).strip():
prev_time_parts = str(schedule_data['times'][i-1]).split(' - ')
end_time = prev_time_parts[1] if len(prev_time_parts) > 1 else prev_time_parts[0]
assignments.append({
'start': start_time,
'end': end_time,
'tech': current_tech
})
current_tech = tech
start_time = current_start
if current_tech is not None and str(current_tech).strip():
last_time = str(schedule_data['times'][-1]).split(' - ')
end_time = last_time[1] if len(last_time) > 1 else last_time[0]
assignments.append({
'start': start_time,
'end': end_time,
'tech': current_tech
})
return assignments
def create_sample_data():
"""Create sample data for demonstration"""
sample_data = {
'Time of Day': ['0700 - 0730', '0730 - 0800', '0800 - 0830', '0830 - 0900',
'0900 - 0930', '0930 - 1000', '1000 - 1030', '1300 - 1330',
'1330 - 1400', '1400 - 1430', '1500 - 1530', '1530 - 1600'],
'Client A': ['', 'Tech1', 'Tech1', 'Tech1', 'Tech2', 'Tech2', 'Tech2',
'NAP TIME', 'Tech1', 'Tech1', 'Tech3', 'Tech3'],
'Client B': ['Tech3', 'Tech3', '', 'Tech4', 'Tech4', 'Tech4', 'Tech3',
'Tech3', 'NAP TIME', 'Tech4', 'Tech1', 'Tech1'],
'Client C': ['', '', 'Tech5', 'Tech5', 'Tech5', 'NAP TIME', 'NAP TIME',
'Tech6', 'Tech6', 'Tech6', 'Tech4', 'Tech4'],
'Client D': ['Tech7', 'Tech7', 'Tech7', 'NAP TIME', 'NAP TIME', 'Tech8',
'Tech8', 'Tech8', 'Tech7', 'Tech7', '', ''],
'Client E': ['', 'Tech9', 'Tech9', 'Tech9', 'Tech10', 'Tech10', 'NAP TIME',
'NAP TIME', 'Tech9', 'Tech9', 'Tech10', 'Tech10'],
'Client F': ['Tech11', 'Tech11', 'Tech11', 'Tech11', '', '', 'Tech12',
'Tech12', 'Tech12', 'NAP TIME', 'NAP TIME', 'Tech11']
}
return pd.DataFrame(sample_data)
def calculate_statistics(schedule_data):
"""Calculate and return schedule statistics"""
total_slots = len(schedule_data['times']) * len(schedule_data['clients'])
assigned_slots = sum(1 for i in range(len(schedule_data['times']))
for j in range(len(schedule_data['clients']))
if schedule_data['assignments'][i][j] and
schedule_data['assignments'][i][j].strip() and
'NAP TIME' not in schedule_data['assignments'][i][j].upper())
nap_slots = sum(1 for i in range(len(schedule_data['times']))
for j in range(len(schedule_data['clients']))
if schedule_data['assignments'][i][j] and
'NAP TIME' in schedule_data['assignments'][i][j].upper())
empty_slots = total_slots - assigned_slots - nap_slots
return {
'total_assignments': assigned_slots,
'nap_slots': nap_slots,
'empty_slots': empty_slots,
'total_clients': len(schedule_data['clients']),
'total_timeslots': len(schedule_data['times'])
}
def extract_date_from_excel(uploaded_file):
"""Extract date from Excel file metadata or filename"""
try:
# First try to get the file creation/modification date
if hasattr(uploaded_file, 'name'):
# Try filename pattern first (schedule_YYYY_MM_DD.xlsx)
filename = uploaded_file.name
date_match = re.search(r'schedule_(\d{4})_(\d{2})_(\d{2})', filename)
if date_match:
year, month, day = date_match.groups()
return f"{year}-{month}-{day}"
# If no pattern in filename, use current date as fallback
return datetime.now().strftime("%Y-%m-%d")
else:
return datetime.now().strftime("%Y-%m-%d")
except:
return datetime.now().strftime("%Y-%m-%d")
def generate_soap_notes_table(schedule_data, uploaded_file):
"""Generate SOAP notes table with client-tech assignments"""
soap_notes_data = []
# Extract date from Excel file
schedule_date = extract_date_from_excel(uploaded_file)
# Collect unique client-tech combinations
client_tech_combinations = set()
for client_index, client in enumerate(schedule_data['clients']):
for time_index, time in enumerate(schedule_data['times']):
tech = schedule_data['assignments'][time_index][client_index]
# Only include actual tech assignments (not empty or nap time)
if tech and tech.strip() and 'NAP TIME' not in tech.upper():
client_tech_combinations.add((client, tech))
# Convert to list and create table data
for idx, (client, tech) in enumerate(sorted(client_tech_combinations)):
filename = f"{client}_{tech}.csv".replace(" ", "_")
soap_notes_data.append({
'ID': idx + 1,
'Filename': filename,
'Client Name': client,
'Tech Name': tech,
'Date': schedule_date
})
return pd.DataFrame(soap_notes_data)
def export_individual_client_tech_files(schedule_data, uploaded_file):
"""Export individual CSV files for each client-tech combination"""
# Extract date from Excel file
schedule_date = extract_date_from_excel(uploaded_file)
client_tech_files = {}
for client_index, client in enumerate(schedule_data['clients']):
# Get all time slots and assignments for this client
client_data = []
for time_index, time in enumerate(schedule_data['times']):
tech = schedule_data['assignments'][time_index][client_index]
client_data.append({
'Time Slot': time,
'Tech Assignment': tech,
'Date': schedule_date,
'Client': client
})
# Group by tech and create individual files
tech_assignments = {}
for entry in client_data:
tech = entry['Tech Assignment']
if tech and tech.strip() and 'NAP TIME' not in tech.upper():
if tech not in tech_assignments:
tech_assignments[tech] = []
tech_assignments[tech].append(entry)
# Create CSV files for each tech
for tech, assignments in tech_assignments.items():
filename = f"{client}_{tech}.csv".replace(" ", "_")
df_tech = pd.DataFrame(assignments)
csv_data = df_tech.to_csv(index=False)
client_tech_files[filename] = csv_data
return client_tech_files
def main():
# Header
st.markdown('📅 CareCoordinator Viewer
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
# Legend
st.markdown("""
""", unsafe_allow_html=True)
# File upload
uploaded_file = st.file_uploader(
"📁 Upload Excel File",
type=['xlsx', 'xls'],
help="Upload your schedule Excel file. First column should be time slots, first row should be client names."
)
# Use sample data if no file uploaded
use_sample = st.checkbox("Use sample data for demonstration", value=(uploaded_file is None))
if uploaded_file is not None or use_sample:
if use_sample and uploaded_file is None:
# Create sample Excel file in memory
sample_df = create_sample_data()
excel_buffer = io.BytesIO()
sample_df.to_excel(excel_buffer, index=False, engine='openpyxl')
excel_buffer.seek(0)
uploaded_file = excel_buffer
st.info("🔬 Using sample data for demonstration")
# Parse the Excel file
with st.spinner("🔄 Processing Excel file..."):
schedule_data = parse_excel_file(uploaded_file)
if schedule_data:
st.success(f"✅ Successfully loaded schedule with {len(schedule_data['clients'])} clients and {len(schedule_data['times'])} time slots")
# Calculate and display statistics
stats = calculate_statistics(schedule_data)
col1, col2, col3, col4 = st.columns(4)
with col1:
st.markdown(f'👥 Clients
{stats["total_clients"]}
', unsafe_allow_html=True)
with col2:
st.markdown(f'⏰ Assignments
{stats["total_assignments"]}
', unsafe_allow_html=True)
with col3:
st.markdown(f'😴 Nap Times
{stats["nap_slots"]}
', unsafe_allow_html=True)
with col4:
st.markdown(f'📊 Time Slots
{stats["total_timeslots"]}
', unsafe_allow_html=True)
st.markdown("---")
# Render the condensed view
render_condensed_view(schedule_data)
# Export section
st.markdown("---")
st.subheader("📤 Export Data")
# Create downloadable data
export_data = []
for i, time in enumerate(schedule_data['times']):
row = {'Time Slot': time}
for j, client in enumerate(schedule_data['clients']):
row[client] = schedule_data['assignments'][i][j]
export_data.append(row)
df_export = pd.DataFrame(export_data)
# CSV download
csv = df_export.to_csv(index=False)
st.download_button(
label="📥 Download as CSV",
data=csv,
file_name="schedule_export.csv",
mime="text/csv",
help="Download the schedule data as CSV file")
# SOAP Notes Section
st.markdown("---")
st.subheader("📝 SOAP Notes of the Clients")
# Generate SOAP notes table
soap_notes_df = generate_soap_notes_table(schedule_data, uploaded_file)
if not soap_notes_df.empty:
# Display the table
st.dataframe(
soap_notes_df,
use_container_width=True,
height=min(400, 35 * len(soap_notes_df) + 38)
)
# Show the extracted date
schedule_date = extract_date_from_excel(uploaded_file)
st.info(f"📅 Schedule Date: {schedule_date}")
# Export individual client-tech files
st.markdown("**Export Individual Client-Tech Files:**")
client_tech_files = export_individual_client_tech_files(schedule_data, uploaded_file)
if client_tech_files:
col1, col2 = st.columns([3, 1])
with col1:
# Dropdown to select individual files
selected_file = st.selectbox(
"Select file to download:",
options=list(client_tech_files.keys())
)
with col2:
if selected_file:
st.download_button(
label=f"📥 {selected_file}",
data=client_tech_files[selected_file],
file_name=selected_file,
mime="text/csv",
key=f"download_{selected_file}"
)
# Bulk download all files as ZIP
import zipfile
if st.button("📦 Download All as ZIP", key="download_all_zip"):
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
for filename, csv_data in client_tech_files.items():
zip_file.writestr(filename, csv_data)
zip_buffer.seek(0)
st.download_button(
label="📥 Download ZIP File",
data=zip_buffer,
file_name="client_tech_files.zip",
mime="application/zip",
key="download_zip"
)
else:
st.info("No client-tech assignments found for SOAP notes.")
else:
# Show instructions when no file is uploaded
st.info("👆 Please upload an Excel file or check 'Use sample data' to get started")
# Example of expected format
with st.expander("📋 Expected Excel File Format"):
st.markdown("""
### Expected Excel Structure:
| Time of Day | Client A | Client B | Client C | ... |
|-------------|----------|----------|----------|-----|
| 0700 - 0730 | | Tech1 | | ... |
| 0730 - 0800 | Tech2 | Tech1 | | ... |
| 0800 - 0830 | Tech2 | | Tech3 | ... |
| ... | ... | ... | ... | ... |
| NAP TIME | NAP TIME | Tech4 | NAP TIME | ... |
**Requirements:**
- **First column**: Time slots (e.g., "0700 - 0730")
- **First row**: Client names as headers
- **Data cells**: Technician names or "NAP TIME"
- **File format**: .xlsx or .xls
""")
# Show sample data preview
st.markdown("### Sample Data Preview:")
sample_df = create_sample_data()
st.dataframe(sample_df, use_container_width=True)
if __name__ == "__main__":
main()