Timesheet Management with CAP & Trello ⏱️ |
Timesheet Management with CAP & Trello ⏱️ – Setup the IDE & MTA Project #1 |
Timesheet Management with CAP & Trello ⏱️ – Setup a Database and Service Module #2 |
Timesheet Management with CAP & Trello ⏱️ – Connect to Trello API’s via a Node.js Module #3 |
Timesheet Management with CAP & Trello ⏱️ – Build the Trello Timesheet HTML5 Module #4 (this blog) |
Timesheet Management with CAP & Trello ⏱️ – Add a Fiori Launchpad Site Module #5 |
Timesheet Management with CAP & Trello ⏱️ GitHub Repository
sap.ui.define([
"sap/ui/base/Object"
], function (Object) {
"use strict";
var Service = Object.extend("cap.trello.TimesheetManager.service.CoreService", {
constructor: function () {},
http: function (url) {
var core = {
ajax: function (method, url, headers, args, mimetype) {
var promise = new Promise(function (resolve, reject) {
var client = new XMLHttpRequest();
var uri = url;
if (args && method === 'GET') {
uri += '?';
var argcount = 0;
for (var key in args) {
if (args.hasOwnProperty(key)) {
if (argcount++) {
uri += '&';
}
uri += encodeURIComponent(key) + '=' + encodeURIComponent(args[key]);
}
}
}
if (args && (method === 'POST' || method === 'PUT')) {
var data = {};
for (var keyp in args) {
if (args.hasOwnProperty(keyp)) {
data[keyp] = args[keyp];
}
}
}
client.open(method, uri);
if (method === 'POST' || method === 'PUT') {
client.setRequestHeader("accept", "application/json");
client.setRequestHeader("content-type", "application/json");
}
for (var keyh in headers) {
if (headers.hasOwnProperty(keyh)) {
client.setRequestHeader(keyh, headers[keyh]);
}
}
if (data) {
client.send(JSON.stringify(data));
} else {
client.send();
}
client.onload = function () {
if (this.status === 200 || this.status === 201 || this.status === 204) {
resolve(this.response);
} else {
reject(this.statusText);
}
};
client.onerror = function () {
reject(this.statusText);
};
});
return promise;
}
};
return {
'get': function (headers, args) {
return core.ajax('GET', url, headers, args);
},
'post': function (headers, args) {
return core.ajax('POST', url, headers, args);
},
'put': function (headers, args) {
return core.ajax('PUT', url, headers, args);
},
'delete': function (headers, args) {
return core.ajax('DELETE', url, headers, args);
}
};
}
});
return Service;
});
sap.ui.define([
"./CoreService",
"sap/ui/model/Filter",
"sap/ui/model/FilterOperator"
], function (CoreService, Filter, FilterOperator) {
"use strict";
var TrelloService = CoreService.extend("cap.trello.TimesheetManager.service.TrelloService", {
constructor: function () {},
setModel: function (model) {
this.model = model;
},
logIn: function () {
return window.open("/TrelloAuthorizer/login", "_self");
},
getCurrentUser: function () {
return this.http("/TrelloAuthorizer/getUserInfo").get().then(function (user) {
return JSON.parse(user);
}).catch(function(){
return this.logIn();
}.bind(this));
}
});
return new TrelloService();
});
/* global _:true */
sap.ui.define([
"../object/BaseObject",
"../service/TrelloService",
"../object/UserObject"
], function (BaseObject, TrelloService, UserObject) {
"use strict";
var UserState = BaseObject.extend("cap.trello.TimesheetManager.state.UserState", {
oUser: null,
constructor: function (data) {
BaseObject.call(this, data);
},
setUser: function (oUser) {
this.oUser = oUser;
this.updateModel();
},
getCurrentUser: function () {
return this.oUser;
},
loadCurrentUser: function () {
if (this.oUser) {
return Promise.resolve(this.oUser);
}
return TrelloService.getCurrentUser().then(function (user) {
this.setUser(new UserObject(user));
return this.oUser;
}.bind(this));
}
});
return new UserState();
});
sap.ui.define([
"sap/ui/base/Object",
"sap/ui/model/json/JSONModel"
], function(Object, JSONModel) {
"use strict";
return Object.extend("cap.trello.TimesheetManager.object.BaseObject", {
constructor: function(data) {
if(data){
for (var field in data) {
switch (typeof(data[field])) {
case "object":
if(data[field] && data[field]["results"]){
this[field] = data[field]["results"];
} else if(data[field]) { // If it is a date object
this[field] = data[field];
}
break;
default:
this[field] = data[field];
}
}
}
},
getModel: function() {
if (!this.model) {
this.model = new JSONModel(this,true);
//this.model.setData(this);
}
return this.model;
},
updateModel:function(bHardRefresh){
if(this.model){
this.model.refresh(bHardRefresh?true:false);
}
},
getData:function(){
var req = jQuery.extend({},this);
delete req["model"];
return req;
}
});
});
/* global _:true */
sap.ui.define([
"./BaseObject",
], function (BaseObject) {
"use strict";
return BaseObject.extend("cap.trello.TimesheetManager.object.UserObject", {
constructor: function (data) {
// When a user object is created the url for the avatar need to be extended with "/50.png"
// This to point to the correct avatar url. Because there are multiple sizes available.
data.avatarUrl = this.convertAvatarUrl50(data.avatarUrl);
BaseObject.call(this, data);
},
convertAvatarUrl50: function(avatarUrl) {
return avatarUrl + "/50.png";
},
getFullName: function() {
return this.fullName;
}
});
});
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/Device",
"cap/trello/TimesheetManager/model/models",
"cap/trello/TimesheetManager/state/UserState"
], function (UIComponent, Device, models, UserState) {
"use strict";
return UIComponent.extend("cap.trello.TimesheetManager.Component", {
metadata: {
manifest: "json"
},
init: function () {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);
// enable routing
this.getRouter().initialize();
// set the device model
this.setModel(models.createDeviceModel(), "device");
// Set the model and get the current user, if model not set here avatar picture not set
this.setModel(UserState.getModel(), "UserState");
UserState.loadCurrentUser();
// force fiori 3 dark in Fiori Launchpad Sandbox environment
sap.ui.getCore().applyTheme("sap_fiori_3_dark");
}
});
});
<mvc:View controllerName="cap.trello.TimesheetManager.controller.Boards" xmlns:mvc="sap.ui.core.mvc" displayBlock="true"
xmlns="sap.m" xmlns:f="sap.f">
<Page id="page" title="{i18n>title}">
<content>
<Toolbar design="Transparent" style="Clear" height="auto" class="sapUiSmallMarginTop">
<Title text="{i18n>welcome}" class="sapUiSmallMarginBegin"/>
<ToolbarSpacer/>
<Title text="{UserState>/oUser/fullName}" class="sapUiSmallMarginTop sapUiSmallMarginEnd" visible="{= !${device>/system/phone}}"/>
<f:Avatar src="{UserState>/oUser/avatarUrl}" displaySize="XS" class="sapUiSmallMarginTop sapUiSmallMarginEnd borderAvatar"
visible="{= !${device>/system/phone} }"/>
</Toolbar>
</content>
</Page>
</mvc:View>
/*global history */
var _fragments = [];
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/routing/History",
'sap/m/Button',
'sap/m/Dialog'
], function (Controller, History, Button, Dialog) {
"use strict";
return Controller.extend("cap.trello.TimesheetManager.controller.BaseController", {
onInit: function () {},
/**
* Convenience method for accessing the router in every controller of the application.
* @public
* @returns {sap.ui.core.routing.Router} the router for this component
*/
getRouter: function () {
return this.getOwnerComponent().getRouter();
},
getEventBus: function () {
return this.getOwnerComponent().getEventBus();
},
/**
* Convenience method for getting the view model by name in every controller of the application.
* @public
* @param {string} sName the model name
* @returns {sap.ui.model.Model} the model instance
*/
getModel: function (sName) {
return this.getView().getModel(sName);
},
/**
* Convenience method for setting the view model in every controller of the application.
* @public
* @param {sap.ui.model.Model} oModel the model instance
* @param {string} sName the model name
* @returns {sap.ui.mvc.View} the view instance
*/
setModel: function (oModel, sName) {
return this.getView().setModel(oModel, sName);
},
/**
* Convenience method for getting the resource bundle.
* @public
* @returns {sap.ui.model.resource.ResourceModel} the resourceModel of the component
*/
getResourceBundle: function () {
return this.getOwnerComponent().getModel("i18n").getResourceBundle(); // this.getView().getModel("i18n"); //
},
/**
* Event handler for navigating back.
* It there is a history entry or an previous app-to-app navigation we go one step back in the browser history
* If not, it will replace the current entry of the browser history with the master route.
* @public
*/
onNavBack: function () {
var sPreviousHash = History.getInstance().getPreviousHash();
try {
var oCrossAppNavigator = sap.ushell.Container.getService("CrossApplicationNavigation");
} catch (ex) {
this.getRouter().navTo("master", {}, true);
return;
}
if (sPreviousHash !== undefined || !oCrossAppNavigator.isInitialNavigation()) {
history.go(-1);
} else {
this.getRouter().navTo("master", {}, true);
}
},
i18n: function (sProperty) {
return this.getResourceBundle().getText(sProperty);
},
fnMetadataLoadingFailed: function () {
var dialog = new Dialog({
title: 'Error',
type: 'Message',
state: 'Error',
content: new Text({
text: 'Metadata loading failed. Please refresh you page.'
}),
beginButton: new Button({
text: 'OK',
press: function () {
dialog.close();
}
}),
afterClose: function () {
dialog.destroy();
}
});
dialog.open();
},
openFragment: function (sName, model, updateModelAlways, callback, data) {
if (sName.indexOf(".") > 0) {
var aViewName = sName.split(".");
sName = sName.substr(sName.lastIndexOf(".") + 1);
} else { //current folder
aViewName = this.getView().getViewName().split("."); // view.login.Login
}
aViewName.pop();
var sViewPath = aViewName.join("."); // view.login
if (sViewPath.toLowerCase().indexOf("fragments") > 0) {
sViewPath += ".";
} else {
sViewPath += ".fragments.";
}
var id = this.getView().getId() + "-" + sName;
if (!_fragments[id]) {
//create controller
var sControllerPath = sViewPath.replace("view", "controller");
try {
var controller = sap.ui.controller(sControllerPath + sName);
} catch (ex) {
controller = this;
}
_fragments[id] = {
fragment: sap.ui.xmlfragment(
id,
sViewPath + sName,
controller
),
controller: controller
};
if (model && !updateModelAlways) {
_fragments[id].fragment.setModel(model);
}
// version >= 1.20.x
this.getView().addDependent(_fragments[id].fragment);
}
var fragment = _fragments[id].fragment;
if (model && updateModelAlways) {
fragment.setModel(model);
}
if (_fragments[id].controller && _fragments[id].controller !== this) {
_fragments[id].controller.onBeforeShow(this, fragment, callback, data);
}
setTimeout(function () {
fragment.open();
}, 100);
},
closeFragments: function () {
for (var f in _fragments) {
if (_fragments[f]["fragment"] && _fragments[f].fragment["isOpen"] && _fragments[f].fragment.isOpen()) {
_fragments[f].fragment.close();
}
}
},
getFragmentControlById: function (parent, id) {
var latest = this.getMetadata().getName().split(".")[this.getMetadata().getName().split(".").length - 1];
return sap.ui.getCore().byId(parent.getView().getId() + "-" + latest + "--" + id);
},
showBusyIndicator: function () {
return sap.ui.core.BusyIndicator.show();
},
hideBusyIndicator: function () {
return sap.ui.core.BusyIndicator.hide();
}
});
});
sap.ui.define([
"./BaseController"
], function (BaseController) {
"use strict";
return BaseController.extend("cap.trello.TimesheetManager.controller.Boards", {
onInit: function () {
}
});
});
<mvc:View controllerName="cap.trello.TimesheetManager.controller.App" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
displayBlock="true" xmlns="sap.m">
<App id="app"></App>
</mvc:View>
sap.ui.define([
"./BaseController"
], function(BaseController) {
"use strict";
return BaseController.extend("cap.trello.TimesheetManager.controller.App", {
});
});
{
"source": "^/timesheetService/(.*)$",
"target": "/timesheet-management/$1",
"authenticationType": "xsuaa",
"destination": "srv_api",
"csrfProtection": false
},
{
"source": "/TrelloAuthorizer/(.*)$",
"target": "$1",
"authenticationType": "xsuaa",
"destination": "TrelloAuthorizer_api",
"csrfProtection": false
}
{
"destinations": [
{
"name": "srv_api",
"url": "http://localhost:4004",
"forwardAuthToken": true
},
{
"name": "TrelloAuthorizer_api",
"url": "http://localhost:3002",
"forwardAuthToken": true
}
]
}
https://{subdomain}-workspaces-ws-{workspace-id}-app{id}.eu10.trial.applicationstudio.cloud.sap/Trel...
https://{subdomain}-workspaces-ws-{workspace-id}-app{id}.eu10.trial.applicationstudio.cloud.sap
title=Timesheet Application
appTitle=Timesheet Application
appDescription=Timesheet Application
welcome=Welcome to the Timesheet Application ⏱️
appIcon=App icon
selectBoard=SELECT A BOARD TO GET STARTED
active=Active
closed=Closed
openBoardOnTrello=Open board in Trello
board=board
cards=Cards
cardInfo=Card Information
Yes=Yes
No=No
noCardSelectedTitle=No Trello card selected
noCardSelectedDesc=Please select a card to add your spent time.
noDeadLineSet=No deadline set
spentHoursTimeline=Timeline spent hours
timelineTitle=The spent time on:
noHoursSpent=No hours spent on card.
addSpentHour=Add spent hour
AddSpentHourTooltip=Add spent hour
deleteSpentHour=Delete spent hour
editSpentHour=Edit spent hour
duePassed=Deadline expired
dueNotPassed=Deadline not expired yet
noDeadLineSet=No deadline set
toExcel=Export to Excel
toExcelTooltip=Export all cards timesheets to Excel
deletionConfirmationText=Are you sure you want to delete this spent hour?
Save=Save
Update=Update
Comment=Comment
Status=Status
SpentHours=Spent hours
SpentOnDate=Spent on
Done=Done
Ongoing=Ongoing
OnHold=On hold
TimesheetExportFinished=Timesheet export has Finished.
cardName=Card name
cardDesc=Card description
date=Date
fullName=User
hours=Hours
status=Status
comment=Comment
errorAddingSpentHour=Could not add Spent hour.
fillInRequiredFieldsSpentHour=Please fill in all the required fields.
getBoards: function () {
return this.http("/TrelloAuthorizer/getAllBoards").get().then(function (boards) {
return JSON.parse(boards);
});
}
/* global _:true */
sap.ui.define([
"../object/BaseObject",
"../service/TrelloService",
"../object/BoardObject"
], function (BaseObject, TrelloService, BoardObject) {
"use strict";
var BoardState = BaseObject.extend("cap.trello.TimesheetManager.state.BoardState", {
aBoards: [],
constructor: function (data) {
BaseObject.call(this, data);
},
loadBoards: function () {
if (this.aBoards.length > 0) {
return Promise.resolve(this.aBoards);
}
return TrelloService.getBoards().then(function (boards) {
var aBoards = boards.map(function (oBoard) {
return new BoardObject(oBoard);
});
this.setBoards(aBoards);
return this.aBoards;
}.bind(this));
},
setBoards: function (boards) {
this.aBoards = boards;
this.updateModel();
}
});
return new BoardState();
});
/* global _:true */
sap.ui.define([
"./BaseObject"
], function (BaseObject) {
"use strict";
return BaseObject.extend("cap.trello.TimesheetManager.object.BoardObject", {
constructor: function (data) {
BaseObject.call(this, data);
},
getId: function () {
return this.id;
},
getName: function () {
return this.name;
},
getUrl: function() {
return this.url;
}
});
});
sap.ui.define([
"./BaseController",
"../state/BoardState",
"../model/Formatter"
], function (BaseController, BoardState, Formatter) {
"use strict";
return BaseController.extend("cap.trello.TimesheetManager.controller.Boards", {
formatter: Formatter,
onInit: function () {
// Set baord state to the view
this.setModel(BoardState.getModel(), "BoardState");
var oRouter = this.getRouter();
oRouter.getRoute("Boards").attachMatched(this._onRouteMatched, this);
},
_onRouteMatched: function (oEvent) {
this.showBusyIndicator();
// Get all the user's boards
BoardState.loadBoards().then(function () {
this.hideBusyIndicator();
}.bind(this));
},
toTrelloBoard: function (oEvent) {
var oSource = oEvent.getSource();
var trelloBoardUrl = oSource.getBindingContext("BoardState").getObject().getUrl();
window.open(trelloBoardUrl, '_blank');
},
});
});
/* global moment: true */
sap.ui.define([], function () {
"use strict";
return {
getTrelloIcon: function (oCurrentCard) {
return sap.ui.require.toUrl("cap/trello/TimesheetManager/images/trello-mark-blue.png");
}
};
});
<Panel>
<content>
<f:GridList id="gridListBoards" headerText="{i18n>selectBoard}" items="{BoardState>/aBoards}">
<f:customLayout>
<grid:GridBoxLayout boxMinWidth="17rem"/>
</f:customLayout>
<CustomListItem press=".onBoardPress" type="Active">
<VBox height="100%">
<VBox class="sapUiSmallMargin">
<layoutData>
<FlexItemData growFactor="1" shrinkFactor="0"/>
</layoutData>
<Title text="{BoardState>name}" class="sapUiSmallMarginBottom"/>
<VBox height="60px">
<Label text="{BoardState>desc}" wrapping="true" class="sapUiSmallMarginBottom"/>
</VBox>
<tnt:InfoLabel text="{= ${BoardState>closed} ? ${i18n>closed} : ${i18n>active}}" colorScheme="{= ${BoardState>closed} ? 3 : 8}" displayOnly="true"/>
<Toolbar style="Clear">
<Text text="{path: 'BoardState>dateLastActivity', type: 'sap.ui.model.type.DateTime', formatOptions: { source : { pattern : 'yyyy-MM-ddTHH:mm:ssZ' }, pattern: 'yyyy-MM-dd HH:mm:ss'}}"/>
<ToolbarSpacer/>
<Image src="{path: 'BoardState>id', formatter: '.formatter.getTrelloIcon'}" width="20px" press=".toTrelloBoard" tooltip="{i18n>openBoardOnTrello}"/>
</Toolbar>
</VBox>
</VBox>
</CustomListItem>
</f:GridList>
</content>
</Panel>
onBoardPress: function (oEvent) {
this.showBusyIndicator();
// Get the selected board object from the BoardState based on its bindingContext against the ovent source
var oBoard = oEvent.getSource().getBindingContext("BoardState").getObject();
// Set the current board in the board state
BoardState.setCurrentBoard(oBoard);
// Next nav to the detail page with all the card
var oRouter = sap.ui.core.UIComponent.getRouterFor(this);
oRouter.navTo("Cards", {
boardId: oBoard.id
});
}
setCurrentBoard: function (oBoard) {
this.oCurrentBoard = oBoard;
this.updateModel();
}
oCurrentBoard: null,
{
"name": "Cards",
"pattern": "board/{boardId}/cards",
"target": ["Cards"]
}
"Cards": {
"viewType": "XML",
"transition": "slide",
"clearControlAggregation": false,
"viewId": "Cards",
"viewName": "Cards"
}
<mvc:View controllerName="cap.trello.TimesheetManager.controller.Cards" xmlns:mvc="sap.ui.core.mvc" displayBlock="true"
xmlns="sap.m" xmlns:semantic="sap.m.semantic">
<Page id="page" title="{i18n>title}" showNavButton="true" navButtonPress=".onNavBack">
<content>
<SplitContainer>
<masterPages>
<semantic:MasterPage title="{BoardState>/oCurrentBoard/name}">
<semantic:content>
<List id="listCards" selectionChange=".onCardSelect" items="{ path: 'BoardState>/oCurrentBoard/cards', sorter: { path: 'name', descending: false }}"
mode="{= ${device>/system/phone} ? 'None' : 'SingleSelectMaster'}" growing="true" growingScrollToLoad="true">
<headerToolbar>
<Toolbar>
<SearchField width="100%" liveChange="onCardSearch" search="onCardSearch"/></Toolbar>
</headerToolbar>
<items>
<ObjectListItem type="Active" title="{BoardState>name}">
<attributes>
<ObjectAttribute text="{BoardState>desc}"/>
</attributes>
</ObjectListItem>
</items>
</List>
</semantic:content>
</semantic:MasterPage>
</masterPages>
<detailPages>
<semantic:DetailPage title="{i18n>cardInfo}">
<semantic:content>
</semantic:content>
<semantic:customFooterContent>
</semantic:customFooterContent>
</semantic:DetailPage>
</detailPages>
</SplitContainer>
</content>
</Page>
</mvc:View>
sap.ui.define([
"./BaseController",
"../state/UserState",
"../state/BoardState",
"sap/ui/core/routing/History",
"../model/Formatter",
'sap/ui/model/Filter',
"sap/ui/model/FilterOperator"
], function (BaseController, UserState, BoardState, History, Formatter, Filter, FilterOperator) {
"use strict";
return BaseController.extend("cap.trello.TimesheetManager.controller.Cards", {
formatter: Formatter,
onInit: function () {
// Set the user model to the view
this.setModel(UserState.getModel(), "UserState");
// Set the board model to the view
this.setModel(BoardState.getModel(), "BoardState");
// Attach a private event listener function _onRouteMatched to the matched event of this route.
var oRouter = this.getOwnerComponent().getRouter();
oRouter.getRoute("Cards").attachMatched(this._onRouteMatched, this);
},
_onRouteMatched: function (oEvent) {
// Get the current user
UserState.loadCurrentUser();
var boardId = oEvent.getParameter("arguments").boardId;
BoardState.loadCurrentBoard(boardId).then(function (board) {
BoardState.loadBoardCards(board.getId()).then(function () {
this.hideBusyIndicator();
}.bind(this));
}.bind(this));
},
// Navigate back
onNavBack: function () {
this.showBusyIndicator();
var oHistory = History.getInstance();
var sPreviousHash = oHistory.getPreviousHash();
if (sPreviousHash !== undefined) {
window.history.go(-1);
} else {
var oRouter = sap.ui.core.UIComponent.getRouterFor(this);
oRouter.navTo("Boards", true);
}
},
onCardSearch: function (oEvent) {
// add filter for search
var aFilters = [];
var sQuery = oEvent.getSource().getValue();
if (sQuery && sQuery.length > 0) {
var filter = new Filter({
filters: [
new Filter("name", FilterOperator.Contains, sQuery),
new Filter("desc", FilterOperator.Contains, sQuery)
],
and: false
});
aFilters.push(filter);
}
// update list binding
var list = this.byId("listCards");
var binding = list.getBinding("items");
binding.filter(aFilters, "Application");
}
});
});
loadCurrentBoard: function (boardId) {
if (this.oCurrentBoard) {
return Promise.resolve(this.oCurrentBoard);
}
return this.getCurrentBoardById(boardId);
},
getCurrentBoardById: function (boardId) {
return TrelloService.getBoardById(boardId).then(function (board) {
var oBoard = new BoardObject(board);
this.setCurrentBoard(oBoard);
return this.oCurrentBoard;
}.bind(this));
}
getBoardById: function (boardId) {
return this.http("/TrelloAuthorizer/getBoardById?boardId=" + boardId).get().then(function (board) {
return JSON.parse(board);
});
}
loadBoardCards: function (boardId) {
return this.loadCurrentBoard(boardId).then(function (board) {
if (board.getCards()) {
return Promise.resolve(this.oCurrentBoard);
}
return TrelloService.getBoardCards(boardId).then(function (cards) {
var aCards = cards.map(function (oCard) {
return new CardObject(oCard);
});
return this.setBoardCards(aCards);
}.bind(this));
}.bind(this));
}
setBoardCards: function (aCards) {
this.getCurrentBoard().setCards(aCards);
this.updateModel();
}
getCurrentBoard: function () {
return this.oCurrentBoard;
}
"../object/CardObject"
function (BaseObject, TrelloService, HrTimesheetService, BoardObject, CardObject)
getCards: function () {
return this.cards;
},
setCards: function (aCards) {
this.cards = aCards.map(function (card) {
return new CardObject(card);
});
}
sap.ui.define([
"./BaseObject",
"./CardObject"
], function (BaseObject, CardObject) {
/* global _:true */
sap.ui.define([
"./BaseObject"
], function (BaseObject) {
"use strict";
return BaseObject.extend("cap.trello.TimesheetManager.object.CardObject", {
constructor: function (data) {
BaseObject.call(this, data);
},
getId: function () {
return this.id;
},
getUrl: function() {
return this.url;
}
});
});
getBoardCards: function (boardId) {
return this.http("/TrelloAuthorizer/getCardsByBoardId?boardId=" + boardId).get().then(function (cards) {
return JSON.parse(cards);
});
}
<MessagePage text="{i18n>noCardSelectedTitle}" description="{i18n>noCardSelectedDesc}" showHeader="false" icon="sap-icon://timesheet" visible="{= !${BoardState>/oCurrentCard} ? true : false}"/>
<ObjectHeader id="oHeaderCard" binding="{BoardState>/oCurrentCard}" visible="{= ${BoardState>/oCurrentCard} ? true : false}" responsive="true" icon="{path: 'BoardState>/oCurrentCard/id', formatter: '.formatter.getTrelloIcon'}" iconTooltip="{i18n>openCardOnTrello}" intro="{BoardState>desc}" title="{BoardState>name}" backgroundDesign="Translucent" iconActive="true" iconPress=".toTrelloCard">
<attributes>
<ObjectAttribute title="{i18n>dateLastActivity}" text="{path: 'BoardState>dateLastActivity', type: 'sap.ui.model.type.DateTime', formatOptions: { source : { pattern : 'yyyy-MM-ddTHH:mm:ssZ' }, pattern: 'yyyy-MM-dd HH:mm:ss'}}"/>
</attributes>
<statuses>
<ObjectStatus title="{i18n>due}" text="{path: 'BoardState>due', formatter: '.formatter.emptyDueDate', type: 'sap.ui.model.type.DateTime', formatOptions: { source : { pattern : 'yyyy-MM-ddTHH:mm:ssZ' }, pattern: 'yyyy-MM-dd HH:mm:ss'}}" state="{path: 'BoardState>due', formatter: '.formatter.checkDueDateStatus'}"/>
<ObjectStatus class="sapUiSmallMarginBottom" text="{path: 'BoardState>due', formatter: '.formatter.checkDueDateText'}" inverted="true" state="{path: 'BoardState>due', formatter: '.formatter.checkDueDateStatusInverted'}"/>
</statuses>
</ObjectHeader>
<Toolbar style="Clear" class="sapUiSmallMarginTop">
<ObjectHeader title="{i18n>spentHoursTimeline}" visible="{= ${BoardState>/oCurrentCard} ? true : false}"/>
<ToolbarSpacer/>
</Toolbar>
// Set sate color for due date if expired or not
checkDueDateStatus: function (dueDate) {
if (dueDate) {
var oDueDate = new Date(dueDate);
if (new Date() > oDueDate) {
return "Error";
} else {
return "Warning";
}
}
return "Success";
},
// Set sate inverted color for due date if expired or not
checkDueDateStatusInverted: function (dueDate) {
if (dueDate) {
var oDueDate = new Date(dueDate);
if (new Date() > oDueDate) {
return "Indication01";
} else {
return "Indication03";
}
}
return "Indication04";
},
checkDueDateText: function (dueDate) {
var i18n = this.getOwnerComponent().getModel("i18n").getResourceBundle();
if (dueDate) {
var oDueDate = new Date(dueDate);
if (new Date() > oDueDate) {
return i18n.getText("duePassed");
} else {
return i18n.getText("dueNotPassed");
}
}
return i18n.getText("noDeadLineSet");
},
emptyDueDate: function (dueDate) {
var i18n = this.getOwnerComponent().getModel("i18n").getResourceBundle();
if (dueDate) {
return dueDate;
}
return i18n.getText("noDeadLineSet");
},
// On card select show details
onCardSelect: function (oEvent) {
var oSource = oEvent.getSource();
var oCard = oSource.getSelectedItem().getBindingContext("BoardState").getObject();
BoardState.setCurrentCard(oCard);
},
setCurrentCard: function (oCard) {
this.oCurrentCard = oCard;
this.getCardSpentHours(this.oCurrentBoard.getId(), this.oCurrentCard.getId()).then(function (spentHours) {
this.updateModel();
return spentHours;
}.bind(this));
}
aBoards: [],
oCurrentBoard: null,
oCurrentCard: null,
setCurrentCard: function (oCard) {
this.oCurrentCard = oCard;
this.updateModel();
}
toTrelloCard: function (oEvent) {
var oSource = oEvent.getSource();
var trelloCardUrl = oSource.getBindingContext("BoardState").getObject().getUrl();
window.open(trelloCardUrl, '_blank');
},
setCurrentCard: function (oCard) {
this.oCurrentCard = oCard;
this.getCardSpentHours(this.oCurrentBoard.getId(), this.oCurrentCard.getId()).then(function (spentHours) {
this.updateModel();
return spentHours;
}.bind(this));
}
getCardSpentHours: function (boardId, cardId) {
if (this.getCurrentCard().getSpentHours()) {
return Promise.resolve(this.getCurrentCard().getSpentHours());
}
return TimesheetManagementService.getCardSpentHours(boardId, cardId).then(function (aSpentHours) {
this.oCurrentCard.setSpentHours(aSpentHours);
return this.oCurrentCard.getSpentHours();
}.bind(this));
}
getCurrentCard: function () {
return this.oCurrentCard;
},
getSpentHours: function () {
return this.spentHours;
},
setSpentHours: function (aSpentHours) {
this.spentHours = aSpentHours.map(function (spentHour) {
// spentHour.date = new Date(spentHour.date);
return new SpentHourObject(spentHour);
});
}
/* global _:true */
sap.ui.define([
"./BaseObject",
"./SpentHourObject"
], function (BaseObject, SpentHourObject) {
/* global _:true */
sap.ui.define([
"./BaseObject"
], function (BaseObject) {
"use strict";
return BaseObject.extend("cap.trello.TimesheetManager.object.SpentHourObject", {
constructor: function (data) {
BaseObject.call(this, data);
if (data) {
this.date = new Date(data.date);
}
},
getId: function () {
return this.ID;
},
setId: function (ID) {
this.ID = ID;
},
setBoardId: function (boardId) {
this.boardId = boardId;
},
setCardId: function (cardId) {
this.cardId = cardId;
},
setFullName: function (fullName) {
this.fullName = fullName;
},
getJSON: function() {
return {
boardId : this.boardId,
cardId : this.cardId,
date: this.date,
fullName: this.fullName,
hours: this.hours.toString(),
status: this.status,
comment: this.comment
};
}
});
});
sap.ui.define([
"./CoreService",
"sap/ui/model/Filter",
"sap/ui/model/FilterOperator"
], function (CoreService, Filter, FilterOperator) {
"use strict";
var TimesheetManagementService = CoreService.extend("cap.trello.TimesheetManager.service.TimesheetManagementService", {
constructor: function () {},
setModel: function (model) {
this.model = model;
},
getCardSpentHours: function (boardId, cardId) {
return this.http("/timesheetService/SpentHours?$filter=boardId eq '" + boardId + "' and cardId eq '" + cardId + "'").get()
.then(function (
aSpentHours) {
return JSON.parse(aSpentHours).value;
});
}
});
return new TimesheetManagementService();
});
/* global _:true */
sap.ui.define([
"../object/BaseObject",
"../service/TrelloService",
"../object/BoardObject",
"../object/CardObject",
"../service/TimesheetManagementService"
], function (BaseObject, TrelloService, BoardObject, CardObject, TimesheetManagementService) {
<commons:Timeline id="idTimeline" showFilterBar="false" noDataText="{i18n>noHoursSpent}" enableDoubleSided="false" content="{BoardState>/oCurrentCard/spentHours}" visible="{= ${BoardState>/oCurrentCard} ? true : false}">
<commons:content>
<commons:TimelineItem dateTime="{path: 'BoardState>date', type: 'sap.ui.model.type.DateTime', pattern: 'yyyy-MM-dd HH:mm:ss'}" userName="{BoardState>hours} {i18n>hours}" title="{BoardState>fullName}" icon="sap-icon://per-diem">
<VBox>
<Text text="{BoardState>comment}"/>
<OverflowToolbar style="Clear">
<ObjectStatus class="sapUiTinyMarginTop" title="{i18n>Status}" text="{path: 'BoardState>status', formatter: '.formatter.spentHourStatus'}" state="{= ${BoardState>status} === 'Done' ? 'Success' : ${BoardState>status} === 'OnHold' ? 'Error' : 'Warning'}"/>
<ToolbarSpacer/>
<Button tooltip="{i18n>deleteSpentHour}" icon="sap-icon://delete" type="Transparent" press=".onDeleteSpentHour"/>
<Button tooltip="{i18n>editSpentHour}" icon="sap-icon://edit" type="Transparent" press=".onEditSpentHour"/>
</OverflowToolbar>
</VBox>
</commons:TimelineItem>
</commons:content>
</commons:Timeline>
xmlns:commons="sap.suite.ui.commons"
spentHourStatus: function(status){
var i18n = this.getOwnerComponent().getModel("i18n").getResourceBundle();
return i18n.getText(status);
},
onAddSpentHour: function (oEvent) {
var i18n = this.getModel("i18n").getResourceBundle();
this.openFragment(
"DialogAddSpentHour",
null,
true,
this.addSpentHour,
{
title: i18n.getText("addSpentHour"),
isNewSpentHour: true
}
);
},
addSpentHour: function (oSpentHour) {
BoardState.addSpentHour(oSpentHour).then(function (response) {
this.hideBusyIndicator();
}.bind(this));
},
<semantic:customFooterContent>
<Button text="{i18n>addSpentHour}" visible="{= ${BoardState>/oCurrentCard} ? true : false}" icon="sap-icon://create-entry-time" tooltip="{i18n>AddSpentHourTooltip}" press=".onAddSpentHour"/>
</semantic:customFooterContent>
addSpentHour: function (oSpentHour) {
oSpentHour = new SpentHourObject(oSpentHour);
// oSpentHour.setId("0");
oSpentHour.setBoardId(this.getCurrentBoard().getId());
oSpentHour.setCardId(this.getCurrentCard().getId());
oSpentHour.setFullName(UserState.getCurrentUser().getFullName());
return TimesheetManagementService.addSpentHour(oSpentHour).then(function (response) {
response = JSON.parse(response);
oSpentHour.setId(response.ID);
this.getCurrentCard().getSpentHours().push(oSpentHour);
this.updateModel();
return this.getCurrentCard();
}.bind(this)).catch(function (oError) {
console.log(oError.message);
}.bind(this));
},
/* global _:true */
sap.ui.define([
"../object/BaseObject",
"../service/TrelloService",
"../object/BoardObject",
"../object/CardObject",
"../service/TimesheetManagementService",
"../object/SpentHourObject",
"./UserState",
], function (BaseObject, TrelloService, BoardObject, CardObject, TimesheetManagementService, SpentHourObject, UserState) {
addSpentHour: function (oSpentHour) {
return this.http("/timesheetService/SpentHours").post(false, oSpentHour.getJSON());
}
<core:FragmentDefinition xmlns="sap.m"
xmlns:f="sap.ui.layout.form"
xmlns:core="sap.ui.core">
<Dialog title="{DialogAddSpentHourModel>/title}">
<f:SimpleForm editable="true" layout="ResponsiveGridLayout">
<f:content>
<VBox>
<ObjectAttribute title="{i18n>Board}" text="{BoardState>/oCurrentBoard/name}"/>
<ObjectAttribute title="{i18n>Card}" text="{BoardState>/oCurrentCard/name}"/>
<ObjectAttribute title="{i18n>User}" text="{UserState>/oUser/fullName}"/>
</VBox>
<Label text="{i18n>SpentOnDate}" required="true"/>
<DateTimePicker value="{DialogAddSpentHourModel>/oSpentHour/date}" placeholder="Select the date..." change="handleSpentHoursDateChange" class="sapUiSmallMarginBottom"/>
<Label text="{i18n>SpentHours}" required="true"/>
<Input value="{DialogAddSpentHourModel>/oSpentHour/hours}" type="Number" placeholder="Enter the spent hours..."/>
<Label text="{i18n>Status}" required="true"/>
<Select forceSelection="false" selectedKey="{DialogAddSpentHourModel>/oSpentHour/status}" items="{ path: 'DialogAddSpentHourModel>/possibleStatus', sorter: { path: 'status' } }">
<core:Item key="{DialogAddSpentHourModel>id}" text="{DialogAddSpentHourModel>status}"/>
</Select>
<Label text="{i18n>Comment}"/>
<TextArea value="{DialogAddSpentHourModel>/oSpentHour/comment}" width="100%"/>
</f:content>
</f:SimpleForm>
<buttons>
<Button text="{i18n>cancel}" press=".onClose" type="Reject"/>
<Button text="{= ${DialogAddSpentHourModel>/isNewSpentHour} ? ${i18n>Save} : ${i18n>Update}}" press=".submitForm" type="Accept"/>
</buttons>
</Dialog>
</core:FragmentDefinition>
sap.ui.define([
"../BaseController",
"sap/ui/model/json/JSONModel",
"../../state/BoardState",
"sap/m/MessageBox"
], function (BaseController, JSONModel, BoardState, MessageBox) {
"use strict";
return BaseController.extend("cap.trello.TimesheetManager.controller.fragments.DialogAddSpentHour", {
onBeforeShow: function (parent, fragment, callback, data) {
this.parent = parent;
this.fragment = fragment;
this.callback = callback;
this.i18n = this.parent.getModel("i18n").getResourceBundle();
var oSpentHour = data.isNewSpentHour === true ? {} : data.oSpentHour;
var dialogmodel = new JSONModel({
title: data.title,
isNewSpentHour: data.isNewSpentHour,
oSpentHour: oSpentHour,
possibleStatus: [{
id: "Done",
status: this.i18n.getText("Done")
}, {
id: "Ongoing",
status: this.i18n.getText("Ongoing")
}, {
id: "OnHold",
status: this.i18n.getText("OnHold")
}]
});
this.fragment.setModel(dialogmodel, "DialogAddSpentHourModel");
},
submitForm: function () {
if (this.spentHourIsValid()) {
var oSpentHour = this.fragment.getModel("DialogAddSpentHourModel").getProperty("/oSpentHour");
this.showBusyIndicator();
this.fragment.close();
return this.callback.call(this.parent, oSpentHour);
}
var me = this;
return MessageBox.error(me.i18n.getText("fillInRequiredFieldsSpentHour"));
},
spentHourIsValid: function () {
var date = this.fragment.getModel("DialogAddSpentHourModel").getProperty("/oSpentHour/date");
var hours = this.fragment.getModel("DialogAddSpentHourModel").getProperty("/oSpentHour/hours");
var status = this.fragment.getModel("DialogAddSpentHourModel").getProperty("/oSpentHour/status");
return date && hours && status ? true : false;
},
// Set the selecte date time on change
handleSpentHoursDateChange: function (oEvent) {
var oSpendHour = oEvent.getSource().getModel("DialogAddSpentHourModel").getContext("/oSpentHour").getObject();
oSpendHour.date = oEvent.getSource().getDateValue();
},
onClose: function () {
this.fragment.close();
}
});
});
charset=UTF-8;IEEE754Compatible=true
if (method === 'POST' || method === 'PUT') {
client.setRequestHeader("accept", "application/json");
client.setRequestHeader("content-type", "application/json;charset=UTF-8;IEEE754Compatible=true");
}
onDeleteSpentHour: function (oEvent) {
var i18n = this.getModel("i18n").getResourceBundle();
var oSpentHour = oEvent.getSource().getBindingContext("BoardState").getObject();
BoardState.setCurrentSpenthour(oSpentHour);
this.openFragment(
"DialogDeleteSpentHour",
null,
true,
this.deleteSpentHour,
{
title: i18n.getText("deleteSpentHour")
}
);
}
setCurrentSpenthour: function (oSpentHour) {
this.oCurrentSpentHour = oSpentHour;
this.updateModel();
},
aBoards: [],
oCurrentBoard: null,
oCurrentCard: null,
oCurrentSpentHour : null,
deleteSpentHour: function () {
var oSpentHour = BoardState.getCurrentSpentHour();
BoardState.deleteSpentHour(oSpentHour).then(function (response) {
this.hideBusyIndicator();
}.bind(this));
},
getCurrentSpentHour: function () {
return this.oCurrentSpentHour;
},
deleteSpentHour: function (oSpentHour) {
return TimesheetManagementService.deleteSpentHour(oSpentHour.getId()).then(function (response) {
var aSpentHours = this.getCurrentCard().getSpentHours();
var index = aSpentHours.map(function (spentHour) {
return spentHour.ID;
}).indexOf(oSpentHour.getId());
aSpentHours.splice(index, 1);
this.updateModel();
return this.getCurrentCard();
}.bind(this, oSpentHour.getId()));
}
deleteSpentHour: function (spentHourId) {
return this.http("/timesheetService/SpentHours(" + spentHourId + ")").delete();
}
<core:FragmentDefinition xmlns="sap.m"
xmlns:core="sap.ui.core">
<Dialog title="{DialogDeleteSpentHourModel>/title}" showHeader="true">
<content>
<Label text="{i18n>deletionConfirmationText}" class="sapUiTinyMarginBegin sapUiTinyMarginTop"/>
</content>
<buttons>
<Button text="{i18n>Yes}" press=".onDeleteSpentHour" type="Reject"/>
<Button text="{i18n>No}" press=".onClose" type="Ghost"/>
</buttons>
</Dialog>
</core:FragmentDefinition>
sap.ui.define([
"../BaseController",
"sap/ui/model/json/JSONModel",
"../../state/BoardState",
], function (BaseController, JSONModel, BoardState) {
"use strict";
return BaseController.extend("cap.trello.TimesheetManager.controller.fragments.DialogDeleteSpentHour", {
onBeforeShow: function (parent, fragment, callback, data) {
this.parent = parent;
this.fragment = fragment;
this.callback = callback;
var dialogmodel = new JSONModel({
title: data.title
});
this.fragment.setModel(dialogmodel, "DialogDeleteSpentHourModel");
//read label control from dialog in fragment
//var label = this.getFragmentControlById(this.parent, "label1");
},
onDeleteSpentHour: function () {
this.showBusyIndicator();
this.fragment.close();
this.callback.call(this.parent);
},
onClose: function () {
this.fragment.close();
}
});
});
onEditSpentHour: function (oEvent) {
var oSpentHour = oEvent.getSource().getBindingContext("BoardState").getObject();
BoardState.setCurrentSpenthour(oSpentHour);
var i18n = this.getModel("i18n").getResourceBundle();
this.openFragment(
"DialogAddSpentHour",
null,
true,
this.editSpentHour, {
title: i18n.getText("editSpentHour"),
isNewSpentHour: false,
oSpentHour: oSpentHour
});
},
editSpentHour: function (oSpentHour) {
BoardState.editSpentHour(oSpentHour).then(function (response) {
this.hideBusyIndicator();
}.bind(this));
},
editSpentHour: function (oSpentHour) {
return TimesheetManagementService.editSpentHour(oSpentHour).then(function (response) {
this.updateModel();
return this.getCurrentCard();
}.bind(this));
},
editSpentHour: function (oSpentHour) {
return this.http("/timesheetService/SpentHours(" + oSpentHour.getId() + ")").put(false, oSpentHour.getJSON());
}
onExcelExport: function () {
var aCols = BoardState.getColumnConfig();
var date = new Date().toLocaleDateString();
var fileName = BoardState.getCurrentBoard().getName() + " - " + date + ".xlsx";
var me = this;
BoardState.getMonthlyExcelExportData().then(function (result) {
var oSettings = {
workbook: {
columns: aCols
},
dataSource: result,
fileName: fileName
};
var oSheet = new Spreadsheet(oSettings);
// Build and export spreadsheet
oSheet.build().then(function () {
var i18n = me.getModel("i18n").getResourceBundle();
MessageToast.show(i18n.getText("TimesheetExportFinished"));
}.bind(this)).finally(function () {
oSheet.destroy();
});
});
},
sap.ui.define([
"./BaseController",
"../state/UserState",
"../state/BoardState",
"sap/ui/core/routing/History",
"../model/Formatter",
'sap/ui/model/Filter',
"sap/ui/model/FilterOperator",
'sap/ui/export/Spreadsheet',
"sap/m/MessageToast"
], function (BaseController, UserState, BoardState, History, Formatter, Filter, FilterOperator, Spreadsheet, MessageToast) {
getColumnConfig: function () {
var columnConfig = [{
label: 'Card name',
property: 'cardName',
type: 'String'
}, {
label: 'Card description',
property: 'cardDesc',
type: 'String'
},
{
label: 'Spent date',
property: 'date',
type: 'datetimeoffset'
},
{
label: 'Spent hours',
property: 'hours',
type: 'Number'
},
{
label: 'Current status',
property: 'status',
type: 'String'
},
{
label: 'Spent by user',
property: 'fullName',
type: 'String'
},
{
label: 'Comment',
property: 'comment',
type: 'String'
}
];
return columnConfig;
},
getMonthlyExcelExportData: function () {
return TimesheetManagementService.getMonthlyExcelExportData(this.getCurrentBoard().getId()).then(function (aSpentHours) {
aSpentHours = JSON.parse(aSpentHours).value;
var aCards = this.getCurrentBoard().getCards();
var aResult = aSpentHours.map(function (oSpentHour) {
var indexCard = aCards.map(function (card) {
return card.id;
}).indexOf(oSpentHour.cardId);
oSpentHour.cardName = aCards[indexCard].name;
oSpentHour.cardDesc = aCards[indexCard].desc;
return oSpentHour;
});
return aResult;
}.bind(this));
}
getMonthlyExcelExportData: function (boardId) {
var date = new Date();
var firstDay = new Date(date.getFullYear(), date.getMonth(), 1).toISOString();
var lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0).toISOString();
var aFilters = [];
var oFilter = new Filter({
filters: [
new Filter("boardId", FilterOperator.EQ, boardId),
new Filter("date", FilterOperator.GE, firstDay),
new Filter("date", FilterOperator.LT, lastDay)
],
and: true
});
aFilters.push(oFilter);
var args = {
"$filter": "boardId eq '" + boardId + "' and date gt " + firstDay + " and date lt " + lastDay
};
return this.http("/timesheetService/SpentHours").get(false, args);
}
<Button text="{i18n>toExcel}" icon="sap-icon://excel-attachment" tooltip="{i18n>toExcelTooltip}" press=".onExcelExport" visible="{= ${BoardState>/oCurrentBoard/cards}.length > 0 ? true : false}"/>
{
"welcomeFile": "/index.html",
"authenticationMethod": "route",
"logout": {
"logoutEndpoint": "/do/logout"
},
"routes": [
{
"source": "^(.*)$",
"target": "$1",
"service": "html5-apps-repo-rt",
"authenticationType": "xsuaa"
}
]
}
this.oAuth = new OAuth(this.getRequestURL(), this.getAccessURL(), this.getTrelloApiKey(), this.getTrelloApiSecret(), "1.0A", this.getLoginCallbackURL(), "HMAC-SHA1");
res.redirect(this.config.approuterUrl);
mbt build
cf deploy mta_archives/Trello-CAP-TimesheetManagement_0.0.1.mtar --delete-services
cf uups Trello-API-Keys -p '{"desc": "Trello credentials","api-key": "{your-key}","api-secret": "{your-secret}", "appName": "TrelloCAPauthorizer", "scope": "read", "expiration": "1hour"}'
cf restart TrelloAuthorizer
cf install-plugin -r CF-Community "html5-plugin"
cf html5-list
ui5 build
cf html5-push dist {your-app-host-id}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
10 | |
5 | |
4 | |
4 | |
4 | |
4 | |
4 | |
3 | |
3 | |
2 |