Refactor work time management and billing summary
- Introduced a new `TasksController` to handle work time logic, moving it from the factory layer to the domain layer. - Created `WorkTimeRepository` to encapsulate data access for work time tasks, including methods to retrieve clients with unsettled tasks and calculate task times. - Updated the work time view to display a consolidated billing summary with improved UI elements and AJAX functionality for closing tasks. - Added new styles for the billing summary section in `style.scss`. - Implemented tests for the `TasksController` and `WorkTimeRepository` to ensure functionality and correctness. - Established a refactoring plan for future improvements and migrations within the CRM system.
This commit is contained in:
@@ -1,158 +1,326 @@
|
||||
<?
|
||||
foreach ( $this -> work_time_clients as $client ):
|
||||
if ( !$client['tasks'] )
|
||||
$format_time = function( $seconds ) {
|
||||
$seconds = (int)$seconds;
|
||||
return sprintf( "%02d%s%02d%s%02d", floor( $seconds / 3600 ), ':', ( $seconds / 60 ) % 60, ':', $seconds % 60 );
|
||||
};
|
||||
|
||||
$format_amount = function( $amount ) {
|
||||
return number_format( (float)$amount, 2, '.', '' ) . ' zł';
|
||||
};
|
||||
|
||||
$billing_clients = [];
|
||||
$billing_total_amount = 0;
|
||||
$billing_total_time = 0;
|
||||
$billing_total_tasks = 0;
|
||||
|
||||
foreach ( $this -> work_time_clients as $client )
|
||||
{
|
||||
if ( !is_array( $client['tasks'] ) or !count( $client['tasks'] ) )
|
||||
continue;
|
||||
|
||||
$pay_rate = null;
|
||||
$pay_rate_summary = 0;
|
||||
?>
|
||||
<div class="card mb25">
|
||||
<div class="card-header" id="<?= $client['firm'];?>"><?= $client['firm'];?></div>
|
||||
<div class="card-body">
|
||||
<? if ( $client['tasks'][date( 'Y-m' )] ):?>
|
||||
<? $time = 0;?>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th colspan="4"><?= date( 'Y-m' );?></th>
|
||||
</tr>
|
||||
<? foreach ( $client['tasks'][date( 'Y-m' )] as $task ):?>
|
||||
<? $time += $task['time'];?>
|
||||
<tr>
|
||||
<td><?= $task['name'];?></td>
|
||||
<td style="width: 100px; text-align: center;"><?= sprintf( "%02d%s%02d%s%02d", floor( $task['time'] / 3600 ), ':', ( $task['time'] / 60) % 60, ':', $task['time'] % 60 );?></td>
|
||||
<td class="text-right" style="width: 100px;">
|
||||
<?
|
||||
if ( $task['pay_rate'] )
|
||||
{
|
||||
echo $task['pay_rate'] . ' zł';
|
||||
$pay_rate_summary += $task['pay_rate'];
|
||||
}
|
||||
else
|
||||
{
|
||||
// calculater pay rate for task ( hourly rate * time spend on task )
|
||||
$pay_rate = number_format( $this -> settings['hourly_rate'] * ( $task['time'] / 3600 ), 2, '.', '' );
|
||||
$pay_rate_summary += $pay_rate;
|
||||
echo $pay_rate . ' zł';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center" style="width: 200px;">
|
||||
<a href="#" class="close-task" task-id="<?= $task['id'];?>" client="<?= $client['firm'];?>">zamknij zadanie</a>
|
||||
</td>
|
||||
</tr>
|
||||
<? endforeach;?>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="bold text-center"><?= sprintf( "%02d%s%02d%s%02d", floor( $time / 3600 ), ':', ( $time / 60) % 60, ':', $time % 60 );?></td>
|
||||
<td class="bold text-right"><?= number_format( $pay_rate_summary, 2, '.', '' );?> zł</td>
|
||||
</tr>
|
||||
</table>
|
||||
<? endif;?>
|
||||
<? if ( $client['tasks'][date( 'Y-m', strtotime( '-1 months', time() ) )] ):?>
|
||||
<? $time = 0;?>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th colspan="4"><?= date( 'Y-m', strtotime( '-1 months', time() ) );?></th>
|
||||
</tr>
|
||||
<? foreach ( $client['tasks'][date( 'Y-m', strtotime( '-1 months', time() ) )] as $task ):?>
|
||||
<? $time += $task['time'];?>
|
||||
<tr>
|
||||
<td><?= $task['name'];?></td>
|
||||
<td style="width: 100px; text-align: center;"><?= sprintf( "%02d%s%02d%s%02d", floor( $task['time'] / 3600 ), ':', ( $task['time'] / 60) % 60, ':', $task['time'] % 60 );?></td>
|
||||
<td class="text-right" style="width: 100px;">
|
||||
<?
|
||||
if ( $task['pay_rate'] )
|
||||
{
|
||||
echo $task['pay_rate'] . ' zł';
|
||||
$pay_rate_summary += $task['pay_rate'];
|
||||
}
|
||||
else
|
||||
{
|
||||
// calculater pay rate for task ( hourly rate * time spend on task )
|
||||
$pay_rate = number_format( $this -> settings['hourly_rate'] * ( $task['time'] / 3600 ), 2, '.', '' );
|
||||
$pay_rate_summary += $pay_rate;
|
||||
echo $pay_rate . ' zł';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center" style="width: 200px;">
|
||||
<a href="#" class="close-task" task-id="<?= $task['id'];?>" client="<?= $client['firm'];?>">zamknij zadanie</a>
|
||||
</td>
|
||||
</tr>
|
||||
<? endforeach;?>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="bold text-center"><?= sprintf( "%02d%s%02d%s%02d", floor( $time / 3600 ), ':', ( $time / 60) % 60, ':', $time % 60 );?></td>
|
||||
<td class="bold text-right"><?= number_format( $pay_rate_summary, 2, '.', '' );?> zł</td>
|
||||
</tr>
|
||||
</table>
|
||||
<? endif;?>
|
||||
<? if ( $client['tasks'][date( 'Y-m', strtotime( '-2 months', time() ) )] ):?>
|
||||
<? $time = 0;?>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th colspan="4"><?= date( 'Y-m', strtotime( '-2 months', time() ) );?></th>
|
||||
</tr>
|
||||
<? foreach ( $client['tasks'][date( 'Y-m', strtotime( '-2 months', time() ) )] as $task ):?>
|
||||
<? $time += $task['time'];?>
|
||||
<tr>
|
||||
<td><?= $task['name'];?></td>
|
||||
<td style="width: 100px; text-align: center;"><?= sprintf( "%02d%s%02d%s%02d", floor( $task['time'] / 3600 ), ':', ( $task['time'] / 60) % 60, ':', $task['time'] % 60 );?></td>
|
||||
<td class="text-right" style="width: 100px;">
|
||||
<?
|
||||
if ( $task['pay_rate'] )
|
||||
{
|
||||
echo $task['pay_rate'] . ' zł';
|
||||
$pay_rate_summary += $task['pay_rate'];
|
||||
}
|
||||
else
|
||||
{
|
||||
// calculater pay rate for task ( hourly rate * time spend on task )
|
||||
$pay_rate = number_format( $this -> settings['hourly_rate'] * ( $task['time'] / 3600 ), 2, '.', '' );
|
||||
$pay_rate_summary += $pay_rate;
|
||||
echo $pay_rate . ' zł';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center" style="width: 200px;">
|
||||
<a href="#" class="close-task" task-id="<?= $task['id'];?>" client="<?= $client['firm'];?>">zamknij zadanie</a>
|
||||
</td>
|
||||
</tr>
|
||||
<? endforeach;?>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="bold text-center"><?= sprintf( "%02d%s%02d%s%02d", floor( $time / 3600 ), ':', ( $time / 60) % 60, ':', $time % 60 );?></td>
|
||||
<td class="bold text-right"><?= number_format( $pay_rate_summary, 2, '.', '' );?> zł</td>
|
||||
</tr>
|
||||
</table>
|
||||
<? endif;?>
|
||||
$months = $client['tasks'];
|
||||
krsort( $months );
|
||||
|
||||
$summary = [
|
||||
'firm' => $client['firm'],
|
||||
'tasks_count' => 0,
|
||||
'time' => 0,
|
||||
'amount' => 0,
|
||||
'rows' => []
|
||||
];
|
||||
|
||||
foreach ( $months as $month => $tasks )
|
||||
{
|
||||
if ( !is_array( $tasks ) or !count( $tasks ) )
|
||||
continue;
|
||||
|
||||
foreach ( $tasks as $task )
|
||||
{
|
||||
$task_time = isset( $task['time'] ) ? (int)$task['time'] : 0;
|
||||
|
||||
if ( isset( $task['pay_rate'] ) and $task['pay_rate'] !== null and $task['pay_rate'] !== '' )
|
||||
$task_amount = (float)$task['pay_rate'];
|
||||
else
|
||||
$task_amount = (float)$this -> settings['hourly_rate'] * ( $task_time / 3600 );
|
||||
|
||||
$summary['tasks_count']++;
|
||||
$summary['time'] += $task_time;
|
||||
$summary['amount'] += $task_amount;
|
||||
$summary['rows'][] = [
|
||||
'month' => $month,
|
||||
'id' => $task['id'],
|
||||
'name' => $task['name'],
|
||||
'time' => $task_time,
|
||||
'amount' => $task_amount
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ( !$summary['tasks_count'] )
|
||||
continue;
|
||||
|
||||
$billing_total_tasks += $summary['tasks_count'];
|
||||
$billing_total_time += $summary['time'];
|
||||
$billing_total_amount += $summary['amount'];
|
||||
$billing_clients[] = $summary;
|
||||
}
|
||||
|
||||
usort( $billing_clients, function( $a, $b ) {
|
||||
if ( $a['amount'] == $b['amount'] )
|
||||
return strcmp( $a['firm'], $b['firm'] );
|
||||
|
||||
return $a['amount'] < $b['amount'] ? 1 : -1;
|
||||
} );
|
||||
?>
|
||||
|
||||
<div class="card mb25 border-primary" id="billing-summary">
|
||||
<div class="card-header bg-primary text-white">Rozliczenia do zamknięcia (widok zbiorczy)</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb15">
|
||||
<div class="col-md-3 col-6 mb10">
|
||||
<div class="billing-kpi border-start border-4 border-primary">
|
||||
<div class="billing-kpi-label">Klienci</div>
|
||||
<div class="billing-kpi-value text-primary" id="billing-kpi-clients"><?= count( $billing_clients );?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb10">
|
||||
<div class="billing-kpi border-start border-4 border-warning">
|
||||
<div class="billing-kpi-label">Nierozliczone zadania</div>
|
||||
<div class="billing-kpi-value text-warning" id="billing-kpi-tasks"><?= $billing_total_tasks;?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb10">
|
||||
<div class="billing-kpi border-start border-4 border-info">
|
||||
<div class="billing-kpi-label">Suma czasu</div>
|
||||
<div class="billing-kpi-value text-info" id="billing-kpi-time"><?= $format_time( $billing_total_time );?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb10">
|
||||
<div class="billing-kpi border-start border-4 border-success">
|
||||
<div class="billing-kpi-label">Suma do zapłaty</div>
|
||||
<div class="billing-kpi-value text-success" id="billing-kpi-amount"><?= $format_amount( $billing_total_amount );?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?
|
||||
endforeach;
|
||||
?>
|
||||
|
||||
<? if ( count( $billing_clients ) ):?>
|
||||
<div class="mb10" id="billing-filter-wrap">
|
||||
<input type="text" class="form-control form-control-sm" id="billing-client-filter" placeholder="Filtruj klientów (nazwa firmy)">
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" id="billing-table-wrap">
|
||||
<table class="table table-sm table-hover billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Klient</th>
|
||||
<th class="text-center" style="width: 130px;">Zadania</th>
|
||||
<th class="text-center" style="width: 130px;">Czas</th>
|
||||
<th class="text-right" style="width: 150px;">Kwota</th>
|
||||
<th class="text-center" style="width: 150px;">Szczegóły</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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, '.', '' );?>">
|
||||
<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>
|
||||
<td class="text-right bold billing-client-amount"><?= $format_amount( $summary['amount'] );?></td>
|
||||
<td class="text-center">
|
||||
<a href="#" class="toggle-billing-details" data-target="<?= $details_id;?>">pokaż zadania</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="<?= $details_id;?>" class="billing-details-row">
|
||||
<td colspan="5" class="billing-details-wrap">
|
||||
<table class="table table-sm mb0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 90px;">Miesiąc</th>
|
||||
<th>Zadanie</th>
|
||||
<th class="text-center" style="width: 120px;">Czas</th>
|
||||
<th class="text-right" style="width: 120px;">Kwota</th>
|
||||
<th class="text-center" style="width: 160px;">Akcja</th>
|
||||
</tr>
|
||||
</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, '.', '' );?>">
|
||||
<td><?= $row['month'];?></td>
|
||||
<td><?= $row['name'];?></td>
|
||||
<td class="text-center"><?= $format_time( $row['time'] );?></td>
|
||||
<td class="text-right"><?= $format_amount( $row['amount'] );?></td>
|
||||
<td class="text-center">
|
||||
<a href="#" class="close-task" task-id="<?= $row['id'];?>" client="<?= $summary['firm'];?>">zamknij zadanie</a>
|
||||
</td>
|
||||
</tr>
|
||||
<? endforeach;?>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<? endforeach;?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<? else:?>
|
||||
<div class="alert alert-success mb0" id="billing-empty-state">Brak zadań oczekujących na rozliczenie.</div>
|
||||
<? endif;?>
|
||||
<? if ( count( $billing_clients ) ):?>
|
||||
<div class="alert alert-success mb0" id="billing-empty-state" style="display: none;">Brak zadań oczekujących na rozliczenie.</div>
|
||||
<? endif;?>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$( function()
|
||||
{
|
||||
$( 'body' ).on( 'click', '.close-task', function(e){
|
||||
e.preventDefault();
|
||||
var task_id = $( this ).attr( 'task-id' );
|
||||
var client = $( this ).attr( 'client' );
|
||||
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' );
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
url: '/tasks/task_change_status/',
|
||||
data: {
|
||||
task_id: task_id,
|
||||
status: 2
|
||||
},
|
||||
beforeSend: function() {},
|
||||
success: function( response ) {
|
||||
document.location.href = '/tasks/work_time/#' + client;
|
||||
location.reload();
|
||||
function formatAmount( amount ) {
|
||||
amount = parseFloat( amount ) || 0;
|
||||
return amount.toFixed( 2 ) + ' z\u0142';
|
||||
}
|
||||
|
||||
function refreshGlobalKpis() {
|
||||
var clients = 0;
|
||||
var tasks = 0;
|
||||
var time = 0;
|
||||
var amount = 0;
|
||||
|
||||
$( '.billing-client-row' ).each( function(){
|
||||
clients++;
|
||||
tasks += parseInt( $( this ).attr( 'data-tasks-count' ), 10 ) || 0;
|
||||
time += parseInt( $( this ).attr( 'data-time' ), 10 ) || 0;
|
||||
amount += parseFloat( $( this ).attr( 'data-amount' ) ) || 0;
|
||||
});
|
||||
|
||||
$( '#billing-kpi-clients' ).text( clients );
|
||||
$( '#billing-kpi-tasks' ).text( tasks );
|
||||
$( '#billing-kpi-time' ).text( formatTime( time ) );
|
||||
$( '#billing-kpi-amount' ).text( formatAmount( amount ) );
|
||||
|
||||
if ( clients === 0 )
|
||||
{
|
||||
$( '#billing-filter-wrap' ).hide();
|
||||
$( '#billing-table-wrap' ).hide();
|
||||
$( '#billing-empty-state' ).show();
|
||||
}
|
||||
}
|
||||
|
||||
function confirmCloseTask( onConfirm ) {
|
||||
if ( typeof $.confirm === 'function' )
|
||||
{
|
||||
$.confirm({
|
||||
title: 'Potwierdzenie',
|
||||
content: 'Czy na pewno chcesz zamknąć to zadanie?',
|
||||
type: 'orange',
|
||||
buttons: {
|
||||
cancel: {
|
||||
text: 'Anuluj'
|
||||
},
|
||||
confirm: {
|
||||
text: 'Zamknij zadanie',
|
||||
btnClass: 'btn-orange',
|
||||
action: function() {
|
||||
onConfirm();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ( confirm( 'Czy na pewno chcesz zamknąć to zadanie?' ) )
|
||||
onConfirm();
|
||||
}
|
||||
|
||||
$( 'body' ).on( 'click', '.toggle-billing-details', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
var details_id = $( this ).attr( 'data-target' );
|
||||
var details_row = $( '#' + details_id );
|
||||
var is_visible = details_row.is( ':visible' );
|
||||
|
||||
details_row.toggle( !is_visible );
|
||||
$( this ).text( is_visible ? 'poka\u017c zadania' : 'ukryj zadania' );
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'keyup', '#billing-client-filter', function(){
|
||||
var term = $( this ).val().toLowerCase().trim();
|
||||
|
||||
$( '.billing-client-row' ).each( function(){
|
||||
var row = $( this );
|
||||
var details_id = row.attr( 'data-details-id' );
|
||||
var client_name = row.find( '.billing-client-name' ).text().toLowerCase();
|
||||
var is_match = !term || client_name.indexOf( term ) !== -1;
|
||||
|
||||
row.toggle( is_match );
|
||||
if ( !is_match )
|
||||
{
|
||||
$( '#' + details_id ).hide();
|
||||
row.find( '.toggle-billing-details' ).text( 'poka\u017c zadania' );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '.close-task', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
var task_id = $( this ).attr( 'task-id' );
|
||||
var button = $( this );
|
||||
var task_row = button.closest( '.billing-task-row' );
|
||||
var details_row = button.closest( 'tr.billing-details-row' );
|
||||
var details_id = details_row.attr( 'id' );
|
||||
var summary_row = $( '.billing-client-row[data-details-id=\"' + details_id + '\"]' );
|
||||
|
||||
confirmCloseTask( function() {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
url: '/tasks/task_change_status/',
|
||||
data: {
|
||||
task_id: task_id,
|
||||
status: 2
|
||||
},
|
||||
beforeSend: function() {},
|
||||
success: function( response ) {
|
||||
task_row.remove();
|
||||
|
||||
var client_tasks = details_row.find( '.billing-task-row' );
|
||||
var client_tasks_count = client_tasks.length;
|
||||
var client_time = 0;
|
||||
var client_amount = 0;
|
||||
|
||||
client_tasks.each( function(){
|
||||
client_time += parseInt( $( this ).attr( 'data-task-time' ), 10 ) || 0;
|
||||
client_amount += parseFloat( $( this ).attr( 'data-task-amount' ) ) || 0;
|
||||
});
|
||||
|
||||
if ( client_tasks_count === 0 )
|
||||
{
|
||||
summary_row.remove();
|
||||
details_row.remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
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.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 ) );
|
||||
}
|
||||
|
||||
refreshGlobalKpis();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user