<?php
namespace App\FHI360\Access\Portal\Controller;
use App\FHI360\Access\Portal\Service\Form\FormManager;
use App\FHI360\Access\Portal\Service\Form\Report\ReportForm;
use App\FHI360\Access\Portal\Service\HookService;
use App\FHI360\Access\Portal\Service\Module\Form\ActiveEditor;
use App\FHI360\Access\Portal\Service\TableBackupService;
use App\FHI360\Access\Portal\Service\UploadFileHandler;
use Curl\Curl;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use App\FHI360\Access\Portal\Service\Loader;
use App\FHI360\Access\Portal\Service\Utility;
use App\FHI360\Access\Portal\Service\DateTimeHelper;
use App\FHI360\Access\Portal\Service\ReportDraftManager;
use Symfony\Component\HttpFoundation\Response;
class ReportFormController extends FormController
{
public static $useJsonPostData = false;
public $recordType = 'report';
public $name = 'report'; // unused - use recordType instead
public $repositoryName = 'reportDrafts'; // deprecated - only use recordType
public $formClass = 'Report\ReportForm';
public $accessRestrictions = [
'all' => ['reportNotInMaintenance'],
'edit' => ['userCanAccessReport'],
];
public $formClasses = [
'english_program' => 'Report\ScholarshipReportForm',
'full' => 'Report\FullReportForm',
'students' => 'Report\StudentsReportForm',
'instructors' => 'Report\InstructorsReportForm',
'teacher_training' => 'Report\TrainingReportForm',
];
public $recordsPerPage = 10;
public function addHooks()
{
parent::addHooks();
HookService::add('form.controller.initialize', fn() => ActiveEditor::initialize());
HookService::add('form.controller.displayForm', function ($properties) {
$controller = $properties['controller'];
if (!$controller->onlineReportsEnabled()) return ['abort' => $controller->abort()];
});
}
public function createReport($id)
{
// get record data
$reportData = Loader::reports()->getRecord([
'fields' => ['agreement_id', 'period_number_c AS current_period'],
'wheres' => ['p0520_program_report.id = :id'],
'parameters' => ['id' => $id],
]);
$draft = ['report_id' => $id];
$currentPeriod = $reportData['current_period'];
// populate draft
$draft = ReportDraftManager::populateDraft($draft, $currentPeriod);
// insert draft and fetch the record (with defaults)
Loader::reportDrafts()->insert($draft);
// update report status
Loader::reports()->update($id, [
'report_status' => $draft['content']['current_status'],
'report_created_date' => date('Y-m-d'),
]);
}
/**
* Show report data
* @Route("/modules/report/show/{id}", name="module-report-show")
* @param $id
* @return
*/
public function show($id)
{
if($this->getParameter('app_env') !== 'dev') Utility::abort('This page is not available');
$draft = Loader::reportDrafts()->getLatestByRecordId($id);
Utility::abort($draft);
}
/**
* Display form to view a previous revision of a record
* @Route("/modules/report/edit/{id}/{revision}", requirements={"revision"="\d+"}, name="module-report-previous-revision")
*/
public function viewPreviousRevision($id, $revision) {
return $this->displayForm($id, [
'pageMode' => 'previousRevision',
'revision' => $revision,
]);
}
/**
* Display form to view record
* @Route("/modules/report/edit/{id}/readonly", name="module-report-view")
*/
public function view($id) {
return $this->displayForm($id, [
'pageMode' => 'readOnly',
]);
}
/**
* Display form to edit record
* @Route("/modules/report/edit/{id}", name="module-report-edit")
*/
public function edit($id) {
return $this->displayForm($id, ['pageMode' => 'edit']);
}
/**
* Create a new report and display the form
* @Route("/modules/report/create/{id}", name="module-report-create")
*/
public function create($id)
{
if (! $this->onlineReportsEnabled()) {
return $this->abort();
}
if ($this->getRestriction('edit', ['id' => $id])) {
return $this->abort();
}
$error = $this->errorForReportCreation($id);
if ($error !== '') {
return $this->abort();
}
$this->createReport($id);
return $this->redirect('/modules/report/edit/' . $id);
}
/**
* Resets the budget fields to the agreement values, then redirects to edit view
* @Route("/modules/report/reset-budget/{id}", name="module-report-reset-budget")
*/
public function resetBudget($id): Response
{
if (Loader::users()->getRole($this->getUserId()) !== 'fhi') {
// Only FHI users can reset
return $this->abort();
}
ReportDraftManager::resetDraftBudget($id);
return $this->redirect('/modules/report/edit/' . $id);
}
/**
* Resets the payments table to the latest values, then redirects to edit view
* @Route("/modules/report/reset-payments/{id}", name="module-report-reset-payments")
*/
public function resetPayments($id): Response
{
if (Loader::users()->getRole($this->getUserId()) !== 'fhi') {
// Only FHI users can reset
return $this->abort();
}
ReportDraftManager::resetDraftDisbursements($id);
return $this->redirect('/modules/report/edit/' . $id);
}
// build and display the form
// FIX: once this method is no longer deppendent on controller methods, it can move elsewhere as a static method (eg. FormManager)
protected function displayForm($id, $options = [])
{
$properties = HookService::trigger('form.controller.displayForm', ['controller' => $this, 'id' => $id]);
if ($properties['abort'] ?? null) return $properties['abort'];
if ($this->getRestriction('edit', ['id' => $id])) {
return $this->abort();
}
// get record fields
$repository = Loader::getRepositoryForRecordType($this->recordType);
$record = $repository->getRecordFields($id);
// get the draft
$draftRepository = Loader::getRepositoryForRecordType($this->recordType, 'draft');
if (!empty($options['revision'])) {
$draft = $draftRepository->getByRecordIdAndMajorRevision($id, $options['revision']);
} else {
$draft = $draftRepository->getLatestByRecordId($id);
if (!$draft) {
$this->createReport($id);
$draft = $draftRepository->getLatestByRecordId($id);
if (!$draft) return $this->abort('A draft could not be created for this report.');
}
}
// upate draft fields and subforms
$draft = ReportDraftManager::updateDraft($draft);
// build the form and set the data
$defaults = ['pageMode' => 'edit', 'formType' => 'english_program', 'navigationButtons' => true];
$options = array_replace($defaults, $options);
// get report information
$userRole = Loader::users()->getRole($this->getUserId());
$options['recordInformation'] = $record;
$form = $this->buildForm($draft, $options);
$form->setDataFromDraft($draft);
// return the form
$statusRepository = Loader::getRepositoryForRecordType($this->recordType, 'status');
$parameters = [
'form' => $form->getForm(),
'audits' => [],
'status' => $draft['current_status'],
'statusName' => $statusRepository->statuses[$draft['current_status']]['name'],
'stepper' => $this->getStepperData($form),
'user_role' => $userRole,
'title' => $draft['content']['name'],
'id' => $id,
'lastUpdated' => $draft['date_modified'],
'sideBarButtons' => $form->getSideBarButtonsToDisplay($id),
'sideBarPreferences' => Loader::userPreferences()->getSideBarPreferences(),
'majorVersion' => $form->getMajorVersion(),
];
return $this->respond('edit', $parameters);
}
public function abortIfInputLimitExceeded(): ?Response
{
if (Utility::arrayExceedsMaxInput(self::getPostData())) {
return $this->error(['method' => 'input_limit_exceeded', 'error' => 'input limit exceeded']);
}
return null;
}
// check if record is in maintenance mode
public function abortIfInMaintenance($id)
{
if(Loader::reports()->isReportUnderMaintenance($id)) {
return $this->error(['method' => 'maintenance', 'error' => "This Report is under maintenance mode so it cannot be modified, please refresh the page in a few minutes and try again."]);
}
}
/**
* @Route("/modules/report/update/{id}", name="module-report-update")
*/
public function handleEdit($id)
{
if (! $this->onlineReportsEnabled()) {
return $this->abort();
}
if ($this->getRestriction('edit', ['id' => $id])) {
return $this->abort();
}
$abortResponse = $this->abortIfInMaintenance($id);
if($abortResponse) return $abortResponse;
$abortResponse = $this->abortIfInputLimitExceeded();
if($abortResponse) return $abortResponse;
// get existing draft
$draft = Loader::reportDrafts()->getLatestByRecordId($id);
$draftId = $draft['id'];
Loader::backup()->run($id, TableBackupService::REPORT_DRAFT);
$data = FormManager::updateFormDataFromPost($draft['content'], 'report');
$draft['content'] = $data;
// ensure that record status hasn't changed since form was loaded - otherwise trigger refresh
if ($draft['current_status'] !== ($_REQUEST['record_status'] ?? $_REQUEST['client_side_status'])) {
return $this->error(['method' => 'staleDataWarning', 'error' => "Form contains outdated data."]);
}
// ensure that form version number hasn't changed since form was loaded - otherwise trigger refresh
$formVersionNumber = FormManager::$versionNumber;
if ($formVersionNumber !== intval($_REQUEST['version_number'])) {
return $this->error(['method' => 'staleDataWarning', 'error' => "The system has new updates, a refresh is required to continue."]);
}
return FormManager::saveData($id, $draftId, $draft, 'report');
}
public function buildForm(array $record, $options = []): ReportForm
{
// get the report type
$programType = ($options['recordInformation']['program_type'] ?? $options['recordInformation']['program_type'] ?? null) ?: 'english_program';
$options['formType'] = $programType;
return parent::buildForm($record, $options);
}
/** @Route("/modules/report/reset/{id}", defaults={"id"=""}, name="module-report-reset") */
public function reset($id)
{
if (! $this->onlineReportsEnabled()) {
return $this->abort();
}
if ($this->getRestriction('edit', ['id' => $id])) {
return $this->abort();
}
$drafts = Loader::reportDrafts()->getRecords(
['fields' => ['id'], 'wheres' => ['report_id = :report_id'], 'parameters' => ['report_id' => $id]]
);
foreach ($drafts as $draft) {
Loader::reportDrafts()->deleteById($draft['id'], ['softDelete' => false]);
}
return $this->success();
}
/** @Route("/modules/report/update-success/{id}", defaults={"id"=""}, name="module-report-update-success") */
public function updateSuccess($id)
{
$userId = $this->getUserId();
$url = Utility::getUrl("/modules/report/edit/$id");
$user_role = Loader::users()->getRole($this->getUserId());
$draft = Loader::reportDrafts()->getLatestByRecordId($id);
$current_status = $draft['current_status'];
$params = [
'id' => compact('id'),
'user_role' => $user_role,
'report_url' => $url,
];
return $this->respond('success', $params);
}
protected function getStepperData(ReportForm $form): array
{
$steps = $this->getStepperSteps();
$status = $this->getLabelForStatus($form->getProperty('status'));
$userId = $this->getUserId();
$user_role = Loader::users()->getRole($userId);
$audit_data = $this->getAuditTrail($form->getProperty('id'));
$new_audit_data = [];
foreach ($audit_data as $data) {
if ($user_role === 'provider' && !in_array('provider', $data['roles_involved'])) {
continue;
}
if ($data['roles_involved'] === ['eca', 'fhi']) {
if ($user_role !== 'fhi' && $user_role !== 'eca') {
unset($data['comment']);
}
}
array_push($new_audit_data, $data);
}
return [
'audits' => $new_audit_data,//$this->getAuditTrail($form->getProperty('id')),
'current' => $status,
'steps' => base64_encode(json_encode(array_values($steps))),
'user_role' => $user_role,
];
}
// reportfix: consolidate code that report and proposal have in common
protected function getStepperSteps(): array
{
$statuses = [];
foreach (Loader::reportStatuses()->statuses as $name => $status) {
$label = $this->getLabelForStatus($name);
if ($label) {
$statuses[$label] = $label;
}
}
return $statuses;
}
protected function getAuditTrail($id): array
{
$formattedAudits = [];
$isProvider = $this->isProvider();
$audits = Loader::reportDrafts()->getAuditData($id);
$statuses = Loader::reportStatuses()->statuses;
$actions = [
'create' => 'Created',
'submit' => 'Submitted',
'send_back' => 'Sent back',
'approve' => 'Approved',
];
$providerStatuses = [
'provider_completes_report',
'provider_revises_report',
];
//Now we have group the audit records by sequence, lets go thorough them and build the audit trail for each
foreach($audits as $audit){
$date = $audit['date_entered'];
$date_to_et = DateTimeHelper::dbToEt($date);
$audit['date_entered'] = $date_to_et;
$oldStatus = $statuses[$audit['status_before_value']] ?? [];
$newStatus = $statuses[$audit['status_after_value']] ?? [];
$labelKey = $isProvider ? 'nameForProvider' : 'name';
if ($isProvider){
if (empty($newStatus[$labelKey]) && empty($oldStatus[$labelKey])) {
continue;
}
}else{
if (empty($newStatus[$labelKey]) || empty($oldStatus[$labelKey])) {
continue;
}
}
if ($isProvider
&& !in_array($audit['status_after_value'], $providerStatuses, true)
&& !in_array($audit['status_before_value'], $providerStatuses, true)) {
continue;
}
$formattedAudit['from_role'] = $oldStatus['awaitingAction'] ?? 'none';
$formattedAudit['to_role'] = $newStatus['awaitingAction'] ?? 'none';
$formattedAudit['roles_involved'] = [$formattedAudit['from_role'], $formattedAudit['to_role']];
sort($formattedAudit['roles_involved']);
$formattedAudit['date_entered'] = date("M j, Y H:i:s A", strtotime($audit['date_entered']));
$formattedAudit['modified_by'] = $audit['user_last_modified'];
$formattedAudit['old_status'] = $oldStatus[$labelKey];
$formattedAudit['new_status'] = $newStatus[$labelKey];
$formattedAudit['action'] = $actions[$audit['decision']] ?? 'Updated';
$formattedAudit['comment'] = $audit['decision_comment'];
$formattedAudits[] = $formattedAudit;
}
return $formattedAudits;
}
// reportfix: consolidate code that report and proposal have in common
protected function getLabelForStatus(string $status): string
{
$isProvider = $this->isProvider();
$statuses = Loader::reportStatuses()->statuses;
if (isset($statuses[$status]['nameForProvider']) && $isProvider) {
return $statuses[$status]['nameForProvider'];
}
return $statuses[$status]['name'] ?? 'Undefined Status';
}
// reportfix: consolidate code that report and proposal have in common
protected function isProvider(): bool
{
return Loader::users()->getRole($this->user->getId()) === 'provider';
}
/**
* @Route("/modules/report/save-document/{id}/{field}", name="module-report-save-document")
* @param $id
* @return Response
*/
public function saveDocument($id, $field)
{
$file = UploadFileHandler::forReports()->getFile();
$filename = $file['name'];
$document_id = Loader::documents()->createFromFile($file);
return $this->success(compact('document_id', 'filename'));
}
/**
* @Route("/modules/report/check-provider-setup/{report_id}", name="module-report-check-provider-setup")
* @param $report_id
* @return Response
*/
public function checkProviderSetup($report_id)
{
return new Response(json_encode(['error' => $this->errorForReportCreation($report_id)]));
}
public function errorForReportCreation(string $id): string
{
$suiteUrl = $this->container->getParameter('suitecrml_url');
$url = $suiteUrl . "/index.php?entryPoint=OnlineReportCheck&report_id=" . $id;
$curl = new Curl($url);
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, false);
$curl->setOpt(CURLOPT_RETURNTRANSFER, true);
$curl->setOpt(CURLOPT_FOLLOWLOCATION, false);
$curl->get($url);
$data = json_decode($curl->response, true);
return $data['error'];
}
protected function onlineReportsEnabled(): bool
{
return $this->container->getParameter('online_reporting') ?? false;
}
}