Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ You get a list of all available commands with `kimai`.

- `kimai active` - display and update all running timesheets (via `--description` and `--tags`)
- `kimai stop` - stop currently active timesheets and update them (via `--description` and `--tags`)
- `kimai start` - start a new timesheet (see below)
- `kimai start` - start a new timesheet, optionally providing specific begin and end times (see below)
- `kimai customer:list` - show a list of customers
- `kimai project:list` - show a list of projects
- `kimai activity:list` - show a list of activities
Expand Down Expand Up @@ -104,6 +104,54 @@ bin/kimai start --customer Schowalter --project analyzer --activity iterate --de
------------- ------------------------------------
```

You may provide a specific begin and even an end time and/or date (useful for automated timesheet inserts). Both will be parsed according to
PHP's [Supported Date and Time Formats rules](https://www.php.net/manual/en/datetime.formats.php) and thus allow relative values
as well as partial time and date-time values.

Examples:

```shell
# Forgot to start timer 20 minutes ago
bin/kimai start --begin "-20min"

# Forgot to start timer this morning
bin/kimai start --begin "8:13"

# Worked all day last Friday and forgot to track time
bin/kimai start --begin "last friday 8:00" --end "last friday 16:00"

# Will be working for an hour and already want to finalize my timesheet
bin/kimai start --end "+1hour"

# Of course ISO date-times work as well
bin/kimai start --begin "2025-12-24 20:00" --end "2025-12-25 06:00" -d "HO HO HO"

# As do epoch timestamps
bin/kimai start --begin "@1766602800" --end "@1766638800" -d "HO HO HO"

# Shortcuts -b for --begin and -e for --end are available
bin/kimai start -b "2025-12-24 20:00" -e "2025-12-25 06:00"
```

If the command fails to parse your date input, it will repeatedly ask for refinement:

```shell
bin/kimai start --begin "202b-12-01 10:11"

Value "202b-12-01 10:11" for field "begin" is not a valid DateTime. Please refine: [202b-12-01 10:11]:
> 202c-12-01 10:11

Value "202c-12-01 10:11" for field "begin" is not a valid DateTime. Please refine: [202c-12-01 10:11]:
> 2025-12-01 10:11


[OK] Started timesheet

```

You might input values that are valid DateTimes but not accepted by Kimai. Final validation (e.g. begin < end) and rounding will always
be handled on Kimai's side.

### Output format

The `:list`ing commands display a formatted table of all found entities.
Expand Down
7 changes: 7 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
ignoreErrors:
-
message: '#^Using nullsafe method call on non\-nullable type DateTime\. Use \-\> instead\.$#'
identifier: nullsafe.neverNull
count: 1
path: src/Command/StartCommand.php
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
includes:
- %rootDir%/../phpstan/conf/bleedingEdge.neon
- phpstan-baseline.neon

parameters:
level: 5
Expand Down
15 changes: 14 additions & 1 deletion src/Command/StartCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ protected function configure(): void
$this
->setName('start')
->setDescription('Starts a new timesheet')
->setHelp('This command lets you start a new timesheet')
->setHelp('This command lets you start a new timesheet and optionally end it immediately.')
->addOption('customer', 'c', InputOption::VALUE_OPTIONAL, 'The customer to filter the project list, can be an ID or a search term or empty (you will be prompted for a customer).')
->addOption('project', 'p', InputOption::VALUE_OPTIONAL, 'The project to use, can be an ID or a search term or empty. You will be prompted for the project.')
->addOption('activity', 'a', InputOption::VALUE_OPTIONAL, 'The activity ID to use')
->addOption('tags', 't', InputOption::VALUE_OPTIONAL, 'Comma separated list of tag names')
->addOption('description', 'd', InputOption::VALUE_OPTIONAL, 'The timesheet description')
->addOption('begin', 'b', InputOption::VALUE_OPTIONAL, 'The begin (date and) time in a format supported by PHP')
->addOption('end', 'e', InputOption::VALUE_OPTIONAL, 'The end (date and) time in a format supported by PHP')
;
}

Expand Down Expand Up @@ -81,6 +83,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$form->setDescription($description);
}

if (null !== ($begin = $input->getOption('begin'))) {
$begin = $this->parseAndRefineDateTime($io, (string) $begin, 'begin');
$form->setBegin($begin);
}

if (null !== ($end = $input->getOption('end'))) {
$end = $this->parseAndRefineDateTime($io, (string) $end, 'end');
$form->setEnd($end);
}

try {
$timesheet = $api->postPostTimesheet($form);
} catch (ApiException $ex) {
Expand All @@ -92,6 +104,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$fields = [
'ID' => $timesheet->getId(),
'Begin' => $timesheet->getBegin()->format(\DateTime::ISO8601),
'End' => $timesheet->getEnd()?->format(\DateTime::ISO8601),
'Description' => $timesheet->getDescription(),
'Tags' => implode(PHP_EOL, $timesheet->getTags()),
'Customer' => $customer->getName(),
Expand Down
17 changes: 17 additions & 0 deletions src/Command/TimesheetCommandTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,21 @@ private function askForActivity(SymfonyStyle $io, array $activities): ?Activity

return null;
}

/**
* Try to convert the given string into a DateTime object and recursively ask for clarification if necessary.
*/
private function parseAndRefineDateTime(SymfonyStyle $io, string $begin, string $valueName): \DateTime
{
try {
return new \DateTime($begin);
} catch (\Exception $e) {
}

return $this->parseAndRefineDateTime(
$io,
$io->ask(\sprintf('Value "%s" for field "%s" is not a valid DateTime. Please refine:', $begin, $valueName), $begin),
$valueName,
);
}
}