tasks: improve attachments UX and update refactoring docs

This commit is contained in:
2026-02-06 23:11:14 +01:00
parent 5161d0f979
commit 1722f171bc
4 changed files with 387 additions and 30 deletions

View File

@@ -11,7 +11,7 @@
- Obszar Czas pracy: zmigrowany i ustabilizowany
## Etap 1: Tasks / Czas pracy (ZROBIONE)
- Dodano `autoload/Domain/Tasks/class.WorkTimeRepository.php`.
- Dodano `autoload/Domain/Tasks/WorkTimeRepository.php`.
- `factory\Tasks::work_time_clients()` deleguje teraz do repozytorium Domain.
- Usunięto limit 3 miesięcy: repozytorium zwraca wszystkie istotne miesiące.
- Uwzględniono statusy zadań:
@@ -23,7 +23,7 @@
- `tests/run.php`
## Etap 2: Standaryzacja kontrolerów (W TOKU)
- [x] Dodano `autoload/Controllers/class.TasksController.php`.
- [x] Dodano `autoload/Controllers/TasksController.php`.
- [x] Przeniesiono akcję czasu pracy do nowego kontrolera: `TasksController::workTime()`.
- [x] Zostawiono adapter kompatybilności w `autoload/controls/class.Tasks.php`.
- [x] Oznaczono starą metodę `controls\Tasks::work_time()` jako deprecated.
@@ -63,3 +63,9 @@
- Bez zmian typu big-bang.
- Jeden ograniczony obszar funkcjonalny na commit.
- Najpierw kompatybilność, adaptery usuwać dopiero po pełnej migracji.
## Ostatnie wdrozenia (2026-02-06)
- Popup zadania: dodano zarzadzanie zalacznikami (dodawanie, edycja nazwy, usuwanie).
- Upload zalacznikow: obsluga wielu plikow w jednym wyslaniu (attachments[] + multiple).
- UX uploadu: dodano loader na przycisku ("Wysylanie..."), blokade wielokliku i odblokowanie po zakonczeniu requestu.
- Poprawiono krytyczny blad JS: dodano brakujaca funkcje is_task_popup_works_time_open().
- Ujednolicono napisy UI dla popupu i listy zadan (usuniecie "krzaczkow" przez encje HTML tam, gdzie to potrzebne).

View File

@@ -939,6 +939,127 @@ body>.top {
}
}
.attachments {
margin-top: 10px;
border: 1px solid #e6e9ed;
border-radius: 8px;
padding: 12px;
background: #f9fbfd;
h3 {
margin-bottom: 10px;
font-size: 16px;
font-weight: 600;
}
.attachments_upload {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
.attachment_file_input {
flex: 1;
margin-bottom: 0;
border-radius: 6px;
background: #fff;
}
.attachment-upload-btn {
white-space: nowrap;
border-radius: 6px;
min-width: 150px;
&.is-loading {
pointer-events: none;
opacity: 0.85;
i {
margin-right: 6px;
}
}
}
}
.attachments_list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 8px;
max-height: 180px;
overflow-y: auto;
li {
display: flex;
align-items: center;
gap: 8px;
background: #fff;
border: 1px solid #e4e8ee;
border-radius: 6px;
padding: 8px 10px;
.attachment-link {
color: #1f3d72;
text-decoration: none;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100% - 140px);
&:hover {
text-decoration: underline;
}
}
small {
color: #6b7280;
margin-right: auto;
}
.attachment-rename,
.attachment-delete {
display: inline-flex;
align-items: center;
justify-content: center;
height: 28px;
width: 28px;
border-radius: 4px;
text-decoration: none;
transition: all 0.2s ease;
}
.attachment-rename {
border: 1px solid #d8e2f6;
color: $cBlue;
background: #f4f8ff;
&:hover {
background: #e7f0ff;
}
}
.attachment-delete {
border: 1px solid #f1d3d1;
color: $cRed;
background: #fff6f6;
&:hover {
background: #ffeaea;
}
}
}
.attachments-empty {
color: #6b7280;
border-style: dashed;
justify-content: center;
font-size: 13px;
}
}
}
.description {
padding: 15px;
border-radius: 0.25rem;
@@ -1579,4 +1700,4 @@ body>.top {
.billing-details-wrap {
background: #f8fbff;
}
}
}

View File

@@ -10,7 +10,7 @@
<a href="#" class="btn btn-success btn_small" id="_new_filtr">zapisz</a>
<a href="#" class="btn btn-primary btn_small" id="_update_filtr">aktualizuj</a>
<!-- set default -->
<a href="#" class="btn btn-dark btn_small" id="_set_default_filtr">domyślny</a>
<a href="#" class="btn btn-dark btn_small" id="_set_default_filtr">domy&#347;lny</a>
</div>
<div class="_projects">
<h4>Projekty</h4>
@@ -25,7 +25,7 @@
</div>
<? if ( $this -> user['id'] == 1 ):?>
<div class="_users">
<h4>Użytkownicy</h4>
<h4>U&#380;ytkownicy</h4>
<? foreach ( $this -> users as $user ):?>
<div class="_user">
<label for="user_<?= $user[ 'id' ];?>">
@@ -79,7 +79,7 @@
</ul>
</div>
<div class="column tasks_to_review">
<h2>Zadania do sprawdzenia <? if ( $this -> show_tasks_to_review == 'hide' ):?><i class="fa fa-eye" title="Pokaż zadania do sprawdzenia"></i><? else:?><i class="fa fa-times" title="Ukryj zadania do sprawdzenia"></i><? endif;?></h2>
<h2>Zadania do sprawdzenia <? if ( $this -> show_tasks_to_review == 'hide' ):?><i class="fa fa-eye" title="Poka&#380; zadania do sprawdzenia"></i><? else:?><i class="fa fa-times" title="Ukryj zadania do sprawdzenia"></i><? endif;?></h2>
<ul>
<? foreach ( $this -> tasks_to_review as $task ):?>
<?
@@ -94,7 +94,7 @@
</ul>
</div>
<div class="column tasks_bulk">
<h2>Do rozliczenia <? if ( $this -> show_tasks_bulk == 'hide' ):?><i class="fa fa-eye" title="Pokaż zadania do rozliczenia"></i><? else:?><i class="fa fa-times" title="Ukryj zadania do rozliczenia"></i><? endif;?></h2>
<h2>Do rozliczenia <? if ( $this -> show_tasks_bulk == 'hide' ):?><i class="fa fa-eye" title="Poka&#380; zadania do rozliczenia"></i><? else:?><i class="fa fa-times" title="Ukryj zadania do rozliczenia"></i><? endif;?></h2>
<ul>
<? foreach ( $this -> tasks_bulk as $task ):?>
<?
@@ -109,7 +109,7 @@
</ul>
</div>
<div class="column tasks_closed">
<h2>Zamknięte zadania <? if ( $this -> show_tasks_closed == 'hide' ):?><i class="fa fa-eye" title="Pokaż zamknięte zadania"></i><? else:?><i class="fa fa-times" title="Ukryj zamknięte zadania"></i><? endif;?></h2>
<h2>Zamkni&#281;te zadania <? if ( $this -> show_tasks_closed == 'hide' ):?><i class="fa fa-eye" title="Poka&#380; zamkni&#281;te zadania"></i><? else:?><i class="fa fa-times" title="Ukryj zamkni&#281;te zadania"></i><? endif;?></h2>
<ul>
<? foreach ( $this -> tasks_closed as $task ):?>
<?
@@ -158,7 +158,7 @@
{
start: new Date().toISOString().split('T')[0],
end: new Date().toISOString().split('T')[0],
name: "Brak zadań do wyświetlenia",
name: "Brak zada&#324; do wy&#347;wietlenia",
id: "0",
progress: 100,
custom_class: "gantt-task-empty",
@@ -237,6 +237,10 @@
});
}
function is_task_popup_works_time_open() {
return $( '.task_popup .task_details' ).hasClass( 'open_works_time' );
}
function task_popup( task_id, open_works_time = false ) {
$.ajax({
@@ -289,7 +293,7 @@
success: function( response ) {
var data = jQuery.parseJSON( response );
if ( data.status == 'success' ) {
show_default_popup( 'Filtr został ustawiony jako domyślny' );
show_default_popup( 'Filtr zosta&#322; ustawiony jako domy&#347;lny' );
// autoclose popup after 2 seconds
setTimeout( function() {
$( '.default_popup .close' ).click();
@@ -716,8 +720,8 @@
var comment_id = $( this ).attr( 'comment_id' );
$.confirm({
title: 'Potwierdź',
content: 'Na pewno chcesz usunąć wybrany komentarz?',
title: 'Potwierd&#378;',
content: 'Na pewno chcesz usun&#261;&#263; wybrany komentarz?',
type: 'orange',
closeIcon: true,
closeIconClass: 'fa fa-close',
@@ -728,7 +732,7 @@
theme: 'material',
buttons: {
confirm: {
text: 'Usuń',
text: 'Usu&#324;',
btnClass: 'btn-red',
action: function(){
$.ajax({
@@ -755,13 +759,75 @@
});
});
$( 'body' ).on( 'click', '.task_popup .checklist li .point-delete', function(e){
$( 'body' ).on( 'click', '.task_popup .attachment-upload-btn', function(e){
e.preventDefault();
var action_id = $( this ).attr( 'action_id' );
var button = $( this );
if ( button.hasClass( 'is-loading' ) ) {
return;
}
var task_id = button.attr( 'task_id' );
var input = button.closest( '.attachments_upload' ).find( '.attachment_file_input' );
var files = input.get( 0 ).files;
var open_works_time = is_task_popup_works_time_open();
var original_button_html = button.html();
if ( !files || !files.length ) {
$.alert( 'Najpierw wybierz pliki.' );
return;
}
button
.addClass( 'is-loading disabled' )
.attr( 'aria-disabled', 'true' )
.html( '<i class="fa fa-spinner fa-spin"></i> Wysy&#322;anie...' );
input.prop( 'disabled', true );
var formData = new FormData();
formData.append( 'task_id', task_id );
for ( var i = 0; i < files.length; i++ ) {
formData.append( 'attachments[]', files[i] );
}
$.ajax({
type: 'POST',
cache: false,
url: '/tasks/task_attachment_upload/',
data: formData,
processData: false,
contentType: false,
success: function( response ) {
var data = jQuery.parseJSON( response );
if ( data.status == 'success' || data.status == 'partial' ) {
if ( data.status == 'partial' && data.msg ) {
$.alert( data.msg );
}
task_popup( task_id, open_works_time );
} else {
$.alert( data.msg ? data.msg : 'Nie uda&#322;o si&#281; doda&#263; za&#322;&#261;cznik&#243;w.' );
}
},
complete: function() {
button
.removeClass( 'is-loading disabled' )
.removeAttr( 'aria-disabled' )
.html( original_button_html );
input.prop( 'disabled', false );
}
});
});
$( 'body' ).on( 'click', '.task_popup .attachment-delete', function(e){
e.preventDefault();
var attachment_id = $( this ).attr( 'attachment_id' );
var task_id = $( '.task_popup .task_details' ).attr( 'task_id' );
var open_works_time = is_task_popup_works_time_open();
$.confirm({
title: 'Potwierdź',
content: 'Na pewno chcesz usunąć wybrany punkt?',
title: 'Potwierd&#378;',
content: 'Na pewno chcesz usunac zalacznik?',
type: 'orange',
closeIcon: true,
closeIconClass: 'fa fa-close',
@@ -772,7 +838,106 @@
theme: 'material',
buttons: {
confirm: {
text: 'Usuń',
text: 'Usu&#324;',
btnClass: 'btn-red',
action: function() {
$.ajax({
type: 'POST',
cache: false,
url: '/tasks/task_attachment_delete/',
data: {
attachment_id: attachment_id
},
success: function( response ) {
var data = jQuery.parseJSON( response );
if ( data.status == 'success' ) {
task_popup( task_id, open_works_time );
}
}
});
}
},
cancel: {
text: 'Anuluj',
btnClass: 'btn-default',
action: function() {}
}
}
});
});
$( 'body' ).on( 'click', '.task_popup .attachment-rename', function(e){
e.preventDefault();
var attachment_id = $( this ).attr( 'attachment_id' );
var current_title = $( this ).attr( 'title_current' );
var task_id = $( '.task_popup .task_details' ).attr( 'task_id' );
var open_works_time = is_task_popup_works_time_open();
$.confirm({
title: 'Zmien nazwe zalacznika',
content: ''
+ '<form action=\"\" class=\"formName\">'
+ '<div class=\"form-group\">'
+ '<input type=\"text\" class=\"attachment-new-title form-control\" value=\"' + $('<div>').text(current_title).html() + '\" />'
+ '</div>'
+ '</form>',
buttons: {
formSubmit: {
text: 'Zapisz',
btnClass: 'btn-blue',
action: function () {
var new_title = this.$content.find( '.attachment-new-title' ).val();
$.ajax({
type: 'POST',
cache: false,
url: '/tasks/task_attachment_rename/',
data: {
attachment_id: attachment_id,
title: new_title
},
success: function( response ) {
var data = jQuery.parseJSON( response );
if ( data.status == 'success' ) {
task_popup( task_id, open_works_time );
}
}
});
}
},
cancel: {
text: 'Anuluj',
action: function () {}
}
},
onContentReady: function () {
var jc = this;
this.$content.find( 'form' ).on( 'submit', function (e) {
e.preventDefault();
jc.$$formSubmit.trigger( 'click' );
});
}
});
});
$( 'body' ).on( 'click', '.task_popup .checklist li .point-delete', function(e){
e.preventDefault();
var action_id = $( this ).attr( 'action_id' );
$.confirm({
title: 'Potwierd&#378;',
content: 'Na pewno chcesz usun&#261;&#263; wybrany punkt?',
type: 'orange',
closeIcon: true,
closeIconClass: 'fa fa-close',
typeAnimated: true,
animation: 'opacity',
boxWidth: '500px',
useBootstrap: false,
theme: 'material',
buttons: {
confirm: {
text: 'Usu&#324;',
btnClass: 'btn-red',
action: function(){
$.ajax({
@@ -901,8 +1066,8 @@
var task_id = $( this ).attr( 'task_id' );
$.confirm({
title: 'Potwierdź',
content: 'Na pewno chcesz usunąć wybrane zadanie?',
title: 'Potwierd&#378;',
content: 'Na pewno chcesz usun&#261;&#263; wybrane zadanie?',
type: 'orange',
closeIcon: true,
closeIconClass: 'fa fa-close',
@@ -913,7 +1078,7 @@
theme: 'material',
buttons: {
confirm: {
text: 'Usuń',
text: 'Usu&#324;',
btnClass: 'btn-red',
action: function(){
$.ajax({
@@ -1060,8 +1225,8 @@
var task_id = $( this ).attr( 'task_id' );
$.confirm({
title: 'Potwierdź',
content: 'Na pewno chcesz usunąć wybrane zadanie?',
title: 'Potwierd&#378;',
content: 'Na pewno chcesz usun&#261;&#263; wybrane zadanie?',
type: 'orange',
closeIcon: true,
closeIconClass: 'fa fa-close',
@@ -1072,7 +1237,7 @@
theme: 'material',
buttons: {
confirm: {
text: 'Usuń',
text: 'Usu&#324;',
btnClass: 'btn-red',
action: function(){
$.ajax({
@@ -1127,4 +1292,4 @@
}
});
})
</script>
</script>

View File

@@ -63,6 +63,29 @@
<? endforeach;?>
</ul>
</div>
<div class="attachments box">
<h3>Za&#322;&#261;czniki</h3>
<div class="attachments_upload">
<input type="file" class="form-control attachment_file_input" name="attachments[]" multiple>
<a href="#" class="attachment-upload-btn btn btn-primary btn-sm" task_id="<?= $this -> task['id'];?>">Dodaj za&#322;&#261;czniki</a>
</div>
<ul class="attachments_list">
<? if ( is_array( $this -> task_attachments ) and count( $this -> task_attachments ) ):?>
<? foreach ( $this -> task_attachments as $attachment ):?>
<li>
<a href="<?= $attachment['url'];?>" target="_blank" rel="noopener noreferrer" class="attachment-link">
<?= htmlspecialchars( $attachment['title_effective'] );?>
</a>
<small>(<?= $attachment['size_human'];?>)</small>
<a href="#" class="attachment-rename" attachment_id="<?= $attachment['id'];?>" title_current="<?= htmlspecialchars( $attachment['title_effective'] );?>"><i class="fa fa-pencil"></i></a>
<a href="#" class="attachment-delete" attachment_id="<?= $attachment['id'];?>"><i class="fa fa-trash"></i></a>
</li>
<? endforeach;?>
<? else:?>
<li class="attachments-empty">Brak za&#322;&#261;cznik&#243;w.</li>
<? endif;?>
</ul>
</div>
</div>
<div class="right">
<div class="status box">
@@ -113,16 +136,16 @@
</div>
<div class="time box">
<h3>Przepracowany czas</h3>
<div class="time_worked">
<a href="#" class="time_worked_toggle">
<div class="time_worked" data-total-seconds="<?= (int)$this -> task['total_time'];?>">
<a href="#" class="time_worked_toggle js-time-worked-value">
<?= sprintf( "%02d%s%02d%s%02d", floor( $this -> task['total_time'] / 3600 ), ':', ( $this -> task['total_time'] / 60) % 60, ':', $this -> task['total_time'] % 60 );?>
</a>
</div>
<a href="#" class="task_start <? if ( $this -> task['is_open'] ):?> hidden<? endif;?>" task_id="<?= $this -> task['id'];?>">
<i class="fa fa-play"></i> Włącz timer
<i class="fa fa-play"></i> W&#322;&#261;cz timer
</a>
<a href="#" class="task_end <? if ( !$this -> task['is_open'] ):?> hidden<? else:?> animate<? endif;?>" task_id="<?= $this -> task['id'];?>">
<i class="fa fa-stop"></i> Wyłącz timer
<i class="fa fa-stop"></i> Wy&#322;&#261;cz timer
</a>
</div>
</div>
@@ -138,4 +161,46 @@
<? endforeach;?>
</div>
</div>
</div>
</div>
<script type="text/javascript">
( function() {
var popup = $( '.task_details[task_id="<?= $this -> task['id'];?>"]' );
if ( !popup.length )
return;
var time_worked = popup.find( '.time_worked' );
var time_value = popup.find( '.js-time-worked-value' );
var total_seconds = parseInt( time_worked.attr( 'data-total-seconds' ), 10 ) || 0;
function formatTime( seconds ) {
seconds = parseInt( seconds, 10 ) || 0;
var h = Math.floor( seconds / 3600 );
var m = Math.floor( ( seconds % 3600 ) / 60 );
var s = seconds % 60;
return String( h ).padStart( 2, '0' ) + ':' + String( m ).padStart( 2, '0' ) + ':' + String( s ).padStart( 2, '0' );
}
function renderTime() {
time_value.text( formatTime( total_seconds ) );
time_worked.attr( 'data-total-seconds', total_seconds );
}
renderTime();
var interval_id = setInterval( function() {
if ( !document.body.contains( popup.get( 0 ) ) )
{
clearInterval( interval_id );
return;
}
var is_timer_running = !popup.find( '.task_end' ).hasClass( 'hidden' );
if ( is_timer_running )
{
total_seconds++;
renderTime();
}
}, 1000 );
} )();
</script>