Frontend Architecture¶
The Stratpoint Timesheet Application frontend is built as a traditional AngularJS 1.x application with a flat file structure, focusing on functionality over architectural complexity. The application follows standard AngularJS patterns with minimal abstraction layers.
Actual Architecture Overview¶
File Organization Structure¶
graph TB
A[StratpointTSApp] --> B[Main Module]
B --> C[Controller Files]
B --> D[services.js]
B --> E[directives.js]
B --> F[app.module.js]
B --> G[navmenu.js]
C --> H[controllers_auth.js]
C --> I[controllers_timelogs.js]
C --> J[controllers_projects.js]
C --> K[controllers_dashboard.js]
C --> L[controllers_users.js]
C --> M[controllers_reports.js]
C --> N[Other controller files...]
O[Third-party Libraries] --> P[Bootstrap 3.x]
O --> Q[ng-table]
O --> R[ui.bootstrap]
O --> S[angular-file-upload]
O --> T[ui.calendar]
B --> O
Real Module Structure¶
// From actual codebase - all defined on main module
var StratpointTSApp = angular.module('StratpointTSApp', [
// Core Angular modules
'ngRoute',
'ngResource',
'ngSanitize',
'ngCookies',
'ngAnimate',
// Third-party modules
'ngTable',
'ngTableExport',
'ngCsv',
'ui.bootstrap',
'ui.calendar',
'ui.select',
'ui.sortable',
'nsPopover',
'ngTagsInput',
'ngLoadingSpinner',
'angularFileUpload',
// Custom modules
'stratpointtsCrud',
'StratpointTSControllers'
]);
Actual File Structure¶
Controllers Organization¶
Controllers are organized in separate files by functional area:
/public/app/js/mine/
├── app.module.js # Main module definition
├── services.js # ALL services in one file
├── directives.js # ALL directives in one file
├── navmenu.js # Navigation menu logic
├── controllers_auth.js # Authentication controllers
├── controllers_timelogs.js # Timelog controllers
├── controllers_projects.js # Project controllers
├── controllers_dashboard.js # Dashboard controllers
├── controllers_users.js # User controllers
├── controllers_reports.js # Report controllers
├── controllers_revenues.js # Revenue controllers
├── controllers_profits.js # Profit controllers
├── controllers_leaves.js # Leave controllers
├── controllers_deals.js # Deal controllers
└── ... (other controller files)
Service Architecture¶
All services are defined directly on the main StratpointTSApp module in a single file:
// From /public/app/js/mine/services.js
StratpointTSApp
.factory('BasicData', ['$http','$resource', '$q', '$filter', '$timeout', 'BASE_URL', 'Cache',
function($http, $resource, $q, $filter, $timeout, BASE_URL, Cache){
// Generic CRUD service implementation
var rs = $resource(BASE_URL + "/api/v2/:backendRoute/:id", {id: '@id', backendRoute: '@backendRoute'}, {
update: { method: 'PUT' },
Add: { method: 'POST' },
Edit: { method: 'PUT' }
});
rs.makeGet = function(url, data) {
var deferred = $q.defer();
var cacheName = url + JSON.stringify(data || {});
if(typeof (result = Cache.get(cacheName)) == 'undefined') {
$http.get(BASE_URL + "/api/v2/" + url, {params: data}).then(function(response){
Cache.put(cacheName, response.data);
deferred.resolve(response.data);
}, function(error) {
deferred.reject(error);
});
} else {
deferred.resolve(angular.copy(result));
}
return deferred.promise;
};
return rs;
}
])
.factory('Authenticate', ['$cookieStore', '$rootScope', '$location', '$q', 'Flash', 'BASE_URL', 'ActiveForm',
function($cookieStore, $rootScope, $location, $q, Flash, BASE_URL, ActiveForm){
// Authentication service implementation
return {
setUser: function(userData, remember) {
if (remember) {
localStorage['timesheet.user'] = JSON.stringify(userData);
} else {
$cookieStore.put('timesheet.user', userData);
}
$rootScope.currentUser = userData.user;
$rootScope.currentToken = userData.access_token;
$rootScope.userId = userData.user.id;
},
getUser: function() {
var user = localStorage['timesheet.user'] || $cookieStore.get('timesheet.user');
return user ? (typeof user === 'string' ? JSON.parse(user) : user) : null;
},
hasPermission: function(permissionId) {
var permissions = $rootScope.loggedPermissionList || [];
return permissions.indexOf(permissionId.toString()) !== -1;
}
};
}
]);
Directive Architecture¶
All directives are defined directly on the main StratpointTSApp module:
// From /public/app/js/mine/directives.js
StratpointTSApp
.directive('hasPermission', ["Authenticate", function(Authenticate) {
return {
link: function(scope, element, attrs) {
if(!angular.isString(attrs.hasPermission))
throw "hasPermission value must be a string";
var value = attrs.hasPermission.trim();
var notPermissionFlag = value[0] === '!';
if(notPermissionFlag) {
value = value.slice(1).trim();
}
function toggleVisibilityBasedOnPermission() {
var hasPermission = Authenticate.hasPermission(value);
if( !(hasPermission && !notPermissionFlag || !hasPermission && notPermissionFlag ) ){
element.remove();
}
}
toggleVisibilityBasedOnPermission();
scope.$on('permissionsChanged', toggleVisibilityBasedOnPermission);
}
};
}])
.directive('loadingContainer', function () {
return {
restrict: 'A',
scope: false,
link: function(scope, element, attrs) {
var loadingLayer = angular.element('<div class="loading"></div>');
element.append(loadingLayer);
element.addClass('loading-container');
scope.$watch(attrs.loadingContainer, function(value) {
loadingLayer.toggleClass('ng-hide', !value);
});
}
};
});
Routing Architecture¶
Basic Route Configuration¶
The application uses simple AngularJS routing without complex resolvers:
// Basic route structure
$routeProvider
.when('/', {
templateUrl: 'app/partials/login.html',
controller: 'LoginController'
})
.when('/dashboard', {
templateUrl: 'app/partials/dashboard/index.html',
controller: 'DashboardController'
})
.when('/tasks/:id?', {
templateUrl: 'app/partials/tasks/index.html',
controller: 'TaskController'
});
Route Protection: Permission checking is handled in controllers, not route resolvers:
// In controller initialization
if (!hasPermission($scope.authUser, 57)) {
return respondAccessNotAllowed('Timelogs');
}
Controller Implementation Patterns¶
Actual Controller Structure¶
Controllers follow a consistent but basic pattern:
// Example from controllers_timelogs.js
StratpointTSControllers.controller('TimelogController', [
'$scope', '$location', '$routeParams', 'BasicData', 'Flash', 'Cache',
function($scope, $location, $routeParams, BasicData, Flash, Cache) {
// Initialize scope variables
$scope.timelogs = [];
$scope.filters = {};
$scope.loading = false;
// Permission check
if (!hasPermission($scope.authUser, 57)) {
Flash.show('Access not allowed', 'error');
$location.path('/dashboard');
return;
}
// Load data function
$scope.loadTimelogs = function() {
$scope.loading = true;
BasicData.makeGet('timelogs', $scope.filters)
.then(function(response) {
if (response.header.status === 200) {
$scope.timelogs = response.body.data;
$scope.pagination = response.body.pagination;
}
})
.finally(function() {
$scope.loading = false;
});
};
// Initialize
$scope.loadTimelogs();
}
]);
Authentication Flow¶
// Simplified authentication in controllers
app.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.$on('$routeChangeStart', function(event, next, current) {
// Basic authentication check
if (!$rootScope.currentUser && next.templateUrl !== 'app/partials/login.html') {
event.preventDefault();
$location.path('/login');
}
});
}]);
Data Management¶
API Communication¶
The application uses a simple API service pattern:
// BasicData service methods
BasicData.makeGet(endpoint, params) // GET requests with caching
BasicData.makePost(data) // POST requests
BasicData.makePut(data) // PUT requests
BasicData.makeDelete(data) // DELETE requests
Caching Strategy¶
// Simple in-memory caching with IndexedDB fallback
StratpointTSApp.factory('Cache', function() {
var memoryCache = {};
return {
put: function(key, value, timeout) {
memoryCache[key] = {
value: value,
timestamp: Date.now(),
timeout: (timeout || CACHE_TIMEOUT) * 1000
};
// Also store in IndexedDB
indexedDBDataSvc.setData(key, value);
},
get: function(key) {
var cached = memoryCache[key];
if (cached) {
var age = Date.now() - cached.timestamp;
if (age < cached.timeout) {
return cached.value;
} else {
delete memoryCache[key];
}
}
// Try IndexedDB as fallback
return indexedDBDataSvc.getData(key);
},
clear: function(pattern) {
if (pattern) {
Object.keys(memoryCache).forEach(function(key) {
if (key.indexOf(pattern) !== -1) {
delete memoryCache[key];
}
});
} else {
memoryCache = {};
}
indexedDBDataSvc.clearData(pattern);
}
};
});
UI Component Integration¶
ng-table Implementation¶
// Standard ng-table usage pattern
$scope.tableParams = new NgTableParams({
page: 1,
count: 50
}, {
getData: function(params) {
return BasicData.makeGet('timelogs', {
page: params.page(),
per_page: params.count()
}).then(function(response) {
params.total(response.body.pagination.total);
return response.body.data;
});
}
});
File Upload Integration¶
// Angular file upload configuration
$scope.uploadOptions = {
url: BASE_URL + '/api/v2/files/upload',
headers: {
'Authorization': 'Bearer ' + $rootScope.currentToken
},
onSuccessItem: function(fileItem, response, status, headers) {
Flash.show('File uploaded successfully', 'success');
$scope.refreshFiles();
}
};
Calendar Integration¶
// FullCalendar basic configuration
$scope.calendarConfig = {
calendar: {
height: 450,
editable: false,
eventSources: [{
url: BASE_URL + '/api/v2/calendar/events',
type: 'GET'
}]
}
};
Key Architectural Characteristics¶
- Flat File Structure: All services in
services.js, all directives indirectives.js - Monolithic Module: Everything defined on the main
StratpointTSAppmodule - Controller-Heavy Logic: Business logic primarily in controllers
- Simple Service Layer: Basic CRUD operations via
BasicDataservice - Permission-Based UI:
has-permissiondirective controls element visibility - Basic Caching: In-memory cache with IndexedDB persistence
- Standard AngularJS Patterns: Traditional directive, service, and controller implementations
- Third-Party Integration: Heavy use of ng-table, ui.bootstrap, and angular-file-upload
The architecture prioritizes simplicity and functionality over complex patterns, making it suitable for an enterprise timesheet application with straightforward requirements.