Skip to content

Instantly share code, notes, and snippets.

@tomginsberg
Created November 28, 2023 23:39
Show Gist options
  • Save tomginsberg/d182cc8c1ec11d14713355b503f7ed5c to your computer and use it in GitHub Desktop.
Save tomginsberg/d182cc8c1ec11d14713355b503f7ed5c to your computer and use it in GitHub Desktop.
timesheet_ui
import calendar
import datetime
import json
import os
import streamlit as st
import yaml
from time import sleep
from plotly import graph_objects as go
import numpy as np
import shutil
st.set_page_config(layout="wide")
def is_valid_date_dir(directory: str):
"""
Checks if the given directory name matches the date pattern (YYYY-MM-DD).
:param directory: Name of the directory.
:return: True if it matches the date pattern, False otherwise.
"""
import re
return bool(re.match(r'^\d{4}-\d{2}-\d{2}$', directory))
def update_timesheet(
timesheet_data,
project_hours,
updated_notes,
project_data,
project_notes_path,
timesheet_json_path
):
project_data['hours'] = project_hours
if project_notes_path:
with open(project_notes_path, 'w') as f:
f.write(updated_notes)
with open(timesheet_json_path, 'w') as f:
json.dump(timesheet_data, f, indent=4)
with open(timesheet_json_path, 'r') as f:
st.session_state['timesheet_data'] = json.load(f)
def sidebar():
all_items = os.listdir()
date_directories = [item for item in all_items if os.path.isdir(item) and is_valid_date_dir(item)]
selected_date = st.sidebar.date_input("Select a date", datetime.date.today())
current_year, current_month = selected_date.year, selected_date.month
num_days = calendar.monthrange(current_year, current_month)[1]
all_weekdays_in_month = [datetime.date(current_year, current_month, day)
for day in range(1, num_days + 1)
if datetime.date(current_year, current_month, day).weekday() < 5 and
datetime.date(current_year, current_month, day) < datetime.date.today()]
existing_dates = set(datetime.date(int(year), int(month), int(day))
for year, month, day in
(date_dir.split('-') for date_dir in date_directories if is_valid_date_dir(date_dir)))
missing_weekdays = [date for date in all_weekdays_in_month if date not in existing_dates]
month_name = calendar.month_name[current_month]
st.sidebar.write('\n' * 100)
st.sidebar.markdown(f":red[**Missing days for {month_name}**]")
st.sidebar.code(', '.join([date.strftime("%d") for date in missing_weekdays]))
return selected_date, date_directories
def editor(selected_date, date_directories):
all_projects = json.load(open('projects.json', 'r'))
date_dir = selected_date.strftime("%Y-%m-%d")
timesheet_path = os.path.join(date_dir, 'timesheet.json')
editor, viewer, project_viewer = st.tabs(["Editor", "Raw Viewer", "Monthly Summary"])
if os.path.exists(timesheet_path):
with open(timesheet_path, 'r') as file:
timesheet_data = json.load(file)
else:
timesheet_data = {'hours': 0.0, 'projects': []}
with editor:
delete_file = None
projects = [x['name'] for x in timesheet_data['projects']]
other_projects = [f'+ {x}' for x in all_projects if x not in projects]
total_hours_submitted = timesheet_data['hours']
summed_hours = sum([x['hours'] for x in timesheet_data['projects']])
updated_hours = st.number_input(f"Total Hours for Today (>= {summed_hours})",
value=total_hours_submitted, step=0.25,
format="%.2f")
project = st.selectbox("View Project", projects + other_projects)
if project in projects:
project_idx = projects.index(project)
project_data = timesheet_data['projects'][project_idx]
else:
project = project.removeprefix('+ ')
project_data = {'name': project, 'hours': 0.0, 'notes': f'{date_dir}/{project}-notes.txt',
'media': None}
timesheet_data['projects'].append(project_data)
project_idx = len(timesheet_data['projects']) - 1
project_hours = st.number_input("Project Hours", value=project_data['hours'], step=0.25, format="%.2f")
project_notes_path = project_data['notes']
if os.path.exists(project_notes_path):
with open(project_notes_path, 'r') as f:
project_notes = f.read()
else:
project_notes = ''
notes = st.text_area(f"Notes for {project}", value=project_notes)
media = st.file_uploader("Media", accept_multiple_files=True)
media_folder = project_data['media']
if media:
if media_folder is None:
if not os.path.exists(date_dir):
os.mkdir(date_dir)
media_folder = os.path.join(date_dir, f"{project}-media")
os.mkdir(media_folder)
project_data['media'] = media_folder
if not os.path.exists(media_folder):
os.mkdir(media_folder)
for file in media:
with open(os.path.join(media_folder, file.name), 'wb') as f:
f.write(file.getbuffer())
st.success(f"Media files saved to {media_folder}")
if media_folder and os.path.exists(media_folder):
media_files = [file for file in os.listdir(media_folder) if
os.path.isfile(os.path.join(media_folder, file))]
if media_files:
st.code(media_files, language='python')
# remove files
delete_file = st.text_area("Delete File (Enter file name)", value='')
# make side by side buttons
if st.button(':green[Update]'):
if not os.path.exists(date_dir):
os.mkdir(date_dir)
with open(project_notes_path, 'w') as f:
f.write(notes)
project_data['hours'] = project_hours
timesheet_data['projects'][project_idx] = project_data
summed_hours = sum([x['hours'] for x in timesheet_data['projects']])
if updated_hours < summed_hours:
updated_hours = summed_hours
timesheet_data['hours'] = updated_hours
with open(timesheet_path, 'w') as f:
json.dump(timesheet_data, f, indent=4)
delete_success = True
if delete_file and delete_file != '':
if delete_file in media_files:
os.remove(os.path.join(media_folder, delete_file))
st.success(f"Deleted {delete_file}")
else:
st.error(f"File {delete_file} does not exist in {media_folder}")
delete_success = False
if delete_success:
st.success('Project details updated successfully!')
sleep(1)
st.experimental_rerun()
if st.button(':red[Delete Project]'):
timesheet_data['projects'].pop(project_idx)
with open(timesheet_path, 'w') as f:
json.dump(timesheet_data, f, indent=4)
# delete notes file
if os.path.exists(project_notes_path):
os.remove(project_notes_path)
# delete media folder
if media_folder and os.path.exists(media_folder):
shutil.rmtree(media_folder)
st.success(f"Deleted {date_dir}/{project}")
sleep(1)
st.experimental_rerun()
with viewer:
# check if timesheet.json exists
prefix = ''
if not os.path.exists(timesheet_path):
prefix = f"# Draft Timesheet for {date_dir} (not saved)\n"
st.code(prefix + yaml.dump(timesheet_data, indent=4, sort_keys=False), language='yaml')
with project_viewer:
# get the total number of hours for each project this month
project_hours = {}
accumulated_project_hours = {}
for date_dir in date_directories:
if int(date_dir.split('-')[1]) != selected_date.month:
continue
timesheet_path = os.path.join(date_dir, 'timesheet.json')
if os.path.exists(timesheet_path):
with open(timesheet_path, 'r') as f:
timesheet_data = json.load(f)
for project in timesheet_data['projects']:
project_name = project['name']
if project_name not in project_hours:
project_hours[project_name] = 0
accumulated_project_hours[project_name] = [], []
project_hours[project_name] += project['hours']
accumulated_project_hours[project_name][1].append(project['hours'])
accumulated_project_hours[project_name][0].append(
datetime.datetime.strptime(date_dir, "%Y-%m-%d"))
fig = go.Figure()
fig.add_trace(go.Bar(x=list(project_hours.keys()), y=list(project_hours.values())))
fig.update_layout(title_text='Total Project Hours This Month')
st.plotly_chart(fig, use_container_width=True)
# line plot accumulated hours
fig = go.Figure()
for project_name, (dates, hours) in accumulated_project_hours.items():
dates, hours = zip(*sorted(zip(dates, hours)))
hours = np.cumsum(hours)
fig.add_trace(go.Scatter(x=dates, y=hours, name=project_name))
fig.update_layout(title_text='Accumulated Project Hours This Month')
st.plotly_chart(fig, use_container_width=True)
if __name__ == '__main__':
selected_date, date_directories = sidebar()
editor(selected_date, date_directories)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment