diff --git a/app/Http/Requests/Activity/Period/PeriodRequest.php b/app/Http/Requests/Activity/Period/PeriodRequest.php index 49c618ada..561081c58 100644 --- a/app/Http/Requests/Activity/Period/PeriodRequest.php +++ b/app/Http/Requests/Activity/Period/PeriodRequest.php @@ -6,6 +6,7 @@ use App\Http\Requests\Activity\ActivityBaseRequest; use App\IATI\Services\Activity\IndicatorService; +use App\Rules\RequiredEitherNumericTargetValueOrActualValue; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator; @@ -59,8 +60,8 @@ public function getWarningForPeriod(array $formFields, bool $fileUpload = false, $tempRules = [ $this->getWarningForIndicatorPeriodStart($formFields['period_start'], 'period_start'), $this->getWarningForIndicatorPeriodEnd($formFields['period_end'], 'period_end', $periodBase), - $this->getWarningForTarget($formFields['target'], 'target', $fileUpload, $indicator, $indicatorId), - $this->getWarningForTarget($formFields['actual'], 'actual', $fileUpload, $indicator, $indicatorId), + $this->getWarningForTargetOrActual($formFields, 'target', $fileUpload, $indicator, $indicatorId), + $this->getWarningForTargetOrActual($formFields, 'actual', $fileUpload, $indicator, $indicatorId), ]; foreach ($tempRules as $index => $tempRule) { @@ -119,8 +120,8 @@ public function getMessagesForPeriod(array $formFields, bool $fileUpload = false $tempMessages = [ $this->getMessagesForResultPeriod($formFields['period_start'], 'period_start'), $this->getMessagesForResultPeriod($formFields['period_end'], 'period_end'), - $this->getMessagesForTarget($formFields['target'], 'target', $fileUpload, $indicator, $indicatorId), - $this->getMessagesForTarget($formFields['actual'], 'actual', $fileUpload, $indicator, $indicatorId), + $this->getMessagesForTargetOrActual($formFields['target'], 'target', $fileUpload, $indicator, $indicatorId), + $this->getMessagesForTargetOrActual($formFields['actual'], 'actual', $fileUpload, $indicator, $indicatorId), ]; foreach ($tempMessages as $index => $tempMessage) { @@ -274,8 +275,12 @@ protected function getMessagesForResultPeriod($formFields, $periodType): array * @return array * @throws BindingResolutionException */ - protected function getWarningForTarget($formFields, $valueType, $fileUpload, $indicator, $indicatorId): array + protected function getWarningForTargetOrActual($formFields, $valueType, $fileUpload, $indicator, $indicatorId): array { + $targetOrActualFields = $formFields[$valueType]; + $fieldToValidateAgainst = $valueType === 'target' ? 'actual' : 'target'; + + /** @var IndicatorService $indicatorService */ $rules = []; $indicatorService = app()->make(IndicatorService::class); @@ -294,7 +299,12 @@ protected function getWarningForTarget($formFields, $valueType, $fileUpload, $in return false; }); - foreach ($formFields as $targetIndex => $target) { + /* + * The variables names are not factual. + * DO NOT get confused with the variable names. + * Since this method is called for both actual and target fields. + */ + foreach ($targetOrActualFields as $targetIndex => $target) { $targetForm = sprintf('%s.%s', $valueType, $targetIndex); $narrativeRules = $this->getWarningForNarrative($target['comment'][0]['narrative'], sprintf('%s.comment.0', $targetForm)); $docLinkRules = $this->getWarningForDocumentLink(Arr::get($target, 'document_link', []), $targetForm); @@ -308,7 +318,7 @@ protected function getWarningForTarget($formFields, $valueType, $fileUpload, $in } if ($indicatorMeasureType['non_qualitative']) { - $rules[sprintf('%s.%s.value', $valueType, $targetIndex)] = 'required|numeric'; + $rules[sprintf('%s.%s.value', $valueType, $targetIndex)] = [new RequiredEitherNumericTargetValueOrActualValue($valueType, $fieldToValidateAgainst, $formFields)]; } elseif ($indicatorMeasureType['qualitative'] && !empty($target['value'])) { $rules[sprintf('%s.%s.value', $valueType, $targetIndex)] = 'qualitative_empty'; } @@ -361,8 +371,9 @@ protected function getErrorsForTarget($formFields, $valueType, $fileUpload, $ind * @return array * @throws BindingResolutionException */ - protected function getMessagesForTarget($formFields, $valueType, $fileUpload, $indicator, $indicatorId): array + protected function getMessagesForTargetOrActual($formFields, $valueType, $fileUpload, $indicator, $indicatorId): array { + /** @var IndicatorService $indicatorService */ $messages = []; $indicatorService = app()->make(IndicatorService::class); @@ -394,9 +405,7 @@ protected function getMessagesForTarget($formFields, $valueType, $fileUpload, $i $messages[$key] = $docLinkMessage; } - if ($indicatorMeasureType['non_qualitative']) { - $messages[sprintf('%s.%s.value', $valueType, $targetIndex)] = 'Value must be filled when the indicator measure is non-qualitative.'; - } elseif ($indicatorMeasureType['qualitative'] && !empty($target['value'])) { + if ($indicatorMeasureType['qualitative'] && !empty($target['value'])) { $messages[sprintf('%s.%s.value.qualitative_empty', $valueType, $targetIndex)] = 'Value must be omitted when the indicator measure is qualitative.'; } } diff --git a/app/Rules/RequiredEitherNumericTargetValueOrActualValue.php b/app/Rules/RequiredEitherNumericTargetValueOrActualValue.php new file mode 100644 index 000000000..cd09d5859 --- /dev/null +++ b/app/Rules/RequiredEitherNumericTargetValueOrActualValue.php @@ -0,0 +1,114 @@ +bothTargetAnDActualHaveEmptyValues()) { + return false; + } + + /** + * I'm doing this because $value can be null or N empty spaces. + * Null during creation/untouched value field. + * Empty spaces when value filed data is edited/cleared. + * strlen > 0 because I'm allowing ''. + */ + $value = trim($value ?? ''); + + if ($this->otherFieldHasAtleastOneNonEmptyValue() && strlen($value) > 0 && !is_numeric($value)) { + $this->errorType = 'numeric'; + + return false; + } + + return true; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message(): string + { + if ($this->errorType === 'numeric') { + return 'The @value field must be numeric.'; + } + + return sprintf('%s value is required if %s value is not provided.', ucfirst($this->field), $this->otherField); + } + + /** + * Check if both target and actual fields have empty value field. + * + * @return bool + */ + private function bothTargetAndActualHaveEmptyValues(): bool + { + $targetValues = Arr::get($this->formFields, 'target', []); + $actualValues = Arr::get($this->formFields, 'actual', []); + + $targetAllNonEmpty = !collect($targetValues)->every(function ($item) { + return !empty(trim($item['value'] ?? '')); + }); + + $actualAllNonEmpty = !collect($actualValues)->every(function ($item) { + return !empty(trim($item['value'] ?? '')); + }); + + return $targetAllNonEmpty && $actualAllNonEmpty; + } + + /** + * Check if the other field (target or actual) has at least one non-empty value field. + * + * @return bool + */ + private function otherFieldHasAtleastOneNonEmptyValue(): bool + { + foreach ($this->formFields[$this->otherField] as $item) { + $item = trim($item['value'] ?? ''); + if ($item !== '') { + return true; + } + } + + return false; + } +} diff --git a/resources/assets/sass/component/_input.scss b/resources/assets/sass/component/_input.scss index e2c936663..0eef68c73 100644 --- a/resources/assets/sass/component/_input.scss +++ b/resources/assets/sass/component/_input.scss @@ -256,41 +256,41 @@ label { } select.select2.default-value-indicator - + .select2 - .selection - .select2-selection:not(:focus) { ++ .select2 +.selection +.select2-selection:not(:focus) { border: 2px solid #3f9a7c; background-color: #3f9a7c15; } select.select2.default-value-indicator - + .select2 - .selection - .select2-selection:not(:focus) { ++ .select2 +.selection +.select2-selection:not(:focus) { border: 2px solid #3f9a7c; background-color: #3f9a7c15; } select.select2.default-value-indicator - + .select2 - .selection - .select2-selection - .select2-selection__placeholder { ++ .select2 +.selection +.select2-selection +.select2-selection__placeholder { color: var(--bluecoral-50); } select.select2.default-value-indicator - + .select2.select2-container--open - .selection - .select2-selection { ++ .select2.select2-container--open +.selection +.select2-selection { border: 1px solid #a6b5ba; background-color: transparent; } select.select2.default-value-indicator - + .select2 - .selection - .select2-selection.select2-selection--clearable { ++ .select2 +.selection +.select2-selection.select2-selection--clearable { border: 1px solid #a6b5ba; background-color: transparent; }