Skip to content

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

  1. Flat File Structure: All services in services.js, all directives in directives.js
  2. Monolithic Module: Everything defined on the main StratpointTSApp module
  3. Controller-Heavy Logic: Business logic primarily in controllers
  4. Simple Service Layer: Basic CRUD operations via BasicData service
  5. Permission-Based UI: has-permission directive controls element visibility
  6. Basic Caching: In-memory cache with IndexedDB persistence
  7. Standard AngularJS Patterns: Traditional directive, service, and controller implementations
  8. 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.