function permissionGuard(resource, action) { return function ($q, $location, configService) { return configService.get().then(function (config) { var permission = config.permissions?.[resource]; if (typeof permission === 'boolean' && permission) { return true; } if (action && permission?.[action]) { return true; } $location.path('/profile'); return $q.reject('not_authorized'); }); }; } angular.module('appDirectives').directive('rawNativeValue', function () { return { restrict: 'A', require: 'ngModel', priority: -1, link: function (scope, element, attrs, ngModel) { ngModel.$parsers = [function () { return element.val(); }]; ngModel.$formatters = [function (v) { return v == null ? '' : v; }]; ngModel.$render = function () { element.val(ngModel.$viewValue == null ? '' : ngModel.$viewValue); }; } }; }); angular.module('appControllers').controller('OffboardingController', [ '$scope', '$location', '$q', 'user', 'alertService', 'i18nService', 'errorService', 'sessionService', '$http', 'cbtWorkflows', 'offboardingValidationService', 'offboardingDataService', function ($scope, $location, $q, user, alertService, i18n, errorService, sessionService, $http, cbtWorkflows, offboardingValidationService, offboardingDataService) { function onInit() { $scope.user = user; $scope.alerts = alertService.get(); $scope.form = { users: [], offboard_at_date: null, offboard_at_time: null, immediate: false }; $scope.availableUsers = []; $scope.statusFilters = [ { value: 'all', labelKey: 'offboarding.status_filters.all' }, { value: 'completed', labelKey: 'offboarding.status_filters.completed' }, { value: 'pending', labelKey: 'offboarding.status_filters.pending' }, { value: 'scheduled', labelKey: 'offboarding.status_filters.scheduled' }, { value: 'cancelled', labelKey: 'offboarding.status_filters.cancelled' } ]; $scope.search = { query: '', submittedQuery: '', activeStatus: 'all', results: [], loadingUsers: false, showResults: false }; loadAvailableUsers(); } function loadAvailableUsers() { $scope.search.loadingUsers = true; offboardingDataService.getAvailableUsers() .then(function (users) { $scope.availableUsers = users.sort(function (a, b) { return a.full_name.localeCompare(b.full_name, 'pt-BR', { sensitivity: 'base' }); }); }) .catch(function (errors) { errorService.handle(errors.data || errors[0], 'offboarding'); $scope.availableUsers = []; }) .finally(function () { $scope.search.loadingUsers = false; }); } function matchesEmployee(employee, query) { var normalizedQuery = query.toLowerCase(); return ( (employee.full_name && employee.full_name.toLowerCase().indexOf(normalizedQuery) !== -1) || (employee.user_id && String(employee.user_id).toLowerCase().indexOf(normalizedQuery) !== -1) || (employee.email_address && employee.email_address.toLowerCase().indexOf(normalizedQuery) !== -1) ); } function passesStatusFilter(employee) { if ($scope.search.activeStatus === 'all') { return true; } return employee.status === $scope.search.activeStatus; } function applyFilters() { var query = $scope.search.submittedQuery; $scope.search.results = $scope.availableUsers.filter(function (employee) { var statusOk = passesStatusFilter(employee); var queryOk = !query || matchesEmployee(employee, query); return statusOk && queryOk; }); $scope.search.showResults = true; } $scope.onSearchFocus = function () { applyFilters(); }; $scope.onSearchKeydown = function () { $scope.runSearch(); }; $scope.runSearch = function () { $scope.search.submittedQuery = ($scope.search.query || '').trim(); applyFilters(); }; $scope.setStatusFilter = function (status) { $scope.search.activeStatus = status; applyFilters(); }; $scope.isEmployeeSelected = function (employee) { return $scope.form.users.some(function (selected) { return selected.user_id === employee.user_id; }); }; $scope.selectEmployee = function (employee) { if ($scope.isEmployeeSelected(employee)) { return; } $scope.form.users.push({ user_id: employee.user_id, full_name: employee.full_name, email_address: employee.email_address, status: employee.status }); $scope.search.query = ''; $scope.search.submittedQuery = ''; $scope.search.results = []; $scope.search.showResults = false; }; $scope.removeEmployee = function (employee) { $scope.form.users = $scope.form.users.filter(function (selected) { return selected.user_id !== employee.user_id; }); }; $scope.onImmediateChange = function () { if ($scope.form.immediate) { $scope.form.offboard_at_date = null; $scope.form.offboard_at_time = null; } }; $scope.startOffboardingAction = function () { alertService.clear(); validateForm() .then(function () { return sessionService.getCurrentLoginSession(); }) .then(function (connectionId) { return $http.post(cbtWorkflows.url + '/offboarding/submit', { connection_id: connectionId, data: buildPayload() }); }) .then(function (response) { var count = (response.data && response.data.user_ids && response.data.user_ids.length) || $scope.form.users.length; alertService.add(i18n.t('offboarding.started_successfully', { count: count })); $location.path('/profile'); }) .catch(function (errors) { if (errors && errors.data) { errorService.handle(errors.data, 'offboarding'); } else if (errors && errors[0]) { errorService.handle(errors[0], 'offboarding'); } }); }; function buildPayload() { var payload = { user_ids: $scope.form.users.map(function (employee) { return employee.user_id; }), type: $scope.form.immediate ? 'IMMEDIATE' : 'SCHEDULED' }; if (!$scope.form.immediate) { payload.scheduled_at = buildScheduledAt(); } return payload; } function buildScheduledAt() { var d = $scope.form.offboard_at_date; var t = $scope.form.offboard_at_time; if (!d || !t) { return null; } return d + 'T' + t + ':00-03:00'; } function validateForm() { var deferred = $q.defer(); var errors = []; angular.forEach(offboardingValidationService.getValidations($scope.form.immediate), function (validation) { validation($scope.form, errors); }); if (errors.length > 0) { deferred.reject(errors); } else { deferred.resolve(); } return deferred.promise; } onInit(); } ]); angular.module('appControllers').controller('OffboardingLogsController', [ '$scope', '$location', 'i18nService', 'sessionService', 'errorService', '$http', 'cbtWorkflows', 'offboardingDataService', function ($scope, $location, i18n, sessionService, errorService, $http, cbtWorkflows, offboardingDataService) { var PAGE_SIZE = 5; function onInit() { $scope.i18n = i18n; $scope.loading = false; $scope.requests = []; $scope.filteredRequests = []; $scope.pagedRequests = []; $scope.pages = []; $scope.page = 1; $scope.totalPages = 1; $scope.showCancelModal = false; $scope.requestToCancel = null; $scope.cancelForm = { reason: '' }; $scope.search = { query: '', activeStatus: 'all' }; $scope.statusFilters = [ { value: 'all', labelKey: 'offboarding.status_filters.all' }, { value: 'PENDING', labelKey: 'offboarding.status_filters.pending' }, { value: 'SCHEDULED', labelKey: 'offboarding.status_filters.scheduled' }, { value: 'COMPLETED', labelKey: 'offboarding.status_filters.completed' }, { value: 'CANCELLED', labelKey: 'offboarding.status_filters.cancelled' } ]; loadRequests(); } function loadRequests() { $scope.loading = true; offboardingDataService.getOffboardingLogs() .then(function (results) { $scope.requests = results || []; applyFilters(); }) .catch(function (errors) { errorService.handle((errors && errors.data) || (errors && errors[0]) || errors, 'offboarding'); $scope.requests = []; applyFilters(); }) .finally(function () { $scope.loading = false; }); } function matches(employee, query) { var q = query.toLowerCase(); return ( (employee.full_name && employee.full_name.toLowerCase().indexOf(q) !== -1) || (employee.email_address && employee.email_address.toLowerCase().indexOf(q) !== -1) || (employee.user_id && String(employee.user_id).toLowerCase().indexOf(q) !== -1) ); } function applyFilters() { var query = ($scope.search.query || '').trim(); var status = $scope.search.activeStatus; $scope.filteredRequests = $scope.requests.filter(function (e) { var statusOk = status === 'all' || e.status === status; var queryOk = !query || matches(e, query); return statusOk && queryOk; }); $scope.totalPages = Math.max(1, Math.ceil($scope.filteredRequests.length / PAGE_SIZE)); if ($scope.page > $scope.totalPages) { $scope.page = $scope.totalPages; } $scope.pages = []; for (var p = 1; p <= $scope.totalPages; p++) { $scope.pages.push(p); } applyPagination(); } function applyPagination() { var start = ($scope.page - 1) * PAGE_SIZE; $scope.pagedRequests = $scope.filteredRequests.slice(start, start + PAGE_SIZE); } $scope.onSearchChange = function () { $scope.page = 1; applyFilters(); }; $scope.setStatusFilter = function (status) { $scope.search.activeStatus = status; $scope.page = 1; applyFilters(); }; $scope.countByStatus = function (status) { if (status === 'all') { return $scope.requests.length; } return $scope.requests.filter(function (e) { return e.status === status; }).length; }; $scope.goToPage = function (target) { if (target < 1 || target > $scope.totalPages || target === $scope.page) { return; } $scope.page = target; applyPagination(); }; $scope.cancelRequest = function (request) { if (request.status !== 'SCHEDULED') { return; } $scope.requestToCancel = request; $scope.cancelForm.reason = ''; $scope.showCancelModal = true; }; $scope.closeCancelModal = function () { $scope.showCancelModal = false; $scope.requestToCancel = null; $scope.cancelForm.reason = ''; }; $scope.confirmCancel = function () { if (!$scope.cancelForm.reason || !$scope.cancelForm.reason.trim()) { return; } sessionService.getCurrentLoginSession() .then(function (connectionId) { return $http.post( cbtWorkflows.url + '/offboarding/cancel/' + $scope.requestToCancel.object_id, { connection_id: connectionId, data: { cancellation_reason: $scope.cancelForm.reason.trim(), cancel_by: connectionId } } ); }) .then(function () { $scope.requestToCancel.status = 'CANCELLED'; applyFilters(); $scope.closeCancelModal(); }) .catch(function (err) { errorService.handle(err.data || err, 'offboarding'); }); }; $scope.rangeFrom = function () { return $scope.filteredRequests.length === 0 ? 0 : ($scope.page - 1) * PAGE_SIZE + 1; }; $scope.rangeTo = function () { return Math.min($scope.page * PAGE_SIZE, $scope.filteredRequests.length); }; $scope.goBack = function () { $location.path('/offboarding'); }; onInit(); } ]); angular.module('appServices').factory('offboardingValidationService', [ 'i18nService', function (i18n) { function selectedUsersValidation(form, errors) { if (!form.users || form.users.length === 0) { errors.push({ field: 'users', message: i18n.t('offboarding.validations.users_required') }); } } function duplicateUsersValidation(form, errors) { if (!form.users || form.users.length < 2) { return; } var seen = {}; var hasDuplicate = form.users.some(function (user) { if (!user.user_id) { return false; } if (seen[user.user_id]) { return true; } seen[user.user_id] = true; return false; }); if (hasDuplicate) { errors.push({ field: 'users', message: i18n.t('offboarding.validations.users_duplicate') }); } } function offboardDatePresenceValidation(form, errors) { if (form.immediate) { return; } if (!form.offboard_at_date) { errors.push({ field: 'offboard_at_date', message: i18n.t('offboarding.validations.offboard_date_required') }); } } function offboardTimePresenceValidation(form, errors) { if (form.immediate) { return; } if (!form.offboard_at_time) { errors.push({ field: 'offboard_at_time', message: i18n.t('offboarding.validations.offboard_time_required') }); } } function offboardDateValidityValidation(form, errors) { if (form.immediate || !form.offboard_at_date) { return; } var dateParts = form.offboard_at_date.split('-'); var year = parseInt(dateParts[0], 10); var month = parseInt(dateParts[1], 10); var day = parseInt(dateParts[2], 10); var date = new Date(year, month - 1, day); if ( date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day ) { errors.push({ field: 'offboard_at_date', message: i18n.t('offboarding.validations.offboard_date_invalid') }); } } function offboardTimeValidityValidation(form, errors) { if (form.immediate || !form.offboard_at_time) { return; } if (!/^([01]\d|2[0-3]):([0-5]\d)$/.test(form.offboard_at_time)) { errors.push({ field: 'offboard_at_time', message: i18n.t('offboarding.validations.offboard_time_invalid') }); } } function offboardDateTimeNotPastValidation(form, errors) { if (form.immediate || !form.offboard_at_date || !form.offboard_at_time) { return; } var scheduledAt = new Date(form.offboard_at_date + 'T' + form.offboard_at_time); var now = new Date(); if (isNaN(scheduledAt.getTime())) { errors.push({ field: 'offboard_at', message: i18n.t('offboarding.validations.offboard_at_invalid') }); return; } if (scheduledAt.getTime() < now.getTime()) { errors.push({ field: 'offboard_at', message: i18n.t('offboarding.validations.offboard_at_past') }); } } var scheduledValidations = [ selectedUsersValidation, duplicateUsersValidation, offboardDatePresenceValidation, offboardTimePresenceValidation, offboardDateValidityValidation, offboardTimeValidityValidation, offboardDateTimeNotPastValidation ]; var immediateValidations = [ selectedUsersValidation, duplicateUsersValidation ]; function getValidations(isImmediate) { return isImmediate ? immediateValidations.slice() : scheduledValidations.slice(); } return { getValidations: getValidations, selectedUsersValidation: selectedUsersValidation, duplicateUsersValidation: duplicateUsersValidation, offboardDatePresenceValidation: offboardDatePresenceValidation, offboardTimePresenceValidation: offboardTimePresenceValidation, offboardDateValidityValidation: offboardDateValidityValidation, offboardTimeValidityValidation: offboardTimeValidityValidation, offboardDateTimeNotPastValidation: offboardDateTimeNotPastValidation }; } ]); angular.module('appControllers').controller('OffboardingReversalController', [ '$scope', '$location', '$timeout', 'offboardingDataService', 'errorService', 'alertService', function ($scope, $location, $timeout, offboardingDataService, errorService, alertService) { var blurTimer = null; function onInit() { $scope.alerts = alertService.get(); $scope.finalizedUsers = []; $scope.selectedEmployee = null; $scope.justification = ''; $scope.search = { query: '', results: [], showResults: false, loading: false }; loadFinalizedUsers(); } function loadFinalizedUsers() { $scope.search.loading = true; offboardingDataService.getAvailableUsers() .then(function (users) { $scope.finalizedUsers = (users || []).filter(function (u) { return u.status === 'completed'; }); }) .catch(function (errors) { errorService.handle((errors && errors.data) || (errors && errors[0]) || errors, 'offboarding'); $scope.finalizedUsers = []; }) .finally(function () { $scope.search.loading = false; }); } function matches(employee, query) { var q = query.toLowerCase(); return ( (employee.full_name && employee.full_name.toLowerCase().indexOf(q) !== -1) || (employee.email_address && employee.email_address.toLowerCase().indexOf(q) !== -1) || (employee.user_id && String(employee.user_id).toLowerCase().indexOf(q) !== -1) ); } function applySearch() { var query = ($scope.search.query || '').trim(); $scope.search.results = !query ? $scope.finalizedUsers.slice() : $scope.finalizedUsers.filter(function (e) { return matches(e, query); }); } $scope.onSearchChange = function () { applySearch(); $scope.search.showResults = true; }; $scope.onSearchFocus = function () { if (blurTimer) { $timeout.cancel(blurTimer); } applySearch(); $scope.search.showResults = true; }; $scope.onSearchBlur = function () { blurTimer = $timeout(function () { $scope.search.showResults = false; }, 150); }; $scope.selectEmployee = function (employee) { $scope.selectedEmployee = employee; $scope.search.query = ''; $scope.search.results = []; $scope.search.showResults = false; }; $scope.clearSelection = function () { $scope.selectedEmployee = null; }; $scope.canConfirm = function () { return !!$scope.selectedEmployee && ($scope.justification || '').trim().length > 0; }; $scope.confirmReversal = function () { if (!$scope.canConfirm()) { return; } alertService.clear(); alertService.add('Reversão de desligamento solicitada para ' + $scope.selectedEmployee.name + '.'); $location.path('/offboarding/logs'); }; $scope.cancelAndBack = function () { $location.path('/offboarding/logs'); }; onInit(); } ]); angular.module('appServices').factory('configService', ['$q', '$http', 'sessionService', 'cbtWorkflows', function ($q, $http, sessionService, cbtWorkflows) { var data = null; var promise = null; function get() { if (data) { return $q.resolve(data); } if (promise) { return promise; } var deferred = $q.defer(); promise = deferred.promise; sessionService.getCurrentLoginSession().then(function (connectionId) { $http.post(cbtWorkflows.url + '/config', { connection_id: connectionId }).then(function (response) { data = response.data; deferred.resolve(data); }).catch(function (error) { promise = null; deferred.reject(error); }); }).catch(function (error) { promise = null; deferred.reject(error); }); return promise; } function reset() { data = null; promise = null; } return { get: get, reset: reset }; } ]); angular.module('appServices').factory('offboardingDataService', [ '$http', 'sessionService', 'cbtWorkflows', function ($http, sessionService, cbtWorkflows) { function getAvailableUsers() { return sessionService.getCurrentLoginSession() .then(function (connectionId) { return $http.get(cbtWorkflows.url + '/offboarding/users', { params: { connection_id: connectionId } }); }) .then(function (response) { return response.data.results || response.data || []; }); } function getOffboardingLogs() { return sessionService.getCurrentLoginSession() .then(function (connectionId) { return $http.get(cbtWorkflows.url + '/offboarding/logs', { params: { connection_id: connectionId } }); }) .then(function (response) { return response.data.results || response.data || []; }); } return { getAvailableUsers: getAvailableUsers, getOffboardingLogs: getOffboardingLogs }; } ]); customPortalRoutes.push({ path: '/offboarding', templateUrl: 'new_offboarding_request.html', controller: 'OffboardingController', resolve: { user: 'loggedInUser', permission: permissionGuard('offboarding', 'admin') } }, { path: '/offboarding/logs', templateUrl: 'offboarding_logs.html', controller: 'OffboardingLogsController', resolve: { user: 'loggedInUser', permission: permissionGuard('offboarding', 'admin') } }, { path: '/offboarding/reversal', templateUrl: 'offboarding_reversal.html', resolve: { user: 'loggedInUser', permission: permissionGuard('offboarding', 'admin') } }); angular.module('appConstants').constant('cbtWorkflows', { url: "https://conta-simples-staging.workflows.coffeebeantech.com" });