Refactor task management and add attachment functionality
- Updated task editing template to handle default status for new tasks and corrected variable names. - Enhanced work time reporting by rounding time to the nearest quarter hour and adjusting amount formatting. - Introduced TasksController to manage task-related operations, including status resolution and email notifications. - Added TaskAttachmentRepository for handling task attachments, including upload, rename, and delete functionalities. - Implemented WorkTimeRepository to fetch clients with unsettled tasks and calculate total work time. - Created unit tests for TasksController and TaskAttachmentRepository to ensure functionality and correctness.
This commit is contained in:
8
ajax.php
8
ajax.php
@@ -4,7 +4,11 @@ function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\' , $classname );
|
||||
$c = array_pop( $q );
|
||||
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
||||
$base_path = 'autoload/' . implode( '/' , $q ) . '/';
|
||||
$f = $base_path . $c . '.php';
|
||||
|
||||
if ( !file_exists( $f ) )
|
||||
$f = $base_path . 'class.' . $c . '.php';
|
||||
|
||||
if ( file_exists( $f ) )
|
||||
require_once( $f );
|
||||
@@ -42,4 +46,4 @@ $mdb = new medoo( [
|
||||
] );
|
||||
|
||||
$user = \S::get_session( 'user' );
|
||||
?>
|
||||
?>
|
||||
|
||||
8
api.php
8
api.php
@@ -5,7 +5,11 @@ function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\', $classname );
|
||||
$c = array_pop( $q );
|
||||
$f = 'autoload/' . implode( '/', $q ) . '/class.' . $c . '.php';
|
||||
$base_path = 'autoload/' . implode( '/', $q ) . '/';
|
||||
$f = $base_path . $c . '.php';
|
||||
|
||||
if ( !file_exists( $f ) )
|
||||
$f = $base_path . 'class.' . $c . '.php';
|
||||
|
||||
if ( file_exists( $f ) )
|
||||
require_once( $f );
|
||||
@@ -123,4 +127,4 @@ if ( \S::get( 'action' ) == 'add_finance_operation' )
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
100
autoload/Controllers/TasksController.php
Normal file
100
autoload/Controllers/TasksController.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
namespace Controllers;
|
||||
|
||||
class TasksController
|
||||
{
|
||||
private const DEFAULT_NEW_TASK_STATUS = 5; // do zrobienia
|
||||
|
||||
public static function workTime()
|
||||
{
|
||||
$work_time_repository = new \Domain\Tasks\WorkTimeRepository();
|
||||
|
||||
$view_model = self::workTimeViewModel(
|
||||
$work_time_repository -> getClientsWithUnsettledTasks(),
|
||||
\factory\Crm::settings()
|
||||
);
|
||||
|
||||
return \Tpl::view( 'tasks/work-time', $view_model );
|
||||
}
|
||||
|
||||
public static function workTimeViewModel( array $work_time_clients, array $settings )
|
||||
{
|
||||
return [
|
||||
'work_time_clients' => $work_time_clients,
|
||||
'settings' => $settings
|
||||
];
|
||||
}
|
||||
|
||||
public static function resolveTaskStatusForForm( array $task )
|
||||
{
|
||||
$is_new_task = !isset( $task['id'] ) or !(int)$task['id'];
|
||||
|
||||
if ( $is_new_task and ( !isset( $task['status'] ) or $task['status'] === null or $task['status'] === '' ) )
|
||||
return self::DEFAULT_NEW_TASK_STATUS;
|
||||
|
||||
return isset( $task['status'] ) ? (int)$task['status'] : 0;
|
||||
}
|
||||
|
||||
public static function resolveTaskStatusForSave( array $values )
|
||||
{
|
||||
$is_new_task = !isset( $values['id'] ) or !(int)$values['id'];
|
||||
|
||||
if ( $is_new_task and ( !isset( $values['status'] ) or $values['status'] === null or $values['status'] === '' ) )
|
||||
return self::DEFAULT_NEW_TASK_STATUS;
|
||||
|
||||
return isset( $values['status'] ) ? (int)$values['status'] : 0;
|
||||
}
|
||||
|
||||
public static function taskChangeStatus()
|
||||
{
|
||||
global $mdb, $user;
|
||||
|
||||
$task_id = (int)\S::get( 'task_id' );
|
||||
$status = (int)\S::get( 'status' );
|
||||
|
||||
if ( $mdb -> update( 'tasks', [ 'status' => $status ], [ 'id' => $task_id ] ) )
|
||||
{
|
||||
if ( $user and self::shouldStopTimerOnStatus( $status ) )
|
||||
\factory\Tasks::task_end( $task_id, $user['id'] );
|
||||
|
||||
if ( self::shouldSendStatusChangeEmail( $status ) )
|
||||
self::sendEmailTaskChangeStatus( $task_id );
|
||||
|
||||
if ( $status === 2 )
|
||||
$mdb -> update( 'tasks', [ 'date_complete' => date( 'Y-m-d H:i:s' ) ], [ 'id' => $task_id ] );
|
||||
|
||||
echo json_encode( [ 'status' => 'success' ] );
|
||||
}
|
||||
else
|
||||
echo json_encode( [ 'status' => 'error' ] );
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function shouldStopTimerOnStatus( $status )
|
||||
{
|
||||
return in_array( (int)$status, [ 1, 2, 3 ], true );
|
||||
}
|
||||
|
||||
public static function shouldSendStatusChangeEmail( $status )
|
||||
{
|
||||
return in_array( (int)$status, [ 1, 3 ], true );
|
||||
}
|
||||
|
||||
public static function sendEmailTaskChangeStatus( $task_id )
|
||||
{
|
||||
$task = \factory\Tasks::task_details( $task_id );
|
||||
$statuses = \factory\Tasks::get_statuses();
|
||||
|
||||
if ( $task['status_change_mail'] )
|
||||
{
|
||||
\S::send_email(
|
||||
'biuro@project-pro.pl',
|
||||
'crmPRO - zmieniono status zadania',
|
||||
'<p>Witaj<br/>zmieniono status zadania <b>' . $task['name'] . ' - ' . \factory\Crm::get_client_name( (int)$task['client_id'] ) . '</b> na <b>' . $statuses[ $task['status'] ] . '</b>.</p>' .
|
||||
'<p>' . html_entity_decode( $task['text'] ) . '</p>' .
|
||||
'<p>Pozdrawiamy<br/>Zespół crmPRO.pl</p>'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
namespace Controllers;
|
||||
|
||||
class TasksController
|
||||
{
|
||||
public static function workTime()
|
||||
{
|
||||
$work_time_repository = new \Domain\Tasks\WorkTimeRepository();
|
||||
|
||||
$view_model = self::workTimeViewModel(
|
||||
$work_time_repository -> getClientsWithUnsettledTasks(),
|
||||
\factory\Crm::settings()
|
||||
);
|
||||
|
||||
return \Tpl::view( 'tasks/work-time', $view_model );
|
||||
}
|
||||
|
||||
public static function workTimeViewModel( array $work_time_clients, array $settings )
|
||||
{
|
||||
return [
|
||||
'work_time_clients' => $work_time_clients,
|
||||
'settings' => $settings
|
||||
];
|
||||
}
|
||||
}
|
||||
195
autoload/Domain/Tasks/TaskAttachmentRepository.php
Normal file
195
autoload/Domain/Tasks/TaskAttachmentRepository.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
namespace Domain\Tasks;
|
||||
|
||||
class TaskAttachmentRepository
|
||||
{
|
||||
private $mdb;
|
||||
private $upload_dir;
|
||||
private $upload_url;
|
||||
private $table_ready = false;
|
||||
|
||||
public function __construct( $mdb = null, $upload_dir = null, $upload_url = '/upload/task_attachments' )
|
||||
{
|
||||
if ( $mdb )
|
||||
$this -> mdb = $mdb;
|
||||
else
|
||||
{
|
||||
global $mdb;
|
||||
$this -> mdb = $mdb;
|
||||
}
|
||||
|
||||
$this -> upload_dir = $upload_dir ? rtrim( $upload_dir, '/\\' ) : getcwd() . DIRECTORY_SEPARATOR . 'upload' . DIRECTORY_SEPARATOR . 'task_attachments';
|
||||
$this -> upload_url = rtrim( $upload_url, '/' );
|
||||
}
|
||||
|
||||
public function listByTaskId( $task_id )
|
||||
{
|
||||
$this -> ensureStorage();
|
||||
$rows = $this -> mdb -> select( 'tasks_attachments', '*', [
|
||||
'AND' => [ 'task_id' => (int)$task_id, 'deleted' => 0 ],
|
||||
'ORDER' => [ 'id' => 'DESC' ]
|
||||
] );
|
||||
|
||||
if ( !is_array( $rows ) )
|
||||
return [];
|
||||
|
||||
foreach ( $rows as &$row )
|
||||
{
|
||||
$row['title_effective'] = self::effectiveTitle( $row['title'], $row['original_name'] );
|
||||
$row['url'] = $this -> buildPublicUrl( $row['relative_path'], $row['stored_name'] );
|
||||
$row['size_human'] = $this -> formatSize( (int)$row['file_size'] );
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function upload( $task_id, $user_id, array $file )
|
||||
{
|
||||
$this -> ensureStorage();
|
||||
|
||||
if ( !isset( $file['error'] ) or $file['error'] !== UPLOAD_ERR_OK )
|
||||
return [ 'status' => 'error', 'msg' => 'Nie udało się wgrać pliku.' ];
|
||||
|
||||
if ( !isset( $file['tmp_name'] ) or !is_uploaded_file( $file['tmp_name'] ) )
|
||||
return [ 'status' => 'error', 'msg' => 'Nieprawidłowy plik.' ];
|
||||
|
||||
$original_name = trim( (string)$file['name'] );
|
||||
if ( $original_name === '' )
|
||||
return [ 'status' => 'error', 'msg' => 'Brak nazwy pliku.' ];
|
||||
|
||||
$safe_original_name = self::sanitizeFileName( $original_name );
|
||||
$ext = strtolower( pathinfo( $safe_original_name, PATHINFO_EXTENSION ) );
|
||||
$stored_name = uniqid( 'att_', true ) . ( $ext ? '.' . $ext : '' );
|
||||
|
||||
$relative_path = date( 'Y' ) . '/' . date( 'm' );
|
||||
$target_dir = $this -> upload_dir . DIRECTORY_SEPARATOR . str_replace( '/', DIRECTORY_SEPARATOR, $relative_path );
|
||||
if ( !is_dir( $target_dir ) )
|
||||
@mkdir( $target_dir, 0777, true );
|
||||
|
||||
$target_file = $target_dir . DIRECTORY_SEPARATOR . $stored_name;
|
||||
if ( !move_uploaded_file( $file['tmp_name'], $target_file ) )
|
||||
return [ 'status' => 'error', 'msg' => 'Nie udało się zapisać pliku.' ];
|
||||
|
||||
$insert = $this -> mdb -> insert( 'tasks_attachments', [
|
||||
'task_id' => (int)$task_id,
|
||||
'user_id' => (int)$user_id,
|
||||
'title' => null,
|
||||
'original_name' => $safe_original_name,
|
||||
'stored_name' => $stored_name,
|
||||
'relative_path' => $relative_path,
|
||||
'file_ext' => $ext,
|
||||
'file_size' => isset( $file['size'] ) ? (int)$file['size'] : 0,
|
||||
'date_add' => date( 'Y-m-d H:i:s' ),
|
||||
'deleted' => 0
|
||||
] );
|
||||
|
||||
if ( !$insert )
|
||||
return [ 'status' => 'error', 'msg' => 'Nie udało się zapisać załącznika w bazie.' ];
|
||||
|
||||
return [ 'status' => 'success' ];
|
||||
}
|
||||
|
||||
public function rename( $attachment_id, $title )
|
||||
{
|
||||
$this -> ensureStorage();
|
||||
$title = trim( (string)$title );
|
||||
|
||||
return $this -> mdb -> update( 'tasks_attachments', [
|
||||
'title' => $title !== '' ? $title : null
|
||||
], [
|
||||
'AND' => [ 'id' => (int)$attachment_id, 'deleted' => 0 ]
|
||||
] );
|
||||
}
|
||||
|
||||
public function delete( $attachment_id, $deleted_by_user_id = null )
|
||||
{
|
||||
$this -> ensureStorage();
|
||||
|
||||
$attachment = $this -> mdb -> get( 'tasks_attachments', '*', [
|
||||
'AND' => [ 'id' => (int)$attachment_id, 'deleted' => 0 ]
|
||||
] );
|
||||
|
||||
if ( !$attachment )
|
||||
return false;
|
||||
|
||||
$file_path = $this -> upload_dir . DIRECTORY_SEPARATOR .
|
||||
str_replace( '/', DIRECTORY_SEPARATOR, $attachment['relative_path'] ) .
|
||||
DIRECTORY_SEPARATOR . $attachment['stored_name'];
|
||||
|
||||
if ( file_exists( $file_path ) )
|
||||
@unlink( $file_path );
|
||||
|
||||
return $this -> mdb -> update( 'tasks_attachments', [
|
||||
'deleted' => 1,
|
||||
'deleted_by' => $deleted_by_user_id ? (int)$deleted_by_user_id : null,
|
||||
'date_delete' => date( 'Y-m-d H:i:s' )
|
||||
], [ 'id' => (int)$attachment_id ] );
|
||||
}
|
||||
|
||||
public static function effectiveTitle( $title, $fallback )
|
||||
{
|
||||
$title = trim( (string)$title );
|
||||
return $title !== '' ? $title : (string)$fallback;
|
||||
}
|
||||
|
||||
public static function sanitizeFileName( $name )
|
||||
{
|
||||
$name = preg_replace( '/[^\p{L}\p{N}\s\.\-_]+/u', '_', (string)$name );
|
||||
$name = preg_replace( '/\s+/', '_', $name );
|
||||
$name = trim( $name, '._-' );
|
||||
return $name !== '' ? $name : 'zalacznik';
|
||||
}
|
||||
|
||||
private function ensureStorage()
|
||||
{
|
||||
if ( !is_dir( $this -> upload_dir ) )
|
||||
@mkdir( $this -> upload_dir, 0777, true );
|
||||
|
||||
if ( !$this -> table_ready )
|
||||
{
|
||||
$this -> ensureTable();
|
||||
$this -> table_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureTable()
|
||||
{
|
||||
$this -> mdb -> query(
|
||||
'CREATE TABLE IF NOT EXISTS `tasks_attachments` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`task_id` INT NOT NULL,
|
||||
`user_id` INT NOT NULL,
|
||||
`title` VARCHAR(255) NULL,
|
||||
`original_name` VARCHAR(255) NOT NULL,
|
||||
`stored_name` VARCHAR(255) NOT NULL,
|
||||
`relative_path` VARCHAR(255) NOT NULL,
|
||||
`file_ext` VARCHAR(20) NULL,
|
||||
`file_size` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`date_add` DATETIME NOT NULL,
|
||||
`deleted` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`deleted_by` INT NULL,
|
||||
`date_delete` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_tasks_attachments_task` (`task_id`),
|
||||
INDEX `idx_tasks_attachments_deleted` (`deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
|
||||
);
|
||||
}
|
||||
|
||||
private function buildPublicUrl( $relative_path, $stored_name )
|
||||
{
|
||||
return $this -> upload_url . '/' . trim( $relative_path, '/' ) . '/' . rawurlencode( $stored_name );
|
||||
}
|
||||
|
||||
private function formatSize( $bytes )
|
||||
{
|
||||
if ( $bytes < 1024 )
|
||||
return $bytes . ' B';
|
||||
|
||||
$kb = $bytes / 1024;
|
||||
if ( $kb < 1024 )
|
||||
return number_format( $kb, 1, '.', '' ) . ' KB';
|
||||
|
||||
return number_format( $kb / 1024, 1, '.', '' ) . ' MB';
|
||||
}
|
||||
}
|
||||
@@ -328,29 +328,12 @@ class Tasks
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use \Controllers\TasksController::taskChangeStatus() instead.
|
||||
*/
|
||||
static public function task_change_status()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
if ( $mdb -> update( 'tasks', [ 'status' => \S::get( 'status' ) ], [ 'id' => \S::get( 'task_id' ) ] ) )
|
||||
{
|
||||
if ( \S::get( 'status' ) == 3 or \S::get( 'status' ) == 1 )
|
||||
{
|
||||
self::send_email_task_change_status( \S::get( 'task_id' ) );
|
||||
}
|
||||
|
||||
if ( \S::get( 'status' ) == 2 )
|
||||
{
|
||||
$mdb -> update( 'tasks', [ 'date_complete' => date( 'Y-m-d H:i:s' ) ], [ 'id' => \S::get( 'task_id' ) ] );
|
||||
}
|
||||
|
||||
echo json_encode( [ 'status' => 'success' ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error' ] );
|
||||
}
|
||||
exit;
|
||||
return \Controllers\TasksController::taskChangeStatus();
|
||||
}
|
||||
|
||||
static public function task_end()
|
||||
@@ -385,17 +368,19 @@ class Tasks
|
||||
exit;
|
||||
}
|
||||
|
||||
$task = \factory\Tasks::task_details( \S::get( 'task_id' ) );
|
||||
$task['status'] = \Controllers\TasksController::resolveTaskStatusForForm( $task );
|
||||
|
||||
return \Tpl::view( 'tasks/task_edit', [
|
||||
'projects' => \factory\Projects::user_projects( $user['id'] ),
|
||||
'priorities' => \factory\Tasks::$priorities,
|
||||
'task' => \factory\Tasks::task_details( \S::get( 'task_id' ) ),
|
||||
'task' => $task,
|
||||
'parent_tasks' => \factory\Tasks::parent_tasks( $user['id'] ),
|
||||
'users' => \factory\Users::users_list(),
|
||||
'clients' => \factory\Crm::get_client_list(),
|
||||
'user' => $user
|
||||
] );
|
||||
}
|
||||
|
||||
static public function task_save()
|
||||
{
|
||||
global $user;
|
||||
@@ -408,9 +393,10 @@ class Tasks
|
||||
|
||||
$response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania zadania wystąpił błąd. Proszę spróbować ponownie.' ];
|
||||
$values = \S::json_to_array( \S::get( 'values' ) );
|
||||
$status = \Controllers\TasksController::resolveTaskStatusForSave( $values );
|
||||
|
||||
if ( $id = \factory\Tasks::task_save(
|
||||
$values['id'], null, $user['id'], $values['name'], $values['text'], $values['date_start'], $values['date_end'], $values['project_id'], $values['client_id'], $values['pay_rate'], $values['reminders_interval'], $values['recursively'], $values['frequency'], $values['period'], $values['users'], null, null, $values['send_email_notification'], $values['status_change_mail'], false, $values['status'], $values['show_in_calendar'], $values['priority']
|
||||
$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']
|
||||
) )
|
||||
{
|
||||
\factory\Tasks::clear_task_opened( $id );
|
||||
@@ -420,7 +406,6 @@ class Tasks
|
||||
echo json_encode( $response );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function task_popup()
|
||||
{
|
||||
global $user;
|
||||
@@ -431,11 +416,14 @@ class Tasks
|
||||
exit;
|
||||
}
|
||||
|
||||
$attachments_repository = new \Domain\Tasks\TaskAttachmentRepository();
|
||||
|
||||
\factory\Tasks::set_task_opened_by_user( \S::get( 'task_id' ), $user['id'] );
|
||||
|
||||
echo \Tpl::view( 'tasks/task_popup', [
|
||||
'task' => \factory\Tasks::task_details( \S::get( 'task_id' ), $user['id'] ),
|
||||
'task_works' => \factory\Tasks::task_works( \S::get( 'task_id' ) ),
|
||||
'task_attachments' => $attachments_repository -> listByTaskId( \S::get( 'task_id' ) ),
|
||||
'user' => $user,
|
||||
'statuses' => \factory\Tasks::get_statuses(),
|
||||
'projects' => \factory\Projects::user_projects( $user['id'] )
|
||||
@@ -443,6 +431,116 @@ class Tasks
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function task_attachment_upload()
|
||||
{
|
||||
global $user;
|
||||
|
||||
if ( !$user )
|
||||
{
|
||||
header( 'Location: /logowanie' );
|
||||
exit;
|
||||
}
|
||||
|
||||
$files_input = isset( $_FILES['attachments'] ) ? $_FILES['attachments'] : ( isset( $_FILES['attachment'] ) ? $_FILES['attachment'] : null );
|
||||
if ( !$files_input )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Nie przesłano plików.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$repository = new \Domain\Tasks\TaskAttachmentRepository();
|
||||
$files = self::normalize_uploads_array( $files_input );
|
||||
|
||||
if ( !count( $files ) )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Brak poprawnych plików do wysłania.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$failed = [];
|
||||
$success = 0;
|
||||
foreach ( $files as $file )
|
||||
{
|
||||
$result = $repository -> upload( \S::get( 'task_id' ), $user['id'], $file );
|
||||
if ( $result['status'] == 'success' )
|
||||
$success++;
|
||||
else
|
||||
$failed[] = isset( $file['name'] ) ? $file['name'] : 'plik';
|
||||
}
|
||||
|
||||
if ( $success and !count( $failed ) )
|
||||
echo json_encode( [ 'status' => 'success', 'msg' => 'Dodano załączniki.' ] );
|
||||
elseif ( $success and count( $failed ) )
|
||||
echo json_encode( [ 'status' => 'partial', 'msg' => 'Część plików dodano, część się nie powiodła.' ] );
|
||||
else
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Nie udało się dodać załączników.' ] );
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
static private function normalize_uploads_array( $files_input )
|
||||
{
|
||||
if ( !is_array( $files_input ) or !isset( $files_input['name'] ) )
|
||||
return [];
|
||||
|
||||
if ( !is_array( $files_input['name'] ) )
|
||||
return [ $files_input ];
|
||||
|
||||
$normalized = [];
|
||||
foreach ( $files_input['name'] as $i => $name )
|
||||
{
|
||||
$normalized[] = [
|
||||
'name' => $name,
|
||||
'type' => isset( $files_input['type'][$i] ) ? $files_input['type'][$i] : null,
|
||||
'tmp_name' => isset( $files_input['tmp_name'][$i] ) ? $files_input['tmp_name'][$i] : null,
|
||||
'error' => isset( $files_input['error'][$i] ) ? $files_input['error'][$i] : UPLOAD_ERR_NO_FILE,
|
||||
'size' => isset( $files_input['size'][$i] ) ? $files_input['size'][$i] : 0
|
||||
];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
static public function task_attachment_delete()
|
||||
{
|
||||
global $user;
|
||||
|
||||
if ( !$user )
|
||||
{
|
||||
header( 'Location: /logowanie' );
|
||||
exit;
|
||||
}
|
||||
|
||||
$repository = new \Domain\Tasks\TaskAttachmentRepository();
|
||||
$result = $repository -> delete( \S::get( 'attachment_id' ), $user['id'] );
|
||||
|
||||
if ( $result )
|
||||
echo json_encode( [ 'status' => 'success' ] );
|
||||
else
|
||||
echo json_encode( [ 'status' => 'error' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function task_attachment_rename()
|
||||
{
|
||||
global $user;
|
||||
|
||||
if ( !$user )
|
||||
{
|
||||
header( 'Location: /logowanie' );
|
||||
exit;
|
||||
}
|
||||
|
||||
$repository = new \Domain\Tasks\TaskAttachmentRepository();
|
||||
$result = $repository -> rename( \S::get( 'attachment_id' ), \S::get( 'title' ) );
|
||||
|
||||
if ( $result )
|
||||
echo json_encode( [ 'status' => 'success' ] );
|
||||
else
|
||||
echo json_encode( [ 'status' => 'error' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function filtr_save_form() {
|
||||
echo json_encode( [
|
||||
'status' => 'success',
|
||||
@@ -545,3 +643,4 @@ class Tasks
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\' , $classname );
|
||||
$c = array_pop( $q );
|
||||
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
||||
$base_path = 'autoload/' . implode( '/' , $q ) . '/';
|
||||
$f = $base_path . $c . '.php';
|
||||
|
||||
if ( !file_exists( $f ) )
|
||||
$f = $base_path . 'class.' . $c . '.php';
|
||||
|
||||
if ( file_exists( $f ) )
|
||||
require_once( $f );
|
||||
@@ -150,4 +154,4 @@ function memory_get_process_usage()
|
||||
// }
|
||||
|
||||
// fclose($file);
|
||||
?>
|
||||
?>
|
||||
|
||||
6
cron.php
6
cron.php
@@ -5,7 +5,11 @@ function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\', $classname );
|
||||
$c = array_pop( $q );
|
||||
$f = 'autoload/' . implode( '/', $q ) . '/class.' . $c . '.php';
|
||||
$base_path = 'autoload/' . implode( '/', $q ) . '/';
|
||||
$f = $base_path . $c . '.php';
|
||||
|
||||
if ( !file_exists( $f ) )
|
||||
$f = $base_path . 'class.' . $c . '.php';
|
||||
|
||||
if ( file_exists( $f ) )
|
||||
require_once( $f );
|
||||
|
||||
@@ -4,7 +4,10 @@ function __autoload_my_classes($classname)
|
||||
{
|
||||
$q = explode('\\', $classname);
|
||||
$c = array_pop($q);
|
||||
$f = 'autoload/' . implode('/', $q) . '/class.' . $c . '.php';
|
||||
$base_path = 'autoload/' . implode('/', $q) . '/';
|
||||
$f = $base_path . $c . '.php';
|
||||
if ( !file_exists( $f ) )
|
||||
$f = $base_path . 'class.' . $c . '.php';
|
||||
|
||||
if (file_exists($f)) {
|
||||
require_once($f);
|
||||
@@ -72,4 +75,4 @@ if ( !$user and !in_array( $_SERVER['REQUEST_URI'], [ '/logowanie', '/rejestracj
|
||||
exit;
|
||||
}
|
||||
|
||||
echo \view\Site::show();
|
||||
echo \view\Site::show();
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -122,7 +122,7 @@ if ( is_array( $this -> parent_tasks ) )
|
||||
'label' => 'Status',
|
||||
'name' => 'status',
|
||||
'id' => 'status',
|
||||
'value' => $this -> task[ 'status' ],
|
||||
'value' => $this -> task[ 'id' ] ? $this -> task[ 'status' ] : 5,
|
||||
'values' => \factory\Tasks::get_statuses()
|
||||
] );?>
|
||||
<!-- priorytet -->
|
||||
@@ -157,7 +157,7 @@ if ( is_array( $this -> priorities ) )
|
||||
'label' => 'Powiadom o zmianie statusu',
|
||||
'name' => 'status_change_mail',
|
||||
'id' => 'status_change_mail',
|
||||
'checked' => ( $this -> task[ 'status_change_mail' ] or !$this -> taks['id'] ) ? true : false,
|
||||
'checked' => ( $this -> task[ 'status_change_mail' ] or !$this -> task['id'] ) ? true : false,
|
||||
'type' => 'checkbox'
|
||||
] );
|
||||
?>
|
||||
@@ -191,11 +191,11 @@ if ( is_array( $this -> priorities ) )
|
||||
'type' => 'checkbox'
|
||||
] );
|
||||
?>
|
||||
<div class="form_group" id="recursive-details">
|
||||
<div class="form_group" id="recursive-details" style="<?= $this -> task[ 'recursively' ] ? '' : 'display: none;';?>">
|
||||
<label class="label">Powtarzaj co:</label>
|
||||
<div class="input">
|
||||
<input type="text" class="form-control" name="frequency" value="<?= $this -> task[ 'id' ] ? $this -> task[ 'frequency' ] : 1;?>">
|
||||
<select name="period" class="form-control">
|
||||
<div class="input" style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="text" class="form-control" name="frequency" style="max-width: 110px;" value="<?= $this -> task[ 'id' ] ? $this -> task[ 'frequency' ] : 1;?>">
|
||||
<select name="period" class="form-control" style="max-width: 140px;">
|
||||
<option value="1" <? if ( $this -> task[ 'period' ] == '1' ):?>selected="selected"<? endif;?>>dni</option>
|
||||
<option value="2" <? if ( $this -> task[ 'period' ] == '2' ):?>selected="selected"<? endif;?>>m-ce</option>
|
||||
<option value="3" <? if ( $this -> task[ 'period' ] == '3' ):?>selected="selected"<? endif;?>>lata</option>
|
||||
@@ -216,7 +216,7 @@ if ( is_array( $this -> priorities ) )
|
||||
'label' => 'Pokaż w kalendarzu',
|
||||
'name' => 'show_in_calendar',
|
||||
'id' => 'show_in_calendar',
|
||||
'checked' => ( $this -> task[ 'show_in_calendar' ] or !$this -> taks['id'] ) ? true : false,
|
||||
'checked' => ( $this -> task[ 'show_in_calendar' ] or !$this -> task['id'] ) ? true : false,
|
||||
'type' => 'checkbox'
|
||||
] );
|
||||
?>
|
||||
@@ -262,11 +262,24 @@ echo $grid -> draw();
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function ()
|
||||
{
|
||||
function toggleRecursiveDetails() {
|
||||
if ( $( '#recursively' ).is( ':checked' ) )
|
||||
$( '#recursive-details' ).show();
|
||||
else
|
||||
$( '#recursive-details' ).hide();
|
||||
}
|
||||
|
||||
$('input[type="checkbox"]').iCheck({
|
||||
checkboxClass: 'icheckbox_square-blue',
|
||||
radioClass: 'iradio_square-blue',
|
||||
});
|
||||
|
||||
toggleRecursiveDetails();
|
||||
|
||||
$( '#recursively' ).on( 'ifChanged change', function() {
|
||||
toggleRecursiveDetails();
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '.task-remove', function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
@@ -323,7 +336,19 @@ echo $grid -> draw();
|
||||
});
|
||||
|
||||
$( '#project_id, #client_id, #status' ).select2({
|
||||
theme: 'bootstrap-5'
|
||||
theme: 'bootstrap-5',
|
||||
minimumResultsForSearch: 0
|
||||
});
|
||||
|
||||
$( '#project_id, #client_id, #status' ).on( 'select2:open', function() {
|
||||
setTimeout( function() {
|
||||
var search_field = document.querySelector( '.select2-container--open .select2-search__field' );
|
||||
if ( search_field )
|
||||
{
|
||||
search_field.focus();
|
||||
search_field.select();
|
||||
}
|
||||
}, 0 );
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -4,8 +4,16 @@ $format_time = function( $seconds ) {
|
||||
return sprintf( "%02d%s%02d%s%02d", floor( $seconds / 3600 ), ':', ( $seconds / 60 ) % 60, ':', $seconds % 60 );
|
||||
};
|
||||
|
||||
$round_time_to_quarter = function( $seconds ) {
|
||||
$seconds = (int)$seconds;
|
||||
if ( $seconds <= 0 )
|
||||
return 0;
|
||||
|
||||
return (int)( round( $seconds / 900 ) * 900 );
|
||||
};
|
||||
|
||||
$format_amount = function( $amount ) {
|
||||
return number_format( (float)$amount, 2, '.', '' ) . ' zł';
|
||||
return number_format( (float)$amount, 0, '.', '' ) . ' zł';
|
||||
};
|
||||
|
||||
$billing_clients = [];
|
||||
@@ -37,11 +45,12 @@ foreach ( $this -> work_time_clients as $client )
|
||||
foreach ( $tasks as $task )
|
||||
{
|
||||
$task_time = isset( $task['time'] ) ? (int)$task['time'] : 0;
|
||||
$task_time = $round_time_to_quarter( $task_time );
|
||||
|
||||
if ( isset( $task['pay_rate'] ) and $task['pay_rate'] !== null and $task['pay_rate'] !== '' )
|
||||
$task_amount = (float)$task['pay_rate'];
|
||||
$task_amount = round( (float)$task['pay_rate'] );
|
||||
else
|
||||
$task_amount = (float)$this -> settings['hourly_rate'] * ( $task_time / 3600 );
|
||||
$task_amount = round( (float)$this -> settings['hourly_rate'] * ( $task_time / 3600 ) );
|
||||
|
||||
$summary['tasks_count']++;
|
||||
$summary['time'] += $task_time;
|
||||
@@ -122,7 +131,7 @@ usort( $billing_clients, function( $a, $b ) {
|
||||
<tbody>
|
||||
<? foreach ( $billing_clients as $summary ):?>
|
||||
<? $details_id = 'billing-details-' . md5( $summary['firm'] );?>
|
||||
<tr class="billing-client-row" data-details-id="<?= $details_id;?>" data-tasks-count="<?= $summary['tasks_count'];?>" data-time="<?= $summary['time'];?>" data-amount="<?= number_format( (float)$summary['amount'], 2, '.', '' );?>">
|
||||
<tr class="billing-client-row" data-details-id="<?= $details_id;?>" data-tasks-count="<?= $summary['tasks_count'];?>" data-time="<?= $summary['time'];?>" data-amount="<?= number_format( (float)$summary['amount'], 0, '.', '' );?>">
|
||||
<td class="billing-client-name"><a href="#<?= $summary['firm'];?>"><?= $summary['firm'];?></a></td>
|
||||
<td class="text-center billing-client-tasks"><?= $summary['tasks_count'];?></td>
|
||||
<td class="text-center billing-client-time"><?= $format_time( $summary['time'] );?></td>
|
||||
@@ -145,7 +154,7 @@ usort( $billing_clients, function( $a, $b ) {
|
||||
</thead>
|
||||
<tbody>
|
||||
<? foreach ( $summary['rows'] as $row ):?>
|
||||
<tr class="billing-task-row" data-task-time="<?= (int)$row['time'];?>" data-task-amount="<?= number_format( (float)$row['amount'], 2, '.', '' );?>">
|
||||
<tr class="billing-task-row" data-task-time="<?= (int)$row['time'];?>" data-task-amount="<?= number_format( (float)$row['amount'], 0, '.', '' );?>">
|
||||
<td><?= $row['month'];?></td>
|
||||
<td><?= $row['name'];?></td>
|
||||
<td class="text-center"><?= $format_time( $row['time'] );?></td>
|
||||
@@ -184,7 +193,7 @@ usort( $billing_clients, function( $a, $b ) {
|
||||
|
||||
function formatAmount( amount ) {
|
||||
amount = parseFloat( amount ) || 0;
|
||||
return amount.toFixed( 2 ) + ' z\u0142';
|
||||
return Math.round( amount ) + ' z\u0142';
|
||||
}
|
||||
|
||||
function refreshGlobalKpis() {
|
||||
@@ -311,7 +320,7 @@ usort( $billing_clients, function( $a, $b ) {
|
||||
{
|
||||
summary_row.attr( 'data-tasks-count', client_tasks_count );
|
||||
summary_row.attr( 'data-time', client_time );
|
||||
summary_row.attr( 'data-amount', client_amount.toFixed( 2 ) );
|
||||
summary_row.attr( 'data-amount', Math.round( client_amount ) );
|
||||
summary_row.find( '.billing-client-tasks' ).text( client_tasks_count );
|
||||
summary_row.find( '.billing-client-time' ).text( formatTime( client_time ) );
|
||||
summary_row.find( '.billing-client-amount' ).text( formatAmount( client_amount ) );
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../autoload/Controllers/class.TasksController.php';
|
||||
require_once __DIR__ . '/../../autoload/Controllers/TasksController.php';
|
||||
|
||||
use Controllers\TasksController;
|
||||
|
||||
@@ -21,4 +21,19 @@ function run_tasks_controller_tests()
|
||||
|
||||
assert_same( $clients, $view_model['work_time_clients'], 'Expected work_time_clients to be passed through.' );
|
||||
assert_same( $settings, $view_model['settings'], 'Expected settings to be passed through.' );
|
||||
|
||||
assert_same( 5, TasksController::resolveTaskStatusForForm( [ 'id' => null, 'status' => null ] ), 'Expected default form status for new task to be 5 (do zrobienia).' );
|
||||
assert_same( 1, TasksController::resolveTaskStatusForForm( [ 'id' => 10, 'status' => 1 ] ), 'Expected existing task form status to be preserved.' );
|
||||
|
||||
assert_same( 5, TasksController::resolveTaskStatusForSave( [ 'id' => null ] ), 'Expected default save status for new task to be 5 (do zrobienia).' );
|
||||
assert_same( 3, TasksController::resolveTaskStatusForSave( [ 'id' => 22, 'status' => 3 ] ), 'Expected existing task save status to be preserved.' );
|
||||
|
||||
assert_same( true, TasksController::shouldStopTimerOnStatus( 1 ), 'Expected timer stop on status do sprawdzenia.' );
|
||||
assert_same( true, TasksController::shouldStopTimerOnStatus( 2 ), 'Expected timer stop on status zamkniete.' );
|
||||
assert_same( true, TasksController::shouldStopTimerOnStatus( 3 ), 'Expected timer stop on status do rozliczenia.' );
|
||||
assert_same( false, TasksController::shouldStopTimerOnStatus( 0 ), 'Expected no timer stop on status nowe.' );
|
||||
|
||||
assert_same( true, TasksController::shouldSendStatusChangeEmail( 1 ), 'Expected status email for do sprawdzenia.' );
|
||||
assert_same( true, TasksController::shouldSendStatusChangeEmail( 3 ), 'Expected status email for do rozliczenia.' );
|
||||
assert_same( false, TasksController::shouldSendStatusChangeEmail( 2 ), 'Expected no status email for zamkniete.' );
|
||||
}
|
||||
|
||||
28
tests/Domain/Tasks/TaskAttachmentRepositoryTest.php
Normal file
28
tests/Domain/Tasks/TaskAttachmentRepositoryTest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../../autoload/Domain/Tasks/TaskAttachmentRepository.php';
|
||||
|
||||
use Domain\Tasks\TaskAttachmentRepository;
|
||||
|
||||
function run_task_attachment_repository_tests()
|
||||
{
|
||||
assert_true(
|
||||
TaskAttachmentRepository::effectiveTitle( ' Raport ', 'plik.pdf' ) === 'Raport',
|
||||
'Expected effective title to prefer trimmed custom title.'
|
||||
);
|
||||
|
||||
assert_true(
|
||||
TaskAttachmentRepository::effectiveTitle( '', 'plik.pdf' ) === 'plik.pdf',
|
||||
'Expected effective title to fallback to original file name.'
|
||||
);
|
||||
|
||||
$sanitized = TaskAttachmentRepository::sanitizeFileName( 'Załącznik #1 (final).pdf' );
|
||||
assert_true(
|
||||
strpos( $sanitized, '#' ) === false
|
||||
&& strpos( $sanitized, '(' ) === false
|
||||
&& strpos( $sanitized, ')' ) === false
|
||||
&& strpos( $sanitized, ' ' ) === false
|
||||
&& substr( $sanitized, -4 ) === '.pdf',
|
||||
'Expected sanitized file name to remove unsupported characters and keep extension.'
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../../autoload/Domain/Tasks/class.WorkTimeRepository.php';
|
||||
require_once __DIR__ . '/../../../autoload/Domain/Tasks/WorkTimeRepository.php';
|
||||
|
||||
use Domain\Tasks\WorkTimeRepository;
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/Domain/Tasks/WorkTimeRepositoryTest.php';
|
||||
require_once __DIR__ . '/Domain/Tasks/TaskAttachmentRepositoryTest.php';
|
||||
require_once __DIR__ . '/Controllers/TasksControllerTest.php';
|
||||
|
||||
$tests = [
|
||||
'run_work_time_repository_tests',
|
||||
'run_task_attachment_repository_tests',
|
||||
'run_tasks_controller_tests'
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user