* Timesheet: allow to record/document pause times with timer and manually

This commit is contained in:
ralf 2024-05-07 15:04:18 +02:00
parent eaa4a6abd0
commit f082055134
12 changed files with 80 additions and 31 deletions

View File

@ -137,6 +137,7 @@ class timesheet_bo extends Api\Storage
'ts_description' => 'Description',
'ts_start' => 'Start',
'ts_duration' => 'Duration',
'ts_paused' => 'Paused',
'ts_quantity' => 'Quantity',
'ts_unitprice' => 'Unitprice',
'ts_owner' => 'Owner',
@ -506,7 +507,7 @@ class timesheet_bo extends Api\Storage
//_debug_array($ids);
if(empty($ids))
{
$this->summary = array('duration' => 0, 'price' => null, 'quantity' => 0);
$this->summary = array('duration' => 0, 'paused' => 0, 'price' => null, 'quantity' => 0);
return array();
}
unset($criteria);
@ -520,6 +521,7 @@ class timesheet_bo extends Api\Storage
// is not joined, as the join causes a multiplication of the sum per customfield found
// joining of the cutomfield table is triggered by criteria being set with either a string or an array
$cols = ['SUM(ts_duration) AS duration',
'SUM(COALESCE(ts_paused,0)) AS paused',
"SUM($total_sql) AS price",
'MAX(ts_modified) AS max_modified'];
if($this->quantity_sum)
@ -569,7 +571,7 @@ class timesheet_bo extends Api\Storage
parent::search($criteria,array(
(string)$sum_ts_id[$type],"''","''","''",'MIN(ts_start)','SUM(ts_duration) AS ts_duration',
($this->quantity_sum ? "SUM(ts_quantity) AS ts_quantity" : '0'),
'0','NULL','0','0','0','0','0','0',"SUM($total_sql) AS ts_total"
'0','NULL','0','0','0','0','0','0',"SUM(COALESCE(ts_paused,0)) AS ts_paused","SUM($total_sql) AS ts_total"
),'GROUP BY '.$sum_sql[$type],$sum_extra_cols,$wildcard,$empty,$op,'UNION',$filter,$join,$need_full_no_count);
$sum_extra_cols[$type][0] = '0';
}
@ -792,7 +794,7 @@ class timesheet_bo extends Api\Storage
{
if(!$ids)
{
return array('duration' => 0, 'quantity' => 0, 'price' => 0, 'max_modified' => null);
return array('duration' => 0, 'paused' => 0, 'quantity' => 0, 'price' => 0, 'max_modified' => null);
}
$filter = [];
if($ignore_acl)

View File

@ -37,6 +37,7 @@ class timesheet_merge extends Api\Storage\Merge
*/
protected $numeric_fields = array(
'$$ts_duration$$',
'$$ts_paused$$',
'$$ts_quantity$$',
'$$ts_unitprice$$'
);
@ -132,7 +133,7 @@ class timesheet_merge extends Api\Storage\Merge
$array = $record->get_record_array();
$array['ts_total'] = $array['ts_quantity'] * $array['ts_unitprice'];
foreach(array('ts_duration','ts_quantity','ts_unitprice','ts_total') as $key)
foreach(array('ts_duration','ts_paused','ts_quantity','ts_unitprice','ts_total') as $key)
{
$array[$key] = self::number_format($array[$key],2,$this->mimetype);
}
@ -204,4 +205,4 @@ class timesheet_merge extends Api\Storage\Merge
}
return $placeholders;
}
}
}

View File

@ -99,7 +99,7 @@ class timesheet_ui extends timesheet_bo
// are we supposed to add pending events, to a new or an existing timesheet
if (isset($_REQUEST['events']))
{
$pending = Events::getPending($_REQUEST['events'] === 'overall', $time);
$pending = Events::getPending($_REQUEST['events'] === 'overall', $time, $paused);
$this->data['events'] = array_merge($this->data['events'], array_values($pending));
$start = $this->data['events'][0]['tse_time'];
$this->data['ts_start'] = $start;
@ -107,6 +107,7 @@ class timesheet_ui extends timesheet_bo
$this->data['end_time'] = '';
$this->data['ts_duration'] = (int)$this->data['ts_duration'] + round($time / 60); // minutes
$this->data['ts_quantity'] = (float)$this->data['ts_quantity'] + $this->data['ts_duration'] / 60.0; // hours
$this->data['ts_paused'] = $paused ? round($paused / 60.0) : null;
// check if any of the events contains an app::id to link the timesheet to
foreach($pending as $event)
{

View File

@ -108,6 +108,8 @@ overwrite time common de Zeit überschreiben
overwriting start or stop time timesheet de Überschreiben der Start- oder Stop-Zeit
parent admin de Übergeordnet
pause common de Pause
pause time timesheet de Pausenzeit
paused timesheet de Pausen
permission denied!!! timesheet de Zugriff verweigert!
permissions error - %1 could not %2 timesheet de Fehler in den Zugriffsberechtigungen - %1 nicht möglich %2
prevent deleting admin de Verhindert Löschung

View File

@ -108,6 +108,8 @@ overwrite time common en Overwrite time
overwriting start or stop time timesheet en Overwriting start or stop time
parent admin en Parent
pause common en Pause
pause time timesheet en Pause time
paused timesheet en Paused
permission denied!!! timesheet en Permission denied!
permissions error - %1 could not %2 timesheet en Permissions error - %1 could not %2
prevent deleting admin en Prevent deleting

View File

@ -16,7 +16,7 @@ if (!defined('TIMESHEET_APP'))
}
$setup_info[TIMESHEET_APP]['name'] = TIMESHEET_APP;
$setup_info[TIMESHEET_APP]['version'] = '23.1';
$setup_info[TIMESHEET_APP]['version'] = '23.1.001';
$setup_info[TIMESHEET_APP]['app_order'] = 5;
$setup_info[TIMESHEET_APP]['tables'] = array('egw_timesheet','egw_timesheet_extra','egw_timesheet_events');
$setup_info[TIMESHEET_APP]['enable'] = 1;
@ -52,4 +52,4 @@ $setup_info[TIMESHEET_APP]['hooks']['config_validate'] = 'EGroupware\\Timesheet\
$setup_info[TIMESHEET_APP]['depends'][] = array(
'appname' => 'api',
'versions' => Array('23.1')
);
);

View File

@ -19,7 +19,7 @@ $phpgw_baseline = array(
'ts_title' => array('type' => 'varchar','precision' => '255','nullable' => False,'comment' => 'title of the timesheet entry'),
'ts_description' => array('type' => 'varchar','precision' => '16384','comment' => 'description of the timesheet entry'),
'ts_start' => array('type' => 'int','meta' => 'timestamp','precision' => '8','nullable' => False,'comment' => 'timestamp of the startdate'),
'ts_duration' => array('type' => 'int','precision' => '8','nullable' => False,'default' => '0','comment' => 'duration of the timesheet-entry'),
'ts_duration' => array('type' => 'int','precision' => '4','nullable' => False,'default' => '0','comment' => 'duration of the timesheet-entry'),
'ts_quantity' => array('type' => 'float','precision' => '8','nullable' => False,'comment' => 'quantity'),
'ts_unitprice' => array('type' => 'float','precision' => '4','comment' => 'unitprice'),
'cat_id' => array('type' => 'int','meta' => 'category','precision' => '4','default' => '0','comment' => 'category'),
@ -28,7 +28,8 @@ $phpgw_baseline = array(
'ts_modifier' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False,'comment' => 'account id of the last modifier'),
'pl_id' => array('type' => 'int','precision' => '4','default' => '0','comment' => 'id of the linked project'),
'ts_status' => array('type' => 'int','precision' => '4','comment' => 'status of the timesheet-entry'),
'ts_created' => array('type' => 'int','meta' => 'timestamp','precision' => '8','nullable' => False,'comment' => 'Creation date of the timesheet')
'ts_created' => array('type' => 'int','meta' => 'timestamp','precision' => '8','nullable' => False,'comment' => 'Creation date of the timesheet'),
'ts_paused' => array('type' => 'int','precision' => '4','default' => '0','comment' => 'pause time(s) of the timesheet-entry')
),
'pk' => array('ts_id'),
'fk' => array(),
@ -64,4 +65,4 @@ $phpgw_baseline = array(
'ix' => array('ts_id'),
'uc' => array('tse_id')
)
);
);

View File

@ -233,4 +233,15 @@ function timesheet_upgrade21_1()
function timesheet_upgrade22_1()
{
return $GLOBALS['setup_info']['timesheet']['currentver'] = '23.1';
}
function timesheet_upgrade23_1()
{
$GLOBALS['egw_setup']->oProc->AddColumn('egw_timesheet','ts_paused',array(
'type' => 'int',
'precision' => '4',
'default' => '0',
'comment' => 'pause time(s) of the timesheet-entry'
));
return $GLOBALS['setup_info']['timesheet']['currentver'] = '23.1.001';
}

View File

@ -218,17 +218,18 @@ class Events extends Api\Storage\Base
*/
public function storeWorkingTime()
{
if (!($events = self::getPending(true, $time)) || !$time)
if (!($events = self::getPending(true, $time, $paused)) || !$time)
{
throw new Api\Exception\AssertionFailed("No pending overall events!");
}
$ids = array_keys($events);
$bo = new \timesheet_bo();
// check if we already have a timesheet for the current periode
// check if we already have a timesheet for the current period
if (($period_ts = $bo->periodeWorkingTimesheet(reset($events)['tse_time'])))
{
$events = array_merge(self::get(['ts_id' => $period_ts['ts_id']], $period_total), $events);
$events = array_merge(self::get(['ts_id' => $period_ts['ts_id']], $period_total, $period_paused), $events);
$time += $period_total;
$paused += $period_paused;
}
$title = self::workingTimeTitle($events, $start);
$bo->init($period_ts);
@ -240,6 +241,7 @@ class Events extends Api\Storage\Base
'end_time' => '',
'ts_duration' => $minutes = round($time / 60),
'ts_quantity' => $minutes / 60.0,
'ts_paused' => round($paused / 60),
'ts_owner' => $this->user,
]);
self::addToTimesheet($bo->data['ts_id'], $ids);
@ -351,6 +353,15 @@ class Events extends Api\Storage\Base
*/
protected static function evaluate(array &$timer, array $row)
{
// paused timer is started or stopped
if ($timer['paused'] && !($row['tse_type'] & self::PAUSE))
{
$timer['was_paused'] = 60000 * round(($row['tse_time']->getTimestamp() - $timer['pause_started']->getTimestamp())/60);
}
else
{
unset($timer['was_paused']);
}
if ($row['tse_type'] & self::START)
{
$timer['start'] = $timer['started'] = $row['tse_time'];
@ -367,6 +378,10 @@ class Events extends Api\Storage\Base
{
$timer['paused'] = ($row['tse_type'] & self::PAUSE) === self::PAUSE;
}
if ($timer['paused'])
{
$timer['pause_started'] = $row['tse_time'];
}
$timer['last'] = $row['tse_time'];
$timer['id'] = $row['tse_id'];
return $time ?? null;
@ -378,10 +393,11 @@ class Events extends Api\Storage\Base
* Not stopped events-sequences are NOT returned (stopped sequences end with a stop event).
*
* @param int|array $filter
* @param int &$total=null on return time in seconds
* @param ?int &$total=null on return time in seconds
* @param ?int &$paused on return paused time in seconds
* @return array[] tse_id => array pairs plus extra key sum (time-sum in seconds)
*/
public static function get($filter, int &$total=null)
public static function get($filter, ?int &$total=null, ?int &$paused=null)
{
if (!is_array($filter))
{
@ -392,7 +408,7 @@ class Events extends Api\Storage\Base
'offset' => 0,
'paused' => false,
];
$total = $open = 0;
$total = $open = $paused = 0;
$events = [];
foreach(self::getInstance()->search('', false, 'tse_id', '', '',
false, 'AND', false, $filter) as $row)
@ -411,6 +427,7 @@ class Events extends Api\Storage\Base
$row['total'] = $total + $timer['offset'] / 1000;
}
$row['time'] = $time / 1000;
$row['paused'] = !empty($timer['was_paused']) ? $paused += $timer['was_paused']/1000 : null;
$events[$row['tse_id']] = $row;
}
// remove open / unstopped timer events
@ -427,16 +444,17 @@ class Events extends Api\Storage\Base
* Not stopped events-sequences are NOT returned (stopped sequences end with a stop event).
*
* @param bool $overall
* @param int &$time=null on return total time in seconds
* @param ?int &$time=null on return total time in seconds
* @param ?int &$paused=null on return total paused time in seconds
* @return array[] tse_id => array pairs
*/
public static function getPending($overall=false, int &$time=null)
public static function getPending($overall=false, ?int &$time=null, ?int &$paused=null)
{
return self::get([
'ts_id' => null,
'account_id' => self::getInstance()->user,
($overall ? '' : 'NOT ').'(tse_type & '.self::OVERALL.')',
], $time);
], $time, $paused);
}
/**

View File

@ -52,6 +52,7 @@ class JsTimesheet extends Api\CalDAV\JsBase
'description' => $timesheet['description'],
'start' => self::UTCDateTime($timesheet['start'], true),
'duration' => (int)$timesheet['duration'],
'paused' => (int)$timesheet['paused'],
'project' => $timesheet['project_blur'] ?? null,
'pm_id' => !empty($timesheet['pm_id']) ? (int)$timesheet['pm_id'] : null,
'quantity' => (double)$timesheet['quantity'],
@ -132,6 +133,10 @@ class JsTimesheet extends Api\CalDAV\JsBase
}
break;
case 'paused':
$timesheet['ts_paused'] = self::parseInt($value);
break;
case 'pricelist':
$timesheet['pl_id'] = self::parseInt($value);
break;

View File

@ -38,15 +38,13 @@
<row class="row" disabled="!@ts_viewtype">
<et2-description value="comment"></et2-description>
<et2-textarea id="ts_description_short" rows="5" cols="50"></et2-textarea>
<et2-description></et2-description>
<et2-description></et2-description>
<et2-description></et2-description>
</row>
</row>
<row class="row" disabled="@ts_viewtype">
<et2-description value="Quantity" for="ts_quantity"></et2-description>
<et2-number statustext="empty if identical to duration" id="ts_quantity" precision="3" placeholder="@ts_quantity_blur"></et2-number>
<et2-description></et2-description>
</row>
<et2-description></et2-description>
<et2-date-duration label="Pause time" id="ts_paused" displayFormat="hm" span="all"></et2-date-duration>
</row>
<row class="row" disabled="@ts_viewtype">
<et2-description value="Category" for="cat_id"></et2-description>
<et2-select-cat span="all" id="cat_id" application="timesheet" emptyLabel="None"></et2-select-cat>
@ -86,6 +84,7 @@
<column/>
<column/>
<column/>
<column/>
</columns>
<rows>
<row class="th">
@ -93,6 +92,7 @@
<et2-description value="Recorded"></et2-description>
<et2-description value="Type"></et2-description>
<et2-description value="Duration"></et2-description>
<et2-description value="Paused"></et2-description>
<et2-description value="Sum"></et2-description>
</row>
<row id="timesheet-events::$row_cont[tse_id]">
@ -100,6 +100,7 @@
<et2-date-time id="${row}[tse_timestamp]" readonly="true"></et2-date-time>
<et2-select id="${row}[tse_type]" readonly="true"></et2-select>
<et2-date-duration id="${row}[time]" readonly="true" displayFormat="h:m" dataFormat="s"></et2-date-duration>
<et2-date-duration id="${row}[paused]" readonly="true" displayFormat="h:m" dataFormat="s"></et2-date-duration>
<et2-date-duration id="${row}[total]" readonly="true" displayFormat="h:m" dataFormat="s"></et2-date-duration>
</row>
</rows>

View File

@ -19,6 +19,7 @@
<column width="70%"/>
<column width="15%"/>
<column width="60"/>
<column width="60"/>
<column width="60" disabled="@no_ts_quantity"/>
<column width="60" disabled="@no_ts_unitprice"/>
<column width="60" disabled="@no_ts_total"/>
@ -54,16 +55,20 @@
<nextmatch-sortheader label="Category" id="cat_id"/>
<et2-vbox>
<nextmatch-sortheader label="Duration" id="ts_duration"/>
<et2-date-duration id="duration" readonly="true" displayFormat="hm"></et2-date-duration>
<et2-date-duration id="duration" readonly="true" displayFormat="hm" align="right"></et2-date-duration>
</et2-vbox>
<et2-vbox>
<nextmatch-sortheader label="Paused" id="ts_paused"/>
<et2-date-duration id="paused" readonly="true" displayFormat="hm" align="right"></et2-date-duration>
</et2-vbox>
<et2-vbox>
<nextmatch-sortheader label="Quantity" id="ts_quantity"/>
<et2-number id="quantity" readonly="true" precision="3"></et2-number>
<et2-number id="quantity" readonly="true" precision="3" align="right"></et2-number>
</et2-vbox>
<nextmatch-sortheader label="Price" id="ts_unitprice"/>
<et2-vbox>
<nextmatch-sortheader label="Total" id="ts_total"/>
<et2-number id="price" readonly="true" precision="2"></et2-number>
<et2-number id="price" readonly="true" precision="2" align="right"></et2-number>
</et2-vbox>
<et2-nextmatch-header-filter id="ts_owner" class="$cont[ownerClass]" noLang="1" emptyLabel="User"/>
<nextmatch-sortheader label="Created" id="ts_created"/>
@ -80,8 +85,8 @@
<et2-description id="${row}[ts_description]" class="ts_description" noLang="1"></et2-description>
</et2-vbox>
<et2-select-cat class="noWrap" id="${row}[cat_id]" readonly="true"></et2-select-cat>
<et2-date-duration id="${row}[ts_duration]" readonly="true" align="right" displayFormat="hm"
selectUnit="false"></et2-date-duration>
<et2-date-duration id="${row}[ts_duration]" readonly="true" align="right" displayFormat="hm" selectUnit="false"></et2-date-duration>
<et2-date-duration id="${row}[ts_paused]" readonly="true" align="right" displayFormat="hm" selectUnit="false"></et2-date-duration>
<et2-number id="${row}[ts_quantity]" readonly="true" precision="3" noLang="1"></et2-number>
<et2-description id="${row}[ts_unitprice]" noLang="1"></et2-description>
<et2-number id="${row}[ts_total]" readonly="true" precision="2" noLang="1"></et2-number>