Skip to content

Commit

Permalink
internal API improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinpapst committed Apr 28, 2024
1 parent 3362604 commit 8b81acb
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 156 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Compatibility: requires minimum Kimai 2.11.0

- Added: toggl timesheet importer
- Added: format values in preview
- Added: internal API improvements
- Fix: user column name detection
- Removed: Translate detected column headers (caused too many problems)

Expand Down
180 changes: 52 additions & 128 deletions Importer/AbstractTimesheetImporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,54 +77,14 @@ public function __construct(
public function importRow(Duration $durationParser, ImportData $data, ImportRow $row, bool $dryRun): void
{
try {
/** @var array<string, string> $record */
$record = $row->getData();

if (!\array_key_exists('Tags', $record)) {
$record['Tags'] = '';
}
if (!\array_key_exists('Exported', $record)) {
$record['Exported'] = false;
}
if (!\array_key_exists('Rate', $record)) {
$record['Rate'] = null;
}
if (!\array_key_exists('HourlyRate', $record)) {
$record['HourlyRate'] = null;
}
if (!\array_key_exists('InternalRate', $record)) {
$record['InternalRate'] = null;
}
if (!\array_key_exists('FixedRate', $record)) {
$record['FixedRate'] = null;
}
if (!\array_key_exists('Billable', $record)) {
$record['Billable'] = true;
}
if (!empty($record['From'])) {
$len = \strlen($record['From']);
if ($len === 1) {
$record['From'] = '0' . $record['From'] . ':00';
} elseif ($len == 2) {
$record['From'] = $record['From'] . ':00';
}
}
if (!empty($record['To'])) {
$len = \strlen($record['To']);
if ($len === 1) {
$record['To'] = '0' . $record['To'] . ':00';
} elseif ($len == 2) {
$record['To'] = $record['To'] . ':00';
}
}

$this->validateRow($record);

$username = \array_key_exists('Username', $record) ? $record['Username'] : $record['User'];
$alias = \array_key_exists('Name', $record) ? $record['Name'] : (\array_key_exists('User', $record) ? $record['User'] : $username);
$email = \array_key_exists('Email', $record) ? $record['Email'] : $username;
if (null === ($user = $this->getUser($username, $alias, $email, $dryRun))) {
if (null === ($user = $this->getUser($record['User'], $record['Email'], $dryRun))) {
throw new ImportException(
sprintf('Unknown user %s', $username)
sprintf('Unknown user %s', $record['User'])
);
}

Expand All @@ -136,97 +96,58 @@ public function importRow(Duration $durationParser, ImportData $data, ImportRow
$duration = 0;
$foundDuration = null;

// most importers should provide seconds via int
if (\is_string($record['Duration']) && \strlen($record['Duration']) > 0) {
$duration = $this->parseDuration($durationParser, $record['Duration']);
$foundDuration = $duration;
}

$timezone = new \DateTimeZone($user->getTimezone());

if (\array_key_exists('Begin', $record) && \array_key_exists('End', $record)) {
try {
$begin = new \DateTime($record['Begin'], $timezone);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
try {
$end = new \DateTime($record['End'], $timezone);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
} elseif (empty($record['From']) && empty($record['To'])) {
try {
$begin = new \DateTime($record['Date'] . ' 12:00:00', $timezone);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
try {
$end = (new \DateTime())->setTimezone($timezone)->setTimestamp($begin->getTimestamp() + $duration);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
} elseif (empty($record['From'])) {
try {
$end = new \DateTime($record['Date'] . ' ' . $record['To'], $timezone);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
try {
$begin = (new \DateTime())->setTimezone($timezone)->setTimestamp($end->getTimestamp() - $duration);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
} elseif (empty($record['To'])) {
try {
$begin = new \DateTime($record['Date'] . ' ' . $record['From'], $timezone);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
try {
$end = (new \DateTime())->setTimezone($timezone)->setTimestamp($begin->getTimestamp() + $duration);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
} else {
try {
$begin = new \DateTime($record['Date'] . ' ' . $record['From'], $timezone);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
try {
$end = new \DateTime($record['Date'] . ' ' . $record['To'], $timezone);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
try {
$begin = new \DateTime($record['Begin'], $timezone);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}
try {
$end = new \DateTime($record['End'], $timezone);
} catch (\Exception $exception) {
throw new ImportException($exception->getMessage());
}

// fix dates, which are running over midnight
if ($end < $begin) {
if ($duration > 0) {
$end = (new \DateTime())->setTimezone($timezone)->setTimestamp($begin->getTimestamp() + $duration);
} else {
$end->add(new \DateInterval('P1D'));
}
// fix dates, which are running over midnight
if ($end < $begin) {
if ($duration > 0) {
$end = (new \DateTime())->setTimezone($timezone)->setTimestamp($begin->getTimestamp() + $duration);
} else {
$end->add(new \DateInterval('P1D'));
}
}

$timesheet = $this->timesheetService->createNewTimesheet($user);
$this->timesheetService->prepareNewTimesheet($timesheet);

$timesheet->setBillable(ImporterHelper::convertBoolean($record['Billable']));
$timesheet->setActivity($activity);
$timesheet->setProject($project);
$timesheet->setBegin($begin);
$timesheet->setEnd($end);
$timesheet->setDescription($record['Description']);
$timesheet->setExported(ImporterHelper::convertBoolean($record['Exported']));

if ($foundDuration !== null) {
$timesheet->setDuration($foundDuration);
} else {
$timesheet->setDuration($timesheet->getDuration());
}

if (!empty($record['Tags'])) {
if (\array_key_exists('Exported', $record)) {
$timesheet->setExported(ImporterHelper::convertBoolean($record['Exported']));
}

if (\array_key_exists('Billable', $record)) {
$timesheet->setBillable(ImporterHelper::convertBoolean($record['Billable']));
}

if (\array_key_exists('Tags', $record) && !empty($record['Tags'])) {
foreach (explode(',', $record['Tags']) as $tagName) {
if (empty($tagName)) {
continue;
Expand All @@ -236,16 +157,16 @@ public function importRow(Duration $durationParser, ImportData $data, ImportRow
}
}

if (!empty($record['Rate'])) {
if (\array_key_exists('Rate', $record) && is_numeric($record['Rate'])) {
$timesheet->setRate((float) $record['Rate']);
}
if (!empty($record['HourlyRate'])) {
if (\array_key_exists('HourlyRate', $record) && is_numeric($record['HourlyRate'])) {
$timesheet->setHourlyRate((float) $record['HourlyRate']);
}
if (!empty($record['FixedRate'])) {
if (\array_key_exists('FixedRate', $record) && is_numeric($record['FixedRate'])) {
$timesheet->setFixedRate((float) $record['FixedRate']);
}
if (!empty($record['InternalRate'])) {
if (\array_key_exists('InternalRate', $record) && is_numeric($record['InternalRate'])) {
$timesheet->setInternalRate((float) $record['InternalRate']);
}

Expand Down Expand Up @@ -327,21 +248,17 @@ public function import(ImportModelInterface $model, array $rows): ImportData
return $data;
}

private function getUser(string $user, string $alias, string $email, bool $dryRun): ?User
private function getUser(string $user, string $email, bool $dryRun): ?User
{
if (!\array_key_exists($user, $this->userCache)) {
$tmpUser = $this->userService->findUserByEmail($email);
if ($tmpUser === null) {
$tmpUser = $this->userService->findUserByName($user);
}
if ($tmpUser === null) {
$tmpUser = $this->userService->findUserByDisplayName($alias);
}

if ($tmpUser === null) {
$tmpUser = $this->userService->createNewUser();
$tmpUser->setEmail($email);
$tmpUser->setAlias($alias);
$tmpUser->setUserIdentifier($user);
$tmpUser->setPlainPassword(uniqid());
if (!$dryRun) {
Expand Down Expand Up @@ -450,6 +367,9 @@ private function getCustomer(string $customer, bool $dryRun): Customer
return $this->customerCache[$customer];
}

/**
* @param array<string, string> $row
*/
private function validateRow(array $row): void
{
$fields = [];
Expand All @@ -463,6 +383,18 @@ private function validateRow(array $row): void
$fields[] = $empty . 'User';
}

if (!\array_key_exists('Email', $row) || empty($row['Email'])) {
$fields[] = $empty . 'Email';
}

if (!\array_key_exists('Begin', $row) || empty($row['Begin'])) {
$fields[] = $empty . 'Begin';
}

if (!\array_key_exists('End', $row) || empty($row['End'])) {
$fields[] = $empty . 'End';
}

if (empty($row['Project'])) {
$fields[] = $empty . 'Project';
} elseif (mb_detect_encoding($row['Project'], 'UTF-8', true) !== 'UTF-8') {
Expand All @@ -486,23 +418,15 @@ private function validateRow(array $row): void
$fields[] = $encoding . 'Description';
}

if (empty($row['Date']) && empty($row['Begin'])) {
$fields[] = $empty . 'Date';
}

if ((empty($row['From']) || empty($row['To']) || empty($row['End'])) && ($row['Duration'] === '')) {
$fields[] = $empty . 'Duration';
}

if ($row['HourlyRate'] !== null && $row['HourlyRate'] !== '' && !is_numeric($row['HourlyRate'])) {
if (\array_key_exists('HourlyRate', $row) && $row['HourlyRate'] !== null && $row['HourlyRate'] !== '' && !is_numeric($row['HourlyRate'])) {
$fields[] = $float . 'HourlyRate';
}

if ($row['InternalRate'] !== null && $row['InternalRate'] !== '' && !is_numeric($row['InternalRate'])) {
if (\array_key_exists('InternalRate', $row) && $row['InternalRate'] !== null && $row['InternalRate'] !== '' && !is_numeric($row['InternalRate'])) {
$fields[] = $float . 'InternalRate';
}

if ($row['FixedRate'] !== null && $row['FixedRate'] !== '' && !is_numeric($row['FixedRate'])) {
if (\array_key_exists('FixedRate', $row) && $row['FixedRate'] !== null && $row['FixedRate'] !== '' && !is_numeric($row['FixedRate'])) {
$fields[] = $float . 'FixedRate';
}

Expand Down
27 changes: 17 additions & 10 deletions Importer/ClockifyTimesheetImporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ public function checkHeader(array $header): array
protected function createImportData(ImportRow $row): ImportData
{
$header = [
'Customer',
'Start',
'Begin',
'End',
'Customer',
'Project',
'Description',
'Activity',
Expand All @@ -76,9 +76,19 @@ public function importRow(Duration $durationParser, ImportData $data, ImportRow
{
$rawData = $row->getData();
$values = [
'Customer' => 'Importer',
'Begin' => $rawData['Start Date'] . ' ' . $rawData['Start Time'],
'End' => $rawData['End Date'] . ' ' . $rawData['End Time'],
'Customer' => 'Importer',
'Project' => '',
'Description' => '',
'Activity' => '',
'User' => '',
'Email' => '',
'Tags' => '',
'Billable' => true,
'Duration' => '',
'HourlyRate' => '',
'Rate' => '',
];

foreach ($rawData as $key => $value) {
Expand All @@ -88,9 +98,11 @@ public function importRow(Duration $durationParser, ImportData $data, ImportRow
case 'Project':
case 'Description':
case 'Tags':
case 'Billable': // Yes and No will be auto converted by the base class!
$values[$key] = $value;
break;
case 'Billable':
$values[$key] = ImporterHelper::convertBoolean($value);
break;
case 'Client':
if ($value !== '') {
$values['Customer'] = $value;
Expand All @@ -111,7 +123,7 @@ public function importRow(Duration $durationParser, ImportData $data, ImportRow
// nothing to do
break;
case 'Duration (decimal)':
$values['Duration'] = $value;
$values['Duration'] = $durationParser->parseDurationString((string) $value);
break;
default:
if (str_starts_with($key, 'Billable Rate')) {
Expand All @@ -125,9 +137,4 @@ public function importRow(Duration $durationParser, ImportData $data, ImportRow

parent::importRow($durationParser, $data, new ImportRow($values), $dryRun);
}

protected function parseDuration(Duration $durationParser, string $duration): int
{
return $durationParser->parseDurationString($duration);
}
}
Loading

0 comments on commit 8b81acb

Please sign in to comment.