Tutorials/Power Apps/Track Project Hours with Power Apps: Adding Approval Stages
Power Appsintermediate

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.

NA
Narmer Abader
@narmer · Published June 3, 2026

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 NameTypeNotes
ConsultantPersonDefault to the current user
WeekStartDateMonday of the chosen week (or Sunday, adjust to preference)
WeekEndDateFriday or Saturday of the same week
TotalHoursNumberWill be summed from entries
StatusChoiceDraft, Submitted, Approved, Rejected

TimeEntries (child)

Column NameTypeNotes
ReportIDNumberLookup to the ID of the parent report
ClientTextFree‑text client name
MonHoursNumberHours for Monday
TueHoursNumberHours for Tuesday
WedHoursNumberHours for Wednesday
ThuHoursNumberHours for Thursday
FriHoursNumberHours for Friday
SatHoursNumberHours for Saturday (if working)
SunHoursNumberHours 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:

powerfxClear current report variable
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:

powerfxShow only when no report is active
IsBlank(gblCurrentReport)

The report title label should change its text based on whether a report is already loaded:

powerfxDynamic report title
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:

powerfxCreate a new WeeklyReport record
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:

powerfxAdd a client line
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.

powerfxSave button OnSelect (full)
// 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).

powerfxSubmit button OnSelect
// 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

  • ForAll over gal_TimeEntries.AllItems will not delegate. Keep the number of lines per report under 500.
  • The Sum functions inside the Patch are evaluated client‑side on the colUpdatedEntries collection, which is also not delegated but still fast for small collections.
  • The Filter and LookUp used to display reports in the sidebar gallery should be delegated if you have many reports. Use Filter(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 Patch calls are sent in sequence, not as a transaction.

Common Mistakes and Troubleshooting

MistakeSymptomFix
Not including the Claims syntax for the Person columnThe Consultant field is blank after PatchUse the exact JSON shown above for the Person object.
Forgetting to set ReportID in the child entriesTime entries appear orphanedAlways include ReportID: gblCurrentReport.ID in every Patch call.
Using Defaults of the wrong listErrors on saveDouble‑check the list name (case‑sensitive in Power Apps).
Not clearing colDeletedEntries after savingOld deletes happen again next saveAlways 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