![[IMG-Automate Your Gains-20250713163417973.png|500]] --- In [[Automate Your Gains, Part 3 - Save Form Data to Google Sheets with Apps Script|Automate Your Gains - Part 3]], a "write path" for the application was created, which allowed for saving workout data from the form directly into Google Sheets. In this post, another feature will be added to the app: the "Last Workout Recall." When a user selects an exercise, the app will automatically fetch the details of the last time that the exercise was performed and display its details. **Sample of Exercise History Display**</br> ![[IMG-A Deep Dive into the 'Automate Your Gains' Workout App UI & Features-20250627175528572.png|300]] </br> --- ### **Section 1: The Trigger - Responding to User Selection** The read operation needs to trigger whenever the user selects an exercise from the dropdown menu. This was already set this up in [[Automate Your Gains, Part 2 - Build a Workout App UI with Apps Script|Part 2]]. In `LogExercise.html`, the exercise `<select>` element has an `onchange` attribute that calls a function named `prefillFormFields()`. This `prefillFormFields()` function in the client-side `<script>` tag is the starting point. It performs the following actions: - clears the form - sets the planned weight/reps for the selected exercise - initiates the call to our backend to fetch the historical data. ) The key part of this function from `LogExercise.html`: >[!tip]- Code Snippet >```js >// From LogExercise.html <script> section > >function prefillFormFields() { > const selectedExerciseId = > exerciseSelect.value; > // ... code to prefill the form with today's planned workout ... > > if (lastWorkoutDetailsToast) { > // Show a loading message while we fetch the data > lastWorkoutDetailsToast > .innerHTML = > '<em><span class="spinner-border spinner-border-sm"></span> Loading previous log...</em>'; > lastWorkoutDetailsToast.style > .display = "block"; > > // --- This is the start of our read path --- > google.script.run > .withSuccessHandler( > function(lastLog) { > // We will define this success handler in Section 3 > if (! > lastWorkoutDetailsToast > ) return; > if (lastLog && > lastLog.error) { > // Handle server error > } > else if (lastLog) { > // Display the successful log details > } > else { > // Handle case where no log was found > } > }) > .withFailureHandler( > function(error) { > // Handle network or other critical failures > }) > .getLastLoggedDetailsForExercise( > selectedExerciseId, > workoutTemplateSelect > .value > ); > } >} </br> The `google.script.run` command calls a server-side function in `main.js` named `getLastLoggedDetailsForExercise`, passing the ID of the selected exercise and workout plan as arguments. </br> --- ### **Section 2: The Query - Finding the Data on the Server** Moving back to the `main.js` file, the next step will be to create the `getLastLoggedDetailsForExercise` function. This function's job is to sift through all the data in the `WorkoutLog` sheet and find the most recent entry that matches the user's selection. Add this function to the `main.js` file: >[!important]- Code Snippet >```js >// From main.js > >/** > * Gets the last logged details for a specific exercise, optionally filtered by template. > * @param {string} exerciseId The ID of the exercise. > * @param {string} templateId The ID of the workout template. > * @return {object|null} An object with last log details or null if not found. > */ >function getLastLoggedDetailsForExercise( > exerciseId, templateId) { > if (!exerciseId) { > return null; > } > > try { > const sheets = getAppSheets(); > const { > data: workoutLogData, > headerMap: logHeaderMap > } = > getSheetDataWithHeadersAndMap( > sheets.workoutLogSheet, > "workoutLogData_singleUser" > ); > > // 1. Filter all logs to find ones that match the exercise and template > let relevantLogs = > workoutLogData.filter(( > row) => { > const > rowExerciseId = > String(row[ > logHeaderMap > .ExerciseID > ]).trim(); > let match = > rowExerciseId === > String( > exerciseId) > .trim(); > > if (templateId && > logHeaderMap > .LinkedTemplateID !== > undefined) { > const > rowTemplateId = > String(row[ > logHeaderMap > .LinkedTemplateID > ]) > .trim(); > match = match && > rowTemplateId === > String( > templateId > ) > .trim(); > } > return match; > }); > > if (relevantLogs.length === 0) { > return null; // No logs found > } > > // 2. Sort the relevant logs by date, newest first > relevantLogs.sort((a, b) => { > const dateA = > new Date(a[ > logHeaderMap > .ExerciseTimestamp > ]); > const dateB = > new Date(b[ > logHeaderMap > .ExerciseTimestamp > ]); > return dateB > .getTime() - > dateA.getTime(); > }); > > const lastLogEntry = > relevantLogs[0]; > > // 3. Package the data into a clean object to send back > return { > exerciseTimestamp: lastLogEntry[ > logHeaderMap > .ExerciseTimestamp], > setsPerformed: lastLogEntry[ > logHeaderMap > .TotalSetsPerformed > ], > repsPerformed: lastLogEntry[ > logHeaderMap[ > "RepsPerformed (per set)" > ]], > weightUsed: lastLogEntry[ > logHeaderMap > .WeightUsed], > weightUnit: lastLogEntry[ > logHeaderMap > .WeightUnit] || > "lbs", > rpeRecorded: lastLogEntry[ > logHeaderMap > .RPE_Recorded], > workoutNotes: lastLogEntry[ > logHeaderMap > .WorkoutNotes] || > "", > }; > } > catch (e) { > Logger.log( > `Error in getLastLoggedDetailsForExercise: ${e.toString()}` > ); > return { > error: `Server error fetching last log details.` > }; > } >} </br> This function is an example of a simple database query: 1. **Filter:** It reduces the entire log down to only the rows that matter for the current context (the same exercise within the same workout plan). 2. **Sort:** It orders the results by date to ensure the first entry is the most recent one. 3. **Return:** It sends a clean, simple object containing just the necessary details back to the client. </br> --- ### **Section 3: The Presentation - Displaying the "Toast"** Once the server finds the last workout and sends it back, our `.withSuccessHandler()` from Section 1 receives this `lastLog` object. Next step is to format it and display it to the user. Complete the success handler inside the `prefillFormFields` function in `LogExercise.html`. >[!important]- Code Snippet >```js >// From LogExercise.html <script> section > >// This is the full success handler function >.withSuccessHandler(function(lastLog) > >if (!lastWorkoutDetailsToast) > return; // Make sure the UI element exists > >// Case 1: An error object was returned from the server >if (lastLog && lastLog.error) { > lastWorkoutDetailsToast > .innerHTML = > `Could not load previous log: ${lastLog.error}`; > lastWorkoutDetailsToast > .className = > "alert alert-warning mt-2 text-dark"; > > // Case 2: A valid log object was returned >} >else if (lastLog) { > let detailsHtml = > "<strong>Last time"; > if (lastLog > .exerciseTimestamp) { > const logDate = > new Date(lastLog > .exerciseTimestamp > ); > detailsHtml += ` (${logDate.toLocaleDateString(undefined, { > month: "numeric", > day: "numeric", > })}):</strong> `; > } > else { > detailsHtml += > ":</strong> "; > } > detailsHtml += `${lastLog.setsPerformed || "-"} sets of ${ > lastLog.repsPerformed || "-" > } reps @ ${lastLog.weightUsed === null ? "-" : lastLog.weightUsed} ${ > lastLog.weightUnit || "lbs" > }`; > if (lastLog.rpeRecorded !== > null) { > detailsHtml += > `, RPE ${lastLog.rpeRecorded}`; > } > detailsHtml += "."; > if (lastLog.workoutNotes && > lastLog.workoutNotes > .trim() !== "") { > const notesText = > String(lastLog > .workoutNotes) > .replace(/</g, > "&lt;"); > detailsHtml += > `<br><small><em>Notes: ${notesText}</em></small>`; > } > lastWorkoutDetailsToast > .innerHTML = > detailsHtml; > lastWorkoutDetailsToast > .className = > "alert alert-info mt-2"; > > // Case 3: No log was found (the server returned null) >} >else { > lastWorkoutDetailsToast > .textContent = > "No previous log found for this exercise in this plan."; > lastWorkoutDetailsToast > .className = > "alert alert-light mt-2 text-muted"; >} > >// Make the toast visible >lastWorkoutDetailsToast.style >.display = "block"; >} >) </br> This client-side JavaScript code handles all possible outcomes. It dynamically builds an HTML string with the workout details and injects it into the `lastWorkoutDetailsToast` div, updating its style to make it visible to the user. </br> ### **Conclusion** The "read path" can now be considered complete, the application is no longer just a data entry form. The web app can now use its own history to provide context. The app now has the ability to both write to and read from our Google Sheet database. In [[Automate Your Gains, Part 5 - Code Smart Automation for Your Fitness App|Part 5]], the focus will be on using the logged RPE to calculate and suggest progressive overload for future workouts. </br> --- ## Resources </br> ### Github and Demo You can find the completed code for the entire project, including all features and documentation, on GitHub. - **[View Project on GitHub](https://github.com/drusho/workout-logger-google-apps-script)** - **[Try the Live Web App Demo](https://script.google.com/macros/s/AKfycbwiQyKHvKap9oiKqSpAhFdbq9xH36wOZCr0a6QRZEgSL0ErCWXhaUoVAIPcqD1zM_2I/exec)** </br> ### Related Articles Check out the other articles from **Automate Your Gains** series: %% DATAVIEW_PUBLISH_CONVERT start ```dataview LIST WITHOUT ID "**" + file.link + "** </br>" + description + "</br></br>" FROM "07 - Publish - Obsidian" WHERE publish = true AND file.name != "About Me" AND file.name != "Home" AND file.name != "Series - Automate Your Gains" AND series = "Automate Your Gains" SORT date DESC ``` %% - **[[07 - Publish - Obsidian/Articles/A Deep Dive into the 'Automate Your Gains' Workout App UI & Features.md|A Deep Dive into the 'Automate Your Gains' Workout App UI & Features]]** </br>Take a tour of a custom workout logger built with Google Apps Script. See its mobile-friendly UI, dynamic workout planning, "last workout recall," and automated progression features in action.</br></br> - **[[07 - Publish - Obsidian/Articles/Automate Your Gains, Part 1 - Plan a Custom Workout App with Google Sheets.md|Automate Your Gains, Part 1 - Plan a Custom Workout App with Google Sheets]]** </br>A fitness app project that shows how to plan a smart workout logger using Google Apps Script and Google Sheets to automate your training.</br></br> - **[[07 - Publish - Obsidian/Articles/Automate Your Gains, Part 2 - Build a Workout App UI with Apps Script.md|Automate Your Gains, Part 2 - Build a Workout App UI with Apps Script]]** </br>Turn a Google Sheet into a database and build a mobile-friendly UI with Google Apps Script. A step-by-step guide to creating the foundation for the workout logger.</br></br> - **[[07 - Publish - Obsidian/Articles/Automate Your Gains, Part 3 - Save Form Data to Google Sheets with Apps Script.md|Automate Your Gains, Part 3 - Save Form Data to Google Sheets with Apps Script]]** </br>Connect front-end to your back-end. This guide covers using google.script.run to capture HTML form data and save it directly to Google Sheets, creating a complete "write path."</br></br> - **[[07 - Publish - Obsidian/Articles/Automate Your Gains, Part 4 - Read & Display Data from Google Sheets in Your App.md|Automate Your Gains, Part 4 - Read & Display Data from Google Sheets in Your App]]** </br>Close the data loop for the workout app. Fetch, filter, and sort data from a Google Sheet backend and display it dynamically into web app's UI for a richer user experience.</br></br> - **[[07 - Publish - Obsidian/Articles/Automate Your Gains, Part 5 - Code Smart Automation for Your Fitness App.md|Automate Your Gains, Part 5 - Code Smart Automation for Your Fitness App]]** </br>Elevate the app from a simple logger to a smart training partner. Code automation logic that analyzes user performance (RPE) to recommend workout progressions.</br></br> - **[[07 - Publish - Obsidian/Articles/Automate Your Gains, Part 6 - When to Scale Your Google Apps Script Project.md|Automate Your Gains, Part 6 - When to Scale Your Google Apps Script Project]]** </br>A complete review of the workout app project. Covers the pros and cons of using Google Sheets as a database, ideas for future features, and how to know when it's time to migrate.</br></br> - **[[07 - Publish - Obsidian/Posts/Series/Automate Your Gains.md|Automate Your Gains]]** </br>Articles related to creating a workout logger web application using Google Sheets and Apps Script</br></br> %% DATAVIEW_PUBLISH_CONVERT end %%