diff --git a/README.md b/README.md index b5e51eab..db7be3ac 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ Kimai is a [multi-language application](var/docs/translations.md) and already tr ### Requirements - PHP 7.1.3 or higher (test your system compatibility with the [requirements-checker](http://symfony.com/doc/current/reference/requirements.html)) -- The PHP extensions [mbstring](http://php.net/manual/en/book.mbstring.php), [gd](http://php.net/manual/en/book.image.php), [intl](https://php.net/manual/en/book.intl.php), [zip](https://php.net/manual/en/book.zip.php) and [PDO](https://php.net/manual/en/book.pdo.php) with either [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) enabled +- The PHP extensions [xml](http://php.net/manual/en/book.xml.php), [mbstring](http://php.net/manual/en/book.mbstring.php), [gd](http://php.net/manual/en/book.image.php), [intl](https://php.net/manual/en/book.intl.php), [zip](https://php.net/manual/en/book.zip.php) and [PDO](https://php.net/manual/en/book.pdo.php) with either [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) enabled +- Kimai needs its own sub-domain or you need to [recompile the frontend assets](var/docs/developers.md) for usage in a sub-directory - If you use MariaDB, make sure its at least v10.2.7 (see [FAQ](var/docs/faq.md)) -- Kimai needs to be installed in the root directory of a domain or you need to [recompile the frontend assets](var/docs/developers.md) - A modern browser, Kimai v2 might be broken on old browsers like IE 10 ## Documentation @@ -56,12 +56,12 @@ You can see our development roadmap in the [Milestones](https://github.com/kevin It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome! > Kimai 2 uses a rolling release concept for delivering updates. -> You can upgrade Kimai at any time, you don't need to wait for the next official release. +> You can upgrade Kimai at any time, you don't need to wait for the next official release. Read the [upgrade docs](UPGRADING.md) first! Release versions will be created on a regular base and you can use these tags if you are familiar with Git, but we will not provide support for any specific version. -Every code change, whether it's a new feature or a bug fix, will be done on the master branch and -intensively tested before merging. We have to do it this way, as we develop Kimai in our free time and want to put our +Every code change, whether it's a new feature or a bug fix, will be done on the master branch. +I have to do it this way, as I develop Kimai in my free time and want to put most effort into the software instead of backporting changes for old versions. ## Extensions for Kimai 2 diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index 5418e333..80aa11bf 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -15,6 +15,7 @@ use App\Entity\Timesheet; use App\Form\TimesheetEditForm; use App\Repository\Query\TimesheetQuery; use App\Repository\TimesheetRepository; +use App\Timesheet\UserDateTimeFactory; use FOS\RestBundle\Controller\Annotations as Rest; use FOS\RestBundle\Controller\Annotations\RouteResource; use FOS\RestBundle\Request\ParamFetcherInterface; @@ -46,16 +47,23 @@ class TimesheetController extends BaseApiController */ protected $hardLimit; + /** + * @var UserDateTimeFactory + */ + protected $dateTime; + /** * @param ViewHandlerInterface $viewHandler * @param TimesheetRepository $repository + * @param UserDateTimeFactory $dateTime * @param int $hardLimit */ - public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, int $hardLimit) + public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, UserDateTimeFactory $dateTime, int $hardLimit) { $this->viewHandler = $viewHandler; $this->repository = $repository; $this->hardLimit = $hardLimit; + $this->dateTime = $dateTime; } /** @@ -164,7 +172,7 @@ class TimesheetController extends BaseApiController { $timesheet = new Timesheet(); $timesheet->setUser($this->getUser()); - $timesheet->setBegin(new \DateTime()); + $timesheet->setBegin($this->dateTime->createDateTime()); $form = $this->createForm(TimesheetEditForm::class, $timesheet, [ 'csrf_protection' => false, diff --git a/src/Command/KimaiImporterCommand.php b/src/Command/KimaiImporterCommand.php index d16e40d6..2a5166b9 100644 --- a/src/Command/KimaiImporterCommand.php +++ b/src/Command/KimaiImporterCommand.php @@ -15,6 +15,7 @@ use App\Entity\Customer; use App\Entity\Project; use App\Entity\Timesheet; use App\Entity\User; +use App\Entity\UserPreference; use Doctrine\Common\Persistence\ObjectManager; use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Connection; @@ -125,7 +126,7 @@ class KimaiImporterCommand extends Command ) ->addArgument('prefix', InputArgument::REQUIRED, 'The database prefix for the old Kimai v1 tables') ->addArgument('password', InputArgument::REQUIRED, 'The new password for all imported user') - ->addArgument('country', InputArgument::OPTIONAL, 'The default country for customer', 'de') + ->addArgument('country', InputArgument::OPTIONAL, 'The default country for customer (2-character uppercase)', 'DE') ; } @@ -249,7 +250,7 @@ class KimaiImporterCommand extends Command $allImports = 0; try { - $counter = $this->importUsers($io, $password, $users); + $counter = $this->importUsers($io, $password, $users, $rates); $allImports += $counter; $io->success('Imported users: ' . $counter); } catch (\Exception $ex) { @@ -398,16 +399,21 @@ class KimaiImporterCommand extends Command } /** - * @param string $table + * @param $table + * @param array $where * @return array */ - protected function fetchAllFromImport($table) + protected function fetchAllFromImport($table, array $where = []) { - return $this->connection->createQueryBuilder() + $query = $this->connection->createQueryBuilder() ->select('*') - ->from($this->connection->quoteIdentifier($this->dbPrefix . $table)) - ->execute() - ->fetchAll(); + ->from($this->connection->quoteIdentifier($this->dbPrefix . $table)); + + foreach ($where as $column => $value) { + $query->andWhere($query->expr()->eq($column, $value)); + } + + return $query->execute()->fetchAll(); } /** @@ -467,10 +473,11 @@ class KimaiImporterCommand extends Command * @param SymfonyStyle $io * @param string $password * @param array $users + * @param array $rates * @return int * @throws \Exception */ - protected function importUsers(SymfonyStyle $io, $password, $users) + protected function importUsers(SymfonyStyle $io, $password, $users, $rates) { $counter = 0; $entityManager = $this->getDoctrine()->getManager(); @@ -495,6 +502,34 @@ class KimaiImporterCommand extends Command throw new \Exception('Failed to validate user: ' . $user->getUsername()); } + // find and migrate user preferences + $prefsToImport = ['ui.lang' => 'language', 'timezone' => 'timezone']; + $preferences = $this->fetchAllFromImport('preferences', ['userID' => $oldUser['userID']]); + foreach ($preferences as $pref) { + $key = $pref['option']; + + if (!array_key_exists($key, $prefsToImport)) { + continue; + } + + $newPref = new UserPreference(); + $newPref + ->setName($prefsToImport[$key]) + ->setValue($pref['value']); + $user->addPreference($newPref); + } + + // find hourly rate + foreach ($rates as $ratesRow) { + if ($ratesRow['userID'] === $oldUser['userID'] && $ratesRow['activityID'] === null && $ratesRow['projectID'] === null) { + $newPref = new UserPreference(); + $newPref + ->setName(UserPreference::HOURLY_RATE) + ->setValue($ratesRow['rate']); + $user->addPreference($newPref); + } + } + try { $entityManager->persist($user); $entityManager->flush(); @@ -571,7 +606,7 @@ class KimaiImporterCommand extends Command ->setAddress($oldCustomer['street'] . PHP_EOL . $oldCustomer['zipcode'] . ' ' . $oldCustomer['city']) ->setTimezone($oldCustomer['timezone']) ->setVisible($isActive) - ->setCountry($country) + ->setCountry(strtoupper($country)) ; if (!$this->validateImport($io, $customer)) { @@ -908,12 +943,18 @@ class KimaiImporterCommand extends Command } if (null === $activity) { - $io->error('Could not create timesheet record, missing activity with ID: ' . $activityId . '/' . $projectId . '/' . $customerId); + $io->error('Could not import timesheet record, missing activity with ID: ' . $activityId . '/' . $projectId . '/' . $customerId); continue; } $duration = $oldRecord['end'] - $oldRecord['start']; + // FIXME create user on the fly + if (!isset($this->users[$oldRecord['userID']])) { + $io->error('Could not import timesheet record, unknown user: ' . $oldRecord['userID']); + continue; + } + $timesheet = new Timesheet(); $fixedRate = $oldRecord['fixedRate']; @@ -934,15 +975,30 @@ class KimaiImporterCommand extends Command $timesheet->setRate(round($rate, 2)); } + $user = $this->users[$oldRecord['userID']]; + $timezone = $user->getPreferenceValue('timezone', date_default_timezone_get()); + $dateTimezone = new \DateTimeZone('UTC'); + + $begin = new \DateTime('@' . $oldRecord['start']); + $begin->setTimezone($dateTimezone); + $end = new \DateTime('@' . $oldRecord['end']); + $end->setTimezone($dateTimezone); + + // ---------- workaround for localizeDates ---------- + // if getBegin() is not execute first, then the dates will we re-written in validateImport() below + $timesheet->setBegin($begin)->setEnd($end)->getBegin(); + // -------------------------------------------------- + $timesheet ->setDescription($oldRecord['description'] ?: ($oldRecord['comment'] ?: null)) ->setUser($this->users[$oldRecord['userID']]) - ->setBegin(new \DateTime('@' . $oldRecord['start'])) - ->setEnd(new \DateTime('@' . $oldRecord['end'])) + ->setBegin($begin) + ->setEnd($end) ->setDuration($duration) ->setActivity($activity) ->setProject($project) ->setExported(intval($oldRecord['cleared']) !== 0) + ->setTimezone($timezone) ; if (!$this->validateImport($io, $timesheet)) { diff --git a/src/Controller/CalendarController.php b/src/Controller/CalendarController.php index 61eacc7b..3bdf5e0d 100644 --- a/src/Controller/CalendarController.php +++ b/src/Controller/CalendarController.php @@ -13,6 +13,7 @@ use App\Calendar\Service; use App\Calendar\TimesheetEntity; use App\Entity\Timesheet; use App\Repository\Query\TimesheetQuery; +use App\Timesheet\UserDateTimeFactory; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\HttpFoundation\Request; @@ -30,13 +31,19 @@ class CalendarController extends AbstractController * @var Service */ protected $calendar; + /** + * @var UserDateTimeFactory + */ + protected $dateTime; /** * @param Service $calendar + * @param UserDateTimeFactory $dateTime */ - public function __construct(Service $calendar) + public function __construct(Service $calendar, UserDateTimeFactory $dateTime) { $this->calendar = $calendar; + $this->dateTime = $dateTime; } /** @@ -47,7 +54,8 @@ class CalendarController extends AbstractController { return $this->render('calendar/user.html.twig', [ 'config' => $this->calendar->getConfig(), - 'google' => $this->calendar->getGoogle() + 'google' => $this->calendar->getGoogle(), + 'now' => $this->dateTime->createDateTime(), ]); } diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index cd31f136..bcec5b10 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -18,6 +18,7 @@ use App\Form\UserPasswordType; use App\Form\UserPreferencesForm; use App\Form\UserRolesType; use App\Repository\TimesheetRepository; +use App\Timesheet\UserDateTimeFactory; use App\Voter\UserVoter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -45,12 +46,20 @@ class ProfileController extends AbstractController protected $encoder; /** - * @param UserPasswordEncoderInterface $encoder + * @var UserDateTimeFactory */ - public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher) + protected $dateTime; + + /** + * @param UserPasswordEncoderInterface $encoder + * @param EventDispatcherInterface $dispatcher + * @param UserDateTimeFactory $dateTime + */ + public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher, UserDateTimeFactory $dateTime) { $this->encoder = $encoder; $this->dispatcher = $dispatcher; + $this->dateTime = $dateTime; } /** @@ -252,6 +261,7 @@ class ProfileController extends AbstractController 'user' => $user, 'stats' => $userStats, 'years' => $monthlyStats, + 'local_time' => $this->dateTime->createDateTime(), 'forms' => [] ]; diff --git a/src/Controller/TimesheetController.php b/src/Controller/TimesheetController.php index 0870bd68..124be452 100644 --- a/src/Controller/TimesheetController.php +++ b/src/Controller/TimesheetController.php @@ -29,14 +29,6 @@ class TimesheetController extends AbstractController { use TimesheetControllerTrait; - /** - * @param int $hardLimit - */ - public function __construct(int $hardLimit) - { - $this->setHardLimit($hardLimit); - } - /** * @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"}) * @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"}) @@ -162,7 +154,7 @@ class TimesheetController extends AbstractController try { $entry = new Timesheet(); $entry - ->setBegin(new \DateTime()) + ->setBegin($this->dateTime->createDateTime()) ->setUser($user) ->setActivity($timesheet->getActivity()) ->setProject($timesheet->getProject()) diff --git a/src/Controller/TimesheetControllerTrait.php b/src/Controller/TimesheetControllerTrait.php index 5f4bdccb..9b2c20b6 100644 --- a/src/Controller/TimesheetControllerTrait.php +++ b/src/Controller/TimesheetControllerTrait.php @@ -12,6 +12,7 @@ namespace App\Controller; use App\Entity\Timesheet; use App\Entity\User; use App\Repository\TimesheetRepository; +use App\Timesheet\UserDateTimeFactory; use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -27,6 +28,21 @@ trait TimesheetControllerTrait */ private $hardLimit = 1; + /** + * @var UserDateTimeFactory + */ + protected $dateTime; + + /** + * @param UserDateTimeFactory $dateTime + * @param int $hardLimit + */ + public function __construct(UserDateTimeFactory $dateTime, int $hardLimit) + { + $this->dateTime = $dateTime; + $this->setHardLimit($hardLimit); + } + /** * @param int $hardLimit */ @@ -118,7 +134,7 @@ trait TimesheetControllerTrait { $entry = new Timesheet(); $entry->setUser($this->getUser()); - $entry->setBegin(new \DateTime()); + $entry->setBegin($this->dateTime->createDateTime()); $start = $request->get('begin'); if ($start !== null) { diff --git a/src/EventSubscriber/NavbarShowUserSubscriber.php b/src/EventSubscriber/NavbarShowUserSubscriber.php index 9979d8bf..9e3590c6 100644 --- a/src/EventSubscriber/NavbarShowUserSubscriber.php +++ b/src/EventSubscriber/NavbarShowUserSubscriber.php @@ -16,9 +16,6 @@ use KevinPapst\AdminLTEBundle\Model\UserModel; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -/** - * Class NavbarShowUserSubscriber - */ class NavbarShowUserSubscriber implements EventSubscriberInterface { /** @@ -27,7 +24,6 @@ class NavbarShowUserSubscriber implements EventSubscriberInterface protected $storage; /** - * NavbarShowUserListener constructor. * @param TokenStorageInterface $tokenStorage */ public function __construct(TokenStorageInterface $tokenStorage) @@ -64,7 +60,7 @@ class NavbarShowUserSubscriber implements EventSubscriberInterface ->setIsOnline(true) ->setTitle($myUser->getTitle()) ->setAvatar($myUser->getAvatar()) - ->setMemberSince(new \DateTime()); + ->setMemberSince($myUser->getRegisteredAt()); $event->setUser($user); } diff --git a/src/EventSubscriber/TimezoneSubscriber.php b/src/EventSubscriber/TimezoneSubscriber.php index a37e5797..32db0268 100644 --- a/src/EventSubscriber/TimezoneSubscriber.php +++ b/src/EventSubscriber/TimezoneSubscriber.php @@ -46,7 +46,7 @@ class TimezoneSubscriber implements EventSubscriberInterface */ public function setTimezone(GetResponseEvent $event) { - if (null === $this->storage->getToken()) { + if (!$this->canHandleEvent()) { return; } @@ -55,4 +55,24 @@ class TimezoneSubscriber implements EventSubscriberInterface $timezone = $user->getPreferenceValue('timezone', date_default_timezone_get()); date_default_timezone_set($timezone); } + + /** + * @return bool + */ + protected function canHandleEvent(): bool + { + if (null === $this->storage->getToken()) { + return false; + } + + /* @var $user User */ + $user = $this->storage->getToken()->getUser(); + + if (null === $user) { + return false; + } + + return ($user instanceof User); + } + } diff --git a/src/Export/Renderer/PDFRenderer.php b/src/Export/Renderer/PDFRenderer.php index 5f3b65b8..d217cdaf 100644 --- a/src/Export/Renderer/PDFRenderer.php +++ b/src/Export/Renderer/PDFRenderer.php @@ -12,6 +12,7 @@ namespace App\Export\Renderer; use App\Entity\Timesheet; use App\Export\RendererInterface; use App\Repository\Query\TimesheetQuery; +use App\Timesheet\UserDateTimeFactory; use Mpdf\Mpdf; use Mpdf\Output\Destination; use Symfony\Component\HttpFoundation\Response; @@ -27,11 +28,18 @@ class PDFRenderer implements RendererInterface protected $twig; /** - * @param \Twig_Environment $twig + * @var UserDateTimeFactory */ - public function __construct(\Twig_Environment $twig) + protected $dateTime; + + /** + * @param \Twig_Environment $twig + * @param UserDateTimeFactory $dateTime + */ + public function __construct(\Twig_Environment $twig, UserDateTimeFactory $dateTime) { $this->twig = $twig; + $this->dateTime = $dateTime; } /** @@ -48,7 +56,7 @@ class PDFRenderer implements RendererInterface $content = $this->twig->render('export/renderer/pdf.html.twig', [ 'entries' => $timesheets, 'query' => $query, - 'now' => new \DateTime(), + 'now' => $this->dateTime->createDateTime(), 'summaries' => $this->calculateSummary($timesheets), ]); diff --git a/src/Form/TimesheetEditForm.php b/src/Form/TimesheetEditForm.php index 9055189b..38abe7de 100644 --- a/src/Form/TimesheetEditForm.php +++ b/src/Form/TimesheetEditForm.php @@ -20,6 +20,7 @@ use App\Form\Type\YesNoType; use App\Repository\ActivityRepository; use App\Repository\CustomerRepository; use App\Repository\ProjectRepository; +use App\Timesheet\UserDateTimeFactory; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; @@ -48,15 +49,22 @@ class TimesheetEditForm extends AbstractType */ private $durationOnly = false; + /** + * @var UserDateTimeFactory + */ + protected $dateTime; + /** * @param CustomerRepository $customer * @param ProjectRepository $project + * @param UserDateTimeFactory $dateTime * @param bool $durationOnly */ - public function __construct(CustomerRepository $customer, ProjectRepository $project, bool $durationOnly) + public function __construct(CustomerRepository $customer, ProjectRepository $project, UserDateTimeFactory $dateTime, bool $durationOnly) { $this->customers = $customer; $this->projects = $project; + $this->dateTime = $dateTime; $this->durationOnly = $durationOnly; } @@ -70,6 +78,7 @@ class TimesheetEditForm extends AbstractType $customer = null; $currency = false; $end = null; + $begin = null; if (isset($options['data'])) { /** @var Timesheet $entry */ @@ -87,12 +96,21 @@ class TimesheetEditForm extends AbstractType $currency = $customer->getCurrency(); } + $begin = $entry->getBegin(); $end = $entry->getEnd(); } + $timezone = $this->dateTime->getTimezone()->getName(); + + if (null !== $begin) { + $timezone = $begin->getTimezone()->getName(); + } + if (null === $end || !$options['duration_only']) { $builder->add('begin', DateTimePickerType::class, [ 'label' => 'label.begin', + 'model_timezone' => $timezone, + 'view_timezone' => $timezone, ]); } @@ -102,7 +120,9 @@ class TimesheetEditForm extends AbstractType ]); } else { $builder->add('end', DateTimePickerType::class, [ - 'label' => 'label.end', + 'label' => 'label.begin', + 'model_timezone' => $timezone, + 'view_timezone' => $timezone, 'required' => false, ]); } @@ -135,7 +155,10 @@ class TimesheetEditForm extends AbstractType } $builder - ->add('project', ProjectType::class, array_merge($projectOptions, [ + ->add( + 'project', + ProjectType::class, + array_merge($projectOptions, [ 'placeholder' => '', 'activity_enabled' => true, // documentation is for NelmioApiDocBundle diff --git a/src/Form/Type/DatePickerType.php b/src/Form/Type/DatePickerType.php index b8d2d779..c8f9f2e2 100644 --- a/src/Form/Type/DatePickerType.php +++ b/src/Form/Type/DatePickerType.php @@ -9,6 +9,7 @@ namespace App\Form\Type; +use App\Timesheet\UserDateTimeFactory; use App\Utils\LocaleSettings; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateType; @@ -26,11 +27,18 @@ class DatePickerType extends AbstractType protected $localeSettings; /** - * @param LocaleSettings $localeSettings + * @var UserDateTimeFactory */ - public function __construct(LocaleSettings $localeSettings) + protected $dateTime; + + /** + * @param LocaleSettings $localeSettings + * @param UserDateTimeFactory $dateTime + */ + public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime) { $this->localeSettings = $localeSettings; + $this->dateTime = $dateTime; } /** @@ -40,12 +48,15 @@ class DatePickerType extends AbstractType { $pickerFormat = $this->localeSettings->getDatePickerFormat(); $dateFormat = $this->localeSettings->getDateTypeFormat(); + $timezone = $this->dateTime->getTimezone()->getName(); $resolver->setDefaults([ 'widget' => 'single_text', 'html5' => false, 'format' => $dateFormat, 'format_picker' => $pickerFormat, + 'model_timezone' => $timezone, + 'view_timezone' => $timezone, ]); $resolver->setDefault('attr', function (Options $options) { diff --git a/src/Form/Type/DateTimePickerType.php b/src/Form/Type/DateTimePickerType.php index 27cb7467..007191a0 100644 --- a/src/Form/Type/DateTimePickerType.php +++ b/src/Form/Type/DateTimePickerType.php @@ -9,6 +9,7 @@ namespace App\Form\Type; +use App\Timesheet\UserDateTimeFactory; use App\Utils\LocaleSettings; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; @@ -25,12 +26,18 @@ class DateTimePickerType extends AbstractType */ protected $localeSettings; + /** + * @var UserDateTimeFactory + */ + protected $dateTime; + /** * @param LocaleSettings $localeSettings */ - public function __construct(LocaleSettings $localeSettings) + public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime) { $this->localeSettings = $localeSettings; + $this->dateTime = $dateTime; } /** @@ -40,6 +47,7 @@ class DateTimePickerType extends AbstractType { $dateTimePicker = $this->localeSettings->getDateTimePickerFormat(); $dateTimeFormat = $this->localeSettings->getDateTimeTypeFormat(); + $timezone = $this->dateTime->getTimezone()->getName(); $resolver->setDefaults([ 'label' => 'label.begin', @@ -48,6 +56,8 @@ class DateTimePickerType extends AbstractType 'format' => $dateTimeFormat, 'format_picker' => $dateTimePicker, 'with_seconds' => false, + 'model_timezone' => $timezone, + 'view_timezone' => $timezone, ]); $resolver->setDefault('attr', function (Options $options) { diff --git a/src/Repository/TimesheetRepository.php b/src/Repository/TimesheetRepository.php index a6cb01d7..c9e52128 100644 --- a/src/Repository/TimesheetRepository.php +++ b/src/Repository/TimesheetRepository.php @@ -55,8 +55,11 @@ class TimesheetRepository extends AbstractRepository } // seems to be necessary so Doctrine will recognize a changed timestamp - $entry->setBegin(clone $entry->getBegin()); - $entry->setEnd(new DateTime()); + $begin = clone $entry->getBegin(); + $end = new \DateTime('now', $begin->getTimezone()); + + $entry->setBegin($begin); + $entry->setEnd($end); $entityManager = $this->getEntityManager(); $entityManager->persist($entry); @@ -103,11 +106,11 @@ class TimesheetRepository extends AbstractRepository /** * @param $select - * @param User|null $user + * @param User $user * @return int * @throws \Doctrine\ORM\NonUniqueResultException */ - protected function queryThisMonth($select, ?User $user) + protected function queryThisMonth($select, User $user) { $begin = new DateTime('first day of this month 00:00:00'); $end = new DateTime('last day of this month 23:59:59'); diff --git a/src/Timesheet/UserDateTimeFactory.php b/src/Timesheet/UserDateTimeFactory.php new file mode 100644 index 00000000..de7ae505 --- /dev/null +++ b/src/Timesheet/UserDateTimeFactory.php @@ -0,0 +1,82 @@ +getToken()) { + return; + } + + /* @var $user User */ + $user = $tokenStorage->getToken()->getUser(); + $timezone = date_default_timezone_get(); + + if ($user instanceof User && null !== $user->getPreferenceValue('timezone')) { + $timezone = $user->getPreferenceValue('timezone'); + } + + $this->timezone = new \DateTimeZone($timezone); + } + + /** + * @return \DateTimeZone + */ + public function getTimezone() + { + return $this->timezone; + } + + /** + * @return \DateTime + */ + public function getStartOfMonth() + { + $date = $this->createDateTime('first day of this month'); + $date->setTime(0, 0, 0); + + return $date; + } + + /** + * @return \DateTime + */ + public function getEndOfMonth() + { + $date = $this->createDateTime('last day of this month'); + $date->setTime(23, 59, 59); + + return $date; + } + + /** + * @param string $datetime + * @return \DateTime + */ + public function createDateTime(string $datetime = 'now') + { + $date = new \DateTime($datetime, $this->timezone); + + return $date; + } +} diff --git a/src/Twig/DateExtensions.php b/src/Twig/DateExtensions.php index c17f6fef..a5b2e7c6 100644 --- a/src/Twig/DateExtensions.php +++ b/src/Twig/DateExtensions.php @@ -40,6 +40,7 @@ class DateExtensions extends \Twig_Extension new TwigFilter('month_name', [$this, 'monthName']), new TwigFilter('date_short', [$this, 'dateShort']), new TwigFilter('date_time', [$this, 'dateTime']), + new TwigFilter('date_format', [$this, 'dateFormat']), new TwigFilter('time', [$this, 'time']), ]; } @@ -66,6 +67,16 @@ class DateExtensions extends \Twig_Extension return date_format($date, $format); } + /** + * @param DateTime $date + * @param string $format + * @return false|string + */ + public function dateFormat(DateTime $date, string $format) + { + return date_format($date, $format); + } + /** * @param DateTime $date * @return string diff --git a/templates/calendar/user.html.twig b/templates/calendar/user.html.twig index 48d4c440..657bbe91 100644 --- a/templates/calendar/user.html.twig +++ b/templates/calendar/user.html.twig @@ -58,6 +58,8 @@ droppable : false, height: 'auto', nowIndicator: true, + now: '{{ now|date_format('c') }}', + //timezone: '{{ app.user.preferenceValue("timezone") }}', businessHours: { dow: [{{ config.businessDays|join(',') }}], start: '{{ config.businessTimeBegin }}', diff --git a/templates/user/profile.html.twig b/templates/user/profile.html.twig index 7381ed5f..209a8e27 100644 --- a/templates/user/profile.html.twig +++ b/templates/user/profile.html.twig @@ -10,7 +10,7 @@