Created
November 28, 2023 23:39
-
-
Save tomginsberg/d182cc8c1ec11d14713355b503f7ed5c to your computer and use it in GitHub Desktop.
timesheet_ui
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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