bootstrapCustomFieldDefinitions(); } public function rules() { return [ [['visit_at', 'provider_name', 'notes', 'recommendations'], 'safe'], [['provider_name'], 'string', 'max' => 190], [['notes', 'recommendations'], 'string'], [['post_to_space_feed', 'post_to_animal_feed'], 'boolean'], [['customFields'], 'safe'], [['customFields'], 'validateCustomFieldValues'], ]; } public function save(): bool { if (!$this->validate()) { return false; } $record = $this->medicalVisit instanceof AnimalMedicalVisit ? $this->medicalVisit : new AnimalMedicalVisit(); $isNew = $record->getIsNewRecord(); if ($isNew) { $record->animal_id = (int)$this->animal->id; $record->created_by = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id; } $record->visit_at = $this->normalizeDateTime($this->visit_at); $record->provider_name = trim($this->provider_name); $record->notes = trim($this->notes); $record->recommendations = trim($this->recommendations); if ($record->hasAttribute('post_to_space_feed') && $record->hasAttribute('post_to_animal_feed')) { $record->post_to_space_feed = $this->post_to_space_feed ? 1 : 0; $record->post_to_animal_feed = $this->post_to_animal_feed ? 1 : 0; } if (!$record->save()) { $this->addErrors($record->getErrors()); return false; } $this->medicalVisit = $record; if (!$this->saveCustomFieldValues($record)) { return false; } if ($this->post_to_space_feed) { AnimalStreamPublisherService::publishMedicalVisit($this->animal, $record); } return true; } public function setMedicalVisit(AnimalMedicalVisit $medicalVisit): void { $this->medicalVisit = $medicalVisit; $this->visit_at = (string)$medicalVisit->visit_at; $this->provider_name = (string)$medicalVisit->provider_name; $this->notes = (string)$medicalVisit->notes; $this->recommendations = (string)$medicalVisit->recommendations; if ($medicalVisit->hasAttribute('post_to_space_feed') && $medicalVisit->hasAttribute('post_to_animal_feed')) { $this->post_to_space_feed = ((int)$medicalVisit->post_to_space_feed) === 1; $this->post_to_animal_feed = ((int)$medicalVisit->post_to_animal_feed) === 1; } else { $this->post_to_space_feed = false; $this->post_to_animal_feed = true; } $this->loadCustomFieldValues($medicalVisit); } public function getCustomFieldDefinitions(): array { return $this->customFieldDefinitions; } public function getCustomFieldSelectOptions(string $fieldKey): array { if (!isset($this->customFieldDefinitions[$fieldKey])) { return []; } return $this->parseSelectOptions((string)$this->customFieldDefinitions[$fieldKey]['options']); } public function validateCustomFieldValues(string $attribute): void { foreach ($this->customFieldDefinitions as $fieldKey => $definition) { $value = $this->customFields[$fieldKey] ?? null; $label = (string)$definition['label']; $inputType = (string)$definition['input_type']; $required = ((int)$definition['required']) === 1; if ($required && $this->isCustomValueEmpty($value, $inputType)) { $this->addError($attribute, Yii::t('AnimalManagementModule.base', '{field} is required.', ['{field}' => $label])); continue; } if ($this->isCustomValueEmpty($value, $inputType)) { continue; } $normalized = is_array($value) ? '' : trim((string)$value); if ($inputType === 'number' && !is_numeric($normalized)) { $this->addError($attribute, Yii::t('AnimalManagementModule.base', '{field} must be a valid number.', ['{field}' => $label])); continue; } if (($inputType === 'date' || $inputType === 'datetime') && strtotime($normalized) === false) { $this->addError($attribute, Yii::t('AnimalManagementModule.base', '{field} must be a valid date/time.', ['{field}' => $label])); continue; } if ($inputType === 'select') { $options = $this->getCustomFieldSelectOptions($fieldKey); if (!empty($options) && !array_key_exists($normalized, $options)) { $this->addError($attribute, Yii::t('AnimalManagementModule.base', 'Invalid selection for {field}.', ['{field}' => $label])); } } } } private function normalizeDateTime(string $value): string { $value = trim($value); if ($value === '') { return date('Y-m-d H:i:s'); } $timestamp = strtotime($value); if ($timestamp === false) { return date('Y-m-d H:i:s'); } return date('Y-m-d H:i:s', $timestamp); } private function bootstrapCustomFieldDefinitions(): void { if (!class_exists(RescueFieldDefinition::class)) { return; } if (Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null) { return; } $definitions = RescueFieldDefinition::find() ->where(['module_id' => self::MODULE_ID, 'group_key' => 'animal_medical_visit', 'is_active' => 1]) ->orderBy(['sort_order' => SORT_ASC, 'id' => SORT_ASC]) ->all(); foreach ($definitions as $definition) { $fieldKey = (string)$definition->field_key; $this->customFieldDefinitions[$fieldKey] = [ 'id' => (int)$definition->id, 'field_key' => $fieldKey, 'label' => trim((string)$definition->label) !== '' ? (string)$definition->label : $fieldKey, 'input_type' => (string)$definition->input_type, 'required' => (int)$definition->required, 'visibility' => (string)$definition->visibility, 'options' => (string)$definition->options, ]; } } private function saveCustomFieldValues(AnimalMedicalVisit $record): bool { foreach ($this->customFieldDefinitions as $fieldKey => $definition) { $fieldDefinitionId = (int)$definition['id']; if ($fieldDefinitionId === 0) { continue; } $inputType = (string)$definition['input_type']; $rawValue = $this->customFields[$fieldKey] ?? null; $valueText = $this->normalizeCustomValue($rawValue, $inputType); $valueRecord = AnimalMedicalVisitFieldValue::findOne([ 'medical_visit_id' => (int)$record->id, 'field_definition_id' => $fieldDefinitionId, ]); if ($this->isCustomValueEmpty($valueText, $inputType)) { if ($valueRecord instanceof AnimalMedicalVisitFieldValue) { $valueRecord->delete(); } continue; } if (!$valueRecord instanceof AnimalMedicalVisitFieldValue) { $valueRecord = new AnimalMedicalVisitFieldValue(); $valueRecord->medical_visit_id = (int)$record->id; $valueRecord->field_definition_id = $fieldDefinitionId; } $valueRecord->value_text = $valueText; $valueRecord->value_json = null; if (!$valueRecord->save()) { $this->addError('customFields', Yii::t('AnimalManagementModule.base', 'Could not save custom medical field {field}.', [ '{field}' => (string)$definition['label'], ])); foreach ($valueRecord->getFirstErrors() as $error) { $this->addError('customFields', $error); } } } return !$this->hasErrors('customFields'); } private function normalizeCustomValue($value, string $inputType): ?string { if ($inputType === 'boolean') { return !empty($value) ? '1' : '0'; } if (is_array($value)) { return Json::encode($value); } return trim((string)$value); } private function isCustomValueEmpty($value, string $inputType): bool { if ($inputType === 'boolean') { return false; } if (is_array($value)) { return empty($value); } return trim((string)$value) === ''; } private function parseSelectOptions(string $options): array { $options = trim($options); if ($options === '') { return []; } $decoded = null; try { $decoded = Json::decode($options, true); } catch (\Throwable $e) { $decoded = null; } if (is_array($decoded)) { $result = []; if (array_values($decoded) === $decoded) { foreach ($decoded as $item) { $item = (string)$item; if ($item === '') { continue; } $result[$item] = $item; } return $result; } foreach ($decoded as $key => $value) { $key = (string)$key; if ($key === '') { continue; } $result[$key] = (string)$value; } return $result; } $result = []; foreach (preg_split('/[\r\n,]+/', $options) as $item) { $item = trim((string)$item); if ($item === '') { continue; } $result[$item] = $item; } return $result; } private function loadCustomFieldValues(AnimalMedicalVisit $record): void { foreach ($this->customFieldDefinitions as $fieldKey => $definition) { $value = AnimalMedicalVisitFieldValue::findOne([ 'medical_visit_id' => (int)$record->id, 'field_definition_id' => (int)$definition['id'], ]); if (!$value instanceof AnimalMedicalVisitFieldValue) { continue; } $this->customFields[$fieldKey] = (string)$value->value_text; } } }