Created
December 12, 2019 21:12
-
-
Save zoitsa/daa48bef55872c35a579bd40432880f0 to your computer and use it in GitHub Desktop.
EMS - Approval cards and animation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<ems-action-bar title="Approvals" [showNav]="true" [showCreateNew]="false"></ems-action-bar> | |
<StackLayout> | |
<StackLayout tkMainContent #mainView class="segmented-bar"> | |
<SegmentedBar [items]="segmentedBarItems" [selectedIndex]="selectedIndex" (selectedIndexChange)="onSelectedIndexChange($event)" | |
class="m-5" (loaded)="segmentedBarLoaded($event)"> | |
</SegmentedBar> | |
<GridLayout orientation="vertical" [visibility]="selectedIndex === 0 ? 'visible' : 'collapsed'" | |
class="segmentedBarContent" tkExampleTitle tkToggleNavButton> | |
<DockLayout stretchLastChild="true"> | |
<StackLayout id="summary-container" dock="Top"> | |
<StackLayout orientation="horizontal"> | |
<GridLayout class="search" rows="25" columns="auto,8,*" [formGroup]="form"> | |
<Label row="0" col="0" text="" class="ico search-icon" horizontalAlignment="left"></Label> | |
<TextField row="0" col="2" hint="Search" formControlName="searchTerm"></TextField> | |
</GridLayout> | |
</StackLayout> | |
<StackLayout class="summary" orientation="horizontal"> | |
<Label class="total-label"> | |
<FormattedString> | |
<Span class="total" text="Total: "></Span> | |
<Span class="number" [text]="pending?.length" fontAttributes="Bold"></Span> | |
</FormattedString> | |
</Label> | |
<Label> | |
<FormattedString class="total"> | |
<Span class="total" text="Overdue: "></Span> | |
<Span class="number" [text]="filterOverdue()" fontAttributes="Bold"></Span> | |
</FormattedString> | |
</Label> | |
</StackLayout> | |
</StackLayout> | |
<StackLayout class="radlist"> | |
<RadListView *ngIf="pending" [items]="pending" height="100%" #myListView | |
(itemSwipeProgressEnded)="onSwipeCellFinished($event)" | |
(itemSwipeProgressStarted)="onSwipeCellStarted($event)" (itemSwipeProgressChanged)="onCellSwiping($event)" | |
swipeActions="true"> | |
<ng-template tkListItemTemplate let-item="item"> | |
<StackLayout class="expenseGroup"> | |
<CardView class="cardStyle" radius="3" shadowOpacity=".2" shadowRadius="2" elevation="20" ripple="true"> | |
<GridLayout rows="11,auto,auto,37,auto,auto,auto,auto,auto,auto,auto" columns="10,auto,*,auto,10" | |
class="expense"> | |
<Label id="vendor" [text]="item.vendor" row="1" col="1"></Label> | |
<Label id="amount" [text]="item.amount | convertToDollars | currency" row="2" col="1"></Label> | |
<Label [text]="getExpenseTypeIcon(item)" class="ico expense-icon text-right" row="1" rowSpan="2" | |
col="3"></Label> | |
<Label row="4" col="1"> | |
<FormattedString class="additional-info"> | |
<Span [text]="item.type?.name"></Span> | |
<Span text=" • "></Span> | |
<Span [text]="item.isBillable ? 'Billable' : 'Non-Billable'" fontAttributes="Bold"></Span> | |
</FormattedString> | |
</Label> | |
<Label class="additional-info text-right" [text]="item.transactionDate | date:'shortDate'" row="4" | |
col="3"></Label> | |
<StackLayout id="submitted-divider" row="5" col="0" colSpan="5" class="hr-light"></StackLayout> | |
<Label row="6" col="1" id="submitted"> | |
<FormattedString> | |
<Span text="Submitted by "></Span> | |
<Span text="{{ item.submitter.firstName }} {{ item.submitter.lastName }}"></Span> | |
</FormattedString> | |
</Label> | |
<Label id="overdue" row="6" col="3" text="OVERDUE" *ngIf="item.isOverdue"></Label> | |
<StackLayout row="7" col="0" colSpan="5" class="hr-light"></StackLayout> | |
<Image height="387" stretch="aspectFit" [src]="item.image" colSpan="5" row="8" id="receipt" | |
*ngIf="item.image"> | |
</Image> | |
<StackLayout id="no-receipt" colSpan="3" col="1" row="8" *ngIf="!item.image"> | |
<Label class="ico no-receipt-icon" text="" horizontalAlignment="center"></Label> | |
<Label class="no-receipt-text" text="NO RECEIPT ATTACHED" horizontalAlignment="center"></Label> | |
</StackLayout> | |
<StackLayout [ngClass]="!item.image ? 'button-border' : ''" row="9" colSpan="5"> | |
<Button class="detail-btn" text="More Details" (tap)="onViewDetails(item.id)"></Button> | |
</StackLayout> | |
</GridLayout> | |
</CardView> | |
</StackLayout> | |
</ng-template> | |
<!-- start swipe template for list view --> | |
<CardView class="cardStyle" radius="3" shadowOpacity=".15" elevation="4" ripple="true"> | |
<GridLayout class="expenseGroup" *tkListItemSwipeTemplate columns="auto, *, auto" col="0"> | |
<GridLayout col="0" id="approve-view"> | |
<Label #approveIconTarget text="" verticalAlignment="center" horizontalAlignment="center" class="ico"></Label> | |
<Label #approveTextTarget opacity="0" text="Approve" verticalAlignment="center" horizontalAlignment="center" | |
class="text-label"></Label> | |
</GridLayout> | |
<GridLayout col="2" id="reject-view"> | |
<Label #rejectIconTarget text="" verticalAlignment="center" horizontalAlignment="center" class="ico"></Label> | |
<Label #rejectTextTarget opacity="0" text="Reject" verticalAlignment="center" horizontalAlignment="center" | |
class="text-label"></Label> | |
</GridLayout> | |
</GridLayout> | |
</CardView> | |
<!-- end swipe template for list view--> | |
</RadListView> | |
</StackLayout> | |
</DockLayout> | |
<StackLayout class="float-btn-container"> | |
<FAB (tap)="onTap($event)" icon="~/assets/camera.png" rippleColor="#229B30" class="fab-button"></FAB> | |
</StackLayout> | |
</GridLayout> | |
<GridLayout orientation="vertical" [visibility]="selectedIndex === 1 ? 'visible' : 'collapsed'" | |
class="segmentedBarContent main-container" tkExampleTitle tkToggleNavButton> | |
<DockLayout stretchLastChild="true"> | |
<StackLayout id="summary-container" dock="Top"> | |
<StackLayout orientation="horizontal"> | |
<GridLayout class="search" rows="25" columns="auto,8,*" [formGroup]="form"> | |
<Label row="0" col="0" text="" class="ico search-icon" horizontalAlignment="left"></Label> | |
<TextField row="0" col="2" hint="Search" formControlName="searchTerm"></TextField> | |
</GridLayout> | |
</StackLayout> | |
<StackLayout class="reviewed-summary" orientation="horizontal"> | |
<Label class="total-label"> | |
<FormattedString> | |
<Span class="total" text="Total: "></Span> | |
<Span class="number" [text]="reviewed?.length" fontAttributes="Bold"></Span> | |
</FormattedString> | |
</Label> | |
</StackLayout> | |
</StackLayout> | |
<RadListView [items]="reviewed" #myListView> | |
<ng-template tkListItemTemplate let-item="item"> | |
<StackLayout class="expenseGroup"> | |
<Label id="date" [text]="item.transactionDate | date"></Label> | |
<CardView class="reviewedCardStyle" elevation="4" radius="3" shadowOpacity=".15"> | |
<GridLayout rows="*,*,*,auto,auto" columns="100,5,*,auto,5"> | |
<Image *ngIf="item.image else icon" id=image-divider col="0" row="0" rowSpan="5" width="100" | |
height="100" stretch="aspectFill" verticalAlignment="stretch" [src]="item.thumbnail"> | |
</Image> | |
<ng-template #icon> | |
<StackLayout col="0" row="0" rowSpan="5" width="100" height="100" verticalAlignment="center" | |
horizontalAlignment="center" id=image-divider> | |
<Image width="40" src="~/assets/cc.png"> | |
</Image> | |
</StackLayout> | |
</ng-template> | |
<Label [text]="item.vendor" id="vendor" col="2" row="0"></Label> | |
<Label id="amount" [text]="item.amount | convertToDollars | currency" row="1" col="2"></Label> | |
<Label row="2" col="2" id="expense-info"> | |
<FormattedString> | |
<Span [text]="item.type?.name"></Span> | |
<Span text=" • "></Span> | |
<Span [text]="item.isBillable ? 'Billable' : 'Non-Billable'"></Span> | |
</FormattedString> | |
</Label> | |
<StackLayout row="3" col="1" colSpan="4" class="hr-light"></StackLayout> | |
<Label row="4" col="2" id="submitted"> | |
<FormattedString> | |
<Span text="Submitted by "></Span> | |
<Span text="{{ item.submitter.firstName }} {{ item.submitter.lastName }}"></Span> | |
</FormattedString> | |
</Label> | |
<Label id="status" row="4" col="3" | |
[text]="item.status === 'denied' ? 'REJECTED' : item.status | uppercase" | |
[ngClass]="item.status === 'denied' ? 'rejected' : 'approved'"></Label> | |
</GridLayout> | |
</CardView> | |
<Label class="shadow-down" row="1" verticalAlignment="top"></Label> | |
</StackLayout> | |
</ng-template> <!-- end ng template for list view--> | |
</RadListView> | |
</DockLayout> | |
<StackLayout class="float-btn-container"> | |
<FAB (tap)="onTap($event)" icon="~/assets/camera.png" rippleColor="#229B30" class="fab-button"></FAB> | |
</StackLayout> | |
</GridLayout> | |
</StackLayout> | |
</StackLayout> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare let UIColor; | |
import { Component, OnInit, Input, OnChanges, SimpleChanges, EventEmitter, Output, ViewChild, ElementRef} from '@angular/core'; | |
import { SegmentedBar, SegmentedBarItem } from 'tns-core-modules/ui/segmented-bar'; | |
import { FormGroup, FormBuilder, FormControl } from '@angular/forms'; | |
import { registerElement } from 'nativescript-angular/element-registry'; | |
import { CardView } from '@nstudio/nativescript-cardview'; | |
import { GestureEventData } from 'tns-core-modules/ui/gestures/gestures'; | |
registerElement('CardView', () => CardView); | |
import { ListViewEventData, SwipeActionsEventData, RadListView, SwipeLimits } from 'nativescript-ui-listview'; | |
import {AnimationCurve} from 'tns-core-modules/ui/enums'; | |
import { View } from 'tns-core-modules/ui/core/view'; | |
import { layout } from 'tns-core-modules/utils/utils'; | |
import { Page } from 'tns-core-modules/ui/page'; | |
import { RadListViewComponent } from 'nativescript-ui-listview/angular'; | |
import * as dialogs from 'tns-core-modules/ui/dialogs'; | |
@Component({ | |
selector: 'ems-approvals-list', | |
templateUrl: './approvals-list.component.html', | |
styleUrls: ['./approvals-list.component.scss'] | |
}) | |
export class ApprovalsListComponent implements OnInit, OnChanges { | |
@Input() pending: Array<any>; | |
@Input() reviewed: Array<any>; | |
@Input() verifyError = ''; | |
@Input() undoVerifyError = ''; | |
@Input() swipeEnabled: boolean; | |
@Input() selectedPageIndex: number; | |
@Output() tap: EventEmitter<GestureEventData> = new EventEmitter<GestureEventData>(); | |
@Output() swipe: EventEmitter<any> = new EventEmitter<any>(); | |
@Output() select: EventEmitter<number> = new EventEmitter<number>(); | |
@ViewChild('myListView', { static: false }) listViewComponent: RadListViewComponent; | |
@ViewChild('approveIconTarget', { read: ElementRef, static: false }) approveIcon: ElementRef; | |
@ViewChild('approveTextTarget', { read: ElementRef, static: false }) approveText: ElementRef; | |
@ViewChild('rejectIconTarget', { read: ElementRef, static: false }) rejectIcon: ElementRef; | |
@ViewChild('rejectTextTarget', { read: ElementRef, static: false }) rejectText: ElementRef; | |
leftThresholdPassed = false; | |
rightThresholdPassed = false; | |
swipeLimitThreshold; | |
@Output() details: EventEmitter<GestureEventData> = new EventEmitter<GestureEventData>(); | |
segmentedBarItems: Array<SegmentedBarItem>; | |
public selectedIndex = 0; | |
form: FormGroup; | |
overdueExpenses = []; | |
expenseTypes = [ | |
{ type: 'Advertising', icon: 0xe946 }, | |
{ type: 'Airfare', icon: 0xe945 }, | |
{ type: 'Company Events', icon: 0xe944 }, | |
{ type: 'Client Entertainment', icon: 0xe943 }, | |
{ type: 'Facilities', icon: 0xe942 }, | |
{ type: 'Hiring and Training:', icon: 0xe941 }, | |
{ type: 'Information Technology', icon: 0xe940 }, | |
{ type: 'Lodging', icon: 0xe93f }, | |
{ type: 'Meal', icon: 0xe93e }, | |
{ type: 'Office Supplies', icon: 0xe93d }, | |
{ type: 'Parking', icon: 0xe93c }, | |
{ type: 'Rental Vehicle', icon: 0xe93b }, | |
{ type: 'Security Equipment', icon: 0xe93a }, | |
{ type: 'Shipping', icon: 0xe939 }, | |
{ type: 'Travel', icon: 0xe938 }, | |
{ type: 'Vehicle', icon: 0xe937 } | |
]; | |
expenseType; | |
constructor( | |
private fb: FormBuilder, | |
) { } | |
private getSegmentedBarItems = () => { | |
const segmentedBarItem1 = new SegmentedBarItem(); | |
segmentedBarItem1.title = 'Awaiting Review'; | |
const segmentedBarItem2 = new SegmentedBarItem(); | |
segmentedBarItem2.title = 'Reviewed'; | |
return [segmentedBarItem1, segmentedBarItem2]; | |
} | |
ngOnInit() { | |
this.form = this.fb.group({ | |
searchTerm: new FormControl(null) | |
}); | |
this.segmentedBarItems = this.getSegmentedBarItems(); | |
} | |
ngOnChanges(changes) { | |
if (changes.selectedPageIndex && changes.selectedPageIndex.currentValue) { | |
this.selectedIndex = changes.selectedPageIndex.currentValue; | |
} | |
if (changes.verifyError && changes.verifyError.currentValue) { | |
dialogs.alert({ | |
title: 'Approval Error', | |
message: changes.verifyError.currentValue, | |
okButtonText: 'Try again' | |
}); | |
} | |
if (changes.undoVerifyError && changes.undoVerifyError.currentValue) { | |
dialogs.alert({ | |
title: 'Undo Approval Error', | |
message: changes.undoVerifyError.currentValue, | |
okButtonText: 'Try again' | |
}); | |
} | |
} | |
get searchTerm() { return this.form.get('searchTerm'); } | |
// segmented bar selection | |
public onSelectedIndexChange(args) { | |
const segmentedBar = <SegmentedBar>args.object; | |
this.select.emit(segmentedBar.selectedIndex); | |
this.selectedIndex = segmentedBar.selectedIndex; | |
} | |
// update selectedTintColor on load (iOS13) - difficulty updating selected font color, so the font color | |
// will stay gray with a white selected tint | |
segmentedBarLoaded(args) { | |
const segmentedBar: SegmentedBar = args.object; | |
const segmentedBarController = segmentedBar.ios; | |
segmentedBarController.selectedSegmentTintColor = UIColor.whiteColor; | |
} | |
public onViewDetails(id) { | |
this.details.emit(id); | |
} | |
filterOverdue() { | |
this.overdueExpenses = this.pending.filter(e => e.isOverdue === true); | |
return this.overdueExpenses.length; | |
} | |
getExpenseTypeIcon(expense) { | |
if (expense && expense.type) { | |
this.expenseType = this.expenseTypes.find(e => e.type === expense.type.name); | |
return String.fromCharCode(this.expenseType.icon); | |
} | |
return null; | |
} | |
onTap(args: GestureEventData) { | |
this.tap.emit(args); | |
} | |
// ---- swiping awaiting approval functionality | |
// this function keeps track of the swipe starting position and defines the swipe limits | |
public onSwipeCellStarted(args: ListViewEventData) { | |
// note: this.verifySwipeAction shows up true here if sidedrawer is opening | |
const swipeLimits = args.data.swipeLimits; | |
const swipeView = args['object']; | |
const leftItem = swipeView.getViewById('reject-view'); | |
const rightItem = swipeView.getViewById('approve-view'); | |
swipeLimits.left = swipeLimits.right = args.data.x > 0 ? swipeView.getMeasuredWidth() : swipeView.getMeasuredWidth(); | |
this.swipeLimitThreshold = swipeLimits.threshold = swipeView.getMeasuredWidth(); | |
} | |
// this function is checking the position while actively swiping. | |
public onCellSwiping(args: ListViewEventData) { | |
// note: this.verifySwipeAction shows up false here if sidedrawer is opening | |
const swipeLimits = args.data.swipeLimits; | |
const swipeView = args['swipeView']; | |
const mainView = args['mainView']; | |
const leftItem = swipeView.getViewById('approve-view'); | |
const rightItem = swipeView.getViewById('reject-view'); | |
// Check whether the threshold has been passed on left and right but make sure if you swipe back before the threshold it updates the var | |
if (args.data.x > this.swipeLimitThreshold / 3) { | |
// Performing left action | |
this.leftThresholdPassed = true; | |
this.approveIcon.nativeElement.animate({ | |
scale: {x: 1.5, y: 1.5}, | |
curve: AnimationCurve.spring, | |
duration: 1000, | |
}); | |
this.approveText.nativeElement.animate({ | |
opacity: 1 | |
}); | |
} else if (args.data.x > 0 && args.data.x < this.swipeLimitThreshold / 3) { | |
this.leftThresholdPassed = false; | |
this.approveIcon.nativeElement.animate({ | |
scale: {x: 1, y: 1}, | |
}); | |
this.approveText.nativeElement.animate({ | |
opacity: 0 | |
}); | |
} else if (args.data.x < -this.swipeLimitThreshold / 3) { | |
// Performing right action | |
this.rightThresholdPassed = true; | |
this.rejectIcon.nativeElement.animate({ | |
scale: {x: 1.5, y: 1.5}, | |
curve: AnimationCurve.spring, | |
duration: 1000, | |
}); | |
this.rejectText.nativeElement.animate({ | |
opacity: 1 | |
}); | |
} else if (args.data.x < 0 && args.data.x > -this.swipeLimitThreshold / 3) { | |
this.rightThresholdPassed = false; | |
this.rejectIcon.nativeElement.animate({ | |
scale: {x: 1, y: 1}, | |
}); | |
this.rejectText.nativeElement.animate({ | |
opacity: 0 | |
}); | |
} | |
if (args.data.x > 0) { | |
const leftDimensions = View.measureChild( | |
leftItem.parent, | |
leftItem, | |
layout.makeMeasureSpec(Math.abs(args.data.x), layout.EXACTLY), | |
layout.makeMeasureSpec(mainView.getMeasuredHeight(), layout.EXACTLY)); | |
View.layoutChild(leftItem.parent, leftItem, 0, 0, leftDimensions.measuredWidth, leftDimensions.measuredHeight); | |
this.hideOtherSwipeTemplateView(args, 'left'); | |
} else { | |
const rightDimensions = View.measureChild( | |
rightItem.parent, | |
rightItem, | |
layout.makeMeasureSpec(Math.abs(args.data.x), layout.EXACTLY), | |
layout.makeMeasureSpec(mainView.getMeasuredHeight(), layout.EXACTLY)); | |
View.layoutChild(rightItem.parent, rightItem, mainView.getMeasuredWidth() - rightDimensions.measuredWidth, | |
0, mainView.getMeasuredWidth(), rightDimensions.measuredHeight); | |
this.hideOtherSwipeTemplateView(args, 'right'); | |
} | |
} | |
// Hide the unused swipe template on either side | |
public hideOtherSwipeTemplateView(args: ListViewEventData, currentSwipeView: string) { | |
const swipeView = args['swipeView']; | |
const mainView = args['mainView']; | |
const leftItem = swipeView.getViewById('approve-view'); | |
const rightItem = swipeView.getViewById('reject-view'); | |
switch (currentSwipeView) { | |
case 'left': | |
if (rightItem.getActualSize().width !== 0) { | |
View.layoutChild(<View>rightItem.parent, rightItem, mainView.getMeasuredWidth(), 0, mainView.getMeasuredWidth(), 0); | |
} | |
break; | |
case 'right': | |
if (leftItem.getActualSize().width !== 0 ) { | |
View.layoutChild(<View>leftItem.parent, leftItem, 0, 0, 0, 0); | |
} | |
break; | |
default: | |
break; | |
} | |
} | |
// this function takes action when the swipe ends (finger lifted) | |
public onSwipeCellFinished(args: ListViewEventData) { | |
if (this.leftThresholdPassed) { | |
const currentExpense = this.listViewComponent.listView.items[args.index]; | |
this.swipe.emit({ reports: currentExpense, status: 'approved' }); | |
} else if (this.rightThresholdPassed) { | |
const currentExpense = this.listViewComponent.listView.items[args.index]; | |
this.swipe.emit({ reports: currentExpense, status: 'rejected' }); | |
} | |
this.leftThresholdPassed = false; | |
this.rightThresholdPassed = false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment