Approval Workflow
A manager approval queue with KPI cards, status tabs, and per-request approve / reject / return actions that require a reason. Every decision is written to an Activity Timeline of the request’s history. Sidebar and bottom nav make it work on desktop and mobile.
Two parts to paste. This template needs both the Screen YAML and the App.Formulas — without the formulas the screen renders blank. Copy both below, or just download the .msapp to skip the paste.
Preview
Interactive preview. The actual template runs natively in Power Apps.
Included in this template
Pending KPIs
Count, total value, approved this month
Status tabs
Segmented control with live per-status counts
Approval queue
Filtered, newest-first request list
Decision dialog
Approve / reject / return with a reason
Decision history
Every action written to an Activity Timeline
Mobile-ready
Sidebar, bottom nav, and mobile sheet
Components Used
This template uses these components. Install them before importing the screen YAML.
cmpKPICards
AIPending count, pending value, approved this month
cmpSegmentedControl
AIStatus tabs with live counts in each label
cmpActivityTimeline
AIDecision history for the selected request
cmpDialog
Approve / reject / return with a mandatory reason
cmpSidebar
Collapsible left navigation rail
cmpBottomNavigation
Mobile tab bar
Code
Screens:
scrApprovals:
Properties:
Fill: =RGBA(249, 250, 251, 1)
LoadingSpinnerColor: =RGBA(37, 99, 235, 1)
OnVisible: |-
=// ============================================================
// Approvals — demo data + screen state
// Seeding is gated on gblApSeedVersion, not IsEmpty, so a session
// holding an older column shape is reseeded instead of silently kept.
// Swap each ClearCollect for a Filter against your real source.
// ============================================================
If(
gblApSeedVersion <> "2026-07-27",
ClearCollect(colApRequests,
{ReqID:"PO-1042", Title:"Design tooling annual renewal", Requester:"Sarah Chen", Dept:"Marketing", Amount:2480, Category:"Software", SubmittedOn:DateAdd(Today(),-1,TimeUnit.Days), Justification:"Annual renewal of design tooling licenses for the 6-person creative team.", Status:"Pending", DecidedBy:"", DecidedOn:Blank(), DecisionNote:"", Selected:false},
{ReqID:"EXP-318", Title:"Newark site visit expenses", Requester:"James Wilson", Dept:"Field Ops", Amount:640, Category:"Travel", SubmittedOn:DateAdd(Today(),-1,TimeUnit.Days), Justification:"Site visit mileage and overnight stay, Newark inspection.", Status:"Pending", DecidedBy:"", DecidedOn:Blank(), DecisionNote:"", Selected:false},
{ReqID:"PO-1043", Title:"HVAC compressor replacement", Requester:"Maria Garcia", Dept:"Facilities", Amount:5120, Category:"Equipment", SubmittedOn:DateAdd(Today(),-2,TimeUnit.Days), Justification:"Replacement HVAC compressor for warehouse B, vendor quote attached.", Status:"Pending", DecidedBy:"", DecidedOn:Blank(), DecisionNote:"", Selected:false},
{ReqID:"EXP-319", Title:"Q2 client dinner and demo", Requester:"David Kim", Dept:"Sales", Amount:1190, Category:"Client", SubmittedOn:DateAdd(Today(),-2,TimeUnit.Days), Justification:"Q2 client dinner and demo event, 4 prospects from the Atlas account.", Status:"Pending", DecidedBy:"", DecidedOn:Blank(), DecisionNote:"", Selected:false},
{ReqID:"PO-1041", Title:"Replacement laptops for onboarding", Requester:"Lisa Anderson", Dept:"IT", Amount:3300, Category:"Hardware", SubmittedOn:DateAdd(Today(),-3,TimeUnit.Days), Justification:"Two replacement laptops for onboarding hires starting next sprint.", Status:"Pending", DecidedBy:"", DecidedOn:Blank(), DecisionNote:"", Selected:false},
{ReqID:"PO-1040", Title:"Forklift annual service", Requester:"Robert Taylor", Dept:"Operations", Amount:1450, Category:"Services", SubmittedOn:DateAdd(Today(),-3,TimeUnit.Days), Justification:"Scheduled annual service and certification for both warehouse forklifts.", Status:"Pending", DecidedBy:"", DecidedOn:Blank(), DecisionNote:"", Selected:false},
{ReqID:"PO-1038", Title:"Quarterly safety supply restock", Requester:"Robert Taylor", Dept:"Operations", Amount:870, Category:"Supplies", SubmittedOn:DateAdd(Today(),-4,TimeUnit.Days), Justification:"Quarterly safety supply restock across all four locations.", Status:"Approved", DecidedBy:"You", DecidedOn:DateAdd(Now(),-2,TimeUnit.Days), DecisionNote:"", Selected:false},
{ReqID:"PO-1039", Title:"Conference room AV upgrade", Requester:"Lisa Anderson", Dept:"IT", Amount:4200, Category:"Equipment", SubmittedOn:DateAdd(Today(),-4,TimeUnit.Days), Justification:"Replace the projector and conferencing mics in the main meeting room.", Status:"Returned", DecidedBy:"You", DecidedOn:DateAdd(Now(),-1,TimeUnit.Days), DecisionNote:"Need three quotes and a install date before I can sign this off.", Selected:false},
{ReqID:"EXP-310", Title:"Trade show booth deposit", Requester:"Emily Parker", Dept:"Marketing", Amount:2050, Category:"Events", SubmittedOn:DateAdd(Today(),-5,TimeUnit.Days), Justification:"Trade show booth deposit for the autumn industry expo.", Status:"Rejected", DecidedBy:"You", DecidedOn:DateAdd(Now(),-3,TimeUnit.Days), DecisionNote:"Over budget for the quarter, resubmit next period.", Selected:false}
);
ClearCollect(colApStatus,
{StatusKey:"Pending", Label:"Pending", Order:1, IsOpen:true, PillBg:RGBA(254, 243, 199, 1), PillText:RGBA(180, 83, 9, 1)},
{StatusKey:"Approved", Label:"Approved", Order:2, IsOpen:false, PillBg:RGBA(220, 252, 231, 1), PillText:RGBA(22, 163, 74, 1)},
{StatusKey:"Returned", Label:"Returned", Order:3, IsOpen:false, PillBg:RGBA(224, 231, 255, 1), PillText:RGBA(55, 48, 163, 1)},
{StatusKey:"Rejected", Label:"Rejected", Order:4, IsOpen:false, PillBg:RGBA(254, 226, 226, 1), PillText:RGBA(220, 38, 38, 1)}
);
ClearCollect(colApDept,
{Dept:"Marketing", Fill:RGBA(79, 70, 229, 1)},
{Dept:"Field Ops", Fill:RGBA(13, 148, 136, 1)},
{Dept:"Facilities", Fill:RGBA(217, 119, 6, 1)},
{Dept:"Sales", Fill:RGBA(219, 39, 119, 1)},
{Dept:"IT", Fill:RGBA(37, 99, 235, 1)},
{Dept:"Operations", Fill:RGBA(100, 116, 139, 1)},
{Dept:"default", Fill:RGBA(107, 114, 128, 1)}
);
ClearCollect(colApHistory,
{EventTitle:"Approved", EventDescription:"PO-1038 · $870", Author:"You", EventOn:DateAdd(Now(), -2, TimeUnit.Days), Category:"Approved", HasSubDetail:false, SubTo:"", SubCC:"", SubPriority:"", SubMessage:""},
{EventTitle:"Returned", EventDescription:"PO-1039 · $4,200", Author:"You", EventOn:DateAdd(Now(), -1, TimeUnit.Days), Category:"Returned", HasSubDetail:true, SubTo:"Lisa Anderson", SubCC:"", SubPriority:"Normal", SubMessage:"Need three quotes and a install date before I can sign this off."},
{EventTitle:"Rejected", EventDescription:"EXP-310 · $2,050", Author:"You", EventOn:DateAdd(Now(), -3, TimeUnit.Days), Category:"Rejected", HasSubDetail:true, SubTo:"Emily Parker", SubCC:"", SubPriority:"Normal", SubMessage:"Over budget for the quarter, resubmit next period."}
);
Set(gblApSeedVersion, "2026-07-27")
);
// ---- screen state ----
If(IsBlank(gblApFilter), Set(gblApFilter, nfApOpenKey));
Set(gblApActionID, "");
Set(gblApAction, "");
Set(gblApActiveTab, 3);
UpdateIf(colApRequests, Selected, {Selected: false});
// ---- component-owned globals (contract, not renameable) ----
Set(_cmpNavIsExpanded, true);
Set(_cmpNavSelID, 3);
Set(_auditFilter, Blank());
UpdateContext({locApSheetOpen: false})
Children:
- cntRootAp:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
Fill: =RGBA(249, 250, 251, 1)
Height: =Parent.Height
LayoutDirection: =LayoutDirection.Vertical
PaddingLeft: =If(App.Width >= 1024, If(_cmpNavIsExpanded, 224, 52), 0)
Width: =Parent.Width
Children:
- cntMiddleAp:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
FillPortions: =0
Height: =Parent.Height
LayoutDirection: =LayoutDirection.Horizontal
Width: =Parent.Width
Children:
- cntRightStackAp:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
Height: =Parent.Height
LayoutAlignItems: =LayoutAlignItems.Stretch
LayoutDirection: =LayoutDirection.Vertical
LayoutMinHeight: =0
Children:
- conTopBarAp:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
Fill: =RGBA(255, 255, 255, 1)
FillPortions: =0
Height: =If(App.Width < 768, 120, 66)
LayoutAlignItems: =If(App.Width < 768, LayoutAlignItems.Stretch, LayoutAlignItems.Center)
LayoutDirection: =If(App.Width < 768, LayoutDirection.Vertical, LayoutDirection.Horizontal)
LayoutGap: =If(App.Width < 768, 8, 12)
LayoutJustifyContent: =LayoutJustifyContent.SpaceBetween
LayoutMinHeight: =0
PaddingBottom: =If(App.Width < 768, 10, 0)
PaddingLeft: =If(App.Width < 640, 14, 22)
PaddingRight: =If(App.Width < 640, 14, 22)
PaddingTop: =If(App.Width < 768, 14, 0)
Children:
- conApTitleGroup:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
Height: =If(App.Width < 768, 44, Parent.Height)
LayoutDirection: =LayoutDirection.Vertical
LayoutGap: =2
LayoutJustifyContent: =LayoutJustifyContent.Center
LayoutMinHeight: =0
LayoutMinWidth: =0
Children:
- lblApTitle:
Control: ModernText@1.0.0
Properties:
AlignInContainer: =AlignInContainer.Stretch
AutoHeight: =true
Color: =RGBA(17, 24, 39, 1)
FontWeight: =FontWeight.Bold
LayoutMinHeight: =0
LayoutMinWidth: =0
Size: =18
Text: ="Approvals"
- lblApSubtitle:
Control: ModernText@1.0.0
Properties:
AlignInContainer: =AlignInContainer.Stretch
AutoHeight: =true
Color: =RGBA(107, 114, 128, 1)
LayoutMinHeight: =0
LayoutMinWidth: =0
Size: =12
Text: ="Purchase orders and expenses awaiting your review"
Visible: =App.Width >= 640
- conApTopActions:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
Height: =If(App.Width < 768, 40, Parent.Height)
LayoutAlignItems: =LayoutAlignItems.Center
LayoutDirection: =LayoutDirection.Horizontal
LayoutGap: =8
LayoutJustifyContent: =LayoutJustifyContent.End
LayoutMinHeight: =0
LayoutMinWidth: =0
Children:
- cmpApTabs:
Control: CanvasComponent
ComponentName: cmpSegmentedControl
Properties:
AlignInContainer: =AlignInContainer.Center
DefaultValue: =nfApOpenKey
Disabled: =false
DisplayMode: ="Text"
EqualWidth: =true
FillPortions: =1
Height: =36
IconPosition: ="Left"
LayoutMaxWidth: =370
LayoutMinHeight: =0
LayoutMinWidth: =0
OnChange: |-
=Set(gblApFilter, cmpApTabs.SelectedValue);
Set(gblApActionID, "");
Set(gblApAction, "")
Options: =nfApTabOptions
Size: ="Small"
Theme: ="Light"
Width: =200
- btnApHistory:
Control: Button@0.0.45
Properties:
AlignInContainer: =AlignInContainer.Center
Appearance: ='ButtonCanvas.Appearance'.Secondary
Height: =36
Icon: ="History"
LayoutMinHeight: =0
LayoutMinWidth: =0
OnSelect: |-
=UpdateContext({locApSheetOpen: true})
Text: =""
Visible: =App.Width < 1024
Width: =40
- shpApTopDivider:
Control: Rectangle@2.3.0
Properties:
AlignInContainer: =AlignInContainer.Stretch
BorderStyle: =BorderStyle.None
Fill: =RGBA(229, 231, 235, 1)
Height: =1
LayoutMinHeight: =1
- conPageAp:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
LayoutDirection: =LayoutDirection.Vertical
LayoutGap: =14
LayoutMinHeight: =0
LayoutOverflowY: =LayoutOverflow.Scroll
PaddingBottom: =16
PaddingLeft: =If(App.Width < 640, 14, 22)
PaddingRight: =If(App.Width < 640, 14, 22)
PaddingTop: =16
Children:
- conApStats:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
FillPortions: =0
Height: =cmpApKpi.Height
LayoutDirection: =LayoutDirection.Vertical
LayoutMinHeight: =0
Children:
- cmpApKpi:
Control: CanvasComponent
ComponentName: cmpKPI
Properties:
AlignInContainer: =AlignInContainer.Stretch
AnimationEffect: ="Pop"
ColumnsLayout: |-
={
Mobile: 2,
Tablet: 3,
Desktop: 3,
MobileBreakpoint: IfError(Index(App.SizeBreakpoints, 1).Value, 640),
TabletBreakpoint: IfError(Index(App.SizeBreakpoints, 2).Value, 1024),
DesktopBreakpoint: IfError(Index(App.SizeBreakpoints, 3).Value, 1366)
}
Data: |-
=Table(
{ID: 1, Icon: "Clock", Value: nfApPendingCount, Label: "Pending review", PercentChange: 0, PercentLabel: "", IconBg: "#EEF2FF", IconColor: "#4F46E5", SparklineData: "", isHidden: false},
{ID: 2, Icon: "Dollar", Value: nfApPendingValue, Label: "Pending value", PercentChange: 0, PercentLabel: "", IconBg: "#FEF3C7", IconColor: "#B45309", SparklineData: "", isHidden: false},
{ID: 3, Icon: "Target", Value: nfApApprovedThisMonth, Label: "Approved this month", PercentChange: 0, PercentLabel: "", IconBg: "#DCFCE7", IconColor: "#16A34A", SparklineData: "", isHidden: App.Width < 640}
)
Height: |-
=With(
{
_cols: If(App.Width < 640, 2, 3),
_cards: CountRows(Filter(cmpApKpi.Data, !IfError(isHidden, false)))
},
RoundUp(_cards / _cols, 0) * (96 + 12) + 12
)
Icons: |-
=Table(
{Name: "Clock", SVG: "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='COLOR' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><polyline points='12 6 12 12 16 14'/></svg>"},
{Name: "Dollar", SVG: "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='COLOR' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><line x1='12' y1='1' x2='12' y2='23'/><path d='M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6'/></svg>"},
{Name: "Target", SVG: "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='COLOR' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><circle cx='12' cy='12' r='6'/><circle cx='12' cy='12' r='2'/></svg>"}
)
IsLoading: =IsEmpty(colApRequests)
LayoutMinHeight: =0
LayoutMinWidth: =0
OnCardClick: |-
=Switch(
ClickedItem.ID,
1, Set(gblApFilter, nfApOpenKey),
3, Set(gblApFilter, "Approved")
)
Style: ="Minimal"
StyleConfig: |-
={
colors: {
cardBg: ColorValue("#FFFFFF"),
border: ColorValue("#E5E7EB"),
text: ColorValue("#111827"),
textMuted: ColorValue("#6B7280"),
positive: ColorValue("#22C55E"),
negative: ColorValue("#EF4444"),
neutral: ColorValue("#9E9E9E"),
skeletonBase: ColorValue("#E5E7EB"),
skeletonShine: ColorValue("#F3F4F6")
},
space: {xs: 4, sm: 8, md: 12, lg: 16, xl: 24},
radius: {md: 8, lg: 12},
type: {
value: {size: 32, sizeCompact: 22},
label: {size: 11},
body: {size: 12}
},
heights: {statsCardMax: 190},
abbreviateThreshold: 0
}
Width: =Parent.Width
- conApSelBar:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
BorderColor: =RGBA(191, 219, 254, 1)
BorderThickness: =1
DropShadow: =DropShadow.None
Fill: =RGBA(239, 246, 255, 1)
FillPortions: =0
Height: =52
LayoutAlignItems: =LayoutAlignItems.Center
LayoutDirection: =LayoutDirection.Horizontal
LayoutGap: =8
LayoutMinHeight: =0
PaddingLeft: =16
PaddingRight: =12
RadiusBottomLeft: =12
RadiusBottomRight: =12
RadiusTopLeft: =12
RadiusTopRight: =12
Visible: =gblApFilter = nfApOpenKey && nfApSelectedCount > 0
Children:
- lblApSelCount:
Control: ModernText@1.0.0
Properties:
AutoHeight: =true
Color: =RGBA(30, 64, 175, 1)
FillPortions: =1
FontWeight: =FontWeight.Semibold
LayoutMinHeight: =0
LayoutMinWidth: =0
Size: =13
Text: |-
=nfApSelectedCount & " selected · " &
Text(Coalesce(Sum(Filter(colApRequests, Status = nfApOpenKey, Selected), Amount), 0), "[$-en-US]$#,##0")
- btnApSelectAll:
Control: Button@0.0.45
Properties:
Appearance: ='ButtonCanvas.Appearance'.Subtle
Height: =34
LayoutMinHeight: =0
LayoutMinWidth: =0
OnSelect: |-
=UpdateIf(
colApRequests,
Status = nfApOpenKey,
{Selected: nfApSelectedCount < nfApPendingCount}
)
Text: =If(nfApSelectedCount < nfApPendingCount, "Select all", "Clear")
- btnApBulkApprove:
Control: Button@0.0.45
Properties:
Appearance: ='ButtonCanvas.Appearance'.Primary
Height: =34
Icon: ="Checkmark"
LayoutMinHeight: =0
LayoutMinWidth: =0
OnSelect: |-
=// SWAP POINT: replace both statements with a single flow call that
// owns the transition, e.g. flowApproveBatch.Run(JSON(Filter(...)))
ForAll(
Filter(colApRequests, Status = nfApOpenKey, Selected) As _r,
Collect(colApHistory,
{
EventTitle: "Approved",
EventDescription: _r.ReqID & " · " & Text(_r.Amount, "[$-en-US]$#,##0"),
Author: "You",
EventOn: Now(),
Category: "Approved",
HasSubDetail: false,
SubTo: "", SubCC: "", SubPriority: "", SubMessage: ""
}
)
);
UpdateIf(
colApRequests,
Status = nfApOpenKey && Selected,
{
Status: "Approved",
DecidedBy: "You",
DecidedOn: Now(),
Selected: false
}
)
Text: ="Approve " & nfApSelectedCount
Width: =128
- conApGrid:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
LayoutAlignItems: =LayoutAlignItems.Stretch
LayoutDirection: =LayoutDirection.Horizontal
LayoutGap: =14
LayoutMinHeight: =0
Children:
- conApQueue:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
FillPortions: =2
LayoutDirection: =LayoutDirection.Vertical
LayoutMinHeight: =300
LayoutMinWidth: =0
Children:
- galApQueue:
Control: Gallery@2.15.0
Variant: VariableHeight
Properties:
DelayItemLoading: =false
Items: =nfApFiltered
LayoutMinHeight: =0
LoadingSpinner: =LoadingSpinner.None
TemplatePadding: =0
TemplateSize: =210
Visible: =nfApCount > 0
Width: =Parent.Width
Children:
- lblApCardHeight:
Control: Label@2.5.1
Properties:
Height: =1
OnSelect: =Select(Parent)
Text: |-
=shpApFootDiv.Y +
If(Parent.TemplateWidth < 520, 92, 58)
Visible: =false
Width: =1
- conApCard:
Control: GroupContainer@1.5.0
Variant: ManualLayout
Properties:
BorderColor: =RGBA(229, 231, 235, 1)
BorderThickness: =1
DropShadow: =DropShadow.None
Fill: =RGBA(255, 255, 255, 1)
Height: =210
RadiusBottomLeft: =12
RadiusBottomRight: =12
RadiusTopLeft: =12
RadiusTopRight: =12
Width: =Parent.TemplateWidth - 4
X: =1
Children:
- conApSelBox:
Control: GroupContainer@1.5.0
Variant: ManualLayout
Properties:
BorderColor: =If(ThisItem.Selected, RGBA(37, 99, 235, 1), RGBA(203, 213, 225, 1))
BorderThickness: =1.5
DropShadow: =DropShadow.None
Fill: =If(ThisItem.Selected, RGBA(37, 99, 235, 1), RGBA(255, 255, 255, 1))
Height: =20
RadiusBottomLeft: =5
RadiusBottomRight: =5
RadiusTopLeft: =5
RadiusTopRight: =5
Visible: =ThisItem.Status = nfApOpenKey
Width: =20
X: =16
Y: =20
Children:
- imgApCheck:
Control: Image@2.2.3
Properties:
Height: =12
Image: ="data:image/svg+xml," & EncodeUrl("<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>")
Visible: =ThisItem.Selected
Width: =12
X: =4
Y: =4
- btnApSelOverlay:
Control: Classic/Button@2.2.0
Properties:
BorderColor: =RGBA(0, 0, 0, 0)
BorderStyle: =BorderStyle.None
Color: =RGBA(0, 0, 0, 0)
Fill: =RGBA(0, 0, 0, 0)
Height: =32
HoverBorderColor: =RGBA(0, 0, 0, 0)
HoverColor: =RGBA(0, 0, 0, 0)
HoverFill: =RGBA(0, 0, 0, 0.04)
OnSelect: |-
=UpdateIf(
colApRequests,
ReqID = ThisItem.ReqID,
{Selected: !Selected}
)
PressedBorderColor: =RGBA(0, 0, 0, 0)
PressedColor: =RGBA(0, 0, 0, 0)
PressedFill: =RGBA(0, 0, 0, 0.08)
RadiusBottomLeft: =16
RadiusBottomRight: =16
RadiusTopLeft: =16
RadiusTopRight: =16
Text: =""
Visible: =ThisItem.Status = nfApOpenKey
Width: =32
X: =10
Y: =14
- conApAvatar:
Control: GroupContainer@1.5.0
Variant: ManualLayout
Properties:
DropShadow: =DropShadow.None
Fill: |-
=Coalesce(
LookUp(colApDept As _d, _d.Dept = ThisItem.Dept, _d.Fill),
LookUp(colApDept As _d, _d.Dept = "default", _d.Fill)
)
Height: =40
RadiusBottomLeft: =20
RadiusBottomRight: =20
RadiusTopLeft: =20
RadiusTopRight: =20
Width: =40
X: =If(ThisItem.Status = nfApOpenKey, 48, 16)
Y: =14
Children:
- lblApInitials:
Control: Label@2.5.1
Properties:
Align: =Align.Center
Color: =RGBA(255, 255, 255, 1)
Font: =Font.'Segoe UI'
FontWeight: =FontWeight.Bold
Size: =12
Text: |-
=Upper(
Concat(
FirstN(Split(ThisItem.Requester, " "), 2),
Left(Value, 1)
)
)
Width: =40
- lblApReqName:
Control: Label@2.5.1
Properties:
Color: =RGBA(17, 24, 39, 1)
Font: =Font.'Segoe UI'
FontWeight: =FontWeight.Semibold
Height: =20
PaddingLeft: =0
Size: =12
Text: =ThisItem.Requester
Width: =Parent.Width - conApAvatar.X - 160
Wrap: =false
X: =conApAvatar.X + 52
Y: =15
- lblApReqDept:
Control: Label@2.5.1
Properties:
Color: =RGBA(107, 114, 128, 1)
Font: =Font.'Segoe UI'
Height: =18
PaddingLeft: =0
Size: =11
Text: =ThisItem.Dept
Width: =Parent.Width - conApAvatar.X - 160
Wrap: =false
X: =conApAvatar.X + 52
Y: =35
- lblApAmount:
Control: Label@2.5.1
Properties:
Align: =Align.Right
Color: =RGBA(17, 24, 39, 1)
Font: =Font.'Segoe UI'
FontWeight: =FontWeight.Bold
Height: =28
PaddingRight: =0
Size: =17
Text: =Text(ThisItem.Amount, "[$-en-US]$#,##0")
Width: =120
Wrap: =false
X: =Parent.Width - 136
Y: =20
- lblApCardTitle:
Control: Label@2.5.1
Properties:
AutoHeight: =true
Color: =RGBA(17, 24, 39, 1)
Font: =Font.'Segoe UI'
FontWeight: =FontWeight.Semibold
Height: =22
PaddingBottom: =0
PaddingLeft: =0
PaddingRight: =0
PaddingTop: =0
Text: =ThisItem.Title
VerticalAlign: =VerticalAlign.Top
Width: =Parent.Width - 32
X: =16
Y: =66
- conApChip:
Control: GroupContainer@1.5.0
Variant: ManualLayout
Properties:
DropShadow: =DropShadow.None
Fill: =RGBA(243, 244, 246, 1)
Height: =22
RadiusBottomLeft: =11
RadiusBottomRight: =11
RadiusTopLeft: =11
RadiusTopRight: =11
Width: =Len(ThisItem.Category) * 6.4 + 20
X: =16
Y: =lblApCardTitle.Y + lblApCardTitle.Height + 8
Children:
- lblApChipText:
Control: Label@2.5.1
Properties:
Align: =Align.Center
Color: =RGBA(75, 85, 99, 1)
Font: =Font.'Segoe UI'
FontWeight: =FontWeight.Semibold
Height: =22
PaddingLeft: =0
PaddingRight: =0
Size: =10
Text: =ThisItem.Category
Width: =Parent.Width
Wrap: =false
- lblApReqCode:
Control: Label@2.5.1
Properties:
Color: =RGBA(107, 114, 128, 1)
Font: =Font.'Segoe UI'
Height: =22
PaddingLeft: =0
Size: =11
Text: =ThisItem.ReqID
Width: =92
Wrap: =false
X: =conApChip.X + conApChip.Width + 10
Y: =conApChip.Y
- conApWaitPill:
Control: GroupContainer@1.5.0
Variant: ManualLayout
Properties:
DropShadow: =DropShadow.None
Fill: |-
=Coalesce(
LookUp(colApStatus As _s, _s.StatusKey = ThisItem.Status, _s.PillBg),
RGBA(243, 244, 246, 1)
)
Height: =22
RadiusBottomLeft: =11
RadiusBottomRight: =11
RadiusTopLeft: =11
RadiusTopRight: =11
Visible: =ThisItem.Status = nfApOpenKey
Width: =66
X: =lblApReqCode.X + 100
Y: =conApChip.Y
Children:
- lblApWaitPill:
Control: Label@2.5.1
Properties:
Align: =Align.Center
Color: |-
=Coalesce(
LookUp(colApStatus As _s, _s.StatusKey = ThisItem.Status, _s.PillText),
RGBA(75, 85, 99, 1)
)
Font: =Font.'Segoe UI'
FontWeight: =FontWeight.Bold
Height: =22
PaddingLeft: =0
PaddingRight: =0
Size: =10
Text: =ThisItem.Status
Width: =66
- lblApJust:
Control: Label@2.5.1
Properties:
AutoHeight: =true
Color: =RGBA(75, 85, 99, 1)
Font: =Font.'Segoe UI'
Height: =20
PaddingBottom: =0
PaddingLeft: =0
PaddingRight: =0
PaddingTop: =0
Size: =11
Text: =ThisItem.Justification
VerticalAlign: =VerticalAlign.Top
Width: =Parent.Width - 32
X: =16
Y: =conApChip.Y + 30
- shpApFootDiv:
Control: Rectangle@2.3.0
Properties:
BorderStyle: =BorderStyle.None
Fill: =RGBA(243, 244, 246, 1)
Height: =1
Width: =Parent.Width - 32
X: =16
Y: =lblApJust.Y + lblApJust.Height + 12
- lblApSubmitted:
Control: Label@2.5.1
Properties:
Color: =RGBA(107, 114, 128, 1)
Font: =Font.'Segoe UI'
Height: =26
PaddingLeft: =0
Size: =11
Text: |-
=With(
{_d: DateDiff(ThisItem.SubmittedOn, Today(), TimeUnit.Days)},
Switch(
true,
_d <= 0, "Submitted today",
_d = 1, "Submitted 1 day ago",
"Submitted " & _d & " days ago"
)
)
Visible: =ThisItem.Status = nfApOpenKey
Width: =Parent.Width - 32
Wrap: =false
X: =16
Y: =shpApFootDiv.Y + If(Parent.Width < 520, 8, 12)
- btnApSendBack:
Control: Button@0.0.45
Properties:
Appearance: ='ButtonCanvas.Appearance'.Subtle
FontColor: =RGBA(55, 48, 163, 1)
Height: =34
OnSelect: |-
=Set(gblApActionID, ThisItem.ReqID);
Set(gblApAction, "Return")
Text: ="Send back"
Visible: =ThisItem.Status = nfApOpenKey
Width: =If(Parent.Width < 520, (Parent.Width - 48) / 3, 100)
X: =If(Parent.Width < 520, 16, Parent.Width - 318)
Y: =shpApFootDiv.Y + If(Parent.Width < 520, 42, 12)
- btnApReject:
Control: Button@0.0.45
Properties:
Appearance: ='ButtonCanvas.Appearance'.Secondary
FontColor: =RGBA(220, 38, 38, 1)
Height: =34
OnSelect: |-
=Set(gblApActionID, ThisItem.ReqID);
Set(gblApAction, "Reject")
Text: ="Reject"
Visible: =ThisItem.Status = nfApOpenKey
Width: =If(Parent.Width < 520, (Parent.Width - 48) / 3, 94)
X: =If(Parent.Width < 520, 24 + (Parent.Width - 48) / 3, Parent.Width - 212)
Y: =shpApFootDiv.Y + If(Parent.Width < 520, 42, 12)
- btnApApprove:
Control: Button@0.0.45
Properties:
Appearance: ='ButtonCanvas.Appearance'.Primary
Height: =34
OnSelect: |-
=// SWAP POINT: replace with a flow call that owns the transition
// and appends history, e.g. flowApprove.Run(ThisItem.ReqID)
Collect(colApHistory,
{
EventTitle: "Approved",
EventDescription: ThisItem.ReqID & " · " & Text(ThisItem.Amount, "[$-en-US]$#,##0"),
Author: "You",
EventOn: Now(),
Category: "Approved",
HasSubDetail: false,
SubTo: "", SubCC: "", SubPriority: "", SubMessage: ""
}
);
UpdateIf(
colApRequests,
ReqID = ThisItem.ReqID,
{
Status: "Approved",
DecidedBy: "You",
DecidedOn: Now(),
Selected: false
}
)
Text: ="Approve"
Visible: =ThisItem.Status = nfApOpenKey
Width: =If(Parent.Width < 520, (Parent.Width - 48) / 3, 98)
X: =If(Parent.Width < 520, 32 + 2 * ((Parent.Width - 48) / 3), Parent.Width - 112)
Y: =shpApFootDiv.Y + If(Parent.Width < 520, 42, 12)
- conApDecidedPill:
Control: GroupContainer@1.5.0
Variant: ManualLayout
Properties:
DropShadow: =DropShadow.None
Fill: |-
=Coalesce(
LookUp(colApStatus As _s, _s.StatusKey = ThisItem.Status, _s.PillBg),
RGBA(243, 244, 246, 1)
)
Height: =24
RadiusBottomLeft: =12
RadiusBottomRight: =12
RadiusTopLeft: =12
RadiusTopRight: =12
Visible: =ThisItem.Status <> nfApOpenKey
Width: =78
X: =16
Y: =shpApFootDiv.Y + 17
Children:
- lblApDecidedPill:
Control: Label@2.5.1
Properties:
Align: =Align.Center
Color: |-
=Coalesce(
LookUp(colApStatus As _s, _s.StatusKey = ThisItem.Status, _s.PillText),
RGBA(75, 85, 99, 1)
)
Font: =Font.'Segoe UI'
FontWeight: =FontWeight.Bold
Height: =24
PaddingLeft: =0
PaddingRight: =0
Size: =10
Text: =ThisItem.Status
Width: =78
- lblApDecidedBy:
Control: Label@2.5.1
Properties:
AutoHeight: =true
Color: =RGBA(75, 85, 99, 1)
Font: =Font.'Segoe UI'
Height: =20
PaddingBottom: =0
PaddingLeft: =0
PaddingRight: =0
PaddingTop: =0
Size: =11
Text: |-
="by " & ThisItem.DecidedBy & " · " &
Text(ThisItem.DecidedOn, "[$-en-US]mmm d, yyyy") &
If(
IsBlank(ThisItem.DecisionNote),
"",
Char(10) & "Note: " & ThisItem.DecisionNote
)
VerticalAlign: =VerticalAlign.Top
Visible: =ThisItem.Status <> nfApOpenKey
Width: =Parent.Width - 122
X: =102
Y: =shpApFootDiv.Y + 18
- conApEmpty:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
BorderColor: =RGBA(229, 231, 235, 1)
BorderThickness: =1
DropShadow: =DropShadow.None
Fill: =RGBA(255, 255, 255, 1)
LayoutAlignItems: =LayoutAlignItems.Center
LayoutDirection: =LayoutDirection.Vertical
LayoutGap: =6
LayoutJustifyContent: =LayoutJustifyContent.Center
LayoutMinHeight: =0
RadiusBottomLeft: =12
RadiusBottomRight: =12
RadiusTopLeft: =12
RadiusTopRight: =12
Visible: =nfApCount = 0
Children:
- lblApEmptyHead:
Control: ModernText@1.0.0
Properties:
AutoHeight: =true
Color: =RGBA(17, 24, 39, 1)
FontWeight: =FontWeight.Semibold
LayoutMinHeight: =0
LayoutMinWidth: =0
Size: =14
Text: ="Nothing " & Lower(gblApFilter) & " right now"
- lblApEmptyBody:
Control: ModernText@1.0.0
Properties:
Align: =Align.Center
AutoHeight: =true
Color: =RGBA(107, 114, 128, 1)
LayoutMinHeight: =0
LayoutMinWidth: =0
Size: =12
Text: ="Decisions you make land in the activity trail on the right."
- conApActivity:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
BorderColor: =RGBA(229, 231, 235, 1)
BorderThickness: =1
DropShadow: =DropShadow.None
Fill: =RGBA(255, 255, 255, 1)
LayoutDirection: =LayoutDirection.Vertical
LayoutMinHeight: =0
LayoutMinWidth: =0
PaddingBottom: =12
PaddingLeft: =18
PaddingRight: =6
PaddingTop: =16
RadiusBottomLeft: =12
RadiusBottomRight: =12
RadiusTopLeft: =12
RadiusTopRight: =12
Visible: =App.Width >= 1024
Children:
- lblApActivityHead:
Control: ModernText@1.0.0
Properties:
AlignInContainer: =AlignInContainer.Stretch
AutoHeight: =true
Color: =RGBA(17, 24, 39, 1)
FontWeight: =FontWeight.Bold
LayoutMinHeight: =0
LayoutMinWidth: =0
Size: =14
Text: ="Activity"
- lblApActivitySub:
Control: ModernText@1.0.0
Properties:
AlignInContainer: =AlignInContainer.Stretch
AutoHeight: =true
Color: =RGBA(107, 114, 128, 1)
LayoutMinHeight: =0
LayoutMinWidth: =0
Size: =11
Text: ="Audit trail of decisions"
- cmpApTimeline:
Control: CanvasComponent
ComponentName: cmpActivityTimeline
Properties:
AlignInContainer: =AlignInContainer.Stretch
CardHeight: =96
FillPortions: =1
FilterOptions: |-
=Table({Value: "All Activities"})
Height: =Parent.Height - 70
HideHeader: =true
IconMap: =nfApTimelineIcons
IsLoading: =false
Items: =nfApHistory
LayoutMinHeight: =0
LayoutMinWidth: =0
OnExport: =false
OnItemSelect: =false
OnPrint: =false
ShowScrollbar: =true
SubDetailCardHeight: =160
Theme: ="Light"
ThemeConfig: =nfApTimelineTheme
Width: =Parent.Width - 24
- cntBottomBarAp:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
Fill: =RGBA(255, 255, 255, 1)
FillPortions: =0
Height: =If(App.Width < 640, 58, 66)
LayoutAlignItems: =LayoutAlignItems.Center
LayoutDirection: =LayoutDirection.Vertical
LayoutMaxHeight: =If(App.Width < 640, 58, 66)
LayoutMinHeight: =If(App.Width < 640, 58, 66)
PaddingBottom: =4
PaddingTop: =4
Visible: =App.Width < 1024
Children:
- cmpApBottomNav:
Control: CanvasComponent
ComponentName: cmpBottomNavigation
Properties:
ActiveColor: =RGBA(37, 99, 235, 1)
ActiveColorHex: ="2563EB"
ActiveIndicator: ="underline"
BadgeColor: =RGBA(220, 38, 38, 1)
Fill: =RGBA(255, 255, 255, 1)
Height: =Parent.Height - 8
InactiveColorHex: ="9CA3AF"
Items: |-
=Table(
{Index: 1, Icon: "dashboard", Label: "Dashboard", Badge: 0},
{Index: 2, Icon: "analytics", Label: "Items", Badge: 0},
{Index: 3, Icon: "folder", Label: "Approvals", Badge: nfApPendingCount},
{Index: 4, Icon: "user", Label: "Account", Badge: 0}
)
OnSelect: |-
=Set(gblApActiveTab, cmpApBottomNav.SelectedItemIndex);
Switch(
cmpApBottomNav.SelectedItemIndex,
1, Notify("Navigate to Dashboard", NotificationType.Information),
2, Notify("Navigate to Items", NotificationType.Information),
3, false,
4, Notify("Navigate to Account", NotificationType.Information)
)
SelectedIndex: =gblApActiveTab
ShowLabels: ="always"
Theme: ="Light"
Width: =Parent.Width
- cntLeftNavHostAp:
Control: GroupContainer@1.5.0
Variant: ManualLayout
Properties:
DropShadow: =DropShadow.None
Fill: =RGBA(13, 17, 23, 1)
Height: =Parent.Height
Visible: =App.Width >= 1024
Width: =If(_cmpNavIsExpanded, 224, 52)
Children:
- cmpApSidebar:
Control: CanvasComponent
ComponentName: cmpSidebar
Properties:
AccentColor: =RGBA(37, 99, 235, 1)
AppLogo: =Blank()
AppName: ="Finance"
AppNameAccent: ="Ops"
Expanded: =true
Height: =App.Height
Icons: |-
=Table(
{Name: "Dashboard", SVG: "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='7' height='7' rx='1'/><rect x='14' y='3' width='7' height='7' rx='1'/><rect x='3' y='14' width='7' height='7' rx='1'/><rect x='14' y='14' width='7' height='7' rx='1'/></svg>"},
{Name: "List", SVG: "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><line x1='3' y1='6' x2='21' y2='6'/><line x1='3' y1='12' x2='21' y2='12'/><line x1='3' y1='18' x2='21' y2='18'/></svg>"},
{Name: "Check", SVG: "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M22 11.08V12a10 10 0 1 1-5.93-9.14'/><polyline points='22 4 12 14.01 9 11.01'/></svg>"},
{Name: "Receipt", SVG: "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 2v20l3-2 3 2 3-2 3 2 3-2V2l-3 2-3-2-3 2-3-2z'/><line x1='8' y1='9' x2='16' y2='9'/><line x1='8' y1='14' x2='14' y2='14'/></svg>"},
{Name: "Chart", SVG: "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><line x1='18' y1='20' x2='18' y2='10'/><line x1='12' y1='20' x2='12' y2='4'/><line x1='6' y1='20' x2='6' y2='14'/></svg>"},
{Name: "Settings", SVG: "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='3'/><path d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'/></svg>"}
)
Items: |-
=Table(
{ID: 100, Title: "OVERVIEW", Icon: "", Letter: "", Badge: 0, Visible: true, BadgeColor: RGBA(0, 0, 0, 0), BadgeDot: false, IsSection: true, ParentID: -1},
{ID: 1, Title: "Dashboard", Icon: "Dashboard", Letter: "", Badge: 0, Visible: true, BadgeColor: RGBA(0, 0, 0, 0), BadgeDot: false, IsSection: false, ParentID: -1},
{ID: 2, Title: "Requests", Icon: "List", Letter: "", Badge: 0, Visible: true, BadgeColor: RGBA(0, 0, 0, 0), BadgeDot: false, IsSection: false, ParentID: -1},
{ID: 101, Title: "REVIEW", Icon: "", Letter: "", Badge: 0, Visible: true, BadgeColor: RGBA(0, 0, 0, 0), BadgeDot: false, IsSection: true, ParentID: -1},
{ID: 3, Title: "Approvals", Icon: "Check", Letter: "", Badge: nfApPendingCount, Visible: true, BadgeColor: RGBA(220, 38, 38, 1), BadgeDot: false, IsSection: false, ParentID: -1},
{ID: 4, Title: "Expenses", Icon: "Receipt", Letter: "", Badge: 0, Visible: true, BadgeColor: RGBA(0, 0, 0, 0), BadgeDot: false, IsSection: false, ParentID: -1},
{ID: 102, Title: "REPORTS", Icon: "", Letter: "", Badge: 0, Visible: true, BadgeColor: RGBA(0, 0, 0, 0), BadgeDot: false, IsSection: true, ParentID: -1},
{ID: 5, Title: "Spend", Icon: "Chart", Letter: "", Badge: 0, Visible: true, BadgeColor: RGBA(0, 0, 0, 0), BadgeDot: false, IsSection: false, ParentID: -1},
{ID: 6, Title: "Settings", Icon: "Settings", Letter: "", Badge: 0, Visible: true, BadgeColor: RGBA(0, 0, 0, 0), BadgeDot: false, IsSection: false, ParentID: -1}
)
OnFooterSettings: =Notify("Settings coming soon", NotificationType.Information)
OnHelp: =Notify("Help coming soon", NotificationType.Information)
OnNavSelect: |-
=Switch(
cmpApSidebar.SelectedID,
1, Notify("Navigate to Dashboard", NotificationType.Information),
2, Notify("Requests coming soon", NotificationType.Information),
3, false,
4, Notify("Expenses coming soon", NotificationType.Information),
5, Notify("Spend reports coming soon", NotificationType.Information),
6, Notify("Settings coming soon", NotificationType.Information)
)
OnThemeToggle: =false
OnToggle: =false
ShowFooterMenu: =true
ShowHeader: =true
ShowSearch: =false
ShowToggle: =true
ShowUser: =true
Theme: ="Dark"
Width: =If(_cmpNavIsExpanded, 224, 52)
- htmApSheetScrim:
Control: HtmlViewer@2.1.0
Properties:
Font: =Font.'Segoe UI'
Height: =Parent.Height
HtmlText: ="<div style='display:block;width:" & Self.Width & "px;height:" & Self.Height - 1 & "px;background:rgba(15,23,42,0.42);backdrop-filter:blur(3px)'></div>"
OnSelect: |-
=UpdateContext({locApSheetOpen: false})
PaddingBottom: =0
PaddingLeft: =0
PaddingRight: =0
PaddingTop: =0
Visible: =locApSheetOpen && App.Width < 1024
Width: =Parent.Width
- conApSheet:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.Bold
Fill: =RGBA(255, 255, 255, 1)
Height: =Min(Parent.Height * 0.86, 620)
LayoutDirection: =LayoutDirection.Vertical
RadiusTopLeft: =18
RadiusTopRight: =18
Visible: =locApSheetOpen && App.Width < 1024
Width: =Parent.Width
Y: =Parent.Height - Self.Height
Children:
- conApSheetHandleRow:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
FillPortions: =0
Height: =18
LayoutAlignItems: =LayoutAlignItems.Center
LayoutDirection: =LayoutDirection.Vertical
LayoutJustifyContent: =LayoutJustifyContent.Center
LayoutMinHeight: =0
Children:
- shpApSheetHandle:
Control: Rectangle@2.3.0
Properties:
BorderStyle: =BorderStyle.None
Fill: =RGBA(209, 213, 219, 1)
Height: =4
LayoutMinHeight: =4
LayoutMinWidth: =38
Width: =38
- conApSheetHead:
Control: GroupContainer@1.5.0
Variant: AutoLayout
Properties:
DropShadow: =DropShadow.None
FillPortions: =0
Height: =44
LayoutAlignItems: =LayoutAlignItems.Center
LayoutDirection: =LayoutDirection.Horizontal
LayoutGap: =8
LayoutMinHeight: =0
PaddingLeft: =16
PaddingRight: =12
Children:
- lblApSheetTitle:
Control: ModernText@1.0.0
Properties:
AutoHeight: =true
Color: =RGBA(17, 24, 39, 1)
FillPortions: =1
FontWeight: =FontWeight.Bold
LayoutMinHeight: =0
LayoutMinWidth: =0
Size: =14
Text: ="Activity"
- btnApSheetClose:
Control: Button@0.0.45
Properties:
Appearance: ='ButtonCanvas.Appearance'.Subtle
Height: =30
Icon: ="Dismiss"
LayoutMinHeight: =0
LayoutMinWidth: =0
OnSelect: |-
=UpdateContext({locApSheetOpen: false})
Text: =""
Width: =32
- shpApSheetDivider:
Control: Rectangle@2.3.0
Properties:
AlignInContainer: =AlignInContainer.Stretch
BorderStyle: =BorderStyle.None
Fill: =RGBA(229, 231, 235, 1)
Height: =1
LayoutMinHeight: =1
- cmpApSheetTimeline:
Control: CanvasComponent
ComponentName: cmpActivityTimeline
Properties:
AlignInContainer: =AlignInContainer.Stretch
CardHeight: =96
FillPortions: =1
FilterOptions: |-
=Table({Value: "All Activities"})
Height: =Parent.Height - 70
HideHeader: =true
IconMap: =nfApTimelineIcons
IsLoading: =false
Items: =nfApHistory
LayoutMinHeight: =0
LayoutMinWidth: =0
OnExport: =false
OnItemSelect: =false
OnPrint: =false
ShowScrollbar: =true
SubDetailCardHeight: =160
Theme: ="Light"
ThemeConfig: =nfApTimelineTheme
Width: =Parent.Width - 24
- cmpApActionDialog:
Control: CanvasComponent
ComponentName: cmpEnhancedDialog
Properties:
BorderRadius: =12
Buttons: |-
=Table(
{Label: "Cancel", ButtonType: "Secondary", Action: "Cancel", Visible: true},
{
Label: If(gblApAction = "Reject", "Reject", "Send back"),
ButtonType: "Primary",
Action: "Commit",
Visible: true
}
)
DialogHeight: =0
DialogType: ="Form"
DialogWidth: =520
HeaderText: |-
=If(gblApAction = "Reject", "Reject ", "Send back ") &
Coalesce(nfApActionRecord.ReqID, "request")
Height: =Parent.Height
IconName: =If(gblApAction = "Reject", "Warning", "Info")
IsTextInputRequired: =true
MessageText: |-
=nfApActionRecord.Title & " · " &
Text(nfApActionRecord.Amount, "[$-en-US]$#,##0") & " · requested by " &
nfApActionRecord.Requester & ". " &
If(
gblApAction = "Reject",
"This closes the request. The reason below is written to the audit trail.",
"This returns the request to the requester for revision. Explain what needs to change."
)
OnButtonSelect: |-
=If(
cmpApActionDialog.SelectedButton.Action = "Commit",
// SWAP POINT: replace both statements with a flow call that owns the
// transition, e.g. flowDecide.Run(gblApActionID, nfApActionTarget, cmpApActionDialog.InputText)
Collect(colApHistory,
{
EventTitle: nfApActionTarget,
EventDescription: nfApActionRecord.ReqID & " · " & Text(nfApActionRecord.Amount, "[$-en-US]$#,##0"),
Author: "You",
EventOn: Now(),
Category: nfApActionTarget,
HasSubDetail: true,
SubTo: nfApActionRecord.Requester,
SubCC: "",
SubPriority: "Normal",
SubMessage: cmpApActionDialog.InputText
}
);
UpdateIf(
colApRequests,
ReqID = gblApActionID,
{
Status: nfApActionTarget,
DecidedBy: "You",
DecidedOn: Now(),
DecisionNote: cmpApActionDialog.InputText,
Selected: false
}
)
);
Set(gblApActionID, "");
Set(gblApAction, "")
OnCloseSelect: |-
=Set(gblApActionID, "");
Set(gblApAction, "")
OverlayBlurColor: ="RGBA(15, 23, 42, 0.45)"
PlaceholderText: =If(gblApAction = "Reject", "Reason for rejection", "What needs to change")
ShowCloseIcon: =true
Theme: ="Light"
Visible: =!IsBlank(gblApActionID)
Width: =Parent.WidthHow It Works
1Statuses are data, not code
Every status decision routes through colApStatus. IsOpen marks the one status the approver still acts on, and nfApOpenKey reads it. Adding a fifth outcome is a new data row — the queue, tabs, and counts all follow.
nfApOpenKey = LookUp(colApStatus, IsOpen, StatusKey)2Tabs are a projection of the status config
nfApTabOptions maps colApStatus (in Order) to Segmented Control options, with the live count carried inside each label since the component has no Badge column. Selecting a tab sets gblApFilter, which nfApFiltered reads.
nfApTabOptions = ForAll(Sort(colApStatus, Order),
{ Value: StatusKey,
Label: Label & " (" & CountRows(Filter(colApRequests, Status = StatusKey)) & ")" })3The dialog looks its record back up by ID
cmpEnhancedDialog lives outside the gallery, so ThisItem isn't in scope. gblApActionID holds the request's ReqID and nfApActionRecord rehydrates it. nfApActionTarget maps the action to the status it commits to.
nfApActionRecord = LookUp(colApRequests, ReqID = gblApActionID)
nfApActionTarget = If(gblApAction = "Reject", "Rejected", "Returned")4History stores a real datetime, then derives the label
colApHistory keeps an EventOn datetime (never a display string) so entries stay sortable as Collect() appends them. nfApHistory derives DisplayDateTime so seeded and freshly-written rows read identically instead of mixing “2 days ago” with a timestamp.
DisplayDateTime: Switch(true,
_m < 1, "just now",
_m < 60, _m & " min ago", …)5The value KPI guards the aggregate
Sum() over an empty table returns blank, so nfApPendingValue wraps it in If(IsEmpty(_p), 0, …). Sum isn't delegable — fine for the seeded collection this ships as; precompute with Power Automate against a real list.
nfApPendingValue = If(IsEmpty(_p), 0, Sum(_p, Amount))Customization
Connect your data
Replace the ClearCollect(colApRequests, Table(…)) seed in the screen's OnVisible with your SharePoint list or Dataverse table. Keep the Status, Amount, SubmittedOn, and DecidedOn fields the formulas rely on.
Add an approval outcome
Add a row to colApStatus with a StatusKey, Label, Order, and IsOpen flag. A new tab, count, and filter appear automatically — no formula changes needed.
Change the KPIs
The three KPI cards read nfApPendingCount, nfApPendingValue, and nfApApprovedThisMonth. Edit those named formulas to track whatever metric matters to your process.
Require a reason per action
The reject / return path sets IsTextInputRequired on cmpEnhancedDialog so the approver can't commit without a note. Toggle it per action if approvals should be one-tap but rejections must be explained.
Wire real decision history
On commit, Collect() a row into colApHistory with an EventOn of Now(). nfApHistory + nfApTimelineIcons render it in the Activity Timeline — no display formatting to maintain.
Get new components and templates when they drop
One email when something new ships. Unsubscribe anytime.
Community
Use the toolbar to format · or type markdown directly