diff --git a/composer.json b/composer.json index c721b14..f57b175 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "OSL-3.0", "require": { "php": ">=8.4", - "forumify/forumify-platform": "^1.2", + "forumify/forumify-platform": "^1.2.x-dev", "league/commonmark": "^2.5" }, "require-dev": { diff --git a/src/Components/SubmissionStatusForm.php b/src/Components/SubmissionStatusForm.php new file mode 100644 index 0000000..c2ab9a0 --- /dev/null +++ b/src/Components/SubmissionStatusForm.php @@ -0,0 +1,81 @@ +createFormBuilder() + ->add('status', EntityType::class, [ + 'choice_label' => 'name', + 'class' => Status::class, + ]) + ->add('reason', TextareaType::class, [ + 'empty_data' => '', + 'required' => false, + ]) + ->getForm() + ; + } + + // @phpstan-ignore-next-line + private function getDataModelValue(): ?string + { + return 'norender|*'; + } + + #[LiveAction] + public function save(): void + { + if (!$this->isGranted(VoterAttribute::ACL->value, [ + 'permission' => 'supervisor_manage_submissions', + 'entity' => $this->submission->getForm(), + ])) { + return; + } + + $this->submitForm(); + + $statusRecord = $this->getForm()->getData(); + $statusRecord['sendNotification'] = true; + $this->submissionStatusUpdateService->createStatusRecord($this->submission, $statusRecord); + + $this->success = true; + $this->resetForm(); + + $this->emitUp('milhq:submission:status_updated'); + } +} diff --git a/src/Components/SupervisorSubmissionList.php b/src/Components/SupervisorSubmissionList.php new file mode 100644 index 0000000..1420ffa --- /dev/null +++ b/src/Components/SupervisorSubmissionList.php @@ -0,0 +1,61 @@ + */ + #[LiveProp] + public array $supervisedUnits; + + public function __construct( + private readonly FormRepository $formRepository, + ) { + } + + protected function getEntityClass(): string + { + return FormSubmission::class; + } + + protected function getQuery(): QueryBuilder + { + $qb = parent::getQuery(); + if (empty($this->supervisedUnits)) { + return $qb->andWhere('1 = 0'); + } + + $qb + ->join('e.soldier', 's') + ->join('s.unit', 'u') + ->join('e.form', 'f') + ->andWhere('s.id != :self') + ->andWhere('u.id IN (:units)') + ->orderBy('e.createdAt', 'DESC') + ->setParameter('units', $this->supervisedUnits) + ->setParameter('self', $this->soldierId) + ; + $this->formRepository->addACLToQuery($qb, 'supervisor_manage_submissions', FormSubmission::class, 'f'); + return $qb; + } + + #[LiveListener('milhq:submission:status_updated')] + public function refresh(): void + { + // no-op, re-rendering the component is enough to refresh the list with the current pagination + } +} diff --git a/src/Controller/OperationsCenterController.php b/src/Controller/OperationsCenterController.php index 6b86a71..44236fa 100644 --- a/src/Controller/OperationsCenterController.php +++ b/src/Controller/OperationsCenterController.php @@ -6,6 +6,7 @@ use Forumify\Core\Repository\SettingRepository; use Forumify\Milhq\Repository\FormRepository; +use Forumify\Milhq\Repository\UnitRepository; use Forumify\Milhq\Service\SoldierService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -19,6 +20,7 @@ public function __construct( private readonly SoldierService $soldierService, private readonly SettingRepository $settingRepository, private readonly FormRepository $formRepository, + private readonly UnitRepository $unitRepository, ) { } @@ -41,6 +43,7 @@ public function __invoke(): Response 'forms' => $this->formRepository->findAllSubmissionsAllowed(), 'soldier' => $soldier, 'milsimUnitsId' => $this->getMilsimUnitsId(), + 'supervisedUnits' => $this->unitRepository->findBySoldierIsSupervisor($soldier), ]); } diff --git a/src/Entity/Form.php b/src/Entity/Form.php index 2620ceb..108584e 100644 --- a/src/Entity/Form.php +++ b/src/Entity/Form.php @@ -148,7 +148,7 @@ public function removeField(FormField $field): void public function getACLPermissions(): array { - return ['create_submissions', 'view_submissions', 'manage_submissions']; + return ['create_submissions', 'view_submissions', 'manage_submissions', 'supervisor_manage_submissions']; } public function getACLParameters(): ACLParameters diff --git a/src/Repository/UnitRepository.php b/src/Repository/UnitRepository.php index 4059466..ce198f4 100644 --- a/src/Repository/UnitRepository.php +++ b/src/Repository/UnitRepository.php @@ -5,6 +5,7 @@ namespace Forumify\Milhq\Repository; use Forumify\Core\Repository\AbstractRepository; +use Forumify\Milhq\Entity\Soldier; use Forumify\Milhq\Entity\Unit; /** @@ -16,4 +17,17 @@ public static function getEntityClass(): string { return Unit::class; } + + public function findBySoldierIsSupervisor(Soldier|int $soldier): array + { + $soldierId = $soldier instanceof Soldier ? $soldier->getId() : $soldier; + + return $this->createQueryBuilder('e') + ->innerJoin('e.supervisors', 'supervisor') + ->innerJoin(Soldier::class, 'soldier', 'WITH', 'soldier.position = supervisor AND soldier.unit = e') + ->where('soldier.id = :soldierId') + ->setParameter('soldierId', $soldierId) + ->getQuery() + ->getResult(); + } } diff --git a/templates/frontend/components/submission_status_form.html.twig b/templates/frontend/components/submission_status_form.html.twig new file mode 100644 index 0000000..33b8cd9 --- /dev/null +++ b/templates/frontend/components/submission_status_form.html.twig @@ -0,0 +1,16 @@ +
+ {{ form_start(form, { + attr: { + 'data-action': 'live#action:prevent', + 'data-live-action-param': 'save', + } + }) }} + {{ form_row(form.status) }} + {{ form_row(form.reason) }} +
+ +
+ {{ form_end(form) }} +
diff --git a/templates/frontend/components/supervisor_submission_list.html.twig b/templates/frontend/components/supervisor_submission_list.html.twig new file mode 100644 index 0000000..9451f62 --- /dev/null +++ b/templates/frontend/components/supervisor_submission_list.html.twig @@ -0,0 +1,48 @@ +{% extends '@Forumify/components/list.html.twig' %} +{% block row %} +
  • +
    + {% embed '@Forumify/components/modal.html.twig' %} + {% block modalOpenButton %} + {{ item.soldier.name }} - {{ item.form.name }} + {% endblock %} + {% block modalContent %} +
    +
    +

    {{ item.form.name }}

    + {% include '@ForumifyMilhqPlugin/components/submission.html.twig' with { + submission: item + } %} +
    + {% if item.status %} +

    Status

    +
    + {% include '@ForumifyMilhqPlugin/frontend/roster/components/status.html.twig' with { + status: item.status, + class: 'text-small' + } only %} +
    + {% endif %} + {% if item.statusReason %} +

    Reason

    +

    {{ item.statusReason }}

    + {% endif %} +
    + {{ component('Milhq\\SubmissionStatusForm', { submission: item }) }} +
    +
    + {% endblock %} + {% endembed %} +

    {{ item.createdAt|format_date }}

    +
    + {% if item.status %} + {% include '@ForumifyMilhqPlugin/frontend/roster/components/status.html.twig' with { + status: item.status, + class: 'text-small' + } only %} + {% endif %} +
  • +{% endblock %} +{% block empty %} + {{ 'table.no_entries'|trans }} +{% endblock %} diff --git a/templates/frontend/operations_center/operations_center.html.twig b/templates/frontend/operations_center/operations_center.html.twig index d6591e5..2fe077a 100644 --- a/templates/frontend/operations_center/operations_center.html.twig +++ b/templates/frontend/operations_center/operations_center.html.twig @@ -38,7 +38,7 @@ {% endblock %} {% embed '@Forumify/components/tabs.html.twig' %} {% block tabs %} - + {% if plugin_version('forumify/forumify-milhq-plugin', 'premium') %} {% if is_granted('milhq.frontend.attendance_sheet.view') %} @@ -48,7 +48,7 @@ {% endif %} {% endblock %} {% block tabpanels %} - {% include '@ForumifyMilhqPlugin/frontend/operations_center/tabs/submissions.html.twig' %} + {% include '@ForumifyMilhqPlugin/frontend/operations_center/tabs/dashboard.html.twig' %} {% if plugin_version('forumify/forumify-milhq-plugin', 'premium') %} {% include '@ForumifyMilhqPlugin/frontend/operations_center/tabs/operations.html.twig' %} {% if is_granted('milhq.frontend.attendance_sheet.view') %} diff --git a/templates/frontend/operations_center/tabs/dashboard.html.twig b/templates/frontend/operations_center/tabs/dashboard.html.twig new file mode 100644 index 0000000..0cd7e1b --- /dev/null +++ b/templates/frontend/operations_center/tabs/dashboard.html.twig @@ -0,0 +1,46 @@ +
    +
    + {% if supervisedUnits is not empty %} + {% embed '@Forumify/components/tabs.html.twig' %} + {% block tabs %} + + + {% endblock %} + {% block tabpanels %} +
    + {{ component('Milhq\\SubmissionList', { soldierId: soldier.id }) }} +
    +
    + {{ component('Milhq\\SupervisorSubmissionList', { + soldierId: soldier.id, + supervisedUnits: supervisedUnits|map(u => u.id), + }) }} +
    + {% endblock %} + {% endembed %} + {% else %} + {{ component('Milhq\\SubmissionList', { soldierId: soldier.id }) }} + {% endif %} +
    +
    + {% if milsimUnitsId is not empty %} +
    + +
    + {% endif %} + {% if plugin_version('forumify/forumify-milhq-plugin', 'premium') and setting('milhq.report_in.enabled') %} + {{ component('Milhq\\ReportInButton') }} + {% endif %} + {% for form in forms %} + + + {{ form.name }} + + {% endfor %} +
    +
    diff --git a/templates/frontend/operations_center/tabs/submissions.html.twig b/templates/frontend/operations_center/tabs/submissions.html.twig deleted file mode 100644 index 765071f..0000000 --- a/templates/frontend/operations_center/tabs/submissions.html.twig +++ /dev/null @@ -1,26 +0,0 @@ -
    -
    - {{ component('Milhq\\SubmissionList', { soldierId: soldier.id }) }} -
    -
    - {% if milsimUnitsId is not empty %} -
    - -
    - {% endif %} - {% if plugin_version('forumify/forumify-milhq-plugin', 'premium') and setting('milhq.report_in.enabled') %} - {{ component('Milhq\\ReportInButton') }} - {% endif %} - {% for form in forms %} - - - {{ form.name }} - - {% endfor %} -
    -
    diff --git a/tests/PluginTestKernel.php b/tests/PluginTestKernel.php index 375c7b2..fc350aa 100644 --- a/tests/PluginTestKernel.php +++ b/tests/PluginTestKernel.php @@ -38,6 +38,7 @@ public function registerBundles(): iterable new \Zenstruck\Foundry\ZenstruckFoundryBundle(), new \Forumify\Milhq\ForumifyMilhqPlugin(), new \ApiPlatform\Symfony\Bundle\ApiPlatformBundle(), + new \Liip\ImagineBundle\LiipImagineBundle(), ]; if (class_exists(\Forumify\Calendar\ForumifyCalendarPlugin::class)) { diff --git a/tests/Tests/Unit/Repository/UnitRepositoryTest.php b/tests/Tests/Unit/Repository/UnitRepositoryTest.php new file mode 100644 index 0000000..bb8da3e --- /dev/null +++ b/tests/Tests/Unit/Repository/UnitRepositoryTest.php @@ -0,0 +1,72 @@ +get(UnitRepository::class); + + $squadLeader = SoldierFactory::createOne([ + 'unit' => MilsimStory::unitFirstSquad(), + 'position' => MilsimStory::positionSquadLeader(), + ]); + + $units = $unitRepository->findBySoldierIsSupervisor($squadLeader); + $unitNames = array_map(static fn ($unit) => $unit->getName(), $units); + + self::assertCount(1, $units); + self::assertEquals(['First Squad'], $unitNames); + } + + public function testFindBySoldierIsSupervisorAcceptsId(): void + { + MilsimStory::load(); + + /** @var UnitRepository $unitRepository */ + $unitRepository = self::getContainer()->get(UnitRepository::class); + + /** @var Soldier $teamLeader */ + $teamLeader = SoldierFactory::createOne([ + 'unit' => MilsimStory::unitSecondSquad(), + 'position' => MilsimStory::positionTeamLeader(), + ]); + + $units = $unitRepository->findBySoldierIsSupervisor($teamLeader->getId()); + + self::assertCount(1, $units); + self::assertEquals('Second Squad', $units[0]->getName()); + } + + public function testFindBySoldierIsSupervisorReturnsEmptyForNonSupervisor(): void + { + MilsimStory::load(); + + /** @var UnitRepository $unitRepository */ + $unitRepository = self::getContainer()->get(UnitRepository::class); + + $rifleman = SoldierFactory::createOne([ + 'unit' => MilsimStory::unitFirstSquad(), + 'position' => MilsimStory::positionRiflemanAT(), + ]); + + $units = $unitRepository->findBySoldierIsSupervisor($rifleman); + + self::assertCount(0, $units); + } +} diff --git a/tests/config/routes/liip.yaml b/tests/config/routes/liip.yaml new file mode 100644 index 0000000..201cbd5 --- /dev/null +++ b/tests/config/routes/liip.yaml @@ -0,0 +1,2 @@ +_liip_imagine: + resource: "@LiipImagineBundle/Resources/config/routing.yaml" diff --git a/translations/messages+intl-icu.en.yaml b/translations/messages+intl-icu.en.yaml index d834dc6..8ab4365 100644 --- a/translations/messages+intl-icu.en.yaml +++ b/translations/messages+intl-icu.en.yaml @@ -222,7 +222,7 @@ milhq: opcenter: operations_center: 'Operations Center' pfile: 'Personnel File' - submissions: 'Dashboard' + dashboard: 'Dashboard' newsfeed: 'Newsfeed' submission_created: 'Form submission created.' status_history: 'Status history' @@ -237,6 +237,8 @@ milhq: class_last: 'Last ran:' class_never: 'Never' attendance: 'Attendance Sheet' + submissions: 'Submissions' + unit_submissions: 'Unit Submissions' operation: missions: 'Missions' operation: 'Operation'