bootstrapCustomFieldDefinitions(); } public function rules() { return [ [['update_at', 'weight', 'vitals', 'behavior_notes', 'meal_plan_changes', 'housing_changes', 'medical_concerns'], 'safe'], [['vitals', 'behavior_notes', 'meal_plan_changes', 'housing_changes', 'medical_concerns'], 'string'], [['weight'], 'string', 'max' => 32], [['post_to_space_feed', 'post_to_animal_feed'], 'boolean'], [['customFields'], 'safe'], [['customFields'], 'validateCustomFieldValues'], ]; } public function save(): bool { if (!$this->validate()) { return false; } $record = $this->progressUpdate instanceof AnimalProgressUpdate ? $this->progressUpdate : new AnimalProgressUpdate(); $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->update_at = $this->normalizeDateTime($this->update_at); $record->weight = trim($this->weight); $record->vitals = trim($this->vitals); $record->behavior_notes = trim($this->behavior_notes); $record->meal_plan_changes = trim($this->meal_plan_changes); $record->housing_changes = trim($this->housing_changes); $record->medical_concerns = trim($this->medical_concerns); $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->progressUpdate = $record; if (!$this->saveCustomFieldValues($record)) { return false; } if ($record->post_to_space_feed) { AnimalStreamPublisherService::publishProgressUpdate($this->animal, $record); } return true; } public function setProgressUpdate(AnimalProgressUpdate $progressUpdate): void { $this->progressUpdate = $progressUpdate; $this->update_at = (string)$progressUpdate->update_at; $this->weight = (string)$progressUpdate->weight; $this->vitals = (string)$progressUpdate->vitals; $this->behavior_notes = (string)$progressUpdate->behavior_notes; $this->meal_plan_changes = (string)$progressUpdate->meal_plan_changes; $this->housing_changes = (string)$progressUpdate->housing_changes; $this->medical_concerns = (string)$progressUpdate->medical_concerns; $this->post_to_space_feed = ((int)$progressUpdate->post_to_space_feed) === 1; $this->post_to_animal_feed = ((int)$progressUpdate->post_to_animal_feed) === 1; $this->loadCustomFieldValues($progressUpdate); } 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_progress_update', '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(AnimalProgressUpdate $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 = AnimalProgressUpdateFieldValue::findOne([ 'progress_update_id' => (int)$record->id, 'field_definition_id' => $fieldDefinitionId, ]); if ($this->isCustomValueEmpty($valueText, $inputType)) { if ($valueRecord instanceof AnimalProgressUpdateFieldValue) { $valueRecord->delete(); } continue; } if (!$valueRecord instanceof AnimalProgressUpdateFieldValue) { $valueRecord = new AnimalProgressUpdateFieldValue(); $valueRecord->progress_update_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 progress 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(AnimalProgressUpdate $record): void { foreach ($this->customFieldDefinitions as $fieldKey => $definition) { $value = AnimalProgressUpdateFieldValue::findOne([ 'progress_update_id' => (int)$record->id, 'field_definition_id' => (int)$definition['id'], ]); if (!$value instanceof AnimalProgressUpdateFieldValue) { continue; } $this->customFields[$fieldKey] = (string)$value->value_text; } } }