Refactor MedTrackerApp and UI components for improved structure and readability
- Simplified initialization logic in init.py - Consolidated testing_mode assignment - Removed unnecessary else statements - Created UIManager class to handle UI-related tasks - Modularized input frame creation, table frame creation, and graph frame creation - Enhanced edit window creation with better organization and error handling - Updated data management methods to improve clarity and maintainability - Improved logging for better debugging and tracking of application flow
This commit is contained in:
51
.gitea/workflows/build.yaml
Normal file
51
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
run-name: ${{ gitea.actor }} is building and pushing TheChart Docker image 🚀
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: gitea-http.taildb3494.ts.net
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: gitea-http.taildb3494.ts.net/will/thechart
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=sha,format=short
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache
|
||||||
|
cache-to: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache,mode=max
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ __pycache__/
|
|||||||
*.spec
|
*.spec
|
||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
|
.vscode/
|
||||||
|
|||||||
71
Dockerfile
71
Dockerfile
@@ -1,10 +1,13 @@
|
|||||||
# Use a Python Alpine base image
|
|
||||||
FROM python:3.13-alpine
|
FROM python:3.13-alpine
|
||||||
|
|
||||||
# Set the working directory in the container
|
ARG TARGET=thechart
|
||||||
|
ARG ICON=chart-671.png
|
||||||
|
ARG UID=1000
|
||||||
|
ARG GUID=1000
|
||||||
|
|
||||||
|
USER root
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install necessary system dependencies
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
build-base \
|
build-base \
|
||||||
libffi-dev \
|
libffi-dev \
|
||||||
@@ -15,17 +18,63 @@ RUN apk add --no-cache \
|
|||||||
xorg-server \
|
xorg-server \
|
||||||
xauth \
|
xauth \
|
||||||
xvfb \
|
xvfb \
|
||||||
&& pip install --upgrade pip
|
font-terminus font-inconsolata font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
|
||||||
|
&& pip install --upgrade pip && pip install pyinstaller python-dotenv
|
||||||
|
|
||||||
# Copy the requirements file and install dependencies
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy the application code
|
# RUN export uid=$UID gid=$GUID
|
||||||
COPY src/ .
|
RUN mkdir -p /home/docker_user
|
||||||
|
RUN echo "docker_user:x:${UID}:${GUID}:docker_user,,,:/home/docker_user:/bin/bash" >> /etc/passwd
|
||||||
|
RUN echo "docker_user:x:${UID}:" >> /etc/group
|
||||||
|
RUN mkdir -p /etc/sudoers.d && echo "docker_user ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/docker_user
|
||||||
|
RUN chmod 0440 /etc/sudoers.d/docker_user
|
||||||
|
RUN chown -R ${UID}:${GUID} /home/docker_user
|
||||||
|
|
||||||
# Expose the port your application listens on (if applicable)
|
COPY --chown=${UID}:${GUID} --chmod=765 ./src/ ./src/
|
||||||
# EXPOSE 8000
|
COPY --chown=${UID}:${GUID} --chmod=765 ./.env .
|
||||||
|
COPY --chown=${UID}:${GUID} --chmod=765 ./thechart_data.csv .
|
||||||
|
COPY --chown=${UID}:${GUID} --chmod=765 ./${ICON} .
|
||||||
|
|
||||||
# Define the command to run your application
|
ARG HOME=/home/docker_user
|
||||||
CMD ["python", "main.py"]
|
ENV HOME=/home/docker_user
|
||||||
|
|
||||||
|
# Make sure the icon file and data file are in the right location
|
||||||
|
RUN cp -f ./${ICON} ./src/ && cp -f ./thechart_data.csv ./.env ./src/
|
||||||
|
|
||||||
|
# Debug the environment
|
||||||
|
RUN echo "Icon file is: ${ICON}"
|
||||||
|
RUN ls -la ./src/
|
||||||
|
|
||||||
|
# Use a shell command with variable expansion to make sure variables are properly substituted
|
||||||
|
RUN sh -c "pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon=./src/chart-671.png --add-data='.env:.' --add-data='./src/chart-671.png:.' --add-data='./src/thechart_data.csv:.' --distpath ${HOME}/${TARGET} ./src/main.py"
|
||||||
|
|
||||||
|
RUN chown -R ${UID}:${GUID} /home/docker_user/
|
||||||
|
RUN chmod -R 777 /home/docker_user/${TARGET}
|
||||||
|
|
||||||
|
# Set environment variables for X11 forwarding
|
||||||
|
ENV DISPLAY=:0
|
||||||
|
ENV XAUTHORITY=/tmp/.docker.xauth
|
||||||
|
ENV QT_X11_NO_MITSHM=1
|
||||||
|
|
||||||
|
# Create a startup script to handle X11 setup and application launch
|
||||||
|
# Create a proper entrypoint script with correct line endings
|
||||||
|
RUN echo '#!/bin/sh' > /home/docker_user/entrypoint.sh && \
|
||||||
|
echo 'set -e' >> /home/docker_user/entrypoint.sh && \
|
||||||
|
echo 'echo "Starting entrypoint script..."' >> /home/docker_user/entrypoint.sh && \
|
||||||
|
echo 'echo "DISPLAY=$DISPLAY"' >> /home/docker_user/entrypoint.sh && \
|
||||||
|
echo 'echo "XAUTHORITY=$XAUTHORITY"' >> /home/docker_user/entrypoint.sh && \
|
||||||
|
echo 'echo "HOME=$HOME"' >> /home/docker_user/entrypoint.sh && \
|
||||||
|
echo 'mkdir -p $(dirname $XAUTHORITY)' >> /home/docker_user/entrypoint.sh && \
|
||||||
|
echo 'touch $XAUTHORITY || echo "Warning: Could not create $XAUTHORITY file"' >> /home/docker_user/entrypoint.sh && \
|
||||||
|
echo 'xauth nlist $DISPLAY | sed -e "s/^..../ffff/" | xauth -f $XAUTHORITY nmerge - || echo "Warning: X auth setup failed"' >> /home/docker_user/entrypoint.sh && \
|
||||||
|
echo "echo \"Starting application: \$HOME/${TARGET}/${TARGET}\"" >> /home/docker_user/entrypoint.sh && \
|
||||||
|
echo "exec \$HOME/${TARGET}/${TARGET}" >> /home/docker_user/entrypoint.sh && \
|
||||||
|
chmod +x /home/docker_user/entrypoint.sh
|
||||||
|
|
||||||
|
USER docker_user
|
||||||
|
|
||||||
|
# Set the entrypoint to our startup script through shell
|
||||||
|
ENTRYPOINT ["/bin/sh"]
|
||||||
|
CMD ["-c", "/home/docker_user/entrypoint.sh"]
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -13,7 +13,7 @@ setup-env:
|
|||||||
build:
|
build:
|
||||||
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE} --push .
|
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE} --push .
|
||||||
install:
|
install:
|
||||||
pyinstaller --name ${TARGET} --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data=".env:." src/main.py
|
pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data=".env:." --add-data='./src/chart-671.png:.' --add-data='./src/thechart_data.csv:.' src/main.py
|
||||||
cp -f ./dist/${TARGET} ${ROOT}/Applications/
|
cp -f ./dist/${TARGET} ${ROOT}/Applications/
|
||||||
cp -f ./deploy/${TARGET}.desktop ${ROOT}/.local/share/applications/
|
cp -f ./deploy/${TARGET}.desktop ${ROOT}/.local/share/applications/
|
||||||
desktop-file-validate ${ROOT}/.local/share/applications/${TARGET}.desktop
|
desktop-file-validate ${ROOT}/.local/share/applications/${TARGET}.desktop
|
||||||
|
|||||||
2
Pipfile
2
Pipfile
@@ -19,11 +19,11 @@ pytz = "==2025.2"
|
|||||||
six = "==1.17.0"
|
six = "==1.17.0"
|
||||||
tzdata = "==2025.2"
|
tzdata = "==2025.2"
|
||||||
colorlog = "*"
|
colorlog = "*"
|
||||||
|
dotenv = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pre-commit = "*"
|
pre-commit = "*"
|
||||||
pyinstaller = "*"
|
pyinstaller = "*"
|
||||||
dotenv = "*"
|
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.13"
|
python_version = "3.13"
|
||||||
|
|||||||
44
Pipfile.lock
generated
44
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "61b1a4d194b2257d4bef97d275fe5d947559a2346020473a0c45cb49940608ab"
|
"sha256": "3f2c36deb68c43ab2082c8b3f7f37b33345ad3693c498101d5b1d1edf6ff3223"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -98,6 +98,13 @@
|
|||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==0.12.1"
|
"version": "==0.12.1"
|
||||||
},
|
},
|
||||||
|
"dotenv": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.9.9"
|
||||||
|
},
|
||||||
"fonttools": {
|
"fonttools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0162a6a37b0ca70d8505311d541e291cd6cab54d1a986ae3d2686c56c0581e8f",
|
"sha256:0162a6a37b0ca70d8505311d541e291cd6cab54d1a986ae3d2686c56c0581e8f",
|
||||||
@@ -522,6 +529,14 @@
|
|||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||||
"version": "==2.9.0.post0"
|
"version": "==2.9.0.post0"
|
||||||
},
|
},
|
||||||
|
"python-dotenv": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc",
|
||||||
|
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==1.1.1"
|
||||||
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3",
|
"sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3",
|
||||||
@@ -572,13 +587,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.4.0"
|
"version": "==0.4.0"
|
||||||
},
|
},
|
||||||
"dotenv": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.9.9"
|
|
||||||
},
|
|
||||||
"filelock": {
|
"filelock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2",
|
"sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2",
|
||||||
@@ -650,19 +658,11 @@
|
|||||||
},
|
},
|
||||||
"pyinstaller-hooks-contrib": {
|
"pyinstaller-hooks-contrib": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:06779d024f7d60dd75b05520923bba16b17df5f64073434b23e570ffb71094dc",
|
"sha256:b0c19dbe9a8428665d2c5c4538eaa16b683579aabd7f2ecd31f41b116c4e4e57",
|
||||||
"sha256:223ae773733fb7a0ee9cb5e817480998a90a6c7a9c3d2b7b580d2dfa2b325751"
|
"sha256:ba4fecc94eb761de2015cd8b1e674354e8a1f13ba65047602912f34c20fb510b"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2025.6"
|
"version": "==2025.7"
|
||||||
},
|
|
||||||
"python-dotenv": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc",
|
|
||||||
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"
|
|
||||||
],
|
|
||||||
"markers": "python_version >= '3.9'",
|
|
||||||
"version": "==1.1.1"
|
|
||||||
},
|
},
|
||||||
"pyyaml": {
|
"pyyaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -733,11 +733,11 @@
|
|||||||
},
|
},
|
||||||
"virtualenv": {
|
"virtualenv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11",
|
"sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56",
|
||||||
"sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"
|
"sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==20.31.2"
|
"version": "==20.32.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ Name=Thechart
|
|||||||
Exec=sh -c "/home/will/Applications/thechart /home/will/Documents/thechart_data.csv"
|
Exec=sh -c "/home/will/Applications/thechart /home/will/Documents/thechart_data.csv"
|
||||||
Icon=/home/will/Code/thechart/chart-671.png
|
Icon=/home/will/Code/thechart/chart-671.png
|
||||||
Categories=Utility;
|
Categories=Utility;
|
||||||
StartupWMClass=tk # Crucial for Dock icon persistence
|
StartupWMClass=Tk # Crucial for Dock icon persistence
|
||||||
|
|||||||
20
docker-build.sh
Executable file
20
docker-build.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
CONTAINER_ENGINE="docker" # podman | docker
|
||||||
|
VERSION="v1.0.0"
|
||||||
|
REGISTRY="gitea-http.taildb3494.ts.net/will/thechart"
|
||||||
|
|
||||||
|
if [ "$CONTAINER_ENGINE" == "podman" ];
|
||||||
|
then
|
||||||
|
buildah build \
|
||||||
|
-t $REGISTRY:$VERSION \
|
||||||
|
--platform linux/amd64,linux/arm64/v8 \
|
||||||
|
--no-cache .
|
||||||
|
else
|
||||||
|
DOCKER_BUILDKIT=1 \
|
||||||
|
docker buildx build \
|
||||||
|
--platform linux/amd64,linux/arm64/v8 \
|
||||||
|
-t $REGISTRY:$VERSION \
|
||||||
|
--no-cache \
|
||||||
|
--push .
|
||||||
|
fi
|
||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
working_dir: '/app'
|
working_dir: '/app'
|
||||||
tty: true
|
tty: true
|
||||||
volumes:
|
volumes:
|
||||||
- ${SRC_PATH}:/app
|
- /tmp/.X11-unix:/tmp/.X11-unix
|
||||||
|
- ${XAUTHORITY:-~/.Xauthority}:/tmp/.docker.xauth:rw
|
||||||
environment:
|
environment:
|
||||||
- DISPLAY=${DISPLAY_IP}:0.0
|
- DISPLAY=${DISPLAY:-:0}
|
||||||
|
- XAUTHORITY=/tmp/.docker.xauth
|
||||||
|
|||||||
59
run-container.sh
Executable file
59
run-container.sh
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
# Check for .env file and create if it doesn't exist
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "Creating .env file..."
|
||||||
|
touch .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Allow local X server connections
|
||||||
|
xhost +local:
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
export DISPLAY=":0" # Default to local display
|
||||||
|
# Try to get IP address if hostname -I is available
|
||||||
|
if command -v hostname >/dev/null 2>&1; then
|
||||||
|
if hostname --help 2>&1 | grep -q -- "-I"; then
|
||||||
|
IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
|
||||||
|
if [ -n "$IP" ]; then
|
||||||
|
export DISPLAY="$IP:0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
export SRC_PATH=$(pwd)
|
||||||
|
export IMAGE="thechart:latest"
|
||||||
|
export XAUTHORITY=$HOME/.Xauthority
|
||||||
|
|
||||||
|
echo "Building and running the container..."
|
||||||
|
echo "Using DISPLAY=$DISPLAY"
|
||||||
|
echo "Using SRC_PATH=$SRC_PATH"
|
||||||
|
echo "Using XAUTHORITY=$XAUTHORITY"
|
||||||
|
|
||||||
|
# Check if debug mode is requested
|
||||||
|
if [ "$1" = "debug" ]; then
|
||||||
|
echo "Running in debug mode - will open shell instead of running app"
|
||||||
|
docker-compose build
|
||||||
|
docker run --rm -it \
|
||||||
|
-v /tmp/.X11-unix:/tmp/.X11-unix \
|
||||||
|
-v ${XAUTHORITY:-$HOME/.Xauthority}:/tmp/.docker.xauth:rw \
|
||||||
|
-e DISPLAY=${DISPLAY:-:0} \
|
||||||
|
-e XAUTHORITY=/tmp/.docker.xauth \
|
||||||
|
${IMAGE} /bin/sh
|
||||||
|
else
|
||||||
|
# First run with debug to see the container's internal state
|
||||||
|
echo "First entering container shell for debugging..."
|
||||||
|
docker run --rm -it \
|
||||||
|
-v /tmp/.X11-unix:/tmp/.X11-unix \
|
||||||
|
-v ${XAUTHORITY:-$HOME/.Xauthority}:/tmp/.docker.xauth:rw \
|
||||||
|
-e DISPLAY=${DISPLAY:-:0} \
|
||||||
|
-e XAUTHORITY=/tmp/.docker.xauth \
|
||||||
|
${IMAGE} /bin/sh
|
||||||
|
|
||||||
|
# Then continue with normal operation if needed
|
||||||
|
echo "Now running the container with docker-compose..."
|
||||||
|
docker-compose up --build
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disallow local X server connections when done
|
||||||
|
xhost -local:
|
||||||
120
src/data_manager.py
Normal file
120
src/data_manager.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import os
|
||||||
|
import csv
|
||||||
|
import logging
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
class DataManager:
|
||||||
|
"""Handle all data operations for the application."""
|
||||||
|
|
||||||
|
def __init__(self, filename: str, logger: logging.Logger) -> None:
|
||||||
|
self.filename: str = filename
|
||||||
|
self.logger: logging.Logger = logger
|
||||||
|
self.initialize_csv()
|
||||||
|
|
||||||
|
def initialize_csv(self) -> None:
|
||||||
|
"""Create CSV file with headers if it doesn't exist."""
|
||||||
|
if not os.path.exists(self.filename):
|
||||||
|
with open(self.filename, mode="w", newline="") as file:
|
||||||
|
writer = csv.writer(file)
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"date",
|
||||||
|
"depression",
|
||||||
|
"anxiety",
|
||||||
|
"sleep",
|
||||||
|
"appetite",
|
||||||
|
"bupropion",
|
||||||
|
"hydroxyzine",
|
||||||
|
"gabapentin",
|
||||||
|
"propranolol",
|
||||||
|
"note",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_data(self) -> pd.DataFrame:
|
||||||
|
"""Load data from CSV file."""
|
||||||
|
if (
|
||||||
|
not os.path.exists(self.filename)
|
||||||
|
or os.path.getsize(self.filename) == 0
|
||||||
|
):
|
||||||
|
self.logger.warning(
|
||||||
|
"CSV file is empty or doesn't exist. No data to load."
|
||||||
|
)
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
try:
|
||||||
|
df: pd.DataFrame = pd.read_csv(
|
||||||
|
self.filename,
|
||||||
|
dtype={
|
||||||
|
"depression": int,
|
||||||
|
"anxiety": int,
|
||||||
|
"sleep": int,
|
||||||
|
"appetite": int,
|
||||||
|
"bupropion": int,
|
||||||
|
"hydroxyzine": int,
|
||||||
|
"gabapentin": int,
|
||||||
|
"propranolol": int,
|
||||||
|
"note": str,
|
||||||
|
"date": str,
|
||||||
|
},
|
||||||
|
).fillna("")
|
||||||
|
return df.sort_values(by="date").reset_index(drop=True)
|
||||||
|
except pd.errors.EmptyDataError:
|
||||||
|
self.logger.warning("CSV file is empty. No data to load.")
|
||||||
|
return pd.DataFrame()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error loading data: {str(e)}")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
def add_entry(self, entry_data: List[Union[str, int]]) -> bool:
|
||||||
|
"""Add a new entry to the CSV file."""
|
||||||
|
try:
|
||||||
|
with open(self.filename, mode="a", newline="") as file:
|
||||||
|
writer = csv.writer(file)
|
||||||
|
writer.writerow(entry_data)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error adding entry: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_entry(self, date: str, values: List[Union[str, int]]) -> bool:
|
||||||
|
"""Update an existing entry identified by date."""
|
||||||
|
try:
|
||||||
|
df: pd.DataFrame = self.load_data()
|
||||||
|
# Find the row to update using date as a unique identifier
|
||||||
|
df.loc[
|
||||||
|
df["date"] == date,
|
||||||
|
[
|
||||||
|
"date",
|
||||||
|
"depression",
|
||||||
|
"anxiety",
|
||||||
|
"sleep",
|
||||||
|
"appetite",
|
||||||
|
"bupropion",
|
||||||
|
"hydroxyzine",
|
||||||
|
"gabapentin",
|
||||||
|
"propranolol",
|
||||||
|
"note",
|
||||||
|
],
|
||||||
|
] = values
|
||||||
|
df.to_csv(self.filename, index=False)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error updating entry: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete_entry(self, date: str) -> bool:
|
||||||
|
"""Delete an entry identified by date."""
|
||||||
|
try:
|
||||||
|
df: pd.DataFrame = self.load_data()
|
||||||
|
# Remove the row with the matching date
|
||||||
|
df = df[df["date"] != date]
|
||||||
|
# Write the updated dataframe back to the CSV
|
||||||
|
df.to_csv(self.filename, index=False)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error deleting entry: {str(e)}")
|
||||||
|
return False
|
||||||
81
src/graph_manager.py
Normal file
81
src/graph_manager.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.figure
|
||||||
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
|
from matplotlib.axes import Axes
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
|
|
||||||
|
class GraphManager:
|
||||||
|
"""Handle all graph-related operations for the application."""
|
||||||
|
|
||||||
|
def __init__(self, parent_frame: ttk.LabelFrame) -> None:
|
||||||
|
self.parent_frame: ttk.LabelFrame = parent_frame
|
||||||
|
|
||||||
|
# Configure graph frame to expand
|
||||||
|
self.parent_frame.grid_rowconfigure(0, weight=1)
|
||||||
|
self.parent_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# Initialize matplotlib figure and canvas
|
||||||
|
self.fig: matplotlib.figure.Figure
|
||||||
|
self.ax: Axes
|
||||||
|
self.fig, self.ax = plt.subplots()
|
||||||
|
self.canvas: FigureCanvasTkAgg = FigureCanvasTkAgg(
|
||||||
|
figure=self.fig, master=self.parent_frame
|
||||||
|
)
|
||||||
|
self.canvas.get_tk_widget().pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
def update_graph(self, df: pd.DataFrame) -> None:
|
||||||
|
"""Update the graph with new data."""
|
||||||
|
self.ax.clear()
|
||||||
|
if not df.empty:
|
||||||
|
# Convert dates and sort
|
||||||
|
df = df.copy() # Create a copy to avoid modifying the original
|
||||||
|
df["date"] = pd.to_datetime(df["date"])
|
||||||
|
df = df.sort_values(by="date")
|
||||||
|
df.set_index(keys="date", inplace=True)
|
||||||
|
|
||||||
|
# Plot data series
|
||||||
|
self._plot_series(
|
||||||
|
df, "depression", "Depression (0:good, 10:bad)", "o", "-"
|
||||||
|
)
|
||||||
|
self._plot_series(
|
||||||
|
df, "anxiety", "Anxiety (0:good, 10:bad)", "o", "-"
|
||||||
|
)
|
||||||
|
self._plot_series(
|
||||||
|
df, "sleep", "Sleep (0:bad, 10:good)", "o", "dashed"
|
||||||
|
)
|
||||||
|
self._plot_series(
|
||||||
|
df, "appetite", "Appetite (0:bad, 10:good)", "o", "dashed"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure graph appearance
|
||||||
|
self.ax.legend()
|
||||||
|
self.ax.set_title("Medication Effects Over Time")
|
||||||
|
self.ax.set_xlabel("Date")
|
||||||
|
self.ax.set_ylabel("Rating (0-10)")
|
||||||
|
self.fig.autofmt_xdate()
|
||||||
|
|
||||||
|
# Redraw the canvas
|
||||||
|
self.canvas.draw()
|
||||||
|
|
||||||
|
def _plot_series(
|
||||||
|
self,
|
||||||
|
df: pd.DataFrame,
|
||||||
|
column: str,
|
||||||
|
label: str,
|
||||||
|
marker: str,
|
||||||
|
linestyle: str,
|
||||||
|
) -> None:
|
||||||
|
"""Helper method to plot a data series."""
|
||||||
|
self.ax.plot(
|
||||||
|
df.index,
|
||||||
|
df[column],
|
||||||
|
marker=marker,
|
||||||
|
linestyle=linestyle,
|
||||||
|
label=label,
|
||||||
|
)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Clean up resources."""
|
||||||
|
plt.close(self.fig)
|
||||||
11
src/init.py
11
src/init.py
@@ -4,8 +4,6 @@ from constants import LOG_PATH, LOG_CLEAR, LOG_LEVEL
|
|||||||
|
|
||||||
if not os.path.exists(LOG_PATH):
|
if not os.path.exists(LOG_PATH):
|
||||||
os.mkdir(LOG_PATH)
|
os.mkdir(LOG_PATH)
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
log_files = (
|
log_files = (
|
||||||
f"{LOG_PATH}/app.log",
|
f"{LOG_PATH}/app.log",
|
||||||
@@ -13,10 +11,7 @@ log_files = (
|
|||||||
f"{LOG_PATH}/app.error.log",
|
f"{LOG_PATH}/app.error.log",
|
||||||
)
|
)
|
||||||
|
|
||||||
if LOG_LEVEL == "DEBUG":
|
testing_mode = LOG_LEVEL == "DEBUG"
|
||||||
testing_mode = True
|
|
||||||
else:
|
|
||||||
testing_mode = False
|
|
||||||
|
|
||||||
logger = init_logger(__name__, testing_mode=testing_mode)
|
logger = init_logger(__name__, testing_mode=testing_mode)
|
||||||
|
|
||||||
@@ -26,10 +21,6 @@ if LOG_CLEAR == "True":
|
|||||||
if os.path.exists(log_file):
|
if os.path.exists(log_file):
|
||||||
with open(log_file, "r+") as t:
|
with open(log_file, "r+") as t:
|
||||||
t.truncate(0)
|
t.truncate(0)
|
||||||
else:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
raise
|
raise
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|||||||
709
src/main.py
709
src/main.py
@@ -1,399 +1,134 @@
|
|||||||
import csv
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import messagebox, ttk
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import pandas as pd
|
|
||||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from PIL import Image, ImageTk
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
from typing import Dict, List, Tuple, Any, Callable, Union
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from init import logger
|
from init import logger
|
||||||
from constants import LOG_LEVEL
|
from constants import LOG_LEVEL
|
||||||
|
from data_manager import DataManager
|
||||||
|
from graph_manager import GraphManager
|
||||||
|
from ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
class MedTrackerApp:
|
class MedTrackerApp:
|
||||||
def __init__(self, root):
|
def __init__(self, root: tk.Tk) -> None:
|
||||||
|
self.root: tk.Tk = root
|
||||||
self.root = root
|
|
||||||
self.root.resizable(True, True)
|
self.root.resizable(True, True)
|
||||||
self.root.title("Thechart - medication tracker")
|
self.root.title("Thechart - medication tracker")
|
||||||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||||
# self.root.iconbitmap("app_icon.ico")
|
|
||||||
# screen_width = self.root.winfo_screenwidth()
|
|
||||||
# screen_height = self.root.winfo_screenheight()
|
|
||||||
# self.root.geometry(f"{screen_width}x{screen_height}")
|
|
||||||
# self.root.configure(background='gold')
|
|
||||||
# self.root.lift()
|
|
||||||
# self.root.attributes("-topmost", True)
|
|
||||||
# self.root.geometry("800x600")
|
|
||||||
|
|
||||||
|
# Set up data file
|
||||||
|
self.filename: str = "thechart_data.csv"
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
script_name = sys.argv[0]
|
first_argument: str = sys.argv[1]
|
||||||
first_argument = sys.argv[1]
|
|
||||||
|
|
||||||
if LOG_LEVEL == "DEBUG":
|
if LOG_LEVEL == "DEBUG":
|
||||||
logger.debug(f"Script name: {script_name}")
|
logger.debug(f"Script name: {sys.argv[0]}")
|
||||||
logger.debug(f"First argument: {first_argument}")
|
logger.debug(f"First argument: {first_argument}")
|
||||||
|
|
||||||
if os.path.exists(first_argument):
|
if os.path.exists(first_argument):
|
||||||
self.filename = first_argument
|
self.filename = first_argument
|
||||||
logger.info(f"Using data file: {first_argument}")
|
logger.info(f"Using data file: {first_argument}")
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Data file {first_argument} does not exist."
|
f"Data file {first_argument} does not exist. Using default file: {self.filename}"
|
||||||
f" Using default file: {self.filename}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.make_icon(
|
# Initialize managers
|
||||||
img="/home/will/Code/thechart/chart-671.png",
|
self.ui_manager: UIManager = UIManager(root, logger)
|
||||||
logger=logger,
|
self.data_manager: DataManager = DataManager(self.filename, logger)
|
||||||
|
|
||||||
|
# Set up application icon
|
||||||
|
icon_path: str = "chart-671.png"
|
||||||
|
if not os.path.exists(icon_path) and os.path.exists("./chart-671.png"):
|
||||||
|
icon_path = "./chart-671.png"
|
||||||
|
self.ui_manager.setup_icon(img_path=icon_path)
|
||||||
|
|
||||||
|
# Set up the main application UI
|
||||||
|
self._setup_main_ui()
|
||||||
|
|
||||||
|
def _setup_main_ui(self) -> None:
|
||||||
|
"""Set up the main UI components."""
|
||||||
|
import tkinter.ttk as ttk
|
||||||
|
|
||||||
|
# --- Main Frame ---
|
||||||
|
main_frame: ttk.Frame = ttk.Frame(self.root, padding="10")
|
||||||
|
main_frame.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|
||||||
|
# Configure root window grid
|
||||||
|
self.root.grid_rowconfigure(0, weight=1)
|
||||||
|
self.root.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# Configure main frame grid for scaling
|
||||||
|
for i in range(2):
|
||||||
|
main_frame.grid_rowconfigure(i, weight=1 if i == 1 else 0)
|
||||||
|
main_frame.grid_columnconfigure(i, weight=3 if i == 1 else 1)
|
||||||
|
logger.debug("Main frame and root grid configured for scaling.")
|
||||||
|
|
||||||
|
# --- Create Graph Frame ---
|
||||||
|
graph_frame: ttk.Frame = self.ui_manager.create_graph_frame(main_frame)
|
||||||
|
self.graph_manager: GraphManager = GraphManager(graph_frame)
|
||||||
|
|
||||||
|
# --- Create Input Frame ---
|
||||||
|
input_ui: Dict[str, Any] = self.ui_manager.create_input_frame(
|
||||||
|
main_frame
|
||||||
|
)
|
||||||
|
self.input_frame: ttk.Frame = input_ui["frame"]
|
||||||
|
self.symptom_vars: Dict[str, tk.IntVar] = input_ui["symptom_vars"]
|
||||||
|
self.medicine_vars: Dict[str, List[Union[tk.IntVar, ttk.Spinbox]]] = (
|
||||||
|
input_ui["medicine_vars"]
|
||||||
|
)
|
||||||
|
self.note_var: tk.StringVar = input_ui["note_var"]
|
||||||
|
self.date_var: tk.StringVar = input_ui["date_var"]
|
||||||
|
|
||||||
|
# Add buttons to input frame
|
||||||
|
self.ui_manager.add_buttons(
|
||||||
|
self.input_frame,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"text": "Add Entry",
|
||||||
|
"command": self.add_entry,
|
||||||
|
"fill": "both",
|
||||||
|
"expand": True,
|
||||||
|
},
|
||||||
|
{"text": "Quit", "command": self.on_closing},
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.filename = "thechart_data.csv"
|
# --- Create Table Frame ---
|
||||||
self.initialize_csv()
|
table_ui: Dict[str, Any] = self.ui_manager.create_table_frame(
|
||||||
|
main_frame
|
||||||
main_frame = ttk.Frame(self.root, padding="10")
|
|
||||||
main_frame.grid(row=0, column=0)
|
|
||||||
|
|
||||||
# --- Input Frame ---
|
|
||||||
input_frame = ttk.LabelFrame(main_frame, text="New Entry")
|
|
||||||
input_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
|
|
||||||
|
|
||||||
ttk.Label(input_frame, text="Depression (0-10):").grid(
|
|
||||||
row=0, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
)
|
||||||
self.depression_var = tk.IntVar()
|
self.tree: ttk.Treeview = table_ui["tree"]
|
||||||
ttk.Scale(
|
|
||||||
input_frame,
|
|
||||||
from_=0,
|
|
||||||
to=10,
|
|
||||||
orient=tk.HORIZONTAL,
|
|
||||||
variable=self.depression_var,
|
|
||||||
).grid(row=0, column=1, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Label(input_frame, text="Anxiety (0-10):").grid(
|
|
||||||
row=1, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
self.anxiety_var = tk.IntVar()
|
|
||||||
ttk.Scale(
|
|
||||||
input_frame,
|
|
||||||
from_=0,
|
|
||||||
to=10,
|
|
||||||
orient=tk.HORIZONTAL,
|
|
||||||
variable=self.anxiety_var,
|
|
||||||
).grid(row=1, column=1, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Label(input_frame, text="Sleep Quality (0-10):").grid(
|
|
||||||
row=2, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
self.sleep_var = tk.IntVar()
|
|
||||||
ttk.Scale(
|
|
||||||
input_frame,
|
|
||||||
from_=0,
|
|
||||||
to=10,
|
|
||||||
orient=tk.HORIZONTAL,
|
|
||||||
variable=self.sleep_var,
|
|
||||||
).grid(row=2, column=1, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Label(input_frame, text="Appetite (0-10):").grid(
|
|
||||||
row=3, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
self.appetite_var = tk.IntVar()
|
|
||||||
ttk.Scale(
|
|
||||||
input_frame,
|
|
||||||
from_=0,
|
|
||||||
to=10,
|
|
||||||
orient=tk.HORIZONTAL,
|
|
||||||
variable=self.appetite_var,
|
|
||||||
).grid(row=3, column=1, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Label(input_frame, text="Treatment:").grid(
|
|
||||||
row=4, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
|
|
||||||
medicine_frame = ttk.LabelFrame(input_frame, text="Medicine")
|
|
||||||
medicine_frame.grid(row=4, column=1, padx=0, pady=10, sticky="nsew")
|
|
||||||
|
|
||||||
self.bupropion_var = tk.IntVar(value=0)
|
|
||||||
self.hydroxyzine_var = tk.IntVar(value=0)
|
|
||||||
self.gabapentin_var = tk.IntVar(value=0)
|
|
||||||
self.propranolol_var = tk.IntVar(value=0)
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
|
||||||
medicine_frame,
|
|
||||||
text="Bupropion 150mg",
|
|
||||||
variable=self.bupropion_var,
|
|
||||||
name="bupropion_check",
|
|
||||||
command=lambda: self.toggle_checkbox(obj_name="bupropion_check"),
|
|
||||||
).grid(row=0, column=0, sticky="w", padx=5, pady=2)
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
|
||||||
medicine_frame,
|
|
||||||
text="Hydroxyzine 25mg",
|
|
||||||
variable=self.hydroxyzine_var,
|
|
||||||
name="hydroxyzine_check",
|
|
||||||
command=lambda: self.toggle_checkbox(obj_name="hydroxyzine_check"),
|
|
||||||
).grid(row=1, column=0, sticky="w", padx=5, pady=2)
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
|
||||||
medicine_frame,
|
|
||||||
text="Gabapentin 100mg",
|
|
||||||
variable=self.gabapentin_var,
|
|
||||||
name="gabapentin_check",
|
|
||||||
command=lambda: self.toggle_checkbox(obj_name="gabapentin_check"),
|
|
||||||
).grid(row=2, column=0, sticky="w", padx=5, pady=2)
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
|
||||||
medicine_frame,
|
|
||||||
text="Propranolol 10mg",
|
|
||||||
name="propranolol_check",
|
|
||||||
variable=self.propranolol_var,
|
|
||||||
command=lambda: self.toggle_checkbox(obj_name="propranolol_check"),
|
|
||||||
).grid(row=3, column=0, sticky="w", padx=5, pady=2)
|
|
||||||
|
|
||||||
ttk.Label(input_frame, text="Note:").grid(
|
|
||||||
row=5, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
self.note_var = tk.StringVar()
|
|
||||||
ttk.Entry(input_frame, textvariable=self.note_var).grid(
|
|
||||||
row=5, column=1, sticky="ew", padx=5, pady=2
|
|
||||||
)
|
|
||||||
|
|
||||||
ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid(
|
|
||||||
row=6, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
self.date_var = tk.StringVar()
|
|
||||||
ttk.Entry(
|
|
||||||
input_frame, textvariable=self.date_var, justify="center"
|
|
||||||
).grid(row=6, column=1, sticky="ew", padx=5, pady=2)
|
|
||||||
|
|
||||||
button_frame = ttk.Frame(input_frame)
|
|
||||||
button_frame.grid(row=7, column=0, columnspan=2, pady=10)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
button_frame, text="Add Entry", command=self.add_entry
|
|
||||||
).pack(side="left", padx=5, fill="both", expand=True)
|
|
||||||
ttk.Button(button_frame, text="Quit", command=self.on_closing).pack(
|
|
||||||
side="left", padx=5
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Table Frame ---
|
|
||||||
table_frame = ttk.LabelFrame(
|
|
||||||
main_frame, text="Log (Double-click to edit)"
|
|
||||||
)
|
|
||||||
table_frame.grid(row=1, column=1, padx=10, pady=10, sticky="nsew")
|
|
||||||
|
|
||||||
self.tree = ttk.Treeview(
|
|
||||||
table_frame,
|
|
||||||
columns=(
|
|
||||||
"Date",
|
|
||||||
"Depression",
|
|
||||||
"Anxiety",
|
|
||||||
"Sleep",
|
|
||||||
"Appetite",
|
|
||||||
"Bupropion",
|
|
||||||
"Hydroxyzine",
|
|
||||||
"Gabapentin",
|
|
||||||
"Propranolol",
|
|
||||||
"Note",
|
|
||||||
),
|
|
||||||
show="headings",
|
|
||||||
)
|
|
||||||
self.tree.heading("Date", text="Date")
|
|
||||||
self.tree.heading("Depression", text="Depression")
|
|
||||||
self.tree.heading("Anxiety", text="Anxiety")
|
|
||||||
self.tree.heading("Sleep", text="Sleep")
|
|
||||||
self.tree.heading("Appetite", text="Appetite")
|
|
||||||
self.tree.heading("Bupropion", text="Bupropion 150mg")
|
|
||||||
self.tree.heading("Hydroxyzine", text="Hydroxyzine 25mg")
|
|
||||||
self.tree.heading("Gabapentin", text="Gabapentin 100mg")
|
|
||||||
self.tree.heading("Propranolol", text="Propranolol 10mg")
|
|
||||||
self.tree.heading("Note", text="Note")
|
|
||||||
|
|
||||||
self.tree.column("Date", width=80, anchor="center")
|
|
||||||
self.tree.column("Depression", width=80, anchor="center")
|
|
||||||
self.tree.column("Anxiety", width=80, anchor="center")
|
|
||||||
self.tree.column("Sleep", width=80, anchor="center")
|
|
||||||
self.tree.column("Appetite", width=80, anchor="center")
|
|
||||||
self.tree.column("Bupropion", width=120, anchor="center")
|
|
||||||
self.tree.column("Hydroxyzine", width=120, anchor="center")
|
|
||||||
self.tree.column("Gabapentin", width=120, anchor="center")
|
|
||||||
self.tree.column("Propranolol", width=120, anchor="center")
|
|
||||||
self.tree.column("Note", width=300, anchor="w")
|
|
||||||
|
|
||||||
# --- Bind double-click event ---
|
|
||||||
self.tree.bind("<Double-1>", self.on_double_click)
|
self.tree.bind("<Double-1>", self.on_double_click)
|
||||||
self.tree.pack(side="left", fill="both", expand=True)
|
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(
|
|
||||||
table_frame, orient="vertical", command=self.tree.yview
|
|
||||||
)
|
|
||||||
self.tree.configure(yscrollcommand=scrollbar.set)
|
|
||||||
scrollbar.pack(side="right", fill="y")
|
|
||||||
|
|
||||||
# --- Graph Frame ---
|
|
||||||
graph_frame = ttk.LabelFrame(main_frame, text="Evolution")
|
|
||||||
graph_frame.grid(
|
|
||||||
row=0, column=0, columnspan=2, padx=10, pady=10, sticky="nsew"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.fig, self.ax = plt.subplots()
|
|
||||||
self.canvas = FigureCanvasTkAgg(figure=self.fig, master=graph_frame)
|
|
||||||
self.canvas.get_tk_widget().pack(fill="both", expand=True)
|
|
||||||
|
|
||||||
|
# Load data
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
def toggle_checkbox(obj_name: str) -> None:
|
def on_double_click(self, event: tk.Event) -> None:
|
||||||
if ttk.Checkbutton.nametowidget(name=obj_name).get():
|
|
||||||
ttk.Checkbutton.nametowidget(name=obj_name).set(False)
|
|
||||||
else:
|
|
||||||
ttk.Checkbutton.nametowidget(name=obj_name).set(True)
|
|
||||||
|
|
||||||
def on_double_click(self, event: any) -> None:
|
|
||||||
"""Handle double-click event to edit an entry."""
|
"""Handle double-click event to edit an entry."""
|
||||||
|
logger.debug("Double-click event triggered on treeview.")
|
||||||
if len(self.tree.get_children()) > 0:
|
if len(self.tree.get_children()) > 0:
|
||||||
item_id = self.tree.selection()[0]
|
item_id = self.tree.selection()[0]
|
||||||
item_values = self.tree.item(item_id, "values")
|
item_values = self.tree.item(item_id, "values")
|
||||||
self.create_edit_window(item_id, item_values)
|
logger.debug(f"Editing item_id={item_id}, values={item_values}")
|
||||||
|
self._create_edit_window(item_id, item_values)
|
||||||
|
|
||||||
def create_edit_window(self, item_id: str, values: tuple) -> None:
|
def _create_edit_window(
|
||||||
|
self, item_id: str, values: Tuple[str, ...]
|
||||||
|
) -> None:
|
||||||
"""Create a new Toplevel window for editing an entry."""
|
"""Create a new Toplevel window for editing an entry."""
|
||||||
edit_win = tk.Toplevel(master=self.root)
|
# Define callbacks for edit window buttons
|
||||||
edit_win.title("Edit Entry")
|
callbacks: Dict[str, Callable] = {
|
||||||
|
"save": self._save_edit,
|
||||||
|
"delete": lambda win: self._delete_entry(win, item_id),
|
||||||
|
}
|
||||||
|
|
||||||
# Unpack values
|
# Create edit window using UI manager
|
||||||
date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values
|
_: tk.Toplevel = self.ui_manager.create_edit_window(values, callbacks)
|
||||||
|
|
||||||
# Create variables for the widgets
|
def _save_edit(
|
||||||
date_var = tk.StringVar(value=str(date))
|
|
||||||
dep_var = tk.IntVar(value=int(dep))
|
|
||||||
anx_var = tk.IntVar(value=int(anx))
|
|
||||||
slp_var = tk.IntVar(value=int(slp))
|
|
||||||
app_var = tk.IntVar(value=int(app))
|
|
||||||
bup_var = tk.IntVar(value=int(bup))
|
|
||||||
hydro_var = tk.IntVar(value=int(hydro))
|
|
||||||
gaba_var = tk.IntVar(value=int(gaba))
|
|
||||||
prop_var = tk.IntVar(value=int(prop))
|
|
||||||
note_var = tk.StringVar(value=str(note))
|
|
||||||
|
|
||||||
# Create form widgets
|
|
||||||
ttk.Label(edit_win, text="Depression:").grid(
|
|
||||||
row=1, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
ttk.Scale(
|
|
||||||
edit_win, from_=0, to=10, variable=dep_var, orient=tk.HORIZONTAL
|
|
||||||
).grid(row=1, column=1, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Label(edit_win, text="Anxiety:").grid(
|
|
||||||
row=2, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
ttk.Scale(
|
|
||||||
edit_win, from_=0, to=10, variable=anx_var, orient=tk.HORIZONTAL
|
|
||||||
).grid(row=2, column=1, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Label(edit_win, text="Sleep:").grid(
|
|
||||||
row=3, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
ttk.Scale(
|
|
||||||
edit_win, from_=0, to=10, variable=slp_var, orient=tk.HORIZONTAL
|
|
||||||
).grid(row=3, column=1, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Label(edit_win, text="Appetite:").grid(
|
|
||||||
row=4, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
ttk.Scale(
|
|
||||||
edit_win, from_=0, to=10, variable=app_var, orient=tk.HORIZONTAL
|
|
||||||
).grid(row=4, column=1, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Label(edit_win, text="Treatment:").grid(
|
|
||||||
row=5, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
|
|
||||||
medicine_frame = ttk.LabelFrame(edit_win, text="Medicine")
|
|
||||||
medicine_frame.grid(row=5, column=1, padx=0, pady=10, sticky="nsew")
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
|
||||||
medicine_frame,
|
|
||||||
text="Bupropion 150mg",
|
|
||||||
name="bupropion_check",
|
|
||||||
variable=bup_var,
|
|
||||||
command=lambda: self.toggle_checkbox(obj_name="bupropion_check"),
|
|
||||||
).grid(row=0, column=0, sticky="w", padx=5, pady=2)
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
|
||||||
medicine_frame,
|
|
||||||
text="Hydroxyzine 25mg",
|
|
||||||
name="hydroxyzine_check",
|
|
||||||
variable=hydro_var,
|
|
||||||
command=lambda: self.toggle_checkbox(obj_name="hydroxyzine_check"),
|
|
||||||
).grid(row=1, column=0, sticky="w", padx=5, pady=2)
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
|
||||||
medicine_frame,
|
|
||||||
text="Gabapentin 100mg",
|
|
||||||
name="gabapentin_check",
|
|
||||||
variable=gaba_var,
|
|
||||||
command=lambda: self.toggle_checkbox(obj_name="gabapentin_check"),
|
|
||||||
).grid(row=2, column=0, sticky="w", padx=5, pady=2)
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
|
||||||
medicine_frame,
|
|
||||||
text="Propranolol 10mg",
|
|
||||||
name="propranolol_check",
|
|
||||||
variable=prop_var,
|
|
||||||
command=lambda: self.toggle_checkbox(obj_name="propranolol_check"),
|
|
||||||
).grid(row=3, column=0, sticky="w", padx=5, pady=2)
|
|
||||||
|
|
||||||
ttk.Label(edit_win, text="Note:").grid(
|
|
||||||
row=6, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
ttk.Entry(edit_win, textvariable=note_var).grid(
|
|
||||||
row=6, column=1, sticky="ew"
|
|
||||||
)
|
|
||||||
|
|
||||||
ttk.Label(edit_win, text="Date:").grid(
|
|
||||||
row=7, column=0, sticky="w", padx=5, pady=2
|
|
||||||
)
|
|
||||||
ttk.Entry(edit_win, textvariable=date_var).grid(
|
|
||||||
row=7, column=1, sticky="ew"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save and Cancel buttons
|
|
||||||
save_btn = ttk.Button(
|
|
||||||
edit_win,
|
|
||||||
text="Save",
|
|
||||||
command=lambda: self.save_edit(
|
|
||||||
edit_win,
|
|
||||||
date_var.get(),
|
|
||||||
dep_var.get(),
|
|
||||||
anx_var.get(),
|
|
||||||
slp_var.get(),
|
|
||||||
app_var.get(),
|
|
||||||
bup_var.get(),
|
|
||||||
hydro_var.get(),
|
|
||||||
gaba_var.get(),
|
|
||||||
prop_var.get(),
|
|
||||||
note_var.get(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
save_btn.grid(row=8, column=0, padx=5, pady=10)
|
|
||||||
|
|
||||||
cancel_btn = ttk.Button(
|
|
||||||
edit_win, text="Cancel", command=edit_win.destroy
|
|
||||||
)
|
|
||||||
cancel_btn.grid(row=8, column=1, padx=5, pady=10)
|
|
||||||
delete_btn = ttk.Button(
|
|
||||||
edit_win,
|
|
||||||
text="Delete",
|
|
||||||
command=lambda: self.delete_entry(edit_win, item_id),
|
|
||||||
)
|
|
||||||
delete_btn.grid(row=8, column=2, padx=5, pady=10)
|
|
||||||
|
|
||||||
def save_edit(
|
|
||||||
self,
|
self,
|
||||||
edit_win: tk.Toplevel,
|
edit_win: tk.Toplevel,
|
||||||
date: str,
|
date: str,
|
||||||
@@ -407,207 +142,121 @@ class MedTrackerApp:
|
|||||||
prop: int,
|
prop: int,
|
||||||
note: str,
|
note: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Save the edited data to the CSV file."""
|
||||||
Save the edited data to the CSV file.
|
values: List[Union[str, int]] = [
|
||||||
"""
|
date,
|
||||||
df = pd.read_csv(self.filename)
|
dep,
|
||||||
# Find the row to update using the date as a unique identifier
|
anx,
|
||||||
df.loc[
|
slp,
|
||||||
df["date"] == date,
|
app,
|
||||||
[
|
bup,
|
||||||
"date",
|
hydro,
|
||||||
"depression",
|
gaba,
|
||||||
"anxiety",
|
prop,
|
||||||
"sleep",
|
note,
|
||||||
"appetite",
|
]
|
||||||
"bupropion",
|
|
||||||
"hydroxyzine",
|
|
||||||
"gabapentin",
|
|
||||||
"propranolol",
|
|
||||||
"note",
|
|
||||||
],
|
|
||||||
] = [date, dep, anx, slp, app, bup, hydro, gaba, prop, note]
|
|
||||||
# Write the updated dataframe back to the CSV
|
|
||||||
df.to_csv(self.filename, index=False)
|
|
||||||
|
|
||||||
edit_win.destroy()
|
if self.data_manager.update_entry(date, values):
|
||||||
messagebox.showinfo(
|
edit_win.destroy()
|
||||||
"Success", "Entry updated successfully!", parent=self.root
|
messagebox.showinfo(
|
||||||
)
|
"Success", "Entry updated successfully!", parent=self.root
|
||||||
self.clear_entries()
|
)
|
||||||
self.load_data()
|
self._clear_entries()
|
||||||
|
self.load_data()
|
||||||
|
else:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Error", "Failed to save changes", parent=edit_win
|
||||||
|
)
|
||||||
|
|
||||||
def on_closing(self) -> None:
|
def on_closing(self) -> None:
|
||||||
if messagebox.askokcancel(
|
if messagebox.askokcancel(
|
||||||
"Quit", "Do you want to quit the application?", parent=self.root
|
"Quit", "Do you want to quit the application?", parent=self.root
|
||||||
):
|
):
|
||||||
plt.close(self.fig)
|
self.graph_manager.close()
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
|
|
||||||
def initialize_csv(self) -> None:
|
|
||||||
if not os.path.exists(self.filename):
|
|
||||||
with open(self.filename, mode="w", newline="") as file:
|
|
||||||
writer = csv.writer(file)
|
|
||||||
writer.writerow(
|
|
||||||
[
|
|
||||||
"date",
|
|
||||||
"depression",
|
|
||||||
"anxiety",
|
|
||||||
"sleep",
|
|
||||||
"appetite",
|
|
||||||
"bupropion",
|
|
||||||
"hydroxyzine",
|
|
||||||
"gabapentin",
|
|
||||||
"propranolol",
|
|
||||||
"note",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_entry(self) -> None:
|
def add_entry(self) -> None:
|
||||||
with open(self.filename, mode="a", newline="") as file:
|
"""Add a new entry to the CSV file."""
|
||||||
writer = csv.writer(file)
|
entry: List[Union[str, int]] = [
|
||||||
writer.writerow(
|
self.date_var.get(),
|
||||||
[
|
self.symptom_vars["depression"].get(),
|
||||||
self.date_var.get(),
|
self.symptom_vars["anxiety"].get(),
|
||||||
self.depression_var.get(),
|
self.symptom_vars["sleep"].get(),
|
||||||
self.anxiety_var.get(),
|
self.symptom_vars["appetite"].get(),
|
||||||
self.sleep_var.get(),
|
self.medicine_vars["bupropion"][0].get(),
|
||||||
self.appetite_var.get(),
|
self.medicine_vars["hydroxyzine"][0].get(),
|
||||||
self.bupropion_var.get(),
|
self.medicine_vars["gabapentin"][0].get(),
|
||||||
self.hydroxyzine_var.get(),
|
self.medicine_vars["propranolol"][0].get(),
|
||||||
self.gabapentin_var.get(),
|
self.note_var.get(),
|
||||||
self.propranolol_var.get(),
|
]
|
||||||
self.note_var.get(),
|
logger.debug(f"Adding entry: {entry}")
|
||||||
]
|
|
||||||
|
if self.data_manager.add_entry(entry):
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Success", "Entry added successfully!", parent=self.root
|
||||||
|
)
|
||||||
|
self._clear_entries()
|
||||||
|
self.load_data()
|
||||||
|
else:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Error", "Failed to add entry", parent=self.root
|
||||||
)
|
)
|
||||||
|
|
||||||
messagebox.showinfo(
|
def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
|
||||||
"Success", "Entry added successfully!", parent=self.root
|
"""Delete the selected entry from the CSV file."""
|
||||||
)
|
logger.debug(f"Delete requested for item_id={item_id}")
|
||||||
self.clear_entries()
|
|
||||||
self.load_data()
|
|
||||||
|
|
||||||
def delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
|
|
||||||
"""
|
|
||||||
Delete the selected entry from the CSV file.
|
|
||||||
"""
|
|
||||||
if messagebox.askyesno(
|
if messagebox.askyesno(
|
||||||
"Delete Entry",
|
"Delete Entry",
|
||||||
"Are you sure you want to delete this entry?",
|
"Are you sure you want to delete this entry?",
|
||||||
parent=edit_win,
|
parent=edit_win,
|
||||||
):
|
):
|
||||||
df = pd.read_csv(self.filename)
|
|
||||||
# Get the date of the entry to delete
|
# Get the date of the entry to delete
|
||||||
date = self.tree.item(item_id, "values")[0]
|
date: str = self.tree.item(item_id, "values")[0]
|
||||||
# Remove the row with the matching date
|
logger.debug(f"Deleting entry with date={date}")
|
||||||
df = df[df["date"] != date]
|
|
||||||
# Write the updated dataframe back to the CSV
|
|
||||||
df.to_csv(self.filename, index=False)
|
|
||||||
|
|
||||||
edit_win.destroy()
|
if self.data_manager.delete_entry(date):
|
||||||
messagebox.showinfo(
|
edit_win.destroy()
|
||||||
"Success", "Entry deleted successfully!", parent=edit_win
|
messagebox.showinfo(
|
||||||
)
|
"Success", "Entry deleted successfully!", parent=edit_win
|
||||||
self.load_data()
|
)
|
||||||
|
self.load_data()
|
||||||
|
else:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Error", "Failed to delete entry", parent=edit_win
|
||||||
|
)
|
||||||
|
|
||||||
def clear_entries(self) -> None:
|
def _clear_entries(self) -> None:
|
||||||
|
"""Clear all input fields."""
|
||||||
|
logger.debug("Clearing input fields.")
|
||||||
self.date_var.set("")
|
self.date_var.set("")
|
||||||
self.depression_var.set(0)
|
for key in self.symptom_vars:
|
||||||
self.anxiety_var.set(0)
|
self.symptom_vars[key].set(0)
|
||||||
self.sleep_var.set(0)
|
for key in self.medicine_vars:
|
||||||
self.appetite_var.set(0)
|
self.medicine_vars[key][0].set(0)
|
||||||
self.bupropion_var.set(False)
|
|
||||||
self.hydroxyzine_var.set(False)
|
|
||||||
self.gabapentin_var.set(False)
|
|
||||||
self.propranolol_var.set(False)
|
|
||||||
self.note_var.set("")
|
self.note_var.set("")
|
||||||
|
|
||||||
def load_data(self) -> None:
|
def load_data(self) -> None:
|
||||||
|
"""Load data from the CSV file into the table and graph."""
|
||||||
|
logger.debug("Loading data from CSV.")
|
||||||
|
|
||||||
|
# Clear existing data in the treeview
|
||||||
for i in self.tree.get_children():
|
for i in self.tree.get_children():
|
||||||
self.tree.delete(i)
|
self.tree.delete(i)
|
||||||
|
|
||||||
if (
|
# Load data from the CSV file
|
||||||
os.path.exists(self.filename)
|
df: pd.DataFrame = self.data_manager.load_data()
|
||||||
and os.path.getsize(self.filename) > 0
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
df = pd.read_csv(
|
|
||||||
self.filename,
|
|
||||||
dtype={
|
|
||||||
"depression": int,
|
|
||||||
"anxiety": int,
|
|
||||||
"sleep": int,
|
|
||||||
"appetite": int,
|
|
||||||
"bupropion": int,
|
|
||||||
"hydroxyzine": int,
|
|
||||||
"gabapentin": int,
|
|
||||||
"propranolol": int,
|
|
||||||
"note": str,
|
|
||||||
"date": str,
|
|
||||||
},
|
|
||||||
).fillna("")
|
|
||||||
df = df.sort_values(by="date").reset_index(drop=True)
|
|
||||||
for index, row in df.iterrows():
|
|
||||||
self.tree.insert(parent="", index="end", values=list(row))
|
|
||||||
self.update_graph(df)
|
|
||||||
except pd.errors.EmptyDataError:
|
|
||||||
self.update_graph(pd.DataFrame())
|
|
||||||
|
|
||||||
def update_graph(self, df: pd.DataFrame) -> None:
|
# Update the treeview with the data
|
||||||
self.ax.clear()
|
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df["date"] = pd.to_datetime(df["date"])
|
for index, row in df.iterrows():
|
||||||
df = df.sort_values(by="date")
|
self.tree.insert(parent="", index="end", values=list(row))
|
||||||
df.set_index(keys="date", inplace=True)
|
logger.debug(f"Loaded {len(df)} entries into treeview.")
|
||||||
self.ax.plot(
|
|
||||||
df.index,
|
|
||||||
df["depression"],
|
|
||||||
marker="o",
|
|
||||||
linestyle="-",
|
|
||||||
label="Depression (0:good, 10:bad)",
|
|
||||||
)
|
|
||||||
self.ax.plot(
|
|
||||||
df.index,
|
|
||||||
df["anxiety"],
|
|
||||||
marker="o",
|
|
||||||
linestyle="-",
|
|
||||||
label="Anxiety (0:good, 10:bad)",
|
|
||||||
)
|
|
||||||
self.ax.plot(
|
|
||||||
df.index,
|
|
||||||
df["sleep"],
|
|
||||||
marker="o",
|
|
||||||
linestyle="dashed",
|
|
||||||
label="Sleep (0:bad, 10:good)",
|
|
||||||
)
|
|
||||||
self.ax.plot(
|
|
||||||
df.index,
|
|
||||||
df["appetite"],
|
|
||||||
marker="o",
|
|
||||||
linestyle="dashed",
|
|
||||||
label="Appetite (0:bad, 10:good)",
|
|
||||||
)
|
|
||||||
self.ax.legend()
|
|
||||||
self.ax.set_title("Medication Effects Over Time")
|
|
||||||
self.ax.set_xlabel("Date")
|
|
||||||
self.ax.set_ylabel("Rating (0-10)")
|
|
||||||
self.fig.autofmt_xdate()
|
|
||||||
self.canvas.draw()
|
|
||||||
|
|
||||||
def make_icon(self, img: str, logger: logging.Logger) -> None:
|
# Update the graph
|
||||||
try:
|
self.graph_manager.update_graph(df)
|
||||||
icon_image = Image.open(img)
|
|
||||||
icon_image = icon_image.resize(
|
|
||||||
size=(32, 32), resample=Image.Resampling.NEAREST
|
|
||||||
)
|
|
||||||
icon_photo = ImageTk.PhotoImage(image=icon_image)
|
|
||||||
self.root.iconphoto(True, icon_photo)
|
|
||||||
self.root.wm_iconphoto(True, icon_photo)
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.warning("Icon file not found.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
root = tk.Tk()
|
root: tk.Tk = tk.Tk()
|
||||||
app = MedTrackerApp(root)
|
app: MedTrackerApp = MedTrackerApp(root)
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|||||||
462
src/ui_manager.py
Normal file
462
src/ui_manager.py
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
from typing import Dict, List, Tuple, Any, Callable, Union
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
|
||||||
|
class UIManager:
|
||||||
|
"""Handle UI creation and management for the application."""
|
||||||
|
|
||||||
|
def __init__(self, root: tk.Tk, logger: logging.Logger) -> None:
|
||||||
|
self.root: tk.Tk = root
|
||||||
|
self.logger: logging.Logger = logger
|
||||||
|
|
||||||
|
def setup_icon(self, img_path: str) -> bool:
|
||||||
|
"""Set up the application icon."""
|
||||||
|
try:
|
||||||
|
self.logger.info(f"Trying to load icon from: {img_path}")
|
||||||
|
# Try to find the icon in various locations
|
||||||
|
if not os.path.exists(img_path):
|
||||||
|
# Check if we're in PyInstaller bundle
|
||||||
|
if hasattr(sys, "_MEIPASS"):
|
||||||
|
# PyInstaller creates a temp folder and stores path in _MEIPASS
|
||||||
|
base_path: str = sys._MEIPASS
|
||||||
|
potential_paths: List[str] = [
|
||||||
|
os.path.join(base_path, os.path.basename(img_path)),
|
||||||
|
os.path.join(base_path, "chart-671.png"),
|
||||||
|
]
|
||||||
|
for path in potential_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
self.logger.info(
|
||||||
|
f"Found icon in PyInstaller bundle: {path}"
|
||||||
|
)
|
||||||
|
img_path = path
|
||||||
|
break
|
||||||
|
|
||||||
|
icon_image: Image.Image = Image.open(img_path)
|
||||||
|
icon_image = icon_image.resize(
|
||||||
|
size=(32, 32), resample=Image.Resampling.NEAREST
|
||||||
|
)
|
||||||
|
icon_photo: ImageTk.PhotoImage = ImageTk.PhotoImage(
|
||||||
|
image=icon_image
|
||||||
|
)
|
||||||
|
self.root.iconphoto(True, icon_photo)
|
||||||
|
self.root.wm_iconphoto(True, icon_photo)
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.logger.warning(f"Icon file not found at {img_path}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error setting icon: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_input_frame(self, parent_frame: ttk.Frame) -> Dict[str, Any]:
|
||||||
|
"""Create and configure the input frame with all widgets."""
|
||||||
|
input_frame: ttk.LabelFrame = ttk.LabelFrame(
|
||||||
|
parent_frame, text="New Entry"
|
||||||
|
)
|
||||||
|
input_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
|
||||||
|
input_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Create variables for symptoms
|
||||||
|
symptom_vars: Dict[str, tk.IntVar] = {
|
||||||
|
"depression": tk.IntVar(value=0),
|
||||||
|
"anxiety": tk.IntVar(value=0),
|
||||||
|
"sleep": tk.IntVar(value=0),
|
||||||
|
"appetite": tk.IntVar(value=0),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create scales for symptoms
|
||||||
|
symptom_labels: List[Tuple[str, str]] = [
|
||||||
|
("Depression (0-10):", "depression"),
|
||||||
|
("Anxiety (0-10):", "anxiety"),
|
||||||
|
("Sleep Quality (0-10):", "sleep"),
|
||||||
|
("Appetite (0-10):", "appetite"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for idx, (label, var_name) in enumerate(symptom_labels):
|
||||||
|
ttk.Label(input_frame, text=label).grid(
|
||||||
|
row=idx, column=0, sticky="w", padx=5, pady=2
|
||||||
|
)
|
||||||
|
ttk.Scale(
|
||||||
|
input_frame,
|
||||||
|
from_=0,
|
||||||
|
to=10,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
variable=symptom_vars[var_name],
|
||||||
|
).grid(row=idx, column=1, sticky="ew")
|
||||||
|
|
||||||
|
# Medicine checkboxes
|
||||||
|
ttk.Label(input_frame, text="Treatment:").grid(
|
||||||
|
row=4, column=0, sticky="w", padx=5, pady=2
|
||||||
|
)
|
||||||
|
medicine_frame = ttk.LabelFrame(input_frame, text="Medicine")
|
||||||
|
medicine_frame.grid(row=4, column=1, padx=0, pady=10, sticky="nsew")
|
||||||
|
|
||||||
|
medicine_vars: Dict[str, Tuple[tk.IntVar, str]] = {
|
||||||
|
"bupropion": (tk.IntVar(value=0), "Bupropion 150mg"),
|
||||||
|
"hydroxyzine": (tk.IntVar(value=0), "Hydroxyzine 25mg"),
|
||||||
|
"gabapentin": (tk.IntVar(value=0), "Gabapentin 100mg"),
|
||||||
|
"propranolol": (tk.IntVar(value=0), "Propranolol 10mg"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, (name, (var, text)) in enumerate(medicine_vars.items()):
|
||||||
|
ttk.Checkbutton(medicine_frame, text=text, variable=var).grid(
|
||||||
|
row=idx, column=0, sticky="w", padx=5, pady=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Note and Date fields
|
||||||
|
note_var: tk.StringVar = tk.StringVar()
|
||||||
|
date_var: tk.StringVar = tk.StringVar()
|
||||||
|
|
||||||
|
ttk.Label(input_frame, text="Note:").grid(
|
||||||
|
row=5, column=0, sticky="w", padx=5, pady=2
|
||||||
|
)
|
||||||
|
ttk.Entry(input_frame, textvariable=note_var).grid(
|
||||||
|
row=5, column=1, sticky="ew", padx=5, pady=2
|
||||||
|
)
|
||||||
|
|
||||||
|
ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid(
|
||||||
|
row=6, column=0, sticky="w", padx=5, pady=2
|
||||||
|
)
|
||||||
|
ttk.Entry(input_frame, textvariable=date_var, justify="center").grid(
|
||||||
|
row=6, column=1, sticky="ew", padx=5, pady=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return all UI elements and variables
|
||||||
|
return {
|
||||||
|
"frame": input_frame,
|
||||||
|
"symptom_vars": symptom_vars,
|
||||||
|
"medicine_vars": medicine_vars,
|
||||||
|
"note_var": note_var,
|
||||||
|
"date_var": date_var,
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_table_frame(self, parent_frame: ttk.Frame) -> Dict[str, Any]:
|
||||||
|
"""Create and configure the table frame with a treeview."""
|
||||||
|
table_frame: ttk.LabelFrame = ttk.LabelFrame(
|
||||||
|
parent_frame, text="Log (Double-click to edit)"
|
||||||
|
)
|
||||||
|
table_frame.grid(row=1, column=1, padx=10, pady=10, sticky="nsew")
|
||||||
|
|
||||||
|
# Configure table frame to expand
|
||||||
|
table_frame.grid_rowconfigure(0, weight=1)
|
||||||
|
table_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
columns: List[str] = [
|
||||||
|
"Date",
|
||||||
|
"Depression",
|
||||||
|
"Anxiety",
|
||||||
|
"Sleep",
|
||||||
|
"Appetite",
|
||||||
|
"Bupropion",
|
||||||
|
"Hydroxyzine",
|
||||||
|
"Gabapentin",
|
||||||
|
"Propranolol",
|
||||||
|
"Note",
|
||||||
|
]
|
||||||
|
|
||||||
|
tree: ttk.Treeview = ttk.Treeview(
|
||||||
|
table_frame, columns=columns, show="headings"
|
||||||
|
)
|
||||||
|
|
||||||
|
col_labels: List[str] = [
|
||||||
|
"Date",
|
||||||
|
"Depression",
|
||||||
|
"Anxiety",
|
||||||
|
"Sleep",
|
||||||
|
"Appetite",
|
||||||
|
"Bupropion 150mg",
|
||||||
|
"Hydroxyzine 25mg",
|
||||||
|
"Gabapentin 100mg",
|
||||||
|
"Propranolol 10mg",
|
||||||
|
"Note",
|
||||||
|
]
|
||||||
|
|
||||||
|
for col, label in zip(columns, col_labels):
|
||||||
|
tree.heading(col, text=label)
|
||||||
|
|
||||||
|
col_settings: List[Tuple[str, int, str]] = [
|
||||||
|
("Date", 80, "center"),
|
||||||
|
("Depression", 80, "center"),
|
||||||
|
("Anxiety", 80, "center"),
|
||||||
|
("Sleep", 80, "center"),
|
||||||
|
("Appetite", 80, "center"),
|
||||||
|
("Bupropion", 120, "center"),
|
||||||
|
("Hydroxyzine", 120, "center"),
|
||||||
|
("Gabapentin", 120, "center"),
|
||||||
|
("Propranolol", 120, "center"),
|
||||||
|
("Note", 300, "w"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for col, width, anchor in col_settings:
|
||||||
|
tree.column(col, width=width, anchor=anchor)
|
||||||
|
|
||||||
|
tree.pack(side="left", fill="both", expand=True)
|
||||||
|
|
||||||
|
# Add scrollbar
|
||||||
|
scrollbar = ttk.Scrollbar(
|
||||||
|
table_frame, orient="vertical", command=tree.yview
|
||||||
|
)
|
||||||
|
tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
|
return {"frame": table_frame, "tree": tree}
|
||||||
|
|
||||||
|
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
|
||||||
|
"""Create and configure the graph frame."""
|
||||||
|
graph_frame: ttk.LabelFrame = ttk.LabelFrame(
|
||||||
|
parent_frame, text="Evolution"
|
||||||
|
)
|
||||||
|
graph_frame.grid(
|
||||||
|
row=0, column=0, columnspan=2, padx=10, pady=10, sticky="nsew"
|
||||||
|
)
|
||||||
|
return graph_frame
|
||||||
|
|
||||||
|
def add_buttons(
|
||||||
|
self, frame: ttk.Frame, buttons_config: List[Dict[str, Any]]
|
||||||
|
) -> ttk.Frame:
|
||||||
|
"""Add buttons to a frame based on configuration."""
|
||||||
|
button_frame: ttk.Frame = ttk.Frame(frame)
|
||||||
|
button_frame.grid(row=7, column=0, columnspan=2, pady=10)
|
||||||
|
|
||||||
|
for btn_config in buttons_config:
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text=btn_config["text"],
|
||||||
|
command=btn_config["command"],
|
||||||
|
).pack(
|
||||||
|
side="left",
|
||||||
|
padx=5,
|
||||||
|
fill=btn_config.get("fill", None),
|
||||||
|
expand=btn_config.get("expand", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
return button_frame
|
||||||
|
|
||||||
|
def create_edit_window(
|
||||||
|
self, values: Tuple[str, ...], callbacks: Dict[str, Callable]
|
||||||
|
) -> tk.Toplevel:
|
||||||
|
"""Create a new window for editing an entry."""
|
||||||
|
edit_win: tk.Toplevel = tk.Toplevel(master=self.root)
|
||||||
|
edit_win.title("Edit Entry")
|
||||||
|
edit_win.transient(self.root) # Make window modal
|
||||||
|
edit_win.minsize(400, 300)
|
||||||
|
|
||||||
|
# Configure grid columns to expand properly
|
||||||
|
edit_win.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Unpack values
|
||||||
|
date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values
|
||||||
|
|
||||||
|
# Create variables and fields
|
||||||
|
vars_dict = self._create_edit_fields(
|
||||||
|
edit_win, date, dep, anx, slp, app
|
||||||
|
)
|
||||||
|
|
||||||
|
# Medicine checkboxes
|
||||||
|
current_row = 6 # After the 5 fields (date, dep, anx, slp, app)
|
||||||
|
med_vars = self._create_medicine_checkboxes(
|
||||||
|
edit_win, current_row, bup, hydro, gaba, prop
|
||||||
|
)
|
||||||
|
vars_dict.update(med_vars)
|
||||||
|
|
||||||
|
# Note field
|
||||||
|
current_row += 1
|
||||||
|
vars_dict["note"] = tk.StringVar(value=str(note))
|
||||||
|
ttk.Label(edit_win, text="Note:").grid(
|
||||||
|
row=current_row, column=0, sticky="w", padx=5, pady=2
|
||||||
|
)
|
||||||
|
ttk.Entry(edit_win, textvariable=vars_dict["note"]).grid(
|
||||||
|
row=current_row, column=1, sticky="ew", padx=5, pady=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
current_row += 1
|
||||||
|
self._add_edit_window_buttons(
|
||||||
|
edit_win, current_row, vars_dict, callbacks
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make window modal
|
||||||
|
edit_win.update_idletasks()
|
||||||
|
edit_win.focus_set()
|
||||||
|
edit_win.grab_set()
|
||||||
|
|
||||||
|
return edit_win
|
||||||
|
|
||||||
|
def _create_edit_fields(
|
||||||
|
self,
|
||||||
|
parent: tk.Toplevel,
|
||||||
|
date: str,
|
||||||
|
dep: int,
|
||||||
|
anx: int,
|
||||||
|
slp: int,
|
||||||
|
app: int,
|
||||||
|
) -> Dict[str, Union[tk.StringVar, tk.IntVar]]:
|
||||||
|
"""Create fields for editing entry values."""
|
||||||
|
vars_dict: Dict[str, Union[tk.StringVar, tk.IntVar]] = {}
|
||||||
|
|
||||||
|
# Ensure values are converted to appropriate types
|
||||||
|
try:
|
||||||
|
app = int(app) if app != "" else 0
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
self.logger.warning(
|
||||||
|
f"Invalid appetite value: {app}, defaulting to 0"
|
||||||
|
)
|
||||||
|
app = 0
|
||||||
|
|
||||||
|
value_map = {
|
||||||
|
"date": date,
|
||||||
|
"depression": dep,
|
||||||
|
"anxiety": anx,
|
||||||
|
"sleep": slp,
|
||||||
|
"appetite": app,
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
("Date", tk.StringVar, "date"),
|
||||||
|
("Depression (0-10)", tk.IntVar, "depression"),
|
||||||
|
("Anxiety (0-10)", tk.IntVar, "anxiety"),
|
||||||
|
("Sleep (0-10)", tk.IntVar, "sleep"),
|
||||||
|
("Appetite (0-10)", tk.IntVar, "appetite"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for idx, (label, var_type, key) in enumerate(fields):
|
||||||
|
try:
|
||||||
|
value = value_map[key]
|
||||||
|
if var_type == tk.IntVar:
|
||||||
|
try:
|
||||||
|
value = int(float(value))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
value = 0
|
||||||
|
self.logger.warning(
|
||||||
|
f"Failed to convert {key} value: {value}, defaulting to 0"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
value = str(value)
|
||||||
|
except (ValueError, TypeError, KeyError):
|
||||||
|
value = 0 if var_type == tk.IntVar else ""
|
||||||
|
self.logger.warning(
|
||||||
|
f"Missing or invalid value for {key}, defaulting to {value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
vars_dict[key] = var_type(value=value)
|
||||||
|
ttk.Label(parent, text=f"{label}:").grid(
|
||||||
|
row=idx + 1, column=0, sticky="w", padx=5, pady=2
|
||||||
|
)
|
||||||
|
|
||||||
|
if var_type == tk.IntVar:
|
||||||
|
self._create_scale_with_label(
|
||||||
|
parent, idx + 1, vars_dict[key], value
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ttk.Entry(parent, textvariable=vars_dict[key]).grid(
|
||||||
|
row=idx + 1, column=1, sticky="ew"
|
||||||
|
)
|
||||||
|
|
||||||
|
return vars_dict
|
||||||
|
|
||||||
|
def _create_scale_with_label(
|
||||||
|
self, parent: tk.Toplevel, row: int, var: tk.IntVar, value: int
|
||||||
|
) -> None:
|
||||||
|
"""Create a scale with a value label."""
|
||||||
|
scale_frame: ttk.Frame = ttk.Frame(parent)
|
||||||
|
scale_frame.grid(row=row, column=1, sticky="ew", padx=5, pady=2)
|
||||||
|
scale_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
scale = ttk.Scale(
|
||||||
|
scale_frame, from_=0, to=10, variable=var, orient=tk.HORIZONTAL
|
||||||
|
)
|
||||||
|
scale.grid(row=0, column=0, sticky="ew", padx=5)
|
||||||
|
|
||||||
|
# Add a value label to show the current value
|
||||||
|
value_label = ttk.Label(scale_frame, width=3)
|
||||||
|
value_label.grid(row=0, column=1, padx=(5, 0))
|
||||||
|
|
||||||
|
# Update label when scale value changes
|
||||||
|
def update_label(event=None):
|
||||||
|
value_label.configure(text=str(var.get()))
|
||||||
|
|
||||||
|
scale.bind("<Motion>", update_label)
|
||||||
|
scale.bind("<ButtonRelease-1>", update_label)
|
||||||
|
update_label() # Set initial value
|
||||||
|
scale.set(value) # Explicitly set scale value
|
||||||
|
|
||||||
|
def _create_medicine_checkboxes(
|
||||||
|
self,
|
||||||
|
parent: tk.Toplevel,
|
||||||
|
row: int,
|
||||||
|
bup: int,
|
||||||
|
hydro: int,
|
||||||
|
gaba: int,
|
||||||
|
prop: int,
|
||||||
|
) -> Dict[str, tk.IntVar]:
|
||||||
|
"""Create medicine checkboxes in the edit window."""
|
||||||
|
ttk.Label(parent, text="Treatment:").grid(
|
||||||
|
row=row, column=0, sticky="w", padx=5, pady=2
|
||||||
|
)
|
||||||
|
medicine_frame: ttk.LabelFrame = ttk.LabelFrame(
|
||||||
|
parent, text="Medicine"
|
||||||
|
)
|
||||||
|
medicine_frame.grid(row=row, column=1, padx=0, pady=10, sticky="nsew")
|
||||||
|
|
||||||
|
medicine_vars: Dict[str, Tuple[int, str]] = {
|
||||||
|
"bupropion": (bup, "Bupropion 150mg"),
|
||||||
|
"hydroxyzine": (hydro, "Hydroxyzine 25mg"),
|
||||||
|
"gabapentin": (gaba, "Gabapentin 100mg"),
|
||||||
|
"propranolol": (prop, "Propranolol 10mg"),
|
||||||
|
}
|
||||||
|
|
||||||
|
vars_dict: Dict[str, tk.IntVar] = {}
|
||||||
|
for idx, (key, (value, label)) in enumerate(medicine_vars.items()):
|
||||||
|
vars_dict[key] = tk.IntVar(value=int(value))
|
||||||
|
ttk.Checkbutton(
|
||||||
|
medicine_frame, text=label, variable=vars_dict[key]
|
||||||
|
).grid(row=idx, column=0, sticky="w", padx=5, pady=2)
|
||||||
|
|
||||||
|
return vars_dict
|
||||||
|
|
||||||
|
def _add_edit_window_buttons(
|
||||||
|
self,
|
||||||
|
parent: tk.Toplevel,
|
||||||
|
row: int,
|
||||||
|
vars_dict: Dict[str, Any],
|
||||||
|
callbacks: Dict[str, Callable],
|
||||||
|
) -> None:
|
||||||
|
"""Add buttons to the edit window."""
|
||||||
|
button_frame: ttk.Frame = ttk.Frame(parent)
|
||||||
|
button_frame.grid(row=row, column=0, columnspan=2, pady=10)
|
||||||
|
|
||||||
|
# Save button
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Save",
|
||||||
|
command=lambda: callbacks["save"](
|
||||||
|
parent,
|
||||||
|
vars_dict["date"].get(),
|
||||||
|
vars_dict["depression"].get(),
|
||||||
|
vars_dict["anxiety"].get(),
|
||||||
|
vars_dict["sleep"].get(),
|
||||||
|
vars_dict["appetite"].get(),
|
||||||
|
vars_dict["bupropion"].get(),
|
||||||
|
vars_dict["hydroxyzine"].get(),
|
||||||
|
vars_dict["gabapentin"].get(),
|
||||||
|
vars_dict["propranolol"].get(),
|
||||||
|
vars_dict["note"].get(),
|
||||||
|
),
|
||||||
|
).pack(side="left", padx=5)
|
||||||
|
|
||||||
|
# Cancel button
|
||||||
|
ttk.Button(button_frame, text="Cancel", command=parent.destroy).pack(
|
||||||
|
side="left", padx=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete button
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Delete",
|
||||||
|
command=lambda: callbacks["delete"](parent),
|
||||||
|
).pack(side="left", padx=5)
|
||||||
Reference in New Issue
Block a user