egroupware_official/timesheet/src/JsTimesheet.php
ralf 0453aede6c * All apps/REST API: fix custom-fields of type "date-time" to be stored timezone aware, if no format is specified
So far date-time values were stored in user-time, now they are stored in UTC with a "Z" suffix" to be able to still read old user-time values unchanged.
2024-07-29 15:57:08 +02:00

207 lines
5.9 KiB
PHP

<?php
/**
* EGroupware Timesheet - JsTimesheet
*
* @link https://www.egroupware.org
* @author Ralf Becker <rb@egroupware.org>
* @package calendar
* @copyright (c) 2023 by Ralf Becker <rb@egroupware.org>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\Timesheet;
use EGroupware\Api;
/**
* Rendering events as JSON using new JsCalendar format
*
* @link https://datatracker.ietf.org/doc/html/rfc8984
* @link https://jmap.io/spec-calendars.html
*/
class JsTimesheet extends Api\CalDAV\JsBase
{
const APP = 'timesheet';
const TYPE_TIMESHEET = 'timesheet';
/**
* Get JsEvent for given event
*
* @param int|array $timesheet
* @param bool|"pretty" $encode true: JSON encode, "pretty": JSON encode with pretty-print, false: return raw data e.g. from listing
* @return string|array
* @throws Api\Exception\NotFound|\Exception
*/
public static function JsTimesheet($timesheet, $encode=true)
{
static $bo = null;
if (!isset($bo)) $bo = new \timesheet_bo();
if (is_scalar($timesheet) && !($timesheet = $bo->read($timesheet)))
{
throw new Api\Exception\NotFound();
}
if (isset($timesheet['ts_id']))
{
$timesheet = Api\Db::strip_array_keys($timesheet, 'ts_');
}
$data = array_filter([
self::AT_TYPE => self::TYPE_TIMESHEET,
//'uid' => self::uid($timesheet['uid']),
'id' => (int)$timesheet['id'],
'title' => $timesheet['title'],
'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'],
'unitprice' => (double)$timesheet['unitprice'],
'category' => self::categories($timesheet['cat_id']),
'owner' => self::account($timesheet['owner']),
'created' => self::UTCDateTime($timesheet['created'], true),
'modified' => self::UTCDateTime($timesheet['modified'], true),
'modifier' => self::account($timesheet['modifier']),
'pricelist' => (int)$timesheet['pl_id'] ?: null,
'status' => $bo->status_labels[$timesheet['status']] ?? null,
'egroupware.org:customfields' => self::customfields($timesheet),
'etag' => ApiHandler::etag($timesheet)
]);
if ($encode)
{
return Api\CalDAV::json_encode($data, $encode === "pretty");
}
return $data;
}
/**
* Parse JsTimesheet
*
* @param string $json
* @param array $old=[] existing contact for patch
* @param ?string $content_type=null application/json no strict parsing and automatic patch detection, if method not 'PATCH' or 'PUT'
* @param string $method='PUT' 'PUT', 'POST' or 'PATCH'
* @return array with "ts_" prefix
*/
public static function parseJsTimesheet(string $json, array $old=[], string $content_type=null, $method='PUT')
{
try
{
$data = json_decode($json, true, 10, JSON_THROW_ON_ERROR);
// check if we use patch: method is PATCH or method is POST AND keys contain slashes
if ($method === 'PATCH')
{
// apply patch on JsCard of contact
$data = self::patch($data, $old ? self::JsTimesheet($old, false) : [], !$old);
}
//if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist
// check required fields
if (!$old || !$method === 'PATCH')
{
static $required = ['title', 'start', 'duration'];
if (($missing = array_diff_key(array_filter(array_intersect_key($data, array_flip($required))), array_flip($required))))
{
throw new Api\CalDAV\JsParseException("Required field(s) ".implode(', ', $missing)." missing");
}
}
$timesheet = $method === 'PATCH' ? $old : [];
foreach ($data as $name => $value)
{
switch ($name)
{
case 'title':
case 'description':
case 'project':
$timesheet['ts_'.$name] = $value;
break;
case 'start':
$timesheet['ts_start'] = self::parseDateTime($value);
break;
case 'duration':
$timesheet['ts_duration'] = self::parseInt($value);
// set default quantity, if none explicitly given
if (!isset($timesheet['ts_quantity']))
{
$timesheet['ts_quantity'] = $timesheet['ts_duration'] / 60.0;
}
break;
case 'paused':
$timesheet['ts_paused'] = self::parseInt($value);
break;
case 'pricelist':
$timesheet['pl_id'] = self::parseInt($value);
break;
case 'quantity':
case 'unitprice':
$timesheet['ts_'.$name] = self::parseFloat($value);
break;
case 'owner':
$timesheet['ts_owner'] = self::parseAccount($value);
break;
case 'category':
$timesheet['cat_id'] = self::parseCategories($value, false);
break;
case 'status':
$timesheet['ts_status'] = self::parseStatus($value);
break;
case 'egroupware.org:customfields':
$timesheet = array_merge($timesheet, self::parseCustomfields($value));
break;
case 'prodId':
case 'created':
case 'modified':
case 'modifier':
case self::AT_TYPE:
case 'id':
case 'etag':
break;
default:
error_log(__METHOD__ . "() $name=" . json_encode($value, self::JSON_OPTIONS_ERROR) . ' --> ignored');
break;
}
}
}
catch (\Throwable $e) {
self::handleExceptions($e, 'JsTimesheet', $name, $value);
}
return $timesheet;
}
/**
* Parse a status label into it's numerical ID
*
* @param string $value
* @return int|null
* @throws Api\CalDAV\JsParseException
*/
protected static function parseStatus(string $value)
{
static $bo=null;
if (!isset($bo)) $bo = new \timesheet_bo();
if (($status_id = array_search($value, $bo->status_labels)) === false)
{
throw new Api\CalDAV\JsParseException("Invalid status value '$value', allowed '".implode("', '", $bo->status_labels)."'");
}
return $status_id;
}
}