feat: Add wiki integration to task management

- Implemented a multi-select dropdown for associating tasks with wiki entries in the task edit form.
- Enhanced task popup to display related wiki entries with visibility controls based on user permissions.
- Updated the wiki main view to support bulk actions for categories, including deletion and search functionality.
- Created a new database migration for establishing many-to-many relationships between tasks and wiki entries.
- Improved styling for wiki components to enhance user experience.
- Added a new AGENTS.md file to outline communication and change management protocols.
This commit is contained in:
2026-03-03 11:52:04 +01:00
parent 447b75bf3e
commit 7c2a42a66f
13 changed files with 747 additions and 98 deletions

View File

@@ -311,6 +311,16 @@
"lmtime": 0, "lmtime": 0,
"modified": true "modified": true
}, },
"docs": {
"migrations": {
"2026-03-01-tasks-recursive-parent-id.sql": {
"type": "-",
"size": 542,
"lmtime": 1772360740310,
"modified": false
}
}
},
".htaccess": { ".htaccess": {
"type": "-", "type": "-",
"size": 1055, "size": 1055,
@@ -319,8 +329,8 @@
}, },
"index.php": { "index.php": {
"type": "-", "type": "-",
"size": 6268, "size": 6563,
"lmtime": 1772276742587, "lmtime": 1772386724937,
"modified": false "modified": false
}, },
"layout": { "layout": {
@@ -634,8 +644,8 @@
}, },
"task_popup.php": { "task_popup.php": {
"type": "-", "type": "-",
"size": 32627, "size": 36954,
"lmtime": 1772282729802, "lmtime": 1772395947362,
"modified": false "modified": false
}, },
"task_single.php": { "task_single.php": {
@@ -786,16 +796,6 @@
"size": 230708, "size": 230708,
"lmtime": 1771920013460, "lmtime": 1771920013460,
"modified": false "modified": false
},
"docs": {
"migrations": {
"2026-03-01-tasks-recursive-parent-id.sql": {
"type": "-",
"size": 542,
"lmtime": 1772360740310,
"modified": false
}
}
} }
} }
}, },

10
AGENTS.md Normal file
View File

@@ -0,0 +1,10 @@
# AGENTS.md
## Sposób pracy
- Pisz do mnie po polsku, zwięźle i krótko, ale merytorycznie
## Wprowadzanie zmian
- Przeanalizuj wprowadzone zadanie
- Jeżeli masz jakieś wątpliwości pytaj
- Przedstaw plan
- Po akceptacji wdróź plan

View File

@@ -458,10 +458,15 @@ class Tasks
$task['id'] = isset( $task['id'] ) ? (int)$task['id'] : $task_id; $task['id'] = isset( $task['id'] ) ? (int)$task['id'] : $task_id;
$task_id_for_attachments = (int)$task['id']; $task_id_for_attachments = (int)$task['id'];
$wiki_categories = \factory\Wiki::get_categories_for_user( (int)$user['id'] );
if ( !is_array( $wiki_categories ) or !count( $wiki_categories ) )
$wiki_categories = \factory\Wiki::get_all_categories();
return \Tpl::view( 'tasks/task_edit', [ return \Tpl::view( 'tasks/task_edit', [
'projects' => \factory\Projects::user_projects( $user['id'] ), 'projects' => \factory\Projects::user_projects( $user['id'] ),
'priorities' => \factory\Tasks::$priorities, 'priorities' => \factory\Tasks::$priorities,
'task' => $task, 'task' => $task,
'wiki_categories' => $wiki_categories,
'task_attachments' => $task_id_for_attachments ? $attachments_repository -> listByTaskId( $task_id_for_attachments ) : [], 'task_attachments' => $task_id_for_attachments ? $attachments_repository -> listByTaskId( $task_id_for_attachments ) : [],
'parent_tasks' => \factory\Tasks::parent_tasks( $user['id'] ), 'parent_tasks' => \factory\Tasks::parent_tasks( $user['id'] ),
'users' => \factory\Users::users_list(), 'users' => \factory\Users::users_list(),
@@ -493,6 +498,12 @@ class Tasks
$values['parent_id'] = null; $values['parent_id'] = null;
$values['priority'] = isset( $values['priority'] ) && $values['priority'] !== '' ? $values['priority'] : ( empty( $values['id'] ) ? 1 : 0 ); $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;
if ( isset( $values['wiki_ids'] ) )
$values['wiki_ids'] = $values['wiki_ids'];
else if ( isset( $values['wiki_ids[]'] ) )
$values['wiki_ids'] = $values['wiki_ids[]'];
else
$values['wiki_ids'] = [];
$status = \Controllers\TasksController::resolveTaskStatusForSave( $values ); $status = \Controllers\TasksController::resolveTaskStatusForSave( $values );
$recursive_parent_id = null; $recursive_parent_id = null;
@@ -504,7 +515,7 @@ class Tasks
} }
if ( $id = \factory\Tasks::task_save( if ( $id = \factory\Tasks::task_save(
$values['id'], $values['parent_id'], $recursive_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'] $values['id'], $values['parent_id'], $recursive_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'], $values['wiki_ids']
) ) ) )
{ {
\factory\Tasks::clear_task_opened( $id ); \factory\Tasks::clear_task_opened( $id );
@@ -527,11 +538,16 @@ class Tasks
$attachments_repository = new \Domain\Tasks\TaskAttachmentRepository(); $attachments_repository = new \Domain\Tasks\TaskAttachmentRepository();
\factory\Tasks::set_task_opened_by_user( \S::get( 'task_id' ), $user['id'] ); \factory\Tasks::set_task_opened_by_user( \S::get( 'task_id' ), $user['id'] );
$wiki_data = \factory\Tasks::task_wiki_entries_for_user( (int)\S::get( 'task_id' ), (int)$user['id'] );
echo \Tpl::view( 'tasks/task_popup', [ echo \Tpl::view( 'tasks/task_popup', [
'task' => \factory\Tasks::task_details( \S::get( 'task_id' ), $user['id'] ), 'task' => \factory\Tasks::task_details( \S::get( 'task_id' ), $user['id'] ),
'task_works' => \factory\Tasks::task_works( \S::get( 'task_id' ) ), 'task_works' => \factory\Tasks::task_works( \S::get( 'task_id' ) ),
'task_attachments' => $attachments_repository -> listByTaskId( \S::get( 'task_id' ) ), 'task_attachments' => $attachments_repository -> listByTaskId( \S::get( 'task_id' ) ),
'task_wiki_entries' => $wiki_data['entries'],
'task_wiki_all_count' => $wiki_data['all_count'],
'task_wiki_visible_count' => $wiki_data['visible_count'],
'task_wiki_hidden_count' => $wiki_data['hidden_count'],
'clients' => \factory\Crm::get_client_list(), 'clients' => \factory\Crm::get_client_list(),
'user' => $user, 'user' => $user,
'statuses' => \factory\Tasks::get_statuses(), 'statuses' => \factory\Tasks::get_statuses(),

View File

@@ -9,6 +9,8 @@ class Wiki
if ( !$user or!\controls\Users::permissions( $user[ 'id' ], 'wiki' ) ) if ( !$user or!\controls\Users::permissions( $user[ 'id' ], 'wiki' ) )
return false; return false;
if ( (int)$user['id'] !== 1 and (int)$user['id'] !== 3 )
return false;
if ( \factory\Wiki::category_delete( \S::get( 'id' ) ) ) if ( \factory\Wiki::category_delete( \S::get( 'id' ) ) )
\S::alert( 'Kategoria została usunięta.' ); \S::alert( 'Kategoria została usunięta.' );
@@ -17,6 +19,30 @@ class Wiki
exit; exit;
} }
public static function categories_delete_bulk()
{
global $user;
if ( !$user or!\controls\Users::permissions( $user[ 'id' ], 'wiki' ) )
return false;
if ( (int)$user['id'] !== 1 and (int)$user['id'] !== 3 )
return false;
$ids = \S::get( 'ids' );
if ( !is_array( $ids ) )
$ids = [];
$deleted_count = \factory\Wiki::categories_delete_bulk( $ids );
if ( $deleted_count > 0 )
\S::alert( 'Usunięto wpisy: ' . $deleted_count . '.' );
else
\S::alert( 'Nie wybrano poprawnych wpisów do usunięcia.' );
header( 'Location: /wiki/main_view/' );
exit;
}
public static function category_save() public static function category_save()
{ {
global $user; global $user;
@@ -69,4 +95,4 @@ class Wiki
] ); ] );
} }
} }

View File

@@ -589,10 +589,123 @@ class Tasks
if ( $user_id ) if ( $user_id )
$task['is_open'] = self::is_task_open( $task_id, $user_id ); $task['is_open'] = self::is_task_open( $task_id, $user_id );
$task['comments'] = $mdb -> select( 'tasks_comments', '*', [ 'task_id' => $task_id, 'ORDER' => [ 'date_add' => 'DESC' ] ] ); $task['comments'] = $mdb -> select( 'tasks_comments', '*', [ 'task_id' => $task_id, 'ORDER' => [ 'date_add' => 'DESC' ] ] );
$task['wiki_ids'] = self::task_wiki_ids( $task_id );
return $task; return $task;
} }
private static function normalize_wiki_ids( $wiki_ids )
{
if ( $wiki_ids === null or $wiki_ids === '' )
return [];
if ( is_string( $wiki_ids ) and strpos( $wiki_ids, ',' ) !== false )
$wiki_ids = explode( ',', $wiki_ids );
if ( !is_array( $wiki_ids ) )
$wiki_ids = [ $wiki_ids ];
$result = [];
foreach ( $wiki_ids as $wiki_id )
{
$wiki_id = (int)$wiki_id;
if ( $wiki_id > 0 )
$result[] = $wiki_id;
}
return array_values( array_unique( $result ) );
}
static public function task_wiki_ids( $task_id )
{
global $mdb;
$task_id = (int)$task_id;
if ( !$task_id )
return [];
$ids = $mdb -> select( 'task_wiki', 'wiki_id', [ 'task_id' => $task_id ] );
if ( !is_array( $ids ) )
return [];
return array_values( array_unique( array_map( 'intval', $ids ) ) );
}
static public function set_task_wiki_links( $task_id, $wiki_ids, $user_id )
{
global $mdb;
$task_id = (int)$task_id;
$user_id = (int)$user_id;
if ( !$task_id or !$user_id )
return false;
$wiki_ids = self::normalize_wiki_ids( $wiki_ids );
$visible_wiki_ids = [];
foreach ( $wiki_ids as $wiki_id )
{
if ( \factory\Wiki::is_category_visible_for_user( (int)$wiki_id, $user_id ) )
$visible_wiki_ids[] = (int)$wiki_id;
}
$existing_ids = self::task_wiki_ids( $task_id );
$hidden_existing_ids = [];
foreach ( $existing_ids as $existing_id )
{
if ( !\factory\Wiki::is_category_visible_for_user( (int)$existing_id, $user_id ) )
$hidden_existing_ids[] = (int)$existing_id;
}
$final_ids = array_values( array_unique( array_merge( $visible_wiki_ids, $hidden_existing_ids ) ) );
$mdb -> delete( 'task_wiki', [ 'task_id' => $task_id ] );
foreach ( $final_ids as $wiki_id )
$mdb -> insert( 'task_wiki', [ 'task_id' => $task_id, 'wiki_id' => $wiki_id ] );
return true;
}
static public function task_wiki_entries_for_user( $task_id, $user_id )
{
global $mdb;
$task_id = (int)$task_id;
$user_id = (int)$user_id;
$response = [
'all_count' => 0,
'visible_count' => 0,
'hidden_count' => 0,
'entries' => []
];
if ( !$task_id or !$user_id )
return $response;
$response['all_count'] = (int)$mdb -> count( 'task_wiki', [ 'task_id' => $task_id ] );
if ( $user_id == 1 or $user_id == 3 )
$sql = 'SELECT w.* FROM task_wiki tw INNER JOIN wiki w ON w.id = tw.wiki_id WHERE tw.task_id = ' . $task_id . ' ORDER BY w.name ASC';
else
$sql = 'SELECT DISTINCT w.* FROM task_wiki tw '
. 'INNER JOIN wiki w ON w.id = tw.wiki_id '
. 'LEFT JOIN wiki_users wu ON wu.wiki_id = w.id '
. 'WHERE tw.task_id = ' . $task_id . ' AND (wu.user_id = ' . $user_id . ' OR NOT EXISTS (SELECT 1 FROM wiki_users wu2 WHERE wu2.wiki_id = w.id)) '
. 'ORDER BY w.name ASC';
$entries = $mdb -> query( $sql ) -> fetchAll( \PDO::FETCH_ASSOC );
if ( !is_array( $entries ) )
$entries = [];
$response['entries'] = $entries;
$response['visible_count'] = count( $entries );
$response['hidden_count'] = max( 0, $response['all_count'] - $response['visible_count'] );
return $response;
}
static public function task_total_time( $task_id, $month = '' ) static public function task_total_time( $task_id, $month = '' )
{ {
global $mdb; global $mdb;
@@ -633,7 +746,7 @@ class Tasks
} }
// przy zmianach pamiętać o zadaniach z CRON // przy zmianach pamiętać o zadaniach z CRON
static public function task_save( $task_id, $parent_id = null, $recursive_parent_id = null, $user_id, $name, $text, $date_start, $date_end, $project_id, $client_id, $pay_rate, $reminders_interval, $recursively, $frequency, $period, $users, $date_end_month_day = null, $date_start_month_day = null, $send_email_notification = false, $status_change_mail, $rescursive = false, $status = 0, $show_in_calendar, $priority = 0, $recursive_last_date = null ) static public function task_save( $task_id, $parent_id = null, $recursive_parent_id = null, $user_id, $name, $text, $date_start, $date_end, $project_id, $client_id, $pay_rate, $reminders_interval, $recursively, $frequency, $period, $users, $date_end_month_day = null, $date_start_month_day = null, $send_email_notification = false, $status_change_mail, $rescursive = false, $status = 0, $show_in_calendar, $priority = 0, $recursive_last_date = null, $wiki_ids = [] )
{ {
global $mdb; global $mdb;
@@ -685,6 +798,7 @@ class Tasks
\factory\Projects::send_email_notification( $id, $users ); \factory\Projects::send_email_notification( $id, $users );
} }
self::set_task_wiki_links( $id, $wiki_ids, $user_id );
return $id; return $id;
} }
else else
@@ -730,6 +844,7 @@ class Tasks
} }
$mdb -> delete( 'tasks_reminders', [ 'task_id' => $task_id ] ); $mdb -> delete( 'tasks_reminders', [ 'task_id' => $task_id ] );
self::set_task_wiki_links( (int)$task_id, $wiki_ids, $user_id );
return $task_id; return $task_id;
} }
@@ -779,6 +894,7 @@ class Tasks
$attachments_repository = new \Domain\Tasks\TaskAttachmentRepository( $mdb ); $attachments_repository = new \Domain\Tasks\TaskAttachmentRepository( $mdb );
$attachments_repository -> purgeByTaskId( $task_id ); $attachments_repository -> purgeByTaskId( $task_id );
$mdb -> delete( 'task_wiki', [ 'task_id' => (int)$task_id ] );
$mdb -> delete( 'tasks', [ 'id' => $task_id ] ); $mdb -> delete( 'tasks', [ 'id' => $task_id ] );
$mdb -> update( 'tasks', [ 'recursive_parent_id' => null ], [ 'recursive_parent_id' => $task_id ] ); $mdb -> update( 'tasks', [ 'recursive_parent_id' => null ], [ 'recursive_parent_id' => $task_id ] );

View File

@@ -3,11 +3,47 @@ namespace factory;
class Wiki class Wiki
{ {
static public function get_all_categories()
{
global $mdb;
return $mdb -> select( 'wiki', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
}
static public function category_delete( int $category_id ) { static public function category_delete( int $category_id ) {
global $mdb; global $mdb;
$category_id = (int)$category_id;
if ( !$category_id )
return false;
$mdb -> delete( 'wiki_users', [ 'wiki_id' => $category_id ] );
$mdb -> delete( 'task_wiki', [ 'wiki_id' => $category_id ] );
return $mdb -> delete( 'wiki', [ 'id' => $category_id ] ); return $mdb -> delete( 'wiki', [ 'id' => $category_id ] );
} }
static public function categories_delete_bulk( array $category_ids )
{
global $mdb;
$ids = [];
foreach ( $category_ids as $category_id )
{
$category_id = (int)$category_id;
if ( $category_id > 0 )
$ids[] = $category_id;
}
$ids = array_values( array_unique( $ids ) );
if ( !count( $ids ) )
return 0;
$mdb -> delete( 'wiki_users', [ 'wiki_id' => $ids ] );
$mdb -> delete( 'task_wiki', [ 'wiki_id' => $ids ] );
$mdb -> delete( 'wiki', [ 'id' => $ids ] );
return count( $ids );
}
public static function category_save( $category_id, $name, $text, $text_admin = '', $users ) public static function category_save( $category_id, $name, $text, $text_admin = '', $users )
{ {
global $mdb; global $mdb;
@@ -59,9 +95,38 @@ class Wiki
{ {
global $mdb, $user; global $mdb, $user;
if ( $user['id'] == 1 or $user['id'] == 3 ) return self::get_categories_for_user( (int)$user['id'] );
}
static public function get_categories_for_user( int $user_id )
{
global $mdb;
if ( $user_id == 1 or $user_id == 3 )
return $mdb -> select( 'wiki', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] ); return $mdb -> select( 'wiki', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
else
return $mdb -> query( 'SELECT w.* FROM wiki AS w INNER JOIN wiki_users AS wu ON wu.wiki_id = w.id WHERE user_id = ' . $user['id'] ) -> fetchAll( \PDO::FETCH_ASSOC ); return $mdb -> query(
'SELECT DISTINCT w.* FROM wiki AS w '
. 'LEFT JOIN wiki_users AS wu ON wu.wiki_id = w.id '
. 'WHERE wu.user_id = ' . $user_id . ' OR NOT EXISTS (SELECT 1 FROM wiki_users wu2 WHERE wu2.wiki_id = w.id) '
. 'ORDER BY w.name ASC'
) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function is_category_visible_for_user( int $category_id, int $user_id ): bool
{
global $mdb;
if ( !$category_id or !$user_id )
return false;
if ( $user_id == 1 or $user_id == 3 )
return (bool)$mdb -> count( 'wiki', [ 'id' => $category_id ] );
$visibility_rows = (int)$mdb -> count( 'wiki_users', [ 'wiki_id' => $category_id ] );
if ( $visibility_rows === 0 )
return (bool)$mdb -> count( 'wiki', [ 'id' => $category_id ] );
return (bool)$mdb -> count( 'wiki_users', [ 'AND' => [ 'wiki_id' => $category_id, 'user_id' => $user_id ] ] );
} }
} }

View File

@@ -0,0 +1,10 @@
-- 2026-03-03: relacja wiele-do-wielu zadania <-> wpisy wiki
CREATE TABLE IF NOT EXISTS `task_wiki` (
`task_id` INT NOT NULL,
`wiki_id` INT NOT NULL,
`date_add` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`task_id`, `wiki_id`),
KEY `idx_task_wiki_wiki` (`wiki_id`)
);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1044,19 +1044,27 @@ $sidebar-hover-bg: rgba(255, 255, 255, 0.08);
justify-content: center; justify-content: center;
border-radius: 6px; border-radius: 6px;
i { font-size: 13px; } i {
font-size: 13px;
}
} }
a.task-title-save { a.task-title-save {
background: $cGreen; background: $cGreen;
border-color: $cGreen; border-color: $cGreen;
i { color: #fff; }
i {
color: #fff;
}
} }
a.task-title-cancel { a.task-title-cancel {
background: $cRed; background: $cRed;
border-color: $cRed; border-color: $cRed;
i { color: #fff; }
i {
color: #fff;
}
} }
} }
} }

View File

@@ -35,6 +35,23 @@ if ( is_array( $this -> clients ) )
foreach ( $this -> clients as $client ) foreach ( $this -> clients as $client )
$clients[ $client[ 'id' ] ] = $client[ 'firm' ]; $clients[ $client[ 'id' ] ] = $client[ 'firm' ];
$wiki_categories = ( isset( $this -> wiki_categories ) and is_array( $this -> wiki_categories ) ) ? $this -> wiki_categories : [];
$current_user_id = isset( $this -> user['id'] ) ? (int)$this -> user['id'] : 0;
if ( !count( $wiki_categories ) and $current_user_id > 0 )
$wiki_categories = \factory\Wiki::get_categories_for_user( $current_user_id );
if ( !count( $wiki_categories ) )
$wiki_categories = \factory\Wiki::get_all_categories();
$task_wiki_ids = isset( $this -> task['wiki_ids'] ) && is_array( $this -> task['wiki_ids'] ) ? array_map( 'intval', $this -> task['wiki_ids'] ) : [];
$wiki_values = [];
if ( is_array( $wiki_categories ) )
foreach ( $wiki_categories as $wiki_category )
{
$wiki_id = isset( $wiki_category['id'] ) ? (int)$wiki_category['id'] : 0;
if ( !$wiki_id )
continue;
$wiki_values[ $wiki_id ] = isset( $wiki_category['name'] ) ? (string)$wiki_category['name'] : (string)$wiki_id;
}
$parent_tasks = [ 0 => '--- wybierz zadanie nadrzędne ---' ]; $parent_tasks = [ 0 => '--- wybierz zadanie nadrzędne ---' ];
if ( is_array( $this -> parent_tasks ) ) if ( is_array( $this -> parent_tasks ) )
foreach ( $this -> parent_tasks as $parent_task ) foreach ( $this -> parent_tasks as $parent_task )
@@ -130,6 +147,20 @@ ob_start();
'values' => $parent_tasks 'values' => $parent_tasks
] ); ] );
?> ?>
<div class="form_group">
<label class="label">Powiazane Wiki:</label>
<div class="input">
<? if ( is_array( $wiki_categories ) and count( $wiki_categories ) ):?>
<select name="wiki_ids[]" id="wiki_ids" class="form-control" multiple>
<? foreach ( $wiki_values as $wiki_id => $wiki_name ):?>
<option value="<?= (int)$wiki_id;?>" <? if ( in_array( (int)$wiki_id, $task_wiki_ids ) ):?>selected="selected"<? endif;?>><?= htmlspecialchars( $wiki_name );?></option>
<? endforeach;?>
</select>
<? else:?>
<div class="task-wiki-empty">Brak dostepnych wpisow Wiki.</div>
<? endif;?>
</div>
</div>
<?= \Html::select( [ <?= \Html::select( [
'label' => 'Status', 'label' => 'Status',
'name' => 'status', 'name' => 'status',
@@ -434,6 +465,29 @@ ob_start();
font-style: italic; font-style: italic;
} }
#wiki_ids + .select2-container {
width: 100% !important;
}
#wiki_ids + .select2-container .select2-selection--multiple {
min-height: 38px;
border: 1px solid #cfd8e3;
border-radius: 4px;
padding: 2px 6px;
}
#wiki_ids + .select2-container .select2-selection__choice {
margin-top: 4px;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.task-wiki-empty {
color: #6b7280;
font-style: italic;
}
.task-edit-message { .task-edit-message {
margin: 12px 0 16px 0; margin: 12px 0 16px 0;
padding: 12px 16px; padding: 12px 16px;
@@ -957,12 +1011,12 @@ echo $grid -> draw();
height: '100' height: '100'
}); });
$( '#project_id, #client_id, #status, #parent_id, #priority' ).select2({ $( '#project_id, #client_id, #status, #parent_id, #priority, #wiki_ids' ).select2({
theme: 'bootstrap-5', theme: 'bootstrap-5',
minimumResultsForSearch: 0 minimumResultsForSearch: 0
}); });
$( '#project_id, #client_id, #status, #parent_id, #priority' ).on( 'select2:open', function() { $( '#project_id, #client_id, #status, #parent_id, #priority, #wiki_ids' ).on( 'select2:open', function() {
setTimeout( function() { setTimeout( function() {
var search_field = document.querySelector( '.select2-container--open .select2-search__field' ); var search_field = document.querySelector( '.select2-container--open .select2-search__field' );
if ( search_field ) if ( search_field )

View File

@@ -26,6 +26,10 @@
$checklist_count = is_array( $this -> task['actions'] ) ? count( $this -> task['actions'] ) : 0; $checklist_count = is_array( $this -> task['actions'] ) ? count( $this -> task['actions'] ) : 0;
$comments_count = is_array( $this -> task['comments'] ) ? count( $this -> task['comments'] ) : 0; $comments_count = is_array( $this -> task['comments'] ) ? count( $this -> task['comments'] ) : 0;
$attachments_count = is_array( $this -> task_attachments ) ? count( $this -> task_attachments ) : 0; $attachments_count = is_array( $this -> task_attachments ) ? count( $this -> task_attachments ) : 0;
$popup_wiki_data = \factory\Tasks::task_wiki_entries_for_user( (int)$this -> task['id'], (int)$this -> user['id'] );
$task_wiki_entries = isset( $popup_wiki_data['entries'] ) && is_array( $popup_wiki_data['entries'] ) ? $popup_wiki_data['entries'] : [];
$wiki_visible_count = isset( $popup_wiki_data['visible_count'] ) ? (int)$popup_wiki_data['visible_count'] : 0;
$wiki_hidden_count = isset( $popup_wiki_data['hidden_count'] ) ? (int)$popup_wiki_data['hidden_count'] : 0;
?> ?>
<? <?
$task_description_html = htmlspecialchars_decode( (string)$this -> task['text'] ); $task_description_html = htmlspecialchars_decode( (string)$this -> task['text'] );
@@ -74,6 +78,7 @@
<a href="#" class="js-task-tab-btn" data-tab="checklist" role="tab" aria-selected="false">Lista kontrolna (<?= (int)$checklist_count;?>)</a> <a href="#" class="js-task-tab-btn" data-tab="checklist" role="tab" aria-selected="false">Lista kontrolna (<?= (int)$checklist_count;?>)</a>
<a href="#" class="js-task-tab-btn" data-tab="comments" role="tab" aria-selected="false">Komentarze (<?= (int)$comments_count;?>)</a> <a href="#" class="js-task-tab-btn" data-tab="comments" role="tab" aria-selected="false">Komentarze (<?= (int)$comments_count;?>)</a>
<a href="#" class="js-task-tab-btn" data-tab="attachments" role="tab" aria-selected="false">Za&#322;&#261;czniki (<?= (int)$attachments_count;?>)</a> <a href="#" class="js-task-tab-btn" data-tab="attachments" role="tab" aria-selected="false">Za&#322;&#261;czniki (<?= (int)$attachments_count;?>)</a>
<a href="#" class="js-task-tab-btn" data-tab="wiki" role="tab" aria-selected="false">Wiki (<?= (int)$wiki_visible_count;?>)</a>
<? if ( $this -> user['id'] == 1 ):?> <? if ( $this -> user['id'] == 1 ):?>
<a href="#" class="js-task-tab-btn" data-tab="users" role="tab" aria-selected="false">Uczestnicy</a> <a href="#" class="js-task-tab-btn" data-tab="users" role="tab" aria-selected="false">Uczestnicy</a>
<? endif;?> <? endif;?>
@@ -177,6 +182,35 @@
</ul> </ul>
</div> </div>
</div> </div>
<div class="task-tab-panel" data-tab="wiki">
<div class="box">
<h3>Powiazane wpisy Wiki</h3>
<? if ( $wiki_hidden_count > 0 ):?>
<div class="alert alert-warning" style="margin-bottom: 12px;">
Nie masz dostepu do <?= (int)$wiki_hidden_count;?> powiazanych wpisow Wiki.
</div>
<? endif;?>
<? if ( is_array( $task_wiki_entries ) and count( $task_wiki_entries ) ):?>
<div class="task-wiki-list">
<? foreach ( $task_wiki_entries as $wiki_entry ):?>
<div class="task-wiki-entry">
<h4><?= htmlspecialchars( (string)$wiki_entry['name'] );?></h4>
<div class="task-wiki-content">
<?= (string)$wiki_entry['text'];?>
<? if ( $this -> user['id'] == 1 or $this -> user['id'] == 3 ):?>
<? if ( isset( $wiki_entry['text_admin'] ) and (string)$wiki_entry['text_admin'] !== '' ):?>
<div class="task-wiki-admin"><?= (string)$wiki_entry['text_admin'];?></div>
<? endif;?>
<? endif;?>
</div>
</div>
<? endforeach;?>
</div>
<? else:?>
<div class="task-wiki-empty">Brak widocznych wpisow Wiki dla tego zadania.</div>
<? endif;?>
</div>
</div>
<? if ( $this -> user['id'] == 1 ):?> <? if ( $this -> user['id'] == 1 ):?>
<div class="task-tab-panel" data-tab="users"> <div class="task-tab-panel" data-tab="users">
<div class="task-users-edit"> <div class="task-users-edit">
@@ -407,6 +441,36 @@
font-weight: 600; font-weight: 600;
} }
.task_popup .task_details .task-wiki-list {
display: grid;
gap: 12px;
}
.task_popup .task_details .task-wiki-entry {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
background: #fff;
}
.task_popup .task_details .task-wiki-entry h4 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 700;
color: #1f3d72;
}
.task_popup .task_details .task-wiki-content {
font-size: 13px;
line-height: 1.5;
}
.task_popup .task_details .task-wiki-admin {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #d1d5db;
}
.task_popup .task_details .task-wiki-empty {
color: #6b7280;
font-style: italic;
}
/* Lightbox - powiększanie zdjęć w opisie */ /* Lightbox - powiększanie zdjęć w opisie */
.task_popup .task_details .description img { .task_popup .task_details .description img {
cursor: zoom-in; cursor: zoom-in;
@@ -676,8 +740,16 @@
if ( !tab_panels.length ) if ( !tab_panels.length )
return; return;
var allowed_tabs = [ 'description', 'checklist', 'comments', 'attachments', 'users' ]; var available_tabs = [];
var selected_tab = allowed_tabs.indexOf( tab_name ) >= 0 ? tab_name : 'description'; tab_buttons.each( function() {
var tab = $( this ).attr( 'data-tab' );
if ( tab )
available_tabs.push( tab );
});
var selected_tab = available_tabs.indexOf( tab_name ) >= 0 ? tab_name : 'description';
if ( available_tabs.indexOf( selected_tab ) < 0 && available_tabs.length )
selected_tab = available_tabs[0];
tab_panels.each( function() { tab_panels.each( function() {
var panel = $( this ); var panel = $( this );

View File

@@ -1,71 +1,303 @@
<div class="row block-header"> <? $is_admin = ( (int)$this -> user['id'] === 1 or (int)$this -> user['id'] === 3 );?>
<div class="row block-header wiki-header">
<div class="col-12"> <div class="col-12">
<h2>Wiki</h2> <h2>Wiki</h2>
</div> </div>
</div> </div>
<div class="box">
<? if ( $this -> user['id'] == 1 or $this -> user['id'] == 3 ):?> <div class="box wiki-main">
<div class="menu-buttons"> <div class="wiki-toolbar">
<a href="/wiki/category_edit/" class="btn btn-success btn-sm" title="Dodaj kategorię"> <div class="wiki-toolbar-left">
<i class="fa fa-plus"></i>Dodaj kategorię <input type="text" class="form-control" id="wiki-search-name" placeholder="Szukaj wpisu po nazwie...">
</a>
</div> </div>
<? endif;?> <div class="wiki-toolbar-right">
<table class="table table-striped table-hover table-sm"> <? if ( $is_admin ):?>
<tr> <a href="/wiki/category_edit/" class="btn btn-success btn-sm" title="Dodaj kategori&#281;">
<td> <i class="fa fa-plus"></i> Dodaj wpis
<input type="text" class="form-control" id="search-name"> </a>
</td> <? endif;?>
</tr> </div>
</table> </div>
<div class="wiki-categories">
<? foreach ( $this -> categories as $category ):?> <? if ( $is_admin ):?>
<div class="category"> <form method="POST" action="/wiki/categories_delete_bulk/" id="wiki-bulk-delete-form">
<div class="title"> <div class="wiki-bulk-actions">
<a href="/wiki/category_preview/id=<?= $category['id'];?>"><?= $category['name'];?></a> <label class="wiki-select-all-label">
</div> <input type="checkbox" class="g-checkbox" id="wiki-select-all">
<? if ( $this -> user['id'] == 1 or $this -> user['id'] == 3 ):?> Zaznacz wszystko
<div class="row"> </label>
<div class="col-md-6"> <span class="wiki-selected-count">Zaznaczono: <strong id="wiki-selected-count-value">0</strong></span>
<div class="users"> <button type="button" class="btn btn-danger btn-sm" id="wiki-bulk-delete-btn" disabled>
<? $category_users = \factory\Wiki::category_users( $category['id'] );?> <i class="fa fa-trash"></i> Usu&#324; zaznaczone
<? if ( is_array( $category_users ) ): foreach ( $category_users as $user_tmp ):?> </button>
<? </div>
$user = \factory\Users::user_details( $user_tmp ); <div class="wiki-categories-grid">
echo '<div class="user" style="background: ' . $user['color'] . '" title="' . $user['name'] . ' ' . $user['surname'] . '">' . $user['name'][0] . $user['surname'][0] . '</div>'; <? if ( is_array( $this -> categories ) and count( $this -> categories ) ):?>
?> <? foreach ( $this -> categories as $category ):?>
<? endforeach; endif;?> <? $category_name = isset( $category['name'] ) ? (string)$category['name'] : '';?>
<? $category_text = isset( $category['text'] ) ? strip_tags( (string)$category['text'] ) : '';?>
<? $category_preview = trim( $category_text );?>
<? if ( strlen( $category_preview ) > 170 ) $category_preview = substr( $category_preview, 0, 170 ) . '...';?>
<article class="wiki-card" data-name="<?= htmlspecialchars( strtolower( $category_name ) );?>">
<div class="wiki-card-top">
<label class="wiki-select-one-label">
<input type="checkbox" class="g-checkbox wiki-select-one" name="ids[]" value="<?= (int)$category['id'];?>">
</label>
<a href="/wiki/category_preview/id=<?= (int)$category['id'];?>" class="wiki-card-title"><?= htmlspecialchars( $category_name );?></a>
</div> </div>
</div> <? if ( $category_preview !== '' ):?>
<div class="col-md-6"> <div class="wiki-card-preview"><?= htmlspecialchars( $category_preview );?></div>
<div class="actions"> <? else:?>
<a href="/wiki/category_edit/id=<?= $category['id'];?>">edytuj</a> <div class="wiki-card-preview wiki-card-preview-empty">Brak podgl&#261;du tre&#347;ci.</div>
<a href="/wiki/category_delete/id=<?= $category['id'];?>" class="category-delete">usuń</a> <? endif;?>
<div class="wiki-card-bottom">
<div class="users">
<? $category_users = \factory\Wiki::category_users( (int)$category['id'] );?>
<? if ( is_array( $category_users ) ): foreach ( $category_users as $user_tmp ):?>
<?
$user = \factory\Users::user_details( $user_tmp );
echo '<div class="user" style="background:' . htmlspecialchars( $user['color'] ) . ';" title="' . htmlspecialchars( $user['name'] . ' ' . $user['surname'] ) . '">' . htmlspecialchars( $user['name'][0] . $user['surname'][0] ) . '</div>';
?>
<? endforeach; endif;?>
</div>
<div class="wiki-card-actions">
<a href="/wiki/category_edit/id=<?= (int)$category['id'];?>">edytuj</a>
<a href="/wiki/category_delete/id=<?= (int)$category['id'];?>" class="category-delete">usu&#324;</a>
</div>
</div> </div>
</div> </article>
</div> <? endforeach;?>
<? else:?>
<div class="wiki-empty-state">Brak wpis&oacute;w Wiki.</div>
<? endif;?> <? endif;?>
</div> </div>
<? endforeach;?> </form>
</div> <? else:?>
<div class="wiki-categories-grid">
<? if ( is_array( $this -> categories ) and count( $this -> categories ) ):?>
<? foreach ( $this -> categories as $category ):?>
<? $category_name = isset( $category['name'] ) ? (string)$category['name'] : '';?>
<? $category_text = isset( $category['text'] ) ? strip_tags( (string)$category['text'] ) : '';?>
<? $category_preview = trim( $category_text );?>
<? if ( strlen( $category_preview ) > 170 ) $category_preview = substr( $category_preview, 0, 170 ) . '...';?>
<article class="wiki-card" data-name="<?= htmlspecialchars( strtolower( $category_name ) );?>">
<div class="wiki-card-top">
<a href="/wiki/category_preview/id=<?= (int)$category['id'];?>" class="wiki-card-title"><?= htmlspecialchars( $category_name );?></a>
</div>
<? if ( $category_preview !== '' ):?>
<div class="wiki-card-preview"><?= htmlspecialchars( $category_preview );?></div>
<? else:?>
<div class="wiki-card-preview wiki-card-preview-empty">Brak podgl&#261;du tre&#347;ci.</div>
<? endif;?>
</article>
<? endforeach;?>
<? else:?>
<div class="wiki-empty-state">Brak wpis&oacute;w Wiki.</div>
<? endif;?>
</div>
<? endif;?>
</div>
<style type="text/css">
.wiki-main {
--wiki-bg-soft: #f4f8ff;
--wiki-border: #d8e2f6;
--wiki-title: #274985;
--wiki-text: #425466;
--wiki-muted: #728197;
overflow-x: hidden;
}
.wiki-main .wiki-toolbar {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
padding: 12px;
border: 1px solid var(--wiki-border);
border-radius: 10px;
background: linear-gradient(180deg, #ffffff 0%, var(--wiki-bg-soft) 100%);
}
.wiki-main .wiki-toolbar-left {
flex: 1;
max-width: 420px;
}
.wiki-main .wiki-bulk-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: 12px;
padding: 10px 12px;
border: 1px solid var(--wiki-border);
border-radius: 8px;
background: #fff;
}
.wiki-main .wiki-select-all-label,
.wiki-main .wiki-select-one-label {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
font-weight: 600;
color: var(--wiki-text);
cursor: pointer;
}
.wiki-main .wiki-selected-count {
color: var(--wiki-muted);
}
.wiki-main .wiki-categories-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.wiki-main .wiki-card {
display: flex;
flex-direction: column;
gap: 10px;
border: 1px solid var(--wiki-border);
border-radius: 10px;
padding: 12px;
background: #fff;
transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease;
min-width: 0;
overflow: hidden;
}
.wiki-main .wiki-card:hover {
border-color: #b8c9eb;
box-shadow: 0 8px 20px rgba(31, 61, 114, .08);
transform: translateY(-1px);
}
.wiki-main .wiki-card-top {
display: flex;
align-items: flex-start;
gap: 8px;
}
.wiki-main .wiki-card-title {
color: var(--wiki-title);
font-size: 15px;
font-weight: 700;
text-decoration: none;
line-height: 1.3;
overflow-wrap: anywhere;
word-break: break-word;
}
.wiki-main .wiki-card-title:hover {
text-decoration: underline;
}
.wiki-main .wiki-card-preview {
color: var(--wiki-text);
font-size: 13px;
line-height: 1.45;
min-height: 38px;
overflow-wrap: anywhere;
word-break: break-word;
}
.wiki-main .wiki-card-preview-empty {
color: var(--wiki-muted);
font-style: italic;
}
.wiki-main .wiki-card-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: auto;
}
.wiki-main .wiki-card-actions a {
font-size: 12px;
margin-left: 8px;
text-decoration: none;
}
.wiki-main .users {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.wiki-main .users .user {
width: 26px;
height: 26px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 11px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .22);
border: 1px solid rgba(255, 255, 255, .45);
}
.wiki-main .wiki-card-actions a:first-child {
margin-left: 0;
}
.wiki-main .wiki-empty-state {
grid-column: 1 / -1;
border: 1px dashed var(--wiki-border);
border-radius: 10px;
background: #fff;
color: var(--wiki-muted);
text-align: center;
padding: 24px 12px;
font-style: italic;
}
@media (max-width: 767px) {
.wiki-main .wiki-toolbar {
flex-direction: column;
align-items: stretch;
}
.wiki-main .wiki-toolbar-left {
max-width: none;
}
}
</style>
<script type="text/javascript"> <script type="text/javascript">
$( function() $( function()
{ {
$( 'body' ).on( 'click', '.category-delete', function(e) { function updateBulkState() {
e.preventDefault(); var selected = $( '.wiki-select-one:checked' ).length;
var href = $( this ).attr( 'href' ); $( '#wiki-selected-count-value' ).text( selected );
$( '#wiki-bulk-delete-btn' ).prop( 'disabled', selected === 0 );
var all_count = $( '.wiki-select-one' ).length;
var all_selected = all_count > 0 && selected === all_count;
$( '#wiki-select-all' ).prop( 'checked', all_selected );
}
function confirmRedirect( href, content_text ) {
$.confirm({ $.confirm({
title: 'Potwierdź', title: 'Potwierd&#378;',
type: 'orange', type: 'orange',
columnClass: 'col-md-8 col-md-offset-2 col-12', columnClass: 'col-md-8 col-md-offset-2 col-12',
closeIcon: true, closeIcon: true,
closeIconClass: 'fa fa-close', closeIconClass: 'fa fa-close',
content: 'Na pewno chcesz usunąć wybrany wpis?', content: content_text,
theme: 'modern', theme: 'modern',
buttons: { buttons: {
submit: { submit: {
text: '<i class="fa fa-check"></i>Zatwierdź', text: '<i class="fa fa-check"></i>Zatwierd&#378;',
btnClass: 'btn-success', btnClass: 'btn-success',
action: function () { action: function () {
document.location.href = href; document.location.href = href;
@@ -74,36 +306,76 @@
cancel: { cancel: {
text: '<i class="fa fa-close"></i>Anuluj', text: '<i class="fa fa-close"></i>Anuluj',
btnClass: 'btn-danger', btnClass: 'btn-danger',
keys: ['enter'], keys: [ 'enter' ],
action: function() {}
}
}
});
}
$( 'body' ).on( 'click', '.category-delete', function( e ) {
e.preventDefault();
confirmRedirect( $( this ).attr( 'href' ), 'Na pewno chcesz usun&#261;&#263; wybrany wpis?' );
});
$( '#wiki-select-all' ).on( 'change', function() {
var is_checked = $( this ).is( ':checked' );
$( '.wiki-select-one' ).prop( 'checked', is_checked );
updateBulkState();
});
$( 'body' ).on( 'change', '.wiki-select-one', function() {
updateBulkState();
});
$( '#wiki-bulk-delete-btn' ).on( 'click', function() {
var selected = $( '.wiki-select-one:checked' ).length;
if ( selected <= 0 )
{
$.alert( 'Najpierw zaznacz wpisy do usuni&#281;cia.' );
return;
}
$.confirm({
title: 'Potwierd&#378;',
type: 'orange',
columnClass: 'col-md-8 col-md-offset-2 col-12',
closeIcon: true,
closeIconClass: 'fa fa-close',
content: 'Na pewno chcesz usun&#261;&#263; zaznaczone wpisy (' + selected + ')?',
theme: 'modern',
buttons: {
submit: {
text: '<i class="fa fa-check"></i>Zatwierd&#378;',
btnClass: 'btn-success',
action: function () {
$( '#wiki-bulk-delete-form' ).trigger( 'submit' );
}
},
cancel: {
text: '<i class="fa fa-close"></i>Anuluj',
btnClass: 'btn-danger',
keys: [ 'enter' ],
action: function() {} action: function() {}
} }
} }
}); });
}); });
var timer = ''; var timer = null;
$( '#search-name' ).keyup( function() $( '#wiki-search-name' ).on( 'keyup', function()
{ {
var _this = $( this); var phrase = String( $( this ).val() || '' ).toLowerCase().trim();
var category = _this.val();
clearTimeout( timer ); clearTimeout( timer );
timer = setTimeout( function() timer = setTimeout( function() {
{ $( '.wiki-card' ).each( function() {
category = category.toLowerCase(); var card = $( this );
var name = String( card.attr( 'data-name' ) || '' );
$( '.category' ).hide(); card.toggle( phrase === '' || name.indexOf( phrase ) >= 0 );
$( ".category .title a" ).each( function ( index )
{
var text = $( this ).text();
text = text.trim().toLowerCase(); console.log( text );
if ( text.indexOf( category ) >= 0 )
$( this ).parents( '.category' ).show();
}); });
}, 180 );
if ( category == '' )
$( '.category' ).show();
}, 500 );
}); });
updateBulkState();
}); });
</script> </script>