diff --git a/core/admin/clients/index.php b/core/admin/clients/index.php index 3fcca1fdc..86c38e066 100644 --- a/core/admin/clients/index.php +++ b/core/admin/clients/index.php @@ -48,4 +48,4 @@ var modJs = modJsList['tabClient']; - + \ No newline at end of file diff --git a/core/admin/connection/index.php b/core/admin/connection/index.php index 5b417bbba..629ee3ca3 100644 --- a/core/admin/connection/index.php +++ b/core/admin/connection/index.php @@ -15,7 +15,7 @@ $data = json_decode($data, true); } -$employeeCount = StatsHelper::getEmployeeCount(); +$employeeCount = StatsHelper::getActiveEmployeeCount(); $userCount = StatsHelper::getUserCount(); $connectionService = new ConnectionService(); ?>
diff --git a/core/admin/dashboard/index.php b/core/admin/dashboard/index.php index 3f25fcba8..a7557de52 100644 --- a/core/admin/dashboard/index.php +++ b/core/admin/dashboard/index.php @@ -67,7 +67,7 @@
diff --git a/core/admin/documents/index.php b/core/admin/documents/index.php index c212740d1..88c7f41d4 100644 --- a/core/admin/documents/index.php +++ b/core/admin/documents/index.php @@ -14,9 +14,10 @@ $activeStr = 'active'; } -$moduleBuilder = new \Classes\ModuleBuilder\ModuleBuilder(); +$moduleBuilder = new \Classes\ModuleBuilderV2\ModuleBuilder(); if($user->user_level == "Admin") { - $moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilder\ModuleTab( + $moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilderV2\ModuleTab( + '\Documents\Common\Model\CompanyDocument', 'CompanyDocument', 'CompanyDocument', 'Company Documents', @@ -25,7 +26,8 @@ '', true )); - $moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilder\ModuleTab( + $moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilderV2\ModuleTab( + '\Documents\Common\Model\Document', 'Document', 'Document', 'Document Types', @@ -36,7 +38,8 @@ )); $options1 = array(); $options1['setRemoteTable'] = 'true'; - $moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilder\ModuleTab( + $moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilderV2\ModuleTab( + '\Documents\Common\Model\EmployeeDocument', 'EmployeeDocument', 'EmployeeDocument', 'Employee Documents', @@ -49,7 +52,8 @@ }else{ $options1 = array(); $options1['setRemoteTable'] = 'true'; - $moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilder\ModuleTab( + $moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilderV2\ModuleTab( + '\Documents\Common\Model\EmployeeDocument', 'EmployeeDocument', 'EmployeeDocument', 'Employee Documents', diff --git a/core/admin/modules/index.php b/core/admin/modules/index.php index a2cda9536..c401dfd02 100644 --- a/core/admin/modules/index.php +++ b/core/admin/modules/index.php @@ -9,137 +9,14 @@ define('MODULE_PATH',dirname(__FILE__)); include APP_BASE_PATH.'header.php'; include APP_BASE_PATH.'modulejslibs.inc.php'; -$groupsStr = \Classes\SettingsManager::getInstance()->getSetting("Modules : Group"); -$groups = array(); -if(!empty($groupsStr)){ - $groups = explode(",",$groupsStr); -} -if(empty($groups)){ - $groups[] = 'all'; -} ?>
-
-
-
-

How Do You Want to Use IceHrm

- -

- In order to make IceHrm user interface much simpler to use for you and your employees you - can select the purpose of using IceHrm for your company. This will disable unwanted modules - and provide you a better user experience. -

-
- - -
- - -
-
-
-

- -  Use All Available Modules

-

- Use all the Available Modules in IceHrm. This option will enable all the modules - including Employee Management, Leave Management, Time Sheets, Attendance, Training, - Expenses, Document Management, Travel, Recruitment Management and Payroll -

-
-
-

- -  Leave Management System -

-

- Use IceHrm as a Leave / Vacation Management System, Allow Employees to Apply for leave, - Approve leave requests and track leave balances -

-
-
-

- -  Document Management System -

-

- Use IceHrm as a Document Management System, Allow Employees upload documents, Automated - notifications for expiring documents, Add company documents and share with specific - employees or departments. -

-
-
-
-
-

- -  Time Tracking System -

-


- Use IceHrm as an Attendance Management and Time Tracking System. Let employees record - attendance and fill in time sheets. -

-
-
-

- -  Training Management System -

-

- Use IceHrm as a Training Management System. Create courses and training sessions. Let - employees subscribe to training sessions and allow them to submit feedback with training - certificates for auditing purposes. -

-
-
-

- -  Expense and Travel Management -

-

- Get your employees to submit expense claims and let managers approve. Also combine - approved expenses with payroll module to have those added to employees salary. - Also you can track and approve employee travel requests -

-
-
- -
-
-

- -  Applicant Tracking System -

-

- Define available vacancies in your company and track applicants. Schedule interviews - and track progress of your candidates -

-
-
-

- -  Salary and Payroll -

-

- Process your company payroll using IceHrm and Store employee salary -

-
-
- -
- - -
-
-
+
@@ -155,37 +32,6 @@ modJsList['tabModule'] = new ModuleAdapter('Module','Module'); modJsList['tabModule'].setShowAddNew(false); - -modJsList['tabUsage'] = new UsageAdapter('Usage','Usage'); -var modJs = modJsList['tabUsage']; - - $(document).ready(function(){ - - - $("#all").click(function() { - if($(this).is(":checked")) { - - $('.module-check').each(function(){ - if($(this).val() != 'all'){ - $(this).removeAttr('checked'); - } - }); - } - }); - - $(".module-check").click(function() { - if($(this).val() != 'all') { - $("#all").removeAttr('checked'); - } - }); - - $('.module-check').each(function(){ - if(jQuery.inArray($(this).val(), ) !== -1){ - $(this).attr('checked','checked'); - } - - }); - }) - +var modJs = modJsList['tabModule']; diff --git a/core/admin/overtime/index.php b/core/admin/overtime/index.php index a6b8d18fb..30f01fe26 100644 --- a/core/admin/overtime/index.php +++ b/core/admin/overtime/index.php @@ -1,30 +1,53 @@
-$options = array(); -$options['setRemoteTable'] = 'true'; + -$moduleBuilder = new \Classes\ModuleBuilder\ModuleBuilder(); -$moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilder\ModuleTab( - 'OvertimeCategory','OvertimeCategory','Overtime Categories','OvertimeCategoryAdapter','','',true,$options -)); -$moduleBuilder->addModuleOrGroup(new \Classes\ModuleBuilder\ModuleTab( - 'EmployeeOvertime','EmployeeOvertime','Overtime Requests','EmployeeOvertimeAdminAdapter','','',false,$options -)); -echo \Classes\UIManager::getInstance()->renderModule($moduleBuilder); +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $user->user_level, + 'permissions' => [ + 'OvertimeCategory' => PermissionManager::checkGeneralAccess(new OvertimeCategory()), + 'EmployeeOvertime' => PermissionManager::checkGeneralAccess(new EmployeeOvertime()), + ] +]; +?> + + -$itemName = 'OvertimeRequest'; -$moduleName = 'Time Management'; -$itemNameLower = strtolower($itemName); -include APP_BASE_PATH.'footer.php'; diff --git a/core/api-rest.php b/core/api-rest.php index fc77d6fc2..ef0f9b920 100644 --- a/core/api-rest.php +++ b/core/api-rest.php @@ -13,7 +13,8 @@ } - \Utils\LogManager::getInstance()->info("Request: " . $_REQUEST); + \Utils\LogManager::getInstance()->info("Request: " . print_r($_REQUEST, true)); + \Utils\LogManager::getInstance()->info("REST_API_PATH: " . REST_API_PATH); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); @@ -24,7 +25,7 @@ echo "Echo " . rand(); }); - \Utils\LogManager::getInstance()->debug('Api registered URI: '.$echoRoute); + \Utils\LogManager::getInstance()->info('Api registered URI: '.$echoRoute); $moduleManagers = \Classes\BaseService::getInstance()->getModuleManagers(); diff --git a/core/api-url-based.php b/core/api-url-based.php index 2051d099a..c3572cb7c 100644 --- a/core/api-url-based.php +++ b/core/api-url-based.php @@ -36,4 +36,4 @@ }else{ echo "REST Api is not enabled. Please set 'Api: REST Api Enabled' setting to true"; -} +} \ No newline at end of file diff --git a/core/config.base.php b/core/config.base.php index 7e25f7ba8..9a8891a34 100644 --- a/core/config.base.php +++ b/core/config.base.php @@ -13,10 +13,10 @@ } //Version -define('VERSION', '30.0.0.OS'); -define('CACHE_VALUE', '30.0.0.OS.2021-06261009'); -define('VERSION_NUMBER', '300000'); -define('VERSION_DATE', '26/06/2021'); +define('VERSION', '31.0.0.OS'); +define('CACHE_VALUE', '31.0.0.OS.2022-01141009'); +define('VERSION_NUMBER', '310000'); +define('VERSION_DATE', '14/01/2022'); if(!defined('CONTACT_EMAIL')){define('CONTACT_EMAIL','icehrm@gamonoid.com');} if(!defined('KEY_PREFIX')){define('KEY_PREFIX','IceHrm');} diff --git a/core/data.php b/core/data.php index 2e0bc6363..60c4cc7cc 100644 --- a/core/data.php +++ b/core/data.php @@ -88,6 +88,26 @@ } } + $searchTerm = $_REQUEST['sSearch']; + $searchColumns = $_REQUEST['cl']; + $searchQuery = ''; + $searchQueryData = []; + if (!empty($searchTerm) && !empty($searchColumns)) { + $searchColumnList = json_decode($searchColumns); + $searchColumnList = array_diff($searchColumnList, $obj->getVirtualFields()); + if (!empty($searchColumnList)) { + $searchQuery = " and ("; + foreach ($searchColumnList as $col) { + if ($searchQuery != " and (") { + $searchQuery.=" or "; + } + $searchQuery.=$col." like ?"; + $searchQueryData[] = "%".$searchTerm."%"; + } + $searchQuery.=")"; + } + } + if (in_array($table, \Classes\BaseService::getInstance()->userTables) && !$skipProfileRestriction && !$isSubOrdinates) { @@ -169,14 +189,12 @@ } $sql = "Select count(id) as count from " . $obj->_table . " where " . $obj->getUserOnlyMeAccessField() . " in (" . $subordinatesIds . ") " - . $countFilterQuery; - $rowCount = $obj->DB()->Execute($sql, $countFilterQueryData); + . $countFilterQuery.$searchQuery; + $rowCount = $obj->DB()->Execute($sql, array_merge($countFilterQueryData, $searchQueryData)); } else { $sql = "Select count(id) as count from " . $obj->_table; - if (!empty($countFilterQuery)) { - $sql .= " where 1=1 " . $countFilterQuery; - } - $rowCount = $obj->DB()->Execute($sql, $countFilterQueryData); + $sql .= " where 1=1 " . $countFilterQuery.$searchQuery; + $rowCount = $obj->DB()->Execute($sql, array_merge($countFilterQueryData, $searchQueryData)); } } } diff --git a/core/extensions/wrapper.php b/core/extensions/wrapper.php new file mode 100644 index 000000000..30efd38b7 --- /dev/null +++ b/core/extensions/wrapper.php @@ -0,0 +1,31 @@ +getExtensionMetaData($moduleName); +if (!$meta) { + LogManager::getInstance()->error("Extension metadata.json not found for $moduleName"); + exit(); +} + +if ($meta->headless) { + LogManager::getInstance()->error("Extension running in headless mode for $moduleName"); + exit(); +} +?> + + + + + + + diff --git a/core/fileupload-new.php b/core/fileupload-new.php new file mode 100644 index 000000000..f156573ac --- /dev/null +++ b/core/fileupload-new.php @@ -0,0 +1,207 @@ +allowedExtensions = $allowedExtensions; + $this->sizeLimit = $sizeLimit; + $this->file = new IceFileUploader(); + } + + private function toBytes($str) + { + $val = trim($str); + $last = strtolower($str[strlen($str)-1]); + switch ($last) { + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + return $val; + } + + /** + * Returns array('success'=>1) or array('error'=>'error message') + */ + function handleUpload($uploadDirectory, $saveFileName, $replaceOldFile = false) + { + if (!is_writable($uploadDirectory)) { + LogManager::getInstance()->debug("Server error. Upload directory is not writable"); + return array('success'=>0,'error' => "Server error. Upload directory is not writable"); + } + + if (!$this->file->getName()) { + LogManager::getInstance()->debug('No files were uploaded.'); + return array('success'=>0,'error' => 'No files were uploaded.'); + } + + $size = $this->file->getSize(); + + if ($size == 0) { + LogManager::getInstance()->debug('Could not calculate the file size'); + return array('success'=>0,'error' => 'Could not upload the file. ' . + 'The upload was cancelled, or the server rejected the file'); + } + + if ($size > $this->sizeLimit) { + LogManager::getInstance()->debug('File size is larger than allowed max limit'); + LogManager::getInstance()->debug('file size ='.$size); + LogManager::getInstance()->debug('file size limit ='.$this->sizeLimit); + return array('success'=>0,'error' => 'File is too large'); + } + + $pathinfo = pathinfo($this->file->getName()); + $ext = $pathinfo['extension']; + + if ($this->allowedExtensions && !in_array(strtolower($ext), $this->allowedExtensions)) { + $these = implode(', ', $this->allowedExtensions); + LogManager::getInstance()->debug('File has an invalid extension, it should be one of '. $these . '.'); + return array('success'=>0,'error' => 'File has an invalid extension, it should be one of '. $these . '.'); + } + $filename = $saveFileName; // file with only name + $saveFileName = $saveFileName.'.'.strtolower($ext); // file with extention + + $final_img_location = $uploadDirectory . $saveFileName; + + if ($this->file->save($final_img_location)) { + $arr = explode("/", $final_img_location); + return array('success'=>1,'filename'=>$arr[count($arr)-1],'error'=>''); + } else { + LogManager::getInstance()->debug('Error occurred while saving the file'); + return array('success'=>0,'error'=> 'Could not save uploaded file.' . + 'The upload was cancelled, or server error encountered'); + } + } +} +//Generate File Name +$saveFileName = $_REQUEST['file_name']; +$saveFileName = str_replace("..", "", $saveFileName); +$saveFileName = str_replace("/", "", $saveFileName); + +if (stristr($saveFileName, ".php")) { + $saveFileName = str_replace(".php", "", $saveFileName); +} + +if (empty($saveFileName) || $saveFileName == "_NEW_") { + $saveFileName = microtime(); + $saveFileName = str_replace(".", "-", $saveFileName); +} + +$file = new \Model\File(); +$file->Load("name = ?", array($saveFileName)); + +// list of valid extensions, ex. array("jpeg", "xml", "bmp") + +$allowedExtensions = explode(',', "csv,doc,xls,docx,xlsx,txt,ppt,pptx,rtf,pdf,xml,jpg,bmp,gif,png,jpeg"); +// max file size in bytes +$sizeLimit =MAX_FILE_SIZE_KB * 1024; +$uploader = new qqFileUploader($allowedExtensions, $sizeLimit); +$result = $uploader->handleUpload(BaseService::getInstance()->getDataDirectory(), $saveFileName); +// to pass data through iframe you will need to encode all html tags + +$uploadFilesToS3 = SettingsManager::getInstance()->getSetting("Files: Upload Files to S3"); +$uploadFilesToS3Key = SettingsManager::getInstance()->getSetting("Files: Amazon S3 Key for File Upload"); +$uploadFilesToS3Secret = SettingsManager::getInstance()->getSetting( + "Files: Amazon S3 Secret for File Upload" +); +$s3Bucket = SettingsManager::getInstance()->getSetting("Files: S3 Bucket"); +$s3WebUrl = SettingsManager::getInstance()->getSetting("Files: S3 Web Url"); + +$uploadedToS3 = false; + +$localFile = BaseService::getInstance()->getDataDirectory().$result['filename']; +$f_size = filesize($localFile); +if ($uploadFilesToS3.'' == '1' && !empty($uploadFilesToS3Key) && !empty($uploadFilesToS3Secret) && + !empty($s3Bucket) && !empty($s3WebUrl)) { + $uploadname = CLIENT_NAME."/".$result['filename']; + LogManager::getInstance()->debug("Upload file to s3:".$uploadname); + LogManager::getInstance()->debug("Local file:".$localFile); + LogManager::getInstance()->debug("Local file size:".$f_size); + + + $s3FileSys = new \Classes\S3FileSystem($uploadFilesToS3Key, $uploadFilesToS3Secret); + $res = $s3FileSys->putObject($s3Bucket, $uploadname, $localFile, 'authenticated-read'); + + $file_url = $s3WebUrl.$uploadname; + $file_url = $s3FileSys->generateExpiringURL($file_url); + LogManager::getInstance()->info("Response from s3 file sys:".print_r($res, true)); + unlink($localFile); + + $uploadedToS3 = true; +} + +if ($result['success'] == 1) { + $file->name = $saveFileName; + $file->filename = $result['filename']; + $signInMappingField = SIGN_IN_ELEMENT_MAPPING_FIELD_NAME; + $file->$signInMappingField = $_REQUEST['user']=="_NONE_"?null:$_REQUEST['user']; + $file->file_group = $_REQUEST['file_group']; + $file->size = $f_size; + $file->size_text = FileService::getInstance()->getReadableSize($f_size); + $file->Save(); + if ($uploadedToS3) { + $url = $file_url; + } else { + $url = \Classes\FileService::getInstance()->getLocalSecureUrl($result['filename']); + } + + echo json_encode([ + 'name' => $file->name, + 'status' => 'success', + 'url' => $url, + ]); +} else { + echo json_encode([ + 'status' => 'error', + 'message' => $result['error'], + ]); +} diff --git a/core/fileupload.php b/core/fileupload.php index 8428ab4c0..21e1c3acd 100644 --- a/core/fileupload.php +++ b/core/fileupload.php @@ -3,6 +3,10 @@ /** * Handle file uploads via XMLHttpRequest */ + +use Classes\BaseService; +use Classes\SettingsManager; + include ("config.base.php"); include ("include.common.php"); include_once ('server.includes.inc.php'); @@ -133,20 +137,20 @@ function handleUpload($uploadDirectory,$saveFileName, $replaceOldFile = FALSE){ // max file size in bytes $sizeLimit =MAX_FILE_SIZE_KB * 1024; $uploader = new qqFileUploader($allowedExtensions, $sizeLimit); -$result = $uploader->handleUpload(CLIENT_BASE_PATH.'data/',$saveFileName); +$result = $uploader->handleUpload(BaseService::getInstance()->getDataDirectory(),$saveFileName); // to pass data through iframe you will need to encode all html tags -$uploadFilesToS3 = \Classes\SettingsManager::getInstance()->getSetting("Files: Upload Files to S3"); -$uploadFilesToS3Key = \Classes\SettingsManager::getInstance()->getSetting("Files: Amazon S3 Key for File Upload"); -$uploadFilesToS3Secret = \Classes\SettingsManager::getInstance()->getSetting( +$uploadFilesToS3 = SettingsManager::getInstance()->getSetting("Files: Upload Files to S3"); +$uploadFilesToS3Key = SettingsManager::getInstance()->getSetting("Files: Amazon S3 Key for File Upload"); +$uploadFilesToS3Secret = SettingsManager::getInstance()->getSetting( "Files: Amazon S3 Secret for File Upload" ); -$s3Bucket = \Classes\SettingsManager::getInstance()->getSetting("Files: S3 Bucket"); -$s3WebUrl = \Classes\SettingsManager::getInstance()->getSetting("Files: S3 Web Url"); +$s3Bucket = SettingsManager::getInstance()->getSetting("Files: S3 Bucket"); +$s3WebUrl = SettingsManager::getInstance()->getSetting("Files: S3 Web Url"); $uploadedToS3 = false; -$localFile = CLIENT_BASE_PATH.'data/'.$result['filename']; +$localFile = BaseService::getInstance()->getDataDirectory().$result['filename']; $f_size = filesize($localFile); if($uploadFilesToS3.'' == '1' && !empty($uploadFilesToS3Key) && !empty($uploadFilesToS3Secret) && !empty($s3Bucket) && !empty($s3WebUrl)){ diff --git a/core/include.common.php b/core/include.common.php index 4d767f79b..4f9c6f8e6 100644 --- a/core/include.common.php +++ b/core/include.common.php @@ -20,15 +20,13 @@ function t($text) return LanguageManager::translateTnrText($text); } -if(!defined('TAGS_TO_PRESERVE')){define('TAGS_TO_PRESERVE','');} +if(!defined('TAGS_TO_PRESERVE')){define('TAGS_TO_PRESERVE', '');} $jsVersion = defined('CACHE_VALUE')?CACHE_VALUE:"v".VERSION; $cssVersion = defined('CACHE_VALUE')?CACHE_VALUE:"v".VERSION; -if(!isset($_REQUEST['content']) || $_REQUEST['content'] != 'HTML'){ - $_REQUEST = InputCleaner::cleanParameters($_REQUEST); - $_GET = InputCleaner::cleanParameters($_GET); - $_POST = InputCleaner::cleanParameters($_POST); -} +$_REQUEST = InputCleaner::cleanParameters($_REQUEST); +$_GET = InputCleaner::cleanParameters($_GET); +$_POST = InputCleaner::cleanParameters($_POST); date_default_timezone_set('Asia/Colombo'); //Find timezone diff with GMT diff --git a/core/login.php b/core/login.php index 6220ca6fb..397bdd5ed 100644 --- a/core/login.php +++ b/core/login.php @@ -251,19 +251,6 @@
- - - - - - - - - - - -
diff --git a/core/migrations/list.php b/core/migrations/list.php index 4a13f18ba..cbc32e8b1 100644 --- a/core/migrations/list.php +++ b/core/migrations/list.php @@ -1,5 +1,12 @@ executeQuery($sql); + } + + public function down() + { + return true; + } +} \ No newline at end of file diff --git a/core/migrations/v20210925_301001_deprecate_logo.php b/core/migrations/v20210925_301001_deprecate_logo.php new file mode 100644 index 000000000..d14a6fb03 --- /dev/null +++ b/core/migrations/v20210925_301001_deprecate_logo.php @@ -0,0 +1,28 @@ +executeQuery($sql); + + $sql = <<<'SQL' +UPDATE Settings set description = 'Update your company name here. For updating company logo copy a file named logo.png to icehrm_root/app/ folder' where name = 'Company: Name'; +SQL; + + return $this->executeQuery($sql); + } + + public function down() + { + return true; + } +} \ No newline at end of file diff --git a/core/migrations/v20211001_310000_employee_status.php b/core/migrations/v20211001_310000_employee_status.php new file mode 100644 index 000000000..4a52030de --- /dev/null +++ b/core/migrations/v20211001_310000_employee_status.php @@ -0,0 +1,30 @@ +executeQuery($sql); + } + + public function down() + { + return true; + } +} diff --git a/core/migrations/v20211203_310001_performance_update.php b/core/migrations/v20211203_310001_performance_update.php new file mode 100644 index 000000000..c50084495 --- /dev/null +++ b/core/migrations/v20211203_310001_performance_update.php @@ -0,0 +1,21 @@ +executeQuery($sql); + } + + public function down() + { + return true; + } +} diff --git a/core/migrations/v20211203_310002_performance_goals.php b/core/migrations/v20211203_310002_performance_goals.php new file mode 100644 index 000000000..f2152b38a --- /dev/null +++ b/core/migrations/v20211203_310002_performance_goals.php @@ -0,0 +1,37 @@ +executeQuery($sql); + } + + public function down() + { + return true; + } +} diff --git a/core/migrations/v20211223_310004_document_visibility.php b/core/migrations/v20211223_310004_document_visibility.php new file mode 100644 index 000000000..39e1e180f --- /dev/null +++ b/core/migrations/v20211223_310004_document_visibility.php @@ -0,0 +1,23 @@ +executeQuery($sql); + } + + public function down() + { + return true; + } +} diff --git a/core/migrations/v20220114_310005_performance_templates.php b/core/migrations/v20220114_310005_performance_templates.php new file mode 100644 index 000000000..e4a654f9f --- /dev/null +++ b/core/migrations/v20220114_310005_performance_templates.php @@ -0,0 +1,27 @@ +executeQuery($sql); + + $sql = <<<'SQL' +INSERT INTO ReviewTemplates (name, description, items, created, updated) VALUES ('Peer feedback', 'This is a sample peer feedback. Based on your organization you can update this template or create a new template.', '[{"id":"items_1","field_label":"How collaborative is your colleague? Please provide an example.","field_type":"textarea","field_validation":"","name":"howcollaborativeisyourcolleaguepleaseprovideanexample","data":"[\\"howcollaborativeisyourcolleaguepleaseprovideanexample\\",{\\"label\\":\\"How collaborative is your colleague? Please provide an example.\\",\\"type\\":\\"textarea\\",\\"validation\\":\\"\\"}]"},{"id":"items_2","field_label":"Do you consider your colleague as a good team player?","field_type":"select2","field_validation":"","field_options":"Yes\\nMost of the time\\nSometimes\\nNo","name":"doyouconsideryourcolleagueasagoodteamplayer","data":"[\\"doyouconsideryourcolleagueasagoodteamplayer\\",{\\"label\\":\\"Do you consider your colleague as a good team player?\\",\\"type\\":\\"select2\\",\\"validation\\":\\"\\",\\"source\\":[[\\"Yes\\",\\"Yes\\"],[\\"Most of the time\\",\\"Most of the time\\"],[\\"Sometimes\\",\\"Sometimes\\"],[\\"No\\",\\"No\\"]]}]"},{"id":"items_3","field_label":"How proactive and supportive is your colleague? Please provide an example.","field_type":"textarea","field_validation":"","name":"howproactiveandsupportiveisyourcolleaguepleaseprovideanexample","data":"[\\"howproactiveandsupportiveisyourcolleaguepleaseprovideanexample\\",{\\"label\\":\\"How proactive and supportive is your colleague? Please provide an example.\\",\\"type\\":\\"textarea\\",\\"validation\\":\\"\\"}]"},{"id":"items_4","field_label":"How do you rate your colleague’s communication skills when it comes to delivering critical messages in verbal and written communication?","field_type":"textarea","field_validation":"","name":"howdoyourateyourcolleaguescommunicationskillswhenitcomestodeliveringcriticalmessagesinverbalandwrittencommunication","data":"[\\"howdoyourateyourcolleaguescommunicationskillswhenitcomestodeliveringcriticalmessagesinverbalandwrittencommunication\\",{\\"label\\":\\"How do you rate your colleague’s communication skills when it comes to delivering critical messages in verbal and written communication?\\",\\"type\\":\\"textarea\\",\\"validation\\":\\"\\"}]"},{"id":"items_5","field_label":"Does your colleague demonstrate high standards of integrity and ethics?","field_type":"textarea","field_validation":"","name":"doesyourcolleaguedemonstratehighstandardsofintegrityandethics","data":"[\\"doesyourcolleaguedemonstratehighstandardsofintegrityandethics\\",{\\"label\\":\\"Does your colleague demonstrate high standards of integrity and ethics?\\",\\"type\\":\\"textarea\\",\\"validation\\":\\"\\"}]"},{"id":"items_6","field_label":"Is your colleague a good fit for the company culture?","field_type":"textarea","field_validation":"","name":"isyourcolleagueagoodfitforthecompanyculture","data":"[\\"isyourcolleagueagoodfitforthecompanyculture\\",{\\"label\\":\\"Is your colleague a good fit for the company culture?\\",\\"type\\":\\"textarea\\",\\"validation\\":\\"\\"}]"}]', '2020-11-15 00:52:51', '2020-11-15 00:52:51'); +SQL; + + return $this->executeQuery($sql); + } + + public function down() + { + return true; + } +} diff --git a/core/modules.php b/core/modules.php index 57b63fc8e..a3740abad 100644 --- a/core/modules.php +++ b/core/modules.php @@ -1,5 +1,5 @@ getSetting("System: Reset Modules and Permissions") == "1") { $permissionTemp = new \Permissions\Common\Model\Permission(); @@ -188,7 +188,7 @@ function createPermissions($meta, $moduleId) $initializer = $manager->getInitializer(); if ($initializer !== null) { $initializer->setBaseService($baseService); - $initializer->init(); + $initializers[] = $initializer; } } } @@ -283,7 +283,7 @@ function createPermissions($meta, $moduleId) $initializer = $manager->getInitializer(); if ($initializer !== null) { $initializer->setBaseService($baseService); - $initializer->init(); + $initializers[] = $initializer; } } } catch (\Exception $e) { @@ -441,3 +441,9 @@ function createPermissions($meta, $moduleId) } } } + +// Run initializers +foreach ($initializers as $initializer) { + $initializer->init(); +} + diff --git a/core/modules/dashboard/index.php b/core/modules/dashboard/index.php index 4b4431f08..44c366d80 100644 --- a/core/modules/dashboard/index.php +++ b/core/modules/dashboard/index.php @@ -4,16 +4,14 @@ Developer: Thilina Hasantha (http://lk.linkedin.com/in/thilinah | https://github.com/thilinah) */ +use Classes\LanguageManager; + $moduleName = 'dashboard'; $moduleGroup = 'modules'; define('MODULE_PATH',dirname(__FILE__)); include APP_BASE_PATH.'header.php'; include APP_BASE_PATH.'modulejslibs.inc.php'; -?>
- -
- getModuleManagers(); $dashBoardList = array(); foreach($moduleManagers as $moduleManagerObj){ @@ -33,47 +31,60 @@ ksort($dashBoardList); + $dashboardList1 =[]; + $dashboardList2 =[]; foreach($dashBoardList as $k=>$v){ - echo \Classes\LanguageManager::translateTnrText($v); + if (count($dashboardList1) === 4 ) { + $dashboardList2[] = $v; + } else { + $dashboardList1[] = $v; + } + } +?>
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ - - - -
+ diff --git a/core/modules/documents/index.php b/core/modules/documents/index.php index 73c711635..8882c8f2c 100644 --- a/core/modules/documents/index.php +++ b/core/modules/documents/index.php @@ -1,8 +1,4 @@
- - -
-
-
- - -
-
- -
-
-
-
- -
- -
-
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+ $user->user_level, + 'permissions' => [ + 'EmployeeDocument' => [ "get","element","save","delete" ], + 'CompanyDocument' => [ "get","element" ], + ] +]; +?> diff --git a/core/modules/overtime/index.php b/core/modules/overtime/index.php index 465ecfd9f..5cb91cd9a 100644 --- a/core/modules/overtime/index.php +++ b/core/modules/overtime/index.php @@ -1,8 +1,6 @@ getSetting('Overtime: Enable Multi Level Approvals') == '1'; include APP_BASE_PATH.'modulejslibs.inc.php'; ?>
-
- -
-
-
- -
- +
+
+
-
-
- -
- +
+
+
+
+ $user->user_level, + 'customFields' => BaseService::getInstance()->getCustomFields("EmployeeOvertime"), + 'permissions' => [ "get","element","save","delete" ], + 'moduleMainName' => $moduleMainName, + 'approveModName' => $approveModName, + 'subModuleMainName' => $subModuleMainName, +]; +?> + diff --git a/core/server.includes.inc.php b/core/server.includes.inc.php index 35a001550..e3eba58c3 100644 --- a/core/server.includes.inc.php +++ b/core/server.includes.inc.php @@ -40,8 +40,6 @@ } } -$user = \Utils\SessionUtils::getSessionObject('user'); - $dbLocal = NewADOConnection('mysqli'); $res = $dbLocal->Connect(APP_HOST, APP_USERNAME, APP_PASSWORD, APP_DB); @@ -54,6 +52,8 @@ Notification::SetDatabaseAdapter($dbLocal); RestAccessToken::SetDatabaseAdapter($dbLocal); +$user = \Utils\SessionUtils::getSessionObject('user'); + $baseService = BaseService::getInstance(); BaseService::getInstance()->setNonDeletables("User", "id", 1); @@ -205,6 +205,7 @@ function shutdown() $error = error_get_last(); if (!empty($error) && isset($error['type']) && in_array($error['type'], [E_ERROR, E_PARSE])) { + LogManager::getInstance()->error(json_encode($error)); LogManager::getInstance()->notifyException(new ErrorException( $error['message'], 0, diff --git a/core/service.php b/core/service.php index c02c27c8d..e6525d466 100644 --- a/core/service.php +++ b/core/service.php @@ -91,6 +91,7 @@ $ret['status'] = IceResponse::SUCCESS; } else { $ret['status'] = IceResponse::ERROR; + $ret['data'] = $response->getData(); } } else if ($action == 'getFieldValues') { @@ -209,18 +210,28 @@ function ($o) use ($reflectionClass) { exit; } - if (!file_exists(CLIENT_BASE_PATH . 'data/' . $file->filename)) { + if (!file_exists(BaseService::getInstance()->getDataDirectory() . $file->filename)) { exit; } $extension = explode('.', $file->filename)[1]; - + $seconds_to_cache = 3600; + $ts = gmdate("D, d M Y H:i:s", time() + $seconds_to_cache) . " GMT"; header('Content-Description: File Transfer'); if ('png' === $extension) { + header("Expires: $ts"); + header("Pragma: cache"); + header("Cache-Control: max-age=$seconds_to_cache"); header('Content-Type: image/png'); } elseif ('gif' === $extension) { + header("Expires: $ts"); + header("Pragma: cache"); + header("Cache-Control: max-age=$seconds_to_cache"); header('Content-Type: image/png'); } elseif ('jpg' === $extension || 'jpeg' === $extension) { + header("Expires: $ts"); + header("Pragma: cache"); + header("Cache-Control: max-age=$seconds_to_cache"); header('Content-Type: image/jpeg'); } elseif ('pdf' === $extension) { header('Content-Type: application/pdf'); @@ -236,10 +247,10 @@ function ($o) use ($reflectionClass) { header('Content-Disposition: attachment; filename=' . basename($file->filename)); - header('Content-Length: ' . filesize(CLIENT_BASE_PATH . 'data/' . $file->filename)); + header('Content-Length: ' . filesize(BaseService::getInstance()->getDataDirectory() . $file->filename)); ob_clean(); flush(); - readfile(CLIENT_BASE_PATH . 'data/' . $file->filename); + readfile(BaseService::getInstance()->getDataDirectory() . $file->filename); exit; } else if ($action == 'rsp') { // linked clicked from password change email diff --git a/core/src/Attendance/Common/Model/AttendanceStatus.php b/core/src/Attendance/Common/Model/AttendanceStatus.php index 1c77395f8..ae6632abe 100644 --- a/core/src/Attendance/Common/Model/AttendanceStatus.php +++ b/core/src/Attendance/Common/Model/AttendanceStatus.php @@ -13,6 +13,14 @@ use Employees\Common\Model\Employee; use Model\BaseModel; +/** + * Class AttendanceStatus + * + * This is a read-only class. Should never be used to query data for a different purpose other + * than checking attendance status + * + * @package Attendance\Common\Model + */ class AttendanceStatus extends BaseModel { public $table = 'Attendance'; diff --git a/core/src/Classes/BaseService.php b/core/src/Classes/BaseService.php index 59c550ee1..50999c6bd 100644 --- a/core/src/Classes/BaseService.php +++ b/core/src/Classes/BaseService.php @@ -13,6 +13,7 @@ use Classes\Crypt\AesCtr; use Classes\Email\EmailSender; use Classes\Exception\IceHttpException; +use Classes\Migration\MigrationInterface; use Company\Common\Model\CompanyStructure; use Employees\Common\Model\Employee; use Employees\Common\Model\EmployeeApproval; @@ -58,6 +59,7 @@ class BaseService public $currentProfileId = false; protected $cacheService = null; + protected $extensionMigrations = []; protected $pro = null; @@ -134,18 +136,26 @@ public function get($table, $mappingStr = null, $filterStr = null, $orderBy = nu $orderBy = " ORDER BY ".$orderBy; } + if ($obj->getFinder() !== null) { + $finder = $obj->getFinder(); + } else { + $finder = $obj; + } if (in_array($table, $this->userTables)) { $cemp = $this->getCurrentProfileId(); if (!empty($cemp)) { $signInMappingField = SIGN_IN_ELEMENT_MAPPING_FIELD_NAME; - $list = $obj->Find($signInMappingField." = ?".$query.$orderBy, array_merge(array($cemp), $queryData)); + $list = $finder->Find( + $signInMappingField." = ?".$query.$orderBy, + array_merge(array($cemp), $queryData) + ); } else { $list = array(); } } else { LogManager::getInstance()->debug("Query: "."1=1".$query.$orderBy); LogManager::getInstance()->debug("Query Data: ".print_r($queryData, true)); - $list = $obj->Find("1=1".$query.$orderBy, $queryData); + $list = $finder->Find("1=1".$query.$orderBy, $queryData); } $newList = array(); @@ -475,6 +485,12 @@ public function getData( $limit = ""; } + if ($obj->getFinder() !== null) { + $finder = $obj->getFinder(); + } else { + $finder = $obj; + } + if (in_array($table, $this->userTables) && !$skipProfileRestriction) { $cemp = $this->getCurrentProfileId(); if (!empty($cemp)) { @@ -486,7 +502,7 @@ public function getData( "Data Load Query (x1):"."1=1".$signInMappingField." = ?".$query.$orderBy.$limit ); LogManager::getInstance()->debug("Data Load Query Data (x1):".json_encode($queryData)); - $list = $obj->Find($signInMappingField." = ?".$query.$orderBy.$limit, $queryData); + $list = $finder->Find($signInMappingField." = ?".$query.$orderBy.$limit, $queryData); } else { $profileClass = $this->getFullQualifiedModelClassName(ucfirst(SIGN_IN_ELEMENT_MAPPING_FIELD_NAME)); $subordinate = new $profileClass(); @@ -556,7 +572,7 @@ public function getData( ); LogManager::getInstance()->debug("Data Load Query Data (x2):".json_encode($queryData)); if (!empty($subordinatesIds)) { - $list = $obj->Find( + $list = $finder->Find( $signInMappingField . " in (" . $subordinatesIds . ") " . $query . $orderBy . $limit, $queryData ); @@ -634,15 +650,15 @@ public function getData( LogManager::getInstance()->debug( "Data Load Query (a1):".$signInMappingField." in (".$subordinatesIds.") ".$query.$orderBy.$limit ); - $list = $obj->Find( + $list = $finder->Find( $signInMappingField." in (".$subordinatesIds.") ".$query.$orderBy.$limit, $queryData ); } else { - $list = $obj->Find("1=1".$query.$orderBy.$limit, $queryData); + $list = $finder->Find("1=1".$query.$orderBy.$limit, $queryData); } } else { - $list = $obj->Find("1=1".$query.$orderBy.$limit, $queryData); + $list = $finder->Find("1=1".$query.$orderBy.$limit, $queryData); } if (!$list) { @@ -892,11 +908,17 @@ public function addElement($table, $obj, $postObject = null) $ele->updated = date("Y-m-d H:i:s"); } if ($isAdd) { - $ele = $ele->executePreSaveActions($ele)->getData(); + $preResponse = $ele->executePreSaveActions($ele); } else { - $ele = $ele->executePreUpdateActions($ele)->getData(); + $preResponse = $ele->executePreUpdateActions($ele); } + if ($preResponse->getStatus() === IceResponse::ERROR) { + return $preResponse; + } + + $ele = $preResponse->getData(); + $ok = $ele->Save(); if (!$ok) { @@ -1052,6 +1074,12 @@ public function getFieldValues($table, $key, $value, $method, $methodParams = nu $ret = array(); $nsTable = $this->getFullQualifiedModelClassName($table); $ele = new $nsTable(); + + $finder = $ele->getFieldMappingFinder(); + if ($finder !== null) { + $ele = $finder; + } + $this->checkSecureAccess("get", $ele, $table, $_POST); if (!empty($method)) { if (method_exists($ele, $method) && in_array($method, $ele->fieldValueMethods())) { @@ -2013,4 +2041,24 @@ public function getSystemData($name) return null; } + + public function getDataDirectory() + { + $dataDir = SettingsManager::getInstance()->getSetting('System: Data Directory'); + if (!empty($dataDir) && is_dir($dataDir)) { + return $dataDir; + } + + return CLIENT_BASE_PATH.'data/'; + } + + public function getExtensionMigrations() + { + return $this->extensionMigrations; + } + + public function registerExtensionMigration(MigrationInterface $migration) + { + $this->extensionMigrations[$migration->getName()] = $migration; + } } diff --git a/core/src/Classes/ExtensionManager.php b/core/src/Classes/ExtensionManager.php index d9dec1439..ea0cb57da 100644 --- a/core/src/Classes/ExtensionManager.php +++ b/core/src/Classes/ExtensionManager.php @@ -57,6 +57,7 @@ public function setupExtensions() $arr['user_roles'] = isset($meta->user_roles)?$meta->user_roles:""; $arr['model_namespace'] = $meta->model_namespace; $arr['manager'] = $meta->manager; + $arr['controller'] = $meta->controller; // Add menu $menu[$meta->menu[0]] = $meta->menu[1]; @@ -120,6 +121,7 @@ public function setupExtensions() } } + $manager->initialize(); $initializer = $manager->getInitializer(); if ($initializer !== null) { $initializer->setBaseService(BaseService::getInstance()); @@ -140,6 +142,12 @@ public function includeModuleManager($name, $data) $moduleManagerObj->setModuleObject($data); $moduleManagerObj->setModuleType(self::GROUP); $moduleManagerObj->setModulePath(CLIENT_PATH.'/'.self::GROUP.'/'.$name); + + $controllerClass = $data['controller']; + if (class_exists($controllerClass)) { + $moduleManagerObj->setActionManager(new $controllerClass()); + } + \Classes\BaseService::getInstance()->addModuleManager($moduleManagerObj); return $moduleManagerObj; } diff --git a/core/src/Classes/FileService.php b/core/src/Classes/FileService.php index a4974e0fb..c2a9306a6 100644 --- a/core/src/Classes/FileService.php +++ b/core/src/Classes/FileService.php @@ -137,7 +137,7 @@ public function checkAddSmallProfileImage($profileImage) if (empty($file->id)) { LogManager::getInstance()->info("Small profile image ".$profileImage->name."_small not found"); - if (file_exists(CLIENT_BASE_PATH.'data/'.$profileImage->filename)) { + if (file_exists(BaseService::getInstance()->getDataDirectory().$profileImage->filename)) { //Resize image to 100 $file->name = $profileImage->name."_small"; @@ -146,12 +146,14 @@ public function checkAddSmallProfileImage($profileImage) $file->filename = $file->name.str_replace($profileImage->name, "", $profileImage->filename); try { - $img = new \Classes\SimpleImage(CLIENT_BASE_PATH . 'data/' . $profileImage->filename); + $img = new \Classes\SimpleImage( + BaseService::getInstance()->getDataDirectory() . $profileImage->filename + ); $img->fitToWidth(140); - $img->save(CLIENT_BASE_PATH . 'data/' . $file->filename); + $img->save(BaseService::getInstance()->getDataDirectory() . $file->filename); $file->employee = $profileImage->employee; $file->file_group = 'profile_image_small'; - $file->size = filesize(CLIENT_BASE_PATH . 'data/' . $file->filename); + $file->size = filesize(BaseService::getInstance()->getDataDirectory() . $file->filename); $file->size_text = $this->getReadableSize($file->size); $file->Save(); } catch (\Exception $e) { @@ -355,8 +357,8 @@ public function deleteFileFromDisk($file) $s3FileSys = new S3FileSystem($uploadFilesToS3Key, $uploadFilesToS3Secret); $s3FileSys->deleteObject($s3Bucket, $uploadname); } else { - LogManager::getInstance()->info("Delete:".CLIENT_BASE_PATH.'data/'.$file->filename); - unlink(CLIENT_BASE_PATH.'data/'.$file->filename); + LogManager::getInstance()->info("Delete:".BaseService::getInstance()->getDataDirectory().$file->filename); + unlink(BaseService::getInstance()->getDataDirectory().$file->filename); } } @@ -385,8 +387,10 @@ public function deleteFileByField($value, $field) $s3FileSys = new S3FileSystem($uploadFilesToS3Key, $uploadFilesToS3Secret); $s3FileSys->deleteObject($s3Bucket, $uploadname); } else { - LogManager::getInstance()->info("Delete:".CLIENT_BASE_PATH.'data/'.$file->filename); - unlink(CLIENT_BASE_PATH.'data/'.$file->filename); + LogManager::getInstance()->info( + "Delete:".BaseService::getInstance()->getDataDirectory().$file->filename + ); + unlink(BaseService::getInstance()->getDataDirectory().$file->filename); } } else { return false; diff --git a/core/src/Classes/IceApiController.php b/core/src/Classes/IceApiController.php new file mode 100644 index 000000000..ec39dcbd9 --- /dev/null +++ b/core/src/Classes/IceApiController.php @@ -0,0 +1,41 @@ +runExtensionMigration($migration); + } + } + + public function runExtensionMigration(MigrationInterface $migrationObject) + { + $migration = new Migration(); + $migration->Load('file = ?', [ $migrationObject->getName() ]); + + if ($migration->file === $migrationObject->getName()) { + return false; + } + + $migration = new Migration(); + $migration->file = $migrationObject->getName(); + $migration->version = 1; + $migration->created = date("Y-m-d H:i:s"); + $migration->updated = date("Y-m-d H:i:s"); + $migration->status = 'Pending'; + $ok = $migration->Save(); + + if (!$ok) { + return false; + } + + $res = $migrationObject->up(); + if (!$res) { + $migration->last_error = $migrationObject->getLastError(); + $migration->status = "UpError"; + $migration->updated = date("Y-m-d H:i:s"); + $migration->Save(); + } + + $migration->status = "Up"; + $migration->updated = date("Y-m-d H:i:s"); + $migration->Save(); + + return true; + } + public function runPendingMigrations() { $migrations = $this->getPendingMigrations(); diff --git a/core/src/Classes/ModuleBuilderV2/ModuleTab.php b/core/src/Classes/ModuleBuilderV2/ModuleTab.php index e79fc52ca..f3462738e 100644 --- a/core/src/Classes/ModuleBuilderV2/ModuleTab.php +++ b/core/src/Classes/ModuleBuilderV2/ModuleTab.php @@ -1,4 +1,5 @@ modelPath = $modelPath; $this->name = $name; $this->class = $class; @@ -45,33 +45,33 @@ public function __construct( $this->options = array_merge( $options, [ - "setObjectTypeName" => "'{$this->name}'", - "setAccess" => "data.permissions.{$this->name} ? data.permissions.{$this->name} : {}", - "setDataPipe" => 'new IceDataPipe(modJsList.tab' . $this->name . ')', - "setRemoteTable" => true, + "setObjectTypeName" => "'{$this->name}'", + "setAccess" => "data.permissions.{$this->name} ? data.permissions.{$this->name} : {}", + "setDataPipe" => 'new IceDataPipe(modJsList.tab' . $this->name . ')', + "setRemoteTable" => true, ] ); } public function getHTML() { - $active = ($this->isActive)?"active":""; + $active = ($this->isActive) ? "active" : ""; if (!$this->isInsideGroup) { return '
  • ' . t($this->label) . '
  • '; + . '" href="#tabPage' . $this->name . '">' . t($this->label) . ''; } else { return '
  • ' . t($this->label) . '
  • '; + . '" href="#tabPage' . $this->name . '">' . t($this->label) . ''; } } public function getPageHTML() { - $active = ($this->isActive)?" active":""; - $html = '
    '. - '
    '. - '
    '. - '
    '. + $active = ($this->isActive) ? " active" : ""; + $html = '
    ' . + '
    ' . + '
    ' . + '
    ' . '
    '; return $html; @@ -79,18 +79,22 @@ public function getPageHTML() public function getJSObjectCode() { + if (!$this->options["setTitle"]) { + $this->options["setTitle"] = "'" . $this->label . "'"; + } + $js = ""; if (empty($this->filter)) { - $js.= "modJsList['tab" . $this->name . "'] = new " . - $this->adapterName . "('" . $this->class . "','" . $this->name . "','','".$this->orderBy. "');\r\n"; + $js .= "modJsList['tab" . $this->name . "'] = new " . + $this->adapterName . "('" . $this->class . "','" . $this->name . "','','" . $this->orderBy . "');\r\n"; } else { - $js.= "modJsList['tab" . $this->name . "'] = new " . + $js .= "modJsList['tab" . $this->name . "'] = new " . $this->adapterName . "('" . $this->class . "','" . $this->name . "'," . - $this->filter . ",'".$this->orderBy. "');\r\n"; + $this->filter . ",'" . $this->orderBy . "');\r\n"; } foreach ($this->options as $key => $val) { - $js.= "modJsList['tab" . $this->name . "'].".$key."(".$val. ");\r\n"; + $js .= "modJsList['tab" . $this->name . "']." . $key . "(" . $val . ");\r\n"; } return $js; diff --git a/core/src/Classes/StatsHelper.php b/core/src/Classes/StatsHelper.php index 5e05b0b1b..577283cd1 100644 --- a/core/src/Classes/StatsHelper.php +++ b/core/src/Classes/StatsHelper.php @@ -19,6 +19,18 @@ public static function getEmployeeCount() return 0; } + public static function getActiveEmployeeCount() + { + $employee = new Employee(); + $employeeCount = $employee->DB()->Execute("select count(id) from Employees where status = ?", ['Active']); + if ($employeeCount) { + $employeeCount = intval($employeeCount->fields[0]); + return $employeeCount; + } + + return 0; + } + public static function getUserCount() { $user = new User(); diff --git a/core/src/Classes/UIManager.php b/core/src/Classes/UIManager.php index ac4122ee8..d5fe20cd0 100644 --- a/core/src/Classes/UIManager.php +++ b/core/src/Classes/UIManager.php @@ -299,19 +299,18 @@ public function renderModule($moduleBuilder) public function getCompanyLogoUrl() { - $logoFileSet = false; - $logoFileName = CLIENT_BASE_PATH."data/logo.png"; $logoSettings = SettingsManager::getInstance()->getSetting("Company: Logo"); if (!empty($logoSettings)) { - $logoFileName = FileService::getInstance()->getFileUrl($logoSettings, false); - $logoFileSet = true; + return FileService::getInstance()->getFileUrl($logoSettings, false); } - if (!$logoFileSet && !file_exists($logoFileName)) { - return BASE_URL."images/logo.png"; + $logoFileName = CLIENT_BASE_PATH.'logo.png'; + + if (file_exists($logoFileName)) { + return CLIENT_BASE_URL.'logo.png'; } - return $logoFileName; + return BASE_URL."images/logo.png"; } /** diff --git a/core/src/Classes/Upload/Uploader.php b/core/src/Classes/Upload/Uploader.php index 5d0a92ec9..8f8bf9cec 100644 --- a/core/src/Classes/Upload/Uploader.php +++ b/core/src/Classes/Upload/Uploader.php @@ -112,7 +112,7 @@ public static function upload($postData, $fileData) // max file size in bytes $sizeLimit =MAX_FILE_SIZE_KB * 1024; $uploader = new Uploader(new TempFile($fileData), $allowedExtensions, $sizeLimit); - $result = $uploader->handleUpload(CLIENT_BASE_PATH.'data/', $saveFileName); + $result = $uploader->handleUpload(BaseService::getInstance()->getDataDirectory(), $saveFileName); if ($result->getStatus() !== IceResponse::SUCCESS) { return $result; @@ -126,7 +126,7 @@ public static function upload($postData, $fileData) $s3Bucket = SettingsManager::getInstance()->getSetting("Files: S3 Bucket"); $s3WebUrl = SettingsManager::getInstance()->getSetting("Files: S3 Web Url"); - $localFile = CLIENT_BASE_PATH.'data/'.$result->getData(); + $localFile = BaseService::getInstance()->getDataDirectory().$result->getData(); $uploadedFileSize = filesize($localFile); if ($uploadFilesToS3.'' == '1' && !empty($uploadFilesToS3Key) && !empty($uploadFilesToS3Secret) && !empty($s3Bucket) && !empty($s3WebUrl)) { diff --git a/core/src/Connection/Common/ConnectionService.php b/core/src/Connection/Common/ConnectionService.php index c9bf5fc77..7d3e9f401 100644 --- a/core/src/Connection/Common/ConnectionService.php +++ b/core/src/Connection/Common/ConnectionService.php @@ -28,6 +28,7 @@ public function getInstallationData() 'version' => VERSION, 'company' => SettingsManager::getInstance()->getSetting('Company: Name'), 'pro_key' => $proKey, + 'url' => CLIENT_BASE_URL, ]; } @@ -64,7 +65,7 @@ public function getSystemReport() public function getSystemErrors() { $errors = []; - $res = fopen(CLIENT_BASE_PATH.'data/connection_test.txt', "w"); + $res = fopen(BaseService::getInstance()->getDataDirectory().'connection_test.txt', "w"); if (false === $res) { $errors[] = [ @@ -76,7 +77,8 @@ public function getSystemErrors() fwrite($res, date('Y-m-d')); $file = CLIENT_BASE_URL.'data/connection_test.txt'; $file_headers = @get_headers($file); - if ($file_headers && $file_headers[0] !== 'HTTP/1.1 404 Not Found') { + $status = explode(' ', $file_headers[0]); + if ($file_headers && count($status) > 1 && $status[1] != '404' && $status[1] != '403') { $errors[] = [ 'type' => 'error', 'link' => 'https://icehrm.gitbook.io/icehrm/getting-started/securing-icehrm-installation', diff --git a/core/src/Data/Admin/Api/DataActionManager.php b/core/src/Data/Admin/Api/DataActionManager.php index 4b544af38..9cd0db1c8 100644 --- a/core/src/Data/Admin/Api/DataActionManager.php +++ b/core/src/Data/Admin/Api/DataActionManager.php @@ -13,6 +13,7 @@ use Classes\SubActionManager; use Data\Common\Model\DataImport; use Data\Common\Model\DataImportFile; +use Model\File; use Utils\LogManager; class DataActionManager extends SubActionManager @@ -29,7 +30,9 @@ public function processDataFile($req) $url = FileService::getInstance()->getFileUrl($dataFile->file); if (strstr($url, CLIENT_BASE_URL) !== false) { - $url = str_replace(CLIENT_BASE_URL, CLIENT_BASE_PATH, $url); + $file = new File(); + $file->Load('name = ?', [$dataFile->file]); + $url = CLIENT_BASE_PATH.'data/'.$file->filename; } LogManager::getInstance()->info("File Path:".$url); diff --git a/core/src/Documents/Common/Model/CompanyDocument.php b/core/src/Documents/Common/Model/CompanyDocument.php index 6d20e70a2..319be3a6a 100644 --- a/core/src/Documents/Common/Model/CompanyDocument.php +++ b/core/src/Documents/Common/Model/CompanyDocument.php @@ -43,61 +43,23 @@ public function getModuleAccess() ]; } - // @codingStandardsIgnoreStart - public function Find($whereOrderBy, $bindarr = false, $cache = false, $pkeysArr = false, $extra = array()) + public function getFinder() { - // @codingStandardsIgnoreEnd - $res = parent::Find($whereOrderBy, $bindarr, $pkeysArr, $extra); - - $user = BaseService::getInstance()->getCurrentUser(); - if ($user->user_level == 'Admin') { - foreach ($res as $entry) { - $file = FileService::getInstance()->getFileData($entry->attachment); - $entry->type = $file->type; - $entry->size = $file->size_text; - } - return $res; - } - $emp = BaseService::getInstance()->getCurrentProfileId(); - $employee = new Employee(); - $employee->Load("id = ?", array($emp)); - - $data = array(); - - foreach ($res as $entry) { - if ($entry->status != 'Active') { - continue; - } - - $file = FileService::getInstance()->getFileData($entry->attachment); - $entry->type = $file->type; - $entry->size = $file->size_text; - - if (!empty($entry->share_departments)) { - $shareDepartments = json_decode($entry->share_departments, true); - if (count($shareDepartments) == 1 && $shareDepartments[0] == "NULL") { - //Shared with All Departments - $data[] = $entry; - continue; - } else { - if (in_array($employee->department, $shareDepartments)) { - //Document is shared with employee's department - $data[] = $entry; - continue; - } - } - } + return new CompanyDocumentFinderProxy(); + } - if (!empty($entry->share_employees)) { - $shareEmployees = json_decode($entry->share_employees, true); - if (in_array($employee->id, $shareEmployees)) { - //Document is shared with the employee - $data[] = $entry; - continue; - } - } + public function executePreSaveActions($obj) + { + $obj->expire_notification_last = -1; + if (empty($obj->visible_to)) { + $obj->visible_to = 'Owner'; } + return new IceResponse(IceResponse::SUCCESS, $obj); + } - return $data; + public function executePreUpdateActions($obj) + { + $obj->expire_notification_last = -1; + return new IceResponse(IceResponse::SUCCESS, $obj); } } diff --git a/core/src/Documents/Common/Model/CompanyDocumentFinderProxy.php b/core/src/Documents/Common/Model/CompanyDocumentFinderProxy.php new file mode 100644 index 000000000..ba493ea0e --- /dev/null +++ b/core/src/Documents/Common/Model/CompanyDocumentFinderProxy.php @@ -0,0 +1,77 @@ +getCurrentUser(); + if ($user->user_level == 'Admin') { + foreach ($res as $entry) { + $file = FileService::getInstance()->getFileData($entry->attachment); + $entry->type = $file->type; + $entry->size = $file->size_text; + } + return $res; + } + $emp = BaseService::getInstance()->getCurrentProfileId(); + $employee = new Employee(); + $employee->Load("id = ?", array($emp)); + + $data = array(); + + foreach ($res as $entry) { + if ($entry->status != 'Active') { + continue; + } + + $file = FileService::getInstance()->getFileData($entry->attachment); + $entry->type = $file->type; + $entry->size = $file->size_text; + + if (!empty($entry->share_employees)) { + $shareEmployees = json_decode($entry->share_employees, true); + if (in_array($employee->id, $shareEmployees)) { + //Document is shared with the employee + $data[] = $entry; + continue; + } + // When the document is assigned to an employee, share department value is ignored + + if (is_array($shareEmployees) && count($shareEmployees) === 0) { + continue; + } + } + + if (empty($entry->share_departments)) { + // When share departments is null / all employees can access + $data[] = $entry; + continue; + } + + $shareDepartments = json_decode($entry->share_departments, true); + if (count($shareDepartments) == 0 || empty($shareDepartments)) { + //Shared with All Departments + $data[] = $entry; + continue; + } else { + if (in_array($employee->department, $shareDepartments)) { + //Document is shared with employee's department + $data[] = $entry; + continue; + } + } + } + + return $data; + } +} diff --git a/core/src/Documents/Common/Model/EmployeeDocument.php b/core/src/Documents/Common/Model/EmployeeDocument.php index 2ec8c5e27..dc8bc933d 100644 --- a/core/src/Documents/Common/Model/EmployeeDocument.php +++ b/core/src/Documents/Common/Model/EmployeeDocument.php @@ -18,54 +18,55 @@ class EmployeeDocument extends BaseModel { public $table = 'EmployeeDocuments'; - private function getHiddenDocumentTypeIds() + public function getFinder() { - } - - // @codingStandardsIgnoreStart - public function Find($whereOrderBy, $bindarr = false, $cache = false, $pkeysArr = false, $extra = array()) - { - $find = ''; - $user = BaseService::getInstance()->getCurrentUser(); - - if ($user->user_level == 'Employee') { - $find = ' visible_to = \'Owner\' AND '; - $document = new Document(); - $hiddenDocumentTypes = $document->Find( - "share_with_employee = ?", - ['No'] - ); - - $hiddenTypeIds = []; - foreach ($hiddenDocumentTypes as $hiddenDocumentType) { - $hiddenTypeIds[] = $hiddenDocumentType->id; - } - - if(count($hiddenTypeIds) > 0) { - $find .= ' document NOT IN (\''.implode('\',\'', $hiddenTypeIds).'\') AND '; + return new class extends EmployeeDocument { + // @codingStandardsIgnoreStart + public function Find($whereOrderBy, $bindarr = false, $cache = false, $pkeysArr = false, $extra = array()) + { + $find = ''; + $user = BaseService::getInstance()->getCurrentUser(); + + if ($user->user_level == 'Employee') { + $find = ' visible_to IN (\'Owner\', \'Owner Only\') AND '; + $document = new Document(); + $hiddenDocumentTypes = $document->Find( + "share_with_employee = ?", + ['No'] + ); + + $hiddenTypeIds = []; + foreach ($hiddenDocumentTypes as $hiddenDocumentType) { + $hiddenTypeIds[] = $hiddenDocumentType->id; + } + + if(count($hiddenTypeIds) > 0) { + $find .= ' document NOT IN (\''.implode('\',\'', $hiddenTypeIds).'\') AND '; + } + + return parent::Find($find.$whereOrderBy, $bindarr, $pkeysArr, $extra); + + } else if ($user->user_level == 'Manager') { + // Original $whereOrderBy already contain employee selection + // So here if isSubOrdinates is true if the query coming from Employee -> Document Management + // In that case we need to show documents from sub ordinates + // These docs can can be owner and manager both + if (isset($isSubOrdinates) && $isSubOrdinates) { + $find .= ' visible_to in (\'Owner\', \'Manager\') AND '; + } else { + // Here we are showing the documents for the manager + // If someone upload a document for this manager and make it visible to manager, + // that means only the manager of this manager can see the document + // So it should not be visible to this manager + $find .= ' visible_to in (\'Owner\') AND '; + } + } + + return parent::Find($find.$whereOrderBy, $bindarr, $pkeysArr, $extra); } - - return parent::Find($find.$whereOrderBy, $bindarr, $pkeysArr, $extra); - - } else if ($user->user_level == 'Manager') { - // Original $whereOrderBy already contain employee selection - // So here if isSubOrdinates is true if the query coming from Employee -> Document Management - // In that case we need to show documents from sub ordinates - // These docs can can be owner and manager both - if (isset($isSubOrdinates) && $isSubOrdinates) { - $find .= ' visible_to in (\'Owner\', \'Manager\') AND '; - } else { - // Here we are showing the documents for the manager - // If someone upload a document for this manager and make it visible to manager, - // that means only the manager of this manager can see the document - // So it should not be visible to this manager - $find .= ' visible_to in (\'Owner\') AND '; - } - } - - return parent::Find($find.$whereOrderBy, $bindarr, $pkeysArr, $extra); + // @codingStandardsIgnoreEnd + }; } - // @codingStandardsIgnoreEnd public function getAdminAccess() { @@ -101,6 +102,9 @@ public function Insert() public function executePreSaveActions($obj) { $obj->expire_notification_last = -1; + if (empty($obj->visible_to)) { + $obj->visible_to = 'Owner'; + } return new IceResponse(IceResponse::SUCCESS, $obj); } diff --git a/core/src/Employees/Admin/Api/EmployeesAdminManager.php b/core/src/Employees/Admin/Api/EmployeesAdminManager.php index a38a868b4..e2fd53e55 100644 --- a/core/src/Employees/Admin/Api/EmployeesAdminManager.php +++ b/core/src/Employees/Admin/Api/EmployeesAdminManager.php @@ -84,6 +84,17 @@ public function setupRestEndPoints() $empRestEndPoint = new EmployeeLanguageRestEndpoint(); $empRestEndPoint->process('listAll', $pathParams); }); + + // Employee status + Macaw::get(REST_API_PATH.'employees/(:num)/status', function ($pathParams) { + $empRestEndPoint = new EmployeeRestEndPoint(); + $empRestEndPoint->process('getEmployeeStatusMessage', $pathParams); + }); + + Macaw::post(REST_API_PATH.'employees/(:num)/status', function ($pathParams) { + $empRestEndPoint = new EmployeeRestEndPoint(); + $empRestEndPoint->process('setEmployeeStatusMessage', $pathParams); + }); } public function initializeDatabaseErrorMappings() @@ -99,6 +110,7 @@ public function setupModuleClassDefinitions() { $this->addModelClass('Employee'); $this->addModelClass('EmploymentStatus'); + $this->addModelClass('EmployeeStatus'); $this->addModelClass('EmployeeApproval'); $this->addModelClass('ArchivedEmployee'); } diff --git a/core/src/Employees/Common/Model/Employee.php b/core/src/Employees/Common/Model/Employee.php index e76c25e4b..7d00242e2 100644 --- a/core/src/Employees/Common/Model/Employee.php +++ b/core/src/Employees/Common/Model/Employee.php @@ -9,6 +9,7 @@ use Metadata\Common\Model\Country; use Model\BaseModel; use Model\CustomFieldTrait; +use Model\File; class Employee extends BaseModel { @@ -250,5 +251,10 @@ public function getModuleAccess() ]; } + public function postProcessGetElement($obj) + { + return FileService::getInstance()->updateProfileImage($obj); + } + public $table = 'Employees'; } diff --git a/core/src/Employees/Common/Model/EmployeeStatus.php b/core/src/Employees/Common/Model/EmployeeStatus.php new file mode 100644 index 000000000..3db060b47 --- /dev/null +++ b/core/src/Employees/Common/Model/EmployeeStatus.php @@ -0,0 +1,36 @@ +getData(), 400); } + + public function getEmployeeStatusMessage(User $user, $parameter) + { + $date = CalendarTools::getServerDate(); + + $employeeId = $parameter['num']; + + $employeeState = new EmployeeStatus(); + $employeeState->Load('employee = ? and status_date = ?', [ $employeeId, $date]); + + $data = $this->cleanObject($employeeState); + unset($data->objectName); + unset($data->id); + unset($data->status_date); + + return new IceResponse(IceResponse::SUCCESS, $data, 200); + } + + public function setEmployeeStatusMessage(User $user, $parameter) + { + $body = $this->getRequestBody(); + + $employeeId = $parameter['num']; + + $permissionResponse = $this->checkBasicPermissions($user, $employeeId); + if ($permissionResponse->getStatus() !== IceResponse::SUCCESS) { + return $permissionResponse; + } + + $date = CalendarTools::getServerDate(); + + $employeeState = new EmployeeStatus(); + $employeeState->Load('employee = ? and status_date = ?', [ $employeeId, $date]); + + $employeeState->employee = $employeeId; + $employeeState->status = $body['status']; + $employeeState->feeling = $body['feeling']; + $employeeState->message = $body['message']; + $employeeState->status_date = $date; + + $employeeState->Save(); + + $data = $this->cleanObject($employeeState); + unset($data->objectName); + unset($data->id); + + return new IceResponse(IceResponse::SUCCESS, $data, 200); + } } diff --git a/core/src/Metadata/Common/Model/Country.php b/core/src/Metadata/Common/Model/Country.php index d318f1703..c81788d10 100644 --- a/core/src/Metadata/Common/Model/Country.php +++ b/core/src/Metadata/Common/Model/Country.php @@ -26,27 +26,32 @@ public function getAnonymousAccess() return array("get","element"); } - // @codingStandardsIgnoreStart - public function Find($whereOrderBy, $bindarr = false, $cache = false, $pkeysArr = false, $extra = array()) + public function getFieldMappingFinder() { - $allowedCountriesStr = SettingsManager::getInstance()->getSetting('System: Allowed Countries'); - $allowedCountries = array(); - if (!empty($allowedCountriesStr)) { - $allowedCountries = json_decode($allowedCountriesStr, true); - } - - if (!empty($allowedCountries)) { - $res = parent::Find("id in (".implode(",", $allowedCountries).")", array()); - if (empty($res)) { - SettingsManager::getInstance()->setSetting('System: Allowed Countries', ''); - } else { - return $res; + return new class extends Country { + // @codingStandardsIgnoreStart + public function Find($whereOrderBy, $bindarr = false, $cache = false, $pkeysArr = false, $extra = array()) + { + $allowedCountriesStr = SettingsManager::getInstance()->getSetting('System: Allowed Countries'); + $allowedCountries = array(); + if (!empty($allowedCountriesStr)) { + $allowedCountries = json_decode($allowedCountriesStr, true); + } + + if (!empty($allowedCountries)) { + $res = parent::Find("id in (".implode(",", $allowedCountries).")", array()); + if (empty($res)) { + SettingsManager::getInstance()->setSetting('System: Allowed Countries', ''); + } else { + return $res; + } + } + + return parent::Find($whereOrderBy, $bindarr, $pkeysArr, $extra); } - } - - return parent::Find($whereOrderBy, $bindarr, $pkeysArr, $extra); + // @codingStandardsIgnoreEnd + }; } - // @codingStandardsIgnoreEnd public function getModuleAccess() { diff --git a/core/src/Metadata/Common/Model/CurrencyType.php b/core/src/Metadata/Common/Model/CurrencyType.php index 4c5d97f7a..bd5d4f0a8 100644 --- a/core/src/Metadata/Common/Model/CurrencyType.php +++ b/core/src/Metadata/Common/Model/CurrencyType.php @@ -26,27 +26,32 @@ public function getAnonymousAccess() return array("get","element"); } - // @codingStandardsIgnoreStart - public function Find($whereOrderBy, $bindarr = false, $cache = false, $pkeysArr = false, $extra = array()) + public function getFieldMappingFinder() { - $allowedCountriesStr = SettingsManager::getInstance()->getSetting('System: Allowed Currencies'); - $allowedCountries = array(); - if (!empty($allowedCountriesStr)) { - $allowedCountries = json_decode($allowedCountriesStr, true); - } - - if (!empty($allowedCountries)) { - $res = parent::Find("id in (".implode(",", $allowedCountries).")", array()); - if (empty($res)) { - SettingsManager::getInstance()->setSetting('System: Allowed Currencies', ''); - } else { - return $res; + return new class extends CurrencyType { + // @codingStandardsIgnoreStart + public function Find($whereOrderBy, $bindarr = false, $cache = false, $pkeysArr = false, $extra = array()) + { + $allowedCountriesStr = SettingsManager::getInstance()->getSetting('System: Allowed Currencies'); + $allowedCountries = array(); + if (!empty($allowedCountriesStr)) { + $allowedCountries = json_decode($allowedCountriesStr, true); + } + + if (!empty($allowedCountries)) { + $res = parent::Find("id in (".implode(",", $allowedCountries).")", array()); + if (empty($res)) { + SettingsManager::getInstance()->setSetting('System: Allowed Currencies', ''); + } else { + return $res; + } + } + + return parent::Find($whereOrderBy, $bindarr, $pkeysArr, $extra); } - } - - return parent::Find($whereOrderBy, $bindarr, $pkeysArr, $extra); + // @codingStandardsIgnoreEnd + }; } - // @codingStandardsIgnoreEnd public function getModuleAccess() { diff --git a/core/src/Metadata/Common/Model/Nationality.php b/core/src/Metadata/Common/Model/Nationality.php index fb8d5b417..9d8e138ca 100644 --- a/core/src/Metadata/Common/Model/Nationality.php +++ b/core/src/Metadata/Common/Model/Nationality.php @@ -25,27 +25,33 @@ public function getAnonymousAccess() { return array("get","element"); } - // @codingStandardsIgnoreStart - public function Find($whereOrderBy, $bindarr = false, $cache = false, $pkeysArr = false, $extra = array()) + + public function getFieldMappingFinder() { - $allowedCountriesStr = SettingsManager::getInstance()->getSetting('System: Allowed Nationality'); - $allowedCountries = array(); - if (!empty($allowedCountriesStr)) { - $allowedCountries = json_decode($allowedCountriesStr, true); - } - - if (!empty($allowedCountries)) { - $res = parent::Find("id in (".implode(",", $allowedCountries).")", array()); - if (empty($res)) { - SettingsManager::getInstance()->setSetting('System: Allowed Countries', ''); - } else { - return $res; + return new class extends Nationality { + // @codingStandardsIgnoreStart + public function Find($whereOrderBy, $bindarr = false, $cache = false, $pkeysArr = false, $extra = array()) + { + $allowedCountriesStr = SettingsManager::getInstance()->getSetting('System: Allowed Nationality'); + $allowedCountries = array(); + if (!empty($allowedCountriesStr)) { + $allowedCountries = json_decode($allowedCountriesStr, true); + } + + if (!empty($allowedCountries)) { + $res = parent::Find("id in (".implode(",", $allowedCountries).")", array()); + if (empty($res)) { + SettingsManager::getInstance()->setSetting('System: Allowed Nationality', ''); + } else { + return $res; + } + } + + return parent::Find($whereOrderBy, $bindarr, $pkeysArr, $extra); } - } - - return parent::Find($whereOrderBy, $bindarr, $pkeysArr, $extra); + // @codingStandardsIgnoreEnd + }; } - // @codingStandardsIgnoreEnd public function getModuleAccess() { diff --git a/core/src/Model/BaseModel.php b/core/src/Model/BaseModel.php index 7d02b421f..acddd697d 100644 --- a/core/src/Model/BaseModel.php +++ b/core/src/Model/BaseModel.php @@ -5,6 +5,7 @@ use Classes\IceResponse; use Classes\ModuleAccess; use Classes\ModuleAccessService; +use Documents\Common\Model\CompanyDocumentFinderProxy; use Modules\Common\Model\Module; use Users\Common\Model\UserRole; use Utils\LogManager; @@ -174,11 +175,19 @@ public function validateSave($obj) return new IceResponse(IceResponse::SUCCESS, ""); } + /** + * @param $obj + * @return IceResponse + */ public function executePreSaveActions($obj) { return new IceResponse(IceResponse::SUCCESS, $obj); } + /** + * @param $obj + * @return IceResponse + */ public function executePreUpdateActions($obj) { return new IceResponse(IceResponse::SUCCESS, $obj); @@ -362,4 +371,14 @@ public function isCustomFieldsEnabled() { return false; } + + public function getFinder() + { + return null; + } + + public function getFieldMappingFinder() + { + return null; + } } diff --git a/core/src/Model/Setting.php b/core/src/Model/Setting.php index 7dffef62a..b04a2a9b8 100644 --- a/core/src/Model/Setting.php +++ b/core/src/Model/Setting.php @@ -78,6 +78,12 @@ public function validateSave($obj) public function executePreSaveActions($obj) { $obj->value = SettingsManager::getInstance()->encryptSetting($obj->name, $obj->value); + + // Check if the data directory exists + if ($obj->name === 'System: Data Directory' && $obj->value != '' && !is_dir($obj->value)) { + return new IceResponse(IceResponse::ERROR, 'Non existing data directory'); + } + return new IceResponse(IceResponse::SUCCESS, $obj); } diff --git a/core/src/Reports/Admin/Api/PDFReportBuilder.php b/core/src/Reports/Admin/Api/PDFReportBuilder.php index d126891ad..06a7a69dc 100644 --- a/core/src/Reports/Admin/Api/PDFReportBuilder.php +++ b/core/src/Reports/Admin/Api/PDFReportBuilder.php @@ -8,6 +8,7 @@ namespace Reports\Admin\Api; +use Classes\BaseService; use Classes\SettingsManager; use Classes\UIManager; use Utils\LogManager; @@ -53,7 +54,7 @@ public function createReportFile($report, $data) $fileFirstPart = "Report_".str_replace(" ", "_", $report->name)."-".date("Y-m-d_H-i-s"); $fileName = $fileFirstPart.".html"; - $fileFullName = CLIENT_BASE_PATH.'data/'.$fileName; + $fileFullName = BaseService::getInstance()->getDataDirectory().$fileName; $this->initTemplateEngine($report); @@ -65,7 +66,7 @@ public function createReportFile($report, $data) fclose($fp); try { - $fileFullNamePdf = CLIENT_BASE_PATH.'data/'.$fileFirstPart.".pdf"; + $fileFullNamePdf = BaseService::getInstance()->getDataDirectory().$fileFirstPart.".pdf"; //Try generating the pdf LogManager::getInstance()->debug( "wkhtmltopdf 1:".print_r(WK_HTML_PATH." ".$fileFullName." ".$fileFullNamePdf, true) diff --git a/core/src/Reports/Admin/Api/ReportBuilder.php b/core/src/Reports/Admin/Api/ReportBuilder.php index 28a98757a..fbeb98521 100644 --- a/core/src/Reports/Admin/Api/ReportBuilder.php +++ b/core/src/Reports/Admin/Api/ReportBuilder.php @@ -64,7 +64,7 @@ public function createReportFile($report, $data) $fileFirstPart = "Report_".str_replace(" ", "_", $report->name)."-".date("Y-m-d_H-i-s"); $fileName = $fileFirstPart.".csv"; - $fileFullName = CLIENT_BASE_PATH.'data/'.$fileName; + $fileFullName = BaseService::getInstance()->getDataDirectory().$fileName; $fp = fopen($fileFullName, 'w'); foreach ($data as $fields) { diff --git a/core/src/Reports/Admin/Reports/PayrollDataExport.php b/core/src/Reports/Admin/Reports/PayrollDataExport.php index b193d8e77..fb3fccd62 100644 --- a/core/src/Reports/Admin/Reports/PayrollDataExport.php +++ b/core/src/Reports/Admin/Reports/PayrollDataExport.php @@ -1,6 +1,7 @@ name)."-".date("Y-m-d_H-i-s"); $fileName = $fileFirstPart.".txt"; - $fileFullName = CLIENT_BASE_PATH.'data/'.$fileName; + $fileFullName = BaseService::getInstance()->getDataDirectory().$fileName; $fp = fopen($fileFullName, 'w'); fwrite($fp, json_encode($data, JSON_PRETTY_PRINT)); diff --git a/core/src/Settings/Admin/Api/SettingsInitialize.php b/core/src/Settings/Admin/Api/SettingsInitialize.php index 302baf7e6..51052295e 100644 --- a/core/src/Settings/Admin/Api/SettingsInitialize.php +++ b/core/src/Settings/Admin/Api/SettingsInitialize.php @@ -39,5 +39,7 @@ public function init() } BaseService::getInstance()->getMigrationManager()->ensureMigrations(); + $migrations = BaseService::getInstance()->getExtensionMigrations(); + BaseService::getInstance()->getMigrationManager()->ensureExtensionMigrations($migrations); } } diff --git a/core/src/Users/Admin/Api/UsersActionManager.php b/core/src/Users/Admin/Api/UsersActionManager.php index b5497c2b7..944fff22d 100644 --- a/core/src/Users/Admin/Api/UsersActionManager.php +++ b/core/src/Users/Admin/Api/UsersActionManager.php @@ -56,6 +56,10 @@ public function saveUser($req) "Error saving user" ); } + + $req->email = trim($req->email); + $req->username = trim($req->username); + if ($this->user->user_level == 'Admin') { $user = new User(); $user->Load("email = ?", array($req->email)); diff --git a/core/src/Utils/CalendarTools.php b/core/src/Utils/CalendarTools.php index 8294d615c..fcff766f1 100644 --- a/core/src/Utils/CalendarTools.php +++ b/core/src/Utils/CalendarTools.php @@ -1,6 +1,8 @@ format("m-d")); } + + public static function getServerDate() + { + $currentEmployeeTimeZone = BaseService::getInstance()->getCurrentEmployeeTimeZone(); + + if (empty($currentEmployeeTimeZone)) { + $currentEmployeeTimeZone = 'Asia/Colombo'; + } + date_default_timezone_set('Asia/Colombo'); + + $date = new \DateTime("now", new \DateTimeZone('Asia/Colombo')); + + $date->setTimezone(new \DateTimeZone($currentEmployeeTimeZone)); + return $date->format('Y-m-d'); + } } diff --git a/core/src/Utils/LogManager.php b/core/src/Utils/LogManager.php index d2df39f1b..d57e221ea 100644 --- a/core/src/Utils/LogManager.php +++ b/core/src/Utils/LogManager.php @@ -1,6 +1,7 @@ log->pushHandler(new StreamHandler('php://stderr', LOG_LEVEL)); } elseif (is_writable(ini_get('error_log'))) { self::$me->log->pushHandler(new StreamHandler(ini_get('error_log'), LOG_LEVEL)); - } elseif (is_writable(CLIENT_BASE_PATH.'data/app.log')) { - self::$me->log->pushHandler(new StreamHandler(CLIENT_BASE_PATH.'data/app.log', LOG_LEVEL)); + } elseif (is_writable(BaseService::getInstance()->getDataDirectory().'app.log')) { + self::$me->log->pushHandler( + new StreamHandler( + BaseService::getInstance()->getDataDirectory().'app.log', + LOG_LEVEL + ) + ); } else { self::$me->log->pushHandler(new StreamHandler('php://stderr', LOG_LEVEL)); } diff --git a/extensions/gitkeep b/extensions/gitkeep new file mode 100755 index 000000000..528030822 --- /dev/null +++ b/extensions/gitkeep @@ -0,0 +1 @@ +git keep diff --git a/gulpfile.js b/gulpfile.js index e5e1b0832..c7ad37d9c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -361,6 +361,47 @@ gulp.task('modules-js', (done) => { .pipe(gulp.dest('./web/dist')); }); +gulp.task('extension-js', (done) => { + let extension = process.argv.filter((item) => item.substr(0, 3) === '--x'); + if (extension.length === 1) { + extension = extension[0].substr(3); + } + + // map them to our stream function + return browserify({ + entries: [`extensions/${extension}/web/js/index.js`], + basedir: '.', + debug: true, + cache: {}, + packageCache: {}, + }) + .external(vendorsFlat) + .transform('babelify', { + plugins: [ + ['@babel/plugin-proposal-class-properties', { loose: true }], + ], + presets: ['@babel/preset-env', '@babel/preset-react'], + extensions: ['.js', '.jsx'], + }) + .transform(require('browserify-css')) + .bundle() + .pipe(source(`${extension}.js`)) + .pipe(buffer()) + .pipe(ifElse(!isProduction, () => sourcemaps.init({ loadMaps: true }))) + .pipe(ifElse(isProduction, () => uglifyes( + { + compress: true, + mangle: { + reserved: [], + }, + }, + ))) + .pipe(ifElse(isProduction, () => javascriptObfuscator({ + compact: true, + }))) + .pipe(ifElse(!isProduction, () => sourcemaps.write('./'))) + .pipe(gulp.dest(`./extensions/${extension}/dist`)); +}); gulp.task('watch', () => { gulp.watch('web/admin/src/*/*.js', gulp.series('admin-js')); diff --git a/release.md b/release.md index 05787e06c..f84783125 100644 --- a/release.md +++ b/release.md @@ -1,5 +1,35 @@ # Release Notes IceHrm Open Source +## Release note v31.0.0.OS + +### 🧲 New features + +* Allow filtering employee skills using either skill or the employee. +* Employee training sessions ability to filter by employee. +* Improvements to employee filters in education, certifications and languages. +* Ability set employee status and daily goals. + +### 🐛 Bug fixes +* Filtering fixed on monitor attendance page. +* Filters on employee travel request are fixed. +* Fix issue: searching from any other page than first page is not showing search results. +* Fix filters for employee projects. +* Changes to employee field names are now correctly reflected on employee profile. +* Fix company document visibility issues. +* Show active employee count under connection module. + +### 🗑️ For developers +* Add slider component. +* Add editor js. + +### 🗑️ Features removed +* Module grouping feature is removed. + +### 🛡️ Security improvements +* Fixing [https://github.com/gamonoid/icehrm/issues/284](https://github.com/gamonoid/icehrm/issues/284) +* Fixing [https://github.com/gamonoid/icehrm/issues/285](https://github.com/gamonoid/icehrm/issues/285) + + ## Release note v30.0.0.OS ### 🛡️ Security improvements diff --git a/web/admin/src/documents/lib.js b/web/admin/src/documents/lib.js index cfb2d50ec..328f1a5ba 100644 --- a/web/admin/src/documents/lib.js +++ b/web/admin/src/documents/lib.js @@ -2,14 +2,18 @@ Copyright (c) 2018 [Glacies UG, Berlin, Germany] (http://glacies.de) Developer: Thilina Hasantha (http://lk.linkedin.com/in/thilinah | https://github.com/thilinah) */ - -import AdapterBase from '../../../api/AdapterBase'; +import React from 'react'; +import { Space, Tag, Form } from 'antd'; +import { + EditOutlined, DeleteOutlined, InfoCircleOutlined, SettingOutlined, +} from '@ant-design/icons'; +import ReactifiedAdapterBase from '../../../api/ReactifiedAdapterBase'; /** * DocumentAdapter */ -class DocumentAdapter extends AdapterBase { +class DocumentAdapter extends ReactifiedAdapterBase { getDataMapping() { return [ 'id', @@ -35,9 +39,6 @@ class DocumentAdapter extends AdapterBase { ['expire_notification_week', { label: 'Notify Expiry Before One Week', type: 'select', source: [['Yes', 'Yes'], ['No', 'No']] }], ['expire_notification_day', { label: 'Notify Expiry Before One Day', type: 'select', source: [['Yes', 'Yes'], ['No', 'No']] }], ['share_with_employee', { label: 'Share with Employee', type: 'select', source: [['Yes', 'Yes'], ['No', 'No']] }], - // [ "sign", {"label":"Require Signature","type":"select","source":[["Yes","Yes"],["No","No"]]}], - // [ "sign", {"label":"Require Signature","type":"select","source":[["Yes","Yes"],["No","No"]]}], - // [ "sign_label", {"label":"Signature Description","type":"textarea","validation":"none"}], ['details', { label: 'Details', type: 'textarea', validation: 'none' }], ]; } @@ -52,12 +53,11 @@ class DocumentAdapter extends AdapterBase { * CompanyDocumentAdapter */ -class CompanyDocumentAdapter extends AdapterBase { +class CompanyDocumentAdapter extends ReactifiedAdapterBase { getDataMapping() { return [ 'id', 'name', - 'details', 'status', ]; } @@ -66,7 +66,6 @@ class CompanyDocumentAdapter extends AdapterBase { return [ { sTitle: 'ID', bVisible: false }, { sTitle: 'Name' }, - { sTitle: 'Details' }, { sTitle: 'Status' }, ]; } @@ -75,7 +74,7 @@ class CompanyDocumentAdapter extends AdapterBase { return [ ['id', { label: 'ID', type: 'hidden' }], ['name', { label: 'Name', type: 'text', validation: '' }], - ['details', { label: 'Details', type: 'textarea', validation: 'none' }], + ['details', { label: 'Details', type: 'editor', validation: 'none' }], ['status', { label: 'Status', type: 'select', source: [['Active', 'Active'], ['Inactive', 'Inactive'], ['Draft', 'Draft']] }], ['attachment', { label: 'Attachment', type: 'fileupload' }], [ @@ -103,13 +102,17 @@ class CompanyDocumentAdapter extends AdapterBase { ], ]; } + + getWidth() { + return 1100; + } } /** * EmployeeDocumentAdapter */ -class EmployeeDocumentAdapter extends AdapterBase { +class EmployeeDocumentAdapter extends ReactifiedAdapterBase { getDataMapping() { return [ 'id', @@ -130,7 +133,6 @@ class EmployeeDocumentAdapter extends AdapterBase { { sTitle: 'Details' }, { sTitle: 'Date Added' }, { sTitle: 'Status' }, - { sTitle: 'Attachment', bVisible: false }, ]; } @@ -148,7 +150,7 @@ class EmployeeDocumentAdapter extends AdapterBase { ['date_added', { label: 'Date Added', type: 'date', validation: '' }], ['valid_until', { label: 'Valid Until', type: 'date', validation: 'none' }], ['status', { label: 'Status', type: 'select', source: [['Active', 'Active'], ['Inactive', 'Inactive'], ['Draft', 'Draft']] }], - ['visible_to', { label: 'Visible To', type: 'select', source: [['Owner', 'Owner'], ['Manager', 'Manager'], ['Admin', 'Admin']] }], + ['visible_to', { label: 'Visible To', type: 'select', source: [['Owner', 'Owner'], ['Owner Only', 'Owner Only'], ['Manager', 'Manager'], ['Admin', 'Admin']] }], ['details', { label: 'Details', type: 'textarea', validation: 'none' }], ['attachment', { label: 'Attachment', type: 'fileupload', validation: '' }], ]; @@ -157,8 +159,8 @@ class EmployeeDocumentAdapter extends AdapterBase { getFilters() { return [ - ['employee', { label: 'Employee', type: 'select2', 'remote-source': ['Employee', 'id', 'first_name+last_name', 'getActiveSubordinateEmployees'] }], - + ['employee', { label: 'Employee', type: 'select2', 'allow-null': true, 'remote-source': ['Employee', 'id', 'first_name+last_name', 'getActiveSubordinateEmployees'] }], + ['document', { label: 'Document', type: 'select2', 'allow-null': true, 'remote-source': ['Document', 'id', 'name'] }], ]; } @@ -175,6 +177,28 @@ class EmployeeDocumentAdapter extends AdapterBase { return html; } + getTableActionButtonJsx(adapter) { + return (text, record) => ( + + modJs.edit(record.id)} style={{ cursor: 'pointer' }}> + + {` ${adapter.gt('Edit')}`} + + download(record.attachment)} style={{ cursor: 'pointer' }}> + + {` ${adapter.gt('Download Document')}`} + + {adapter.hasAccess('delete') + && ( + modJs.deleteRow(record.id)} style={{ cursor: 'pointer' }}> + + {` ${adapter.gt('Delete')}`} + + )} + + ); + } + isSubProfileTable() { return this.user.user_level !== 'Admin' && this.user.user_level !== 'Restricted Admin'; } diff --git a/web/admin/src/employees/components/EmployeeProfile.js b/web/admin/src/employees/components/EmployeeProfile.js index 309829b8d..2b372fa37 100644 --- a/web/admin/src/employees/components/EmployeeProfile.js +++ b/web/admin/src/employees/components/EmployeeProfile.js @@ -1,11 +1,12 @@ import React, {Component} from 'react'; -import {Col, Card, Skeleton, Avatar, Input, Row, Descriptions, Typography, Table, Space, Button, Tag, message, Tabs} from 'antd'; +import {Col, Card, Skeleton, Avatar, Input, Row, Descriptions, Typography, Table, Space, Button, Tag, message, Tabs, Dropdown, Menu} from 'antd'; import { FilterOutlined, EditOutlined, PhoneTwoTone, MailTwoTone, SyncOutlined, + DownOutlined, } from '@ant-design/icons'; import TagList from "../../../../components/TagList"; const { Search } = Input; @@ -60,6 +61,8 @@ class EmployeeProfile extends React.Component { } render() { + const { adapter } = this.props; + const gm = (text) => adapter.getMappedText(text); return ( <> @@ -71,7 +74,8 @@ class EmployeeProfile extends React.Component { this.updateProfileImage()}/> - {`${this.props.element.first_name} ${this.props.element.last_name}`} + {`${this.props.element.first_name} ${this.props.element.last_name}`} + {` ${this.props.element.mobile_phone || ''}`} @@ -82,14 +86,14 @@ class EmployeeProfile extends React.Component { - + {this.props.element.employee_id} - + {this.props.element.nic_num || ''} {this.props.element.ssn_num && this.props.element.ssn_num !== '' && - + {this.props.element.ssn_num || ''} } @@ -108,25 +112,25 @@ class EmployeeProfile extends React.Component { style={{ width: '100%' }} > - + {this.props.element.birthday || ''} - + {this.props.element.gender} - + {this.props.element.nationality_Name} - + {this.props.element.marital_status} - + {this.props.element.joined_date} - + {this.props.element.driving_license || ''} - + {this.props.element.other_id || ''} @@ -138,31 +142,31 @@ class EmployeeProfile extends React.Component { style={{ width: '100%' }} > - + {`${this.props.element.address1}, ${this.props.element.address2 || ''}`} - + {this.props.element.city} - + {this.props.element.country_Name} - + {this.props.element.postal_code} - + {` ${this.props.element.home_phone || ''}`} - + {` ${this.props.element.work_phone || ''}`} - + {` ${this.props.element.private_email || ''}`} @@ -177,16 +181,16 @@ class EmployeeProfile extends React.Component { style={{ width: '100%' }} > - + {this.props.element.job_title_Name} - + {this.props.element.employment_status_Name} - + {this.props.element.department_Name} - + {this.props.element.supervisor_Name} diff --git a/web/admin/src/employees/lib.js b/web/admin/src/employees/lib.js index 64f510053..812340629 100644 --- a/web/admin/src/employees/lib.js +++ b/web/admin/src/employees/lib.js @@ -33,6 +33,7 @@ class EmployeeAdapter extends ReactModalAdapterBase { constructor(endPoint, tab, filter, orderBy) { super(endPoint, tab, filter, orderBy); this.fieldNameMap = {}; + this.fieldNameMapOrig = {}; this.hiddenFields = {}; this.tableFields = {}; this.formOnlyFields = {}; @@ -47,6 +48,7 @@ class EmployeeAdapter extends ReactModalAdapterBase { for (let i = 0; i < fields.length; i++) { field = fields[i]; this.fieldNameMap[field.name] = field; + this.fieldNameMapOrig[field.textOrig] = field.textMapped; if (field.display === 'Hidden') { this.hiddenFields[field.name] = field; } else if (field.display === 'Table and Form' || field.display === 'Form') { @@ -162,6 +164,10 @@ class EmployeeAdapter extends ReactModalAdapterBase { return tableColumns; } + getMappedText(text) { + return this.fieldNameMapOrig[text] ? this.fieldNameMapOrig[text] : text; + } + showElement(element) { this.tableContainer.current.setCurrentElement(element); } @@ -901,7 +907,7 @@ class EmployeeSkillAdapter extends ReactModalAdapterBase { label: 'Employee', type: 'select2', sort: 'none', - 'allow-null': false, + 'allow-null': true, 'remote-source': ['Employee', 'id', 'first_name+last_name', 'getActiveSubordinateEmployees'], }], ['skill_id', { @@ -1000,7 +1006,7 @@ class EmployeeEducationAdapter extends SubProfileEnabledAdapterBase { label: 'Employee', type: 'select2', sort: 'none', - 'allow-null': false, + 'allow-null': true, 'remote-source': ['Employee', 'id', 'first_name+last_name', 'getActiveSubordinateEmployees'], }], ['education_id', { @@ -1099,7 +1105,7 @@ class EmployeeCertificationAdapter extends SubProfileEnabledAdapterBase { label: 'Employee', type: 'select2', sort: 'none', - 'allow-null': false, + 'allow-null': true, 'remote-source': ['Employee', 'id', 'first_name+last_name', 'getActiveSubordinateEmployees'], }], ['certification_id', { @@ -1213,7 +1219,7 @@ class EmployeeLanguageAdapter extends SubProfileEnabledAdapterBase { label: 'Employee', type: 'select2', sort: 'none', - 'allow-null': false, + 'allow-null': true, 'remote-source': ['Employee', 'id', 'first_name+last_name', 'getActiveSubordinateEmployees'], }], ['language_id', { diff --git a/web/admin/src/overtime/index.js b/web/admin/src/overtime/index.js index 25a00705d..2d88ed858 100644 --- a/web/admin/src/overtime/index.js +++ b/web/admin/src/overtime/index.js @@ -3,5 +3,24 @@ import { EmployeeOvertimeAdminAdapter, } from './lib'; -window.OvertimeCategoryAdapter = OvertimeCategoryAdapter; -window.EmployeeOvertimeAdminAdapter = EmployeeOvertimeAdminAdapter; +import IceDataPipe from '../../../api/IceDataPipe'; + +function init(data) { + const modJsList = {}; + + modJsList.tabOvertimeCategory = new OvertimeCategoryAdapter('OvertimeCategory', 'OvertimeCategory', '', ''); + modJsList.tabOvertimeCategory.setObjectTypeName('Overtime Category'); + modJsList.tabOvertimeCategory.setDataPipe(new IceDataPipe(modJsList.tabOvertimeCategory)); + modJsList.tabOvertimeCategory.setAccess(data.permissions.OvertimeCategory); + + modJsList.tabEmployeeOvertime = new EmployeeOvertimeAdminAdapter('EmployeeOvertime', 'EmployeeOvertime', '', ''); + modJsList.tabEmployeeOvertime.setObjectTypeName('Overtime Request'); + modJsList.tabEmployeeOvertime.setDataPipe(new IceDataPipe(modJsList.tabEmployeeOvertime)); + modJsList.tabEmployeeOvertime.setAccess(data.permissions.EmployeeOvertime); + + + window.modJs = modJsList.tabOvertimeCategory; + window.modJsList = modJsList; +} + +window.initAdminOvertime = init; diff --git a/web/admin/src/overtime/lib.js b/web/admin/src/overtime/lib.js index 69cb72a8a..8e3239f75 100644 --- a/web/admin/src/overtime/lib.js +++ b/web/admin/src/overtime/lib.js @@ -3,34 +3,14 @@ Developer: Thilina Hasantha (http://lk.linkedin.com/in/thilinah | https://github.com/thilinah) */ -import AdapterBase from '../../../api/AdapterBase'; -import ApproveAdminAdapter from '../../../api/ApproveAdminAdapter'; +import ReactApproveAdminAdapter from '../../../api/ReactApproveAdminAdapter'; +import ReactIdNameAdapter from '../../../api/ReactIdNameAdapter'; /** * OvertimeCategoryAdapter */ -class OvertimeCategoryAdapter extends AdapterBase { - getDataMapping() { - return [ - 'id', - 'name', - ]; - } - - getHeaders() { - return [ - { sTitle: 'ID', bVisible: false }, - { sTitle: 'Name' }, - ]; - } - - getFormFields() { - return [ - ['id', { label: 'ID', type: 'hidden' }], - ['name', { label: 'Name', type: 'text', validation: '' }], - ]; - } +class OvertimeCategoryAdapter extends ReactIdNameAdapter { } @@ -39,7 +19,7 @@ class OvertimeCategoryAdapter extends AdapterBase { */ -class EmployeeOvertimeAdminAdapter extends ApproveAdminAdapter { +class EmployeeOvertimeAdminAdapter extends ReactApproveAdminAdapter { constructor(endPoint, tab, filter, orderBy) { super(endPoint, tab, filter, orderBy); this.itemName = 'OvertimeRequest'; @@ -71,6 +51,37 @@ class EmployeeOvertimeAdminAdapter extends ApproveAdminAdapter { ]; } + getTableColumns() { + return [ + { + title: 'Employee', + dataIndex: 'employee', + sorter: true, + }, + { + title: 'Category', + dataIndex: 'category', + sorter: true, + }, + { + title: 'Start Time', + dataIndex: 'start_time', + }, + { + title: 'End Time', + dataIndex: 'end_time', + }, + { + title: 'Project', + dataIndex: 'project', + }, + { + title: 'Status', + dataIndex: 'status', + }, + ]; + } + getFormFields() { return [ ['id', { label: 'ID', type: 'hidden' }], diff --git a/web/admin/src/payroll/lib.js b/web/admin/src/payroll/lib.js index ec9bb165d..468b2a845 100644 --- a/web/admin/src/payroll/lib.js +++ b/web/admin/src/payroll/lib.js @@ -344,7 +344,7 @@ class PayrollColumnAdapter extends AdapterBase { ['enabled', { label: 'Enabled', type: 'select', source: [['Yes', 'Yes'], ['No', 'No']] }], ['default_value', { label: 'Default Value', type: 'text', validation: '' }], fucntionColumnList, - ['function_type', { label: 'Function Type', type: 'select', source: [['Simple', 'Simple']] }], + ['function_type', { label: 'Function Type', type: 'select', source: [['Advanced', 'Advanced'], ['Simple', 'Simple']] }], ['calculation_function', { label: 'Function', type: 'code', validation: 'none' }], ]; } diff --git a/web/admin/src/projects/lib.js b/web/admin/src/projects/lib.js index 4dce2fc4e..1eb6aab95 100644 --- a/web/admin/src/projects/lib.js +++ b/web/admin/src/projects/lib.js @@ -127,7 +127,7 @@ class EmployeeProjectAdapter extends ReactModalAdapterBase { getFormFields() { return [ ['id', { label: 'ID', type: 'hidden' }], - ['employee', { label: 'Employee', type: 'select2', 'remote-source': ['Employee', 'id', 'first_name+last_name'] }], + ['employee', { label: 'Employee', type: 'select2', 'remote-source': ['Employee', 'id', 'first_name+last_name','getActiveSubordinateEmployees'] }], ['project', { label: 'Project', type: 'select2', 'remote-source': ['Project', 'id', 'name'] }], ['details', { label: 'Details', type: 'textarea', validation: 'none' }], ]; @@ -135,8 +135,8 @@ class EmployeeProjectAdapter extends ReactModalAdapterBase { getFilters() { return [ - ['employee', { label: 'Employee', type: 'select2', 'remote-source': ['Employee', 'id', 'first_name+last_name'] }], - + ['employee', { label: 'Employee', type: 'select2','allow-null': true, 'remote-source': ['Employee', 'id', 'first_name+last_name','getActiveSubordinateEmployees'] }], + ['project', { label: 'Project', type: 'select2','allow-null': true, 'remote-source': ['Project', 'id', 'name'] }], ]; } diff --git a/web/admin/src/travel/lib.js b/web/admin/src/travel/lib.js index 726f8e141..10da535b7 100644 --- a/web/admin/src/travel/lib.js +++ b/web/admin/src/travel/lib.js @@ -185,6 +185,12 @@ class EmployeeTravelRecordAdminAdapter extends ApproveAdminAdapter { ['attachment3', { label: 'Attachment', type: 'fileupload', validation: 'none' }], ]); } + + getFilters() { + return [ + ['employee', { label: 'Employee', type: 'select2', 'allow-null': true, 'remote-source': ['Employee', 'id', 'first_name+last_name', 'getActiveSubordinateEmployees'] }], + ]; + } } module.exports = { diff --git a/web/api/AdapterBase.js b/web/api/AdapterBase.js index 8a293e56f..c0ae67c4c 100644 --- a/web/api/AdapterBase.js +++ b/web/api/AdapterBase.js @@ -19,6 +19,7 @@ class AdapterBase extends ModuleBase { this.origFilter = null; this.orderBy = null; this.currentElement = null; + this.title = null; this.initAdapter(endPoint, tab, filter, orderBy); } @@ -86,6 +87,10 @@ class AdapterBase extends ModuleBase { this.currentFilterString = this.getFilterString(filter); } + setTitle(title) { + this.title = title; + } + getFilter() { return this.filter; } @@ -284,6 +289,10 @@ class AdapterBase extends ModuleBase { return false; } + isAdminUser() { + return this.user.user_level === 'Admin'; + } + remoteTableSkipProfileRestriction() { return false; } diff --git a/web/api/CrudAdapter.js b/web/api/CrudAdapter.js new file mode 100644 index 000000000..b0cca38d0 --- /dev/null +++ b/web/api/CrudAdapter.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { + Modal +} from 'antd'; +import { + ExclamationCircleOutlined +} from "@ant-design/icons"; +const qs = require('qs'); +const axios = require('axios'); + +class CrudAdapter { + constructor(adapter) { + this.adapter = adapter; + } + + delete(id, callback) { + const that = this; + + const deleteConfirm = () => { + this.adapter.requestCache.invalidateTable(that.adapter.table); + + const data = { + t: that.adapter.table, + a: 'delete', id + }; + // Using qs because when calling service.php axios should send parameters as formData (not in JSON body) + axios.post(that.adapter.moduleRelativeURL, qs.stringify(data)) + .then((data) => { + if (data.data.status === 'SUCCESS') { + callback(); + that.adapter.trackEvent('delete', that.adapter.tab, that.adapter.table); + } else { + Modal.error({ + title: 'Error', + content: `Error deleting item: ${data.data.data}`, + okText: 'Ok', + }); + } + + }).catch ((e) => { + Modal.error({ + title: 'Error', + content: `${e.message}. ${e.response.data.message}`, + okText: 'Ok', + }); + }); + }; + + Modal.confirm({ + title: 'Confirm', + icon: , + content: 'Do you want to delete this item?', + okText: 'Delete', + cancelText: 'Cancel', + onOk: deleteConfirm, + }); + } +} + +export default CrudAdapter; \ No newline at end of file diff --git a/web/api/IceApiClient.js b/web/api/IceApiClient.js index a011e501d..b12d7ac55 100644 --- a/web/api/IceApiClient.js +++ b/web/api/IceApiClient.js @@ -20,6 +20,20 @@ class IceApiClient { }, }); } + + post(endpoint, data) { + if (this.legacyApiWrapper) { + const url = `${this.clientBaseUrl}api/index.php?token=${this.token}&method=post&url=/${endpoint}`; + return axios.post(url, data); + } + + return axios.post(this.baseUrl + endpoint, { + headers: { + Authorization: `Bearer ${this.token}`, + }, + data, + }); + } } export default IceApiClient; diff --git a/web/api/ModuleBase.js b/web/api/ModuleBase.js index ce9676d75..81d7c9dab 100644 --- a/web/api/ModuleBase.js +++ b/web/api/ModuleBase.js @@ -287,16 +287,26 @@ class ModuleBase { let values; const fields = this.getFormFields(); const filterFields = this.getFilters(); + const additionalFields = this.getAdditionalRemoteFields(); if (filterFields != null) { for (let j = 0; j < filterFields.length; j++) { - values = this.getMetaFieldValues(filterFields[j][0], fields); - if (values == null || (values.type !== 'select' && values.type !== 'select2' && values.type !== 'select2multi')) { + values = filterFields[j][1]; + if (values.type === 'select' || values.type === 'select2' || values.type === 'select2multi') { fields.push(filterFields[j]); } } } + if (additionalFields != null) { + for (let j = 0; j < additionalFields.length; j++) { + values = additionalFields[j][1]; + if (values.type === 'select' || values.type === 'select2' || values.type === 'select2multi') { + fields.push(additionalFields[j]); + } + } + } + const remoteSourceFields = []; const remoteSourceFieldKeys = []; let field = null; @@ -445,27 +455,27 @@ class ModuleBase { type = type.toLowerCase(); const iconMap = {}; - iconMap.pdf = 'fa fa-file-pdf-o'; - iconMap.csv = 'fa fa fa-file-code-o'; - iconMap.xls = 'fa fa-file-excel-o'; - iconMap.xlsx = 'fa fa-file-excel-o'; - iconMap.doc = 'fa fa-file-word-o'; - iconMap.docx = 'fa fa-file-word-o'; - iconMap.ppt = 'fa fa-file-powerpoint-o'; - iconMap.pptx = 'fa fa-file-powerpoint-o'; - iconMap.jpg = 'fa fa-file-image-o'; - iconMap.jpeg = 'fa fa-file-image-o'; - iconMap.gif = 'fa fa-file-image-o'; - iconMap.png = 'fa fa-file-image-o'; - iconMap.bmp = 'fa fa-file-image-o'; - iconMap.txt = 'fa fa-file-text-o'; - iconMap.rtf = 'fa fa-file-text-o'; + iconMap.pdf = 'far fa-file-pdf'; + iconMap.csv = 'fa far fa-file-code'; + iconMap.xls = 'far fa-file-excel'; + iconMap.xlsx = 'far fa-file-excel'; + iconMap.doc = 'far fa-file-word'; + iconMap.docx = 'far fa-file-word'; + iconMap.ppt = 'far fa-file-powerpoint'; + iconMap.pptx = 'far fa-file-powerpoint'; + iconMap.jpg = 'far fa-file-image'; + iconMap.jpeg = 'far fa-file-image'; + iconMap.gif = 'far fa-file-image'; + iconMap.png = 'far fa-file-image'; + iconMap.bmp = 'far fa-file-image'; + iconMap.txt = 'far fa-file-text'; + iconMap.rtf = 'far fa-file-text'; if (iconMap[type] !== undefined || iconMap[type] != null) { return iconMap[type]; } - return 'fa fa-file-o'; + return 'far fa-file'; } getSourceMapping() { @@ -829,6 +839,10 @@ class ModuleBase { return null; } + getAdditionalRemoteFields() { + return null; + } + /** * Show the edit form for an item * @method edit diff --git a/web/api/ReactIdNameAdapter.js b/web/api/ReactIdNameAdapter.js index 7aa92edfb..2a3fbf4e9 100644 --- a/web/api/ReactIdNameAdapter.js +++ b/web/api/ReactIdNameAdapter.js @@ -28,11 +28,6 @@ class ReactIdNameAdapter extends ReactModalAdapterBase { getTableColumns() { return [ - { - title: 'Id', - dataIndex: 'id', - sorter: true, - }, { title: 'Name', dataIndex: 'name', diff --git a/web/api/ReactModalAdapterBase.js b/web/api/ReactModalAdapterBase.js index 5f02b0452..f2221fc4e 100644 --- a/web/api/ReactModalAdapterBase.js +++ b/web/api/ReactModalAdapterBase.js @@ -95,6 +95,14 @@ class ReactModalAdapterBase extends AdapterBase { return true; } + keepTableVisibleWhileShowingCustomView() { + return false; + } + + getFormLayout(viewOnly) { + return 'horizontal'; + } + initForm() { if (this.formInitialized) { return false; @@ -103,6 +111,7 @@ class ReactModalAdapterBase extends AdapterBase { if (this.modalType === this.MODAL_TYPE_NORMAL) { ReactDOM.render( { + if (headers[index]) { + return { + title: headers[index].sTitle, + dataIndex: field, + sorter: true, + ...(headers[index].fieldRenderer ? { render: headers[index].fieldRenderer } : {}) + } + } + return null; + }).filter(column => !!column); + } +} + +export default ReactifiedAdapterBase; diff --git a/web/components/CustomIcons.js b/web/components/CustomIcons.js new file mode 100644 index 000000000..2737198a0 --- /dev/null +++ b/web/components/CustomIcons.js @@ -0,0 +1,144 @@ +/** + * https://openmoji.org/ + */ +import Icon from '@ant-design/icons'; +import React from 'react'; + +const WonderfulFace = () => ( + + + +); + +const WonderfulFaceIcon = (props) => ; + +const HappyFace = () => ( + + + +); + +const HappyFaceIcon = (props) => ; + +const NeutralFace = () => ( + + + +); + +const NeutralFaceIcon = (props) => ; + +const SadFace = () => ( + + + +); + +const SadFaceIcon = (props) => ; + +const SleepyFace = () => ( + + + +); + +const SleepyFaceIcon = (props) => ; + +const ExhaustedFace = () => ( + + + +); + +const ExhaustedFaceIcon = (props) => ; + +const Motivated = () => ( + + + +); + +const MotivatedIcon = (props) => ; + +const DoNotDisturb = () => ( + + + +); + +const DoNotDisturbIcon = (props) => ; + +const Busy = () => ( + + + +); + +const BusyIcon = (props) => ; + +const OutForLunch = () => ( + + + +); + +const OutForLunchIcon = (props) => ; + +const Away = () => ( + + + +); + +const AwayIcon = (props) => ; + +const Available = () => ( + + + +); + +const AvailableIcon = (props) => ; + +const Meeting = () => ( + + + +); + +const MeetingIcon = (props) => ; + +const NotSet = () => ( + + + +); + +const NotSetIcon = (props) => ; + +const Sick = () => ( + + + +); + +const SickIcon = (props) => ; + + +module.exports = { + WonderfulFaceIcon, + HappyFaceIcon, + NeutralFaceIcon, + SadFaceIcon, + SleepyFaceIcon, + ExhaustedFaceIcon, + MotivatedIcon, + DoNotDisturbIcon, + BusyIcon, + OutForLunchIcon, + AwayIcon, + MeetingIcon, + AvailableIcon, + NotSetIcon, + SickIcon, +}; diff --git a/web/components/EmployeeStatus.js b/web/components/EmployeeStatus.js new file mode 100644 index 000000000..94058734a --- /dev/null +++ b/web/components/EmployeeStatus.js @@ -0,0 +1,266 @@ +import React from 'react'; +import {Button, Dropdown, Menu, Space, Skeleton, Input, Typography, Tag} from 'antd'; +const { TextArea } = Input; +import { + WonderfulFaceIcon, + HappyFaceIcon, + NeutralFaceIcon, + SadFaceIcon, + SleepyFaceIcon, + ExhaustedFaceIcon, + MotivatedIcon, + DoNotDisturbIcon, + BusyIcon, + OutForLunchIcon, + AwayIcon, + MeetingIcon, + AvailableIcon, + NotSetIcon, + SickIcon, +} from './CustomIcons'; + +const { Title, Text } = Typography; + +import {DownOutlined, EditOutlined} from "@ant-design/icons"; + + +class EmployeeStatus extends React.Component { + state = { + feeling: 0, + status: 0, + message: '', + loading: true, + } + + constructor(props) { + super(props); + this.inputRef = React.createRef(); + this.openModelCallback = props.openModelCallback; + } + + componentDidMount() { + this.fetch(); + } + + fetch() { + this.props.apiClient + .get(`employees/${this.props.employee}/status`) + .then((response) => { + const feeling = response.data.status != null ? response.data.feeling : 0; + const status = response.data.status != null ? response.data.status : 0; + this.setState({ + feeling: parseInt(feeling, 10), + status: parseInt(status, 10), + message: response.data.message, + loading: false, + }); + }); + } + + handleStatusSelect(e) { + this.setState({ + status: parseInt(e.key, 10), + loading: true, + }, () => {this.syncState()}); + } + + handleFeelingSelect(e) { + this.setState({ + feeling: parseInt(e.key, 10), + loading: true, + }, () => {this.syncState()}); + } + + onTextClick() { + this.openModelCallback(); + } + + onMessageChange() { + this.setState({ + message: this.inputRef.state.value, + }); + } + + onMessageBlur() { + this.setState({ + message: this.inputRef.state.value, + }, () => {this.syncState()}); + } + + syncState() { + const data = { + feeling: this.state.feeling, + status: this.state.status, + message: this.state.message, + } + this.props.apiClient + .post(`employees/${this.props.employee}/status`, data) + .then((response) => { + this.setState({ + feeling: parseInt(response.data.feeling, 10), + status: parseInt(response.data.status, 10), + message: response.data.message, + loading: false, + }); + }); + } + + render() { + + const getStatusData = () => { + const states = {}; + states[0] = ['No Status Set', ]; + states[1] = ['Feeling Wonderful', ]; + states[2] = ['Feeling Happy', ]; + states[3] = ['Feeling OK', ]; + states[4] = ['Feeling Sad', ]; + states[5] = ['Feeling Sleepy', ]; + states[6] = ['Feeling Exhausted', ]; + states[7] = ['Feeling Motivated', ]; + states[8] = ['Do not Disturb', ]; + states[9] = ['Busy', ]; + states[10] = ['Out for Lunch', ]; + states[11] = ['Away', ]; + states[12] = ['Available', ]; + states[13] = ['In a Meeting', ]; + states[14] = ['Not Feeling Well', ]; + + return states; + } + + const menuFeelings = ( + + }> + Feeling Wonderful + + }> + Feeling Happy + + }> + Feeling OK + + }> + Feeling Sad + + }> + Feeling Sleepy + + }> + Feeling Exhausted + + }> + Feeling Motivated + + }> + Not Feeling Well + + + ); + + const menuStatus = ( + + }> + Do not Disturb + + }> + Busy + + }> + Out for Lunch + + }> + Away + + }> + Available + + }> + In a Meeting + + + ); + + const getIcon = (id) => { + + const data = getStatusData(); + const item = data[id]; + const icon = (item != null) ? item[1] : null; + + if (icon == null) { + return () + } + + return icon; + } + + const getStatusDescription = (id) => { + + const data = getStatusData(); + const item = data[id]; + const label = (item != null) ? item[0] : null; + if (label == null) { + return 'Away'; + } + return label; + } + + return ( + + {this.props.showStatusSelect && + + + + + + + + + + } + {!this.props.showInput && + + + {this.props.adapter.gt('Today\'s Goal')} + {this.state.message || this.props.adapter.gt("Click edit set your goal for the day!")} + } color="processing" onClick={this.onTextClick.bind(this)}> + {this.props.adapter.gt('Edit')} + + + + } + <> + {this.props.showInput && this.state.message && +