From cd016ceec2985861fc03296df295ef967fc3854f Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Mon, 29 Jul 2024 16:49:00 +0200 Subject: [PATCH 01/36] db migration for customer sharing --- Migrations/Version20240726082447.php | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 Migrations/Version20240726082447.php diff --git a/Migrations/Version20240726082447.php b/Migrations/Version20240726082447.php new file mode 100644 index 0000000..a5b8189 --- /dev/null +++ b/Migrations/Version20240726082447.php @@ -0,0 +1,50 @@ +addSql(sprintf('ALTER TABLE %s ADD customer_id INT(11) DEFAULT NULL AFTER id', $tableName)); + $this->addSql(sprintf('ALTER TABLE %s MODIFY project_id INT(11) DEFAULT NULL', $tableName)); + $this->addSql(sprintf('ALTER TABLE %s DROP INDEX UNIQ_BE51C9A166D1F9CF06F2E59', $tableName)); + $this->addSql(sprintf('ALTER TABLE %s ADD CONSTRAINT UNIQ_customer_id_project_id_share_key UNIQUE (customer_id, project_id, share_key)', $tableName)); + $this->addSql(sprintf(' + ALTER TABLE %s ADD CONSTRAINT fk_customer + FOREIGN KEY (customer_id) + REFERENCES kimai2_customers (id) + ON DELETE CASCADE + ON UPDATE CASCADE + ', $tableName)); + } + + public function down(Schema $schema): void + { + $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); + $table->dropIndex('UNIQ_customer_id_project_id_share_key'); + $table->addUniqueIndex(['project_id', 'share_key']); + $table->removeForeignKey('fk_customer'); + $table->dropColumn('customer_id'); + $table->modifyColumn('project_id', ['notnull' => true]); + } +} From 276d4056736a3230feb94debe758c881e6bb1148 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 09:58:50 +0200 Subject: [PATCH 02/36] set the project in timerecord model --- Model/TimeRecord.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Model/TimeRecord.php b/Model/TimeRecord.php index b57d94e..82d630b 100644 --- a/Model/TimeRecord.php +++ b/Model/TimeRecord.php @@ -10,6 +10,7 @@ namespace KimaiPlugin\SharedProjectTimesheetsBundle\Model; +use App\Entity\Project; use App\Entity\Timesheet; use App\Entity\User; @@ -33,6 +34,7 @@ public static function fromTimesheet(Timesheet $timesheet, string $mergeMode = R $record = new TimeRecord($timesheet->getBegin(), $timesheet->getUser(), $mergeMode); $record->addTimesheet($timesheet); + $record->setProject($timesheet->getProject()); return $record; } @@ -47,6 +49,7 @@ public static function fromTimesheet(Timesheet $timesheet, string $mergeMode = R private int $duration = 0; private ?User $user = null; private ?string $mergeMode = null; + private Project $project; private function __construct(\DateTimeInterface $date, User $user, string $mergeMode) { @@ -103,6 +106,11 @@ public function addTimesheet(Timesheet $timesheet): void $this->setDescription($timesheet); } + public function getProject(): ?Project + { + return $this->project ?? null; + } + protected function addHourlyRate(?float $hourlyRate, ?int $duration): void { if ($hourlyRate > 0 && $duration > 0) { @@ -161,4 +169,9 @@ protected function setDescription(Timesheet $timesheet): void } } } + + protected function setProject(Project $project): void + { + $this->project = $project; + } } From ea2f3fbe66143201e07fc0815cddee9c7f27bc15 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 10:07:13 +0200 Subject: [PATCH 03/36] renamed timesheet to project to reflect its purpose --- .../views/view/{timesheet.html.twig => project.html.twig} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename Resources/views/view/{timesheet.html.twig => project.html.twig} (96%) diff --git a/Resources/views/view/timesheet.html.twig b/Resources/views/view/project.html.twig similarity index 96% rename from Resources/views/view/timesheet.html.twig rename to Resources/views/view/project.html.twig index cd169f0..ee7ad7d 100644 --- a/Resources/views/view/timesheet.html.twig +++ b/Resources/views/view/project.html.twig @@ -1,7 +1,7 @@ {% extends '@theme/fullpage.html.twig' %} {% from "macros/widgets.html.twig" import nothing_found %} -{% block title %}{{ 'shared_project_timesheets.view.title' | trans }} {{ sharedProject.project.name }}{% endblock %} +{% block title %}{{ 'shared_project_timesheets.view.title' | trans }} {{ project.name }}{% endblock %} {% block stylesheets %} {% if tabler_bundle.isRightToLeft() %} @@ -71,7 +71,7 @@ - {{ sharedProject.project.name }} + {{ project.name }} {% if statsPerMonth is not null %} @@ -95,10 +95,10 @@ {% if monthlyChartVisible %}
From acab8033871dd9f1ef3e199402695193597acec7 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 10:08:11 +0200 Subject: [PATCH 04/36] template for timesheet for customer --- Resources/views/view/customer.html.twig | 213 ++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 Resources/views/view/customer.html.twig diff --git a/Resources/views/view/customer.html.twig b/Resources/views/view/customer.html.twig new file mode 100644 index 0000000..fe91ecf --- /dev/null +++ b/Resources/views/view/customer.html.twig @@ -0,0 +1,213 @@ +{% extends '@theme/fullpage.html.twig' %} +{% from "macros/widgets.html.twig" import nothing_found %} + +{% block title %}{{ 'customer' | trans }}: {{ customer.getName() }}{% endblock %} + +{% block stylesheets %} + {% if tabler_bundle.isRightToLeft() %} + {{ encore_entry_link_tags('app-rtl') }} + {% else %} + {{ encore_entry_link_tags('app') }} + {% endif %} + {{ encore_entry_link_tags('chart') }} +{% endblock %} + +{% block head %} + {{ encore_entry_script_tags('app') }} + {{ encore_entry_script_tags('chart') }} +{% endblock %} + +{% block javascripts %} + {% import "macros/webloader.html.twig" as webloader %} + {{ webloader.init_frontend_loader() }} +{% endblock %} + +{% block page_classes %}page{% endblock %} + +{% block page_content %} +
+ +

+ +
+ 1 %}href="{{ app.request.pathinfo }}?year={{ year }}&month={{ month - 1 }}{% if detailsMode != 'table' %}&details={{ detailsMode }}{% endif %}"{% endif %}> + + +
+ + +
+ + + +
+ +
+ + + +
+ + +
+ + + +
+ + {{ 'customer' | trans }}: {{ sharedProject.customer.name }} +

+ + {% if statsPerMonth is not null %} +
+
+
+
+

{{ 'shared_project_timesheets.view.chart.per_month_title' | trans({'%year%': year}) }}

+
+
+ {% include '@SharedProjectTimesheets/view/chart/annual-chart.html.twig' with {year: year, month: month, statsPerMonth: statsPerMonth} %} +
+
+
+
+ {% endif %} + +
+
+

{{ 'shared_project_timesheets.view.table.title' | trans }}

+ {% if monthlyChartVisible %} +
+ +
+ {% endif %} +
+
+ {% if timeRecords is empty %} + {{ nothing_found() }} + {% elseif statsPerDay is null %} + + + + + {% if sharedProject.entryUserVisible %} + + {% endif %} + + + + {% if sharedProject.entryRateVisible %} + + + {% endif %} + + + {% for record in timeRecords %} + + + {% if sharedProject.entryUserVisible %} + + {% endif %} + + + + {% if sharedProject.entryRateVisible %} + {% if record.differentHourlyRates %} + + {% else %} + + {% endif %} + + {% endif %} + + {% endfor %} + {% if timeRecords is not empty %} + + + + {% if sharedProject.entryUserVisible %} + + {% endif %} + + + + {% if sharedProject.entryRateVisible %} + + + {% endif %} + + + {% endif %} +
{{ 'shared_project_timesheets.view.table.date' | trans }}{{ 'shared_project_timesheets.view.table.user' | trans }}{{ 'project' | trans }}{{ 'shared_project_timesheets.view.table.description' | trans }}{{ 'duration'|trans }}{{ 'hourlyRate' | trans }}{{ 'total_rate' | trans }}
{{ record.date | date_short }}{{ record.user.displayName }}{{ record.getProject.name }}{{ record.description | e | nl2br }}{{ record.duration | duration }} + {% for info in record.hourlyRates %} +
{{ info.duration | duration }} - {{ info.hourlyRate | format_currency(currency) }}
+ {% endfor %} +
+ {% if record.hourlyRates is not empty %} + {{ record.hourlyRates[0].hourlyRate | format_currency(currency) }} + {% endif %} + {{ record.rate | format_currency(currency) }}
{{ durationSum | duration }}{{ rateSum | format_currency(currency) }}
+ {% else %} + {% include '@SharedProjectTimesheets/view/chart/monthly-chart.html.twig' with {year: year, month: month, statsPerDay: statsPerDay} %} + {% endif %} +
+
+ + + +
+
+

{{ 'projects'|trans }}

+
+
+ {% if projects is empty %} + {{ nothing_found() }} + {% else %} + + + + + + + + {% for project in projects %} + + + + + {% endfor %} +
NAMECOMMENT
+ {% endif %} +
+
+ +{% endblock %} \ No newline at end of file From b51fc1efbefc8bc9a26e207385215182f280eebf Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 10:16:16 +0200 Subject: [PATCH 05/36] update entity: add customer relation + type getter --- Entity/SharedProjectTimesheet.php | 43 +++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/Entity/SharedProjectTimesheet.php b/Entity/SharedProjectTimesheet.php index b9f70c7..5e6e41d 100644 --- a/Entity/SharedProjectTimesheet.php +++ b/Entity/SharedProjectTimesheet.php @@ -10,27 +10,36 @@ namespace KimaiPlugin\SharedProjectTimesheetsBundle\Entity; +use App\Entity\Customer; use App\Entity\Project; use Doctrine\ORM\Mapping as ORM; use KimaiPlugin\SharedProjectTimesheetsBundle\Model\RecordMergeMode; use KimaiPlugin\SharedProjectTimesheetsBundle\Repository\SharedProjectTimesheetRepository; +use LogicException; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Table(name: 'kimai2_shared_project_timesheets')] +#[ORM\Index(columns: ['customer_id'])] #[ORM\Index(columns: ['project_id'])] #[ORM\Index(columns: ['share_key'])] -#[ORM\Index(columns: ['project_id', 'share_key'])] +#[ORM\Index(columns: ['customer_id', 'project_id', 'share_key'])] #[ORM\Entity(repositoryClass: SharedProjectTimesheetRepository::class)] class SharedProjectTimesheet { + public const TYPE_PROJECT = 'project'; + public const TYPE_CUSTOMER = 'customer'; + #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\Column(name: 'id', type: 'integer')] private ?int $id = null; + #[ORM\ManyToOne(targetEntity: Customer::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')] + private ?Customer $customer = null; + #[ORM\ManyToOne(targetEntity: Project::class)] #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')] - #[Assert\NotNull] private ?Project $project = null; #[ORM\Column(name: 'share_key', type: 'string', length: 20, nullable: false)] @@ -78,6 +87,16 @@ public function setProject(Project $project): void $this->project = $project; } + public function getCustomer(): ?Customer + { + return $this->customer; + } + + public function setCustomer(Customer $customer): void + { + $this->customer = $customer; + } + public function getShareKey(): ?string { return $this->shareKey; @@ -172,4 +191,24 @@ public function setTimeBudgetStatsVisible(bool $timeBudgetStatsVisible): void { $this->timeBudgetStatsVisible = $timeBudgetStatsVisible; } + + public function getType(): ?string + { + $customer = $this->getCustomer(); + $project = $this->getProject(); + + if (isset($customer, $project)) { + throw new LogicException('Invalid state: customer and project cannot be filled both'); + } + + if (isset($customer)) { + return static::TYPE_CUSTOMER; + } + + if (isset($project)) { + return static::TYPE_PROJECT; + } + + return null; + } } From ec54c2115f92bfa16c3d696084e1fc7711abdfe8 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 10:21:24 +0200 Subject: [PATCH 06/36] two separate forms, one for each type --- Controller/ManageController.php | 26 ++++++++++++++++++-------- Form/SharedCustomerFormType.php | 13 +++++++++++++ Form/SharedProjectFormType.php | 19 ++++++++++++++++--- 3 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 Form/SharedCustomerFormType.php diff --git a/Controller/ManageController.php b/Controller/ManageController.php index a4281ba..cdfbaaf 100644 --- a/Controller/ManageController.php +++ b/Controller/ManageController.php @@ -14,7 +14,9 @@ use App\Repository\Query\BaseQuery; use App\Utils\DataTable; use App\Utils\PageSetup; +use InvalidArgumentException; use KimaiPlugin\SharedProjectTimesheetsBundle\Entity\SharedProjectTimesheet; +use KimaiPlugin\SharedProjectTimesheetsBundle\Form\SharedCustomerFormType; use KimaiPlugin\SharedProjectTimesheetsBundle\Form\SharedProjectFormType; use KimaiPlugin\SharedProjectTimesheetsBundle\Model\RecordMergeMode; use KimaiPlugin\SharedProjectTimesheetsBundle\Repository\SharedProjectTimesheetRepository; @@ -70,11 +72,21 @@ public function index(): Response #[Route(path: '/create', name: 'create_shared_project_timesheets', methods: ['GET', 'POST'])] public function create(Request $request): Response { + $type = $request->query->get('type'); + + if (!in_array($type, [SharedProjectTimesheet::TYPE_CUSTOMER, SharedProjectTimesheet::TYPE_PROJECT])) { + throw new InvalidArgumentException('Invalid value for type'); + } + $sharedProject = new SharedProjectTimesheet(); - $form = $this->createForm(SharedProjectFormType::class, $sharedProject, [ + $formClass = $type === SharedProjectTimesheet::TYPE_CUSTOMER ? + SharedCustomerFormType::class : + SharedProjectFormType::class; + + $form = $this->createForm($formClass, $sharedProject, [ 'method' => 'POST', - 'action' => $this->generateUrl('create_shared_project_timesheets') + 'action' => $this->generateUrl('create_shared_project_timesheets', ['type' => $type]), ]); $form->handleRequest($request); @@ -102,13 +114,11 @@ public function update(string $projectId, string $shareKey, Request $request): R throw $this->createNotFoundException('Project not found'); } - /** @var SharedProjectTimesheet $sharedProject */ - $sharedProject = $this->shareProjectTimesheetRepository->findOneBy(['project' => $projectId, 'shareKey' => $shareKey]); - if ($sharedProject === null) { - throw $this->createNotFoundException('Given project not found'); - } + $formClass = $sharedProject->getType() === SharedProjectTimesheet::TYPE_CUSTOMER ? + SharedCustomerFormType::class : + SharedProjectFormType::class; - // Store data in temporary SharedProjectTimesheet object + $form = $this->createForm($formClass, $sharedProject, [ $form = $this->createForm(SharedProjectFormType::class, $sharedProject, [ 'method' => 'POST', 'action' => $this->generateUrl('update_shared_project_timesheets', ['projectId' => $projectId, 'shareKey' => $shareKey]) diff --git a/Form/SharedCustomerFormType.php b/Form/SharedCustomerFormType.php new file mode 100644 index 0000000..5b0c98d --- /dev/null +++ b/Form/SharedCustomerFormType.php @@ -0,0 +1,13 @@ +add('project', ProjectType::class, [ + if ($this->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + $builder->add('project', ProjectType::class, [ 'required' => true, - ]) + ]); + } elseif ($this->getType() === SharedProjectTimesheet::TYPE_CUSTOMER) { + $builder->add('customer', CustomerType::class, [ + 'required' => true, + ]); + } + + $builder ->add('recordMergeMode', ChoiceType::class, [ 'label' => 'shared_project_timesheets.manage.form.record_merge_mode', 'required' => true, @@ -112,4 +120,9 @@ public function finishView(FormView $view, FormInterface $form, array $options): $view['password']->vars['value'] = ManageService::PASSWORD_DO_NOT_CHANGE_VALUE; } } + + protected function getType(): string + { + return SharedProjectTimesheet::TYPE_PROJECT; + } } From 50ed27dcbc1f1c4a3b591e617f8c308d5caa6804 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 10:35:21 +0200 Subject: [PATCH 07/36] add type column to index table --- Controller/ManageController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Controller/ManageController.php b/Controller/ManageController.php index cdfbaaf..da6c08e 100644 --- a/Controller/ManageController.php +++ b/Controller/ManageController.php @@ -48,6 +48,7 @@ public function index(): Response $table->setReloadEvents('kimai.sharedProject'); $table->addColumn('name', ['class' => 'alwaysVisible', 'orderBy' => false]); + $table->addColumn('type', ['class' => 'alwaysVisible', 'orderBy' => false]); $table->addColumn('url', ['class' => 'alwaysVisible', 'orderBy' => false]); $table->addColumn('password', ['class' => 'd-none', 'orderBy' => false]); $table->addColumn('record_merge_mode', ['class' => 'd-none text-center w-min', 'orderBy' => false, 'title' => 'shared_project_timesheets.manage.table.record_merge_mode']); From b193051cf02033e708133f73e2ac6ed2a26a655f Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 10:55:22 +0200 Subject: [PATCH 08/36] modify update/delete urls, we cannot use project_id anymore --- Controller/ManageController.php | 24 ++++++--------------- EventSubscriber/SharedProjectSubscriber.php | 3 ++- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/Controller/ManageController.php b/Controller/ManageController.php index da6c08e..a123e5f 100644 --- a/Controller/ManageController.php +++ b/Controller/ManageController.php @@ -108,10 +108,10 @@ public function create(Request $request): Response ]); } - #[Route(path: '/{projectId}/{shareKey}', name: 'update_shared_project_timesheets', methods: ['GET', 'POST'])] - public function update(string $projectId, string $shareKey, Request $request): Response + #[Route(path: '/{id}/{shareKey}', name: 'update_shared_project_timesheets', methods: ['GET', 'POST'])] + public function update(SharedProjectTimesheet $sharedProject, string $shareKey, Request $request): Response { - if ($projectId == null || $shareKey == null) { + if ($shareKey == null || $sharedProject->getShareKey() !== $shareKey) { throw $this->createNotFoundException('Project not found'); } @@ -120,9 +120,8 @@ public function update(string $projectId, string $shareKey, Request $request): R SharedProjectFormType::class; $form = $this->createForm($formClass, $sharedProject, [ - $form = $this->createForm(SharedProjectFormType::class, $sharedProject, [ 'method' => 'POST', - 'action' => $this->generateUrl('update_shared_project_timesheets', ['projectId' => $projectId, 'shareKey' => $shareKey]) + 'action' => $this->generateUrl('update_shared_project_timesheets', ['id' => $sharedProject->getId(), 'shareKey' => $shareKey]) ]); $form->handleRequest($request); @@ -147,22 +146,13 @@ public function update(string $projectId, string $shareKey, Request $request): R ]); } - #[Route(path: '/{projectId}/{shareKey}/remove', name: 'remove_shared_project_timesheets', methods: ['GET', 'POST'])] - public function remove(Request $request): Response + #[Route(path: '/{id}/{shareKey}/remove', name: 'remove_shared_project_timesheets', methods: ['GET', 'POST'])] + public function remove(SharedProjectTimesheet $sharedProject, string $shareKey): Response { - $projectId = $request->get('projectId'); - $shareKey = $request->get('shareKey'); - - if ($projectId == null || $shareKey == null) { + if ($shareKey == null || $sharedProject->getShareKey() !== $shareKey) { throw $this->createNotFoundException('Project not found'); } - /** @var SharedProjectTimesheet $sharedProject */ - $sharedProject = $this->shareProjectTimesheetRepository->findOneBy(['project' => $projectId, 'shareKey' => $shareKey]); - if (!$sharedProject || $sharedProject->getProject() === null || $sharedProject->getShareKey() === null) { - throw $this->createNotFoundException('Given project not found'); - } - try { $this->shareProjectTimesheetRepository->remove($sharedProject); $this->flashSuccess('action.delete.success'); diff --git a/EventSubscriber/SharedProjectSubscriber.php b/EventSubscriber/SharedProjectSubscriber.php index 85f08c6..d03154f 100644 --- a/EventSubscriber/SharedProjectSubscriber.php +++ b/EventSubscriber/SharedProjectSubscriber.php @@ -36,7 +36,8 @@ public function onActions(PageActionsEvent $event): void return; } - $event->addEdit($this->path('update_shared_project_timesheets', ['projectId' => $sharedProject->getProject()->getId(), 'shareKey' => $sharedProject->getShareKey()])); + $event->addEdit($this->path('update_shared_project_timesheets', ['id' => $sharedProject->getId(), 'shareKey' => $sharedProject->getShareKey()])); + $event->addAction('project', ['url' => $this->path('project_details', ['id' => $sharedProject->getProject()->getId()])]); $event->addDelete($this->path('remove_shared_project_timesheets', ['projectId' => $sharedProject->getProject()->getId(), 'shareKey' => $sharedProject->getShareKey()]), false); } From f1c144dd4018ded7584ec41c233d530585b3b55b Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 10:59:37 +0200 Subject: [PATCH 09/36] correct links in action buttons --- EventSubscriber/SharedProjectSubscriber.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/EventSubscriber/SharedProjectSubscriber.php b/EventSubscriber/SharedProjectSubscriber.php index d03154f..f77e5a8 100644 --- a/EventSubscriber/SharedProjectSubscriber.php +++ b/EventSubscriber/SharedProjectSubscriber.php @@ -32,13 +32,18 @@ public function onActions(PageActionsEvent $event): void /** @var SharedProjectTimesheet $sharedProject */ $sharedProject = $payload['shared_project']; - if ($sharedProject->getId() === null || $sharedProject->getProject() === null) { + if ($sharedProject->getId() === null || $sharedProject->getType() === null) { return; } $event->addEdit($this->path('update_shared_project_timesheets', ['id' => $sharedProject->getId(), 'shareKey' => $sharedProject->getShareKey()])); - $event->addAction('project', ['url' => $this->path('project_details', ['id' => $sharedProject->getProject()->getId()])]); - $event->addDelete($this->path('remove_shared_project_timesheets', ['projectId' => $sharedProject->getProject()->getId(), 'shareKey' => $sharedProject->getShareKey()]), false); + if ($sharedProject->getType() === SharedProjectTimesheet::TYPE_CUSTOMER) { + $event->addAction('customer', ['url' => $this->path('customer_details', ['id' => $sharedProject->getCustomer()->getId()])]); + } else { + $event->addAction('project', ['url' => $this->path('project_details', ['id' => $sharedProject->getProject()->getId()])]); + } + + $event->addDelete($this->path('remove_shared_project_timesheets', ['id' => $sharedProject->getId(), 'shareKey' => $sharedProject->getShareKey()]), false); } } From cf36c38b02f66489394fb08c719580714aad331e Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 11:00:48 +0200 Subject: [PATCH 10/36] added submenu items to create button for each type --- EventSubscriber/SharedProjectsSubscriber.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/EventSubscriber/SharedProjectsSubscriber.php b/EventSubscriber/SharedProjectsSubscriber.php index 276c62c..2dba960 100644 --- a/EventSubscriber/SharedProjectsSubscriber.php +++ b/EventSubscriber/SharedProjectsSubscriber.php @@ -12,6 +12,7 @@ use App\Event\PageActionsEvent; use App\EventSubscriber\Actions\AbstractActionsSubscriber; +use KimaiPlugin\SharedProjectTimesheetsBundle\Entity\SharedProjectTimesheet; class SharedProjectsSubscriber extends AbstractActionsSubscriber { @@ -23,5 +24,17 @@ public static function getActionName(): string public function onActions(PageActionsEvent $event): void { $event->addCreate($this->path('create_shared_project_timesheets')); + + $event->addActionToSubmenu('create', 'project', [ + 'url' => $this->path('create_shared_project_timesheets', ['type' => SharedProjectTimesheet::TYPE_PROJECT]), + 'class' => 'action-create modal-ajax-form', + 'title' => 'project', + ]); + + $event->addActionToSubmenu('create', 'customer', [ + 'url' => $this->path('create_shared_project_timesheets', ['type' => SharedProjectTimesheet::TYPE_CUSTOMER]), + 'class' => 'action-create modal-ajax-form', + 'title' => 'customer', + ]); } } From 24669b18bb5b4c1a1149c581bf9b09f7c2ea58c5 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 11:07:19 +0200 Subject: [PATCH 11/36] update index table template with extra col + correct urls --- Resources/views/manage/index.html.twig | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Resources/views/manage/index.html.twig b/Resources/views/manage/index.html.twig index 8991dd0..eb27564 100644 --- a/Resources/views/manage/index.html.twig +++ b/Resources/views/manage/index.html.twig @@ -2,14 +2,21 @@ {% import "macros/widgets.html.twig" as widgets %} {% import "@SharedProjectTimesheets/manage/actions.html.twig" as actions %} -{% block datatable_row_attr %} class="modal-ajax-form open-edit" data-href="{{ url('update_shared_project_timesheets', {projectId: entry.project.id, shareKey: entry.shareKey}) }}"{% endblock %} +{# TODO base update url on id, not projectId! #} +{% block datatable_row_attr %} class="modal-ajax-form open-edit" data-href="{{ url('update_shared_project_timesheets', {id: entry.id, shareKey: entry.shareKey}) }}"{% endblock %} {% block datatable_column_value %} {% if column == 'name' %} - {{ entry.project.name }} + {{ entry.project.name ?? entry.customer.name }} + {% elseif column == 'type' %} + {{ entry.getType() | trans }} {% elseif column == 'url' %} {% if entry.shareKey %} - {% set p_url = url('view_shared_project_timesheets', {id: entry.project.id, shareKey: entry.shareKey}) %} + {% if entry.project %} + {% set p_url = url('view_shared_project_timesheets', {id: entry.project.id, shareKey: entry.shareKey}) %} + {% elseif entry.customer %} + {% set p_url = url('view_shared_project_timesheets_customer', {customer: entry.customer.id, shareKey: entry.shareKey}) %} + {% endif %} {{ p_url }} From 30824fea55936616004558a271a319b1357b0087 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 11:16:46 +0200 Subject: [PATCH 12/36] more changes to handle the new type --- .../SharedProjectTimesheetRepository.php | 44 +++++++++++++++++-- Service/ManageService.php | 22 ++++++---- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/Repository/SharedProjectTimesheetRepository.php b/Repository/SharedProjectTimesheetRepository.php index 1e38e48..fc648c8 100644 --- a/Repository/SharedProjectTimesheetRepository.php +++ b/Repository/SharedProjectTimesheetRepository.php @@ -10,10 +10,12 @@ namespace KimaiPlugin\SharedProjectTimesheetsBundle\Repository; +use App\Entity\Customer; use App\Entity\Project; use App\Repository\Loader\DefaultLoader; use App\Repository\Paginator\LoaderPaginator; use App\Repository\Query\BaseQuery; +use App\Repository\Query\ProjectQuery; use App\Utils\Pagination; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\NonUniqueResultException; @@ -28,8 +30,9 @@ class SharedProjectTimesheetRepository extends EntityRepository public function findAllSharedProjects(BaseQuery $query): Pagination { $qb = $this->createQueryBuilder('spt') - ->join(Project::class, 'p', Join::WITH, 'spt.project = p') - ->orderBy('p.name, spt.shareKey', 'ASC'); + ->leftJoin(Project::class, 'p', Join::WITH, 'spt.project = p') + ->leftJoin(Customer::class, 'c', Join::WITH, 'spt.customer = c') + ->orderBy('p.name, c.name, spt.shareKey', 'ASC'); $loader = new LoaderPaginator(new DefaultLoader(), $qb, $this->count([])); @@ -55,6 +58,7 @@ public function findByProjectAndShareKey(Project|int|null $project, ?string $sha try { return $this->createQueryBuilder('spt') ->where('spt.project = :project') + ->andWhere('spt.customer is null') ->andWhere('spt.shareKey = :shareKey') ->setMaxResults(1) ->setParameter('project', $project) @@ -62,8 +66,42 @@ public function findByProjectAndShareKey(Project|int|null $project, ?string $sha ->getQuery() ->getOneOrNullResult(); } catch (NonUniqueResultException $e) { - // We can ignore that as we have a unique database key for project and shareKey + // We can ignore that as we have a unique database key for project/customer/shareKey return null; } } + + public function findByCustomerAndShareKey(Customer|int $customer, ?string $shareKey): ?SharedProjectTimesheet + { + try { + return $this->createQueryBuilder('spt') + ->where('spt.project is null') + ->andWhere('spt.customer = :customer') + ->andWhere('spt.shareKey = :shareKey') + ->setMaxResults(1) + ->setParameter('customer', $customer) + ->setParameter('shareKey', $shareKey) + ->getQuery() + ->getOneOrNullResult(); + } catch (NonUniqueResultException $e) { + // We can ignore that as we have a unique database key for project/customer/shareKey + return null; + } + } + + /** + * @param SharedProjectTimesheet $sharedProject + * @return Project[] + */ + public function getProjects(SharedProjectTimesheet $sharedProject): array + { + if ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + return [$sharedProject->getProject()]; + } + + /** @var \App\Repository\ProjectRepository $projectRepository */ + $projectRepository = $this->_em->getRepository(Project::class); + + return (array)$projectRepository->getProjectsForQuery((new ProjectQuery())->setCustomers([$sharedProject->getCustomer()])); + } } diff --git a/Service/ManageService.php b/Service/ManageService.php index e950dfb..3df71fc 100644 --- a/Service/ManageService.php +++ b/Service/ManageService.php @@ -10,7 +10,6 @@ namespace KimaiPlugin\SharedProjectTimesheetsBundle\Service; -use App\Entity\Project; use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\ORMException; use KimaiPlugin\SharedProjectTimesheetsBundle\Entity\SharedProjectTimesheet; @@ -46,10 +45,18 @@ public function create(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ substr(preg_replace('/[^A-Za-z0-9]+/', '', $this->getUuidV4()), 0, 12) ); - $existingEntry = $this->sharedProjectTimesheetRepository->findByProjectAndShareKey( - $sharedProjectTimesheet->getProject(), - $sharedProjectTimesheet->getShareKey() - ); + if ($sharedProjectTimesheet->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + $existingEntry = $this->sharedProjectTimesheetRepository->findByProjectAndShareKey( + $sharedProjectTimesheet->getProject(), + $sharedProjectTimesheet->getShareKey() + ); + } elseif ($sharedProjectTimesheet->getType() === SharedProjectTimesheet::TYPE_CUSTOMER) { + $existingEntry = $this->sharedProjectTimesheetRepository->findByCustomerAndShareKey( + $sharedProjectTimesheet->getCustomer(), + $sharedProjectTimesheet->getShareKey() + ); + } + } while ($existingEntry !== null); } @@ -69,9 +76,8 @@ public function update(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ throw new \InvalidArgumentException('Cannot update shared project timesheet with share key equals null'); } - // Ensure project - if (!($sharedProjectTimesheet->getProject() instanceof Project)) { - throw new \InvalidArgumentException("Project of shared project timesheet is not an instance of App\Entity\Project"); + if ($sharedProjectTimesheet->getType() === null) { + throw new \InvalidArgumentException('No valid project or customer specified'); } // Handle password From 672fe83d29b8d6255a99ad7aa2fbf4c4e98a6072 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 11:18:36 +0200 Subject: [PATCH 13/36] url for shared customer timesheets + refactor project view logic --- Controller/ViewController.php | 145 ++++++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 5 deletions(-) diff --git a/Controller/ViewController.php b/Controller/ViewController.php index 07add32..0dc0f7e 100644 --- a/Controller/ViewController.php +++ b/Controller/ViewController.php @@ -11,8 +11,11 @@ namespace KimaiPlugin\SharedProjectTimesheetsBundle\Controller; use App\Controller\AbstractController; +use App\Customer\CustomerStatisticService; +use App\Entity\Customer; use App\Entity\Project; use App\Project\ProjectStatisticService; +use KimaiPlugin\SharedProjectTimesheetsBundle\Entity\SharedProjectTimesheet; use KimaiPlugin\SharedProjectTimesheetsBundle\Repository\SharedProjectTimesheetRepository; use KimaiPlugin\SharedProjectTimesheetsBundle\Service\ViewService; use Symfony\Component\HttpFoundation\Request; @@ -33,9 +36,6 @@ public function indexAction( ): Response { $givenPassword = $request->get('spt-password'); - $year = (int) $request->get('year', date('Y')); - $month = (int) $request->get('month', date('m')); - $detailsMode = $request->get('details', 'table'); // Get project. $sharedProject = $sharedProjectTimesheetRepository->findByProjectAndShareKey( @@ -55,6 +55,46 @@ public function indexAction( ]); } + return $this->renderProjectView( + $sharedProject, + $sharedProject->getProject(), + $request, + $viewService, + $statisticsService, + ); + } + + #[Route(path: '/customer/{customer}/{shareKey}', name: 'view_shared_project_timesheets_customer', methods: ['GET', 'POST'])] + public function viewCustomerAction( + Customer $customer, + string $shareKey, + Request $request, + CustomerStatisticService $statisticsService, + ViewService $viewService, + SharedProjectTimesheetRepository $sharedProjectTimesheetRepository, + ): Response + { + $givenPassword = $request->get('spt-password'); + $year = (int) $request->get('year', date('Y')); + $month = (int) $request->get('month', date('m')); + $detailsMode = $request->get('details', 'table'); + $sharedProject = $sharedProjectTimesheetRepository->findByCustomerAndShareKey( + $customer, + $shareKey + ); + + if ($sharedProject === null) { + throw $this->createNotFoundException('Project not found'); + } + + // Check access. + if (!$viewService->hasAccess($sharedProject, $givenPassword)) { + return $this->render('@SharedProjectTimesheets/view/auth.html.twig', [ + 'project' => $sharedProject->getCustomer(), + 'invalidPassword' => $request->isMethod('POST') && $givenPassword !== null, + ]); + } + // Get time records. $timeRecords = $viewService->getTimeRecords($sharedProject, $year, $month); @@ -66,6 +106,101 @@ public function indexAction( $durationSum += $record->getDuration(); } + // Define currency. + $currency = $customer->getCurrency(); + + // Prepare stats for charts. + $annualChartVisible = $sharedProject->isAnnualChartVisible(); + $monthlyChartVisible = $sharedProject->isMonthlyChartVisible(); + + $statsPerMonth = $annualChartVisible ? $viewService->getAnnualStats($sharedProject, $year) : null; + $statsPerDay = ($monthlyChartVisible && $detailsMode === 'chart') + ? $viewService->getMonthlyStats($sharedProject, $year, $month) : null; + + // we cannot call $this->getDateTimeFactory() as it throws a AccessDeniedException for anonymous users + $timezone = $customer->getTimezone() ?? date_default_timezone_get(); + $date = new \DateTimeImmutable('now', new \DateTimeZone($timezone)); + $stats = $statisticsService->getBudgetStatisticModel($customer, $date); + $projects = $sharedProjectTimesheetRepository->getProjects($sharedProject); + + return $this->render('@SharedProjectTimesheets/view/customer.html.twig', [ + 'sharedProject' => $sharedProject, + 'customer' => $customer, + 'projects' => $projects, + 'shareKey' => $shareKey, + 'timeRecords' => $timeRecords, + 'rateSum' => $rateSum, + 'durationSum' => $durationSum, + 'year' => $year, + 'month' => $month, + 'currency' => $currency, + 'statsPerMonth' => $statsPerMonth, + 'monthlyChartVisible' => $monthlyChartVisible, + 'statsPerDay' => $statsPerDay, + 'detailsMode' => $detailsMode, + 'stats' => $stats, + ]); + } + + #[Route(path: '/customer/{customer}/{shareKey}/project/{project}', name: 'view_shared_project_timesheets_project', methods: ['GET', 'POST'])] + public function viewProjectAction( + Customer $customer, + string $shareKey, + Project $project, + Request $request, + ProjectStatisticService $statisticsService, + ViewService $viewService, + SharedProjectTimesheetRepository $sharedProjectTimesheetRepository, + ): Response + { + $givenPassword = $request->get('spt-password'); + $sharedProject = $sharedProjectTimesheetRepository->findByCustomerAndShareKey( + $customer, + $shareKey + ); + + if ($sharedProject === null) { + throw $this->createNotFoundException('Project not found'); + } + + // Check access. + if (!$viewService->hasAccess($sharedProject, $givenPassword)) { + return $this->render('@SharedProjectTimesheets/view/auth.html.twig', [ + 'project' => $sharedProject->getProject(), + 'invalidPassword' => $request->isMethod('POST') && $givenPassword !== null, + ]); + } + + return $this->renderProjectView( + $sharedProject, + $project, + $request, + $viewService, + $statisticsService, + ); + } + + protected function renderProjectView( + SharedProjectTimesheet $sharedProject, + Project $project, + Request $request, + ViewService $viewService, + ProjectStatisticService $statisticsService, + ): Response + { + $year = (int) $request->get('year', date('Y')); + $month = (int) $request->get('month', date('m')); + $detailsMode = $request->get('details', 'table'); + $timeRecords = $viewService->getTimeRecords($sharedProject, $year, $month); + + // Calculate summary. + $rateSum = 0; + $durationSum = 0; + foreach($timeRecords as $record) { + $rateSum += $record->getRate(); + $durationSum += $record->getDuration(); + } + // Define currency. $currency = 'EUR'; $customer = $sharedProject->getProject()?->getCustomer(); @@ -76,7 +211,6 @@ public function indexAction( // Prepare stats for charts. $annualChartVisible = $sharedProject->isAnnualChartVisible(); $monthlyChartVisible = $sharedProject->isMonthlyChartVisible(); - $statsPerMonth = $annualChartVisible ? $viewService->getAnnualStats($sharedProject, $year) : null; $statsPerDay = ($monthlyChartVisible && $detailsMode === 'chart') ? $viewService->getMonthlyStats($sharedProject, $year, $month) : null; @@ -87,7 +221,7 @@ public function indexAction( $stats = $statisticsService->getBudgetStatisticModel($project, $date); - return $this->render('@SharedProjectTimesheets/view/timesheet.html.twig', [ + return $this->render('@SharedProjectTimesheets/view/project.html.twig', [ 'sharedProject' => $sharedProject, 'timeRecords' => $timeRecords, 'rateSum' => $rateSum, @@ -100,6 +234,7 @@ public function indexAction( 'statsPerDay' => $statsPerDay, 'detailsMode' => $detailsMode, 'stats' => $stats, + 'project' => $project, ]); } } From cc8591edf0660aadb780ef7973c2bfc6dd34d954 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 11:20:44 +0200 Subject: [PATCH 14/36] updates for stats per customer --- Service/ViewService.php | 80 ++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/Service/ViewService.php b/Service/ViewService.php index d7e3461..801ba94 100644 --- a/Service/ViewService.php +++ b/Service/ViewService.php @@ -19,13 +19,19 @@ use KimaiPlugin\SharedProjectTimesheetsBundle\Model\ChartStat; use KimaiPlugin\SharedProjectTimesheetsBundle\Model\RecordMergeMode; use KimaiPlugin\SharedProjectTimesheetsBundle\Model\TimeRecord; +use KimaiPlugin\SharedProjectTimesheetsBundle\Repository\SharedProjectTimesheetRepository; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; use Symfony\Component\PasswordHasher\PasswordHasherInterface; class ViewService { - public function __construct(private TimesheetRepository $timesheetRepository, private RequestStack $request, private PasswordHasherFactoryInterface $passwordHasherFactory) + public function __construct( + private TimesheetRepository $timesheetRepository, + private RequestStack $request, + private PasswordHasherFactoryInterface $passwordHasherFactory, + private SharedProjectTimesheetRepository $sharedTimesheetRepository, + ) { } @@ -43,11 +49,10 @@ public function hasAccess(SharedProjectTimesheet $sharedProject, ?string $givenP if ($hashedPassword !== null) { // Check session - $projectId = $sharedProject->getProject()->getId(); $shareKey = $sharedProject->getShareKey(); $passwordMd5 = md5($hashedPassword); - $sessionPasswordKey = "spt-authed-$projectId-$shareKey-$passwordMd5"; + $sessionPasswordKey = sprintf('spt-authed-%d-%s-%s', $sharedProject->getId(), $shareKey, $passwordMd5); if (!$this->request->getSession()->has($sessionPasswordKey)) { // Check given password @@ -83,7 +88,11 @@ public function getTimeRecords(SharedProjectTimesheet $sharedProject, int $year, $query = new TimesheetQuery(); $query->setBegin($begin); $query->setEnd($end); - $query->addProject($sharedProject->getProject()); + + foreach ($this->sharedTimesheetRepository->getProjects($sharedProject) as $project) { + $query->addProject($project); + } + $query->setOrderBy('begin'); $query->setOrder(BaseQuery::ORDER_ASC); @@ -140,21 +149,35 @@ public function getTimeRecords(SharedProjectTimesheet $sharedProject, int $year, */ public function getAnnualStats(SharedProjectTimesheet $sharedProject, int $year): array { - $result = $this->timesheetRepository->createQueryBuilder('t') + $queryBuilder = $this->timesheetRepository->createQueryBuilder('t') ->select([ 'YEAR(t.begin) as year', 'MONTH(t.begin) as month', 'SUM(t.duration) as duration', 'SUM(t.rate) as rate', ]) - ->where('t.project = :project') - ->andWhere('YEAR(t.begin) = :year') + ->where('YEAR(t.begin) = :year') ->groupBy('year') - ->addGroupBy('month') - ->setParameters([ - 'project' => $sharedProject->getProject(), - 'year' => $year, - ]) + ->addGroupBy('month'); + + if ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + $queryBuilder = $queryBuilder + ->andWhere('t.project = :project') + ->setParameters([ + 'project' => $sharedProject->getProject(), + 'year' => $year, + ]); + } else { + $queryBuilder = $queryBuilder + ->innerJoin('t.project', 'p') + ->andWhere('p.customer = :customer') + ->setParameters([ + 'customer' => $sharedProject->getCustomer(), + 'year' => $year, + ]); + } + + $result = $queryBuilder ->getQuery() ->getArrayResult(); @@ -185,7 +208,7 @@ public function getAnnualStats(SharedProjectTimesheet $sharedProject, int $year) */ public function getMonthlyStats(SharedProjectTimesheet $sharedProject, int $year, int $month): array { - $result = $this->timesheetRepository->createQueryBuilder('t') + $queryBuilder = $this->timesheetRepository->createQueryBuilder('t') ->select([ 'YEAR(t.begin) as year', 'MONTH(t.begin) as month', @@ -193,17 +216,32 @@ public function getMonthlyStats(SharedProjectTimesheet $sharedProject, int $year 'SUM(t.duration) as duration', 'SUM(t.rate) as rate', ]) - ->where('t.project = :project') - ->andWhere('YEAR(t.begin) = :year') + ->where('YEAR(t.begin) = :year') ->andWhere('MONTH(t.begin) = :month') ->groupBy('year') ->addGroupBy('month') - ->addGroupBy('day') - ->setParameters([ - 'project' => $sharedProject->getProject(), - 'year' => $year, - 'month' => $month - ]) + ->addGroupBy('day'); + + if ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + $queryBuilder = $queryBuilder + ->andWhere('t.project = :project') + ->setParameters([ + 'project' => $sharedProject->getProject(), + 'year' => $year, + 'month' => $month, + ]); + } else { + $queryBuilder = $queryBuilder + ->innerJoin('t.project', 'p') + ->andWhere('p.customer = :customer') + ->setParameters([ + 'customer' => $sharedProject->getCustomer(), + 'year' => $year, + 'month' => $month, + ]); + } + + $result = $queryBuilder ->getQuery() ->getArrayResult(); From f7ea6e3dc3ef3fe7ac88e64c4d93e4eae9282cfb Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 11:23:37 +0200 Subject: [PATCH 15/36] fixes for phpstan errors --- Service/ManageService.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Service/ManageService.php b/Service/ManageService.php index 3df71fc..ccee1d2 100644 --- a/Service/ManageService.php +++ b/Service/ManageService.php @@ -45,16 +45,16 @@ public function create(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ substr(preg_replace('/[^A-Za-z0-9]+/', '', $this->getUuidV4()), 0, 12) ); - if ($sharedProjectTimesheet->getType() === SharedProjectTimesheet::TYPE_PROJECT) { - $existingEntry = $this->sharedProjectTimesheetRepository->findByProjectAndShareKey( - $sharedProjectTimesheet->getProject(), - $sharedProjectTimesheet->getShareKey() - ); - } elseif ($sharedProjectTimesheet->getType() === SharedProjectTimesheet::TYPE_CUSTOMER) { + if ($sharedProjectTimesheet->getType() === SharedProjectTimesheet::TYPE_CUSTOMER) { $existingEntry = $this->sharedProjectTimesheetRepository->findByCustomerAndShareKey( $sharedProjectTimesheet->getCustomer(), $sharedProjectTimesheet->getShareKey() ); + } else { + $existingEntry = $this->sharedProjectTimesheetRepository->findByProjectAndShareKey( + $sharedProjectTimesheet->getProject(), + $sharedProjectTimesheet->getShareKey() + ); } } while ($existingEntry !== null); From de8bb26678343e9311d8632bce084313c04cd43a Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 11:25:34 +0200 Subject: [PATCH 16/36] code style fixes --- Controller/ManageController.php | 2 +- Form/SharedCustomerFormType.php | 10 +++++++++- Repository/SharedProjectTimesheetRepository.php | 2 +- Service/ManageService.php | 1 - 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Controller/ManageController.php b/Controller/ManageController.php index a123e5f..680c2e2 100644 --- a/Controller/ManageController.php +++ b/Controller/ManageController.php @@ -75,7 +75,7 @@ public function create(Request $request): Response { $type = $request->query->get('type'); - if (!in_array($type, [SharedProjectTimesheet::TYPE_CUSTOMER, SharedProjectTimesheet::TYPE_PROJECT])) { + if (!\in_array($type, [SharedProjectTimesheet::TYPE_CUSTOMER, SharedProjectTimesheet::TYPE_PROJECT])) { throw new InvalidArgumentException('Invalid value for type'); } diff --git a/Form/SharedCustomerFormType.php b/Form/SharedCustomerFormType.php index 5b0c98d..c29079e 100644 --- a/Form/SharedCustomerFormType.php +++ b/Form/SharedCustomerFormType.php @@ -1,5 +1,13 @@ _em->getRepository(Project::class); - return (array)$projectRepository->getProjectsForQuery((new ProjectQuery())->setCustomers([$sharedProject->getCustomer()])); + return (array) $projectRepository->getProjectsForQuery((new ProjectQuery())->setCustomers([$sharedProject->getCustomer()])); } } diff --git a/Service/ManageService.php b/Service/ManageService.php index ccee1d2..44e6bae 100644 --- a/Service/ManageService.php +++ b/Service/ManageService.php @@ -56,7 +56,6 @@ public function create(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ $sharedProjectTimesheet->getShareKey() ); } - } while ($existingEntry !== null); } From ecdc98225256adea7074762bc2b14fdaf922fe34 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 11:31:10 +0200 Subject: [PATCH 17/36] removed todo --- Resources/views/manage/index.html.twig | 1 - 1 file changed, 1 deletion(-) diff --git a/Resources/views/manage/index.html.twig b/Resources/views/manage/index.html.twig index eb27564..a00870d 100644 --- a/Resources/views/manage/index.html.twig +++ b/Resources/views/manage/index.html.twig @@ -2,7 +2,6 @@ {% import "macros/widgets.html.twig" as widgets %} {% import "@SharedProjectTimesheets/manage/actions.html.twig" as actions %} -{# TODO base update url on id, not projectId! #} {% block datatable_row_attr %} class="modal-ajax-form open-edit" data-href="{{ url('update_shared_project_timesheets', {id: entry.id, shareKey: entry.shareKey}) }}"{% endblock %} {% block datatable_column_value %} From 63fd5e649b31c2504863b8f9e208d3171521b05e Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 11:48:41 +0200 Subject: [PATCH 18/36] added null check --- Model/TimeRecord.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Model/TimeRecord.php b/Model/TimeRecord.php index 82d630b..2cadf85 100644 --- a/Model/TimeRecord.php +++ b/Model/TimeRecord.php @@ -34,7 +34,10 @@ public static function fromTimesheet(Timesheet $timesheet, string $mergeMode = R $record = new TimeRecord($timesheet->getBegin(), $timesheet->getUser(), $mergeMode); $record->addTimesheet($timesheet); - $record->setProject($timesheet->getProject()); + + if ($timesheet->getProject() !== null) { + $record->setProject($timesheet->getProject()); + } return $record; } From b1cc62a83e6a8542edf6eb07d12e93dd74c89006 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 13:24:44 +0200 Subject: [PATCH 19/36] bugfix: limit to project --- Controller/ViewController.php | 6 +++--- Service/ViewService.php | 37 ++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Controller/ViewController.php b/Controller/ViewController.php index 0dc0f7e..58d25fd 100644 --- a/Controller/ViewController.php +++ b/Controller/ViewController.php @@ -191,7 +191,7 @@ protected function renderProjectView( $year = (int) $request->get('year', date('Y')); $month = (int) $request->get('month', date('m')); $detailsMode = $request->get('details', 'table'); - $timeRecords = $viewService->getTimeRecords($sharedProject, $year, $month); + $timeRecords = $viewService->getTimeRecords($sharedProject, $year, $month, $project); // Calculate summary. $rateSum = 0; @@ -211,9 +211,9 @@ protected function renderProjectView( // Prepare stats for charts. $annualChartVisible = $sharedProject->isAnnualChartVisible(); $monthlyChartVisible = $sharedProject->isMonthlyChartVisible(); - $statsPerMonth = $annualChartVisible ? $viewService->getAnnualStats($sharedProject, $year) : null; + $statsPerMonth = $annualChartVisible ? $viewService->getAnnualStats($sharedProject, $year, $project) : null; $statsPerDay = ($monthlyChartVisible && $detailsMode === 'chart') - ? $viewService->getMonthlyStats($sharedProject, $year, $month) : null; + ? $viewService->getMonthlyStats($sharedProject, $year, $month, $project) : null; // we cannot call $this->getDateTimeFactory() as it throws a AccessDeniedException for anonymous users $timezone = $project->getCustomer()->getTimezone() ?? date_default_timezone_get(); diff --git a/Service/ViewService.php b/Service/ViewService.php index 801ba94..e641a98 100644 --- a/Service/ViewService.php +++ b/Service/ViewService.php @@ -10,6 +10,7 @@ namespace KimaiPlugin\SharedProjectTimesheetsBundle\Service; +use App\Entity\Project; use App\Repository\Query\BaseQuery; use App\Repository\Query\TimesheetQuery; use App\Repository\TimesheetRepository; @@ -72,12 +73,13 @@ public function hasAccess(SharedProjectTimesheet $sharedProject, ?string $givenP * @param SharedProjectTimesheet $sharedProject * @param int $year * @param int $month + * @param Project|null $limitProject limit to this project * @return TimeRecord[] * @throws \Exception * * @todo Unit test */ - public function getTimeRecords(SharedProjectTimesheet $sharedProject, int $year, int $month): array + public function getTimeRecords(SharedProjectTimesheet $sharedProject, int $year, int $month, ?Project $limitProject = null): array { $month = max(min($month, 12), 1); @@ -89,8 +91,12 @@ public function getTimeRecords(SharedProjectTimesheet $sharedProject, int $year, $query->setBegin($begin); $query->setEnd($end); - foreach ($this->sharedTimesheetRepository->getProjects($sharedProject) as $project) { - $query->addProject($project); + if (isset($limitProject)) { + $query->addProject($limitProject); + } else { + foreach ($this->sharedTimesheetRepository->getProjects($sharedProject) as $project) { + $query->addProject($project); + } } $query->setOrderBy('begin'); @@ -143,11 +149,12 @@ public function getTimeRecords(SharedProjectTimesheet $sharedProject, int $year, * Delivers stats for the given year (e.g. duration per month). * @param SharedProjectTimesheet $sharedProject * @param int $year + * @param Project|null $limitProject limit to this project * @return ChartStat[] stats per month, one-based index (1 - 12) * * @todo Unit test */ - public function getAnnualStats(SharedProjectTimesheet $sharedProject, int $year): array + public function getAnnualStats(SharedProjectTimesheet $sharedProject, int $year, ?Project $limitProject = null): array { $queryBuilder = $this->timesheetRepository->createQueryBuilder('t') ->select([ @@ -160,7 +167,14 @@ public function getAnnualStats(SharedProjectTimesheet $sharedProject, int $year) ->groupBy('year') ->addGroupBy('month'); - if ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + if (isset($limitProject)) { + $queryBuilder = $queryBuilder + ->andWhere('t.project = :project') + ->setParameters([ + 'project' => $limitProject, + 'year' => $year, + ]); + } elseif ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { $queryBuilder = $queryBuilder ->andWhere('t.project = :project') ->setParameters([ @@ -202,11 +216,12 @@ public function getAnnualStats(SharedProjectTimesheet $sharedProject, int $year) * @param SharedProjectTimesheet $sharedProject * @param int $year * @param int $month + * @param Project|null $limitProject limit to this project * @return ChartStat[] stats per day * * @todo Unit test */ - public function getMonthlyStats(SharedProjectTimesheet $sharedProject, int $year, int $month): array + public function getMonthlyStats(SharedProjectTimesheet $sharedProject, int $year, int $month, ?Project $limitProject = null): array { $queryBuilder = $this->timesheetRepository->createQueryBuilder('t') ->select([ @@ -222,7 +237,15 @@ public function getMonthlyStats(SharedProjectTimesheet $sharedProject, int $year ->addGroupBy('month') ->addGroupBy('day'); - if ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + if (isset($limitProject)) { + $queryBuilder = $queryBuilder + ->andWhere('t.project = :project') + ->setParameters([ + 'project' => $limitProject, + 'year' => $year, + 'month' => $month, + ]); + } elseif ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { $queryBuilder = $queryBuilder ->andWhere('t.project = :project') ->setParameters([ From 98a8d0837f9526076e11a24cf4d2fb5c039b3dd9 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 13:25:14 +0200 Subject: [PATCH 20/36] bugfix: get correct currency --- Controller/ViewController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Controller/ViewController.php b/Controller/ViewController.php index 58d25fd..417e261 100644 --- a/Controller/ViewController.php +++ b/Controller/ViewController.php @@ -203,7 +203,8 @@ protected function renderProjectView( // Define currency. $currency = 'EUR'; - $customer = $sharedProject->getProject()?->getCustomer(); + $customer = $project->getCustomer(); + if ($customer !== null) { $currency = $customer->getCurrency(); } From ccd0633eb3c3314767b4d3fc457d1273357b5fbb Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 13:42:55 +0200 Subject: [PATCH 21/36] include stats for customer as well --- Resources/views/view/customer.html.twig | 34 +++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/Resources/views/view/customer.html.twig b/Resources/views/view/customer.html.twig index fe91ecf..0137045 100644 --- a/Resources/views/view/customer.html.twig +++ b/Resources/views/view/customer.html.twig @@ -176,7 +176,38 @@
- + {% if ((stats.hasBudget() and sharedProject.isBudgetStatsVisible()) or (stats.hasTimeBudget() and sharedProject.isTimeBudgetStatsVisible())) %} + {% import "macros/progressbar.html.twig" as progress %} +
+
+

{{ 'shared_project_timesheets.view.stats.title' | trans }}

+
+
+ + {% if stats.hasBudget() and sharedProject.isBudgetStatsVisible() %} + + + + + {% endif %} + {% if stats.hasTimeBudget() and sharedProject.isTimeBudgetStatsVisible() %} + + + + + {% endif %} +
+ {{ 'budget' | trans }} + + {{ progress.progressbar_budget(stats, currency) }} +
+ {{ 'timeBudget' | trans }} + + {{ progress.progressbar_timebudget(stats) }} +
+
+
+ {% endif %}
@@ -209,5 +240,4 @@ {% endif %}
- {% endblock %} \ No newline at end of file From cf4a4a0d6031f3850b54e0b42b3a582820b3849e Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 13:52:33 +0200 Subject: [PATCH 22/36] partly fixed tests --- tests/Model/TimeRecordTest.php | 2 +- tests/Service/ViewServiceTest.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/Model/TimeRecordTest.php b/tests/Model/TimeRecordTest.php index 8ee6c5a..836c9f9 100644 --- a/tests/Model/TimeRecordTest.php +++ b/tests/Model/TimeRecordTest.php @@ -91,7 +91,7 @@ public function testValidFilledTimesheet(): void public function testMergeModeNull(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(\TypeError::class); TimeRecord::fromTimesheet( self::createTimesheet(new DateTime(), new User(), 0, 0, null), diff --git a/tests/Service/ViewServiceTest.php b/tests/Service/ViewServiceTest.php index 4839744..bd53648 100644 --- a/tests/Service/ViewServiceTest.php +++ b/tests/Service/ViewServiceTest.php @@ -13,7 +13,9 @@ use App\Entity\Project; use App\Repository\TimesheetRepository; use KimaiPlugin\SharedProjectTimesheetsBundle\Entity\SharedProjectTimesheet; +use KimaiPlugin\SharedProjectTimesheetsBundle\Repository\SharedProjectTimesheetRepository; use KimaiPlugin\SharedProjectTimesheetsBundle\Service\ViewService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\SessionInterface; @@ -28,12 +30,12 @@ class ViewServiceTest extends TestCase private $service; /** - * @var SessionInterface + * @var SessionInterface|MockObject */ private $session; /** - * @var PasswordHasherInterface + * @var PasswordHasherInterface|MockObject */ private $encoder; @@ -45,6 +47,7 @@ class ViewServiceTest extends TestCase protected function setUp(): void { $timesheetRepository = $this->createMock(TimesheetRepository::class); + $sharedProjectTimesheetRepository = $this->createMock(SharedProjectTimesheetRepository::class); $request = new RequestStack(); $this->session = $this->createPartialMock(SessionInterface::class, []); @@ -52,7 +55,7 @@ protected function setUp(): void $this->encoder = $this->createMock(PasswordHasherInterface::class); $factory->method('getPasswordHasher')->willReturn($this->encoder); - $this->service = new ViewService($timesheetRepository, $this->session, $factory); + $this->service = new ViewService($timesheetRepository, $request, $factory, $sharedProjectTimesheetRepository); } private function createSharedProjectTimesheet(): SharedProjectTimesheet From 3ecf41642533995f9c5b74c49135f0b503c37eb8 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 13:52:45 +0200 Subject: [PATCH 23/36] whitespace --- Service/ViewService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/ViewService.php b/Service/ViewService.php index e641a98..0b7a889 100644 --- a/Service/ViewService.php +++ b/Service/ViewService.php @@ -96,7 +96,7 @@ public function getTimeRecords(SharedProjectTimesheet $sharedProject, int $year, } else { foreach ($this->sharedTimesheetRepository->getProjects($sharedProject) as $project) { $query->addProject($project); - } + } } $query->setOrderBy('begin'); From 641ac1d4ff6eae12f088d5faa4032dbf76af1b96 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 14:02:00 +0200 Subject: [PATCH 24/36] fix: rollback breaks if there are null values --- Migrations/Version20240726082447.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Migrations/Version20240726082447.php b/Migrations/Version20240726082447.php index a5b8189..5c6f09f 100644 --- a/Migrations/Version20240726082447.php +++ b/Migrations/Version20240726082447.php @@ -45,6 +45,5 @@ public function down(Schema $schema): void $table->addUniqueIndex(['project_id', 'share_key']); $table->removeForeignKey('fk_customer'); $table->dropColumn('customer_id'); - $table->modifyColumn('project_id', ['notnull' => true]); } } From 00e075aa0541d6035e607d4a306973dae9ba2189 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Wed, 31 Jul 2024 14:23:06 +0200 Subject: [PATCH 25/36] code style fixes --- Migrations/Version20240726082447.php | 10 +++++----- Service/ManageService.php | 2 +- Service/ViewService.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Migrations/Version20240726082447.php b/Migrations/Version20240726082447.php index 5c6f09f..235683c 100644 --- a/Migrations/Version20240726082447.php +++ b/Migrations/Version20240726082447.php @@ -25,11 +25,11 @@ public function up(Schema $schema): void $tableName = Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME; // raw sql needed because column position is not supported - $this->addSql(sprintf('ALTER TABLE %s ADD customer_id INT(11) DEFAULT NULL AFTER id', $tableName)); - $this->addSql(sprintf('ALTER TABLE %s MODIFY project_id INT(11) DEFAULT NULL', $tableName)); - $this->addSql(sprintf('ALTER TABLE %s DROP INDEX UNIQ_BE51C9A166D1F9CF06F2E59', $tableName)); - $this->addSql(sprintf('ALTER TABLE %s ADD CONSTRAINT UNIQ_customer_id_project_id_share_key UNIQUE (customer_id, project_id, share_key)', $tableName)); - $this->addSql(sprintf(' + $this->addSql(\sprintf('ALTER TABLE %s ADD customer_id INT(11) DEFAULT NULL AFTER id', $tableName)); + $this->addSql(\sprintf('ALTER TABLE %s MODIFY project_id INT(11) DEFAULT NULL', $tableName)); + $this->addSql(\sprintf('ALTER TABLE %s DROP INDEX UNIQ_BE51C9A166D1F9CF06F2E59', $tableName)); + $this->addSql(\sprintf('ALTER TABLE %s ADD CONSTRAINT UNIQ_customer_id_project_id_share_key UNIQUE (customer_id, project_id, share_key)', $tableName)); + $this->addSql(\sprintf(' ALTER TABLE %s ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES kimai2_customers (id) diff --git a/Service/ManageService.php b/Service/ManageService.php index 44e6bae..f981800 100644 --- a/Service/ManageService.php +++ b/Service/ManageService.php @@ -106,7 +106,7 @@ public function update(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ */ private function getUuidV4(): string { - return sprintf( + return \sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', // 32 bits for "time_low" diff --git a/Service/ViewService.php b/Service/ViewService.php index 0b7a889..d0b8fa0 100644 --- a/Service/ViewService.php +++ b/Service/ViewService.php @@ -53,7 +53,7 @@ public function hasAccess(SharedProjectTimesheet $sharedProject, ?string $givenP $shareKey = $sharedProject->getShareKey(); $passwordMd5 = md5($hashedPassword); - $sessionPasswordKey = sprintf('spt-authed-%d-%s-%s', $sharedProject->getId(), $shareKey, $passwordMd5); + $sessionPasswordKey = \sprintf('spt-authed-%d-%s-%s', $sharedProject->getId(), $shareKey, $passwordMd5); if (!$this->request->getSession()->has($sessionPasswordKey)) { // Check given password From 1bdffd37cc6ccbe284e3d4fb0d0069218cd065a0 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 10:23:21 +0200 Subject: [PATCH 26/36] pr feedback: remove null check, default value for project --- Model/TimeRecord.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Model/TimeRecord.php b/Model/TimeRecord.php index 2cadf85..6564f46 100644 --- a/Model/TimeRecord.php +++ b/Model/TimeRecord.php @@ -52,7 +52,7 @@ public static function fromTimesheet(Timesheet $timesheet, string $mergeMode = R private int $duration = 0; private ?User $user = null; private ?string $mergeMode = null; - private Project $project; + private ?Project $project = null; private function __construct(\DateTimeInterface $date, User $user, string $mergeMode) { @@ -111,7 +111,7 @@ public function addTimesheet(Timesheet $timesheet): void public function getProject(): ?Project { - return $this->project ?? null; + return $this->project; } protected function addHourlyRate(?float $hourlyRate, ?int $duration): void From f4c8eb0a15fa44399294f9eb4f338679269306c9 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 10:23:39 +0200 Subject: [PATCH 27/36] pr feedback: col order + extra class --- Controller/ManageController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Controller/ManageController.php b/Controller/ManageController.php index 680c2e2..a4d98bb 100644 --- a/Controller/ManageController.php +++ b/Controller/ManageController.php @@ -47,8 +47,8 @@ public function index(): Response $table->setPagination($sharedProjects); $table->setReloadEvents('kimai.sharedProject'); + $table->addColumn('type', ['class' => 'alwaysVisible w-min', 'orderBy' => false]); $table->addColumn('name', ['class' => 'alwaysVisible', 'orderBy' => false]); - $table->addColumn('type', ['class' => 'alwaysVisible', 'orderBy' => false]); $table->addColumn('url', ['class' => 'alwaysVisible', 'orderBy' => false]); $table->addColumn('password', ['class' => 'd-none', 'orderBy' => false]); $table->addColumn('record_merge_mode', ['class' => 'd-none text-center w-min', 'orderBy' => false, 'title' => 'shared_project_timesheets.manage.table.record_merge_mode']); From cf121401353b2fe59c389078b89a0bb5cc640420 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 10:36:19 +0200 Subject: [PATCH 28/36] pr feedback: extra functions to prevent use of constants outside class --- Controller/ManageController.php | 2 +- Entity/SharedProjectTimesheet.php | 10 ++++++++++ EventSubscriber/SharedProjectSubscriber.php | 2 +- Repository/SharedProjectTimesheetRepository.php | 2 +- Service/ManageService.php | 2 +- Service/ViewService.php | 4 ++-- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Controller/ManageController.php b/Controller/ManageController.php index a4d98bb..1ab0702 100644 --- a/Controller/ManageController.php +++ b/Controller/ManageController.php @@ -115,7 +115,7 @@ public function update(SharedProjectTimesheet $sharedProject, string $shareKey, throw $this->createNotFoundException('Project not found'); } - $formClass = $sharedProject->getType() === SharedProjectTimesheet::TYPE_CUSTOMER ? + $formClass = $sharedProject->isCustomerSharing() ? SharedCustomerFormType::class : SharedProjectFormType::class; diff --git a/Entity/SharedProjectTimesheet.php b/Entity/SharedProjectTimesheet.php index 5e6e41d..235bfc2 100644 --- a/Entity/SharedProjectTimesheet.php +++ b/Entity/SharedProjectTimesheet.php @@ -211,4 +211,14 @@ public function getType(): ?string return null; } + + public function isCustomerSharing(): bool + { + return $this->getType() === static::TYPE_CUSTOMER; + } + + public function isProjectSharing(): bool + { + return $this->getType() === static::TYPE_PROJECT; + } } diff --git a/EventSubscriber/SharedProjectSubscriber.php b/EventSubscriber/SharedProjectSubscriber.php index f77e5a8..0ed83c6 100644 --- a/EventSubscriber/SharedProjectSubscriber.php +++ b/EventSubscriber/SharedProjectSubscriber.php @@ -38,7 +38,7 @@ public function onActions(PageActionsEvent $event): void $event->addEdit($this->path('update_shared_project_timesheets', ['id' => $sharedProject->getId(), 'shareKey' => $sharedProject->getShareKey()])); - if ($sharedProject->getType() === SharedProjectTimesheet::TYPE_CUSTOMER) { + if ($sharedProject->isCustomerSharing()) { $event->addAction('customer', ['url' => $this->path('customer_details', ['id' => $sharedProject->getCustomer()->getId()])]); } else { $event->addAction('project', ['url' => $this->path('project_details', ['id' => $sharedProject->getProject()->getId()])]); diff --git a/Repository/SharedProjectTimesheetRepository.php b/Repository/SharedProjectTimesheetRepository.php index 61f4054..cc6f147 100644 --- a/Repository/SharedProjectTimesheetRepository.php +++ b/Repository/SharedProjectTimesheetRepository.php @@ -95,7 +95,7 @@ public function findByCustomerAndShareKey(Customer|int $customer, ?string $share */ public function getProjects(SharedProjectTimesheet $sharedProject): array { - if ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + if ($sharedProject->isProjectSharing()) { return [$sharedProject->getProject()]; } diff --git a/Service/ManageService.php b/Service/ManageService.php index f981800..92134f4 100644 --- a/Service/ManageService.php +++ b/Service/ManageService.php @@ -45,7 +45,7 @@ public function create(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ substr(preg_replace('/[^A-Za-z0-9]+/', '', $this->getUuidV4()), 0, 12) ); - if ($sharedProjectTimesheet->getType() === SharedProjectTimesheet::TYPE_CUSTOMER) { + if ($sharedProjectTimesheet->isCustomerSharing()) { $existingEntry = $this->sharedProjectTimesheetRepository->findByCustomerAndShareKey( $sharedProjectTimesheet->getCustomer(), $sharedProjectTimesheet->getShareKey() diff --git a/Service/ViewService.php b/Service/ViewService.php index d0b8fa0..c232b06 100644 --- a/Service/ViewService.php +++ b/Service/ViewService.php @@ -174,7 +174,7 @@ public function getAnnualStats(SharedProjectTimesheet $sharedProject, int $year, 'project' => $limitProject, 'year' => $year, ]); - } elseif ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + } elseif ($sharedProject->isProjectSharing()) { $queryBuilder = $queryBuilder ->andWhere('t.project = :project') ->setParameters([ @@ -245,7 +245,7 @@ public function getMonthlyStats(SharedProjectTimesheet $sharedProject, int $year 'year' => $year, 'month' => $month, ]); - } elseif ($sharedProject->getType() === SharedProjectTimesheet::TYPE_PROJECT) { + } elseif ($sharedProject->isProjectSharing()) { $queryBuilder = $queryBuilder ->andWhere('t.project = :project') ->setParameters([ From be7808b2513855842db323845e57f22bd8a60082 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 10:36:55 +0200 Subject: [PATCH 29/36] pr feedback: use icon --- Resources/views/manage/index.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/views/manage/index.html.twig b/Resources/views/manage/index.html.twig index a00870d..b87066a 100644 --- a/Resources/views/manage/index.html.twig +++ b/Resources/views/manage/index.html.twig @@ -8,7 +8,7 @@ {% if column == 'name' %} {{ entry.project.name ?? entry.customer.name }} {% elseif column == 'type' %} - {{ entry.getType() | trans }} + {% elseif column == 'url' %} {% if entry.shareKey %} {% if entry.project %} From 6ad52683cf2cfeb0a4eb4bb9daf2a06751cbb62a Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 10:52:27 +0200 Subject: [PATCH 30/36] pr feedback: refactor getType(), return value is not nullable --- Entity/SharedProjectTimesheet.php | 15 ++++----------- Service/ManageService.php | 4 ---- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/Entity/SharedProjectTimesheet.php b/Entity/SharedProjectTimesheet.php index 235bfc2..71de073 100644 --- a/Entity/SharedProjectTimesheet.php +++ b/Entity/SharedProjectTimesheet.php @@ -192,24 +192,17 @@ public function setTimeBudgetStatsVisible(bool $timeBudgetStatsVisible): void $this->timeBudgetStatsVisible = $timeBudgetStatsVisible; } - public function getType(): ?string + public function getType(): string { - $customer = $this->getCustomer(); - $project = $this->getProject(); - - if (isset($customer, $project)) { + if ($this->customer !== null && $this->project !== null) { throw new LogicException('Invalid state: customer and project cannot be filled both'); } - if (isset($customer)) { + if ($this->customer !== null) { return static::TYPE_CUSTOMER; } - if (isset($project)) { - return static::TYPE_PROJECT; - } - - return null; + return static::TYPE_PROJECT; } public function isCustomerSharing(): bool diff --git a/Service/ManageService.php b/Service/ManageService.php index 92134f4..6e194a5 100644 --- a/Service/ManageService.php +++ b/Service/ManageService.php @@ -75,10 +75,6 @@ public function update(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ throw new \InvalidArgumentException('Cannot update shared project timesheet with share key equals null'); } - if ($sharedProjectTimesheet->getType() === null) { - throw new \InvalidArgumentException('No valid project or customer specified'); - } - // Handle password $currentHashedPassword = $sharedProjectTimesheet !== null && !empty($sharedProjectTimesheet->getPassword()) ? $sharedProjectTimesheet->getPassword() : null; From f5effeb1ba4d5e16786e7e6d0e6f62ce15c88603 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 11:23:24 +0200 Subject: [PATCH 31/36] pr feedback: use sharedProject i/o id in path params --- Controller/ManageController.php | 6 +++--- Controller/ViewController.php | 2 +- EventSubscriber/SharedProjectSubscriber.php | 8 ++++---- Resources/views/manage/index.html.twig | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Controller/ManageController.php b/Controller/ManageController.php index 1ab0702..4309e02 100644 --- a/Controller/ManageController.php +++ b/Controller/ManageController.php @@ -108,7 +108,7 @@ public function create(Request $request): Response ]); } - #[Route(path: '/{id}/{shareKey}', name: 'update_shared_project_timesheets', methods: ['GET', 'POST'])] + #[Route(path: '/{sharedProject}/{shareKey}', name: 'update_shared_project_timesheets', methods: ['GET', 'POST'])] public function update(SharedProjectTimesheet $sharedProject, string $shareKey, Request $request): Response { if ($shareKey == null || $sharedProject->getShareKey() !== $shareKey) { @@ -121,7 +121,7 @@ public function update(SharedProjectTimesheet $sharedProject, string $shareKey, $form = $this->createForm($formClass, $sharedProject, [ 'method' => 'POST', - 'action' => $this->generateUrl('update_shared_project_timesheets', ['id' => $sharedProject->getId(), 'shareKey' => $shareKey]) + 'action' => $this->generateUrl('update_shared_project_timesheets', ['sharedProject' => $sharedProject->getId(), 'shareKey' => $shareKey]) ]); $form->handleRequest($request); @@ -146,7 +146,7 @@ public function update(SharedProjectTimesheet $sharedProject, string $shareKey, ]); } - #[Route(path: '/{id}/{shareKey}/remove', name: 'remove_shared_project_timesheets', methods: ['GET', 'POST'])] + #[Route(path: '/{sharedProject}/{shareKey}/remove', name: 'remove_shared_project_timesheets', methods: ['GET', 'POST'])] public function remove(SharedProjectTimesheet $sharedProject, string $shareKey): Response { if ($shareKey == null || $sharedProject->getShareKey() !== $shareKey) { diff --git a/Controller/ViewController.php b/Controller/ViewController.php index 417e261..3ebff02 100644 --- a/Controller/ViewController.php +++ b/Controller/ViewController.php @@ -25,7 +25,7 @@ #[Route(path: '/auth/shared-project-timesheets')] class ViewController extends AbstractController { - #[Route(path: '/{id}/{shareKey}', name: 'view_shared_project_timesheets', methods: ['GET', 'POST'])] + #[Route(path: '/{sharedProject}/{shareKey}', name: 'view_shared_project_timesheets', methods: ['GET', 'POST'])] public function indexAction( Project $project, string $shareKey, diff --git a/EventSubscriber/SharedProjectSubscriber.php b/EventSubscriber/SharedProjectSubscriber.php index 0ed83c6..74577ab 100644 --- a/EventSubscriber/SharedProjectSubscriber.php +++ b/EventSubscriber/SharedProjectSubscriber.php @@ -25,18 +25,18 @@ public function onActions(PageActionsEvent $event): void { $payload = $event->getPayload(); - if (!\is_array($payload) || !\array_key_exists('shared_project', $payload)) { + if (!\is_array($payload) || !\array_key_exists('sharedProject', $payload)) { return; } /** @var SharedProjectTimesheet $sharedProject */ - $sharedProject = $payload['shared_project']; + $sharedProject = $payload['sharedProject']; if ($sharedProject->getId() === null || $sharedProject->getType() === null) { return; } - $event->addEdit($this->path('update_shared_project_timesheets', ['id' => $sharedProject->getId(), 'shareKey' => $sharedProject->getShareKey()])); + $event->addEdit($this->path('update_shared_project_timesheets', ['sharedProject' => $sharedProject->getId(), 'shareKey' => $sharedProject->getShareKey()])); if ($sharedProject->isCustomerSharing()) { $event->addAction('customer', ['url' => $this->path('customer_details', ['id' => $sharedProject->getCustomer()->getId()])]); @@ -44,6 +44,6 @@ public function onActions(PageActionsEvent $event): void $event->addAction('project', ['url' => $this->path('project_details', ['id' => $sharedProject->getProject()->getId()])]); } - $event->addDelete($this->path('remove_shared_project_timesheets', ['id' => $sharedProject->getId(), 'shareKey' => $sharedProject->getShareKey()]), false); + $event->addDelete($this->path('remove_shared_project_timesheets', ['sharedProject' => $sharedProject->getId(), 'shareKey' => $sharedProject->getShareKey()]), false); } } diff --git a/Resources/views/manage/index.html.twig b/Resources/views/manage/index.html.twig index b87066a..fc758f9 100644 --- a/Resources/views/manage/index.html.twig +++ b/Resources/views/manage/index.html.twig @@ -2,7 +2,7 @@ {% import "macros/widgets.html.twig" as widgets %} {% import "@SharedProjectTimesheets/manage/actions.html.twig" as actions %} -{% block datatable_row_attr %} class="modal-ajax-form open-edit" data-href="{{ url('update_shared_project_timesheets', {id: entry.id, shareKey: entry.shareKey}) }}"{% endblock %} +{% block datatable_row_attr %} class="modal-ajax-form open-edit" data-href="{{ url('update_shared_project_timesheets', {sharedProject: entry.id, shareKey: entry.shareKey}) }}"{% endblock %} {% block datatable_column_value %} {% if column == 'name' %} @@ -12,7 +12,7 @@ {% elseif column == 'url' %} {% if entry.shareKey %} {% if entry.project %} - {% set p_url = url('view_shared_project_timesheets', {id: entry.project.id, shareKey: entry.shareKey}) %} + {% set p_url = url('view_shared_project_timesheets', {sharedProject: entry.project.id, shareKey: entry.shareKey}) %} {% elseif entry.customer %} {% set p_url = url('view_shared_project_timesheets_customer', {customer: entry.customer.id, shareKey: entry.shareKey}) %} {% endif %} @@ -39,7 +39,7 @@ {% elseif column == 'monthly_chart_visible' %} {{ widgets.label_boolean(entry.monthlyChartVisible) }} {% elseif column == 'actions' %} - {% set event = actions(app.user, 'shared_project', 'index', {'shared_project': entry}) %} + {% set event = actions(app.user, 'shared_project', 'index', {'sharedProject': entry}) %} {{ widgets.table_actions(event.actions) }} {% endif %} {% endblock %} From 44113b8d449bb045c2c8e625a5f306c29de187ac Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 11:34:30 +0200 Subject: [PATCH 32/36] pr feedback: $shareKey is always a (non empty) string --- Controller/ManageController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Controller/ManageController.php b/Controller/ManageController.php index 4309e02..180ccf9 100644 --- a/Controller/ManageController.php +++ b/Controller/ManageController.php @@ -111,7 +111,7 @@ public function create(Request $request): Response #[Route(path: '/{sharedProject}/{shareKey}', name: 'update_shared_project_timesheets', methods: ['GET', 'POST'])] public function update(SharedProjectTimesheet $sharedProject, string $shareKey, Request $request): Response { - if ($shareKey == null || $sharedProject->getShareKey() !== $shareKey) { + if ($sharedProject->getShareKey() !== $shareKey) { throw $this->createNotFoundException('Project not found'); } @@ -149,7 +149,7 @@ public function update(SharedProjectTimesheet $sharedProject, string $shareKey, #[Route(path: '/{sharedProject}/{shareKey}/remove', name: 'remove_shared_project_timesheets', methods: ['GET', 'POST'])] public function remove(SharedProjectTimesheet $sharedProject, string $shareKey): Response { - if ($shareKey == null || $sharedProject->getShareKey() !== $shareKey) { + if ($sharedProject->getShareKey() !== $shareKey) { throw $this->createNotFoundException('Project not found'); } From 961842ef20de36d692c245e6aeb64e77c9cbabd0 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 12:21:06 +0200 Subject: [PATCH 33/36] pr feedback: use migration api for portability --- Migrations/Version20240726082447.php | 37 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/Migrations/Version20240726082447.php b/Migrations/Version20240726082447.php index 235683c..5e83d6a 100644 --- a/Migrations/Version20240726082447.php +++ b/Migrations/Version20240726082447.php @@ -11,6 +11,7 @@ namespace KimaiPlugin\SharedProjectTimesheetsBundle\Migrations; use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Types\Types; use Doctrine\Migrations\AbstractMigration; final class Version20240726082447 extends AbstractMigration @@ -22,28 +23,32 @@ public function getDescription(): string public function up(Schema $schema): void { - $tableName = Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME; - - // raw sql needed because column position is not supported - $this->addSql(\sprintf('ALTER TABLE %s ADD customer_id INT(11) DEFAULT NULL AFTER id', $tableName)); - $this->addSql(\sprintf('ALTER TABLE %s MODIFY project_id INT(11) DEFAULT NULL', $tableName)); - $this->addSql(\sprintf('ALTER TABLE %s DROP INDEX UNIQ_BE51C9A166D1F9CF06F2E59', $tableName)); - $this->addSql(\sprintf('ALTER TABLE %s ADD CONSTRAINT UNIQ_customer_id_project_id_share_key UNIQUE (customer_id, project_id, share_key)', $tableName)); - $this->addSql(\sprintf(' - ALTER TABLE %s ADD CONSTRAINT fk_customer - FOREIGN KEY (customer_id) - REFERENCES kimai2_customers (id) - ON DELETE CASCADE - ON UPDATE CASCADE - ', $tableName)); + $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); + $table->addColumn('customer_id', Types::INTEGER, [ + 'notnull' => false, + ]); + $table->modifyColumn('project_id', [ + 'notnull' => false, + ]); + $table->dropIndex('UNIQ_BE51C9A166D1F9CF06F2E59'); + $table->addUniqueIndex(['customer_id', 'project_id', 'share_key']); + $table->addForeignKeyConstraint( + 'kimai2_customers', + ['customer_id'], + ['id'], + [ + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE', + ] + ); } public function down(Schema $schema): void { $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); - $table->dropIndex('UNIQ_customer_id_project_id_share_key'); + $table->dropIndex('UNIQ_BE51C9A9395C3F3166D1F9CF06F2E59'); $table->addUniqueIndex(['project_id', 'share_key']); - $table->removeForeignKey('fk_customer'); + $table->removeForeignKey('FK_BE51C9A9395C3F3'); $table->dropColumn('customer_id'); } } From b6f1ac63c228585f428ab591a0e34df5a39b4ec1 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 12:30:45 +0200 Subject: [PATCH 34/36] regression fix: use correct param name --- Controller/ViewController.php | 2 +- Resources/views/manage/index.html.twig | 2 +- Resources/views/view/project.html.twig | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Controller/ViewController.php b/Controller/ViewController.php index 3ebff02..12bf7f5 100644 --- a/Controller/ViewController.php +++ b/Controller/ViewController.php @@ -25,7 +25,7 @@ #[Route(path: '/auth/shared-project-timesheets')] class ViewController extends AbstractController { - #[Route(path: '/{sharedProject}/{shareKey}', name: 'view_shared_project_timesheets', methods: ['GET', 'POST'])] + #[Route(path: '/{project}/{shareKey}', name: 'view_shared_project_timesheets', methods: ['GET', 'POST'])] public function indexAction( Project $project, string $shareKey, diff --git a/Resources/views/manage/index.html.twig b/Resources/views/manage/index.html.twig index fc758f9..01bf09b 100644 --- a/Resources/views/manage/index.html.twig +++ b/Resources/views/manage/index.html.twig @@ -12,7 +12,7 @@ {% elseif column == 'url' %} {% if entry.shareKey %} {% if entry.project %} - {% set p_url = url('view_shared_project_timesheets', {sharedProject: entry.project.id, shareKey: entry.shareKey}) %} + {% set p_url = url('view_shared_project_timesheets', {project: entry.project.id, shareKey: entry.shareKey}) %} {% elseif entry.customer %} {% set p_url = url('view_shared_project_timesheets_customer', {customer: entry.customer.id, shareKey: entry.shareKey}) %} {% endif %} diff --git a/Resources/views/view/project.html.twig b/Resources/views/view/project.html.twig index ee7ad7d..7d8b604 100644 --- a/Resources/views/view/project.html.twig +++ b/Resources/views/view/project.html.twig @@ -95,10 +95,10 @@ {% if monthlyChartVisible %}
From 657867411b0df9c04b110f02fd837d3ebbbcd5cb Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 12:37:52 +0200 Subject: [PATCH 35/36] shorter heading Co-authored-by: Kevin Papst --- Resources/views/view/customer.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/views/view/customer.html.twig b/Resources/views/view/customer.html.twig index 0137045..7df242d 100644 --- a/Resources/views/view/customer.html.twig +++ b/Resources/views/view/customer.html.twig @@ -71,7 +71,7 @@
- {{ 'customer' | trans }}: {{ sharedProject.customer.name }} + {{ sharedProject.customer.name }} {% if statsPerMonth is not null %} From d29d440e3e8c9c221584c58774ac840ec7b4e267 Mon Sep 17 00:00:00 2001 From: Lennart Hengstmengel Date: Fri, 9 Aug 2024 12:34:51 +0200 Subject: [PATCH 36/36] pr feedback: added missing closing tag --- Resources/views/view/customer.html.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/views/view/customer.html.twig b/Resources/views/view/customer.html.twig index 7df242d..8e77ee4 100644 --- a/Resources/views/view/customer.html.twig +++ b/Resources/views/view/customer.html.twig @@ -240,4 +240,5 @@ {% endif %} + {% endblock %} \ No newline at end of file