Initial import of animal_management module

This commit is contained in:
Kelin Rescue Hub
2026-04-04 13:13:00 -04:00
commit 20adb1bd1e
65 changed files with 14004 additions and 0 deletions

213
models/Animal.php Normal file
View File

@@ -0,0 +1,213 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
use Yii;
use yii\helpers\Json;
class Animal extends ActiveRecord
{
public const STATUS_INTAKE = 'intake';
public const STATUS_ACTIVE = 'active';
public const STATUS_TRANSFER_PENDING = 'transfer_pending';
public const STATUS_TRANSFERRED = 'transferred';
public const STATUS_ADOPTED = 'adopted';
public static function tableName()
{
return 'rescue_animal';
}
public function rules()
{
return [
[['contentcontainer_id', 'animal_uid'], 'required'],
[['contentcontainer_id', 'previous_owner_user_id', 'in_possession'], 'integer'],
[['public_summary', 'medical_notes'], 'string'],
[['name', 'animal_uid'], 'string', 'max' => 190],
[['species', 'breed', 'location_name'], 'string', 'max' => 120],
[['sex', 'status'], 'string', 'max' => 32],
[['city'], 'string', 'max' => 120],
[['state'], 'string', 'max' => 2],
[['zip'], 'string', 'max' => 10],
[['animal_uid'], 'unique'],
[['status'], 'in', 'range' => array_keys(self::statusOptions())],
];
}
public function beforeValidate()
{
if (!parent::beforeValidate()) {
return false;
}
if ($this->isNewRecord && empty($this->animal_uid)) {
$this->animal_uid = $this->generateAnimalUid();
}
if (empty($this->status)) {
$this->status = self::STATUS_INTAKE;
}
return true;
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}
public static function statusOptions(): array
{
return [
self::STATUS_INTAKE => Yii::t('AnimalManagementModule.base', 'Intake'),
self::STATUS_ACTIVE => Yii::t('AnimalManagementModule.base', 'Active'),
self::STATUS_TRANSFER_PENDING => Yii::t('AnimalManagementModule.base', 'Transfer Pending'),
self::STATUS_TRANSFERRED => Yii::t('AnimalManagementModule.base', 'Transferred'),
self::STATUS_ADOPTED => Yii::t('AnimalManagementModule.base', 'Adopted'),
];
}
public function getDisplayName(): string
{
$name = trim((string)$this->name);
return $name !== '' ? $name : (string)$this->animal_uid;
}
public function getTransfers()
{
return $this->hasMany(AnimalTransfer::class, ['animal_id' => 'id']);
}
public function getMedicalVisits()
{
return $this->hasMany(AnimalMedicalVisit::class, ['animal_id' => 'id']);
}
public function getProgressUpdates()
{
return $this->hasMany(AnimalProgressUpdate::class, ['animal_id' => 'id']);
}
public function getTransferEvents()
{
return $this->hasMany(AnimalTransferEvent::class, ['animal_id' => 'id']);
}
public function getFieldValues()
{
return $this->hasMany(AnimalFieldValue::class, ['animal_id' => 'id']);
}
public function getGalleryItems()
{
return $this->hasMany(AnimalGalleryItem::class, ['animal_id' => 'id']);
}
public function getCustomFieldDisplayValues(bool $includeRestricted = false): array
{
$values = [];
foreach ($this->getFieldValues()->with('fieldDefinition')->all() as $fieldValue) {
if (!$fieldValue instanceof AnimalFieldValue) {
continue;
}
$definition = $fieldValue->fieldDefinition;
if ($definition === null || (string)$definition->module_id !== 'animal_management') {
continue;
}
if ((int)$definition->is_active !== 1 || (int)$definition->is_core === 1) {
continue;
}
if ((string)$definition->group_key !== 'animal_profile') {
continue;
}
if (in_array((string)$definition->field_key, [
'cover_image_url',
'profile_image_url',
'photo_url',
'image_url',
'profile_image',
'photo',
'tile_display_fields',
'hero_display_fields',
], true)) {
continue;
}
$visibility = (string)$definition->visibility;
if (!$includeRestricted && $visibility !== 'public') {
continue;
}
$raw = trim((string)$fieldValue->value_text);
if ($raw === '') {
continue;
}
$display = $raw;
$inputType = (string)$definition->input_type;
if ($inputType === 'boolean') {
$display = in_array(strtolower($raw), ['1', 'true', 'yes', 'on'], true)
? Yii::t('AnimalManagementModule.base', 'Yes')
: Yii::t('AnimalManagementModule.base', 'No');
} elseif ($inputType === 'select') {
$display = $this->mapSelectDisplayValue((string)$definition->options, $raw);
}
$values[] = [
'field_key' => (string)$definition->field_key,
'label' => trim((string)$definition->label) !== '' ? (string)$definition->label : (string)$definition->field_key,
'value' => $display,
'visibility' => $visibility,
];
}
return $values;
}
private function mapSelectDisplayValue(string $options, string $raw): string
{
$options = trim($options);
if ($options === '') {
return $raw;
}
$decoded = Json::decode($options, true);
if (!is_array($decoded)) {
return $raw;
}
if (array_values($decoded) === $decoded) {
return in_array($raw, $decoded, true) ? $raw : $raw;
}
return isset($decoded[$raw]) ? (string)$decoded[$raw] : $raw;
}
private function generateAnimalUid(): string
{
do {
$candidate = 'ANI-' . date('Ymd') . '-' . strtoupper(substr(Yii::$app->security->generateRandomString(8), 0, 6));
$exists = static::find()->where(['animal_uid' => $candidate])->exists();
} while ($exists);
return $candidate;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
use humhub\modules\rescue_foundation\models\RescueFieldDefinition;
class AnimalFieldValue extends ActiveRecord
{
public static function tableName()
{
return 'rescue_animal_field_value';
}
public function rules()
{
return [
[['animal_id', 'field_definition_id'], 'required'],
[['animal_id', 'field_definition_id'], 'integer'],
[['value_text', 'value_json'], 'string'],
[['animal_id', 'field_definition_id'], 'unique', 'targetAttribute' => ['animal_id', 'field_definition_id']],
];
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}
public function getAnimal()
{
return $this->hasOne(Animal::class, ['id' => 'animal_id']);
}
public function getFieldDefinition()
{
return $this->hasOne(RescueFieldDefinition::class, ['id' => 'field_definition_id']);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
use humhub\modules\file\models\File;
class AnimalGalleryItem extends ActiveRecord
{
public static function tableName()
{
return 'rescue_animal_gallery_item';
}
public function rules()
{
return [
[['animal_id'], 'required'],
[['animal_id', 'file_id', 'source_post_id', 'created_by'], 'integer'],
[['caption'], 'string'],
[['source_type'], 'string', 'max' => 32],
[['file_path'], 'string', 'max' => 500],
];
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
if (empty($this->source_type)) {
$this->source_type = 'upload';
}
$this->updated_at = $now;
return true;
}
public function getAnimal()
{
return $this->hasOne(Animal::class, ['id' => 'animal_id']);
}
public function getFile()
{
return $this->hasOne(File::class, ['id' => 'file_id']);
}
public function getImageUrl(): string
{
$path = trim((string)$this->file_path);
if ($path !== '') {
if (preg_match('/^https?:\/\//i', $path) || substr($path, 0, 1) === '/') {
return $path;
}
return '/' . ltrim($path, '/');
}
if ($this->file_id) {
$file = $this->file;
if ($file instanceof File) {
return (string)$file->getUrl([], false);
}
}
return '';
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
class AnimalGalleryLink extends ActiveRecord
{
public static function tableName()
{
return 'rescue_animal_gallery_link';
}
public function rules()
{
return [
[['animal_id', 'gallery_id', 'contentcontainer_id'], 'required'],
[['animal_id', 'gallery_id', 'contentcontainer_id'], 'integer'],
[['animal_id'], 'unique'],
[['gallery_id'], 'unique'],
];
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}
public function getAnimal()
{
return $this->hasOne(Animal::class, ['id' => 'animal_id']);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
use Yii;
class AnimalMedicalVisit extends ActiveRecord
{
public static function tableName()
{
return 'rescue_animal_medical_visit';
}
public function rules()
{
$rules = [
[['animal_id'], 'required'],
[['animal_id', 'created_by'], 'integer'],
[['visit_at'], 'safe'],
[['notes', 'recommendations'], 'string'],
[['provider_name'], 'string', 'max' => 190],
];
$table = Yii::$app->db->schema->getTableSchema(static::tableName(), true);
if ($table !== null && isset($table->columns['post_to_space_feed']) && isset($table->columns['post_to_animal_feed'])) {
$rules[] = [['post_to_space_feed', 'post_to_animal_feed'], 'integer'];
}
return $rules;
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}
public function getFieldValues()
{
return $this->hasMany(AnimalMedicalVisitFieldValue::class, ['medical_visit_id' => 'id']);
}
public function getCustomFieldDisplayValues(bool $includeRestricted = false): array
{
$values = [];
foreach ($this->getFieldValues()->with('fieldDefinition')->all() as $fieldValue) {
$definition = $fieldValue->fieldDefinition;
if ($definition === null || (string)$definition->module_id !== 'animal_management') {
continue;
}
if ((int)$definition->is_active !== 1 || (string)$definition->group_key !== 'animal_medical_visit') {
continue;
}
$visibility = (string)$definition->visibility;
if (!$includeRestricted && $visibility !== 'public') {
continue;
}
$raw = trim((string)$fieldValue->value_text);
if ($raw === '') {
continue;
}
$display = $raw;
$inputType = (string)$definition->input_type;
if ($inputType === 'boolean') {
$display = in_array(strtolower($raw), ['1', 'true', 'yes', 'on'], true)
? Yii::t('AnimalManagementModule.base', 'Yes')
: Yii::t('AnimalManagementModule.base', 'No');
}
$values[] = [
'field_key' => (string)$definition->field_key,
'label' => trim((string)$definition->label) !== '' ? (string)$definition->label : (string)$definition->field_key,
'value' => $display,
];
}
return $values;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
use humhub\modules\rescue_foundation\models\RescueFieldDefinition;
class AnimalMedicalVisitFieldValue extends ActiveRecord
{
public static function tableName()
{
return 'rescue_animal_medical_visit_field_value';
}
public function rules()
{
return [
[['medical_visit_id', 'field_definition_id'], 'required'],
[['medical_visit_id', 'field_definition_id'], 'integer'],
[['value_text', 'value_json'], 'string'],
[['medical_visit_id', 'field_definition_id'], 'unique', 'targetAttribute' => ['medical_visit_id', 'field_definition_id']],
];
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}
public function getMedicalVisit()
{
return $this->hasOne(AnimalMedicalVisit::class, ['id' => 'medical_visit_id']);
}
public function getFieldDefinition()
{
return $this->hasOne(RescueFieldDefinition::class, ['id' => 'field_definition_id']);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
use Yii;
class AnimalProgressUpdate extends ActiveRecord
{
public static function tableName()
{
return 'rescue_animal_progress_update';
}
public function rules()
{
return [
[['animal_id'], 'required'],
[['animal_id', 'created_by', 'post_to_space_feed', 'post_to_animal_feed'], 'integer'],
[['update_at'], 'safe'],
[['vitals', 'behavior_notes', 'meal_plan_changes', 'housing_changes', 'medical_concerns'], 'string'],
[['weight'], 'string', 'max' => 32],
];
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}
public function getFieldValues()
{
return $this->hasMany(AnimalProgressUpdateFieldValue::class, ['progress_update_id' => 'id']);
}
public function getCustomFieldDisplayValues(bool $includeRestricted = false): array
{
$values = [];
foreach ($this->getFieldValues()->with('fieldDefinition')->all() as $fieldValue) {
$definition = $fieldValue->fieldDefinition;
if ($definition === null || (string)$definition->module_id !== 'animal_management') {
continue;
}
if ((int)$definition->is_active !== 1 || (string)$definition->group_key !== 'animal_progress_update') {
continue;
}
$visibility = (string)$definition->visibility;
if (!$includeRestricted && $visibility !== 'public') {
continue;
}
$raw = trim((string)$fieldValue->value_text);
if ($raw === '') {
continue;
}
$display = $raw;
$inputType = (string)$definition->input_type;
if ($inputType === 'boolean') {
$display = in_array(strtolower($raw), ['1', 'true', 'yes', 'on'], true)
? Yii::t('AnimalManagementModule.base', 'Yes')
: Yii::t('AnimalManagementModule.base', 'No');
}
$values[] = [
'field_key' => (string)$definition->field_key,
'label' => trim((string)$definition->label) !== '' ? (string)$definition->label : (string)$definition->field_key,
'value' => $display,
];
}
return $values;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
use humhub\modules\rescue_foundation\models\RescueFieldDefinition;
class AnimalProgressUpdateFieldValue extends ActiveRecord
{
public static function tableName()
{
return 'rescue_animal_progress_update_field_value';
}
public function rules()
{
return [
[['progress_update_id', 'field_definition_id'], 'required'],
[['progress_update_id', 'field_definition_id'], 'integer'],
[['value_text', 'value_json'], 'string'],
[['progress_update_id', 'field_definition_id'], 'unique', 'targetAttribute' => ['progress_update_id', 'field_definition_id']],
];
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}
public function getProgressUpdate()
{
return $this->hasOne(AnimalProgressUpdate::class, ['id' => 'progress_update_id']);
}
public function getFieldDefinition()
{
return $this->hasOne(RescueFieldDefinition::class, ['id' => 'field_definition_id']);
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\modules\animal_management\widgets\stream\AnimalStreamEntryWallEntry;
use humhub\modules\content\components\ContentActiveRecord;
use Yii;
class AnimalStreamEntry extends ContentActiveRecord
{
public const TYPE_MEDICAL = 'medical';
public const TYPE_PROGRESS = 'progress';
public $wallEntryClass = AnimalStreamEntryWallEntry::class;
public $moduleId = 'animal_management';
public static function tableName()
{
return 'rescue_animal_stream_entry';
}
public function rules()
{
return [
[['animal_id', 'entry_type'], 'required'],
[['animal_id', 'medical_visit_id', 'progress_update_id'], 'integer'],
[['entry_type'], 'string', 'max' => 32],
[['entry_type'], 'in', 'range' => [self::TYPE_MEDICAL, self::TYPE_PROGRESS]],
[['medical_visit_id', 'progress_update_id'], 'validateLinkedRecord'],
];
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}
public function validateLinkedRecord(string $attribute, $params): void
{
if ((string)$this->entry_type === self::TYPE_MEDICAL) {
if ((int)$this->medical_visit_id <= 0) {
$this->addError('medical_visit_id', Yii::t('AnimalManagementModule.base', 'Medical visit reference is required.'));
}
return;
}
if ((string)$this->entry_type === self::TYPE_PROGRESS) {
if ((int)$this->progress_update_id <= 0) {
$this->addError('progress_update_id', Yii::t('AnimalManagementModule.base', 'Progress update reference is required.'));
}
return;
}
}
public function getAnimal()
{
return $this->hasOne(Animal::class, ['id' => 'animal_id']);
}
public function getMedicalVisit()
{
return $this->hasOne(AnimalMedicalVisit::class, ['id' => 'medical_visit_id']);
}
public function getProgressUpdate()
{
return $this->hasOne(AnimalProgressUpdate::class, ['id' => 'progress_update_id']);
}
public function getIcon()
{
return 'fa-paw';
}
public function getContentName()
{
return Yii::t('AnimalManagementModule.base', 'Animal stream update');
}
public function getContentDescription()
{
$animal = $this->animal;
$animalName = $animal instanceof Animal ? $animal->getDisplayName() : Yii::t('AnimalManagementModule.base', 'Animal');
if ((string)$this->entry_type === self::TYPE_MEDICAL) {
return Yii::t('AnimalManagementModule.base', 'Medical visit for {animal}', ['{animal}' => $animalName]);
}
return Yii::t('AnimalManagementModule.base', 'Progress update for {animal}', ['{animal}' => $animalName]);
}
}

158
models/AnimalTransfer.php Normal file
View File

@@ -0,0 +1,158 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
use humhub\modules\space\models\Space;
use Yii;
class AnimalTransfer extends ActiveRecord
{
public const STATUS_REQUESTED = 'requested';
public const STATUS_ACCEPTED = 'accepted';
public const STATUS_DECLINED = 'declined';
public const STATUS_COMPLETED = 'completed';
public const STATUS_CANCELLED = 'cancelled';
public static function tableName()
{
return 'rescue_animal_transfer';
}
public function rules()
{
return [
[['animal_id', 'from_contentcontainer_id', 'to_contentcontainer_id', 'status'], 'required'],
[['animal_id', 'from_contentcontainer_id', 'to_contentcontainer_id', 'requested_by'], 'integer'],
[['request_message', 'conditions_text'], 'string'],
[['status'], 'string', 'max' => 32],
[['status'], 'in', 'range' => array_keys(self::statusOptions())],
];
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = date('Y-m-d H:i:s');
if ($insert && empty($this->created_at)) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}
public static function statusOptions(): array
{
return [
self::STATUS_REQUESTED => Yii::t('AnimalManagementModule.base', 'Requested'),
self::STATUS_ACCEPTED => Yii::t('AnimalManagementModule.base', 'Accepted'),
self::STATUS_DECLINED => Yii::t('AnimalManagementModule.base', 'Declined'),
self::STATUS_COMPLETED => Yii::t('AnimalManagementModule.base', 'Completed'),
self::STATUS_CANCELLED => Yii::t('AnimalManagementModule.base', 'Cancelled'),
];
}
public function getAnimal()
{
return $this->hasOne(Animal::class, ['id' => 'animal_id']);
}
public function getFromSpace(): ?Space
{
return Space::findOne(['contentcontainer_id' => $this->from_contentcontainer_id]);
}
public function getToSpace(): ?Space
{
return Space::findOne(['contentcontainer_id' => $this->to_contentcontainer_id]);
}
public function getAuditEvents()
{
return $this->hasMany(AnimalTransferEvent::class, ['transfer_id' => 'id']);
}
public function markAccepted(): bool
{
$fromStatus = $this->status;
$this->status = self::STATUS_ACCEPTED;
$this->responded_at = date('Y-m-d H:i:s');
if (!$this->save(false, ['status', 'responded_at', 'updated_at'])) {
return false;
}
AnimalTransferEvent::log(
$this,
AnimalTransferEvent::EVENT_ACCEPTED,
$fromStatus,
$this->status,
Yii::t('AnimalManagementModule.base', 'Transfer request accepted.')
);
return true;
}
public function markDeclined(): bool
{
$fromStatus = $this->status;
$this->status = self::STATUS_DECLINED;
$this->responded_at = date('Y-m-d H:i:s');
if (!$this->save(false, ['status', 'responded_at', 'updated_at'])) {
return false;
}
AnimalTransferEvent::log(
$this,
AnimalTransferEvent::EVENT_DECLINED,
$fromStatus,
$this->status,
Yii::t('AnimalManagementModule.base', 'Transfer request declined.')
);
return true;
}
public function markCompleted(): bool
{
$fromStatus = $this->status;
$this->status = self::STATUS_COMPLETED;
$this->completed_at = date('Y-m-d H:i:s');
if (!$this->save(false, ['status', 'completed_at', 'updated_at'])) {
return false;
}
AnimalTransferEvent::log(
$this,
AnimalTransferEvent::EVENT_COMPLETED,
$fromStatus,
$this->status,
Yii::t('AnimalManagementModule.base', 'Transfer completed.')
);
return true;
}
public function markCancelled(string $message = ''): bool
{
$fromStatus = $this->status;
$this->status = self::STATUS_CANCELLED;
if (!$this->save(false, ['status', 'updated_at'])) {
return false;
}
AnimalTransferEvent::log(
$this,
AnimalTransferEvent::EVENT_CANCELLED,
$fromStatus,
$this->status,
$message !== '' ? $message : Yii::t('AnimalManagementModule.base', 'Transfer cancelled.')
);
return true;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace humhub\modules\animal_management\models;
use humhub\components\ActiveRecord;
use humhub\modules\user\models\User;
use Yii;
use yii\helpers\Json;
class AnimalTransferEvent extends ActiveRecord
{
public const EVENT_REQUESTED = 'requested';
public const EVENT_ACCEPTED = 'accepted';
public const EVENT_DECLINED = 'declined';
public const EVENT_COMPLETED = 'completed';
public const EVENT_CANCELLED = 'cancelled';
public static function tableName()
{
return 'rescue_animal_transfer_event';
}
public function rules()
{
return [
[['transfer_id', 'animal_id', 'event_type'], 'required'],
[['transfer_id', 'animal_id', 'created_by'], 'integer'],
[['message', 'metadata_json'], 'string'],
[['event_type', 'from_status', 'to_status'], 'string', 'max' => 32],
[['event_type'], 'in', 'range' => [
self::EVENT_REQUESTED,
self::EVENT_ACCEPTED,
self::EVENT_DECLINED,
self::EVENT_COMPLETED,
self::EVENT_CANCELLED,
]],
];
}
public function beforeSave($insert)
{
if (!$insert) {
return false;
}
if (!parent::beforeSave($insert)) {
return false;
}
if (empty($this->created_at)) {
$this->created_at = date('Y-m-d H:i:s');
}
return true;
}
public static function log(
AnimalTransfer $transfer,
string $eventType,
?string $fromStatus,
?string $toStatus,
string $message = '',
array $metadata = []
): bool {
$event = new self();
$event->transfer_id = (int)$transfer->id;
$event->animal_id = (int)$transfer->animal_id;
$event->event_type = $eventType;
$event->from_status = $fromStatus;
$event->to_status = $toStatus;
$event->message = $message;
$event->metadata_json = empty($metadata) ? null : Json::encode($metadata);
$event->created_by = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id;
return $event->save(false);
}
public function getCreatedByUser()
{
return $this->hasOne(User::class, ['id' => 'created_by']);
}
}

1024
models/forms/AnimalForm.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,346 @@
<?php
namespace humhub\modules\animal_management\models\forms;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\animal_management\models\AnimalMedicalVisitFieldValue;
use humhub\modules\animal_management\services\AnimalStreamPublisherService;
use humhub\modules\rescue_foundation\models\RescueFieldDefinition;
use Yii;
use yii\base\Model;
use yii\helpers\Json;
class AnimalMedicalVisitForm extends Model
{
public const MODULE_ID = 'animal_management';
public Animal $animal;
public string $visit_at = '';
public string $provider_name = '';
public string $notes = '';
public string $recommendations = '';
public bool $post_to_space_feed = false;
public bool $post_to_animal_feed = true;
public array $customFields = [];
public ?AnimalMedicalVisit $medicalVisit = null;
private array $customFieldDefinitions = [];
public function init()
{
parent::init();
$this->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;
}
}
}

View File

@@ -0,0 +1,348 @@
<?php
namespace humhub\modules\animal_management\models\forms;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalProgressUpdate;
use humhub\modules\animal_management\models\AnimalProgressUpdateFieldValue;
use humhub\modules\animal_management\services\AnimalStreamPublisherService;
use humhub\modules\rescue_foundation\models\RescueFieldDefinition;
use Yii;
use yii\base\Model;
use yii\helpers\Json;
class AnimalProgressUpdateForm extends Model
{
public const MODULE_ID = 'animal_management';
public Animal $animal;
public string $update_at = '';
public string $weight = '';
public string $vitals = '';
public string $behavior_notes = '';
public string $meal_plan_changes = '';
public string $housing_changes = '';
public string $medical_concerns = '';
public bool $post_to_space_feed = false;
public bool $post_to_animal_feed = true;
public array $customFields = [];
public ?AnimalProgressUpdate $progressUpdate = null;
private array $customFieldDefinitions = [];
public function init()
{
parent::init();
$this->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;
}
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace humhub\modules\animal_management\models\forms;
use humhub\modules\content\components\ContentContainerActiveRecord;
use Yii;
use yii\base\Model;
use yii\helpers\Json;
class DisplaySettingsForm extends Model
{
public const DEFAULT_SEARCH_BLOCK_HEADING = 'Search Animal Profiles';
public const DEFAULT_TILE_FIELDS = [
'name',
'species',
'status',
];
public const DEFAULT_DETAIL_FIELDS = [
'species',
'breed',
'sex',
'status',
'location_name',
];
public ?ContentContainerActiveRecord $contentContainer = null;
public string $search_block_heading = self::DEFAULT_SEARCH_BLOCK_HEADING;
public array $tile_fields = self::DEFAULT_TILE_FIELDS;
public array $detail_fields = self::DEFAULT_DETAIL_FIELDS;
public function rules()
{
return [
[['search_block_heading'], 'string', 'max' => 190],
[['tile_fields', 'detail_fields'], 'each', 'rule' => ['in', 'range' => array_keys(self::fieldOptions())]],
[['tile_fields', 'detail_fields'], 'default', 'value' => []],
];
}
public function attributeLabels()
{
return [
'search_block_heading' => Yii::t('AnimalManagementModule.base', 'Search Block Heading'),
'tile_fields' => Yii::t('AnimalManagementModule.base', 'Animal Tile Fields'),
'detail_fields' => Yii::t('AnimalManagementModule.base', 'Animal Detail Hero Fields'),
];
}
public function loadValues(): void
{
if ($this->contentContainer === null) {
return;
}
$settings = Yii::$app->getModule('animal_management')->settings->contentContainer($this->contentContainer);
$heading = trim((string)$settings->get('searchBlockHeading', self::DEFAULT_SEARCH_BLOCK_HEADING));
$this->search_block_heading = $heading !== '' ? $heading : self::DEFAULT_SEARCH_BLOCK_HEADING;
$this->tile_fields = $this->normalizeFields(
$settings->get('tileFields', Json::encode(self::DEFAULT_TILE_FIELDS)),
self::DEFAULT_TILE_FIELDS
);
$this->detail_fields = $this->normalizeFields(
$settings->get('detailHeroFields', Json::encode(self::DEFAULT_DETAIL_FIELDS)),
self::DEFAULT_DETAIL_FIELDS
);
}
public function save(): bool
{
if ($this->contentContainer === null) {
$this->addError('search_block_heading', Yii::t('AnimalManagementModule.base', 'Missing content container.'));
return false;
}
if (!$this->validate()) {
return false;
}
$settings = Yii::$app->getModule('animal_management')->settings->contentContainer($this->contentContainer);
$heading = trim($this->search_block_heading);
$settings->set('searchBlockHeading', $heading !== '' ? $heading : self::DEFAULT_SEARCH_BLOCK_HEADING);
$settings->set('tileFields', Json::encode($this->normalizeFieldArray($this->tile_fields, self::DEFAULT_TILE_FIELDS)));
$settings->set('detailHeroFields', Json::encode($this->normalizeFieldArray($this->detail_fields, self::DEFAULT_DETAIL_FIELDS)));
return true;
}
public static function fieldOptions(): array
{
return [
'name' => Yii::t('AnimalManagementModule.base', 'Name'),
'species' => Yii::t('AnimalManagementModule.base', 'Species'),
'breed' => Yii::t('AnimalManagementModule.base', 'Breed'),
'sex' => Yii::t('AnimalManagementModule.base', 'Sex'),
'status' => Yii::t('AnimalManagementModule.base', 'Status'),
'location_name' => Yii::t('AnimalManagementModule.base', 'Location'),
'animal_uid' => Yii::t('AnimalManagementModule.base', 'ID'),
'last_medical' => Yii::t('AnimalManagementModule.base', 'Last Medical Visit'),
'public_summary' => Yii::t('AnimalManagementModule.base', 'Public Summary'),
];
}
private function normalizeFields($raw, array $default): array
{
$decoded = [];
if (is_string($raw)) {
$raw = trim($raw);
if ($raw !== '') {
try {
$decoded = Json::decode($raw);
} catch (\Throwable $e) {
$decoded = [];
}
}
} elseif (is_array($raw)) {
$decoded = $raw;
}
if (!is_array($decoded)) {
return $default;
}
return $this->normalizeFieldArray($decoded, $default);
}
private function normalizeFieldArray(array $fields, array $default): array
{
$normalized = [];
$options = self::fieldOptions();
foreach ($fields as $field) {
$field = trim((string)$field);
if ($field === '' || !isset($options[$field])) {
continue;
}
if (!in_array($field, $normalized, true)) {
$normalized[] = $field;
}
}
if (empty($normalized)) {
return $default;
}
return $normalized;
}
}

View File

@@ -0,0 +1,335 @@
<?php
namespace humhub\modules\animal_management\models\forms;
use humhub\modules\rescue_foundation\models\RescueFieldDefinition;
use Yii;
use yii\base\Model;
class FieldDefinitionSettingsForm extends Model
{
public const MODULE_ID = 'animal_management';
public array $rows = [];
public string $new_field_key = '';
public string $new_label = '';
public string $new_input_type = 'text';
public string $new_group_key = 'animal_profile';
public string $new_visibility = 'public';
public int $new_required = 0;
public int $new_sort_order = 500;
public string $new_options = '';
public function rules()
{
return [
[['rows'], 'safe'],
[['new_field_key', 'new_label', 'new_group_key', 'new_visibility', 'new_input_type', 'new_options'], 'string'],
[['new_required', 'new_sort_order'], 'integer'],
];
}
public static function inputTypeOptions(): array
{
return [
'text' => 'Text',
'textarea' => 'Textarea',
'boolean' => 'Boolean',
'select' => 'Select',
'number' => 'Number',
'date' => 'Date',
'datetime' => 'Date/Time',
];
}
public static function visibilityOptions(): array
{
return [
'public' => 'Public',
'private' => 'Private',
'internal' => 'Internal',
];
}
public function loadRows(): void
{
if (!$this->canUseFieldDefinition()) {
return;
}
$this->rows = [];
$definitions = RescueFieldDefinition::find()
->where(['module_id' => self::MODULE_ID])
->orderBy(['group_key' => SORT_ASC, 'sort_order' => SORT_ASC, 'id' => SORT_ASC])
->all();
foreach ($definitions as $definition) {
$this->rows[] = [
'id' => (int)$definition->id,
'field_key' => (string)$definition->field_key,
'label' => (string)$definition->label,
'group_key' => (string)$definition->group_key,
'input_type' => (string)$definition->input_type,
'required' => (int)$definition->required,
'is_core' => (int)$definition->is_core,
'is_active' => (int)$definition->is_active,
'visibility' => (string)$definition->visibility,
'sort_order' => (int)$definition->sort_order,
];
}
}
public function save(): bool
{
if (!$this->canUseFieldDefinition()) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Field definition storage is unavailable.'));
return false;
}
$this->clearValidationErrors();
$byId = [];
foreach (RescueFieldDefinition::find()->where(['module_id' => self::MODULE_ID])->all() as $definition) {
$byId[(int)$definition->id] = $definition;
}
$transaction = Yii::$app->db->beginTransaction();
try {
$now = date('Y-m-d H:i:s');
foreach ($this->rows as $index => $row) {
$id = (int)($row['id'] ?? 0);
if ($id === 0 || !isset($byId[$id])) {
continue;
}
$definition = $byId[$id];
$isCore = ((int)$definition->is_core) === 1;
$remove = !empty($row['remove']);
if ($remove && !$isCore) {
if (!$definition->delete()) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Could not remove field {field}.', [
'{field}' => (string)$definition->field_key,
]));
}
continue;
}
$label = trim((string)($row['label'] ?? ''));
$groupKey = trim((string)($row['group_key'] ?? $definition->group_key));
$visibility = trim((string)($row['visibility'] ?? $definition->visibility));
$sortOrder = (int)($row['sort_order'] ?? $definition->sort_order);
$required = !empty($row['required']) ? 1 : 0;
$isActive = !empty($row['is_active']) ? 1 : 0;
$inputType = trim((string)($row['input_type'] ?? $definition->input_type));
$options = trim((string)$definition->options);
if ($label === '') {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Field label cannot be empty for {field}.', [
'{field}' => (string)$definition->field_key,
]));
continue;
}
if ($groupKey === '') {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Group key cannot be empty for {field}.', [
'{field}' => (string)$definition->field_key,
]));
continue;
}
if (!array_key_exists($visibility, self::visibilityOptions())) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Invalid visibility for {field}.', [
'{field}' => (string)$definition->field_key,
]));
continue;
}
if (!$isCore && !$this->validateSelectOptions($inputType, $options, (string)$definition->field_key)) {
continue;
}
$definition->label = $label;
$definition->group_key = $groupKey;
$definition->visibility = $visibility;
$definition->sort_order = $sortOrder;
$definition->required = $isCore ? (int)$definition->required : $required;
$definition->is_active = $isCore ? (int)$definition->is_active : $isActive;
$definition->updated_at = $now;
if (!$definition->save()) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Could not save field {field}.', [
'{field}' => (string)$definition->field_key,
]));
foreach ($definition->getFirstErrors() as $error) {
$this->addError('rows', $error);
}
}
$this->rows[$index]['is_core'] = (int)$definition->is_core;
}
if ($this->hasErrors()) {
$transaction->rollBack();
return false;
}
if (!$this->saveNewField($now)) {
$transaction->rollBack();
return false;
}
$transaction->commit();
return true;
} catch (\Throwable $e) {
$transaction->rollBack();
Yii::error($e, 'animal_management.field_definition_settings');
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Unexpected error while saving field settings.'));
return false;
}
}
public function canUseFieldDefinition(): bool
{
if (!class_exists(RescueFieldDefinition::class)) {
return false;
}
return Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) !== null;
}
private function saveNewField(string $now): bool
{
$newFieldKey = strtolower(trim($this->new_field_key));
$newLabel = trim($this->new_label);
if ($newFieldKey === '' && $newLabel === '') {
return true;
}
if ($newFieldKey === '' || $newLabel === '') {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Both new field key and label are required.'));
return false;
}
if (!preg_match('/^[a-z0-9_]+$/', $newFieldKey)) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'New field key must contain only lowercase letters, numbers, and underscores.'));
return false;
}
if (!array_key_exists($this->new_input_type, self::inputTypeOptions())) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Invalid input type for new field.'));
return false;
}
if (!array_key_exists($this->new_visibility, self::visibilityOptions())) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Invalid visibility for new field.'));
return false;
}
$groupKey = trim($this->new_group_key);
if ($groupKey === '') {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'New field group key is required.'));
return false;
}
$options = trim($this->new_options);
if (!$this->validateSelectOptions($this->new_input_type, $options, $newFieldKey)) {
return false;
}
$exists = RescueFieldDefinition::find()
->where(['module_id' => self::MODULE_ID, 'field_key' => $newFieldKey])
->exists();
if ($exists) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Field key {key} already exists.', ['{key}' => $newFieldKey]));
return false;
}
$field = new RescueFieldDefinition();
$field->module_id = self::MODULE_ID;
$field->group_key = $groupKey;
$field->field_key = $newFieldKey;
$field->label = $newLabel;
$field->input_type = $this->new_input_type;
$field->required = $this->new_required ? 1 : 0;
$field->is_core = 0;
$field->is_active = 1;
$field->visibility = $this->new_visibility;
$field->sort_order = (int)$this->new_sort_order;
$field->options = $options !== '' ? $options : '{}';
$field->created_at = $now;
$field->updated_at = $now;
if (!$field->save()) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Could not create new field.'));
foreach ($field->getFirstErrors() as $error) {
$this->addError('rows', $error);
}
return false;
}
$this->new_field_key = '';
$this->new_label = '';
$this->new_input_type = 'text';
$this->new_group_key = 'animal_profile';
$this->new_visibility = 'public';
$this->new_required = 0;
$this->new_sort_order = 500;
$this->new_options = '';
return true;
}
private function clearValidationErrors(): void
{
foreach (array_keys($this->errors) as $attribute) {
$this->clearErrors($attribute);
}
}
private function validateSelectOptions(string $inputType, string $options, string $fieldKey): bool
{
if ($inputType !== 'select') {
return true;
}
if ($options === '' || $options === '{}') {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Select field {field} requires options JSON or comma/newline list.', [
'{field}' => $fieldKey,
]));
return false;
}
$decoded = json_decode($options, true);
if (json_last_error() === JSON_ERROR_NONE) {
if (!is_array($decoded) || empty($decoded)) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Select field {field} options JSON must be a non-empty array/object.', [
'{field}' => $fieldKey,
]));
return false;
}
return true;
}
$parts = preg_split('/[\r\n,]+/', $options);
$parts = array_filter(array_map(static function ($item) {
return trim((string)$item);
}, $parts));
if (empty($parts)) {
$this->addError('rows', Yii::t('AnimalManagementModule.base', 'Select field {field} requires at least one option.', [
'{field}' => $fieldKey,
]));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace humhub\modules\animal_management\models\forms;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalTransfer;
use humhub\modules\animal_management\models\AnimalTransferEvent;
use humhub\modules\animal_management\notifications\TransferNotifier;
use humhub\modules\animal_management\notifications\TransferRequestedNotification;
use humhub\modules\space\models\Space;
use Yii;
use yii\base\Model;
class TransferRequestForm extends Model
{
public Animal $animal;
public Space $sourceSpace;
public int $to_space_id = 0;
public string $request_message = '';
public string $conditions_text = '';
private ?AnimalTransfer $transfer = null;
public function rules()
{
return [
[['to_space_id'], 'required'],
[['to_space_id'], 'integer'],
[['request_message', 'conditions_text'], 'string'],
[['to_space_id'], 'validateTargetSpace'],
];
}
public function validateTargetSpace($attribute): void
{
if ($this->hasErrors()) {
return;
}
if ($this->to_space_id === (int)$this->sourceSpace->id) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', 'You must choose a different destination rescue.'));
return;
}
$target = Space::findOne(['id' => $this->to_space_id, 'status' => Space::STATUS_ENABLED]);
if (!$target instanceof Space) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', 'Selected destination rescue was not found.'));
}
}
public function getTargetOptions(): array
{
return Space::find()
->select(['name', 'id'])
->where(['status' => Space::STATUS_ENABLED])
->andWhere(['!=', 'id', (int)$this->sourceSpace->id])
->orderBy(['name' => SORT_ASC])
->indexBy('id')
->column();
}
public function save(): bool
{
if (!$this->validate()) {
return false;
}
$targetSpace = Space::findOne(['id' => $this->to_space_id, 'status' => Space::STATUS_ENABLED]);
if (!$targetSpace instanceof Space) {
$this->addError('to_space_id', Yii::t('AnimalManagementModule.base', 'Selected destination rescue was not found.'));
return false;
}
$transaction = Yii::$app->db->beginTransaction();
try {
$transfer = new AnimalTransfer();
$transfer->animal_id = (int)$this->animal->id;
$transfer->from_contentcontainer_id = (int)$this->sourceSpace->contentcontainer_id;
$transfer->to_contentcontainer_id = (int)$targetSpace->contentcontainer_id;
$transfer->requested_by = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id;
$transfer->status = AnimalTransfer::STATUS_REQUESTED;
$transfer->request_message = trim($this->request_message);
$transfer->conditions_text = trim($this->conditions_text);
if (!$transfer->save()) {
$this->addErrors($transfer->getErrors());
$transaction->rollBack();
return false;
}
$this->animal->status = Animal::STATUS_TRANSFER_PENDING;
if (!$this->animal->save(false, ['status', 'updated_at'])) {
$transaction->rollBack();
$this->addError('to_space_id', Yii::t('AnimalManagementModule.base', 'Could not update animal transfer status.'));
return false;
}
$auditSaved = AnimalTransferEvent::log(
$transfer,
AnimalTransferEvent::EVENT_REQUESTED,
null,
AnimalTransfer::STATUS_REQUESTED,
Yii::t('AnimalManagementModule.base', 'Transfer requested from {from} to {to}.', [
'{from}' => (string)$this->sourceSpace->name,
'{to}' => (string)$targetSpace->name,
]),
[
'from_space_id' => (int)$this->sourceSpace->id,
'to_space_id' => (int)$targetSpace->id,
]
);
if (!$auditSaved) {
$transaction->rollBack();
$this->addError('to_space_id', Yii::t('AnimalManagementModule.base', 'Could not write transfer audit log.'));
return false;
}
$transaction->commit();
} catch (\Throwable $e) {
$transaction->rollBack();
Yii::error($e, 'animal_management.transfer_request');
$this->addError('to_space_id', Yii::t('AnimalManagementModule.base', 'Could not create transfer request.'));
return false;
}
$this->transfer = $transfer;
$this->notifyTargetManagers();
return true;
}
private function notifyTargetManagers(): void
{
$targetSpace = Space::findOne(['id' => $this->to_space_id]);
if (!$targetSpace instanceof Space) {
return;
}
$recipients = TransferNotifier::privilegedUsersForSpace($targetSpace);
$originator = Yii::$app->user->isGuest ? null : Yii::$app->user->getIdentity();
foreach ($recipients as $recipient) {
if ($originator instanceof \humhub\modules\user\models\User && (int)$recipient->id === (int)$originator->id) {
continue;
}
$notification = TransferRequestedNotification::instance();
if ($originator) {
$notification->from($originator);
}
$notification->animalName = $this->animal->getDisplayName();
$notification->fromSpaceName = (string)$this->sourceSpace->name;
$notification->toSpaceGuid = (string)$targetSpace->guid;
$notification->payload([
'animalName' => $notification->animalName,
'fromSpaceName' => $notification->fromSpaceName,
'toSpaceGuid' => $notification->toSpaceGuid,
]);
$notification->send($recipient);
}
}
}