diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index aad116f..61f9d0f 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -127,9 +127,9 @@ }, "class.Users.php": { "type": "-", - "size": 4291, - "lmtime": 1770653518273, - "modified": true + "size": 4974, + "lmtime": 1772141683315, + "modified": false }, "class.Wiki.php": { "type": "-", @@ -319,8 +319,8 @@ }, "index.php": { "type": "-", - "size": 3592, - "lmtime": 1772131174733, + "size": 3935, + "lmtime": 1772141695415, "modified": false }, "layout": { @@ -597,8 +597,8 @@ "users": { "login-form.php": { "type": "-", - "size": 3336, - "lmtime": 0, + "size": 3355, + "lmtime": 1772141702952, "modified": false }, "main-view.php": { diff --git a/autoload/Controllers/UsersController.php b/autoload/Controllers/UsersController.php index 17f8313..d9df18a 100644 --- a/autoload/Controllers/UsersController.php +++ b/autoload/Controllers/UsersController.php @@ -373,8 +373,119 @@ class UsersController exit; } + public static function permissionPopup() + { + global $user, $mdb; + header( 'Content-Type: application/json; charset=utf-8' ); + + $response = [ 'status' => 'error', 'msg' => 'Nie mozna otworzyc ustawien uprawnien.' ]; + + if ( !$user || !self::canManageUsers( $user, self::getImpersonatorUser() ) ) + { + $response['msg'] = 'Brak uprawnien.'; + echo json_encode( $response ); + exit; + } + + if ( !\S::csrf_verify() ) + { + $response['msg'] = 'Nieprawidlowy token bezpieczenstwa. Odswiez strone.'; + echo json_encode( $response ); + exit; + } + + $target_user_id = (int)\S::get( 'user_id' ); + if ( !$target_user_id ) + { + echo json_encode( $response ); + exit; + } + + $users_repository = new \Domain\Users\UserRepository(); + $target_user = $users_repository -> byId( $target_user_id ); + if ( !$target_user ) + { + $response['msg'] = 'Nie znaleziono uzytkownika.'; + echo json_encode( $response ); + exit; + } + + $permission_repo = new \Domain\Users\PermissionRepository( $mdb ); + $permissions = (int)$target_user['id'] === self::ADMIN_USER_ID + ? \Domain\Users\PermissionRepository::defaults() + : $permission_repo -> byUserId( (int)$target_user['id'] ); + + $defs = self::permissionDefinitions(); + + $response = [ + 'status' => 'success', + 'popup_content' => \Tpl::view( 'users/permissions-popup', [ + 'target_user' => $target_user, + 'permissions' => $permissions, + 'module_labels' => $defs['module_labels'], + 'permission_groups' => $defs['permission_groups'] + ] ) + ]; + + echo json_encode( $response ); + exit; + } + + public static function permissionSaveBulk() + { + global $user, $mdb; + header( 'Content-Type: application/json; charset=utf-8' ); + + $response = [ 'status' => 'error', 'msg' => 'Wystapil blad podczas zapisywania uprawnien.' ]; + + if ( !$user || !self::canManageUsers( $user, self::getImpersonatorUser() ) ) + { + $response['msg'] = 'Brak uprawnien.'; + echo json_encode( $response ); + exit; + } + + if ( !\S::csrf_verify() ) + { + $response['msg'] = 'Nieprawidlowy token bezpieczenstwa. Odswiez strone.'; + echo json_encode( $response ); + exit; + } + + $target_user_id = (int)\S::get( 'user_id' ); + $selected_modules_raw = (string)\S::get( 'selected_modules' ); + + if ( !$target_user_id ) + { + echo json_encode( $response ); + exit; + } + + if ( $target_user_id === self::ADMIN_USER_ID ) + { + $response['msg'] = 'Nie mozna zmieniac uprawnien administratora.'; + echo json_encode( $response ); + exit; + } + + $selected_modules = array_filter( array_map( 'trim', explode( ',', $selected_modules_raw ) ) ); + $selected_modules = array_values( array_unique( $selected_modules ) ); + + $payload = []; + foreach ( \Domain\Users\PermissionRepository::MODULES as $module ) + $payload[ $module ] = in_array( $module, $selected_modules, true ) ? 1 : 0; + + $repo = new \Domain\Users\PermissionRepository( $mdb ); + $repo -> save( $target_user_id, $payload ); + + echo json_encode( [ 'status' => 'success', 'msg' => 'Uprawnienia zostaly zapisane.' ] ); + exit; + } + public static function buildMainViewModel( $current_user, $impersonator_user, array $users, array $permissions_map = [] ) { + $defs = self::permissionDefinitions(); + return [ 'current_user' => $current_user, 'impersonator_user' => $impersonator_user, @@ -383,14 +494,8 @@ class UsersController 'can_switch_back' => is_array( $impersonator_user ) and isset( $impersonator_user['id'] ) and (int)$impersonator_user['id'] === self::ADMIN_USER_ID, 'permissions_map' => $permissions_map, 'modules' => \Domain\Users\PermissionRepository::MODULES, - 'module_labels' => [ - 'tasks' => 'Zadania', - 'projects' => 'Projekty', - 'work_time' => 'Czas pracy', - 'finances' => 'Finanse', - 'crm' => 'CRM', - 'wiki' => 'Wiki' - ] + 'module_labels' => $defs['module_labels'], + 'permission_groups' => $defs['permission_groups'] ]; } @@ -423,4 +528,26 @@ class UsersController header( 'Location: /' ); exit; } + + private static function permissionDefinitions() + { + return [ + 'module_labels' => [ + 'tasks' => 'Zadania', + 'projects_view' => 'Projekty: przegladanie', + 'projects_add' => 'Projekty: dodawanie', + 'projects_edit' => 'Projekty: edycja', + 'projects_delete' => 'Projekty: usuwanie', + 'work_time' => 'Czas pracy', + 'finances' => 'Finanse', + 'crm' => 'CRM', + 'wiki' => 'Wiki' + ], + 'permission_groups' => [ + 'Podstawowe' => [ 'tasks', 'work_time', 'wiki' ], + 'Projekty' => [ 'projects_view', 'projects_add', 'projects_edit', 'projects_delete' ], + 'Pozostale' => [ 'finances', 'crm' ] + ] + ]; + } } diff --git a/autoload/Domain/Users/PermissionRepository.php b/autoload/Domain/Users/PermissionRepository.php index 7ef0a3f..8484dd3 100644 --- a/autoload/Domain/Users/PermissionRepository.php +++ b/autoload/Domain/Users/PermissionRepository.php @@ -3,11 +3,14 @@ namespace Domain\Users; class PermissionRepository { - const MODULES = [ 'tasks', 'projects', 'finances', 'wiki', 'crm', 'work_time' ]; + const MODULES = [ 'tasks', 'projects_view', 'projects_add', 'projects_edit', 'projects_delete', 'finances', 'wiki', 'crm', 'work_time' ]; const DEFAULTS = [ 'tasks' => 1, - 'projects' => 1, + 'projects_view' => 1, + 'projects_add' => 1, + 'projects_edit' => 1, + 'projects_delete' => 1, 'finances' => 0, 'wiki' => 1, 'crm' => 0, @@ -42,8 +45,16 @@ class PermissionRepository return self::defaults(); $result = []; + $legacy_projects_value = isset( $row['projects'] ) ? (int)$row['projects'] : 1; foreach ( self::MODULES as $module ) - $result[ $module ] = isset( $row[ $module ] ) ? (int)$row[ $module ] : 0; + { + if ( isset( $row[ $module ] ) ) + $result[ $module ] = (int)$row[ $module ]; + else if ( strpos( $module, 'projects_' ) === 0 ) + $result[ $module ] = $legacy_projects_value; + else + $result[ $module ] = isset( self::DEFAULTS[ $module ] ) ? (int)self::DEFAULTS[ $module ] : 0; + } return $result; } diff --git a/autoload/controls/class.Site.php b/autoload/controls/class.Site.php index e265363..54a4ce0 100644 --- a/autoload/controls/class.Site.php +++ b/autoload/controls/class.Site.php @@ -14,7 +14,7 @@ class Site $module = \S::get( 'module' ); $action = \S::get( 'action' ); - if ( !\controls\Users::permissions( $user['id'], $module ) ) + if ( !\controls\Users::permissions( $user['id'], $module, $action ) ) return; // New Controllers namespace (camelCase methods) diff --git a/autoload/controls/class.Tasks.php b/autoload/controls/class.Tasks.php index 3ec9587..1ca0873 100644 --- a/autoload/controls/class.Tasks.php +++ b/autoload/controls/class.Tasks.php @@ -353,6 +353,17 @@ class Tasks exit; } + static public function task_change_text() { + global $mdb; + + if ( $mdb -> update( 'tasks', [ 'text' => \S::get( 'text' ) ], [ 'id' => \S::get( 'task_id' ) ] ) ) { + echo json_encode( [ 'status' => 'success' ] ); + } else { + echo json_encode( [ 'status' => 'error' ] ); + } + exit; + } + static public function task_change_users() { global $mdb, $user; @@ -477,12 +488,15 @@ class Tasks $values['status_change_mail'] = isset( $values['status_change_mail'] ) ? $values['status_change_mail'] : 'off'; $values['send_email_notification'] = isset( $values['send_email_notification'] ) ? $values['send_email_notification'] : 'off'; $values['users'] = isset( $values['users'] ) ? $values['users'] : []; - $values['priority'] = isset( $values['priority'] ) && $values['priority'] !== '' ? $values['priority'] : 0; + $values['parent_id'] = isset( $values['parent_id'] ) && $values['parent_id'] !== '' ? (int)$values['parent_id'] : null; + if ( !empty( $values['id'] ) and (int)$values['id'] === (int)$values['parent_id'] ) + $values['parent_id'] = null; + $values['priority'] = isset( $values['priority'] ) && $values['priority'] !== '' ? $values['priority'] : ( empty( $values['id'] ) ? 1 : 0 ); $values['recursive_last_date'] = isset( $values['recursive_last_date'] ) ? $values['recursive_last_date'] : null; $status = \Controllers\TasksController::resolveTaskStatusForSave( $values ); if ( $id = \factory\Tasks::task_save( - $values['id'], null, $user['id'], $values['name'], $values['text'], $values['date_start'], $values['date_end'], $values['project_id'], $values['client_id'], $values['pay_rate'], $values['reminders_interval'], $values['recursively'], $values['frequency'], $values['period'], $values['users'], null, null, $values['send_email_notification'], $values['status_change_mail'], false, $status, $values['show_in_calendar'], $values['priority'], $values['recursive_last_date'] + $values['id'], $values['parent_id'], $user['id'], $values['name'], $values['text'], $values['date_start'], $values['date_end'], $values['project_id'], $values['client_id'], $values['pay_rate'], $values['reminders_interval'], $values['recursively'], $values['frequency'], $values['period'], $values['users'], null, null, $values['send_email_notification'], $values['status_change_mail'], false, $status, $values['show_in_calendar'], $values['priority'], $values['recursive_last_date'] ) ) { \factory\Tasks::clear_task_opened( $id ); diff --git a/autoload/controls/class.Users.php b/autoload/controls/class.Users.php index 9f1e5e8..f085286 100644 --- a/autoload/controls/class.Users.php +++ b/autoload/controls/class.Users.php @@ -19,6 +19,35 @@ class Users $cache[ $user_id ] = $repo -> byUserId( (int)$user_id ); } + if ( $module === 'projects' ) + { + $permissions = $cache[ $user_id ]; + + if ( !$action ) + return isset( $permissions['projects_view'] ) ? (bool)$permissions['projects_view'] : true; + + if ( $action === 'project_delete' ) + return isset( $permissions['projects_delete'] ) ? (bool)$permissions['projects_delete'] : false; + + if ( $action === 'project_edit' || $action === 'project_save' ) + { + $project_id = (int)\S::get( 'project_id' ); + $values = \S::json_to_array( \S::get( 'values' ) ); + if ( is_array( $values ) && isset( $values['id'] ) ) + $project_id = (int)$values['id']; + + if ( $project_id > 0 ) + return isset( $permissions['projects_edit'] ) ? (bool)$permissions['projects_edit'] : false; + + return isset( $permissions['projects_add'] ) ? (bool)$permissions['projects_add'] : false; + } + + if ( strpos( $action, 'project_' ) === 0 ) + return isset( $permissions['projects_view'] ) ? (bool)$permissions['projects_view'] : false; + + return isset( $permissions['projects_view'] ) ? (bool)$permissions['projects_view'] : false; + } + if ( $module && isset( $cache[ $user_id ][ $module ] ) ) return (bool)$cache[ $user_id ][ $module ]; @@ -28,15 +57,18 @@ class Users public static function logout() { - global $mdb, $user; + global $mdb; $domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); $cookie_name = str_replace( '.', '-', $domain ); + $remember_token = $_COOKIE[$cookie_name] ?? ''; - if ( $user && isset( $user['id'] ) ) - $mdb -> update( 'users', [ 'remember_token' => null ], [ 'id' => $user['id'] ] ); + if ( is_string( $remember_token ) && strlen( $remember_token ) === 64 && ctype_xdigit( $remember_token ) ) + { + $mdb -> delete( 'users_remember_tokens', [ 'token_hash' => hash( 'sha256', $remember_token ) ] ); + } - setcookie( $cookie_name, "", [ + setcookie( $cookie_name, '', [ 'expires' => strtotime( "-1 year" ), 'path' => '/', 'domain' => $domain, @@ -122,11 +154,41 @@ class Users { $domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); $cookie_name = str_replace( '.', '-', $domain ); + $remember_token = $_COOKIE[$cookie_name] ?? ''; + + $clear_remember_cookie = function() use ( $cookie_name, $domain ) + { + setcookie( $cookie_name, '', [ + 'expires' => strtotime( '-1 year' ), + 'path' => '/', + 'domain' => $domain, + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Lax' + ] ); + }; + $cleanup_remember_tokens = function() use ( $mdb ) + { + $mdb -> query( 'DELETE FROM `users_remember_tokens` WHERE COALESCE(`last_used_at`, `created_at`) < DATE_SUB(NOW(), INTERVAL 6 MONTH)' ); + }; + $cleanup_remember_tokens(); + + if ( is_string( $remember_token ) && strlen( $remember_token ) === 64 && ctype_xdigit( $remember_token ) ) + { + $mdb -> delete( 'users_remember_tokens', [ 'token_hash' => hash( 'sha256', $remember_token ) ] ); + } if ( \S::get( 'remember' ) === 'true' ) { $token = bin2hex( random_bytes( 32 ) ); - $mdb -> update( 'users', [ 'remember_token' => $token ], [ 'id' => $user['id'] ] ); + $mdb -> insert( 'users_remember_tokens', [ + 'user_id' => (int)$user['id'], + 'token_hash' => hash( 'sha256', $token ), + 'created_at' => date( 'Y-m-d H:i:s' ), + 'last_used_at' => date( 'Y-m-d H:i:s' ), + 'user_agent' => substr( (string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255 ), + 'ip' => (string)($_SERVER['REMOTE_ADDR'] ?? '') + ] ); setcookie( $cookie_name, $token, [ 'expires' => strtotime( "+1 year" ), 'path' => '/', @@ -138,15 +200,7 @@ class Users } else { - $mdb -> update( 'users', [ 'remember_token' => null ], [ 'id' => $user['id'] ] ); - setcookie( $cookie_name, "", [ - 'expires' => strtotime( "-1 year" ), - 'path' => '/', - 'domain' => $domain, - 'secure' => true, - 'httponly' => true, - 'samesite' => 'Lax' - ] ); + $clear_remember_cookie(); } \S::set_session( 'user', $user ); diff --git a/autoload/factory/class.Tasks.php b/autoload/factory/class.Tasks.php index 356bcbf..748029e 100644 --- a/autoload/factory/class.Tasks.php +++ b/autoload/factory/class.Tasks.php @@ -75,36 +75,218 @@ class Tasks $data = []; if ( $users ) { - $sql = ' AND id IN (SELECT task_id FROM task_user WHERE user_id IN (' . implode( ',', $users ) . ')) '; + $sql = ' AND t.id IN (SELECT task_id FROM task_user WHERE user_id IN (' . implode( ',', $users ) . ')) '; } else { $sql = ''; } if ( $projects ) { - $sql .= ' AND project_id IN (' . implode( ',', $projects ) . ') '; + $sql .= ' AND t.project_id IN (' . implode( ',', $projects ) . ') '; } if ( $user_id != 1 ) { $sql_query = 'SELECT ' - . 't.id, t.name, t.date_start, t.date_end, t.status, t.client_id, parent_id, priority ' + . 't.id, t.name, t.date_start, t.date_end, t.status, t.client_id, t.parent_id, t.priority, pt.date_start AS parent_date_start, pt.date_end AS parent_date_end ' . 'FROM tasks AS t ' + . 'LEFT JOIN tasks AS pt ON t.parent_id = pt.id ' . 'LEFT JOIN task_user AS tu ON t.id = tu.task_id ' - . 'WHERE tu.user_id = ' . $user_id . ' AND show_in_calendar = 1 AND status != 2 AND status != 3 AND status != 1 AND t.date_start <= DATE_ADD(NOW(), INTERVAL 1 MONTH) ' . $sql . ' ORDER BY priority DESC, date_start ASC, date_end ASC, o ASC'; + . 'WHERE tu.user_id = ' . $user_id . ' AND t.status != 2 AND t.status != 3 AND t.status != 1 AND ((t.show_in_calendar = 1 AND t.date_start <= DATE_ADD(NOW(), INTERVAL 1 MONTH)) OR (t.parent_id IS NOT NULL AND COALESCE(t.date_start, pt.date_start) IS NOT NULL AND COALESCE(t.date_start, pt.date_start) <= DATE_ADD(NOW(), INTERVAL 1 MONTH))) ' . $sql . ' ORDER BY t.priority DESC, COALESCE(t.date_start, pt.date_start) ASC, COALESCE(t.date_end, pt.date_end) ASC, t.o ASC'; } else { $sql_query = 'SELECT ' - . 't.id, t.name, t.date_start, t.date_end, t.status, t.client_id, parent_id, priority ' + . 't.id, t.name, t.date_start, t.date_end, t.status, t.client_id, t.parent_id, t.priority, pt.date_start AS parent_date_start, pt.date_end AS parent_date_end ' . 'FROM tasks AS t ' - . 'WHERE show_in_calendar = 1 AND status != 2 AND status != 3 AND status != 1 AND t.date_start <= DATE_ADD(NOW(), INTERVAL 1 MONTH) ' . $sql . ' ORDER BY priority DESC, date_start ASC, date_end ASC, o ASC'; + . 'LEFT JOIN tasks AS pt ON t.parent_id = pt.id ' + . 'WHERE t.status != 2 AND t.status != 3 AND t.status != 1 AND ((t.show_in_calendar = 1 AND t.date_start <= DATE_ADD(NOW(), INTERVAL 1 MONTH)) OR (t.parent_id IS NOT NULL AND COALESCE(t.date_start, pt.date_start) IS NOT NULL AND COALESCE(t.date_start, pt.date_start) <= DATE_ADD(NOW(), INTERVAL 1 MONTH))) ' . $sql . ' ORDER BY t.priority DESC, COALESCE(t.date_start, pt.date_start) ASC, COALESCE(t.date_end, pt.date_end) ASC, t.o ASC'; } $tasks = $mdb -> query( $sql_query ) -> fetchAll( \PDO::FETCH_ASSOC ); - foreach ( $tasks as $task ) { + + // If subtasks are present but their parent task is outside the base filter, + // force-load missing parents so dependency tree expand/collapse can work. + $tasks_by_id = []; + foreach ( $tasks as $tmp_task ) + $tasks_by_id[ (int)$tmp_task['id'] ] = true; + + $pending_parent_ids = []; + foreach ( $tasks as $tmp_task ) + { + $parent_id = (int)$tmp_task['parent_id']; + if ( $parent_id > 0 and !isset( $tasks_by_id[ $parent_id ] ) ) + $pending_parent_ids[ $parent_id ] = $parent_id; + } + + while ( !empty( $pending_parent_ids ) ) + { + $parent_ids = array_map( 'intval', array_values( $pending_parent_ids ) ); + $pending_parent_ids = []; + + $parents_query = 'SELECT ' + . 't.id, t.name, t.date_start, t.date_end, t.status, t.client_id, t.parent_id, t.priority, pt.date_start AS parent_date_start, pt.date_end AS parent_date_end ' + . 'FROM tasks AS t ' + . 'LEFT JOIN tasks AS pt ON t.parent_id = pt.id ' + . 'WHERE t.id IN (' . implode( ',', $parent_ids ) . ')'; + + $parent_rows = $mdb -> query( $parents_query ) -> fetchAll( \PDO::FETCH_ASSOC ); + foreach ( $parent_rows as $parent_row ) + { + $parent_row_id = (int)$parent_row['id']; + if ( isset( $tasks_by_id[ $parent_row_id ] ) ) + continue; + + $tasks[] = $parent_row; + $tasks_by_id[ $parent_row_id ] = true; + + $next_parent_id = (int)$parent_row['parent_id']; + if ( $next_parent_id > 0 and !isset( $tasks_by_id[ $next_parent_id ] ) ) + $pending_parent_ids[ $next_parent_id ] = $next_parent_id; + } + } + + // Build a stable hierarchical order: parent task followed by its subtasks. + $task_rows_by_id = []; + $task_order_index = []; + $task_children_map = []; + + foreach ( $tasks as $idx => $task_row ) + { + $task_row_id = (int)$task_row['id']; + if ( !$task_row_id ) + continue; + + if ( !isset( $task_rows_by_id[ $task_row_id ] ) ) + { + $task_rows_by_id[ $task_row_id ] = $task_row; + $task_order_index[ $task_row_id ] = $idx; + } + } + + foreach ( $task_rows_by_id as $task_row_id => $task_row ) + { + $task_parent_id = (int)$task_row['parent_id']; + if ( $task_parent_id > 0 ) + { + if ( !isset( $task_children_map[ $task_parent_id ] ) ) + $task_children_map[ $task_parent_id ] = []; + $task_children_map[ $task_parent_id ][] = $task_row_id; + } + } + + $root_ids = []; + foreach ( $task_rows_by_id as $task_row_id => $task_row ) + { + $task_parent_id = (int)$task_row['parent_id']; + if ( $task_parent_id <= 0 or !isset( $task_rows_by_id[ $task_parent_id ] ) ) + $root_ids[] = $task_row_id; + } + + // Effective priority for display: parent inherits the highest priority found + // in its subtree (without changing DB value). + $task_effective_priority = []; + $effective_priority_stack = []; + $compute_effective_priority = null; + $compute_effective_priority = function( $task_id ) use ( &$compute_effective_priority, &$task_effective_priority, &$effective_priority_stack, $task_rows_by_id, $task_children_map ) { + if ( isset( $task_effective_priority[ $task_id ] ) ) + return $task_effective_priority[ $task_id ]; + + if ( isset( $effective_priority_stack[ $task_id ] ) ) + return isset( $task_rows_by_id[ $task_id ]['priority'] ) ? (int)$task_rows_by_id[ $task_id ]['priority'] : 0; + + $effective_priority_stack[ $task_id ] = true; + + $max_priority = isset( $task_rows_by_id[ $task_id ]['priority'] ) ? (int)$task_rows_by_id[ $task_id ]['priority'] : 0; + if ( isset( $task_children_map[ $task_id ] ) ) + { + foreach ( $task_children_map[ $task_id ] as $child_id ) + { + $child_priority = (int)$compute_effective_priority( $child_id ); + if ( $child_priority > $max_priority ) + $max_priority = $child_priority; + } + } + + unset( $effective_priority_stack[ $task_id ] ); + $task_effective_priority[ $task_id ] = $max_priority; + return $max_priority; + }; + + foreach ( array_keys( $task_rows_by_id ) as $task_id ) + $compute_effective_priority( $task_id ); + + // Sort by effective priority first (DESC), then keep previous visual stability + // by original SQL order (ASC) for equal priorities. + $sort_by_effective_priority = function( $a, $b ) use ( $task_order_index, $task_effective_priority ) { + $a_priority = isset( $task_effective_priority[ $a ] ) ? (int)$task_effective_priority[ $a ] : 0; + $b_priority = isset( $task_effective_priority[ $b ] ) ? (int)$task_effective_priority[ $b ] : 0; + if ( $a_priority !== $b_priority ) + return $a_priority > $b_priority ? -1 : 1; + + $a_idx = isset( $task_order_index[ $a ] ) ? $task_order_index[ $a ] : 999999; + $b_idx = isset( $task_order_index[ $b ] ) ? $task_order_index[ $b ] : 999999; + if ( $a_idx == $b_idx ) + return 0; + return $a_idx < $b_idx ? -1 : 1; + }; + + usort( $root_ids, $sort_by_effective_priority ); + foreach ( $task_children_map as $parent_id => $child_ids ) + { + usort( $child_ids, $sort_by_effective_priority ); + $task_children_map[ $parent_id ] = $child_ids; + } + + $ordered_task_ids = []; + $ordered_visited = []; + $append_task_with_children = null; + $append_task_with_children = function( $task_id ) use ( &$append_task_with_children, &$ordered_task_ids, &$ordered_visited, $task_children_map ) { + if ( isset( $ordered_visited[ $task_id ] ) ) + return; + + $ordered_visited[ $task_id ] = true; + $ordered_task_ids[] = $task_id; + + if ( isset( $task_children_map[ $task_id ] ) ) + { + foreach ( $task_children_map[ $task_id ] as $child_id ) + $append_task_with_children( $child_id ); + } + }; + + foreach ( $root_ids as $root_id ) + $append_task_with_children( $root_id ); + + foreach ( array_keys( $task_rows_by_id ) as $task_id ) + $append_task_with_children( $task_id ); + + foreach ( $ordered_task_ids as $ordered_task_id ) { + $task = $task_rows_by_id[ $ordered_task_id ]; + $effective_start = $task['date_start'] ? $task['date_start'] : $task['parent_date_start']; + $effective_end = $task['date_end'] ? $task['date_end'] : $task['parent_date_end']; + + if ( !$effective_start and $effective_end ) + $effective_start = $effective_end; + if ( !$effective_end and $effective_start ) + $effective_end = $effective_start; + if ( !$effective_start or !$effective_end ) + continue; + + $task_depth = 0; + $depth_parent_id = (int)$task['parent_id']; + while ( $depth_parent_id > 0 and isset( $task_rows_by_id[ $depth_parent_id ] ) and $task_depth < 10 ) + { + $task_depth++; + $depth_parent_id = (int)$task_rows_by_id[ $depth_parent_id ]['parent_id']; + } + $task_json = []; - $task_json['name'] = $task['client_id'] ? \factory\Crm::get_client_name( (int)$task['client_id'] ) . ' - ' . htmlspecialchars( $task['name'] ) : htmlspecialchars( $task['name'] ); + $task_name = $task['client_id'] ? \factory\Crm::get_client_name( (int)$task['client_id'] ) . ' - ' . htmlspecialchars( $task['name'] ) : htmlspecialchars( $task['name'] ); + if ( $task_depth > 0 ) + $task_name = str_repeat( ' ', $task_depth ) . '-> ' . $task_name; + + $task_json['name'] = $task_name; // start - $task_json['start'] = $task['date_start']; + $task_json['start'] = $effective_start; // end - $task_json['end'] = $task['date_end']; + $task_json['end'] = $effective_end; // id $task_json['id'] = $task['id']; // custom class @@ -119,7 +301,10 @@ class Tasks $task_json['custom_class'] = 'gantt-task-faktura'; if ( !$task_json['custom_class'] ) - $task_json['custom_class'] = 'gantt-task-priority-' . $task['priority']; + { + $display_priority = isset( $task_effective_priority[ (int)$task['id'] ] ) ? (int)$task_effective_priority[ (int)$task['id'] ] : (int)$task['priority']; + $task_json['custom_class'] = 'gantt-task-priority-' . $display_priority; + } // progress $task_json['progress'] = 0; @@ -503,6 +688,7 @@ class Tasks else { $mdb -> update( 'tasks', [ + 'parent_id' => $parent_id, 'name' => $name, 'text' => htmlspecialchars( $text ), 'date_start' => $date_start != '' ? $date_start : null, diff --git a/index.php b/index.php index 8aebf57..941991e 100644 --- a/index.php +++ b/index.php @@ -54,8 +54,23 @@ $mdb = new medoo([ $domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); $cookie_name = str_replace( '.', '-', $domain ); $settings = array_merge( $settings, \factory\Crm::settings()); +$clear_remember_cookie = function() use ( $cookie_name, $domain ) +{ + setcookie( $cookie_name, '', [ + 'expires' => strtotime( '-1 year' ), + 'path' => '/', + 'domain' => $domain, + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Lax' + ] ); +}; +$cleanup_remember_tokens = function() use ( $mdb ) +{ + $mdb -> query( 'DELETE FROM `users_remember_tokens` WHERE COALESCE(`last_used_at`, `created_at`) < DATE_SUB(NOW(), INTERVAL 6 MONTH)' ); +}; -if ( empty( $_SESSION['_db_migrated_v3'] ) ) +if ( empty( $_SESSION['_db_migrated_v5'] ) ) { $col = $mdb -> query( "SHOW COLUMNS FROM `users` LIKE 'remember_token'" ) -> fetch(); if ( !$col ) @@ -69,6 +84,10 @@ if ( empty( $_SESSION['_db_migrated_v3'] ) ) `user_id` INT UNSIGNED NOT NULL PRIMARY KEY, `tasks` TINYINT(1) NOT NULL DEFAULT 1, `projects` TINYINT(1) NOT NULL DEFAULT 1, + `projects_view` TINYINT(1) NOT NULL DEFAULT 1, + `projects_add` TINYINT(1) NOT NULL DEFAULT 1, + `projects_edit` TINYINT(1) NOT NULL DEFAULT 1, + `projects_delete` TINYINT(1) NOT NULL DEFAULT 1, `finances` TINYINT(1) NOT NULL DEFAULT 0, `wiki` TINYINT(1) NOT NULL DEFAULT 1, `crm` TINYINT(1) NOT NULL DEFAULT 0, @@ -81,32 +100,77 @@ if ( empty( $_SESSION['_db_migrated_v3'] ) ) $col_z = $mdb -> query( "SHOW COLUMNS FROM `users_permissions` LIKE 'zaplecze'" ) -> fetch(); if ( $col_z ) $mdb -> pdo -> exec( "ALTER TABLE `users_permissions` DROP COLUMN `zaplecze`" ); + + $project_permission_columns = [ 'projects_view', 'projects_add', 'projects_edit', 'projects_delete' ]; + foreach ( $project_permission_columns as $permission_column ) + { + $col_perm = $mdb -> query( "SHOW COLUMNS FROM `users_permissions` LIKE '" . $permission_column . "'" ) -> fetch(); + if ( !$col_perm ) + { + $mdb -> pdo -> exec( "ALTER TABLE `users_permissions` ADD COLUMN `" . $permission_column . "` TINYINT(1) NOT NULL DEFAULT 1" ); + $mdb -> pdo -> exec( "UPDATE `users_permissions` SET `" . $permission_column . "` = `projects`" ); + } + } } - $_SESSION['_db_migrated_v3'] = true; + $tbl_tokens = $mdb -> query( "SHOW TABLES LIKE 'users_remember_tokens'" ) -> fetch(); + if ( !$tbl_tokens ) + { + $mdb -> pdo -> exec( " + CREATE TABLE `users_remember_tokens` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + `user_id` INT UNSIGNED NOT NULL, + `token_hash` CHAR(64) NOT NULL, + `created_at` DATETIME NOT NULL, + `last_used_at` DATETIME NULL, + `user_agent` VARCHAR(255) NULL, + `ip` VARCHAR(45) NULL, + UNIQUE KEY `uniq_token_hash` (`token_hash`), + KEY `idx_user_id` (`user_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + " ); + } + + $_SESSION['_db_migrated_v5'] = true; } if ( isset( $_COOKIE[$cookie_name] ) && !isset( $_SESSION['user'] ) ) { + $cleanup_remember_tokens(); $remember_token = $_COOKIE[$cookie_name]; if ( is_string( $remember_token ) && strlen( $remember_token ) === 64 && ctype_xdigit( $remember_token ) ) { - $user_tmp = $mdb -> get( 'users', '*', [ 'remember_token' => $remember_token ] ); - if ( $user_tmp ) - \S::set_session( 'user', $user_tmp ); + $token_hash = hash( 'sha256', $remember_token ); + $token_row = $mdb -> get( 'users_remember_tokens', '*', [ 'token_hash' => $token_hash ] ); + + if ( $token_row ) + { + $user_tmp = $mdb -> get( 'users', '*', [ 'id' => $token_row['user_id'] ] ); + if ( $user_tmp ) + { + \S::set_session( 'user', $user_tmp ); + $mdb -> update( 'users_remember_tokens', [ + 'last_used_at' => date( 'Y-m-d H:i:s' ), + 'user_agent' => substr( (string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255 ), + 'ip' => (string)($_SERVER['REMOTE_ADDR'] ?? '') + ], [ 'id' => (int)$token_row['id'] ] ); + } + else + { + $mdb -> delete( 'users_remember_tokens', [ 'id' => (int)$token_row['id'] ] ); + $clear_remember_cookie(); + } + } + else + { + $clear_remember_cookie(); + } } else { - // stare cookie w nieaktualnym formacie — usunięcie - setcookie( $cookie_name, "", [ - 'expires' => strtotime( "-1 year" ), - 'path' => '/', - 'domain' => $domain, - 'secure' => true, - 'httponly' => true, - 'samesite' => 'Lax' - ] ); + // stale cookie w nieaktualnym formacie - usuniecie + $clear_remember_cookie(); } } diff --git a/libraries/Simple-Gant-master/frappe-gantt.js b/libraries/Simple-Gant-master/frappe-gantt.js index 1789ef6..7f68517 100644 --- a/libraries/Simple-Gant-master/frappe-gantt.js +++ b/libraries/Simple-Gant-master/frappe-gantt.js @@ -1426,8 +1426,8 @@ class Gantt { } } - if(s) - task.end=s.end; + if(s && this.options.sync_parent_end_with_children) + task.end=s.end; task.has=this.get_all_dependent_tasks(task.id); return task; @@ -1666,7 +1666,8 @@ class Gantt { date_format: 'YYYY-MM-DD', popup_trigger: 'click', custom_popup_html: null, - language: 'en' + language: 'en', + sync_parent_end_with_children: true }; this.options = Object.assign({}, default_options, options); } @@ -2747,3 +2748,5 @@ function generate_id(task) { return Gantt; }()); + + diff --git a/templates/tasks/main_view.php b/templates/tasks/main_view.php index bb5645e..0fb0405 100644 --- a/templates/tasks/main_view.php +++ b/templates/tasks/main_view.php @@ -16,10 +16,17 @@