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'