Track Project Hours with Power Apps: Adding Approval Stages
Learn how to extend a time-tracking app by generating weekly reports, linking entry lines, and submitting them for manager review.
In the previous article we built a core gallery for entering daily hours. Now we’ll add the ability to create a full weekly report, save it to SharePoint, and move it through an approval flow. This instalment focuses on the parent–child relationship between a report and its time entries, automatic total calculation, and a simple submit action.
Example Scenario
A small marketing agency wants its team to log hours per client every week. Each consultant creates a Weekly Report for the current week, then fills in hours for each client they worked on. Once the week is complete, they submit the report for the account manager to review. The app we build here will handle the “create report” and “submit for approval” stages.
SharePoint Lists Design
We’ll use two SharePoint lists. Use your own naming conventions, but the columns should resemble the structure below.
WeeklyReports (parent)
| Column Name | Type | Notes |
|---|---|---|
| Consultant | Person | Default to the current user |
| WeekStart | Date | Monday of the chosen week (or Sunday, adjust to preference) |
| WeekEnd | Date | Friday or Saturday of the same week |
| TotalHours | Number | Will be summed from entries |
| Status | Choice | Draft, Submitted, Approved, Rejected |
TimeEntries (child)
| Column Name | Type | Notes |
|---|---|---|
| ReportID | Number | Lookup to the ID of the parent report |
| Client | Text | Free‑text client name |
| MonHours | Number | Hours for Monday |
| TueHours | Number | Hours for Tuesday |
| WedHours | Number | Hours for Wednesday |
| ThuHours | Number | Hours for Thursday |
| FriHours | Number | Hours for Friday |
| SatHours | Number | Hours for Saturday (if working) |
| SunHours | Number | Hours for Sunday (if working) |
Child entries are linked to the parent via ReportID. This is not a formal SharePoint lookup column – just a number column that holds the parent’s ID.
App Layout – Sidebar Navigation
Add a sidebar container to hold two main actions:
- Reports – shows a list of existing reports (in a gallery)
- New Report – clears the current report variable and opens the create screen
Place an icon and a label for each action inside the sidebar. For the New Report icon, set its OnSelect to:
Set(gblCurrentReport, Blank()); Clear(colDeletedEntries);
Creating a New Weekly Report
When the user clicks New Report, the main area should display a date picker and a Create Report button.
Set the Visible property of the date picker and button to:
IsBlank(gblCurrentReport)
The report title label should change its text based on whether a report is already loaded:
If( IsBlank(gblCurrentReport), "New Weekly Report", "Week: " & Text(gblCurrentReport.WeekStart, "mmm dd") & " – " & Text(gblCurrentReport.WeekEnd, "mmm dd, yyyy") )
The Create Report Button
OnSelect of the button:
Set(
gblCurrentReport,
Patch(
WeeklyReports,
Defaults(WeeklyReports),
{
Consultant: {
'@odata.type': "#Microsoft.Azure.Connectors.SharePoint.SPListExpandedUser",
Claims: "i:0#.f|membership|" & User().Email,
DisplayName: User().FullName,
Email: User().Email
},
WeekStart: DateAdd(
dte_WeekSelected.SelectedDate,
1 - Weekday(dte_WeekSelected.SelectedDate, StartOfWeek.Monday),
TimeUnit.Days
),
WeekEnd: DateAdd(
dte_WeekSelected.SelectedDate,
7 - Weekday(dte_WeekSelected.SelectedDate, StartOfWeek.Monday),
TimeUnit.Days
),
Status: {Value: "Draft"},
TotalHours: 0
}
)
);
// Immediately create one empty time entry line
ClearCollect(
colTimeEntries,
Patch(
TimeEntries,
Defaults(TimeEntries),
{ReportID: gblCurrentReport.ID}
)
);
Reset(dte_WeekSelected);This creates the parent record, stores it in a variable, and creates a single child row so the user can start filling hours.
Adding Time Entry Lines
The time entry gallery should have an “Add Line” button. Its OnSelect creates a new entry linked to the current report:
Patch(
colTimeEntries,
Defaults(TimeEntries),
{ReportID: gblCurrentReport.ID}
);Each line in the gallery shows a text input for the client name and seven numeric inputs for the days of the week. The gallery is bound to the colTimeEntries collection, not directly to the SharePoint list, for better offline performance.
Saving the Report with Totals
A Save icon commits all changes to SharePoint and recalculates the total hours.
// Build a collection of updated entries from the gallery
ClearCollect(
colUpdatedEntries,
ForAll(
gal_TimeEntries.AllItems,
TimeEntries@{
ID: ID,
ReportID: gblCurrentReport.ID,
Client: txt_Client.Text,
MonHours: Value(txt_Mon.Text),
TueHours: Value(txt_Tue.Text),
WedHours: Value(txt_Wed.Text),
ThuHours: Value(txt_Thu.Text),
FriHours: Value(txt_Fri.Text),
SatHours: Value(txt_Sat.Text),
SunHours: Value(txt_Sun.Text)
}
)
);
// Patch all changed entries back to SharePoint
ClearCollect(
colTimeEntries,
Patch(TimeEntries, gal_TimeEntries.AllItems, colUpdatedEntries)
);
// Remove any entries marked for deletion
ForAll(colDeletedEntries, Remove(TimeEntries, ThisRecord));
Clear(colDeletedEntries);
// Calculate and update the parent total
Set(
gblCurrentReport,
Patch(
WeeklyReports,
gblCurrentReport,
{
TotalHours:
Sum(colUpdatedEntries, MonHours) +
Sum(colUpdatedEntries, TueHours) +
Sum(colUpdatedEntries, WedHours) +
Sum(colUpdatedEntries, ThuHours) +
Sum(colUpdatedEntries, FriHours) +
Sum(colUpdatedEntries, SatHours) +
Sum(colUpdatedEntries, SunHours)
}
)
);The ForAll with AllItems works fine for typical numbers of entries (tens, not thousands). If you anticipate hundreds of rows per report, consider a batch‑patch approach or storing the data differently.
Submitting the Report for Approval
A Submit icon changes the report status from “Draft” to “Submitted”. After submission, the report should become read‑only in your gallery (not covered here, but easy to add with a check on the status).
// Update status to Submitted
Set(
gblCurrentReport,
Patch(
WeeklyReports,
gblCurrentReport,
{Status: {Value: "Submitted"}}
)
);
// Optionally trigger the same save code to ensure all lines are persisted
Select(ico_Save);Now the report appears in a Submitted state, ready for a manager to review in a separate part of the app.
Delegation and Performance Notes
ForAllovergal_TimeEntries.AllItemswill not delegate. Keep the number of lines per report under 500.- The
Sumfunctions inside thePatchare evaluated client‑side on thecolUpdatedEntriescollection, which is also not delegated but still fast for small collections. - The
FilterandLookUpused to display reports in the sidebar gallery should be delegated if you have many reports. UseFilter(WeeklyReports, Consultant.Email = User().Email)– this is delegable. - Because we use a simple number column for the relationship, you may need to manually ensure referential integrity inside your app logic. The
Patchcalls are sent in sequence, not as a transaction.
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
Not including the Claims syntax for the Person column | The Consultant field is blank after Patch | Use the exact JSON shown above for the Person object. |
Forgetting to set ReportID in the child entries | Time entries appear orphaned | Always include ReportID: gblCurrentReport.ID in every Patch call. |
Using Defaults of the wrong list | Errors on save | Double‑check the list name (case‑sensitive in Power Apps). |
Not clearing colDeletedEntries after saving | Old deletes happen again next save | Always call Clear(colDeletedEntries) after the ForAll(Remove…). |
Final Recommendation
This pattern – a parent record with a small number of child records, status changes, and a simple submit action – is versatile. You can adapt it for expense reports, mileage logs, or even project time tracking. Keep the collections small and test the delegation points if you scale up. For a production app, consider adding error handling and a read‑only view for approved reports.
References
- Original article by Matthew Devaney: Make A Power Apps Timesheet App – Part 2
- Microsoft Learn: Patch function in Power Apps
- Microsoft Learn: Person column in SharePoint
Learn how small coding practices—like using descriptive names, flattening conditions, and simplifying logic—can make your apps easier to update and less error-prone.
Move past the gallery and discover the hidden patterns for validation, navigation, and smart submission handling in your data entry forms.
PowerShell unlocks admin capabilities that the Power Platform admin center simply doesn’t offer—from recovering deleted apps to blocking trial licenses. Here’s how to wield them safely.