This commit is contained in:
2026-02-28 14:48:24 +01:00
parent 20f502543a
commit f6ba7ebc36
15 changed files with 1030 additions and 129 deletions

View File

@@ -127,9 +127,9 @@
}, },
"class.Users.php": { "class.Users.php": {
"type": "-", "type": "-",
"size": 4291, "size": 4974,
"lmtime": 1770653518273, "lmtime": 1772141683315,
"modified": true "modified": false
}, },
"class.Wiki.php": { "class.Wiki.php": {
"type": "-", "type": "-",
@@ -319,8 +319,8 @@
}, },
"index.php": { "index.php": {
"type": "-", "type": "-",
"size": 3592, "size": 3935,
"lmtime": 1772131174733, "lmtime": 1772141695415,
"modified": false "modified": false
}, },
"layout": { "layout": {
@@ -597,8 +597,8 @@
"users": { "users": {
"login-form.php": { "login-form.php": {
"type": "-", "type": "-",
"size": 3336, "size": 3355,
"lmtime": 0, "lmtime": 1772141702952,
"modified": false "modified": false
}, },
"main-view.php": { "main-view.php": {

View File

@@ -373,8 +373,119 @@ class UsersController
exit; 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 = [] ) public static function buildMainViewModel( $current_user, $impersonator_user, array $users, array $permissions_map = [] )
{ {
$defs = self::permissionDefinitions();
return [ return [
'current_user' => $current_user, 'current_user' => $current_user,
'impersonator_user' => $impersonator_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, '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, 'permissions_map' => $permissions_map,
'modules' => \Domain\Users\PermissionRepository::MODULES, 'modules' => \Domain\Users\PermissionRepository::MODULES,
'module_labels' => [ 'module_labels' => $defs['module_labels'],
'tasks' => 'Zadania', 'permission_groups' => $defs['permission_groups']
'projects' => 'Projekty',
'work_time' => 'Czas pracy',
'finances' => 'Finanse',
'crm' => 'CRM',
'wiki' => 'Wiki'
]
]; ];
} }
@@ -423,4 +528,26 @@ class UsersController
header( 'Location: /' ); header( 'Location: /' );
exit; 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' ]
]
];
}
} }

View File

@@ -3,11 +3,14 @@ namespace Domain\Users;
class PermissionRepository 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 = [ const DEFAULTS = [
'tasks' => 1, 'tasks' => 1,
'projects' => 1, 'projects_view' => 1,
'projects_add' => 1,
'projects_edit' => 1,
'projects_delete' => 1,
'finances' => 0, 'finances' => 0,
'wiki' => 1, 'wiki' => 1,
'crm' => 0, 'crm' => 0,
@@ -42,8 +45,16 @@ class PermissionRepository
return self::defaults(); return self::defaults();
$result = []; $result = [];
$legacy_projects_value = isset( $row['projects'] ) ? (int)$row['projects'] : 1;
foreach ( self::MODULES as $module ) 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; return $result;
} }

View File

@@ -14,7 +14,7 @@ class Site
$module = \S::get( 'module' ); $module = \S::get( 'module' );
$action = \S::get( 'action' ); $action = \S::get( 'action' );
if ( !\controls\Users::permissions( $user['id'], $module ) ) if ( !\controls\Users::permissions( $user['id'], $module, $action ) )
return; return;
// New Controllers namespace (camelCase methods) // New Controllers namespace (camelCase methods)

View File

@@ -353,6 +353,17 @@ class Tasks
exit; 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() { static public function task_change_users() {
global $mdb, $user; global $mdb, $user;
@@ -477,12 +488,15 @@ class Tasks
$values['status_change_mail'] = isset( $values['status_change_mail'] ) ? $values['status_change_mail'] : 'off'; $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['send_email_notification'] = isset( $values['send_email_notification'] ) ? $values['send_email_notification'] : 'off';
$values['users'] = isset( $values['users'] ) ? $values['users'] : []; $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; $values['recursive_last_date'] = isset( $values['recursive_last_date'] ) ? $values['recursive_last_date'] : null;
$status = \Controllers\TasksController::resolveTaskStatusForSave( $values ); $status = \Controllers\TasksController::resolveTaskStatusForSave( $values );
if ( $id = \factory\Tasks::task_save( 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 ); \factory\Tasks::clear_task_opened( $id );

View File

@@ -19,6 +19,35 @@ class Users
$cache[ $user_id ] = $repo -> byUserId( (int)$user_id ); $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 ] ) ) if ( $module && isset( $cache[ $user_id ][ $module ] ) )
return (bool)$cache[ $user_id ][ $module ]; return (bool)$cache[ $user_id ][ $module ];
@@ -28,15 +57,18 @@ class Users
public static function logout() public static function logout()
{ {
global $mdb, $user; global $mdb;
$domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); $domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] );
$cookie_name = str_replace( '.', '-', $domain ); $cookie_name = str_replace( '.', '-', $domain );
$remember_token = $_COOKIE[$cookie_name] ?? '';
if ( $user && isset( $user['id'] ) ) if ( is_string( $remember_token ) && strlen( $remember_token ) === 64 && ctype_xdigit( $remember_token ) )
$mdb -> update( 'users', [ 'remember_token' => null ], [ 'id' => $user['id'] ] ); {
$mdb -> delete( 'users_remember_tokens', [ 'token_hash' => hash( 'sha256', $remember_token ) ] );
}
setcookie( $cookie_name, "", [ setcookie( $cookie_name, '', [
'expires' => strtotime( "-1 year" ), 'expires' => strtotime( "-1 year" ),
'path' => '/', 'path' => '/',
'domain' => $domain, 'domain' => $domain,
@@ -122,11 +154,41 @@ class Users
{ {
$domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); $domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] );
$cookie_name = str_replace( '.', '-', $domain ); $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' ) if ( \S::get( 'remember' ) === 'true' )
{ {
$token = bin2hex( random_bytes( 32 ) ); $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, [ setcookie( $cookie_name, $token, [
'expires' => strtotime( "+1 year" ), 'expires' => strtotime( "+1 year" ),
'path' => '/', 'path' => '/',
@@ -138,15 +200,7 @@ class Users
} }
else else
{ {
$mdb -> update( 'users', [ 'remember_token' => null ], [ 'id' => $user['id'] ] ); $clear_remember_cookie();
setcookie( $cookie_name, "", [
'expires' => strtotime( "-1 year" ),
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
] );
} }
\S::set_session( 'user', $user ); \S::set_session( 'user', $user );

View File

@@ -75,36 +75,218 @@ class Tasks
$data = []; $data = [];
if ( $users ) { 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 { } else {
$sql = ''; $sql = '';
} }
if ( $projects ) { if ( $projects ) {
$sql .= ' AND project_id IN (' . implode( ',', $projects ) . ') '; $sql .= ' AND t.project_id IN (' . implode( ',', $projects ) . ') ';
} }
if ( $user_id != 1 ) { if ( $user_id != 1 ) {
$sql_query = 'SELECT ' $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 ' . '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 ' . '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 { } else {
$sql_query = 'SELECT ' $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 ' . '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 ); $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 = [];
$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 // start
$task_json['start'] = $task['date_start']; $task_json['start'] = $effective_start;
// end // end
$task_json['end'] = $task['date_end']; $task_json['end'] = $effective_end;
// id // id
$task_json['id'] = $task['id']; $task_json['id'] = $task['id'];
// custom class // custom class
@@ -119,7 +301,10 @@ class Tasks
$task_json['custom_class'] = 'gantt-task-faktura'; $task_json['custom_class'] = 'gantt-task-faktura';
if ( !$task_json['custom_class'] ) 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 // progress
$task_json['progress'] = 0; $task_json['progress'] = 0;
@@ -503,6 +688,7 @@ class Tasks
else else
{ {
$mdb -> update( 'tasks', [ $mdb -> update( 'tasks', [
'parent_id' => $parent_id,
'name' => $name, 'name' => $name,
'text' => htmlspecialchars( $text ), 'text' => htmlspecialchars( $text ),
'date_start' => $date_start != '' ? $date_start : null, 'date_start' => $date_start != '' ? $date_start : null,

View File

@@ -54,8 +54,23 @@ $mdb = new medoo([
$domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); $domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] );
$cookie_name = str_replace( '.', '-', $domain ); $cookie_name = str_replace( '.', '-', $domain );
$settings = array_merge( $settings, \factory\Crm::settings()); $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(); $col = $mdb -> query( "SHOW COLUMNS FROM `users` LIKE 'remember_token'" ) -> fetch();
if ( !$col ) if ( !$col )
@@ -69,6 +84,10 @@ if ( empty( $_SESSION['_db_migrated_v3'] ) )
`user_id` INT UNSIGNED NOT NULL PRIMARY KEY, `user_id` INT UNSIGNED NOT NULL PRIMARY KEY,
`tasks` TINYINT(1) NOT NULL DEFAULT 1, `tasks` TINYINT(1) NOT NULL DEFAULT 1,
`projects` 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, `finances` TINYINT(1) NOT NULL DEFAULT 0,
`wiki` TINYINT(1) NOT NULL DEFAULT 1, `wiki` TINYINT(1) NOT NULL DEFAULT 1,
`crm` TINYINT(1) NOT NULL DEFAULT 0, `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(); $col_z = $mdb -> query( "SHOW COLUMNS FROM `users_permissions` LIKE 'zaplecze'" ) -> fetch();
if ( $col_z ) if ( $col_z )
$mdb -> pdo -> exec( "ALTER TABLE `users_permissions` DROP COLUMN `zaplecze`" ); $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'] ) ) if ( isset( $_COOKIE[$cookie_name] ) && !isset( $_SESSION['user'] ) )
{ {
$cleanup_remember_tokens();
$remember_token = $_COOKIE[$cookie_name]; $remember_token = $_COOKIE[$cookie_name];
if ( is_string( $remember_token ) && strlen( $remember_token ) === 64 && ctype_xdigit( $remember_token ) ) if ( is_string( $remember_token ) && strlen( $remember_token ) === 64 && ctype_xdigit( $remember_token ) )
{ {
$user_tmp = $mdb -> get( 'users', '*', [ 'remember_token' => $remember_token ] ); $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 ) if ( $user_tmp )
{
\S::set_session( 'user', $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 else
{ {
// stare cookie w nieaktualnym formacie — usunięcie $mdb -> delete( 'users_remember_tokens', [ 'id' => (int)$token_row['id'] ] );
setcookie( $cookie_name, "", [ $clear_remember_cookie();
'expires' => strtotime( "-1 year" ), }
'path' => '/', }
'domain' => $domain, else
'secure' => true, {
'httponly' => true, $clear_remember_cookie();
'samesite' => 'Lax' }
] ); }
else
{
// stale cookie w nieaktualnym formacie - usuniecie
$clear_remember_cookie();
} }
} }

View File

@@ -1426,7 +1426,7 @@ class Gantt {
} }
} }
if(s) if(s && this.options.sync_parent_end_with_children)
task.end=s.end; task.end=s.end;
task.has=this.get_all_dependent_tasks(task.id); task.has=this.get_all_dependent_tasks(task.id);
@@ -1666,7 +1666,8 @@ class Gantt {
date_format: 'YYYY-MM-DD', date_format: 'YYYY-MM-DD',
popup_trigger: 'click', popup_trigger: 'click',
custom_popup_html: null, custom_popup_html: null,
language: 'en' language: 'en',
sync_parent_end_with_children: true
}; };
this.options = Object.assign({}, default_options, options); this.options = Object.assign({}, default_options, options);
} }
@@ -2747,3 +2748,5 @@ function generate_id(task) {
return Gantt; return Gantt;
}()); }());

View File

@@ -16,10 +16,17 @@
<h4>Projekty</h4> <h4>Projekty</h4>
<? foreach ( $this -> projects as $project ):?> <? foreach ( $this -> projects as $project ):?>
<div class="_project"> <div class="_project">
<div class="project_row">
<label for="project_<?= $project[ 'id' ];?>"> <label for="project_<?= $project[ 'id' ];?>">
<input type="checkbox" class="g-checkbox" name="projects" value="<?= $project[ 'id' ];?>" <? if ( is_array( $this -> selected_projects ) and in_array( $project['id'], $this -> selected_projects ) ):?>checked<? endif;?>> <input type="checkbox" class="g-checkbox" name="projects" value="<?= $project[ 'id' ];?>" <? if ( is_array( $this -> selected_projects ) and in_array( $project['id'], $this -> selected_projects ) ):?>checked<? endif;?>>
<?= $project[ 'name' ];?> (<?= $project['total_tasks'];?>) <?= $project[ 'name' ];?> <span class="project_count">(<?= (int)$project['total_tasks'];?>)</span>
</label> </label>
<? if ( \controls\Users::permissions( $this -> user['id'], 'projects', 'project_delete' ) ):?>
<a href="#" class="project_delete_inline" project_id="<?= (int)$project['id'];?>" project_name="<?= htmlspecialchars( $project['name'] );?>" title="Usuń projekt">
<i class="fa fa-trash"></i>
</a>
<? endif;?>
</div>
</div> </div>
<? endforeach;?> <? endforeach;?>
</div> </div>
@@ -127,6 +134,69 @@
<div class="task_popup"> <div class="task_popup">
</div> </div>
<style type="text/css">
.tasks_main_view ._left_column {
width: fit-content;
min-width: 350px;
max-width: 520px;
}
.tasks_main_view ._right_column {
flex: 1;
max-width: none;
min-width: 0;
}
.tasks_main_view ._left_column ._projects ._project .project_row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.tasks_main_view ._left_column ._projects ._project .project_row label {
margin: 0;
flex: 1;
white-space: nowrap;
}
.tasks_main_view ._left_column ._projects ._project .project_row .project_count {
display: inline-block;
margin-left: 3px;
padding: 0 6px;
border-radius: 10px;
background: #1f3d72;
color: #fff;
font-weight: 700;
font-size: 11px;
line-height: 18px;
vertical-align: middle;
}
.tasks_main_view ._left_column ._projects ._project .project_delete_inline {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 4px;
border: 1px solid #cc563d;
background: #cc563d;
color: #fff;
text-decoration: none;
transition: all .2s ease;
margin: 1px 0;
}
.tasks_main_view ._left_column ._projects ._project .project_delete_inline:hover {
background: #b74831;
border-color: #b74831;
}
.tasks_main_view ._left_column ._projects ._project .project_delete_inline i {
font-size: 10px;
}
</style>
<script type="text/javascript"> <script type="text/javascript">
let isProgrammaticUpdate = false; let isProgrammaticUpdate = false;
@@ -172,7 +242,49 @@
function normalizeGanttTasks( tasksData ) function normalizeGanttTasks( tasksData )
{ {
if ( Array.isArray( tasksData ) && tasksData.length > 0 ) if ( Array.isArray( tasksData ) && tasksData.length > 0 )
return tasksData; {
var longTaskThresholdDays = 21;
var clipStartMoment = null;
tasksData.forEach( function( task ) {
var startMoment = moment( task.start, 'YYYY-MM-DD', true );
var endMoment = moment( task.end, 'YYYY-MM-DD', true );
if ( !startMoment.isValid() || !endMoment.isValid() )
return;
var durationDays = endMoment.diff( startMoment, 'days' );
if ( durationDays < longTaskThresholdDays )
{
if ( !clipStartMoment || startMoment.isBefore( clipStartMoment ) )
clipStartMoment = startMoment.clone();
}
} );
if ( !clipStartMoment )
clipStartMoment = moment().startOf( 'day' );
return tasksData.map( function( task ) {
var normalizedTask = Object.assign( {}, task );
var startMoment = moment( normalizedTask.start, 'YYYY-MM-DD', true );
var endMoment = moment( normalizedTask.end, 'YYYY-MM-DD', true );
if ( !startMoment.isValid() || !endMoment.isValid() )
return normalizedTask;
var durationDays = endMoment.diff( startMoment, 'days' );
if ( durationDays >= longTaskThresholdDays && startMoment.isBefore( clipStartMoment ) && endMoment.isSameOrAfter( clipStartMoment ) )
{
normalizedTask.start = clipStartMoment.format( 'YYYY-MM-DD' );
if ( typeof normalizedTask.name === 'string' && normalizedTask.name.indexOf( '... ' ) !== 0 )
normalizedTask.name = '... ' + normalizedTask.name;
}
return normalizedTask;
} );
}
return getEmptyGanttTasks(); return getEmptyGanttTasks();
} }
@@ -232,7 +344,8 @@
console.log(mode); console.log(mode);
}, },
view_mode: 'Half Day', view_mode: 'Half Day',
language: 'en' language: 'en',
sync_parent_end_with_children: false
}); });
console.log(gantt_chart); console.log(gantt_chart);
@@ -449,6 +562,41 @@
return false; return false;
}); });
$( 'body' ).on( 'click', '.project_delete_inline', function(e) {
e.preventDefault();
var project_id = $( this ).attr( 'project_id' );
var project_name = $( this ).attr( 'project_name' );
$.confirm({
title: 'Potwierdź',
content: 'Na pewno chcesz usunąć projekt <b>' + project_name + '</b>?',
type: 'orange',
closeIcon: true,
closeIconClass: 'fa fa-close',
typeAnimated: true,
animation: 'opacity',
boxWidth: '500px',
useBootstrap: false,
theme: 'material',
buttons: {
cancel: {
text: 'Anuluj',
btnClass: 'btn-default',
action: function() {}
},
confirm: {
text: 'Usuń projekt',
btnClass: 'btn-red',
keys: [ 'enter' ],
action: function() {
document.location.href = '/projects/project_delete/project_id=' + project_id;
}
}
}
});
});
$( 'body' ).on( 'click', '.current_status', function() { $( 'body' ).on( 'click', '.current_status', function() {
$( this ).find( '.status_change' ).toggle(); $( this ).find( '.status_change' ).toggle();
}); });

View File

@@ -126,7 +126,7 @@ ob_start();
'label' => 'Priorytet', 'label' => 'Priorytet',
'name' => 'priority', 'name' => 'priority',
'id' => 'priority', 'id' => 'priority',
'value' => $this -> task[ 'priority' ], 'value' => $this -> task['id'] ? $this -> task['priority'] : 1,
'values' => $this -> priorities 'values' => $this -> priorities
] ); ] );
?> ?>

View File

@@ -11,13 +11,14 @@
<? endif;?> <? endif;?>
<span class="task-title-wrapper"> <span class="task-title-wrapper">
<span class="task-id">#<?= $this -> task['id'];?></span> <span class="task-id">#<?= $this -> task['id'];?></span>
<span class="task-title-view">
<span class="task-title-text"><?= $this -> task['name'];?></span> <span class="task-title-text"><?= $this -> task['name'];?></span>
<a href="#" class="task-title-edit-btn" title="Edytuj tytuł"><i class="fa fa-pencil-square-o"></i></a> <a href="#" class="task-title-edit-btn" title="Edytuj tytuł"><i class="fa fa-pencil"></i></a>
</span>
<span class="task-title-edit-box" style="display: none;"> <span class="task-title-edit-box" style="display: none;">
<input type="text" class="task-title-input" value="<?= htmlspecialchars($this -> task['name']);?>"> <input type="text" class="task-title-input form-control" value="<?= htmlspecialchars($this -> task['name']);?>">
<a href="#" class="task-title-save"><i class="fa fa-check"></i></a> <a href="#" class="task-title-save" title="Zapisz"><i class="fa fa-check"></i></a>
<a href="#" class="task-title-cancel"><i class="fa fa-times"></i></a> <a href="#" class="task-title-cancel" title="Anuluj"><i class="fa fa-times"></i></a>
</span> </span>
</span> </span>
</div> </div>
@@ -87,6 +88,11 @@
<? endforeach; endif;?> <? endforeach; endif;?>
</div> </div>
<div class="task-tab-panel is-active" data-tab="description"> <div class="task-tab-panel is-active" data-tab="description">
<div class="task-description-editor box">
<h3>Treść zadania</h3>
<textarea class="form-control task-description-input" rows="5"><?= htmlspecialchars( (string)$this -> task['text'] );?></textarea>
<a href="#" class="btn btn-primary btn-sm task-popup-compact-btn js-save-task-description" task_id="<?= (int)$this -> task['id'];?>" style="margin-top: 10px;">Zapisz treść</a>
</div>
<? if ( $this -> task['text'] ):?> <? if ( $this -> task['text'] ):?>
<div class="description"> <div class="description">
<a href="#" class="fullscreen"><i class="fa fa-expand"></i></a> <a href="#" class="fullscreen"><i class="fa fa-expand"></i></a>
@@ -219,12 +225,17 @@
</div> </div>
<div class="dates box"> <div class="dates box">
<h3>Termin</h3> <h3>Termin</h3>
<? if ( $this -> task['date_start'] ):?> <div class="task-date-edit-grid">
<div class="date_start"><i class="fa fa-regular fa-calendar"></i><?= $this -> task['date_start'];?></div> <div class="task-date-field">
<? endif;?> <label>Data rozpoczęcia</label>
<? if ( $this -> task['date_end'] ):?> <input type="date" class="form-control task-date-start-input" value="<?= htmlspecialchars( (string)$this -> task['date_start'] );?>">
<div class="date_end <? if ( $this -> task['status'] != 2 and $this -> task['date_end'] == date( 'Y-m-d' ) ):?> warning<? endif;?> <? if ( $this -> task['status'] != 2 and $this -> task['date_end'] < date( 'Y-m-d' ) ):?> dangerx<? endif;?>"><i class="fa fa-regular fa-calendar"></i><?= $this -> task['date_end'];?></div> </div>
<? endif;?> <div class="task-date-field">
<label>Data zakończenia</label>
<input type="date" class="form-control task-date-end-input" value="<?= htmlspecialchars( (string)$this -> task['date_end'] );?>">
</div>
</div>
<a href="#" class="btn btn-primary btn-sm task-popup-compact-btn js-save-task-dates" task_id="<?= (int)$this -> task['id'];?>" style="margin-top: 10px;">Zapisz terminy</a>
</div> </div>
<div class="client box"> <div class="client box">
<h3>Klient</h3> <h3>Klient</h3>
@@ -432,31 +443,95 @@
} }
.task_popup .task_details .title .task-title-wrapper { .task_popup .task_details .title .task-title-wrapper {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: nowrap;
min-width: 0;
flex: 1;
}
.task_popup .task_details .title .task-title-view {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
flex-wrap: wrap; min-width: 0;
flex: 1;
} }
.task_popup .task_details .title .task-title-edit-btn { .task_popup .task_details .title .task-title-text {
font-size: 14px; display: inline-block;
color: #999; max-width: 100%;
margin-left: 5px; white-space: nowrap;
opacity: 0.6; overflow: hidden;
transition: opacity 0.2s; text-overflow: ellipsis;
font-weight: 600;
} }
.task_popup .task_details .title .task-title-edit-btn:hover { .task_popup .task_details .title .task-title-edit-btn,
opacity: 1; .task_popup .task_details .title .task-title-save,
color: #333; .task_popup .task_details .title .task-title-cancel {
border: 1px solid #099885;
width: 30px;
height: 30px;
border-radius: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #099885;
text-decoration: none;
flex-shrink: 0;
transition: all 0.2s ease;
}
.task_popup .task_details .title .task-title-edit-btn:hover,
.task_popup .task_details .title .task-title-save:hover,
.task_popup .task_details .title .task-title-cancel:hover {
background: #099885;
color: #fff;
}
.task_popup .task_details .title .task-title-cancel {
border-color: #cc563d;
color: #cc563d;
}
.task_popup .task_details .title .task-title-cancel:hover {
background: #cc563d;
color: #fff;
} }
.task_popup .task_details .title .task-title-edit-box { .task_popup .task_details .title .task-title-edit-box {
align-items: center; align-items: center;
gap: 5px; gap: 8px;
flex: 1;
min-width: 0;
} }
.task_popup .task_details .title .task-title-input { .task_popup .task_details .title .task-title-input {
height: 36px;
border-radius: 6px;
font-size: 18px; font-size: 18px;
padding: 4px 8px; width: 100%;
width: 400px; min-width: 240px;
max-width: 100%; }
.task_popup .task_details .content .left .task-description-editor {
margin-bottom: 12px;
}
.task_popup .task_details .content .left .task-description-editor .task-description-input {
min-height: 120px;
resize: vertical;
}
.task_popup .task_details .content .right .dates .task-date-edit-grid {
display: grid;
gap: 8px;
grid-template-columns: 1fr;
}
.task_popup .task_details .content .right .dates .task-date-field label {
display: block;
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
color: #4e5e6a;
}
.task_popup .task_details .task-popup-compact-btn {
height: 30px;
padding: 0 10px;
font-size: 12px;
border-radius: 5px;
min-width: 0;
} }
</style> </style>
<script type="text/javascript"> <script type="text/javascript">
@@ -610,8 +685,7 @@
popup.on( 'click', '.task-title-edit-btn', function( e ) { popup.on( 'click', '.task-title-edit-btn', function( e ) {
e.preventDefault(); e.preventDefault();
popup.find( '.task-title-edit-btn' ).hide(); popup.find( '.task-title-view' ).hide();
popup.find( '.task-title-text' ).hide();
popup.find( '.task-title-edit-box' ).css( 'display', 'inline-flex' ); popup.find( '.task-title-edit-box' ).css( 'display', 'inline-flex' );
popup.find( '.task-title-input' ).focus(); popup.find( '.task-title-input' ).focus();
}); });
@@ -619,8 +693,7 @@
popup.on( 'click', '.task-title-cancel', function( e ) { popup.on( 'click', '.task-title-cancel', function( e ) {
e.preventDefault(); e.preventDefault();
popup.find( '.task-title-edit-box' ).hide(); popup.find( '.task-title-edit-box' ).hide();
popup.find( '.task-title-text' ).show(); popup.find( '.task-title-view' ).show();
popup.find( '.task-title-edit-btn' ).show();
// Reset input to current text // Reset input to current text
popup.find( '.task-title-input' ).val( popup.find( '.task-title-text' ).text() ); popup.find( '.task-title-input' ).val( popup.find( '.task-title-text' ).text() );
}); });
@@ -658,6 +731,70 @@
} }
}); });
}); });
popup.on( 'keypress', '.task-title-input', function( e ) {
if ( e.which === 13 )
{
e.preventDefault();
popup.find( '.task-title-save' ).trigger( 'click' );
}
});
popup.on( 'click', '.js-save-task-dates', function( e ) {
e.preventDefault();
var btn = $( this );
var task_id = btn.attr( 'task_id' );
var date_start = popup.find( '.task-date-start-input' ).val();
var date_end = popup.find( '.task-date-end-input' ).val();
btn.text( 'Zapisywanie...' ).addClass( 'disabled' );
$.ajax({
type: 'POST',
url: '/tasks/task_change_dates/',
data: { task_id: task_id, date_start: date_start, date_end: date_end },
success: function( response ) {
var res = typeof response === 'string' ? JSON.parse( response ) : response;
if ( res.status === 'success' )
{
btn.text( 'Zapisano!' );
if ( typeof getSelectedTaskFilters === 'function' && typeof reload_tasks === 'function' )
{
var selected_filters = getSelectedTaskFilters();
reload_tasks( selected_filters.projects, selected_filters.users );
}
}
else
btn.text( 'Błąd' );
setTimeout( function() { btn.text( 'Zapisz terminy' ).removeClass( 'disabled' ); }, 1400 );
}
} );
} );
popup.on( 'click', '.js-save-task-description', function( e ) {
e.preventDefault();
var btn = $( this );
var task_id = btn.attr( 'task_id' );
var text = popup.find( '.task-description-input' ).val();
btn.text( 'Zapisywanie...' ).addClass( 'disabled' );
$.ajax({
type: 'POST',
url: '/tasks/task_change_text/',
data: { task_id: task_id, text: text },
success: function( response ) {
var res = typeof response === 'string' ? JSON.parse( response ) : response;
if ( res.status === 'success' )
{
btn.text( 'Zapisano!' );
task_popup( task_id, is_task_popup_works_time_open() );
}
else
btn.text( 'Błąd' );
setTimeout( function() { btn.text( 'Zapisz treść' ).removeClass( 'disabled' ); }, 1400 );
}
} );
} );
var interval_id = setInterval( function() { var interval_id = setInterval( function() {
if ( !document.body.contains( popup.get( 0 ) ) ) if ( !document.body.contains( popup.get( 0 ) ) )
@@ -675,4 +812,3 @@
}, 1000 ); }, 1000 );
} )(); } )();
</script> </script>

View File

@@ -18,7 +18,7 @@
<th style="width: 60px;">ID</th> <th style="width: 60px;">ID</th>
<th>Imi&#281; i nazwisko</th> <th>Imi&#281; i nazwisko</th>
<th>Email</th> <th>Email</th>
<th>Uprawnienia</th> <th>Status uprawnien</th>
<th style="width: 240px;">Akcje</th> <th style="width: 240px;">Akcje</th>
</tr> </tr>
</thead> </thead>
@@ -38,20 +38,20 @@
<? if ( (int)$user_tmp['id'] === 1 ):?> <? if ( (int)$user_tmp['id'] === 1 ):?>
<span class="label label-info">Pelny dostep</span> <span class="label label-info">Pelny dostep</span>
<? elseif ( isset( $this -> permissions_map[ (int)$user_tmp['id'] ] ) ):?> <? elseif ( isset( $this -> permissions_map[ (int)$user_tmp['id'] ] ) ):?>
<? $enabled = 0;?>
<? foreach ( $this -> modules as $mod ):?> <? foreach ( $this -> modules as $mod ):?>
<label style="margin-right: 10px; font-weight: normal; white-space: nowrap;"> <? if ( !empty( $this -> permissions_map[ (int)$user_tmp['id'] ][ $mod ] ) ) $enabled++;?>
<input type="checkbox"
class="permission-checkbox"
data-user-id="<?= (int)$user_tmp['id'];?>"
data-module="<?= $mod;?>"
<?= $this -> permissions_map[ (int)$user_tmp['id'] ][ $mod ] ? 'checked' : '';?>
>
<?= htmlspecialchars( $this -> module_labels[ $mod ] );?>
</label>
<? endforeach;?> <? endforeach;?>
<span class="label label-default"><?= (int)$enabled;?> / <?= count( $this -> modules );?></span>
<? endif;?> <? endif;?>
</td> </td>
<td class="center"> <td class="center">
<? if ( (int)$user_tmp['id'] !== 1 ):?>
<a href="#" class="btn btn-info btn_small js-open-user-permissions" data-user-id="<?= (int)$user_tmp['id'];?>">
<i class="fa fa-key"></i>
Uprawnienia
</a>
<? endif;?>
<? if ( $is_current ):?> <? if ( $is_current ):?>
<span class="btn btn-default btn_small disabled">Aktywna sesja</span> <span class="btn btn-default btn_small disabled">Aktywna sesja</span>
<? else:?> <? else:?>
@@ -71,32 +71,146 @@
</table> </table>
</div> </div>
</div> </div>
<style>
.users-permissions-popup {
max-width: 760px;
}
.users-permissions-popup .header {
margin-bottom: 12px;
}
.users-permissions-popup .header h3 {
margin: 0 0 6px 0;
}
.users-permissions-popup .groups {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 14px;
}
.users-permissions-popup .group {
border: 1px solid #dfe4ea;
border-radius: 6px;
padding: 10px;
background: #f9fbfd;
}
.users-permissions-popup .group h4 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 600;
}
.users-permissions-popup .items {
display: grid;
gap: 6px;
}
.users-permissions-popup .item {
display: flex;
align-items: center;
gap: 6px;
font-weight: normal;
margin: 0;
}
.users-permissions-popup .actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.users-permissions-popup .admin-note {
background: #eef5ff;
border: 1px solid #cfdcf3;
border-radius: 6px;
padding: 10px;
margin-bottom: 12px;
}
@media (max-width: 980px) {
.users-permissions-popup .groups {
grid-template-columns: 1fr;
}
}
</style>
<script> <script>
$( document ).on( 'change', '.permission-checkbox', function() $( document ).on( 'click', '.js-open-user-permissions', function(e)
{ {
var $cb = $( this ); e.preventDefault();
var userId = $( this ).data( 'user-id' );
$.ajax({ $.ajax({
url: '/users/permission_save/', url: '/users/permission_popup/',
type: 'POST', type: 'POST',
data: { data: {
user_id: $cb.data( 'user-id' ), user_id: userId,
perm_module: $cb.data( 'module' ),
value: $cb.is( ':checked' ) ? 1 : 0,
csrf_token: '<?= \S::csrf_token();?>' csrf_token: '<?= \S::csrf_token();?>'
}, },
dataType: 'json', dataType: 'json',
success: function( r ) success: function( r )
{ {
if ( r.status !== 'success' ) if ( r.status === 'success' && r.popup_content )
{ {
alert( r.msg || 'Blad zapisu uprawnien.' ); show_default_popup( r.popup_content );
$cb.prop( 'checked', !$cb.is( ':checked' ) ); $( '.users-permissions-popup input.g-checkbox' ).iCheck({
checkboxClass: 'icheckbox_square-blue',
radioClass: 'iradio_square-blue'
});
}
else
{
alert( r.msg || 'Nie udalo sie otworzyc okna uprawnien.' );
}
},
error: function()
{
alert( 'Blad polaczenia z serwerem.' );
}
});
});
$( document ).on( 'click', '.js-users-permissions-cancel', function(e)
{
e.preventDefault();
$( '.default_popup .close' ).trigger( 'click' );
});
$( document ).on( 'click', '.js-users-permissions-save', function(e)
{
e.preventDefault();
var userId = $( this ).data( 'user-id' );
var selected = $( '.users-permissions-popup .permission-popup-checkbox:checked' ).map(function() {
return $( this ).data( 'module' );
}).get();
$.ajax({
url: '/users/permission_save_bulk/',
type: 'POST',
data: {
user_id: userId,
selected_modules: selected.join( ',' ),
csrf_token: '<?= \S::csrf_token();?>'
},
dataType: 'json',
success: function( r )
{
if ( r.status === 'success' )
{
$( '.default_popup .close' ).trigger( 'click' );
window.location.reload();
}
else
{
alert( r.msg || 'Blad zapisu uprawnien.' );
} }
}, },
error: function() error: function()
{ {
alert( 'Blad polaczenia z serwerem.' ); alert( 'Blad polaczenia z serwerem.' );
$cb.prop( 'checked', !$cb.is( ':checked' ) );
} }
}); });
}); });

View File

@@ -0,0 +1,36 @@
<div class="users-permissions-popup">
<div class="header">
<h3>Uprawnienia uzytkownika</h3>
<div class="user">
<?= htmlspecialchars( trim( $this -> target_user['name'] . ' ' . $this -> target_user['surname'] ) );?>
(<?= htmlspecialchars( $this -> target_user['email'] );?>)
</div>
</div>
<? if ( (int)$this -> target_user['id'] === 1 ):?>
<div class="admin-note">Administrator ma pelny dostep i jego uprawnienia nie sa edytowalne.</div>
<? else:?>
<div class="groups">
<? foreach ( $this -> permission_groups as $group_name => $group_modules ):?>
<div class="group">
<h4><?= htmlspecialchars( $group_name );?></h4>
<div class="items">
<? foreach ( $group_modules as $mod ):?>
<label class="item">
<input type="checkbox" class="g-checkbox permission-popup-checkbox" data-module="<?= htmlspecialchars( $mod );?>" <?= !empty( $this -> permissions[ $mod ] ) ? 'checked="checked"' : '';?>>
<span><?= htmlspecialchars( $this -> module_labels[ $mod ] );?></span>
</label>
<? endforeach;?>
</div>
</div>
<? endforeach;?>
</div>
<? endif;?>
<div class="actions">
<a href="#" class="btn btn-default js-users-permissions-cancel">Anuluj</a>
<? if ( (int)$this -> target_user['id'] !== 1 ):?>
<a href="#" class="btn btn-primary js-users-permissions-save" data-user-id="<?= (int)$this -> target_user['id'];?>">Zapisz</a>
<? endif;?>
</div>
</div>

View File

@@ -15,18 +15,26 @@ function run_permission_repository_tests()
// Test MODULES constant // Test MODULES constant
$modules = PermissionRepository::MODULES; $modules = PermissionRepository::MODULES;
assert_perm( true, in_array( 'tasks', $modules ), 'MODULES should contain tasks' ); assert_perm( true, in_array( 'tasks', $modules ), 'MODULES should contain tasks' );
assert_perm( true, in_array( 'projects_view', $modules ), 'MODULES should contain projects_view' );
assert_perm( true, in_array( 'projects_add', $modules ), 'MODULES should contain projects_add' );
assert_perm( true, in_array( 'projects_edit', $modules ), 'MODULES should contain projects_edit' );
assert_perm( true, in_array( 'projects_delete', $modules ), 'MODULES should contain projects_delete' );
assert_perm( true, in_array( 'finances', $modules ), 'MODULES should contain finances' ); assert_perm( true, in_array( 'finances', $modules ), 'MODULES should contain finances' );
assert_perm( 6, count( $modules ), 'MODULES should have 6 entries' ); assert_perm( 9, count( $modules ), 'MODULES should have 9 entries' );
assert_perm( false, in_array( 'zaplecze', $modules ), 'MODULES should not contain zaplecze' ); assert_perm( false, in_array( 'zaplecze', $modules ), 'MODULES should not contain zaplecze' );
// Test DEFAULTS constant // Test DEFAULTS constant
$defaults = PermissionRepository::DEFAULTS; $defaults = PermissionRepository::DEFAULTS;
assert_perm( 1, $defaults['tasks'], 'tasks should default to 1' ); assert_perm( 1, $defaults['tasks'], 'tasks should default to 1' );
assert_perm( 1, $defaults['projects_view'], 'projects_view should default to 1' );
assert_perm( 1, $defaults['projects_add'], 'projects_add should default to 1' );
assert_perm( 1, $defaults['projects_edit'], 'projects_edit should default to 1' );
assert_perm( 1, $defaults['projects_delete'], 'projects_delete should default to 1' );
assert_perm( 0, $defaults['finances'], 'finances should default to 0' ); assert_perm( 0, $defaults['finances'], 'finances should default to 0' );
assert_perm( 0, $defaults['crm'], 'crm should default to 0' ); assert_perm( 0, $defaults['crm'], 'crm should default to 0' );
// Test defaults() returns full module array // Test defaults() returns full module array
$result = PermissionRepository::defaults(); $result = PermissionRepository::defaults();
assert_perm( 6, count( $result ), 'defaults() should return 6 modules' ); assert_perm( 9, count( $result ), 'defaults() should return 9 modules' );
assert_perm( 1, $result['tasks'], 'defaults() tasks should be 1' ); assert_perm( 1, $result['tasks'], 'defaults() tasks should be 1' );
} }