Skip to content

Instantly share code, notes, and snippets.

@zoitsa
Created December 12, 2019 21:12
Show Gist options
  • Save zoitsa/daa48bef55872c35a579bd40432880f0 to your computer and use it in GitHub Desktop.
Save zoitsa/daa48bef55872c35a579bd40432880f0 to your computer and use it in GitHub Desktop.
EMS - Approval cards and animation
<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="&#xe920;" 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=" &#8226; "></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="&#xe935;" 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="&#xe90f;" 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="&#xe92c;" 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="&#xe920;" 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=" &#8226; "></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>
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