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

182
Events.php Normal file
View File

@@ -0,0 +1,182 @@
<?php
namespace humhub\modules\animal_management;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\services\GalleryIntegrationService;
use humhub\modules\file\models\File;
use humhub\modules\post\models\Post;
use humhub\modules\space\models\Space;
use Yii;
class Events
{
public static function onSpaceMenuInit($event): void
{
$space = $event->sender->space ?? null;
if ($space === null || !$space->moduleManager->isEnabled('animal_management')) {
return;
}
$event->sender->addItem([
'label' => Yii::t('AnimalManagementModule.base', 'Animals'),
'group' => 'modules',
'url' => $space->createUrl('/animal_management/animals/index'),
'icon' => '<i class="fa fa-paw"></i>',
'sortOrder' => 110,
'isActive' => (
Yii::$app->controller
&& Yii::$app->controller->module
&& Yii::$app->controller->module->id === 'animal_management'
&& Yii::$app->controller->id === 'animals'
),
]);
}
public static function onRescueSettingsMenuInit($event): void
{
$space = $event->sender->space ?? null;
if ($space === null || !$space->moduleManager->isEnabled('animal_management')) {
return;
}
$event->sender->addItem([
'label' => Yii::t('AnimalManagementModule.base', 'Animal Management'),
'url' => $space->createUrl('/animal_management/settings'),
'sortOrder' => 300,
'isActive' => (
Yii::$app->controller
&& Yii::$app->controller->module
&& Yii::$app->controller->module->id === 'animal_management'
&& Yii::$app->controller->id === 'settings'
),
]);
}
public static function onSpaceAdminMenuInitFallback($event): void
{
$space = $event->sender->space ?? null;
if ($space === null || !$space->moduleManager->isEnabled('animal_management')) {
return;
}
if ($space->moduleManager->isEnabled('rescue_foundation') || !$space->isAdmin()) {
return;
}
$event->sender->addItem([
'label' => Yii::t('AnimalManagementModule.base', 'Animal Management'),
'group' => 'admin',
'url' => $space->createUrl('/animal_management/settings'),
'icon' => '<i class="fa fa-paw"></i>',
'sortOrder' => 600,
'isActive' => (
Yii::$app->controller
&& Yii::$app->controller->module
&& Yii::$app->controller->module->id === 'animal_management'
&& Yii::$app->controller->id === 'settings'
),
]);
}
public static function onPostAfterSave($event): void
{
$post = $event->sender ?? null;
if (!$post instanceof Post) {
return;
}
if (Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_item', true) === null) {
return;
}
$message = trim((string)$post->message);
if ($message === '' || stripos($message, '#gallery') === false) {
return;
}
$content = $post->content;
$container = $content->container ?? null;
if (!$container instanceof Space) {
return;
}
if (!$container->moduleManager->isEnabled('animal_management')) {
return;
}
$attachedFiles = File::findByRecord($post);
if (empty($attachedFiles)) {
return;
}
$imageFiles = [];
foreach ($attachedFiles as $file) {
if (!$file instanceof File) {
continue;
}
if (strpos((string)$file->mime_type, 'image/') !== 0) {
continue;
}
$imageFiles[] = $file;
}
if (empty($imageFiles)) {
return;
}
$animals = Animal::find()
->where(['contentcontainer_id' => $container->contentcontainer_id])
->all();
if (empty($animals)) {
return;
}
$matchedAnimals = [];
$searchableMessage = strtolower(strip_tags($message));
foreach ($animals as $animal) {
$uid = strtolower(trim((string)$animal->animal_uid));
if ($uid === '') {
continue;
}
if (strpos($searchableMessage, $uid) !== false) {
$matchedAnimals[] = $animal;
}
}
if (empty($matchedAnimals)) {
return;
}
$createdBy = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id;
foreach ($matchedAnimals as $animal) {
foreach ($imageFiles as $file) {
$exists = AnimalGalleryItem::findOne([
'animal_id' => (int)$animal->id,
'file_id' => (int)$file->id,
]);
if ($exists instanceof AnimalGalleryItem) {
continue;
}
$item = new AnimalGalleryItem();
$item->animal_id = (int)$animal->id;
$item->file_id = (int)$file->id;
$item->source_post_id = (int)$post->id;
$item->source_type = 'post';
$item->created_by = $createdBy;
$item->save();
}
if ($container instanceof Space) {
GalleryIntegrationService::ensureAnimalGallery($animal, $container);
}
}
}
}

37
Module.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace humhub\modules\animal_management;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\components\ContentContainerModule;
use humhub\modules\space\models\Space;
class Module extends ContentContainerModule
{
public $resourcesPath = 'resources';
public function getContentContainerTypes()
{
return [Space::class];
}
public function getPermissions($contentContainer = null)
{
if ($contentContainer instanceof Space) {
return [
new permissions\ManageAnimals(),
];
}
return [];
}
public function getContentContainerConfigUrl(ContentContainerActiveRecord $container)
{
if ($container->moduleManager->isEnabled('rescue_foundation')) {
return $container->createUrl('/rescue_foundation/settings');
}
return $container->createUrl('/animal_management/settings');
}
}

27
README.md Normal file
View File

@@ -0,0 +1,27 @@
# Animal Management Module
Animal intake, profile management, medical/progress tracking, gallery integration, and transfer workflows for rescue spaces in HumHub.
## Requirements
- HumHub 1.14+
- `rescue_foundation` module available in the same HumHub instance
## Installation
Clone into the HumHub modules path using the folder name `animal_management`:
```bash
git clone https://gitea.kelinreij.duckdns.org/humhub-modules/animal-management.git /var/www/localhost/htdocs/protected/modules/animal_management
```
Enable the module in HumHub (Marketplace / Modules UI), then run migrations:
```bash
php /var/www/localhost/htdocs/protected/yii migrate/up --include-module-migrations=1 --interactive=0
```
## Notes
- Transfer notifications rely on HumHub queue processing.
- If you run in Docker, execute commands inside the HumHub app container.

51
components/UrlRule.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
namespace humhub\modules\animal_management\components;
use humhub\components\ContentContainerUrlRuleInterface;
use humhub\modules\content\components\ContentContainerActiveRecord;
use yii\base\Component;
use yii\web\UrlManager;
use yii\web\UrlRuleInterface;
class UrlRule extends Component implements UrlRuleInterface, ContentContainerUrlRuleInterface
{
public const CONTAINER_ROUTE = 'animal_management/animals/index';
private const ROUTE_SEGMENT = 'animals';
public function parseContentContainerRequest(ContentContainerActiveRecord $container, UrlManager $manager, string $containerUrlPath, array $urlParams)
{
if ($containerUrlPath === self::ROUTE_SEGMENT) {
return [self::CONTAINER_ROUTE, $urlParams];
}
return false;
}
public function createContentContainerUrl(UrlManager $manager, string $containerUrlPath, string $route, array $params)
{
if (ltrim($route, '/') !== self::CONTAINER_ROUTE) {
return false;
}
$url = $containerUrlPath . '/' . self::ROUTE_SEGMENT;
if (!empty($params)) {
$query = http_build_query($params);
if ($query !== '') {
$url .= '?' . $query;
}
}
return $url;
}
public function createUrl($manager, $route, $params)
{
return false;
}
public function parseRequest($manager, $request)
{
return false;
}
}

22
config.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
use humhub\modules\animal_management\Events;
use humhub\modules\post\models\Post;
use humhub\modules\space\widgets\HeaderControlsMenu;
use humhub\modules\space\widgets\Menu;
return [
'id' => 'animal_management',
'class' => 'humhub\\modules\\animal_management\\Module',
'namespace' => 'humhub\\modules\\animal_management',
'urlManagerRules' => [
['class' => 'humhub\\modules\\animal_management\\components\\UrlRule'],
],
'events' => [
[Menu::class, Menu::EVENT_INIT, [Events::class, 'onSpaceMenuInit']],
['humhub\\modules\\rescue_foundation\\widgets\\RescueSettingsMenu', 'rescueSettingsMenuInit', [Events::class, 'onRescueSettingsMenuInit']],
[HeaderControlsMenu::class, HeaderControlsMenu::EVENT_INIT, [Events::class, 'onSpaceAdminMenuInitFallback']],
[Post::class, Post::EVENT_AFTER_INSERT, [Events::class, 'onPostAfterSave']],
[Post::class, Post::EVENT_AFTER_UPDATE, [Events::class, 'onPostAfterSave']],
],
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
<?php
namespace humhub\modules\animal_management\controllers;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\forms\DisplaySettingsForm;
use humhub\modules\animal_management\models\forms\FieldDefinitionSettingsForm;
use humhub\modules\content\components\ContentContainerController;
use humhub\modules\content\components\ContentContainerControllerAccess;
use humhub\modules\rescue_foundation\widgets\RescueSettingsMenu;
use humhub\modules\space\models\Space;
use Yii;
class SettingsController extends ContentContainerController
{
protected function getAccessRules()
{
return [[ContentContainerControllerAccess::RULE_USER_GROUP_ONLY => [Space::USERGROUP_OWNER, Space::USERGROUP_ADMIN]]];
}
public function actionIndex()
{
$subNav = null;
if (class_exists(RescueSettingsMenu::class)) {
$subNav = RescueSettingsMenu::widget(['space' => $this->contentContainer]);
}
$fieldSettingsForm = new FieldDefinitionSettingsForm();
$fieldSettingsForm->loadRows();
$displaySettingsForm = new DisplaySettingsForm([
'contentContainer' => $this->contentContainer,
]);
$displaySettingsForm->loadValues();
if (Yii::$app->request->post('DisplaySettingsForm') !== null) {
if ($displaySettingsForm->load(Yii::$app->request->post()) && $displaySettingsForm->save()) {
$this->view->success(Yii::t('AnimalManagementModule.base', 'Display settings saved.'));
return $this->redirect($this->contentContainer->createUrl('/animal_management/settings'));
}
}
if (Yii::$app->request->post('FieldDefinitionSettingsForm') !== null) {
if ($fieldSettingsForm->load(Yii::$app->request->post()) && $fieldSettingsForm->save()) {
$this->view->success(Yii::t('AnimalManagementModule.base', 'Field settings saved.'));
return $this->redirect($this->contentContainer->createUrl('/animal_management/settings'));
}
}
$animalCount = Animal::find()->where(['contentcontainer_id' => $this->contentContainer->contentcontainer_id])->count();
return $this->render('index', [
'subNav' => $subNav,
'animalCount' => (int)$animalCount,
'fieldSettingsForm' => $fieldSettingsForm,
'displaySettingsForm' => $displaySettingsForm,
]);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace humhub\modules\animal_management\helpers;
use Yii;
class DateDisplayHelper
{
public const DATETIME_FORMAT = 'php:m/d/Y h:i A';
public static function format(?string $value): string
{
$value = trim((string)$value);
if ($value === '') {
return '';
}
$timestamp = strtotime($value);
if ($timestamp === false) {
return $value;
}
return Yii::$app->formatter->asDatetime($timestamp, self::DATETIME_FORMAT);
}
}

View File

@@ -0,0 +1,160 @@
<?php
use humhub\components\Migration;
class m260401_210000_initial extends Migration
{
public function safeUp()
{
$this->safeCreateTable('rescue_animal', [
'id' => $this->primaryKey(),
'contentcontainer_id' => $this->integer()->notNull(),
'animal_uid' => $this->string(190)->notNull(),
'name' => $this->string(190)->null(),
'species' => $this->string(120)->null(),
'breed' => $this->string(120)->null(),
'sex' => $this->string(32)->null(),
'status' => $this->string(32)->notNull()->defaultValue('intake'),
'in_possession' => $this->boolean()->notNull()->defaultValue(1),
'location_name' => $this->string(120)->null(),
'city' => $this->string(120)->null(),
'state' => $this->string(2)->null(),
'zip' => $this->string(10)->null(),
'previous_owner_user_id' => $this->integer()->null(),
'public_summary' => $this->text()->null(),
'medical_notes' => $this->text()->null(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex('ux_rescue_animal_uid', 'rescue_animal', 'animal_uid', true);
$this->safeCreateIndex('idx_rescue_animal_container', 'rescue_animal', 'contentcontainer_id', false);
$this->safeCreateIndex('idx_rescue_animal_status', 'rescue_animal', 'status', false);
$this->safeAddForeignKey('fk_rescue_animal_container', 'rescue_animal', 'contentcontainer_id', 'contentcontainer', 'id', 'CASCADE', 'CASCADE');
if ($this->db->getSchema()->getTableSchema('user', true) !== null) {
$this->safeAddForeignKey('fk_rescue_animal_prev_owner', 'rescue_animal', 'previous_owner_user_id', 'user', 'id', 'SET NULL', 'CASCADE');
}
$this->safeCreateTable('rescue_animal_transfer', [
'id' => $this->primaryKey(),
'animal_id' => $this->integer()->notNull(),
'from_contentcontainer_id' => $this->integer()->notNull(),
'to_contentcontainer_id' => $this->integer()->notNull(),
'requested_by' => $this->integer()->null(),
'status' => $this->string(32)->notNull()->defaultValue('requested'),
'request_message' => $this->text()->null(),
'conditions_text' => $this->text()->null(),
'responded_at' => $this->dateTime()->null(),
'completed_at' => $this->dateTime()->null(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex('idx_rescue_animal_transfer_animal', 'rescue_animal_transfer', 'animal_id', false);
$this->safeCreateIndex('idx_rescue_animal_transfer_status', 'rescue_animal_transfer', 'status', false);
$this->safeAddForeignKey('fk_rescue_animal_transfer_animal', 'rescue_animal_transfer', 'animal_id', 'rescue_animal', 'id', 'CASCADE', 'CASCADE');
$this->safeAddForeignKey('fk_rescue_animal_transfer_from_container', 'rescue_animal_transfer', 'from_contentcontainer_id', 'contentcontainer', 'id', 'CASCADE', 'CASCADE');
$this->safeAddForeignKey('fk_rescue_animal_transfer_to_container', 'rescue_animal_transfer', 'to_contentcontainer_id', 'contentcontainer', 'id', 'CASCADE', 'CASCADE');
if ($this->db->getSchema()->getTableSchema('user', true) !== null) {
$this->safeAddForeignKey('fk_rescue_animal_transfer_requested_by', 'rescue_animal_transfer', 'requested_by', 'user', 'id', 'SET NULL', 'CASCADE');
}
$this->safeCreateTable('rescue_animal_medical_visit', [
'id' => $this->primaryKey(),
'animal_id' => $this->integer()->notNull(),
'visit_at' => $this->dateTime()->null(),
'provider_name' => $this->string(190)->null(),
'notes' => $this->text()->null(),
'recommendations' => $this->text()->null(),
'created_by' => $this->integer()->null(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex('idx_rescue_medical_visit_animal', 'rescue_animal_medical_visit', 'animal_id', false);
$this->safeAddForeignKey('fk_rescue_medical_visit_animal', 'rescue_animal_medical_visit', 'animal_id', 'rescue_animal', 'id', 'CASCADE', 'CASCADE');
$this->safeCreateTable('rescue_animal_progress_update', [
'id' => $this->primaryKey(),
'animal_id' => $this->integer()->notNull(),
'update_at' => $this->dateTime()->null(),
'weight' => $this->string(32)->null(),
'vitals' => $this->text()->null(),
'behavior_notes' => $this->text()->null(),
'meal_plan_changes' => $this->text()->null(),
'housing_changes' => $this->text()->null(),
'medical_concerns' => $this->text()->null(),
'post_to_space_feed' => $this->boolean()->notNull()->defaultValue(0),
'post_to_animal_feed' => $this->boolean()->notNull()->defaultValue(1),
'created_by' => $this->integer()->null(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex('idx_rescue_progress_animal', 'rescue_animal_progress_update', 'animal_id', false);
$this->safeAddForeignKey('fk_rescue_progress_animal', 'rescue_animal_progress_update', 'animal_id', 'rescue_animal', 'id', 'CASCADE', 'CASCADE');
$this->seedFieldMetadata();
}
public function safeDown()
{
$this->safeDropTable('rescue_animal_progress_update');
$this->safeDropTable('rescue_animal_medical_visit');
$this->safeDropTable('rescue_animal_transfer');
$this->safeDropTable('rescue_animal');
}
private function seedFieldMetadata(): void
{
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$createdAt = date('Y-m-d H:i:s');
$rows = [
['name', 'Animal name', 'text', 0, 1, 100],
['species', 'Species', 'text', 0, 1, 110],
['breed', 'Breed', 'text', 0, 1, 120],
['sex', 'Sex', 'text', 0, 1, 130],
['status', 'Status', 'select', 1, 1, 140],
['in_possession', 'In possession', 'boolean', 1, 1, 150],
['location_name', 'Location name', 'text', 0, 1, 160],
['city', 'City', 'text', 0, 1, 170],
['state', 'State', 'text', 0, 1, 180],
['zip', 'ZIP', 'text', 0, 1, 190],
['public_summary', 'Public summary', 'textarea', 0, 1, 200],
['medical_notes', 'Medical notes', 'textarea', 0, 1, 210],
];
foreach ($rows as $row) {
[$fieldKey, $label, $inputType, $required, $isCore, $sortOrder] = $row;
$exists = (new \yii\db\Query())
->from('rescue_field_definition')
->where(['module_id' => 'animal_management', 'field_key' => $fieldKey])
->exists($this->db);
if ($exists) {
continue;
}
$this->insert('rescue_field_definition', [
'module_id' => 'animal_management',
'group_key' => 'animal_profile',
'field_key' => $fieldKey,
'label' => $label,
'input_type' => $inputType,
'required' => $required,
'is_core' => $isCore,
'is_active' => 1,
'visibility' => 'public',
'options' => '{}',
'sort_order' => $sortOrder,
'created_at' => $createdAt,
'updated_at' => $createdAt,
]);
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
use humhub\components\Migration;
class m260402_010000_transfer_event_audit extends Migration
{
public function safeUp()
{
$this->safeCreateTable('rescue_animal_transfer_event', [
'id' => $this->primaryKey(),
'transfer_id' => $this->integer()->notNull(),
'animal_id' => $this->integer()->notNull(),
'event_type' => $this->string(32)->notNull(),
'from_status' => $this->string(32)->null(),
'to_status' => $this->string(32)->null(),
'message' => $this->text()->null(),
'metadata_json' => $this->text()->null(),
'created_by' => $this->integer()->null(),
'created_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex('idx_rescue_transfer_event_transfer', 'rescue_animal_transfer_event', 'transfer_id', false);
$this->safeCreateIndex('idx_rescue_transfer_event_animal', 'rescue_animal_transfer_event', 'animal_id', false);
$this->safeCreateIndex('idx_rescue_transfer_event_type', 'rescue_animal_transfer_event', 'event_type', false);
$this->safeCreateIndex('idx_rescue_transfer_event_created_at', 'rescue_animal_transfer_event', 'created_at', false);
$this->safeAddForeignKey(
'fk_rescue_transfer_event_transfer',
'rescue_animal_transfer_event',
'transfer_id',
'rescue_animal_transfer',
'id',
'CASCADE',
'CASCADE'
);
$this->safeAddForeignKey(
'fk_rescue_transfer_event_animal',
'rescue_animal_transfer_event',
'animal_id',
'rescue_animal',
'id',
'CASCADE',
'CASCADE'
);
if ($this->db->getSchema()->getTableSchema('user', true) !== null) {
$this->safeAddForeignKey(
'fk_rescue_transfer_event_created_by',
'rescue_animal_transfer_event',
'created_by',
'user',
'id',
'SET NULL',
'CASCADE'
);
}
}
public function safeDown()
{
$this->safeDropTable('rescue_animal_transfer_event');
}
}

View File

@@ -0,0 +1,50 @@
<?php
use humhub\components\Migration;
class m260402_020000_animal_field_values extends Migration
{
public function safeUp()
{
$this->safeCreateTable('rescue_animal_field_value', [
'id' => $this->primaryKey(),
'animal_id' => $this->integer()->notNull(),
'field_definition_id' => $this->integer()->notNull(),
'value_text' => $this->text()->null(),
'value_json' => $this->text()->null(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex('ux_rescue_animal_field_value_unique', 'rescue_animal_field_value', ['animal_id', 'field_definition_id'], true);
$this->safeCreateIndex('idx_rescue_animal_field_value_animal', 'rescue_animal_field_value', 'animal_id', false);
$this->safeCreateIndex('idx_rescue_animal_field_value_definition', 'rescue_animal_field_value', 'field_definition_id', false);
$this->safeAddForeignKey(
'fk_rescue_animal_field_value_animal',
'rescue_animal_field_value',
'animal_id',
'rescue_animal',
'id',
'CASCADE',
'CASCADE'
);
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) !== null) {
$this->safeAddForeignKey(
'fk_rescue_animal_field_value_definition',
'rescue_animal_field_value',
'field_definition_id',
'rescue_field_definition',
'id',
'CASCADE',
'CASCADE'
);
}
}
public function safeDown()
{
$this->safeDropTable('rescue_animal_field_value');
}
}

View File

@@ -0,0 +1,117 @@
<?php
use humhub\components\Migration;
class m260402_030000_medical_progress_field_values extends Migration
{
public function safeUp()
{
$this->safeCreateTable('rescue_animal_medical_visit_field_value', [
'id' => $this->primaryKey(),
'medical_visit_id' => $this->integer()->notNull(),
'field_definition_id' => $this->integer()->notNull(),
'value_text' => $this->text()->null(),
'value_json' => $this->text()->null(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex(
'ux_rescue_medical_visit_field_value_unique',
'rescue_animal_medical_visit_field_value',
['medical_visit_id', 'field_definition_id'],
true
);
$this->safeCreateIndex(
'idx_rescue_medical_visit_field_value_visit',
'rescue_animal_medical_visit_field_value',
'medical_visit_id',
false
);
$this->safeCreateIndex(
'idx_rescue_medical_visit_field_value_definition',
'rescue_animal_medical_visit_field_value',
'field_definition_id',
false
);
$this->safeAddForeignKey(
'fk_rescue_medical_visit_field_value_visit',
'rescue_animal_medical_visit_field_value',
'medical_visit_id',
'rescue_animal_medical_visit',
'id',
'CASCADE',
'CASCADE'
);
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) !== null) {
$this->safeAddForeignKey(
'fk_rescue_medical_visit_field_value_definition',
'rescue_animal_medical_visit_field_value',
'field_definition_id',
'rescue_field_definition',
'id',
'CASCADE',
'CASCADE'
);
}
$this->safeCreateTable('rescue_animal_progress_update_field_value', [
'id' => $this->primaryKey(),
'progress_update_id' => $this->integer()->notNull(),
'field_definition_id' => $this->integer()->notNull(),
'value_text' => $this->text()->null(),
'value_json' => $this->text()->null(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex(
'ux_rescue_progress_update_field_value_unique',
'rescue_animal_progress_update_field_value',
['progress_update_id', 'field_definition_id'],
true
);
$this->safeCreateIndex(
'idx_rescue_progress_update_field_value_update',
'rescue_animal_progress_update_field_value',
'progress_update_id',
false
);
$this->safeCreateIndex(
'idx_rescue_progress_update_field_value_definition',
'rescue_animal_progress_update_field_value',
'field_definition_id',
false
);
$this->safeAddForeignKey(
'fk_rescue_progress_update_field_value_update',
'rescue_animal_progress_update_field_value',
'progress_update_id',
'rescue_animal_progress_update',
'id',
'CASCADE',
'CASCADE'
);
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) !== null) {
$this->safeAddForeignKey(
'fk_rescue_progress_update_field_value_definition',
'rescue_animal_progress_update_field_value',
'field_definition_id',
'rescue_field_definition',
'id',
'CASCADE',
'CASCADE'
);
}
}
public function safeDown()
{
$this->safeDropTable('rescue_animal_progress_update_field_value');
$this->safeDropTable('rescue_animal_medical_visit_field_value');
}
}

View File

@@ -0,0 +1,61 @@
<?php
use humhub\components\Migration;
class m260402_040000_add_image_profile_fields extends Migration
{
public function safeUp()
{
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$createdAt = date('Y-m-d H:i:s');
$rows = [
['cover_image_url', 'Cover image URL', 230],
['profile_image_url', 'Profile image URL', 240],
];
foreach ($rows as $row) {
[$fieldKey, $label, $sortOrder] = $row;
$exists = (new \yii\db\Query())
->from('rescue_field_definition')
->where(['module_id' => 'animal_management', 'field_key' => $fieldKey])
->exists($this->db);
if ($exists) {
continue;
}
$this->insert('rescue_field_definition', [
'module_id' => 'animal_management',
'group_key' => 'animal_profile',
'field_key' => $fieldKey,
'label' => $label,
'input_type' => 'text',
'required' => 0,
'is_core' => 0,
'is_active' => 1,
'visibility' => 'public',
'options' => '{}',
'sort_order' => $sortOrder,
'created_at' => $createdAt,
'updated_at' => $createdAt,
]);
}
}
public function safeDown()
{
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$this->delete('rescue_field_definition', [
'module_id' => 'animal_management',
'field_key' => ['cover_image_url', 'profile_image_url'],
'is_core' => 0,
]);
}
}

View File

@@ -0,0 +1,160 @@
<?php
use humhub\components\Migration;
class m260402_050000_seed_requirement_default_fields extends Migration
{
public function safeUp()
{
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$createdAt = date('Y-m-d H:i:s');
$rows = [
// Animal profile defaults from requirements
['animal_profile', 'dob', 'DOB', 'date', 0, 1, 'public', 112],
['animal_profile', 'age', 'Age', 'text', 0, 1, 'public', 113],
['animal_profile', 'previous_owner_user_id', 'Previous Owner User ID', 'number', 0, 1, 'internal', 114],
['animal_profile', 'previous_owner_name', 'Previous Owner Name', 'text', 0, 1, 'internal', 115],
['animal_profile', 'previous_owner_business_name', 'Previous Owner Business Name', 'text', 0, 1, 'internal', 116],
['animal_profile', 'previous_owner_street_address', 'Previous Owner Street Address', 'text', 0, 1, 'internal', 117],
['animal_profile', 'previous_owner_city', 'Previous Owner City', 'text', 0, 1, 'internal', 118],
['animal_profile', 'previous_owner_state', 'Previous Owner State', 'text', 0, 1, 'internal', 119],
['animal_profile', 'previous_owner_zip', 'Previous Owner Zip', 'text', 0, 1, 'internal', 120],
['animal_profile', 'previous_owner_cell_phone', 'Previous Owner Cell Phone', 'text', 0, 1, 'internal', 121],
['animal_profile', 'previous_owner_business_phone', 'Previous Owner Business Phone', 'text', 0, 1, 'internal', 122],
['animal_profile', 'previous_owner_email', 'Previous Owner Email', 'text', 0, 1, 'internal', 123],
['animal_profile', 'lineage', 'Lineage', 'textarea', 0, 1, 'public', 124],
['animal_profile', 'backstory', 'Backstory', 'textarea', 0, 1, 'public', 125],
['animal_profile', 'rescue', 'Rescue', 'text', 0, 1, 'internal', 126],
// Medical visit defaults from requirements
['animal_medical_visit', 'weight', 'Weight', 'text', 0, 1, 'internal', 210],
['animal_medical_visit', 'pulse', 'Pulse', 'text', 0, 1, 'internal', 211],
['animal_medical_visit', 'blood_pressure', 'Blood Pressure', 'text', 0, 1, 'internal', 212],
['animal_medical_visit', 'oxygen', 'Oxygen', 'text', 0, 1, 'internal', 213],
['animal_medical_visit', 'chronic_conditions', 'Chronic Conditions', 'textarea', 0, 1, 'internal', 214],
['animal_medical_visit', 'acute_conditions', 'Acute Conditions', 'textarea', 0, 1, 'internal', 215],
['animal_medical_visit', 'special_needs', 'Special Needs', 'textarea', 0, 1, 'internal', 216],
['animal_medical_visit', 'date_of_most_recent_medical_visit', 'Date of Most Recent Medical Visit', 'date', 0, 1, 'internal', 217],
['animal_medical_visit', 'physician_name', 'Physician Name', 'text', 0, 1, 'internal', 218],
['animal_medical_visit', 'physician_business_name', 'Physician Business Name', 'text', 0, 1, 'internal', 219],
['animal_medical_visit', 'physician_street_address', 'Physician Street Address', 'text', 0, 1, 'internal', 220],
['animal_medical_visit', 'physician_city', 'Physician City', 'text', 0, 1, 'internal', 221],
['animal_medical_visit', 'physician_state', 'Physician State', 'text', 0, 1, 'internal', 222],
['animal_medical_visit', 'physician_zip', 'Physician Zip', 'text', 0, 1, 'internal', 223],
['animal_medical_visit', 'physician_cell_phone', 'Physician Cell Phone', 'text', 0, 1, 'internal', 224],
['animal_medical_visit', 'physician_business_phone', 'Physician Business Phone', 'text', 0, 1, 'internal', 225],
['animal_medical_visit', 'physician_license_number', 'Physician License Number', 'text', 0, 1, 'internal', 226],
['animal_medical_visit', 'second_physician_name', 'Second Physician Name', 'text', 0, 1, 'internal', 227],
['animal_medical_visit', 'second_physician_business_name', 'Second Physician Business Name', 'text', 0, 1, 'internal', 228],
['animal_medical_visit', 'second_physician_street_address', 'Second Physician Street Address', 'text', 0, 1, 'internal', 229],
['animal_medical_visit', 'second_physician_city', 'Second Physician City', 'text', 0, 1, 'internal', 230],
['animal_medical_visit', 'second_physician_state', 'Second Physician State', 'text', 0, 1, 'internal', 231],
['animal_medical_visit', 'second_physician_zip', 'Second Physician Zip', 'text', 0, 1, 'internal', 232],
['animal_medical_visit', 'second_physician_cell_phone', 'Second Physician Cell Phone', 'text', 0, 1, 'internal', 233],
['animal_medical_visit', 'second_physician_business_phone', 'Second Physician Business Phone', 'text', 0, 1, 'internal', 234],
['animal_medical_visit', 'second_physician_license_number', 'Second Physician License Number', 'text', 0, 1, 'internal', 235],
['animal_medical_visit', 'previous_physicians', 'Previous Physician(s)', 'textarea', 0, 1, 'internal', 236],
// Progress defaults from requirements
['animal_progress_update', 'progress_notes', 'Notes', 'textarea', 0, 1, 'internal', 310],
['animal_progress_update', 'routine_updates', 'Updates', 'textarea', 0, 1, 'internal', 311],
['animal_progress_update', 'media_reference', 'Media', 'text', 0, 1, 'internal', 312],
];
foreach ($rows as $row) {
[$groupKey, $fieldKey, $label, $inputType, $required, $isActive, $visibility, $sortOrder] = $row;
$exists = (new \yii\db\Query())
->from('rescue_field_definition')
->where(['module_id' => 'animal_management', 'field_key' => $fieldKey])
->exists($this->db);
if ($exists) {
continue;
}
$this->insert('rescue_field_definition', [
'module_id' => 'animal_management',
'group_key' => $groupKey,
'field_key' => $fieldKey,
'label' => $label,
'input_type' => $inputType,
'required' => $required,
'is_core' => 0,
'is_active' => $isActive,
'visibility' => $visibility,
'options' => '{}',
'sort_order' => $sortOrder,
'created_at' => $createdAt,
'updated_at' => $createdAt,
]);
}
}
public function safeDown()
{
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$fieldKeys = [
'dob',
'age',
'previous_owner_user_id',
'previous_owner_name',
'previous_owner_business_name',
'previous_owner_street_address',
'previous_owner_city',
'previous_owner_state',
'previous_owner_zip',
'previous_owner_cell_phone',
'previous_owner_business_phone',
'previous_owner_email',
'lineage',
'backstory',
'rescue',
'pulse',
'blood_pressure',
'oxygen',
'chronic_conditions',
'acute_conditions',
'special_needs',
'date_of_most_recent_medical_visit',
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
'second_physician_name',
'second_physician_business_name',
'second_physician_street_address',
'second_physician_city',
'second_physician_state',
'second_physician_zip',
'second_physician_cell_phone',
'second_physician_business_phone',
'second_physician_license_number',
'previous_physicians',
'progress_notes',
'routine_updates',
'media_reference',
];
$this->delete('rescue_field_definition', [
'module_id' => 'animal_management',
'field_key' => $fieldKeys,
'is_core' => 0,
]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
use humhub\components\Migration;
class m260402_060000_create_animal_gallery extends Migration
{
public function safeUp()
{
$this->safeCreateTable('rescue_animal_gallery_item', [
'id' => $this->primaryKey(),
'animal_id' => $this->integer()->notNull(),
'file_path' => $this->string(500)->null(),
'file_id' => $this->integer()->null(),
'source_post_id' => $this->integer()->null(),
'source_type' => $this->string(32)->notNull()->defaultValue('upload'),
'caption' => $this->text()->null(),
'created_by' => $this->integer()->null(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex('idx_rescue_animal_gallery_animal', 'rescue_animal_gallery_item', 'animal_id', false);
$this->safeCreateIndex('idx_rescue_animal_gallery_file', 'rescue_animal_gallery_item', 'file_id', false);
$this->safeCreateIndex('idx_rescue_animal_gallery_post', 'rescue_animal_gallery_item', 'source_post_id', false);
$this->safeCreateIndex('idx_rescue_animal_gallery_created', 'rescue_animal_gallery_item', ['animal_id', 'id'], false);
$this->safeAddForeignKey(
'fk_rescue_animal_gallery_animal',
'rescue_animal_gallery_item',
'animal_id',
'rescue_animal',
'id',
'CASCADE',
'CASCADE'
);
if ($this->db->getSchema()->getTableSchema('file', true) !== null) {
$this->safeAddForeignKey(
'fk_rescue_animal_gallery_file',
'rescue_animal_gallery_item',
'file_id',
'file',
'id',
'SET NULL',
'CASCADE'
);
}
if ($this->db->getSchema()->getTableSchema('post', true) !== null) {
$this->safeAddForeignKey(
'fk_rescue_animal_gallery_post',
'rescue_animal_gallery_item',
'source_post_id',
'post',
'id',
'SET NULL',
'CASCADE'
);
}
}
public function safeDown()
{
$this->safeDropTable('rescue_animal_gallery_item');
}
}

View File

@@ -0,0 +1,133 @@
<?php
use humhub\components\Migration;
class m260402_070000_lock_requirement_default_fields extends Migration
{
public function safeUp()
{
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$fieldKeys = [
'dob',
'age',
'previous_owner_user_id',
'previous_owner_name',
'previous_owner_business_name',
'previous_owner_street_address',
'previous_owner_city',
'previous_owner_state',
'previous_owner_zip',
'previous_owner_cell_phone',
'previous_owner_business_phone',
'previous_owner_email',
'lineage',
'backstory',
'rescue',
'weight',
'pulse',
'blood_pressure',
'oxygen',
'chronic_conditions',
'acute_conditions',
'special_needs',
'date_of_most_recent_medical_visit',
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
'second_physician_name',
'second_physician_business_name',
'second_physician_street_address',
'second_physician_city',
'second_physician_state',
'second_physician_zip',
'second_physician_cell_phone',
'second_physician_business_phone',
'second_physician_license_number',
'previous_physicians',
'progress_notes',
'routine_updates',
'media_reference',
];
$this->update('rescue_field_definition', [
'is_core' => 1,
'is_active' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], [
'module_id' => 'animal_management',
'field_key' => $fieldKeys,
]);
}
public function safeDown()
{
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$fieldKeys = [
'dob',
'age',
'previous_owner_user_id',
'previous_owner_name',
'previous_owner_business_name',
'previous_owner_street_address',
'previous_owner_city',
'previous_owner_state',
'previous_owner_zip',
'previous_owner_cell_phone',
'previous_owner_business_phone',
'previous_owner_email',
'lineage',
'backstory',
'rescue',
'weight',
'pulse',
'blood_pressure',
'oxygen',
'chronic_conditions',
'acute_conditions',
'special_needs',
'date_of_most_recent_medical_visit',
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
'second_physician_name',
'second_physician_business_name',
'second_physician_street_address',
'second_physician_city',
'second_physician_state',
'second_physician_zip',
'second_physician_cell_phone',
'second_physician_business_phone',
'second_physician_license_number',
'previous_physicians',
'progress_notes',
'routine_updates',
'media_reference',
];
$this->update('rescue_field_definition', [
'is_core' => 0,
'updated_at' => date('Y-m-d H:i:s'),
], [
'module_id' => 'animal_management',
'field_key' => $fieldKeys,
]);
}
}

View File

@@ -0,0 +1,79 @@
<?php
use humhub\components\Migration;
class m260402_080000_add_display_override_profile_fields extends Migration
{
public function safeUp()
{
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$createdAt = date('Y-m-d H:i:s');
$rows = [
['tile_display_fields', 'Tile Display Fields', 250],
['hero_display_fields', 'Hero Display Fields', 251],
];
foreach ($rows as $row) {
[$fieldKey, $label, $sortOrder] = $row;
$existingId = (new \yii\db\Query())
->select('id')
->from('rescue_field_definition')
->where([
'module_id' => 'animal_management',
'group_key' => 'animal_profile',
'field_key' => $fieldKey,
])
->scalar($this->db);
if ($existingId) {
$this->update('rescue_field_definition', [
'label' => $label,
'input_type' => 'text',
'required' => 0,
'is_core' => 1,
'is_active' => 1,
'visibility' => 'internal',
'sort_order' => $sortOrder,
'updated_at' => $createdAt,
], ['id' => (int)$existingId]);
continue;
}
$this->insert('rescue_field_definition', [
'module_id' => 'animal_management',
'group_key' => 'animal_profile',
'field_key' => $fieldKey,
'label' => $label,
'input_type' => 'text',
'required' => 0,
'is_core' => 1,
'is_active' => 1,
'visibility' => 'internal',
'options' => '{}',
'sort_order' => $sortOrder,
'created_at' => $createdAt,
'updated_at' => $createdAt,
]);
}
}
public function safeDown()
{
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$this->update('rescue_field_definition', [
'is_core' => 0,
'updated_at' => date('Y-m-d H:i:s'),
], [
'module_id' => 'animal_management',
'group_key' => 'animal_profile',
'field_key' => ['tile_display_fields', 'hero_display_fields'],
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
use humhub\components\Migration;
class m260403_120000_create_animal_gallery_link extends Migration
{
public function safeUp()
{
if ($this->db->getSchema()->getTableSchema('rescue_animal_gallery_link', true) !== null) {
return;
}
$this->safeCreateTable('rescue_animal_gallery_link', [
'id' => $this->primaryKey(),
'animal_id' => $this->integer()->notNull(),
'gallery_id' => $this->integer()->notNull(),
'contentcontainer_id' => $this->integer()->notNull(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex('idx_rescue_animal_gallery_link_animal', 'rescue_animal_gallery_link', 'animal_id', true);
$this->safeCreateIndex('idx_rescue_animal_gallery_link_gallery', 'rescue_animal_gallery_link', 'gallery_id', true);
$this->safeCreateIndex('idx_rescue_animal_gallery_link_container', 'rescue_animal_gallery_link', 'contentcontainer_id', false);
if ($this->db->getSchema()->getTableSchema('rescue_animal', true) !== null) {
$this->safeAddForeignKey(
'fk_rescue_animal_gallery_link_animal',
'rescue_animal_gallery_link',
'animal_id',
'rescue_animal',
'id',
'CASCADE',
'CASCADE'
);
}
if ($this->db->getSchema()->getTableSchema('gallery_gallery', true) !== null) {
$this->safeAddForeignKey(
'fk_rescue_animal_gallery_link_gallery',
'rescue_animal_gallery_link',
'gallery_id',
'gallery_gallery',
'id',
'CASCADE',
'CASCADE'
);
}
}
public function safeDown()
{
$this->safeDropTable('rescue_animal_gallery_link');
}
}

View File

@@ -0,0 +1,94 @@
<?php
use humhub\components\Migration;
class m260403_130000_add_medical_stream_and_flags extends Migration
{
public function safeUp()
{
$medicalTable = $this->db->getSchema()->getTableSchema('rescue_animal_medical_visit', true);
if ($medicalTable !== null) {
if (!isset($medicalTable->columns['post_to_space_feed'])) {
$this->addColumn('rescue_animal_medical_visit', 'post_to_space_feed', $this->boolean()->notNull()->defaultValue(0));
}
if (!isset($medicalTable->columns['post_to_animal_feed'])) {
$this->addColumn('rescue_animal_medical_visit', 'post_to_animal_feed', $this->boolean()->notNull()->defaultValue(1));
}
$this->update('rescue_animal_medical_visit', ['post_to_space_feed' => 0], ['post_to_space_feed' => null]);
$this->update('rescue_animal_medical_visit', ['post_to_animal_feed' => 1], ['post_to_animal_feed' => null]);
}
if ($this->db->getSchema()->getTableSchema('rescue_animal_stream_entry', true) === null) {
$this->safeCreateTable('rescue_animal_stream_entry', [
'id' => $this->primaryKey(),
'animal_id' => $this->integer()->notNull(),
'entry_type' => $this->string(32)->notNull(),
'medical_visit_id' => $this->integer()->null(),
'progress_update_id' => $this->integer()->null(),
'created_at' => $this->dateTime()->null(),
'updated_at' => $this->dateTime()->null(),
]);
$this->safeCreateIndex('idx_rescue_animal_stream_entry_animal', 'rescue_animal_stream_entry', 'animal_id', false);
$this->safeCreateIndex('idx_rescue_animal_stream_entry_type', 'rescue_animal_stream_entry', 'entry_type', false);
$this->safeCreateIndex('idx_rescue_animal_stream_entry_medical', 'rescue_animal_stream_entry', 'medical_visit_id', true);
$this->safeCreateIndex('idx_rescue_animal_stream_entry_progress', 'rescue_animal_stream_entry', 'progress_update_id', true);
$this->safeAddForeignKey(
'fk_rescue_animal_stream_entry_animal',
'rescue_animal_stream_entry',
'animal_id',
'rescue_animal',
'id',
'CASCADE',
'CASCADE'
);
$medicalVisitTable = $this->db->getSchema()->getTableSchema('rescue_animal_medical_visit', true);
if ($medicalVisitTable !== null) {
$this->safeAddForeignKey(
'fk_rescue_animal_stream_entry_medical',
'rescue_animal_stream_entry',
'medical_visit_id',
'rescue_animal_medical_visit',
'id',
'CASCADE',
'CASCADE'
);
}
$progressTable = $this->db->getSchema()->getTableSchema('rescue_animal_progress_update', true);
if ($progressTable !== null) {
$this->safeAddForeignKey(
'fk_rescue_animal_stream_entry_progress',
'rescue_animal_stream_entry',
'progress_update_id',
'rescue_animal_progress_update',
'id',
'CASCADE',
'CASCADE'
);
}
}
}
public function safeDown()
{
if ($this->db->getSchema()->getTableSchema('rescue_animal_stream_entry', true) !== null) {
$this->safeDropTable('rescue_animal_stream_entry');
}
$medicalTable = $this->db->getSchema()->getTableSchema('rescue_animal_medical_visit', true);
if ($medicalTable !== null) {
if (isset($medicalTable->columns['post_to_space_feed'])) {
$this->dropColumn('rescue_animal_medical_visit', 'post_to_space_feed');
}
if (isset($medicalTable->columns['post_to_animal_feed'])) {
$this->dropColumn('rescue_animal_medical_visit', 'post_to_animal_feed');
}
}
}
}

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);
}
}
}

15
module.json Normal file
View File

@@ -0,0 +1,15 @@
{
"id": "animal_management",
"name": "Animal Management",
"description": "Animal intake, profile management, and transfer workflow foundation for rescue spaces.",
"keywords": ["animals", "rescue", "intake", "transfer"],
"version": "0.1.0",
"humhub": {
"minVersion": "1.14"
},
"authors": [
{
"name": "Kelin Rescue Hub"
}
]
}

View File

@@ -0,0 +1,21 @@
<?php
namespace humhub\modules\animal_management\notifications;
use humhub\modules\notification\components\NotificationCategory;
use Yii;
class TransferNotificationCategory extends NotificationCategory
{
public $id = 'animal_management_transfer';
public function getTitle()
{
return Yii::t('AnimalManagementModule.base', 'Animal Transfers');
}
public function getDescription()
{
return Yii::t('AnimalManagementModule.base', 'Receive notifications about animal transfer requests and status updates.');
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace humhub\modules\animal_management\notifications;
use humhub\modules\animal_management\models\AnimalTransfer;
use humhub\modules\space\models\Space;
use humhub\modules\user\models\User;
class TransferNotifier
{
public static function notifyStatusChange(AnimalTransfer $transfer, string $eventType, ?User $originator = null, string $details = ''): int
{
$animal = $transfer->animal;
if ($animal === null) {
return 0;
}
$fromSpace = $transfer->getFromSpace();
$toSpace = $transfer->getToSpace();
if (!$fromSpace instanceof Space || !$toSpace instanceof Space) {
return 0;
}
$targetSpaces = static::targetSpacesForEvent($eventType, $fromSpace, $toSpace);
$sentCount = 0;
$seenRecipients = [];
foreach ($targetSpaces as $space) {
$recipients = static::privilegedUsersForSpace($space);
foreach ($recipients as $recipient) {
$recipientId = (int)$recipient->id;
if ($originator instanceof User && $recipientId === (int)$originator->id) {
continue;
}
if (isset($seenRecipients[$recipientId])) {
continue;
}
$seenRecipients[$recipientId] = true;
$notification = TransferStatusNotification::instance();
if ($originator instanceof User) {
$notification->from($originator);
}
$notification->animalName = $animal->getDisplayName();
$notification->fromSpaceName = (string)$fromSpace->name;
$notification->toSpaceName = (string)$toSpace->name;
$notification->spaceGuid = (string)$space->guid;
$notification->eventType = $eventType;
$notification->details = $details;
$notification->payload([
'animalName' => $notification->animalName,
'fromSpaceName' => $notification->fromSpaceName,
'toSpaceName' => $notification->toSpaceName,
'spaceGuid' => $notification->spaceGuid,
'eventType' => $notification->eventType,
'details' => $notification->details,
]);
$notification->send($recipient);
$sentCount++;
}
}
return $sentCount;
}
public static function privilegedUsersForSpace(Space $space): array
{
$recipients = [];
foreach ($space->getPrivilegedGroupUsers() as $users) {
foreach ($users as $user) {
if ($user instanceof User && (int)$user->status === User::STATUS_ENABLED) {
$recipients[(int)$user->id] = $user;
}
}
}
if (empty($recipients)) {
$owner = $space->getOwnerUser()->one();
if ($owner instanceof User && (int)$owner->status === User::STATUS_ENABLED) {
$recipients[(int)$owner->id] = $owner;
}
}
return $recipients;
}
private static function targetSpacesForEvent(string $eventType, Space $fromSpace, Space $toSpace): array
{
$spaces = [];
switch ($eventType) {
case 'accepted':
case 'declined':
$spaces[] = $fromSpace;
break;
case 'cancelled':
$spaces[] = $toSpace;
break;
case 'completed':
$spaces[] = $fromSpace;
break;
default:
$spaces[] = $fromSpace;
$spaces[] = $toSpace;
break;
}
$unique = [];
foreach ($spaces as $space) {
$key = (int)$space->contentcontainer_id;
if (!isset($unique[$key])) {
$unique[$key] = $space;
}
}
return array_values($unique);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace humhub\modules\animal_management\notifications;
use humhub\libs\Html;
use humhub\modules\notification\components\BaseNotification;
use humhub\modules\space\models\Space;
use Yii;
use yii\helpers\Json;
use yii\helpers\Url;
class TransferRequestedNotification extends BaseNotification
{
public $moduleId = 'animal_management';
public $requireSource = false;
public $requireOriginator = false;
public string $animalName = '';
public string $fromSpaceName = '';
public string $toSpaceGuid = '';
public function category()
{
return new TransferNotificationCategory();
}
public function getUrl()
{
$toSpaceGuid = $this->payloadString('toSpaceGuid', $this->toSpaceGuid);
$space = Space::findOne(['guid' => $toSpaceGuid]);
if ($space instanceof Space) {
return $space->createUrl('/animal_management/animals/index') . '#incoming-transfers';
}
return Url::to(['/animal_management/animals/index', 'sguid' => $toSpaceGuid]) . '#incoming-transfers';
}
public function html()
{
$animalName = $this->payloadString('animalName', $this->animalName);
if ($this->originator) {
return Yii::t('AnimalManagementModule.base', '{displayName} requested to transfer {animalName}.', [
'displayName' => Html::tag('strong', Html::encode($this->originator->displayName)),
'animalName' => Html::tag('strong', Html::encode($animalName)),
]);
}
return Yii::t('AnimalManagementModule.base', 'A transfer was requested for {animalName}.', [
'animalName' => Html::tag('strong', Html::encode($animalName)),
]);
}
public function getMailSubject()
{
$animalName = $this->payloadString('animalName', $this->animalName);
return Yii::t('AnimalManagementModule.base', 'Animal transfer request: {animalName}', [
'animalName' => $animalName,
]);
}
public function __serialize(): array
{
$data = parent::__serialize();
$data['animalName'] = $this->animalName;
$data['fromSpaceName'] = $this->fromSpaceName;
$data['toSpaceGuid'] = $this->toSpaceGuid;
$data['payload'] = $this->payload;
return $data;
}
public function __unserialize($unserializedArr)
{
parent::__unserialize($unserializedArr);
$this->animalName = (string)($unserializedArr['animalName'] ?? '');
$this->fromSpaceName = (string)($unserializedArr['fromSpaceName'] ?? '');
$this->toSpaceGuid = (string)($unserializedArr['toSpaceGuid'] ?? '');
if (isset($unserializedArr['payload']) && is_array($unserializedArr['payload'])) {
$this->payload = $unserializedArr['payload'];
}
}
private function payloadString(string $key, string $fallback = ''): string
{
if (is_array($this->payload) && array_key_exists($key, $this->payload)) {
return trim((string)$this->payload[$key]);
}
if ($this->record !== null && !empty($this->record->payload)) {
try {
$decoded = Json::decode((string)$this->record->payload);
if (is_array($decoded)) {
$this->payload = $decoded;
if (array_key_exists($key, $decoded)) {
return trim((string)$decoded[$key]);
}
}
} catch (\Throwable $e) {
// Fall back to explicit property values when payload is unavailable.
}
}
return trim($fallback);
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace humhub\modules\animal_management\notifications;
use humhub\libs\Html;
use humhub\modules\notification\components\BaseNotification;
use humhub\modules\space\models\Space;
use Yii;
use yii\helpers\Json;
use yii\helpers\Url;
class TransferStatusNotification extends BaseNotification
{
public $moduleId = 'animal_management';
public $requireSource = false;
public $requireOriginator = false;
public string $animalName = '';
public string $fromSpaceName = '';
public string $toSpaceName = '';
public string $spaceGuid = '';
public string $eventType = '';
public string $details = '';
public function category()
{
return new TransferNotificationCategory();
}
public function getUrl()
{
$eventType = $this->payloadString('eventType', $this->eventType);
$spaceGuid = $this->payloadString('spaceGuid', $this->spaceGuid);
$anchor = static::eventAnchor($eventType);
$space = Space::findOne(['guid' => $spaceGuid]);
if ($space instanceof Space) {
return $space->createUrl('/animal_management/animals/index') . '#' . $anchor;
}
return Url::to(['/animal_management/animals/index', 'sguid' => $spaceGuid]) . '#' . $anchor;
}
public function html()
{
$animalName = $this->payloadString('animalName', $this->animalName);
$eventType = $this->payloadString('eventType', $this->eventType);
$details = $this->payloadString('details', $this->details);
$params = [
'animalName' => Html::tag('strong', Html::encode($animalName)),
'status' => Html::encode(static::statusLabel($eventType)),
];
if ($this->originator) {
$params['displayName'] = Html::tag('strong', Html::encode($this->originator->displayName));
$message = Yii::t('AnimalManagementModule.base', '{displayName} {status} the transfer of {animalName}.', $params);
} else {
$message = Yii::t('AnimalManagementModule.base', '{status} the transfer of {animalName}.', $params);
}
if ($details !== '') {
$message .= ' ' . Html::encode($details);
}
return $message;
}
public function getMailSubject()
{
$animalName = $this->payloadString('animalName', $this->animalName);
$eventType = $this->payloadString('eventType', $this->eventType);
return Yii::t('AnimalManagementModule.base', 'Animal transfer update: {animalName} ({status})', [
'animalName' => $animalName,
'status' => static::statusLabel($eventType),
]);
}
public function __serialize(): array
{
$data = parent::__serialize();
$data['animalName'] = $this->animalName;
$data['fromSpaceName'] = $this->fromSpaceName;
$data['toSpaceName'] = $this->toSpaceName;
$data['spaceGuid'] = $this->spaceGuid;
$data['eventType'] = $this->eventType;
$data['details'] = $this->details;
$data['payload'] = $this->payload;
return $data;
}
public function __unserialize($unserializedArr)
{
parent::__unserialize($unserializedArr);
$this->animalName = (string)($unserializedArr['animalName'] ?? '');
$this->fromSpaceName = (string)($unserializedArr['fromSpaceName'] ?? '');
$this->toSpaceName = (string)($unserializedArr['toSpaceName'] ?? '');
$this->spaceGuid = (string)($unserializedArr['spaceGuid'] ?? '');
$this->eventType = (string)($unserializedArr['eventType'] ?? '');
$this->details = (string)($unserializedArr['details'] ?? '');
if (isset($unserializedArr['payload']) && is_array($unserializedArr['payload'])) {
$this->payload = $unserializedArr['payload'];
}
}
public static function statusLabel(string $eventType): string
{
switch ($eventType) {
case 'accepted':
return Yii::t('AnimalManagementModule.base', 'Accepted');
case 'declined':
return Yii::t('AnimalManagementModule.base', 'Declined');
case 'completed':
return Yii::t('AnimalManagementModule.base', 'Completed');
case 'cancelled':
return Yii::t('AnimalManagementModule.base', 'Cancelled');
default:
return Yii::t('AnimalManagementModule.base', 'Updated');
}
}
private static function eventAnchor(string $eventType): string
{
switch ($eventType) {
case 'requested':
case 'cancelled':
return 'incoming-transfers';
case 'accepted':
case 'declined':
case 'completed':
return 'outgoing-transfers';
default:
return 'outgoing-transfers';
}
}
private function payloadString(string $key, string $fallback = ''): string
{
if (is_array($this->payload) && array_key_exists($key, $this->payload)) {
return trim((string)$this->payload[$key]);
}
if ($this->record !== null && !empty($this->record->payload)) {
try {
$decoded = Json::decode((string)$this->record->payload);
if (is_array($decoded)) {
$this->payload = $decoded;
if (array_key_exists($key, $decoded)) {
return trim((string)$decoded[$key]);
}
}
} catch (\Throwable $e) {
// Fall back to explicit property values when payload is unavailable.
}
}
return trim($fallback);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace humhub\modules\animal_management\permissions;
use humhub\libs\BasePermission;
use humhub\modules\space\models\Space;
use Yii;
class ManageAnimals extends BasePermission
{
public $moduleId = 'animal_management';
public $defaultAllowedGroups = [
Space::USERGROUP_OWNER,
Space::USERGROUP_ADMIN,
Space::USERGROUP_MODERATOR,
];
protected $fixedGroups = [
Space::USERGROUP_USER,
Space::USERGROUP_GUEST,
];
public function getTitle()
{
return Yii::t('AnimalManagementModule.base', 'Manage animals');
}
public function getDescription()
{
return Yii::t('AnimalManagementModule.base', 'Allows creating, editing, and transferring animal records.');
}
}

View File

@@ -0,0 +1,90 @@
<?php
$pdo = new PDO('mysql:host=humhub-db;dbname=humhub', 'humhub', 'Tiberi0u$', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$animals = $pdo->query('SELECT id FROM rescue_animal ORDER BY id')->fetchAll(PDO::FETCH_COLUMN);
if (!$animals) {
echo "No animals found\n";
exit(0);
}
$coverId = (int)$pdo->query("SELECT id FROM rescue_field_definition WHERE module_id='animal_management' AND field_key='cover_image_url' LIMIT 1")->fetchColumn();
$profileId = (int)$pdo->query("SELECT id FROM rescue_field_definition WHERE module_id='animal_management' AND field_key='profile_image_url' LIMIT 1")->fetchColumn();
$seedDir = '/var/www/localhost/htdocs/uploads/animal-management/seed';
$seedFiles = glob($seedDir . '/*.jpg');
sort($seedFiles);
if (empty($seedFiles)) {
echo "No seed images downloaded\n";
exit(1);
}
$now = date('Y-m-d H:i:s');
$idx = 0;
$upsert = $pdo->prepare("INSERT INTO rescue_animal_field_value (animal_id, field_definition_id, value_text, value_json, created_at, updated_at) VALUES (:animal_id,:field_definition_id,:value_text,NULL,:created_at,:updated_at) ON DUPLICATE KEY UPDATE value_text=VALUES(value_text),updated_at=VALUES(updated_at)");
$insertGallery = $pdo->prepare("INSERT INTO rescue_animal_gallery_item (animal_id, file_path, file_id, source_post_id, source_type, caption, created_by, created_at, updated_at) VALUES (:animal_id,:file_path,NULL,NULL,'seed',NULL,NULL,:created_at,:updated_at)");
$deleteSeedGallery = $pdo->prepare("DELETE FROM rescue_animal_gallery_item WHERE animal_id=:animal_id AND source_type='seed'");
foreach ($animals as $animalIdRaw) {
$animalId = (int)$animalIdRaw;
$source = $seedFiles[$idx % count($seedFiles)];
$idx++;
$targetDir = '/var/www/localhost/htdocs/uploads/animal-management/animals/' . $animalId;
if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) {
echo "Could not create directory for animal {$animalId}\n";
continue;
}
$stamp = time() . '-' . $animalId;
$coverName = 'cover-seed-' . $stamp . '.jpg';
$profileName = 'profile-seed-' . $stamp . '.jpg';
$galleryName = 'gallery-seed-' . $stamp . '.jpg';
$coverPath = $targetDir . '/' . $coverName;
$profilePath = $targetDir . '/' . $profileName;
$galleryPath = $targetDir . '/' . $galleryName;
if (!copy($source, $coverPath) || !copy($source, $profilePath) || !copy($source, $galleryPath)) {
echo "Could not copy seed image for animal {$animalId}\n";
continue;
}
$coverUrl = '/uploads/animal-management/animals/' . $animalId . '/' . $coverName;
$profileUrl = '/uploads/animal-management/animals/' . $animalId . '/' . $profileName;
$galleryUrl = '/uploads/animal-management/animals/' . $animalId . '/' . $galleryName;
if ($coverId > 0) {
$upsert->execute([
':animal_id' => $animalId,
':field_definition_id' => $coverId,
':value_text' => $coverUrl,
':created_at' => $now,
':updated_at' => $now,
]);
}
if ($profileId > 0) {
$upsert->execute([
':animal_id' => $animalId,
':field_definition_id' => $profileId,
':value_text' => $profileUrl,
':created_at' => $now,
':updated_at' => $now,
]);
}
$deleteSeedGallery->execute([':animal_id' => $animalId]);
$insertGallery->execute([
':animal_id' => $animalId,
':file_path' => $galleryUrl,
':created_at' => $now,
':updated_at' => $now,
]);
echo "Seeded animal {$animalId} with " . basename($source) . "\n";
}

View File

@@ -0,0 +1,87 @@
<?php
namespace humhub\modules\animal_management\services;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\animal_management\models\AnimalProgressUpdate;
use humhub\modules\animal_management\models\AnimalStreamEntry;
use humhub\modules\space\models\Space;
use Yii;
class AnimalStreamPublisherService
{
public static function publishMedicalVisit(Animal $animal, AnimalMedicalVisit $visit): void
{
if (!self::streamTableExists()) {
return;
}
$exists = AnimalStreamEntry::find()->where([
'entry_type' => AnimalStreamEntry::TYPE_MEDICAL,
'medical_visit_id' => (int)$visit->id,
])->exists();
if ($exists) {
return;
}
self::publishEntry($animal, AnimalStreamEntry::TYPE_MEDICAL, (int)$visit->id, null);
}
public static function publishProgressUpdate(Animal $animal, AnimalProgressUpdate $update): void
{
if (!self::streamTableExists()) {
return;
}
$exists = AnimalStreamEntry::find()->where([
'entry_type' => AnimalStreamEntry::TYPE_PROGRESS,
'progress_update_id' => (int)$update->id,
])->exists();
if ($exists) {
return;
}
self::publishEntry($animal, AnimalStreamEntry::TYPE_PROGRESS, null, (int)$update->id);
}
private static function publishEntry(Animal $animal, string $entryType, ?int $medicalVisitId, ?int $progressUpdateId): void
{
try {
$space = Space::findOne(['contentcontainer_id' => (int)$animal->contentcontainer_id]);
if (!$space instanceof Space) {
return;
}
$entry = new AnimalStreamEntry();
$entry->animal_id = (int)$animal->id;
$entry->entry_type = $entryType;
$entry->medical_visit_id = $medicalVisitId;
$entry->progress_update_id = $progressUpdateId;
$entry->content->container = $space;
if (!$entry->save()) {
Yii::warning([
'message' => 'Could not save animal stream entry.',
'animal_id' => (int)$animal->id,
'entry_type' => $entryType,
'errors' => $entry->getErrors(),
], 'animal_management.stream_publish');
}
} catch (\Throwable $e) {
Yii::warning([
'message' => 'Unexpected error while publishing animal stream entry.',
'animal_id' => (int)$animal->id,
'entry_type' => $entryType,
'exception' => $e->getMessage(),
], 'animal_management.stream_publish');
}
}
private static function streamTableExists(): bool
{
return Yii::$app->db->schema->getTableSchema('rescue_animal_stream_entry', true) !== null;
}
}

View File

@@ -0,0 +1,343 @@
<?php
namespace humhub\modules\animal_management\services;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\AnimalGalleryLink;
use humhub\modules\gallery\models\CustomGallery;
use humhub\modules\space\models\Space;
use Yii;
class GalleryIntegrationService
{
public static function canSyncForSpace(Space $space): bool
{
if (!$space->moduleManager->isEnabled('animal_management')) {
return false;
}
if (!$space->moduleManager->isEnabled('gallery')) {
return false;
}
return self::hasRequiredTables();
}
public static function syncSpaceAnimalGalleries(Space $space): void
{
if (!self::canSyncForSpace($space)) {
return;
}
$animals = Animal::find()->where(['contentcontainer_id' => (int)$space->contentcontainer_id])->all();
$synced = 0;
foreach ($animals as $animal) {
if ($animal instanceof Animal) {
if (self::ensureAnimalGallery($animal, $space) instanceof CustomGallery) {
$synced++;
}
}
}
Yii::warning([
'space_id' => (int)$space->id,
'contentcontainer_id' => (int)$space->contentcontainer_id,
'animal_count' => count($animals),
'synced_count' => $synced,
'has_link_table' => self::hasLinkTable(),
], 'animal_management.gallery_integration.sync');
}
public static function ensureAnimalGallery(Animal $animal, ?Space $space = null): ?CustomGallery
{
if (!self::hasRequiredTables()) {
return null;
}
$space = $space ?: Space::findOne(['contentcontainer_id' => (int)$animal->contentcontainer_id]);
if (!$space instanceof Space || !self::canSyncForSpace($space)) {
return null;
}
$link = self::hasLinkTable() ? AnimalGalleryLink::findOne(['animal_id' => (int)$animal->id]) : null;
$gallery = null;
if ($link instanceof AnimalGalleryLink) {
$gallery = CustomGallery::find()
->contentContainer($space)
->where(['gallery_gallery.id' => (int)$link->gallery_id])
->one();
}
if (!$gallery instanceof CustomGallery) {
$gallery = CustomGallery::find()
->contentContainer($space)
->where(['like', 'description', self::buildMarker((int)$animal->id)])
->one();
if (!$gallery instanceof CustomGallery) {
$gallery = CustomGallery::find()
->contentContainer($space)
->where(['title' => self::buildTitle($animal)])
->one();
}
}
if (!$gallery instanceof CustomGallery) {
$gallery = new CustomGallery();
$gallery->content->container = $space;
$gallery->content->visibility = (int)$space->getDefaultContentVisibility();
$gallery->title = self::buildTitle($animal);
$gallery->description = self::buildDescription($animal);
try {
if (!$gallery->save()) {
Yii::warning($gallery->getErrors(), 'animal_management.gallery_integration.gallery_create_failed');
return null;
}
} catch (\Throwable $e) {
Yii::warning($e->getMessage(), 'animal_management.gallery_integration.gallery_create_failed');
return null;
}
} else {
$newTitle = self::buildTitle($animal);
$newDescription = self::buildDescription($animal);
$dirty = false;
if ((string)$gallery->title !== $newTitle) {
$gallery->title = $newTitle;
$dirty = true;
}
if ((string)$gallery->description !== $newDescription) {
$gallery->description = $newDescription;
$dirty = true;
}
if ($dirty) {
try {
$gallery->save();
} catch (\Throwable $e) {
Yii::warning($e->getMessage(), 'animal_management.gallery_integration.gallery_update_failed');
}
}
}
if (self::hasLinkTable()) {
if (!$link instanceof AnimalGalleryLink) {
$link = new AnimalGalleryLink();
$link->animal_id = (int)$animal->id;
}
$link->gallery_id = (int)$gallery->id;
$link->contentcontainer_id = (int)$space->contentcontainer_id;
try {
if (!$link->save()) {
Yii::warning($link->getErrors(), 'animal_management.gallery_integration.link_save_failed');
}
} catch (\Throwable $e) {
Yii::warning($e->getMessage(), 'animal_management.gallery_integration.link_save_failed');
}
}
self::refreshGalleryThumb($animal, $gallery);
return $gallery;
}
public static function isAnimalBackedGallery(CustomGallery $gallery): bool
{
return self::getAnimalByGalleryId((int)$gallery->id) instanceof Animal;
}
public static function getAnimalByGalleryId(int $galleryId): ?Animal
{
if (!self::hasRequiredTables()) {
return null;
}
if (self::hasLinkTable()) {
$link = AnimalGalleryLink::findOne(['gallery_id' => $galleryId]);
if ($link instanceof AnimalGalleryLink) {
return Animal::findOne(['id' => (int)$link->animal_id]);
}
}
$gallery = CustomGallery::findOne(['id' => $galleryId]);
if (!$gallery instanceof CustomGallery) {
return null;
}
$animalId = self::extractAnimalIdFromDescription((string)$gallery->description);
if ($animalId === null) {
$animalId = self::extractAnimalIdFromTitle($gallery);
if ($animalId === null) {
return null;
}
}
return Animal::findOne(['id' => $animalId]);
}
public static function getAnimalItemsQuery(CustomGallery $gallery)
{
$animal = self::getAnimalByGalleryId((int)$gallery->id);
if (!$animal instanceof Animal) {
Yii::warning([
'gallery_id' => (int)$gallery->id,
'title' => (string)$gallery->title,
'description' => (string)$gallery->description,
], 'animal_management.gallery_integration.gallery_unmapped');
return null;
}
return AnimalGalleryItem::find()
->where(['animal_id' => (int)$animal->id])
->andWhere([
'or',
['and', ['not', ['file_path' => null]], ['!=', 'file_path', '']],
['not', ['file_id' => null]],
])
->orderBy(['id' => SORT_DESC]);
}
public static function getAnimalPreviewUrl(CustomGallery $gallery): ?string
{
$animal = self::getAnimalByGalleryId((int)$gallery->id);
if (!$animal instanceof Animal) {
return null;
}
$item = AnimalGalleryItem::find()
->where(['animal_id' => (int)$animal->id])
->orderBy(['id' => SORT_DESC])
->one();
if (!$item instanceof AnimalGalleryItem) {
return null;
}
$url = trim((string)$item->getImageUrl());
return $url !== '' ? $url : null;
}
public static function isAnimalGalleryEmpty(CustomGallery $gallery): ?bool
{
$animal = self::getAnimalByGalleryId((int)$gallery->id);
if (!$animal instanceof Animal) {
return null;
}
return !AnimalGalleryItem::find()->where(['animal_id' => (int)$animal->id])->exists();
}
private static function refreshGalleryThumb(Animal $animal, CustomGallery $gallery): void
{
$thumbFileId = AnimalGalleryItem::find()
->where(['animal_id' => (int)$animal->id])
->andWhere(['not', ['file_id' => null]])
->orderBy(['id' => SORT_DESC])
->select('file_id')
->scalar();
$thumbFileId = $thumbFileId === null ? null : (int)$thumbFileId;
if ((int)($gallery->thumb_file_id ?? 0) === (int)($thumbFileId ?? 0)) {
return;
}
$gallery->thumb_file_id = $thumbFileId;
$gallery->save(false, ['thumb_file_id']);
}
private static function hasRequiredTables(): bool
{
$schema = Yii::$app->db->schema;
return $schema->getTableSchema('rescue_animal', true) !== null
&& $schema->getTableSchema('rescue_animal_gallery_item', true) !== null
&& $schema->getTableSchema('gallery_gallery', true) !== null;
}
private static function hasLinkTable(): bool
{
return Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_link', true) !== null;
}
private static function buildTitle(Animal $animal): string
{
return $animal->getDisplayName() . ' Gallery';
}
private static function buildDescription(Animal $animal): string
{
return self::buildMarker((int)$animal->id);
}
private static function buildMarker(int $animalId): string
{
return '[animal-gallery:' . $animalId . ']';
}
private static function extractAnimalIdFromDescription(string $description): ?int
{
if (!preg_match('/\[animal-gallery:(\d+)\]/', $description, $matches)) {
return null;
}
return isset($matches[1]) ? (int)$matches[1] : null;
}
private static function extractAnimalIdFromTitle(CustomGallery $gallery): ?int
{
$container = $gallery->content->container ?? null;
if (!$container instanceof Space) {
return null;
}
$title = trim((string)$gallery->title);
if ($title === '') {
return null;
}
$titleNormalized = self::normalizeTitleToken($title);
if ($titleNormalized === '') {
return null;
}
$animals = Animal::find()
->where(['contentcontainer_id' => (int)$container->contentcontainer_id])
->orderBy(['id' => SORT_ASC])
->all();
foreach ($animals as $animal) {
if (!$animal instanceof Animal) {
continue;
}
$nameNormalized = self::normalizeTitleToken((string)$animal->name);
if ($nameNormalized !== '' && ($nameNormalized === $titleNormalized || strpos($titleNormalized, $nameNormalized) !== false)) {
return (int)$animal->id;
}
$uidNormalized = self::normalizeTitleToken((string)$animal->animal_uid);
if ($uidNormalized !== '' && ($uidNormalized === $titleNormalized || strpos($titleNormalized, $uidNormalized) !== false)) {
return (int)$animal->id;
}
}
return null;
}
private static function normalizeTitleToken(string $value): string
{
$value = strtolower(trim($value));
if ($value === '') {
return '';
}
$value = preg_replace('/\bgallery\b/i', ' ', $value);
$value = preg_replace('/[^a-z0-9]+/i', ' ', (string)$value);
return trim((string)$value);
}
}

93
views/animals/_tile.php Normal file
View File

@@ -0,0 +1,93 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\helpers\DateDisplayHelper;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\content\components\ContentContainerActiveRecord;
use yii\helpers\Html;
/* @var Animal $animal */
/* @var ContentContainerActiveRecord $contentContainer */
/* @var AnimalMedicalVisit|null $lastMedical */
/* @var string $imageUrl */
/* @var array $tileFields */
/* @var bool $showMedicalIcon */
$showMedicalIcon = isset($showMedicalIcon) ? (bool)$showMedicalIcon : true;
$imageUrl = trim((string)$imageUrl);
$hasImage = $imageUrl !== '' && (preg_match('/^https?:\/\//i', $imageUrl) || substr($imageUrl, 0, 1) === '/');
$statusLabel = Animal::statusOptions()[$animal->status] ?? (string)$animal->status;
$fieldMap = [
'name' => (string)$animal->getDisplayName(),
'species' => (string)$animal->species,
'breed' => (string)$animal->breed,
'sex' => (string)$animal->sex,
'status' => (string)$statusLabel,
'location_name' => (string)$animal->location_name,
'animal_uid' => (string)$animal->animal_uid,
'public_summary' => trim((string)$animal->public_summary),
'last_medical' => $lastMedical instanceof AnimalMedicalVisit ? DateDisplayHelper::format((string)$lastMedical->visit_at) : '',
];
$tileFields = is_array($tileFields) ? $tileFields : [];
$selectedMeta = [];
foreach ($tileFields as $fieldKey) {
$fieldKey = trim((string)$fieldKey);
if ($fieldKey === '' || $fieldKey === 'name' || !array_key_exists($fieldKey, $fieldMap)) {
continue;
}
$value = trim((string)$fieldMap[$fieldKey]);
if ($value === '') {
continue;
}
$selectedMeta[] = $value;
}
$summaryText = trim((string)$fieldMap['public_summary']);
if ($summaryText !== '') {
$summaryText = substr($summaryText, 0, 160) . (strlen($summaryText) > 160 ? '...' : '');
}
$animalViewUrl = $contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id]);
$medicalUrl = $contentContainer->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id]);
?>
<div class="panel panel-default" style="margin-bottom:0;overflow:hidden;border-radius:12px;border:0;box-shadow:0 8px 24px rgba(15,23,42,0.14);">
<div style="display:block;position:relative;aspect-ratio:4 / 3;background:#d8dee8;">
<a href="<?= Html::encode($animalViewUrl) ?>" aria-label="<?= Html::encode($animal->getDisplayName()) ?>" style="position:absolute;inset:0;z-index:1;"></a>
<?php if ($hasImage): ?>
<img src="<?= Html::encode($imageUrl) ?>" alt="<?= Html::encode($animal->getDisplayName()) ?>" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;">
<?php endif; ?>
<div style="position:absolute;inset:0;background:linear-gradient(180deg, rgba(7,10,16,0.05) 0%, rgba(7,10,16,0.62) 58%, rgba(7,10,16,0.78) 100%);"></div>
<div style="position:absolute;left:12px;right:12px;bottom:12px;z-index:2;color:#fff;pointer-events:none;">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px;">
<span style="font-size:20px;font-weight:700;line-height:1.15;text-shadow:0 2px 8px rgba(0,0,0,0.4);color:#fff;">
<?= Html::encode($animal->getDisplayName()) ?>
</span>
<?php if ($showMedicalIcon): ?>
<a href="<?= Html::encode($medicalUrl) ?>" class="btn btn-xs btn-default" title="<?= Yii::t('AnimalManagementModule.base', 'Medical Visits') ?>" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Medical Visits') ?>" style="border-radius:999px;padding:6px 8px;background:rgba(255,255,255,0.92);border:0;position:relative;z-index:3;pointer-events:auto;">
<i class="fa fa-heartbeat"></i>
</a>
<?php endif; ?>
</div>
<?php if (!empty($selectedMeta)): ?>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px;">
<?php foreach ($selectedMeta as $metaValue): ?>
<span style="display:inline-block;background:rgba(15,23,42,0.58);border:1px solid rgba(255,255,255,0.28);padding:2px 8px;border-radius:999px;font-size:12px;">
<?= Html::encode($metaValue) ?>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($summaryText !== ''): ?>
<div style="font-size:12px;line-height:1.35;color:rgba(255,255,255,0.92);text-shadow:0 1px 4px rgba(0,0,0,0.5);">
<?= Html::encode($summaryText) ?>
</div>
<?php endif; ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,130 @@
<?php
use humhub\modules\animal_management\models\AnimalTransfer;
use humhub\modules\space\models\Space;
use yii\helpers\Html;
/* @var AnimalTransfer $transfer */
/* @var Space $space */
/* @var string $otherRescueName */
/* @var string $otherRescueUrl */
/* @var string $animalProfileUrl */
/* @var string $imageUrl */
/* @var bool $isIncoming */
$animalName = $transfer->animal ? $transfer->animal->getDisplayName() : ('#' . (int)$transfer->animal_id);
$statusLabel = AnimalTransfer::statusOptions()[$transfer->status] ?? (string)$transfer->status;
$hasImage = $imageUrl !== '' && (preg_match('/^https?:\/\//i', $imageUrl) || substr($imageUrl, 0, 1) === '/');
$statusTextColor = '#ffffff';
switch ($transfer->status) {
case AnimalTransfer::STATUS_REQUESTED:
$statusTextColor = '#facc15';
break;
case AnimalTransfer::STATUS_ACCEPTED:
$statusTextColor = '#4ade80';
break;
case AnimalTransfer::STATUS_COMPLETED:
$statusTextColor = '#60a5fa';
break;
case AnimalTransfer::STATUS_DECLINED:
$statusTextColor = '#fca5a5';
break;
case AnimalTransfer::STATUS_CANCELLED:
$statusTextColor = '#d1d5db';
break;
}
?>
<div class="panel panel-default" style="margin-bottom:0;overflow:hidden;border-radius:12px;border:0;box-shadow:0 8px 24px rgba(15,23,42,0.14);">
<div style="position:relative;min-height:190px;background:#d8dee8;">
<?php if ($hasImage): ?>
<img src="<?= Html::encode($imageUrl) ?>" alt="<?= Html::encode($animalName) ?>" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#8d98a5;">
<i class="fa fa-paw fa-3x"></i>
</div>
<?php endif; ?>
<div style="position:absolute;inset:0;background:linear-gradient(180deg, rgba(7,10,16,0.08) 0%, rgba(7,10,16,0.6) 58%, rgba(7,10,16,0.82) 100%);"></div>
<div style="position:absolute;top:12px;left:12px;z-index:2;max-width:60%;">
<?php if (!empty($otherRescueUrl)): ?>
<?= Html::a(
Html::encode($otherRescueName),
$otherRescueUrl,
[
'style' => 'display:inline-block;background:rgba(15,23,42,0.66);border:1px solid rgba(255,255,255,0.28);color:#fff;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;text-decoration:none;'
]
) ?>
<?php else: ?>
<span style="display:inline-block;background:rgba(15,23,42,0.66);border:1px solid rgba(255,255,255,0.28);color:#fff;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;">
<?= Html::encode($otherRescueName) ?>
</span>
<?php endif; ?>
</div>
<div style="position:absolute;top:12px;right:12px;z-index:2;display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;max-width:65%;">
<?php if ($isIncoming && $transfer->status === AnimalTransfer::STATUS_REQUESTED): ?>
<?= Html::a(
Yii::t('AnimalManagementModule.base', 'Accept'),
$space->createUrl('/animal_management/animals/transfer-respond', ['id' => $transfer->id, 'decision' => 'accept']),
[
'class' => 'btn btn-xs btn-default',
'data-method' => 'post',
'style' => 'border-radius:999px;border:0;background:rgba(255,255,255,0.92);font-weight:600;'
]
) ?>
<?= Html::a(
Yii::t('AnimalManagementModule.base', 'Decline'),
$space->createUrl('/animal_management/animals/transfer-respond', ['id' => $transfer->id, 'decision' => 'decline']),
[
'class' => 'btn btn-xs btn-default',
'data-method' => 'post',
'style' => 'border-radius:999px;border:0;background:rgba(255,255,255,0.92);font-weight:600;'
]
) ?>
<?php elseif ($isIncoming && $transfer->status === AnimalTransfer::STATUS_ACCEPTED): ?>
<?= Html::a(
Yii::t('AnimalManagementModule.base', 'Complete Transfer'),
$space->createUrl('/animal_management/animals/transfer-complete', ['id' => $transfer->id]),
[
'class' => 'btn btn-xs btn-default',
'data-method' => 'post',
'style' => 'border-radius:999px;border:0;background:rgba(255,255,255,0.92);font-weight:600;'
]
) ?>
<?php elseif (!$isIncoming && in_array($transfer->status, [AnimalTransfer::STATUS_REQUESTED, AnimalTransfer::STATUS_ACCEPTED], true)): ?>
<?= Html::a(
Yii::t('AnimalManagementModule.base', 'Cancel Request'),
$space->createUrl('/animal_management/animals/transfer-cancel', ['id' => $transfer->id]),
[
'class' => 'btn btn-xs btn-default',
'data-method' => 'post',
'style' => 'border-radius:999px;border:0;background:rgba(255,255,255,0.92);font-weight:600;'
]
) ?>
<?php endif; ?>
</div>
<div style="position:absolute;left:12px;right:12px;bottom:12px;z-index:2;color:#fff;">
<div style="display:flex;align-items:flex-end;justify-content:space-between;gap:10px;">
<div style="font-size:24px;font-weight:700;line-height:1.15;text-shadow:0 2px 8px rgba(0,0,0,0.45);">
<?php if (!empty($animalProfileUrl)): ?>
<?= Html::a(
Html::encode($animalName),
$animalProfileUrl,
[
'style' => 'color:#fff;text-decoration:none;'
]
) ?>
<?php else: ?>
<?= Html::encode($animalName) ?>
<?php endif; ?>
</div>
<span style="display:inline-block;background:rgba(15,23,42,0.7);border:1px solid rgba(255,255,255,0.22);padding:5px 12px;border-radius:999px;font-size:14px;font-weight:800;letter-spacing:0.02em;color:<?= Html::encode($statusTextColor) ?>;text-transform:uppercase;white-space:nowrap;">
<?= Html::encode($statusLabel) ?>
</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,518 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\forms\AnimalMedicalVisitForm;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
use yii\helpers\Json;
/* @var Space $space */
/* @var Animal $animal */
/* @var AnimalMedicalVisitForm $model */
/* @var string $returnTo */
/* @var AnimalGalleryItem[] $galleryItems */
/* @var bool $isInline */
$isInline = isset($isInline) ? (bool)$isInline : false;
$hiddenMedicalKeys = [
'second_physician_name',
'second_physician_business_name',
'second_physician_street_address',
'second_physician_city',
'second_physician_state',
'second_physician_zip',
'second_physician_cell_phone',
'second_physician_business_phone',
'second_physician_license_number',
'previous_physicians',
];
$renderCustomField = static function (string $fieldKey, AnimalMedicalVisitForm $formModel, array $definitions): string {
if (!isset($definitions[$fieldKey])) {
return '';
}
$definition = $definitions[$fieldKey];
$inputType = (string)$definition['input_type'];
$vitalLabelOverrides = [
'blood_pressure' => 'BP',
'oxygen' => 'O₂',
];
$label = (string)($vitalLabelOverrides[$fieldKey] ?? $definition['label']);
if ((int)$definition['required'] === 1) {
$label .= ' *';
}
$fieldName = "AnimalMedicalVisitForm[customFields][$fieldKey]";
$fieldValue = $formModel->customFields[$fieldKey] ?? '';
ob_start();
?>
<?php if ($inputType === 'textarea'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textarea($fieldName, (string)$fieldValue, ['class' => 'form-control', 'rows' => 3, 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'boolean'): ?>
<div class="checkbox" style="margin-bottom:10px;">
<label>
<?= Html::hiddenInput($fieldName, '0') ?>
<?= Html::checkbox($fieldName, !empty($fieldValue), ['value' => '1']) ?>
<?= Html::encode($label) ?>
</label>
</div>
<?php elseif ($inputType === 'select'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::dropDownList(
$fieldName,
(string)$fieldValue,
$formModel->getCustomFieldSelectOptions($fieldKey),
['class' => 'form-control', 'prompt' => Yii::t('AnimalManagementModule.base', 'Select...'), 'id' => "animalmedicalvisitform-customfields-$fieldKey"]
) ?>
</div>
<?php elseif ($inputType === 'number'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('number', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'step' => 'any', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'date'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('date', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'datetime'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('datetime-local', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php else: ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textInput($fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php endif; ?>
<?php
return (string)ob_get_clean();
};
$customDefinitions = $model->getCustomFieldDefinitions();
$knownMedicalKeys = [
'weight',
'pulse',
'blood_pressure',
'oxygen',
'chronic_conditions',
'acute_conditions',
'special_needs',
'date_of_most_recent_medical_visit',
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
'medical_media_reference',
'media_reference',
];
$remainingDefinitions = [];
foreach ($customDefinitions as $fieldKey => $definition) {
if (in_array($fieldKey, $knownMedicalKeys, true) || in_array($fieldKey, $hiddenMedicalKeys, true)) {
continue;
}
$remainingDefinitions[$fieldKey] = $definition;
}
$medicalMediaPath = trim((string)($model->customFields['medical_media_reference'] ?? $model->customFields['media_reference'] ?? ''));
$hasMedicalMedia = $medicalMediaPath !== '' && (preg_match('/^https?:\/\//i', $medicalMediaPath) || substr($medicalMediaPath, 0, 1) === '/');
$medicalGalleryModalId = 'add-medical-media-gallery-modal';
$medicalFormId = 'add-medical-visit-inline-form';
$this->registerCss(<<<CSS
.inline-add-shell.panel {
position: relative;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 12px;
background: rgba(10, 18, 28, 0.36);
background-size: cover;
background-position: center;
}
.inline-add-shell.panel.has-media::before {
content: '';
position: absolute;
inset: 0;
background: rgba(10, 18, 28, 0.22);
pointer-events: none;
}
.inline-add-shell > .panel-body {
position: relative;
z-index: 1;
background: rgba(10, 18, 28, 0.2);
}
.inline-add-shell .panel.panel-default {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(10, 18, 28, 0.34);
}
.inline-add-shell .panel.panel-default > .panel-heading {
color: #eef5fb;
background: rgba(10, 18, 28, 0.42);
border-color: rgba(255, 255, 255, 0.2);
}
.inline-add-shell,
.inline-add-shell .panel-body,
.inline-add-shell .control-label,
.inline-add-shell .checkbox label,
.inline-add-shell .radio label,
.inline-add-shell .help-block {
color: #eef5fb;
}
.inline-add-shell .text-muted {
color: rgba(233, 242, 250, 0.78) !important;
}
.inline-add-shell .form-control {
background: rgba(10, 18, 28, 0.56);
border-color: rgba(255, 255, 255, 0.44);
color: #f3f8ff;
}
.inline-add-shell .form-control::placeholder {
color: rgba(243, 248, 255, 0.72);
}
.inline-add-shell .form-control[readonly],
.inline-add-shell .form-control[disabled] {
background: rgba(10, 18, 28, 0.42);
color: rgba(243, 248, 255, 0.72);
}
.inline-add-shell select.form-control option {
color: #0f1b2a;
}
CSS
);
if ($isInline) {
$this->registerCss(<<<CSS
html, body {
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
}
body > .panel:first-child {
margin-top: 0 !important;
}
CSS
);
}
?>
<style>
.medical-media-select-thumb.is-selected {
border-color: #1f8dd6;
box-shadow: 0 0 0 2px rgba(31, 141, 214, 0.2);
}
</style>
<div class="panel panel-default inline-add-shell<?= $hasMedicalMedia ? ' has-media' : '' ?>" id="medical-inline-add-shell"<?= $hasMedicalMedia ? ' style="background-image:url(' . Html::encode($medicalMediaPath) . ');"' : '' ?>>
<div class="panel-body">
<?php
$formOptions = ['id' => $medicalFormId, 'enctype' => 'multipart/form-data'];
if (!$isInline) {
$formOptions['target'] = '_top';
}
$form = ActiveForm::begin(['options' => $formOptions]);
?>
<?= Html::hiddenInput('returnTo', (string)($returnTo ?? 'medical-visits')) ?>
<?= Html::hiddenInput('medicalMediaGalleryPath', $medicalMediaPath, ['id' => 'medical-media-gallery-path']) ?>
<?php if ($isInline): ?>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-bottom:10px;">
<?= Html::submitButton('<i class="fa fa-check"></i>', [
'class' => 'btn btn-default btn-sm',
'title' => Yii::t('AnimalManagementModule.base', 'Save Medical Visit'),
'form' => $medicalFormId,
]) ?>
<?= Html::button('<i class="fa fa-times"></i>', [
'type' => 'button',
'class' => 'btn btn-default btn-sm',
'id' => 'medical-inline-add-cancel-icon',
'title' => Yii::t('AnimalManagementModule.base', 'Cancel'),
]) ?>
</div>
<?php endif; ?>
<?= $form->errorSummary($model, ['showAllErrors' => true]) ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Media') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-4" style="margin-bottom:8px;">
<div id="medical-media-preview" style="border-radius:8px;overflow:hidden;background:#f2f4f6;height:150px;display:flex;align-items:center;justify-content:center;">
<?php if ($hasMedicalMedia): ?>
<img src="<?= Html::encode($medicalMediaPath) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Selected medical media') ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>
<?php endif; ?>
</div>
</div>
<div class="col-sm-8">
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#<?= Html::encode($medicalGalleryModalId) ?>" style="margin-bottom:8px;">
<i class="fa fa-photo"></i> <?= Yii::t('AnimalManagementModule.base', 'Choose from Gallery or Upload') ?>
</button>
<div class="checkbox" style="margin-top:0;">
<label>
<input type="checkbox" name="removeMedicalMedia" value="1">
<?= Yii::t('AnimalManagementModule.base', 'Remove selected media') ?>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Medical Visit') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6"><?= $form->field($model, 'visit_at')->input('datetime-local') ?></div>
<div class="col-sm-6"><?= $form->field($model, 'provider_name') ?></div>
</div>
<?= $form->field($model, 'notes')->textarea(['rows' => 3]) ?>
<?= $form->field($model, 'recommendations')->textarea(['rows' => 3]) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Vitals') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-3"><?= $renderCustomField('weight', $model, $customDefinitions) ?></div>
<div class="col-sm-3"><?= $renderCustomField('pulse', $model, $customDefinitions) ?></div>
<div class="col-sm-3"><?= $renderCustomField('blood_pressure', $model, $customDefinitions) ?></div>
<div class="col-sm-3"><?= $renderCustomField('oxygen', $model, $customDefinitions) ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Conditions') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('chronic_conditions', $model, $customDefinitions) ?>
<?= $renderCustomField('acute_conditions', $model, $customDefinitions) ?>
<?= $renderCustomField('special_needs', $model, $customDefinitions) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Medical Visit Detail') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('date_of_most_recent_medical_visit', $model, $customDefinitions) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Physician') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6"><?= $renderCustomField('physician_name', $model, $customDefinitions) ?></div>
<div class="col-sm-6"><?= $renderCustomField('physician_business_name', $model, $customDefinitions) ?></div>
<div class="col-sm-12"><?= $renderCustomField('physician_street_address', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_city', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_state', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_zip', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_cell_phone', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_business_phone', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_license_number', $model, $customDefinitions) ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Social Post') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $form->field($model, 'post_to_space_feed')->checkbox() ?>
<?= $form->field($model, 'post_to_animal_feed')->checkbox() ?>
</div>
</div>
<?php if (!empty($remainingDefinitions)): ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Additional Details') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?php foreach ($remainingDefinitions as $fieldKey => $definition): ?>
<?= $renderCustomField($fieldKey, $model, $remainingDefinitions) ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?= Button::save(Yii::t('AnimalManagementModule.base', 'Save Medical Visit'))->submit() ?>
<?php if ($isInline): ?>
<?= Html::button(Yii::t('AnimalManagementModule.base', 'Cancel'), [
'type' => 'button',
'class' => 'btn btn-default',
'id' => 'medical-inline-add-cancel',
]) ?>
<?php else: ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Cancel'))
->link(($returnTo ?? 'medical-visits') === 'medical-visits'
? $space->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id])
: $space->createUrl('/animal_management/animals/view', ['id' => $animal->id])) ?>
<?php endif; ?>
<?php ActiveForm::end(); ?>
</div>
</div>
<div class="modal fade" id="<?= Html::encode($medicalGalleryModalId) ?>" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Select Medical Media from Gallery') ?></h4>
</div>
<div class="modal-body">
<?php if (empty($galleryItems)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No gallery images available yet.') ?></div>
<?php else: ?>
<div class="row" style="max-height:280px;overflow:auto;margin-bottom:10px;">
<?php foreach ($galleryItems as $galleryItem): ?>
<?php $galleryUrl = trim((string)$galleryItem->getImageUrl()); ?>
<?php if ($galleryUrl === '') { continue; } ?>
<div class="col-xs-6 col-sm-4" style="margin-bottom:8px;">
<button type="button" class="btn btn-default medical-media-select-thumb<?= $medicalMediaPath === $galleryUrl ? ' is-selected' : '' ?>" data-media-url="<?= Html::encode($galleryUrl) ?>" style="width:100%;padding:3px;">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Gallery image') ?>" style="width:100%;height:120px;object-fit:cover;display:block;">
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="form-group" style="margin-bottom:0;">
<label class="control-label" for="medicalMediaUpload"><?= Yii::t('AnimalManagementModule.base', 'Upload New Image') ?></label>
<input type="file" class="form-control" id="medicalMediaUpload" name="medicalMediaUpload" form="<?= Html::encode($medicalFormId) ?>" accept="image/*">
</div>
</div>
</div>
</div>
</div>
<?php
$this->registerJs(<<<JS
(function(){
function escapeCssUrl(source) {
return String(source || '').replace(/"/g, '\\"');
}
function setMedicalShellBackground(source) {
var shell = $('#medical-inline-add-shell');
if (!shell.length) {
return;
}
if (source) {
shell.addClass('has-media').css('background-image', 'url("' + escapeCssUrl(source) + '")');
} else {
shell.removeClass('has-media').css('background-image', 'none');
}
}
function renderMedicalPreview(source) {
var preview = $('#medical-media-preview');
if (!preview.length) {
return;
}
if (source) {
preview.html('<img src="' + source + '" alt="Selected medical media" style="width:100%;height:100%;object-fit:cover;">');
} else {
preview.html('<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>');
}
setMedicalShellBackground(source);
}
function markSelectedMedicalThumb(value) {
$('.medical-media-select-thumb').removeClass('is-selected');
if (!value) {
return;
}
$('.medical-media-select-thumb').each(function() {
if ($(this).data('media-url') === value) {
$(this).addClass('is-selected');
}
});
}
$(document).on('click', '.medical-media-select-thumb', function() {
var mediaUrl = $(this).data('media-url');
$('#medical-media-gallery-path').val(mediaUrl);
markSelectedMedicalThumb(mediaUrl);
$('#medicalMediaUpload').val('');
$('input[name="removeMedicalMedia"]').prop('checked', false);
if (mediaUrl) {
renderMedicalPreview(mediaUrl);
}
$('#{$medicalGalleryModalId}').modal('hide');
});
$('#medicalMediaUpload').on('change', function() {
var file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
return;
}
$('#medical-media-gallery-path').val('');
markSelectedMedicalThumb('');
$('input[name="removeMedicalMedia"]').prop('checked', false);
var reader = new FileReader();
reader.onload = function(e) {
renderMedicalPreview(e.target.result);
$('#{$medicalGalleryModalId}').modal('hide');
};
reader.readAsDataURL(file);
});
$('#{$medicalGalleryModalId}').on('shown.bs.modal', function() {
markSelectedMedicalThumb($('#medical-media-gallery-path').val());
});
})();
JS
, \yii\web\View::POS_END);
if ($isInline) {
$cancelPayload = Json::htmlEncode([
'source' => 'animal-inline-editor',
'type' => 'cancel',
'collapseId' => 'medical-add-inline',
]);
$this->registerJs(<<<JS
$(document).on('click', '#medical-inline-add-cancel, #medical-inline-add-cancel-icon', function() {
if (window.parent && window.parent !== window) {
window.parent.postMessage($cancelPayload, '*');
}
});
JS
, \yii\web\View::POS_END);
}
?>

View File

@@ -0,0 +1,445 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\forms\AnimalProgressUpdateForm;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
use yii\helpers\Json;
/* @var Space $space */
/* @var Animal $animal */
/* @var AnimalProgressUpdateForm $model */
/* @var string $returnTo */
/* @var AnimalGalleryItem[] $galleryItems */
/* @var bool $isInline */
$isInline = isset($isInline) ? (bool)$isInline : false;
$renderCustomField = static function (string $fieldKey, AnimalProgressUpdateForm $formModel, array $definitions): string {
if (!isset($definitions[$fieldKey])) {
return '';
}
$definition = $definitions[$fieldKey];
$inputType = (string)$definition['input_type'];
$label = (string)$definition['label'];
if ((int)$definition['required'] === 1) {
$label .= ' *';
}
$fieldName = "AnimalProgressUpdateForm[customFields][$fieldKey]";
$fieldValue = $formModel->customFields[$fieldKey] ?? '';
ob_start();
?>
<?php if ($inputType === 'textarea'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textarea($fieldName, (string)$fieldValue, ['class' => 'form-control', 'rows' => 3, 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'boolean'): ?>
<div class="checkbox" style="margin-bottom:10px;">
<label>
<?= Html::hiddenInput($fieldName, '0') ?>
<?= Html::checkbox($fieldName, !empty($fieldValue), ['value' => '1']) ?>
<?= Html::encode($label) ?>
</label>
</div>
<?php elseif ($inputType === 'select'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::dropDownList(
$fieldName,
(string)$fieldValue,
$formModel->getCustomFieldSelectOptions($fieldKey),
['class' => 'form-control', 'prompt' => Yii::t('AnimalManagementModule.base', 'Select...'), 'id' => "animalprogressupdateform-customfields-$fieldKey"]
) ?>
</div>
<?php elseif ($inputType === 'number'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('number', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'step' => 'any', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'date'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('date', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'datetime'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('datetime-local', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php else: ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textInput($fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php endif; ?>
<?php
return (string)ob_get_clean();
};
$customDefinitions = $model->getCustomFieldDefinitions();
$knownProgressKeys = ['progress_notes', 'routine_updates', 'media_reference'];
$otherCustomDefinitions = [];
foreach ($customDefinitions as $fieldKey => $definition) {
if (in_array($fieldKey, $knownProgressKeys, true)) {
continue;
}
$otherCustomDefinitions[$fieldKey] = $definition;
}
$currentMediaReference = trim((string)($model->customFields['media_reference'] ?? ''));
$progressFormId = 'add-progress-update-inline-form';
$this->registerCss(<<<CSS
.inline-add-shell.panel {
position: relative;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 12px;
background: rgba(10, 18, 28, 0.36);
background-size: cover;
background-position: center;
}
.inline-add-shell.panel.has-media::before {
content: '';
position: absolute;
inset: 0;
background: rgba(10, 18, 28, 0.22);
pointer-events: none;
}
.inline-add-shell > .panel-body {
position: relative;
z-index: 1;
background: rgba(10, 18, 28, 0.2);
}
.inline-add-shell .panel.panel-default {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(10, 18, 28, 0.34);
}
.inline-add-shell .panel.panel-default > .panel-heading {
color: #eef5fb;
background: rgba(10, 18, 28, 0.42);
border-color: rgba(255, 255, 255, 0.2);
}
.inline-add-shell,
.inline-add-shell .panel-body,
.inline-add-shell .control-label,
.inline-add-shell .checkbox label,
.inline-add-shell .radio label,
.inline-add-shell .help-block {
color: #eef5fb;
}
.inline-add-shell .text-muted {
color: rgba(233, 242, 250, 0.78) !important;
}
.inline-add-shell .form-control {
background: rgba(10, 18, 28, 0.56);
border-color: rgba(255, 255, 255, 0.44);
color: #f3f8ff;
}
.inline-add-shell .form-control::placeholder {
color: rgba(243, 248, 255, 0.72);
}
.inline-add-shell .form-control[readonly],
.inline-add-shell .form-control[disabled] {
background: rgba(10, 18, 28, 0.42);
color: rgba(243, 248, 255, 0.72);
}
.inline-add-shell select.form-control option {
color: #0f1b2a;
}
CSS
);
if ($isInline) {
$this->registerCss(<<<CSS
html, body {
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
}
body > .panel:first-child {
margin-top: 0 !important;
}
CSS
);
}
?>
<div class="panel panel-default inline-add-shell<?= $currentMediaReference !== '' ? ' has-media' : '' ?>" id="progress-inline-add-shell"<?= $currentMediaReference !== '' ? ' style="background-image:url(' . Html::encode($currentMediaReference) . ');"' : '' ?>>
<div class="panel-body">
<?php
$formOptions = ['id' => $progressFormId, 'enctype' => 'multipart/form-data'];
if (!$isInline) {
$formOptions['target'] = '_top';
}
$form = ActiveForm::begin(['options' => $formOptions]);
?>
<?= Html::hiddenInput('returnTo', (string)($returnTo ?? 'progress-updates')) ?>
<?php if ($isInline): ?>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-bottom:10px;">
<?= Html::submitButton('<i class="fa fa-check"></i>', [
'class' => 'btn btn-default btn-sm',
'title' => Yii::t('AnimalManagementModule.base', 'Save Progress Update'),
'form' => $progressFormId,
]) ?>
<?= Html::button('<i class="fa fa-times"></i>', [
'type' => 'button',
'class' => 'btn btn-default btn-sm',
'id' => 'progress-inline-add-cancel-icon',
'title' => Yii::t('AnimalManagementModule.base', 'Cancel'),
]) ?>
</div>
<?php endif; ?>
<?= $form->errorSummary($model, ['showAllErrors' => true]) ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Media') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<input type="hidden" id="progress-media-gallery-path" name="progressMediaGalleryPath" value="<?= Html::encode($currentMediaReference) ?>">
<div class="row">
<div class="col-sm-4" style="margin-bottom:8px;">
<div id="progress-media-preview" style="border-radius:8px;overflow:hidden;background:#f2f4f6;height:150px;display:flex;align-items:center;justify-content:center;">
<?php if ($currentMediaReference !== '' && (preg_match('/^https?:\/\//i', $currentMediaReference) || substr($currentMediaReference, 0, 1) === '/')): ?>
<img src="<?= Html::encode($currentMediaReference) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Selected media') ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>
<?php endif; ?>
</div>
</div>
<div class="col-sm-8">
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#progress-media-modal" style="margin-bottom:8px;">
<i class="fa fa-photo"></i> <?= Yii::t('AnimalManagementModule.base', 'Choose from Gallery or Upload') ?>
</button>
<div class="checkbox" style="margin-top:0;">
<label>
<input type="checkbox" name="removeProgressMedia" value="1">
<?= Yii::t('AnimalManagementModule.base', 'Remove selected media') ?>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Progress Update') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-4"><?= $form->field($model, 'weight') ?></div>
<div class="col-sm-8"><?= $form->field($model, 'vitals')->textInput(['maxlength' => 255]) ?></div>
<div class="col-sm-6"><?= $form->field($model, 'behavior_notes')->textarea(['rows' => 2]) ?></div>
<div class="col-sm-6"><?= $form->field($model, 'medical_concerns')->textarea(['rows' => 2]) ?></div>
<div class="col-sm-6"><?= $form->field($model, 'meal_plan_changes')->textarea(['rows' => 2]) ?></div>
<div class="col-sm-6"><?= $form->field($model, 'housing_changes')->textarea(['rows' => 2]) ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Notes') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('progress_notes', $model, $customDefinitions) ?>
<?= $renderCustomField('routine_updates', $model, $customDefinitions) ?>
</div>
</div>
<?php if (!empty($otherCustomDefinitions)): ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Additional Details') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?php foreach ($otherCustomDefinitions as $fieldKey => $definition): ?>
<?= $renderCustomField($fieldKey, $model, $otherCustomDefinitions) ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Social Post') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $form->field($model, 'post_to_space_feed')->checkbox() ?>
<?= $form->field($model, 'post_to_animal_feed')->checkbox() ?>
</div>
</div>
<div class="modal fade" id="progress-media-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Select Progress Media') ?></h4>
</div>
<div class="modal-body">
<?php if (empty($galleryItems)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No gallery images available yet.') ?></div>
<?php else: ?>
<div class="row" style="max-height:280px;overflow:auto;margin-bottom:10px;">
<?php foreach ($galleryItems as $galleryItem): ?>
<?php $galleryUrl = trim((string)$galleryItem->getImageUrl()); ?>
<?php if ($galleryUrl === '') { continue; } ?>
<div class="col-xs-6 col-sm-4" style="margin-bottom:8px;">
<button type="button" class="btn btn-default progress-media-select-thumb<?= $currentMediaReference === $galleryUrl ? ' is-selected' : '' ?>" data-media-url="<?= Html::encode($galleryUrl) ?>" style="width:100%;padding:3px;">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Gallery image') ?>" style="width:100%;height:120px;object-fit:cover;border-radius:4px;">
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="form-group" style="margin-bottom:0;">
<label class="control-label" for="progressMediaUpload"><?= Yii::t('AnimalManagementModule.base', 'Upload New Image') ?></label>
<input type="file" class="form-control" id="progressMediaUpload" name="progressMediaUpload" accept="image/*">
</div>
</div>
</div>
</div>
</div>
<?= Button::save(Yii::t('AnimalManagementModule.base', 'Save Progress Update'))->submit() ?>
<?php if ($isInline): ?>
<?= Html::button(Yii::t('AnimalManagementModule.base', 'Cancel'), [
'type' => 'button',
'class' => 'btn btn-default',
'id' => 'progress-inline-add-cancel',
]) ?>
<?php else: ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Cancel'))
->link(($returnTo ?? 'progress-updates') === 'progress-updates'
? $space->createUrl('/animal_management/animals/progress-updates', ['id' => $animal->id])
: $space->createUrl('/animal_management/animals/view', ['id' => $animal->id])) ?>
<?php endif; ?>
<?php ActiveForm::end(); ?>
</div>
</div>
<?php
$this->registerCss(<<<CSS
.progress-media-select-thumb.is-selected {
border-color: #2f7df4 !important;
box-shadow: 0 0 0 2px rgba(47, 125, 244, 0.22);
}
CSS
);
$this->registerJs(<<<JS
(function() {
function escapeCssUrl(source) {
return String(source || '').replace(/"/g, '\\"');
}
function setProgressShellBackground(source) {
var shell = $('#progress-inline-add-shell');
if (!shell.length) {
return;
}
if (source) {
shell.addClass('has-media').css('background-image', 'url("' + escapeCssUrl(source) + '")');
} else {
shell.removeClass('has-media').css('background-image', 'none');
}
}
function renderProgressPreview(source) {
var preview = $('#progress-media-preview');
if (!preview.length) {
return;
}
if (source) {
preview.html('<img src="' + source + '" alt="Selected media" style="width:100%;height:100%;object-fit:cover;">');
} else {
preview.html('<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>');
}
setProgressShellBackground(source);
}
function markSelectedMediaThumb(value) {
$('.progress-media-select-thumb').removeClass('is-selected');
if (!value) {
return;
}
$('.progress-media-select-thumb').each(function() {
if (($(this).attr('data-media-url') || '') === value) {
$(this).addClass('is-selected');
}
});
}
$(document).off('click.addProgressMediaSelect', '.progress-media-select-thumb').on('click.addProgressMediaSelect', '.progress-media-select-thumb', function() {
var mediaUrl = $(this).attr('data-media-url') || '';
$('#progress-media-gallery-path').val(mediaUrl);
markSelectedMediaThumb(mediaUrl);
$('#progressMediaUpload').val('');
$('input[name="removeProgressMedia"]').prop('checked', false);
if (mediaUrl) {
renderProgressPreview(mediaUrl);
}
$('#progress-media-modal').modal('hide');
});
$(document).off('change.addProgressMediaUpload', '#progressMediaUpload').on('change.addProgressMediaUpload', '#progressMediaUpload', function() {
var file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
return;
}
$('#progress-media-gallery-path').val('');
markSelectedMediaThumb('');
$('input[name="removeProgressMedia"]').prop('checked', false);
var reader = new FileReader();
reader.onload = function(e) {
renderProgressPreview(e.target.result);
$('#progress-media-modal').modal('hide');
};
reader.readAsDataURL(file);
});
$(document).off('shown.bs.modal.addProgressMediaModal', '#progress-media-modal').on('shown.bs.modal.addProgressMediaModal', '#progress-media-modal', function() {
markSelectedMediaThumb($('#progress-media-gallery-path').val());
});
})();
JS
, \yii\web\View::POS_END);
if ($isInline) {
$cancelPayload = Json::htmlEncode([
'source' => 'animal-inline-editor',
'type' => 'cancel',
'collapseId' => 'progress-add-inline',
]);
$this->registerJs(<<<JS
$(document).on('click', '#progress-inline-add-cancel, #progress-inline-add-cancel-icon', function() {
if (window.parent && window.parent !== window) {
window.parent.postMessage($cancelPayload, '*');
}
});
JS
, \yii\web\View::POS_END);
}
?>

534
views/animals/create.php Normal file
View File

@@ -0,0 +1,534 @@
<?php
use humhub\modules\animal_management\models\forms\AnimalForm;
use humhub\modules\animal_management\models\forms\DisplaySettingsForm;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\rescue_foundation\components\UploadStandards;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
/* @var AnimalForm $model */
/* @var Space $space */
/* @var bool $isEdit */
/* @var Animal|null $animal */
?>
<div class="panel panel-default">
<div class="panel-heading">
<?php if (!empty($isEdit)): ?>
<?= Yii::t('AnimalManagementModule.base', '<strong>Edit Animal</strong> Profile') ?>
<?php else: ?>
<?= Yii::t('AnimalManagementModule.base', '<strong>New Animal</strong> Intake') ?>
<?php endif; ?>
</div>
<div class="panel-body">
<?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]); ?>
<?= $form->errorSummary($model, ['showAllErrors' => true]) ?>
<?php
$customDefinitions = $model->getCustomFieldDefinitions();
$galleryImageOptions = $model->getGalleryImageOptions();
$galleryImageUrls = array_keys($galleryImageOptions);
$knownProfileFieldKeys = [
'dob',
'age',
'rescue',
'lineage',
'backstory',
'previous_owner_user_id',
'previous_owner_name',
'previous_owner_business_name',
'previous_owner_street_address',
'previous_owner_city',
'previous_owner_state',
'previous_owner_zip',
'previous_owner_cell_phone',
'previous_owner_business_phone',
'previous_owner_email',
];
$renderCustomField = static function (string $fieldKey, AnimalForm $model, array $definitions): string {
if (!isset($definitions[$fieldKey])) {
return '';
}
$definition = $definitions[$fieldKey];
$inputType = (string)$definition['input_type'];
$label = (string)$definition['label'];
if ((int)$definition['required'] === 1) {
$label .= ' *';
}
$fieldName = "AnimalForm[customFields][$fieldKey]";
$fieldValue = $model->customFields[$fieldKey] ?? '';
ob_start();
?>
<?php if ($inputType === 'textarea'): ?>
<div class="form-group">
<label class="control-label" for="animalform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textarea($fieldName, (string)$fieldValue, ['class' => 'form-control', 'rows' => 3, 'id' => "animalform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'boolean'): ?>
<div class="checkbox" style="margin-bottom:10px;">
<label>
<?= Html::hiddenInput($fieldName, '0') ?>
<?= Html::checkbox($fieldName, !empty($fieldValue), ['value' => '1']) ?>
<?= Html::encode($label) ?>
</label>
</div>
<?php elseif ($inputType === 'select'): ?>
<div class="form-group">
<label class="control-label" for="animalform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::dropDownList(
$fieldName,
(string)$fieldValue,
$model->getCustomFieldSelectOptions($fieldKey),
['class' => 'form-control', 'prompt' => Yii::t('AnimalManagementModule.base', 'Select...'), 'id' => "animalform-customfields-$fieldKey"]
) ?>
</div>
<?php elseif ($inputType === 'number'): ?>
<div class="form-group">
<label class="control-label" for="animalform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('number', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'step' => 'any', 'id' => "animalform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'date'): ?>
<div class="form-group">
<label class="control-label" for="animalform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('date', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'datetime'): ?>
<div class="form-group">
<label class="control-label" for="animalform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('datetime-local', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalform-customfields-$fieldKey"]) ?>
</div>
<?php else: ?>
<div class="form-group">
<label class="control-label" for="animalform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textInput($fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalform-customfields-$fieldKey"]) ?>
</div>
<?php endif; ?>
<?php
return (string)ob_get_clean();
};
?>
<div class="panel panel-default" style="margin-bottom:14px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Profile & Cover Images') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="help-block" style="margin-top:0;">
<?= Yii::t('AnimalManagementModule.base', 'Tap the edit icon on either image to choose from gallery thumbnails or upload from device camera roll.') ?>
</div>
<div class="row">
<div class="col-sm-6" style="margin-bottom:10px;">
<div id="animal-cover-preview" style="position:relative;border-radius:10px;overflow:hidden;background:#eef1f4;min-height:180px;">
<?php if ($model->getExistingCoverImagePath()): ?>
<img src="<?= Html::encode($model->getExistingCoverImagePath()) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Cover Image') ?>" style="width:100%;height:220px;object-fit:cover;display:block;">
<?php else: ?>
<div style="height:220px;display:flex;align-items:center;justify-content:center;color:#9ba5af;">
<i class="fa fa-image fa-3x"></i>
</div>
<?php endif; ?>
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#animal-cover-image-manage-modal" style="position:absolute;top:10px;right:10px;border-radius:999px;">
<i class="fa fa-pencil"></i>
</button>
</div>
<div style="margin-top:6px;font-weight:600;"><?= Yii::t('AnimalManagementModule.base', 'Cover Image') ?></div>
</div>
<div class="col-sm-6" style="margin-bottom:10px;">
<div id="animal-profile-preview" style="position:relative;border-radius:10px;overflow:hidden;background:#eef1f4;min-height:180px;">
<?php if ($model->getExistingProfileImagePath()): ?>
<img src="<?= Html::encode($model->getExistingProfileImagePath()) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Profile Image') ?>" style="width:100%;height:220px;object-fit:cover;display:block;">
<?php else: ?>
<div style="height:220px;display:flex;align-items:center;justify-content:center;color:#9ba5af;">
<i class="fa fa-user-circle fa-3x"></i>
</div>
<?php endif; ?>
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#animal-profile-image-manage-modal" style="position:absolute;top:10px;right:10px;border-radius:999px;">
<i class="fa fa-pencil"></i>
</button>
</div>
<div style="margin-top:6px;font-weight:600;"><?= Yii::t('AnimalManagementModule.base', 'Profile Image') ?></div>
</div>
</div>
<div style="display:none;">
<?= $form->field($model, 'coverImageGalleryPath')->hiddenInput()->label(false) ?>
<?= $form->field($model, 'profileImageGalleryPath')->hiddenInput()->label(false) ?>
<?= $form->field($model, 'coverImageFile')->fileInput(['accept' => 'image/*']) ?>
<?= $form->field($model, 'profileImageFile')->fileInput(['accept' => 'image/*']) ?>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default" style="margin-bottom:14px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Details') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6"><?php if ($model->isFieldActive('name')) { echo $form->field($model, 'name')->textInput(['maxlength' => 190]); } ?></div>
<div class="col-sm-6"><?php if ($model->isFieldActive('species')) { echo $form->field($model, 'species')->textInput(['maxlength' => 120]); } ?></div>
<div class="col-sm-6"><?php if ($model->isFieldActive('breed')) { echo $form->field($model, 'breed')->textInput(['maxlength' => 120]); } ?></div>
<div class="col-sm-6"><?php if ($model->isFieldActive('sex')) { echo $form->field($model, 'sex')->textInput(['maxlength' => 32]); } ?></div>
<div class="col-sm-6"><?= $renderCustomField('dob', $model, $customDefinitions) ?></div>
<div class="col-sm-6"><?= $renderCustomField('age', $model, $customDefinitions) ?></div>
<div class="col-sm-6"><?php if ($model->isFieldActive('status')) { echo $form->field($model, 'status')->dropDownList($model->getStatusOptions()); } ?></div>
<div class="col-sm-6"><?php if ($model->isFieldActive('in_possession')) { echo $form->field($model, 'in_possession')->checkbox(); } ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:14px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Location') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6"><?= $renderCustomField('rescue', $model, $customDefinitions) ?></div>
<div class="col-sm-6"><?php if ($model->isFieldActive('location_name')) { echo $form->field($model, 'location_name')->textInput(['maxlength' => 120]); } ?></div>
<div class="col-sm-4"><?php if ($model->isFieldActive('city')) { echo $form->field($model, 'city')->textInput(['maxlength' => 120]); } ?></div>
<div class="col-sm-4"><?php if ($model->isFieldActive('state')) { echo $form->field($model, 'state')->textInput(['maxlength' => 2]); } ?></div>
<div class="col-sm-4"><?php if ($model->isFieldActive('zip')) { echo $form->field($model, 'zip')->textInput(['maxlength' => 10]); } ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:14px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'History') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('lineage', $model, $customDefinitions) ?>
<?= $renderCustomField('backstory', $model, $customDefinitions) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:14px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Public') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?php if ($model->isFieldActive('public_summary')): ?>
<?= $form->field($model, 'public_summary')->textarea(['rows' => 4]) ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default" style="margin-bottom:14px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Previous Owner') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6"><?= $renderCustomField('previous_owner_user_id', $model, $customDefinitions) ?></div>
<div class="col-sm-6"><?= $renderCustomField('previous_owner_name', $model, $customDefinitions) ?></div>
<div class="col-sm-12"><?= $renderCustomField('previous_owner_business_name', $model, $customDefinitions) ?></div>
<div class="col-sm-12"><?= $renderCustomField('previous_owner_street_address', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('previous_owner_city', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('previous_owner_state', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('previous_owner_zip', $model, $customDefinitions) ?></div>
<div class="col-sm-6"><?= $renderCustomField('previous_owner_cell_phone', $model, $customDefinitions) ?></div>
<div class="col-sm-6"><?= $renderCustomField('previous_owner_business_phone', $model, $customDefinitions) ?></div>
<div class="col-sm-12"><?= $renderCustomField('previous_owner_email', $model, $customDefinitions) ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:14px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Display Field Overrides') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6">
<label class="control-label" style="display:block;"><?= Yii::t('AnimalManagementModule.base', 'Tile Fields') ?></label>
<?= Html::checkboxList('AnimalForm[tileDisplayFields]', $model->tileDisplayFields, DisplaySettingsForm::fieldOptions(), ['separator' => '<br>']) ?>
</div>
<div class="col-sm-6">
<label class="control-label" style="display:block;"><?= Yii::t('AnimalManagementModule.base', 'Hero Fields') ?></label>
<?= Html::checkboxList('AnimalForm[heroDisplayFields]', $model->heroDisplayFields, DisplaySettingsForm::fieldOptions(), ['separator' => '<br>']) ?>
</div>
</div>
</div>
</div>
<?php
$remainingCustomFields = [];
foreach ($customDefinitions as $fieldKey => $definition) {
if (in_array($fieldKey, $knownProfileFieldKeys, true)) {
continue;
}
$remainingCustomFields[$fieldKey] = $definition;
}
?>
<?php if (!empty($remainingCustomFields)): ?>
<div class="panel panel-default" style="margin-bottom:14px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Custom Fields') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?php foreach ($remainingCustomFields as $fieldKey => $definition): ?>
<?= $renderCustomField($fieldKey, $model, $remainingCustomFields) ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?= Button::save(!empty($isEdit)
? Yii::t('AnimalManagementModule.base', 'Save Changes')
: Yii::t('AnimalManagementModule.base', 'Create Animal'))->submit() ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Cancel'))->link(
!empty($isEdit) && $animal instanceof Animal
? $space->createUrl('/animal_management/animals/view', ['id' => $animal->id])
: $space->createUrl('/animal_management/animals/index')
) ?>
<?php if (!empty($isEdit) && $animal instanceof Animal): ?>
<?= Html::a(
Yii::t('AnimalManagementModule.base', 'Delete Animal'),
$space->createUrl('/animal_management/animals/delete', ['id' => $animal->id]),
[
'class' => 'btn btn-danger pull-right',
'style' => 'margin-left:8px;',
'data-method' => 'post',
'data-confirm' => Yii::t('AnimalManagementModule.base', 'Delete {name}? This will remove the animal record and local gallery uploads. This cannot be undone.', ['name' => $animal->getDisplayName()]),
]
) ?>
<?php endif; ?>
<div class="modal fade" id="animal-cover-image-manage-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Manage Cover Image') ?></h4>
</div>
<div class="modal-body">
<div class="help-block"><?= Yii::t('AnimalManagementModule.base', 'Select an existing gallery image or upload a new cover image.') ?></div>
<div class="alert alert-info" style="padding:8px 10px;margin-bottom:10px;">
<?= Yii::t('AnimalManagementModule.base', 'Your image preview updates immediately. Final save happens when you click {action} at the bottom of this intake form.', [
'action' => !empty($isEdit)
? Yii::t('AnimalManagementModule.base', 'Save Changes')
: Yii::t('AnimalManagementModule.base', 'Create Animal'),
]) ?>
</div>
<?php if (!empty($galleryImageUrls)): ?>
<div class="row" style="max-height:260px;overflow:auto;margin-bottom:8px;">
<?php foreach ($galleryImageUrls as $galleryUrl): ?>
<div class="col-xs-6 col-sm-4" style="margin-bottom:8px;">
<button type="button" class="btn btn-default animal-image-select-thumb" data-select-kind="cover" data-select-target="#animalform-coverimagegallerypath" data-select-value="<?= Html::encode($galleryUrl) ?>" style="padding:3px;width:100%;">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Cover option') ?>" style="width:100%;height:120px;object-fit:cover;border-radius:4px;">
</button>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="text-muted" style="margin-bottom:8px;"><?= Yii::t('AnimalManagementModule.base', 'No gallery images available yet.') ?></div>
<?php endif; ?>
<div class="form-group">
<button type="button" class="btn btn-default" id="animal-cover-upload-trigger">
<i class="fa fa-upload"></i> <?= Yii::t('AnimalManagementModule.base', 'Upload New Cover Image') ?>
</button>
<div id="animal-cover-upload-file-name" class="help-block" style="margin-bottom:0;"></div>
</div>
<?= $form->field($model, 'removeCoverImage')->checkbox() ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><?= Yii::t('AnimalManagementModule.base', 'Done') ?></button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="animal-profile-image-manage-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Manage Profile Image') ?></h4>
</div>
<div class="modal-body">
<div class="help-block"><?= Yii::t('AnimalManagementModule.base', 'Select an existing gallery image or upload a new profile image.') ?></div>
<div class="alert alert-info" style="padding:8px 10px;margin-bottom:10px;">
<?= Yii::t('AnimalManagementModule.base', 'Your image preview updates immediately. Final save happens when you click {action} at the bottom of this intake form.', [
'action' => !empty($isEdit)
? Yii::t('AnimalManagementModule.base', 'Save Changes')
: Yii::t('AnimalManagementModule.base', 'Create Animal'),
]) ?>
</div>
<?php if (!empty($galleryImageUrls)): ?>
<div class="row" style="max-height:260px;overflow:auto;margin-bottom:8px;">
<?php foreach ($galleryImageUrls as $galleryUrl): ?>
<div class="col-xs-6 col-sm-4" style="margin-bottom:8px;">
<button type="button" class="btn btn-default animal-image-select-thumb" data-select-kind="profile" data-select-target="#animalform-profileimagegallerypath" data-select-value="<?= Html::encode($galleryUrl) ?>" style="padding:3px;width:100%;">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Profile option') ?>" style="width:100%;height:120px;object-fit:cover;border-radius:4px;">
</button>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="text-muted" style="margin-bottom:8px;"><?= Yii::t('AnimalManagementModule.base', 'No gallery images available yet.') ?></div>
<?php endif; ?>
<div class="form-group">
<button type="button" class="btn btn-default" id="animal-profile-upload-trigger">
<i class="fa fa-upload"></i> <?= Yii::t('AnimalManagementModule.base', 'Upload New Profile Image') ?>
</button>
<div id="animal-profile-upload-file-name" class="help-block" style="margin-bottom:0;"></div>
</div>
<?= $form->field($model, 'removeProfileImage')->checkbox() ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><?= Yii::t('AnimalManagementModule.base', 'Done') ?></button>
</div>
</div>
</div>
</div>
<?php ActiveForm::end(); ?>
</div>
</div>
<?php
$this->registerCss(<<<CSS
.animal-image-select-thumb.is-selected {
border-color: #2f7df4 !important;
box-shadow: 0 0 0 2px rgba(47, 125, 244, 0.22);
}
CSS
);
$this->registerJs(<<<JS
(function() {
function renderPlaceholder(kind) {
if (kind === 'profile') {
return '<div style="height:220px;display:flex;align-items:center;justify-content:center;color:#9ba5af;"><i class="fa fa-user-circle fa-3x"></i></div>';
}
return '<div style="height:220px;display:flex;align-items:center;justify-content:center;color:#9ba5af;"><i class="fa fa-image fa-3x"></i></div>';
}
function updatePreview(kind, value) {
var previewId = kind === 'profile' ? '#animal-profile-preview' : '#animal-cover-preview';
if (value && (value.indexOf('/') === 0 || /^https?:\/\//i.test(value) || /^data:image\//i.test(value))) {
$(previewId).html('<img src="' + value + '" alt="' + kind + ' image" style="width:100%;height:220px;object-fit:cover;display:block;">' +
'<button type="button" class="btn btn-default" data-toggle="modal" data-target="' + (kind === 'profile' ? '#animal-profile-image-manage-modal' : '#animal-cover-image-manage-modal') + '" style="position:absolute;top:10px;right:10px;border-radius:999px;"><i class="fa fa-pencil"></i></button>');
return;
}
$(previewId).html(renderPlaceholder(kind) +
'<button type="button" class="btn btn-default" data-toggle="modal" data-target="' + (kind === 'profile' ? '#animal-profile-image-manage-modal' : '#animal-cover-image-manage-modal') + '" style="position:absolute;top:10px;right:10px;border-radius:999px;"><i class="fa fa-pencil"></i></button>');
}
function markSelectedThumb(kind, value) {
var modalId = kind === 'profile' ? '#animal-profile-image-manage-modal' : '#animal-cover-image-manage-modal';
$(modalId + ' .animal-image-select-thumb').removeClass('is-selected');
if (!value) {
return;
}
$(modalId + ' .animal-image-select-thumb').each(function() {
if ($(this).data('select-value') === value) {
$(this).addClass('is-selected');
}
});
}
$(document).on('click', '.animal-image-select-thumb', function() {
var kind = $(this).data('select-kind') || 'cover';
var target = $(this).data('select-target');
var value = $(this).data('select-value');
if (target && value !== undefined) {
$(target).val(value);
if (kind === 'profile') {
$('#animalform-profileimagefile').val('');
$('#animalform-removeprofileimage').prop('checked', false);
} else {
$('#animalform-coverimagefile').val('');
$('#animalform-removecoverimage').prop('checked', false);
}
updatePreview(kind, value);
markSelectedThumb(kind, value);
if (kind === 'profile') {
$('#animal-profile-upload-file-name').text('');
} else {
$('#animal-cover-upload-file-name').text('');
}
}
$(this).closest('.modal').modal('hide');
});
$('#animal-cover-upload-trigger').on('click', function() {
$('#animalform-coverimagefile').trigger('click');
});
$('#animal-profile-upload-trigger').on('click', function() {
$('#animalform-profileimagefile').trigger('click');
});
$('#animalform-coverimagefile').on('change', function() {
var file = this.files && this.files[0] ? this.files[0] : null;
$('#animalform-coverimagegallerypath').val('');
$('#animalform-removecoverimage').prop('checked', false);
if (!file) {
return;
}
var reader = new FileReader();
reader.onload = function(e) {
updatePreview('cover', e.target.result);
markSelectedThumb('cover', '');
};
reader.readAsDataURL(file);
$('#animal-cover-upload-file-name').text(file.name || '');
$('#animal-cover-image-manage-modal').modal('hide');
});
$('#animalform-profileimagefile').on('change', function() {
var file = this.files && this.files[0] ? this.files[0] : null;
$('#animalform-profileimagegallerypath').val('');
$('#animalform-removeprofileimage').prop('checked', false);
if (!file) {
return;
}
var reader = new FileReader();
reader.onload = function(e) {
updatePreview('profile', e.target.result);
markSelectedThumb('profile', '');
};
reader.readAsDataURL(file);
$('#animal-profile-upload-file-name').text(file.name || '');
$('#animal-profile-image-manage-modal').modal('hide');
});
$('#animalform-removecoverimage').on('change', function() {
if ($(this).is(':checked')) {
$('#animalform-coverimagegallerypath').val('');
$('#animalform-coverimagefile').val('');
$('#animal-cover-upload-file-name').text('');
markSelectedThumb('cover', '');
updatePreview('cover', '');
}
});
$('#animalform-removeprofileimage').on('change', function() {
if ($(this).is(':checked')) {
$('#animalform-profileimagegallerypath').val('');
$('#animalform-profileimagefile').val('');
$('#animal-profile-upload-file-name').text('');
markSelectedThumb('profile', '');
updatePreview('profile', '');
}
});
$('#animal-cover-image-manage-modal').on('shown.bs.modal', function() {
markSelectedThumb('cover', $('#animalform-coverimagegallerypath').val());
});
$('#animal-profile-image-manage-modal').on('shown.bs.modal', function() {
markSelectedThumb('profile', $('#animalform-profileimagegallerypath').val());
});
})();
JS
);
?>

View File

@@ -0,0 +1,497 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\animal_management\models\forms\AnimalMedicalVisitForm;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
use yii\helpers\Json;
/* @var Space $space */
/* @var Animal $animal */
/* @var AnimalMedicalVisitForm $model */
/* @var AnimalMedicalVisit $medicalVisit */
/* @var string $returnTo */
/* @var AnimalGalleryItem[] $galleryItems */
/* @var bool $isInline */
$isInline = isset($isInline) ? (bool)$isInline : false;
$hiddenMedicalKeys = [
'second_physician_name',
'second_physician_business_name',
'second_physician_street_address',
'second_physician_city',
'second_physician_state',
'second_physician_zip',
'second_physician_cell_phone',
'second_physician_business_phone',
'second_physician_license_number',
'previous_physicians',
];
$renderCustomField = static function (string $fieldKey, AnimalMedicalVisitForm $formModel, array $definitions): string {
if (!isset($definitions[$fieldKey])) {
return '';
}
$definition = $definitions[$fieldKey];
$inputType = (string)$definition['input_type'];
$vitalLabelOverrides = [
'blood_pressure' => 'BP',
'oxygen' => 'O₂',
];
$label = (string)($vitalLabelOverrides[$fieldKey] ?? $definition['label']);
if ((int)$definition['required'] === 1) {
$label .= ' *';
}
$fieldName = "AnimalMedicalVisitForm[customFields][$fieldKey]";
$fieldValue = $formModel->customFields[$fieldKey] ?? '';
ob_start();
?>
<?php if ($inputType === 'textarea'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textarea($fieldName, (string)$fieldValue, ['class' => 'form-control', 'rows' => 3, 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'boolean'): ?>
<div class="checkbox" style="margin-bottom:10px;">
<label>
<?= Html::hiddenInput($fieldName, '0') ?>
<?= Html::checkbox($fieldName, !empty($fieldValue), ['value' => '1']) ?>
<?= Html::encode($label) ?>
</label>
</div>
<?php elseif ($inputType === 'select'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::dropDownList(
$fieldName,
(string)$fieldValue,
$formModel->getCustomFieldSelectOptions($fieldKey),
['class' => 'form-control', 'prompt' => Yii::t('AnimalManagementModule.base', 'Select...'), 'id' => "animalmedicalvisitform-customfields-$fieldKey"]
) ?>
</div>
<?php elseif ($inputType === 'number'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('number', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'step' => 'any', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'date'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('date', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'datetime'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('datetime-local', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php else: ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textInput($fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php endif; ?>
<?php
return (string)ob_get_clean();
};
$customDefinitions = $model->getCustomFieldDefinitions();
$knownMedicalKeys = [
'weight',
'pulse',
'blood_pressure',
'oxygen',
'chronic_conditions',
'acute_conditions',
'special_needs',
'date_of_most_recent_medical_visit',
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
'medical_media_reference',
'media_reference',
];
$remainingDefinitions = [];
foreach ($customDefinitions as $fieldKey => $definition) {
if (in_array($fieldKey, $knownMedicalKeys, true) || in_array($fieldKey, $hiddenMedicalKeys, true)) {
continue;
}
$remainingDefinitions[$fieldKey] = $definition;
}
$medicalMediaPath = trim((string)($model->customFields['medical_media_reference'] ?? $model->customFields['media_reference'] ?? ''));
$hasMedicalMedia = $medicalMediaPath !== '' && (preg_match('/^https?:\/\//i', $medicalMediaPath) || substr($medicalMediaPath, 0, 1) === '/');
$medicalGalleryModalId = 'edit-medical-media-gallery-modal';
$medicalFormId = 'edit-medical-visit-form';
$this->registerCss(<<<CSS
.inline-editor-shell.panel {
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 12px;
background: rgba(10, 18, 28, 0.36);
}
.inline-editor-shell > .panel-heading {
color: #eef5fb;
background: rgba(10, 18, 28, 0.42);
border-color: rgba(255, 255, 255, 0.2);
}
.inline-editor-shell > .panel-body {
background: rgba(10, 18, 28, 0.2);
}
.inline-editor-shell .panel.panel-default {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(10, 18, 28, 0.34);
}
.inline-editor-shell .panel.panel-default > .panel-heading {
color: #eef5fb;
background: rgba(10, 18, 28, 0.42);
border-color: rgba(255, 255, 255, 0.2);
}
.inline-editor-shell,
.inline-editor-shell .panel-body,
.inline-editor-shell .control-label,
.inline-editor-shell .checkbox label,
.inline-editor-shell .radio label,
.inline-editor-shell .help-block {
color: #eef5fb;
}
.inline-editor-shell .text-muted {
color: rgba(233, 242, 250, 0.78) !important;
}
.inline-editor-shell .form-control {
background: rgba(10, 18, 28, 0.56);
border-color: rgba(255, 255, 255, 0.44);
color: #f3f8ff;
}
.inline-editor-shell .form-control::placeholder {
color: rgba(243, 248, 255, 0.72);
}
.inline-editor-shell .form-control[readonly],
.inline-editor-shell .form-control[disabled] {
background: rgba(10, 18, 28, 0.42);
color: rgba(243, 248, 255, 0.72);
}
.inline-editor-shell select.form-control option {
color: #0f1b2a;
}
CSS
);
if ($isInline) {
$this->registerCss(<<<CSS
html, body {
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
}
body > .panel:first-child {
margin-top: 0 !important;
}
CSS
);
}
?>
<style>
.medical-media-select-thumb.is-selected {
border-color: #1f8dd6;
box-shadow: 0 0 0 2px rgba(31, 141, 214, 0.2);
}
</style>
<div class="panel panel-default inline-editor-shell">
<div class="panel-heading">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;">
<span><?= Yii::t('AnimalManagementModule.base', '<strong>Edit</strong> Medical Visit') ?></span>
<?php if ($isInline): ?>
<span style="display:inline-flex;gap:8px;">
<?= Html::submitButton('<i class="fa fa-check"></i>', [
'class' => 'btn btn-default btn-sm',
'title' => Yii::t('AnimalManagementModule.base', 'Save Medical Visit'),
'form' => $medicalFormId,
]) ?>
<?= Html::button('<i class="fa fa-times"></i>', [
'type' => 'button',
'class' => 'btn btn-default btn-sm',
'id' => 'medical-inline-cancel-icon',
'title' => Yii::t('AnimalManagementModule.base', 'Cancel'),
]) ?>
</span>
<?php endif; ?>
</div>
</div>
<div class="panel-body">
<?php
$formOptions = ['id' => $medicalFormId, 'enctype' => 'multipart/form-data'];
if (!$isInline) {
$formOptions['target'] = '_top';
}
$form = ActiveForm::begin(['options' => $formOptions]);
?>
<?= Html::hiddenInput('returnTo', (string)($returnTo ?? 'view')) ?>
<?= Html::hiddenInput('medicalMediaGalleryPath', $medicalMediaPath, ['id' => 'medical-media-gallery-path']) ?>
<?= $form->errorSummary($model, ['showAllErrors' => true]) ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Visit') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6"><?= $form->field($model, 'visit_at')->input('datetime-local') ?></div>
<div class="col-sm-6"><?= $form->field($model, 'provider_name') ?></div>
</div>
<?= $form->field($model, 'notes')->textarea(['rows' => 3]) ?>
<?= $form->field($model, 'recommendations')->textarea(['rows' => 3]) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Vitals') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-3"><?= $renderCustomField('weight', $model, $customDefinitions) ?></div>
<div class="col-sm-3"><?= $renderCustomField('pulse', $model, $customDefinitions) ?></div>
<div class="col-sm-3"><?= $renderCustomField('blood_pressure', $model, $customDefinitions) ?></div>
<div class="col-sm-3"><?= $renderCustomField('oxygen', $model, $customDefinitions) ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Conditions') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('chronic_conditions', $model, $customDefinitions) ?>
<?= $renderCustomField('acute_conditions', $model, $customDefinitions) ?>
<?= $renderCustomField('special_needs', $model, $customDefinitions) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Medical Visit Detail') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('date_of_most_recent_medical_visit', $model, $customDefinitions) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Media') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-4" style="margin-bottom:8px;">
<div id="medical-media-preview" style="border-radius:8px;overflow:hidden;background:#f2f4f6;height:150px;display:flex;align-items:center;justify-content:center;">
<?php if ($hasMedicalMedia): ?>
<img src="<?= Html::encode($medicalMediaPath) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Selected medical media') ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>
<?php endif; ?>
</div>
</div>
<div class="col-sm-8">
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#<?= Html::encode($medicalGalleryModalId) ?>" style="margin-bottom:8px;">
<i class="fa fa-photo"></i> <?= Yii::t('AnimalManagementModule.base', 'Choose from Gallery or Upload') ?>
</button>
<div class="checkbox" style="margin-top:0;">
<label>
<input type="checkbox" name="removeMedicalMedia" value="1">
<?= Yii::t('AnimalManagementModule.base', 'Remove selected media') ?>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Physician') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6"><?= $renderCustomField('physician_name', $model, $customDefinitions) ?></div>
<div class="col-sm-6"><?= $renderCustomField('physician_business_name', $model, $customDefinitions) ?></div>
<div class="col-sm-12"><?= $renderCustomField('physician_street_address', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_city', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_state', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_zip', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_cell_phone', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_business_phone', $model, $customDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_license_number', $model, $customDefinitions) ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Social Post') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $form->field($model, 'post_to_space_feed')->checkbox() ?>
<?= $form->field($model, 'post_to_animal_feed')->checkbox() ?>
</div>
</div>
<?php if (!empty($remainingDefinitions)): ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Additional Details') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?php foreach ($remainingDefinitions as $fieldKey => $definition): ?>
<?= $renderCustomField($fieldKey, $model, $remainingDefinitions) ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?= Button::save(Yii::t('AnimalManagementModule.base', 'Save Medical Visit'))->submit() ?>
<?php if ($isInline): ?>
<?= Html::button(Yii::t('AnimalManagementModule.base', 'Cancel'), [
'type' => 'button',
'class' => 'btn btn-default',
'id' => 'medical-inline-cancel',
]) ?>
<?php else: ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Cancel'))
->link(($returnTo ?? 'view') === 'medical-visits'
? $space->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id])
: $space->createUrl('/animal_management/animals/view', ['id' => $animal->id])) ?>
<?php endif; ?>
<?php ActiveForm::end(); ?>
</div>
</div>
<div class="modal fade" id="<?= Html::encode($medicalGalleryModalId) ?>" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Select Medical Media from Gallery') ?></h4>
</div>
<div class="modal-body">
<?php if (empty($galleryItems)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No gallery images available yet.') ?></div>
<?php else: ?>
<div class="row" style="max-height:280px;overflow:auto;margin-bottom:10px;">
<?php foreach ($galleryItems as $galleryItem): ?>
<?php $galleryUrl = trim((string)$galleryItem->getImageUrl()); ?>
<?php if ($galleryUrl === '') { continue; } ?>
<div class="col-xs-6 col-sm-4" style="margin-bottom:8px;">
<button type="button" class="btn btn-default medical-media-select-thumb<?= $medicalMediaPath === $galleryUrl ? ' is-selected' : '' ?>" data-media-url="<?= Html::encode($galleryUrl) ?>" style="width:100%;padding:3px;">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Gallery image') ?>" style="width:100%;height:120px;object-fit:cover;display:block;">
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="form-group" style="margin-bottom:0;">
<label class="control-label" for="medicalMediaUpload"><?= Yii::t('AnimalManagementModule.base', 'Upload New Image') ?></label>
<input type="file" class="form-control" id="medicalMediaUpload" name="medicalMediaUpload" form="<?= Html::encode($medicalFormId) ?>" accept="image/*">
</div>
</div>
</div>
</div>
</div>
<?php
$this->registerJs(<<<JS
(function(){
function renderMedicalPreview(source) {
var preview = $('#medical-media-preview');
if (!preview.length) {
return;
}
if (source) {
preview.html('<img src="' + source + '" alt="Selected medical media" style="width:100%;height:100%;object-fit:cover;">');
} else {
preview.html('<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>');
}
}
function markSelectedMedicalThumb(value) {
$('.medical-media-select-thumb').removeClass('is-selected');
if (!value) {
return;
}
$('.medical-media-select-thumb').each(function() {
if ($(this).data('media-url') === value) {
$(this).addClass('is-selected');
}
});
}
$(document).on('click', '.medical-media-select-thumb', function() {
var mediaUrl = $(this).data('media-url');
$('#medical-media-gallery-path').val(mediaUrl);
markSelectedMedicalThumb(mediaUrl);
$('#medicalMediaUpload').val('');
$('input[name="removeMedicalMedia"]').prop('checked', false);
if (mediaUrl) {
renderMedicalPreview(mediaUrl);
}
$('#{$medicalGalleryModalId}').modal('hide');
});
$('#medicalMediaUpload').on('change', function() {
var file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
return;
}
$('#medical-media-gallery-path').val('');
markSelectedMedicalThumb('');
$('input[name="removeMedicalMedia"]').prop('checked', false);
var reader = new FileReader();
reader.onload = function(e) {
renderMedicalPreview(e.target.result);
$('#{$medicalGalleryModalId}').modal('hide');
};
reader.readAsDataURL(file);
});
$('#{$medicalGalleryModalId}').on('shown.bs.modal', function() {
markSelectedMedicalThumb($('#medical-media-gallery-path').val());
});
})();
JS
, \yii\web\View::POS_END);
if ($isInline) {
$cancelPayload = Json::htmlEncode([
'source' => 'animal-inline-editor',
'type' => 'cancel',
'collapseId' => 'medical-edit-inline-' . (int)$medicalVisit->id,
]);
$this->registerJs(<<<JS
$(document).on('click', '#medical-inline-cancel, #medical-inline-cancel-icon', function() {
if (window.parent && window.parent !== window) {
window.parent.postMessage($cancelPayload, '*');
}
});
JS
, \yii\web\View::POS_END);
}
?>

View File

@@ -0,0 +1,424 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\AnimalProgressUpdate;
use humhub\modules\animal_management\models\forms\AnimalProgressUpdateForm;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
use yii\helpers\Json;
/* @var Space $space */
/* @var Animal $animal */
/* @var AnimalProgressUpdateForm $model */
/* @var AnimalProgressUpdate $progressUpdate */
/* @var string $returnTo */
/* @var AnimalGalleryItem[] $galleryItems */
/* @var bool $isInline */
$isInline = isset($isInline) ? (bool)$isInline : false;
$renderCustomField = static function (string $fieldKey, AnimalProgressUpdateForm $formModel, array $definitions): string {
if (!isset($definitions[$fieldKey])) {
return '';
}
$definition = $definitions[$fieldKey];
$inputType = (string)$definition['input_type'];
$label = (string)$definition['label'];
if ((int)$definition['required'] === 1) {
$label .= ' *';
}
$fieldName = "AnimalProgressUpdateForm[customFields][$fieldKey]";
$fieldValue = $formModel->customFields[$fieldKey] ?? '';
ob_start();
?>
<?php if ($inputType === 'textarea'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textarea($fieldName, (string)$fieldValue, ['class' => 'form-control', 'rows' => 3, 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'boolean'): ?>
<div class="checkbox" style="margin-bottom:10px;">
<label>
<?= Html::hiddenInput($fieldName, '0') ?>
<?= Html::checkbox($fieldName, !empty($fieldValue), ['value' => '1']) ?>
<?= Html::encode($label) ?>
</label>
</div>
<?php elseif ($inputType === 'select'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::dropDownList(
$fieldName,
(string)$fieldValue,
$formModel->getCustomFieldSelectOptions($fieldKey),
['class' => 'form-control', 'prompt' => Yii::t('AnimalManagementModule.base', 'Select...'), 'id' => "animalprogressupdateform-customfields-$fieldKey"]
) ?>
</div>
<?php elseif ($inputType === 'number'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('number', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'step' => 'any', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'date'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('date', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'datetime'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('datetime-local', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php else: ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textInput($fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php endif; ?>
<?php
return (string)ob_get_clean();
};
$customDefinitions = $model->getCustomFieldDefinitions();
$knownProgressKeys = ['progress_notes', 'routine_updates', 'media_reference'];
$otherCustomDefinitions = [];
foreach ($customDefinitions as $fieldKey => $definition) {
if (in_array($fieldKey, $knownProgressKeys, true)) {
continue;
}
$otherCustomDefinitions[$fieldKey] = $definition;
}
$currentMediaReference = trim((string)($model->customFields['media_reference'] ?? ''));
$progressFormId = 'edit-progress-update-form';
$this->registerCss(<<<CSS
.inline-editor-shell.panel {
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 12px;
background: rgba(10, 18, 28, 0.36);
}
.inline-editor-shell > .panel-heading {
color: #eef5fb;
background: rgba(10, 18, 28, 0.42);
border-color: rgba(255, 255, 255, 0.2);
}
.inline-editor-shell > .panel-body {
background: rgba(10, 18, 28, 0.2);
}
.inline-editor-shell .panel.panel-default {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(10, 18, 28, 0.34);
}
.inline-editor-shell .panel.panel-default > .panel-heading {
color: #eef5fb;
background: rgba(10, 18, 28, 0.42);
border-color: rgba(255, 255, 255, 0.2);
}
.inline-editor-shell,
.inline-editor-shell .panel-body,
.inline-editor-shell .control-label,
.inline-editor-shell .checkbox label,
.inline-editor-shell .radio label,
.inline-editor-shell .help-block {
color: #eef5fb;
}
.inline-editor-shell .text-muted {
color: rgba(233, 242, 250, 0.78) !important;
}
.inline-editor-shell .form-control {
background: rgba(10, 18, 28, 0.56);
border-color: rgba(255, 255, 255, 0.44);
color: #f3f8ff;
}
.inline-editor-shell .form-control::placeholder {
color: rgba(243, 248, 255, 0.72);
}
.inline-editor-shell .form-control[readonly],
.inline-editor-shell .form-control[disabled] {
background: rgba(10, 18, 28, 0.42);
color: rgba(243, 248, 255, 0.72);
}
.inline-editor-shell select.form-control option {
color: #0f1b2a;
}
CSS
);
if ($isInline) {
$this->registerCss(<<<CSS
html, body {
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
}
body > .panel:first-child {
margin-top: 0 !important;
}
CSS
);
}
?>
<div class="panel panel-default inline-editor-shell">
<div class="panel-heading">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;">
<span><?= Yii::t('AnimalManagementModule.base', '<strong>Edit</strong> Progress Update') ?></span>
<?php if ($isInline): ?>
<span style="display:inline-flex;gap:8px;">
<?= Html::submitButton('<i class="fa fa-check"></i>', [
'class' => 'btn btn-default btn-sm',
'title' => Yii::t('AnimalManagementModule.base', 'Save Progress Update'),
'form' => $progressFormId,
]) ?>
<?= Html::button('<i class="fa fa-times"></i>', [
'type' => 'button',
'class' => 'btn btn-default btn-sm',
'id' => 'progress-inline-cancel-icon',
'title' => Yii::t('AnimalManagementModule.base', 'Cancel'),
]) ?>
</span>
<?php endif; ?>
</div>
</div>
<div class="panel-body">
<?php
$formOptions = ['id' => $progressFormId, 'enctype' => 'multipart/form-data'];
if (!$isInline) {
$formOptions['target'] = '_top';
}
$form = ActiveForm::begin(['options' => $formOptions]);
?>
<?= Html::hiddenInput('returnTo', (string)($returnTo ?? 'view')) ?>
<?= $form->errorSummary($model, ['showAllErrors' => true]) ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Progress Update') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-4"><?= $form->field($model, 'weight') ?></div>
<div class="col-sm-8"><?= $form->field($model, 'vitals')->textInput(['maxlength' => 255]) ?></div>
<div class="col-sm-6"><?= $form->field($model, 'behavior_notes')->textarea(['rows' => 2]) ?></div>
<div class="col-sm-6"><?= $form->field($model, 'medical_concerns')->textarea(['rows' => 2]) ?></div>
<div class="col-sm-6"><?= $form->field($model, 'meal_plan_changes')->textarea(['rows' => 2]) ?></div>
<div class="col-sm-6"><?= $form->field($model, 'housing_changes')->textarea(['rows' => 2]) ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Notes') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('progress_notes', $model, $customDefinitions) ?>
<?= $renderCustomField('routine_updates', $model, $customDefinitions) ?>
</div>
</div>
<?php if (!empty($otherCustomDefinitions)): ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Additional Details') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?php foreach ($otherCustomDefinitions as $fieldKey => $definition): ?>
<?= $renderCustomField($fieldKey, $model, $otherCustomDefinitions) ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Media') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<input type="hidden" id="progress-media-gallery-path" name="progressMediaGalleryPath" value="<?= Html::encode($currentMediaReference) ?>">
<div class="row">
<div class="col-sm-4" style="margin-bottom:8px;">
<div id="progress-media-preview" style="border-radius:8px;overflow:hidden;background:#f2f4f6;height:150px;display:flex;align-items:center;justify-content:center;">
<?php if ($currentMediaReference !== '' && (preg_match('/^https?:\/\//i', $currentMediaReference) || substr($currentMediaReference, 0, 1) === '/')): ?>
<img src="<?= Html::encode($currentMediaReference) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Selected media') ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>
<?php endif; ?>
</div>
</div>
<div class="col-sm-8">
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#progress-media-modal" style="margin-bottom:8px;">
<i class="fa fa-photo"></i> <?= Yii::t('AnimalManagementModule.base', 'Choose from Gallery or Upload') ?>
</button>
<div class="checkbox" style="margin-top:0;">
<label>
<input type="checkbox" name="removeProgressMedia" value="1">
<?= Yii::t('AnimalManagementModule.base', 'Remove selected media') ?>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Social Post') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $form->field($model, 'post_to_space_feed')->checkbox() ?>
<?= $form->field($model, 'post_to_animal_feed')->checkbox() ?>
</div>
</div>
<div class="modal fade" id="progress-media-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Select Progress Media') ?></h4>
</div>
<div class="modal-body">
<?php if (empty($galleryItems)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No gallery images available yet.') ?></div>
<?php else: ?>
<div class="row" style="max-height:280px;overflow:auto;margin-bottom:10px;">
<?php foreach ($galleryItems as $galleryItem): ?>
<?php $galleryUrl = trim((string)$galleryItem->getImageUrl()); ?>
<?php if ($galleryUrl === '') { continue; } ?>
<div class="col-xs-6 col-sm-4" style="margin-bottom:8px;">
<button type="button" class="btn btn-default progress-media-select-thumb<?= $currentMediaReference === $galleryUrl ? ' is-selected' : '' ?>" data-media-url="<?= Html::encode($galleryUrl) ?>" style="width:100%;padding:3px;">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Gallery image') ?>" style="width:100%;height:120px;object-fit:cover;border-radius:4px;">
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="form-group" style="margin-bottom:0;">
<label class="control-label" for="progressMediaUpload"><?= Yii::t('AnimalManagementModule.base', 'Upload New Image') ?></label>
<input type="file" class="form-control" id="progressMediaUpload" name="progressMediaUpload" accept="image/*">
</div>
</div>
</div>
</div>
</div>
<?= Button::save(Yii::t('AnimalManagementModule.base', 'Save Progress Update'))->submit() ?>
<?php if ($isInline): ?>
<?= Html::button(Yii::t('AnimalManagementModule.base', 'Cancel'), [
'type' => 'button',
'class' => 'btn btn-default',
'id' => 'progress-inline-cancel',
]) ?>
<?php else: ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Cancel'))
->link(($returnTo ?? 'view') === 'progress-updates'
? $space->createUrl('/animal_management/animals/progress-updates', ['id' => $animal->id])
: $space->createUrl('/animal_management/animals/view', ['id' => $animal->id])) ?>
<?php endif; ?>
<?php ActiveForm::end(); ?>
</div>
</div>
<?php
$this->registerCss(<<<CSS
.progress-media-select-thumb.is-selected {
border-color: #2f7df4 !important;
box-shadow: 0 0 0 2px rgba(47, 125, 244, 0.22);
}
CSS
);
$this->registerJs(<<<JS
(function() {
function renderProgressPreview(source) {
var preview = $('#progress-media-preview');
if (!preview.length) {
return;
}
if (source) {
preview.html('<img src="' + source + '" alt="Selected media" style="width:100%;height:100%;object-fit:cover;">');
} else {
preview.html('<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>');
}
}
function markSelectedMediaThumb(value) {
$('.progress-media-select-thumb').removeClass('is-selected');
if (!value) {
return;
}
$('.progress-media-select-thumb').each(function() {
if (($(this).attr('data-media-url') || '') === value) {
$(this).addClass('is-selected');
}
});
}
$(document).off('click.editProgressMediaSelect', '.progress-media-select-thumb').on('click.editProgressMediaSelect', '.progress-media-select-thumb', function() {
var mediaUrl = $(this).attr('data-media-url') || '';
$('#progress-media-gallery-path').val(mediaUrl);
markSelectedMediaThumb(mediaUrl);
$('#progressMediaUpload').val('');
$('input[name="removeProgressMedia"]').prop('checked', false);
if (mediaUrl) {
renderProgressPreview(mediaUrl);
}
$('#progress-media-modal').modal('hide');
});
$(document).off('change.editProgressMediaUpload', '#progressMediaUpload').on('change.editProgressMediaUpload', '#progressMediaUpload', function() {
var file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
return;
}
$('#progress-media-gallery-path').val('');
markSelectedMediaThumb('');
$('input[name="removeProgressMedia"]').prop('checked', false);
var reader = new FileReader();
reader.onload = function(e) {
renderProgressPreview(e.target.result);
$('#progress-media-modal').modal('hide');
};
reader.readAsDataURL(file);
});
$(document).off('shown.bs.modal.editProgressMediaModal', '#progress-media-modal').on('shown.bs.modal.editProgressMediaModal', '#progress-media-modal', function() {
markSelectedMediaThumb($('#progress-media-gallery-path').val());
});
})();
JS
, \yii\web\View::POS_END);
if ($isInline) {
$cancelPayload = Json::htmlEncode([
'source' => 'animal-inline-editor',
'type' => 'cancel',
'collapseId' => 'progress-edit-inline-' . (int)$progressUpdate->id,
]);
$this->registerJs(<<<JS
$(document).on('click', '#progress-inline-cancel, #progress-inline-cancel-icon', function() {
if (window.parent && window.parent !== window) {
window.parent.postMessage($cancelPayload, '*');
}
});
JS
, \yii\web\View::POS_END);
}
?>

283
views/animals/index.php Normal file
View File

@@ -0,0 +1,283 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\helpers\DateDisplayHelper;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\animal_management\models\AnimalTransfer;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
use yii\helpers\Html;
/* @var Animal[] $animals */
/* @var AnimalTransfer[] $incomingTransfers */
/* @var AnimalTransfer[] $outgoingTransfers */
/* @var string $queryValue */
/* @var string $statusFilter */
/* @var string $speciesFilter */
/* @var string $viewMode */
/* @var string $sortKey */
/* @var string $sortDirection */
/* @var array $availableColumns */
/* @var array $selectedColumns */
/* @var array $speciesOptions */
/* @var array<int, AnimalMedicalVisit> $latestMedicalVisitByAnimal */
/* @var array<int, string> $animalImageUrls */
/* @var array<int, string> $transferAnimalImageUrls */
/* @var array $tileFields */
/* @var array<int, array> $tileFieldOverrides */
/* @var Space $space */
/* @var bool $canManage */
$currentParams = [
'q' => $queryValue,
'status' => $statusFilter,
'species' => $speciesFilter,
'view' => $viewMode,
'sort' => $sortKey,
'direction' => $sortDirection,
'cols' => $selectedColumns,
];
$buildUrl = static function (array $overrides) use ($space, $currentParams): string {
$params = array_merge($currentParams, $overrides);
return $space->createUrl('/animal_management/animals/index', $params);
};
$sortUrl = static function (string $column) use ($buildUrl, $sortKey, $sortDirection): string {
$nextDirection = ($sortKey === $column && $sortDirection === 'asc') ? 'desc' : 'asc';
return $buildUrl(['sort' => $column, 'direction' => $nextDirection, 'view' => 'table']);
};
?>
<div class="panel panel-default">
<div class="panel-heading" style="display:flex;justify-content:space-between;align-items:center;">
<span><?= Yii::t('AnimalManagementModule.base', '<strong>Animals</strong>') ?></span>
<span style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<?= Html::a('<i class="fa fa-th-large"></i>', $buildUrl(['view' => 'tiles']), [
'class' => 'btn btn-default btn-sm' . ($viewMode === 'tiles' ? ' active' : ''),
'title' => Yii::t('AnimalManagementModule.base', 'Tile View'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Tile View'),
]) ?>
<?= Html::a('<i class="fa fa-table"></i>', $buildUrl(['view' => 'table']), [
'class' => 'btn btn-default btn-sm' . ($viewMode === 'table' ? ' active' : ''),
'title' => Yii::t('AnimalManagementModule.base', 'Table View'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Table View'),
]) ?>
<?php if ($canManage): ?>
<?= Html::a('<i class="fa fa-plus"></i> ' . Yii::t('AnimalManagementModule.base', 'Intake'), $space->createUrl('/animal_management/animals/create'), [
'class' => 'btn btn-primary btn-sm',
'title' => Yii::t('AnimalManagementModule.base', 'Intake'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Intake'),
]) ?>
<?php endif; ?>
</span>
</div>
<div class="panel-body">
<form method="get" action="<?= Html::encode($space->createUrl('/animal_management/animals/index')) ?>" class="form-inline" style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
<input type="hidden" name="view" value="<?= Html::encode($viewMode) ?>">
<input type="hidden" name="sort" value="<?= Html::encode($sortKey) ?>">
<input type="hidden" name="direction" value="<?= Html::encode($sortDirection) ?>">
<?php foreach ($selectedColumns as $col): ?>
<input type="hidden" name="cols[]" value="<?= Html::encode($col) ?>">
<?php endforeach; ?>
<div class="form-group" style="flex:1;min-width:260px;">
<input type="text" class="form-control" style="width:100%;" name="q" value="<?= Html::encode($queryValue) ?>" placeholder="<?= Yii::t('AnimalManagementModule.base', 'Search by name, species, ID, or breed') ?>">
</div>
<div class="form-group">
<?= Html::dropDownList('status', $statusFilter, ['' => Yii::t('AnimalManagementModule.base', 'All Statuses')] + Animal::statusOptions(), ['class' => 'form-control']) ?>
</div>
<div class="form-group">
<?php $speciesDropDown = ['' => Yii::t('AnimalManagementModule.base', 'All Species')]; ?>
<?php foreach ($speciesOptions as $speciesOption): ?>
<?php $speciesDropDown[$speciesOption] = $speciesOption; ?>
<?php endforeach; ?>
<?= Html::dropDownList('species', $speciesFilter, $speciesDropDown, ['class' => 'form-control']) ?>
</div>
<button type="submit" class="btn btn-default"><?= Yii::t('AnimalManagementModule.base', 'Apply') ?></button>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Reset'))->link($space->createUrl('/animal_management/animals/index', ['view' => $viewMode])) ?>
</form>
<?php if ($viewMode === 'table'): ?>
<details style="margin-bottom:12px;">
<summary style="cursor:pointer;"><?= Yii::t('AnimalManagementModule.base', 'Show/Hide Columns') ?></summary>
<form method="get" action="<?= Html::encode($space->createUrl('/animal_management/animals/index')) ?>" style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<input type="hidden" name="view" value="table">
<input type="hidden" name="q" value="<?= Html::encode($queryValue) ?>">
<input type="hidden" name="status" value="<?= Html::encode($statusFilter) ?>">
<input type="hidden" name="species" value="<?= Html::encode($speciesFilter) ?>">
<input type="hidden" name="sort" value="<?= Html::encode($sortKey) ?>">
<input type="hidden" name="direction" value="<?= Html::encode($sortDirection) ?>">
<?php foreach ($availableColumns as $columnKey => $columnLabel): ?>
<label style="margin:0 8px 0 0;">
<input type="checkbox" name="cols[]" value="<?= Html::encode($columnKey) ?>" <?= in_array($columnKey, $selectedColumns, true) ? 'checked' : '' ?> <?= $columnKey === 'name' ? 'disabled' : '' ?>>
<?= Html::encode($columnLabel) ?>
</label>
<?php endforeach; ?>
<button type="submit" class="btn btn-default btn-sm"><?= Yii::t('AnimalManagementModule.base', 'Update Columns') ?></button>
</form>
</details>
<?php endif; ?>
<?php if (empty($animals)): ?>
<div class="alert alert-info" style="margin-bottom:0;">
<?= Yii::t('AnimalManagementModule.base', 'No animal profiles yet. Create the first intake record to begin tracking.') ?>
</div>
<?php else: ?>
<?php if ($viewMode === 'tiles'): ?>
<div class="row">
<?php foreach ($animals as $animal): ?>
<?php $animalId = (int)$animal->id; ?>
<?php $lastMedical = $latestMedicalVisitByAnimal[$animalId] ?? null; ?>
<div class="col-sm-6 col-md-4" style="margin-bottom:16px;">
<?= $this->render('_tile', [
'animal' => $animal,
'contentContainer' => $space,
'lastMedical' => $lastMedical,
'imageUrl' => $animalImageUrls[$animalId] ?? '',
'tileFields' => $tileFieldOverrides[$animalId] ?? $tileFields,
'showMedicalIcon' => true,
]) ?>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<?php if (in_array('animal_uid', $selectedColumns, true)): ?>
<th><a href="<?= Html::encode($sortUrl('animal_uid')) ?>"><?= Yii::t('AnimalManagementModule.base', 'ID') ?></a></th>
<?php endif; ?>
<th><a href="<?= Html::encode($sortUrl('name')) ?>"><?= Yii::t('AnimalManagementModule.base', 'Name') ?></a></th>
<?php if (in_array('species', $selectedColumns, true)): ?>
<th><a href="<?= Html::encode($sortUrl('species')) ?>"><?= Yii::t('AnimalManagementModule.base', 'Species') ?></a></th>
<?php endif; ?>
<?php if (in_array('status', $selectedColumns, true)): ?>
<th><a href="<?= Html::encode($sortUrl('status')) ?>"><?= Yii::t('AnimalManagementModule.base', 'Status') ?></a></th>
<?php endif; ?>
<?php if (in_array('last_medical', $selectedColumns, true)): ?>
<th><a href="<?= Html::encode($sortUrl('last_medical')) ?>"><?= Yii::t('AnimalManagementModule.base', 'Last Medical Visit') ?></a></th>
<?php endif; ?>
<?php if (in_array('updated_at', $selectedColumns, true)): ?>
<th><a href="<?= Html::encode($sortUrl('updated_at')) ?>"><?= Yii::t('AnimalManagementModule.base', 'Updated') ?></a></th>
<?php endif; ?>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($animals as $animal): ?>
<?php $lastMedical = $latestMedicalVisitByAnimal[(int)$animal->id] ?? null; ?>
<tr>
<?php if (in_array('animal_uid', $selectedColumns, true)): ?>
<td><?= Html::encode($animal->animal_uid) ?></td>
<?php endif; ?>
<td>
<a href="<?= Html::encode($space->createUrl('/animal_management/animals/view', ['id' => $animal->id])) ?>">
<?= Html::encode($animal->getDisplayName()) ?>
</a>
</td>
<?php if (in_array('species', $selectedColumns, true)): ?>
<td><?= Html::encode((string)$animal->species) ?></td>
<?php endif; ?>
<?php if (in_array('status', $selectedColumns, true)): ?>
<td><?= Html::encode(Animal::statusOptions()[$animal->status] ?? $animal->status) ?></td>
<?php endif; ?>
<?php if (in_array('last_medical', $selectedColumns, true)): ?>
<td>
<?php if ($lastMedical instanceof AnimalMedicalVisit): ?>
<a href="<?= Html::encode($space->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id]) . '#medical-visit-' . (int)$lastMedical->id) ?>">
<?= Html::encode(DateDisplayHelper::format((string)$lastMedical->visit_at)) ?>
</a>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<?php endif; ?>
<?php if (in_array('updated_at', $selectedColumns, true)): ?>
<td><?= Html::encode(DateDisplayHelper::format((string)$animal->updated_at)) ?></td>
<?php endif; ?>
<td class="text-right" style="white-space:nowrap;">
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'View'))
->link($space->createUrl('/animal_management/animals/view', ['id' => $animal->id])) ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Medical'))
->link($space->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id])) ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Progress'))
->link($space->createUrl('/animal_management/animals/progress-updates', ['id' => $animal->id])) ?>
<?php if ($canManage): ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Edit'))
->link($space->createUrl('/animal_management/animals/edit', ['id' => $animal->id])) ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Transfer'))
->link($space->createUrl('/animal_management/animals/transfer', ['id' => $animal->id])) ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if ($canManage): ?>
<hr>
<h4 id="incoming-transfers" style="margin-top:0;"><?= Yii::t('AnimalManagementModule.base', 'Incoming Transfer Requests') ?></h4>
<?php if (empty($incomingTransfers)): ?>
<div class="text-muted" style="margin-bottom:12px;"><?= Yii::t('AnimalManagementModule.base', 'No incoming requests.') ?></div>
<?php else: ?>
<div class="row" style="margin-bottom:4px;">
<?php foreach ($incomingTransfers as $transfer): ?>
<?php
$fromSpace = $transfer->getFromSpace();
$toSpace = $transfer->getToSpace();
$animalLinkSpace = ($transfer->status === AnimalTransfer::STATUS_COMPLETED)
? ($toSpace ?: $fromSpace)
: ($fromSpace ?: $toSpace);
?>
<div class="col-sm-6" style="margin-bottom:14px;">
<?= $this->render('_transfer_tile', [
'transfer' => $transfer,
'space' => $space,
'otherRescueName' => $fromSpace ? $fromSpace->name : Yii::t('AnimalManagementModule.base', 'Unknown Rescue'),
'otherRescueUrl' => $fromSpace ? $fromSpace->createUrl('/space/space/home') : '',
'animalProfileUrl' => $animalLinkSpace ? $animalLinkSpace->createUrl('/animal_management/animals/view', ['id' => $transfer->animal_id]) : '',
'imageUrl' => trim((string)($transferAnimalImageUrls[(int)$transfer->animal_id] ?? '')),
'isIncoming' => true,
]) ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<h4 id="outgoing-transfers"><?= Yii::t('AnimalManagementModule.base', 'Outgoing Transfer History') ?></h4>
<?php if (empty($outgoingTransfers)): ?>
<div class="text-muted"><?= Yii::t('AnimalManagementModule.base', 'No outgoing transfers yet.') ?></div>
<?php else: ?>
<div class="row" style="margin-bottom:0;">
<?php foreach ($outgoingTransfers as $transfer): ?>
<?php
$fromSpace = $transfer->getFromSpace();
$toSpace = $transfer->getToSpace();
$animalLinkSpace = ($transfer->status === AnimalTransfer::STATUS_COMPLETED)
? ($toSpace ?: $fromSpace)
: ($fromSpace ?: $toSpace);
?>
<div class="col-sm-6 col-md-4" style="margin-bottom:14px;">
<?= $this->render('_transfer_tile', [
'transfer' => $transfer,
'space' => $space,
'otherRescueName' => $toSpace ? $toSpace->name : Yii::t('AnimalManagementModule.base', 'Unknown Rescue'),
'otherRescueUrl' => $toSpace ? $toSpace->createUrl('/space/space/home') : '',
'animalProfileUrl' => $animalLinkSpace ? $animalLinkSpace->createUrl('/animal_management/animals/view', ['id' => $transfer->animal_id]) : '',
'imageUrl' => trim((string)($transferAnimalImageUrls[(int)$transfer->animal_id] ?? '')),
'isIncoming' => false,
]) ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>

View File

@@ -0,0 +1,34 @@
<?php
use yii\helpers\Html;
/* @var string $collapseId */
/* @var array $refreshSelectors */
$payload = [
'source' => 'animal-inline-editor',
'type' => 'saved',
'collapseId' => (string)$collapseId,
'refreshSelectors' => array_values(array_map('strval', $refreshSelectors ?? [])),
];
$jsonPayload = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
?>
<div class="panel panel-success" style="margin:10px;">
<div class="panel-body" style="padding:12px;">
<?= Html::encode(Yii::t('AnimalManagementModule.base', 'Saved. Updating section...')) ?>
</div>
</div>
<?php
$this->registerJs(<<<JS
(function() {
var payload = {$jsonPayload};
if (window.parent && window.parent !== window) {
window.parent.postMessage(payload, '*');
}
})();
JS
, \yii\web\View::POS_END);
?>

View File

@@ -0,0 +1,901 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\helpers\DateDisplayHelper;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\animal_management\models\forms\AnimalMedicalVisitForm;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
/* @var Space $space */
/* @var Animal $animal */
/* @var bool $canManage */
/* @var AnimalMedicalVisitForm $medicalVisitForm */
/* @var AnimalMedicalVisit[] $medicalVisits */
/* @var AnimalGalleryItem[] $galleryItems */
$hiddenMedicalKeys = [
'second_physician_name',
'second_physician_business_name',
'second_physician_street_address',
'second_physician_city',
'second_physician_state',
'second_physician_zip',
'second_physician_cell_phone',
'second_physician_business_phone',
'second_physician_license_number',
'previous_physicians',
];
$renderCustomField = static function (string $fieldKey, AnimalMedicalVisitForm $model, array $definitions): string {
if (!isset($definitions[$fieldKey])) {
return '';
}
$definition = $definitions[$fieldKey];
$inputType = (string)$definition['input_type'];
$vitalLabelOverrides = [
'blood_pressure' => 'BP',
'oxygen' => 'O₂',
];
$label = (string)($vitalLabelOverrides[$fieldKey] ?? $definition['label']);
if ((int)$definition['required'] === 1) {
$label .= ' *';
}
$fieldName = "AnimalMedicalVisitForm[customFields][$fieldKey]";
$fieldValue = $model->customFields[$fieldKey] ?? '';
ob_start();
?>
<?php if ($inputType === 'textarea'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textarea($fieldName, (string)$fieldValue, ['class' => 'form-control', 'rows' => 3, 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'boolean'): ?>
<div class="checkbox" style="margin-bottom:10px;">
<label>
<?= Html::hiddenInput($fieldName, '0') ?>
<?= Html::checkbox($fieldName, !empty($fieldValue), ['value' => '1']) ?>
<?= Html::encode($label) ?>
</label>
</div>
<?php elseif ($inputType === 'select'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::dropDownList(
$fieldName,
(string)$fieldValue,
$model->getCustomFieldSelectOptions($fieldKey),
['class' => 'form-control', 'prompt' => Yii::t('AnimalManagementModule.base', 'Select...'), 'id' => "animalmedicalvisitform-customfields-$fieldKey"]
) ?>
</div>
<?php elseif ($inputType === 'number'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('number', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'step' => 'any', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'date'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('date', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'datetime'): ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('datetime-local', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php else: ?>
<div class="form-group">
<label class="control-label" for="animalmedicalvisitform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textInput($fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalmedicalvisitform-customfields-$fieldKey"]) ?>
</div>
<?php endif; ?>
<?php
return (string)ob_get_clean();
};
$medicalCustomDefinitions = $medicalVisitForm->getCustomFieldDefinitions();
$knownMedicalKeys = [
'weight',
'pulse',
'blood_pressure',
'oxygen',
'chronic_conditions',
'acute_conditions',
'special_needs',
'date_of_most_recent_medical_visit',
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
'medical_media_reference',
'media_reference',
];
$remainingMedicalDefinitions = [];
foreach ($medicalCustomDefinitions as $fieldKey => $definition) {
if (in_array($fieldKey, $knownMedicalKeys, true) || in_array($fieldKey, $hiddenMedicalKeys, true)) {
continue;
}
$remainingMedicalDefinitions[$fieldKey] = $definition;
}
$newMedicalMediaPath = trim((string)($medicalVisitForm->customFields['medical_media_reference'] ?? $medicalVisitForm->customFields['media_reference'] ?? ''));
$hasNewMedicalMedia = $newMedicalMediaPath !== '' && (preg_match('/^https?:\/\//i', $newMedicalMediaPath) || substr($newMedicalMediaPath, 0, 1) === '/');
$medicalGalleryModalId = 'medical-media-gallery-modal';
$medicalAddModalId = 'add-medical-visit-modal';
$medicalFormId = 'add-medical-visit-form';
$openMedicalEditId = (int)Yii::$app->request->get('inlineMedicalEdit', 0);
$openMedicalAdd = (int)Yii::$app->request->get('inlineMedicalAdd', 0) === 1;
$medicalVitalLabelOverrides = [
'blood_pressure' => 'BP',
'oxygen' => 'O₂',
];
?>
<style>
.medical-feed-card {
position: relative;
overflow: hidden;
border: 1px solid #d5dfe8;
border-radius: 12px;
background: #223446;
margin-bottom: 12px;
min-height: 260px;
box-shadow: 0 8px 22px rgba(12, 24, 36, 0.16);
}
.medical-feed-cover {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.medical-feed-overlay {
position: absolute;
inset: 0;
background: linear-gradient(110deg, rgba(10, 18, 28, 0.28) 10%, rgba(10, 18, 28, 0.55) 52%, rgba(10, 18, 28, 0.75) 100%);
}
.medical-feed-content {
position: relative;
z-index: 1;
min-height: 260px;
padding: 14px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
.medical-feed-top-row {
width: 100%;
max-width: none;
margin-left: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.medical-feed-text-wrap {
width: 55%;
max-width: 55%;
min-width: 0;
margin-left: auto;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.22);
background: rgba(10, 18, 28, 0.5);
backdrop-filter: blur(2px);
padding: 12px;
color: #ecf2f8;
}
.medical-feed-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
margin-bottom: 10px;
}
.medical-feed-date {
font-size: 15px;
font-weight: 700;
color: #ffffff;
margin-right: auto;
}
@media (max-width: 1200px) {
.medical-feed-text-wrap {
width: 55%;
max-width: 55%;
min-width: 0;
}
}
@media (max-width: 991px) {
.medical-feed-text-wrap {
width: 55%;
max-width: 55%;
min-width: 0;
}
.medical-chip-row--top {
justify-content: flex-start;
}
}
.medical-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.medical-chip-row--top {
margin-bottom: 0;
justify-content: flex-end;
}
.medical-chip {
display: inline-block;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
color: #ffffff;
}
.medical-media-select-thumb.is-selected {
border-color: #1f8dd6;
box-shadow: 0 0 0 2px rgba(31, 141, 214, 0.2);
}
.medical-section-label {
font-size: 11px;
letter-spacing: .04em;
text-transform: uppercase;
color: rgba(231, 241, 249, 0.78);
margin-bottom: 4px;
}
.medical-section-body {
color: #eff5fb;
margin-bottom: 10px;
}
.medical-feed-inline-editor {
position: relative;
z-index: 2;
width: auto;
max-width: none;
min-width: 0;
margin: 0 14px 14px 14px;
padding-top: 0;
align-self: stretch;
box-sizing: border-box;
}
.medical-feed-inline-editor iframe {
display: block;
width: 100%;
}
@media (max-width: 1200px) {
.medical-feed-inline-editor {
width: auto;
max-width: none;
min-width: 0;
}
}
@media (max-width: 991px) {
.medical-feed-inline-editor {
width: auto;
max-width: none;
min-width: 0;
}
}
</style>
<div id="medical-visits-page" class="panel panel-default">
<div class="panel-heading" style="display:flex;align-items:center;">
<span style="font-size:20px;line-height:1.2;"><?= Yii::t('AnimalManagementModule.base', '<strong>Medical Visits</strong>') ?></span>
</div>
<div class="panel-body">
<div style="display:flex;gap:18px;flex-wrap:wrap;align-items:center;margin-bottom:12px;">
<?= Html::a(
Html::encode($animal->getDisplayName()),
$space->createUrl('/animal_management/animals/view', ['id' => $animal->id]),
['style' => 'font-size:20px;font-weight:700;line-height:1.2;']
) ?>
<?= Html::a(
Yii::t('AnimalManagementModule.base', 'All Animals'),
$space->createUrl('/animal_management/animals/index'),
['style' => 'font-size:16px;line-height:1.2;']
) ?>
<?php if ($canManage): ?>
<?= Html::a('<i class="fa fa-plus"></i> ' . Yii::t('AnimalManagementModule.base', 'Add Medical Visit'), '#medical-add-inline', [
'class' => 'btn btn-success btn-sm',
'title' => Yii::t('AnimalManagementModule.base', 'Add Medical Visit'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Add Medical Visit'),
'data-toggle' => 'collapse',
]) ?>
<?php endif; ?>
</div>
<?php if ($canManage): ?>
<div id="medical-add-inline" class="collapse medical-feed-inline-editor<?= $openMedicalAdd ? ' in' : '' ?>" style="margin:0 0 14px 0;">
<iframe
src="<?= Html::encode($space->createUrl('/animal_management/animals/add-medical-visit-inline', ['id' => $animal->id, 'inline' => 1, 'returnTo' => 'medical-visits', '_v' => time()])) ?>"
style="width:100%;min-height:860px;border:1px solid rgba(255,255,255,0.22);border-radius:10px;background:transparent;"
loading="lazy"
></iframe>
</div>
<?php endif; ?>
<?php if (empty($medicalVisits)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No medical visits recorded.') ?></div>
<?php else: ?>
<?php foreach ($medicalVisits as $visit): ?>
<?php
$visitCustomValues = $visit->getCustomFieldDisplayValues($canManage);
$visitFieldsByKey = [];
$additionalVisitFields = [];
$medicalMedia = '';
foreach ($visitCustomValues as $customField) {
$fieldKey = (string)($customField['field_key'] ?? '');
if (in_array($fieldKey, $hiddenMedicalKeys, true)) {
continue;
}
$fieldValue = trim((string)($customField['value'] ?? ''));
if ($fieldValue === '') {
continue;
}
if ($fieldKey === 'medical_media_reference' || $fieldKey === 'media_reference') {
$medicalMedia = $fieldValue;
continue;
}
if (in_array($fieldKey, $knownMedicalKeys, true)) {
$visitFieldsByKey[$fieldKey] = [
'label' => (string)($medicalVitalLabelOverrides[$fieldKey] ?? ($customField['label'] ?? $fieldKey)),
'value' => $fieldValue,
];
continue;
}
$additionalVisitFields[] = [
'label' => (string)($customField['label'] ?? $fieldKey),
'value' => $fieldValue,
];
}
$hasMedicalMedia = $medicalMedia !== '' && (preg_match('/^https?:\/\//i', $medicalMedia) || substr($medicalMedia, 0, 1) === '/');
$visitDateDisplay = DateDisplayHelper::format((string)$visit->visit_at);
$vitalKeys = ['weight', 'pulse', 'blood_pressure', 'oxygen'];
$hasVitals = false;
foreach ($vitalKeys as $vitalKey) {
if (!empty($visitFieldsByKey[$vitalKey]['value'])) {
$hasVitals = true;
break;
}
}
?>
<div id="medical-visit-<?= (int)$visit->id ?>" class="medical-feed-card">
<?php if ($hasMedicalMedia): ?>
<img class="medical-feed-cover" src="<?= Html::encode($medicalMedia) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Medical media') ?>">
<?php endif; ?>
<div class="medical-feed-overlay"></div>
<div class="medical-feed-content">
<div class="medical-feed-top-row">
<span class="medical-feed-date"><?= Html::encode($visitDateDisplay !== '' ? $visitDateDisplay : (string)$visit->visit_at) ?></span>
<?php if ($hasVitals): ?>
<div class="medical-chip-row medical-chip-row--top">
<?php foreach ($vitalKeys as $vitalKey): ?>
<?php if (empty($visitFieldsByKey[$vitalKey]['value'])) { continue; } ?>
<span class="medical-chip">
<?= Html::encode($visitFieldsByKey[$vitalKey]['label']) ?>: <?= Html::encode($visitFieldsByKey[$vitalKey]['value']) ?>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="medical-feed-text-wrap">
<div class="medical-feed-header">
<div>
<?php if (!empty($visit->provider_name)): ?>
<div style="font-size:12px;color:rgba(239,245,251,0.86);margin-top:2px;"><?= Yii::t('AnimalManagementModule.base', 'Provider') ?>: <?= Html::encode((string)$visit->provider_name) ?></div>
<?php endif; ?>
</div>
<?php if ($canManage): ?>
<?= Html::a(
'<i class="fa fa-pencil"></i>',
'#medical-edit-inline-' . (int)$visit->id,
[
'class' => 'btn btn-xs btn-default',
'data-toggle' => 'collapse',
'title' => Yii::t('AnimalManagementModule.base', 'Edit'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Edit'),
]
) ?>
<?php endif; ?>
</div>
<?php if (!empty($visit->notes)): ?>
<div class="medical-section-label"><?= Yii::t('AnimalManagementModule.base', 'Clinical Notes') ?></div>
<div class="medical-section-body"><?= nl2br(Html::encode((string)$visit->notes)) ?></div>
<?php endif; ?>
<?php if (!empty($visit->recommendations)): ?>
<div class="medical-section-label"><?= Yii::t('AnimalManagementModule.base', 'Recommendations') ?></div>
<div class="medical-section-body"><?= nl2br(Html::encode((string)$visit->recommendations)) ?></div>
<?php endif; ?>
<?php
$conditionKeys = ['chronic_conditions', 'acute_conditions', 'special_needs'];
foreach ($conditionKeys as $conditionKey):
if (empty($visitFieldsByKey[$conditionKey]['value'])) {
continue;
}
?>
<div class="medical-section-label"><?= Html::encode($visitFieldsByKey[$conditionKey]['label']) ?></div>
<div class="medical-section-body"><?= nl2br(Html::encode($visitFieldsByKey[$conditionKey]['value'])) ?></div>
<?php endforeach; ?>
<?php
$contactKeys = [
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
];
$hasContact = false;
foreach ($contactKeys as $contactKey) {
if (!empty($visitFieldsByKey[$contactKey]['value'])) {
$hasContact = true;
break;
}
}
?>
<?php if ($hasContact): ?>
<?php
$contactLines = [];
if (!empty($visitFieldsByKey['physician_name']['value'])) {
$contactLines[] = (string)$visitFieldsByKey['physician_name']['value'];
}
if (!empty($visitFieldsByKey['physician_business_name']['value'])) {
$contactLines[] = (string)$visitFieldsByKey['physician_business_name']['value'];
}
if (!empty($visitFieldsByKey['physician_street_address']['value'])) {
$contactLines[] = (string)$visitFieldsByKey['physician_street_address']['value'];
}
$cityStateZip = trim(implode(', ', array_filter([
trim((string)($visitFieldsByKey['physician_city']['value'] ?? '')),
trim((string)($visitFieldsByKey['physician_state']['value'] ?? '')),
])));
$zipValue = trim((string)($visitFieldsByKey['physician_zip']['value'] ?? ''));
if ($zipValue !== '') {
$cityStateZip = trim($cityStateZip . ($cityStateZip !== '' ? ' ' : '') . $zipValue);
}
if ($cityStateZip !== '') {
$contactLines[] = $cityStateZip;
}
$phones = array_filter([
trim((string)($visitFieldsByKey['physician_cell_phone']['value'] ?? '')),
trim((string)($visitFieldsByKey['physician_business_phone']['value'] ?? '')),
]);
if (!empty($phones)) {
$contactLines[] = implode(' · ', $phones);
}
if (!empty($visitFieldsByKey['physician_license_number']['value'])) {
$contactLines[] = (string)$visitFieldsByKey['physician_license_number']['value'];
}
?>
<div class="medical-section-label"><?= Yii::t('AnimalManagementModule.base', 'Care Contact') ?></div>
<div class="medical-section-body">
<?php foreach ($contactLines as $contactLine): ?>
<div><?= Html::encode($contactLine) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($visitFieldsByKey['date_of_most_recent_medical_visit']['value'])): ?>
<div class="medical-section-label"><?= Html::encode($visitFieldsByKey['date_of_most_recent_medical_visit']['label']) ?></div>
<div class="medical-section-body"><?= Html::encode(DateDisplayHelper::format((string)$visitFieldsByKey['date_of_most_recent_medical_visit']['value'])) ?></div>
<?php endif; ?>
<?php if (!empty($additionalVisitFields)): ?>
<div class="medical-section-label"><?= Yii::t('AnimalManagementModule.base', 'Additional Fields') ?></div>
<div class="medical-section-body">
<?php foreach ($additionalVisitFields as $additionalField): ?>
<div><strong><?= Html::encode((string)$additionalField['label']) ?>:</strong> <?= nl2br(Html::encode((string)$additionalField['value'])) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($canManage): ?>
<div id="medical-edit-inline-<?= (int)$visit->id ?>" class="collapse medical-feed-inline-editor<?= $openMedicalEditId === (int)$visit->id ? ' in' : '' ?>">
<iframe
src="<?= Html::encode($space->createUrl('/animal_management/animals/edit-medical-visit', ['id' => $animal->id, 'visitId' => $visit->id, 'inline' => 1, 'returnTo' => 'medical-visits', '_v' => time()])) ?>"
style="width:100%;min-height:760px;border:1px solid rgba(255,255,255,0.22);border-radius:10px;background:transparent;"
loading="lazy"
></iframe>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($canManage): ?>
<div class="modal fade" id="<?= Html::encode($medicalAddModalId) ?>" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Add Medical Visit') ?></h4>
</div>
<div class="modal-body">
<?php $medicalForm = ActiveForm::begin([
'action' => $space->createUrl('/animal_management/animals/add-medical-visit', ['id' => $animal->id]),
'options' => ['id' => $medicalFormId, 'enctype' => 'multipart/form-data'],
]); ?>
<?= Html::hiddenInput('returnTo', 'medical-visits') ?>
<?= Html::hiddenInput('medicalMediaGalleryPath', $newMedicalMediaPath, ['id' => 'medical-media-gallery-path']) ?>
<?= $medicalForm->errorSummary($medicalVisitForm, ['showAllErrors' => true]) ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Visit') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6"><?= $medicalForm->field($medicalVisitForm, 'visit_at')->input('datetime-local') ?></div>
<div class="col-sm-6"><?= $medicalForm->field($medicalVisitForm, 'provider_name') ?></div>
</div>
<?= $medicalForm->field($medicalVisitForm, 'notes')->textarea(['rows' => 3]) ?>
<?= $medicalForm->field($medicalVisitForm, 'recommendations')->textarea(['rows' => 3]) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Vitals') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-3"><?= $renderCustomField('weight', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-3"><?= $renderCustomField('pulse', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-3"><?= $renderCustomField('blood_pressure', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-3"><?= $renderCustomField('oxygen', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Conditions') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('chronic_conditions', $medicalVisitForm, $medicalCustomDefinitions) ?>
<?= $renderCustomField('acute_conditions', $medicalVisitForm, $medicalCustomDefinitions) ?>
<?= $renderCustomField('special_needs', $medicalVisitForm, $medicalCustomDefinitions) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Medical Visit Detail') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('date_of_most_recent_medical_visit', $medicalVisitForm, $medicalCustomDefinitions) ?>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Media') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-4" style="margin-bottom:8px;">
<div id="medical-media-preview" style="border-radius:8px;overflow:hidden;background:#f2f4f6;height:150px;display:flex;align-items:center;justify-content:center;">
<?php if ($hasNewMedicalMedia): ?>
<img src="<?= Html::encode($newMedicalMediaPath) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Selected medical media') ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>
<?php endif; ?>
</div>
</div>
<div class="col-sm-8">
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#<?= Html::encode($medicalGalleryModalId) ?>" style="margin-bottom:8px;">
<i class="fa fa-photo"></i> <?= Yii::t('AnimalManagementModule.base', 'Choose from Gallery or Upload') ?>
</button>
<div class="checkbox" style="margin-top:0;">
<label>
<input type="checkbox" name="removeMedicalMedia" value="1">
<?= Yii::t('AnimalManagementModule.base', 'Remove selected media') ?>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Physician') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-6"><?= $renderCustomField('physician_name', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-6"><?= $renderCustomField('physician_business_name', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-12"><?= $renderCustomField('physician_street_address', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_city', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_state', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_zip', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_cell_phone', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_business_phone', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
<div class="col-sm-4"><?= $renderCustomField('physician_license_number', $medicalVisitForm, $medicalCustomDefinitions) ?></div>
</div>
</div>
</div>
<?php if (!empty($remainingMedicalDefinitions)): ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Additional Details') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?php foreach ($remainingMedicalDefinitions as $fieldKey => $definition): ?>
<?= $renderCustomField($fieldKey, $medicalVisitForm, $remainingMedicalDefinitions) ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?= Button::save(Yii::t('AnimalManagementModule.base', 'Add Medical Visit'))->submit() ?>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($canManage): ?>
<div class="modal fade" id="<?= Html::encode($medicalGalleryModalId) ?>" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Select Medical Media from Gallery') ?></h4>
</div>
<div class="modal-body">
<?php if (empty($galleryItems)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No gallery images available yet.') ?></div>
<?php else: ?>
<div class="row" style="max-height:280px;overflow:auto;margin-bottom:10px;">
<?php foreach ($galleryItems as $galleryItem): ?>
<?php $galleryUrl = trim((string)$galleryItem->getImageUrl()); ?>
<?php if ($galleryUrl === '') { continue; } ?>
<div class="col-xs-6 col-sm-4" style="margin-bottom:8px;">
<button type="button" class="btn btn-default medical-media-select-thumb<?= $newMedicalMediaPath === $galleryUrl ? ' is-selected' : '' ?>" data-media-url="<?= Html::encode($galleryUrl) ?>" style="width:100%;padding:3px;">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Gallery image') ?>" style="width:100%;height:120px;object-fit:cover;display:block;">
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="form-group" style="margin-bottom:0;">
<label class="control-label" for="medicalMediaUpload"><?= Yii::t('AnimalManagementModule.base', 'Upload New Image') ?></label>
<input type="file" class="form-control" id="medicalMediaUpload" name="medicalMediaUpload" form="<?= Html::encode($medicalFormId) ?>" accept="image/*">
</div>
</div>
</div>
</div>
</div>
<?php
$this->registerJs(<<<JS
(function(){
var pageRootSelector = '#medical-visits-page';
var formSelector = '#{$medicalFormId}';
function scrollInlineEditorIntoView(editor) {
var editorNode = $(editor);
if (!editorNode.length) {
return;
}
var sideSpacing = parseFloat(editorNode.css('margin-left'));
if (!(sideSpacing > 0)) {
sideSpacing = parseFloat(editorNode.closest('.panel-body').css('padding-left'));
}
if (!(sideSpacing > 0)) {
sideSpacing = 14;
}
var fixedHeaderHeight = 0;
$('.navbar-fixed-top:visible, #topbar:visible, .topbar:visible, .layout-top-container:visible').each(function() {
var h = $(this).outerHeight() || 0;
if (h > fixedHeaderHeight) {
fixedHeaderHeight = h;
}
});
var topReserve = Math.max(sideSpacing, 14) + Math.max(fixedHeaderHeight, 64) + 28;
var top = Math.max(0, editorNode.offset().top - topReserve);
$('html, body').stop(true).animate({scrollTop: top}, 220);
}
function refreshMedicalVisitsPageRoot() {
return $.get(window.location.href).done(function(html) {
var doc = $('<div></div>').append($.parseHTML(html, document, true));
var nextRoot = doc.find(pageRootSelector).first();
if (!nextRoot.length) {
return;
}
$(pageRootSelector).replaceWith(nextRoot);
if (typeof window.initMedicalVisitsPage === 'function') {
window.initMedicalVisitsPage();
}
});
}
if (!window.__animalMedicalVisitsInlineListenerBound) {
window.__animalMedicalVisitsInlineListenerBound = true;
window.addEventListener('message', function(event) {
var data = event.data || {};
if (!data || typeof data !== 'object' || data.source !== 'animal-inline-editor') {
return;
}
if (data.type === 'cancel') {
if (data.collapseId) {
$('#' + data.collapseId).collapse('hide');
}
return;
}
if (data.type === 'saved') {
if (data.collapseId) {
$('#' + data.collapseId).collapse('hide');
}
refreshMedicalVisitsPageRoot();
}
});
}
window.initMedicalVisitsPage = function() {
$(document)
.off('shown.bs.collapse.medicalInlineScroll', pageRootSelector + ' .medical-feed-inline-editor')
.on('shown.bs.collapse.medicalInlineScroll', pageRootSelector + ' .medical-feed-inline-editor', function() {
scrollInlineEditorIntoView(this);
});
$(document)
.off('click.medicalInlineScroll', pageRootSelector + ' a[href^="#medical-edit-inline-"], ' + pageRootSelector + ' a[href="#medical-add-inline"]')
.on('click.medicalInlineScroll', pageRootSelector + ' a[href^="#medical-edit-inline-"], ' + pageRootSelector + ' a[href="#medical-add-inline"]', function() {
var target = $(this).attr('href');
if (!target || target.charAt(0) !== '#') {
return;
}
window.setTimeout(function() {
scrollInlineEditorIntoView($(target));
}, 260);
});
var preopenedEditor = $(pageRootSelector + ' .medical-feed-inline-editor.in').first();
if (preopenedEditor.length) {
window.setTimeout(function() {
scrollInlineEditorIntoView(preopenedEditor);
}, 260);
}
function renderMedicalPreview(source) {
var preview = $('#medical-media-preview');
if (!preview.length) {
return;
}
if (source) {
preview.html('<img src="' + source + '" alt="Selected medical media" style="width:100%;height:100%;object-fit:cover;">');
} else {
preview.html('<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>');
}
}
function markSelectedMedicalThumb(value) {
$('.medical-media-select-thumb').removeClass('is-selected');
if (!value) {
return;
}
$('.medical-media-select-thumb').each(function() {
if (($(this).attr('data-media-url') || '') === value) {
$(this).addClass('is-selected');
}
});
}
$(document).off('click.medicalMediaSelect', pageRootSelector + ' .medical-media-select-thumb').on('click.medicalMediaSelect', pageRootSelector + ' .medical-media-select-thumb', function() {
var mediaUrl = $(this).attr('data-media-url') || '';
$('#medical-media-gallery-path').val(mediaUrl);
markSelectedMedicalThumb(mediaUrl);
$('#medicalMediaUpload').val('');
$('input[name="removeMedicalMedia"]').prop('checked', false);
if (mediaUrl) {
renderMedicalPreview(mediaUrl);
}
$('#{$medicalGalleryModalId}').modal('hide');
});
$(document).off('change.medicalMediaUpload', '#medicalMediaUpload').on('change.medicalMediaUpload', '#medicalMediaUpload', function() {
var file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
return;
}
$('#medical-media-gallery-path').val('');
markSelectedMedicalThumb('');
$('input[name="removeMedicalMedia"]').prop('checked', false);
var reader = new FileReader();
reader.onload = function(e) {
renderMedicalPreview(e.target.result);
$('#{$medicalGalleryModalId}').modal('hide');
};
reader.readAsDataURL(file);
});
$(document).off('shown.bs.modal.medicalMediaModal', '#{$medicalGalleryModalId}').on('shown.bs.modal.medicalMediaModal', '#{$medicalGalleryModalId}', function() {
markSelectedMedicalThumb($('#medical-media-gallery-path').val());
});
$(document).off('submit.medicalVisitAjax', formSelector).on('submit.medicalVisitAjax', formSelector, function(event) {
event.preventDefault();
var form = this;
var formData = new FormData(form);
var submitButtons = $(form).find('button[type="submit"], input[type="submit"]');
submitButtons.prop('disabled', true);
$.ajax({
url: form.action,
type: 'POST',
data: formData,
processData: false,
contentType: false
}).always(function() {
submitButtons.prop('disabled', false);
}).done(function() {
$('#{$medicalAddModalId}').modal('hide');
refreshMedicalVisitsPageRoot();
});
});
};
window.initMedicalVisitsPage();
})();
JS
, \yii\web\View::POS_END);
?>
<?php endif; ?>

View File

@@ -0,0 +1,694 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\helpers\DateDisplayHelper;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\AnimalProgressUpdate;
use humhub\modules\animal_management\models\forms\AnimalProgressUpdateForm;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
/* @var Space $space */
/* @var Animal $animal */
/* @var bool $canManage */
/* @var AnimalProgressUpdateForm $progressUpdateForm */
/* @var AnimalProgressUpdate[] $progressUpdates */
/* @var AnimalGalleryItem[] $galleryItems */
$renderCustomField = static function (string $fieldKey, AnimalProgressUpdateForm $model, array $definitions): string {
if (!isset($definitions[$fieldKey])) {
return '';
}
$definition = $definitions[$fieldKey];
$inputType = (string)$definition['input_type'];
$label = (string)$definition['label'];
if ((int)$definition['required'] === 1) {
$label .= ' *';
}
$fieldName = "AnimalProgressUpdateForm[customFields][$fieldKey]";
$fieldValue = $model->customFields[$fieldKey] ?? '';
ob_start();
?>
<?php if ($inputType === 'textarea'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textarea($fieldName, (string)$fieldValue, ['class' => 'form-control', 'rows' => 3, 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'boolean'): ?>
<div class="checkbox" style="margin-bottom:10px;">
<label>
<?= Html::hiddenInput($fieldName, '0') ?>
<?= Html::checkbox($fieldName, !empty($fieldValue), ['value' => '1']) ?>
<?= Html::encode($label) ?>
</label>
</div>
<?php elseif ($inputType === 'select'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::dropDownList(
$fieldName,
(string)$fieldValue,
$model->getCustomFieldSelectOptions($fieldKey),
['class' => 'form-control', 'prompt' => Yii::t('AnimalManagementModule.base', 'Select...'), 'id' => "animalprogressupdateform-customfields-$fieldKey"]
) ?>
</div>
<?php elseif ($inputType === 'number'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('number', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'step' => 'any', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'date'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('date', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php elseif ($inputType === 'datetime'): ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::input('datetime-local', $fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php else: ?>
<div class="form-group">
<label class="control-label" for="animalprogressupdateform-customfields-<?= Html::encode($fieldKey) ?>"><?= Html::encode($label) ?></label>
<?= Html::textInput($fieldName, (string)$fieldValue, ['class' => 'form-control', 'id' => "animalprogressupdateform-customfields-$fieldKey"]) ?>
</div>
<?php endif; ?>
<?php
return (string)ob_get_clean();
};
$progressCustomDefinitions = $progressUpdateForm->getCustomFieldDefinitions();
$knownProgressKeys = ['progress_notes', 'routine_updates', 'media_reference'];
$otherProgressCustomDefinitions = [];
foreach ($progressCustomDefinitions as $fieldKey => $definition) {
if (in_array($fieldKey, $knownProgressKeys, true)) {
continue;
}
$otherProgressCustomDefinitions[$fieldKey] = $definition;
}
$currentMediaReference = trim((string)($progressUpdateForm->customFields['media_reference'] ?? ''));
$progressAddModalId = 'add-progress-update-modal';
$progressFormId = 'add-progress-update-main-form';
$openProgressEditId = (int)Yii::$app->request->get('inlineProgressEdit', 0);
$openProgressAdd = (int)Yii::$app->request->get('inlineProgressAdd', 0) === 1;
?>
<style>
.progress-feed-card {
position: relative;
overflow: hidden;
border: 1px solid #d5dfe8;
border-radius: 12px;
background: #223446;
margin-bottom: 12px;
min-height: 240px;
box-shadow: 0 8px 22px rgba(12, 24, 36, 0.16);
}
.progress-feed-cover {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.progress-feed-overlay {
position: absolute;
inset: 0;
background: linear-gradient(110deg, rgba(10, 18, 28, 0.28) 10%, rgba(10, 18, 28, 0.55) 52%, rgba(10, 18, 28, 0.75) 100%);
}
.progress-feed-content {
position: relative;
z-index: 1;
min-height: 240px;
padding: 14px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
.progress-feed-top-row {
width: 100%;
max-width: none;
margin-left: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.progress-feed-date {
font-size: 15px;
font-weight: 700;
color: #ffffff;
margin-right: auto;
}
.progress-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.progress-chip-row--top {
margin-bottom: 0;
justify-content: flex-end;
}
.progress-chip {
display: inline-block;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
color: #ffffff;
}
.progress-feed-details {
width: 55%;
max-width: 55%;
min-width: 0;
margin-left: auto;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.22);
background: rgba(10, 18, 28, 0.5);
backdrop-filter: blur(2px);
padding: 12px;
color: #ecf2f8;
}
.progress-feed-header {
display: flex;
justify-content: flex-end;
align-items: flex-start;
gap: 8px;
margin-bottom: 8px;
}
.progress-feed-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: .04em;
color: rgba(231, 241, 249, 0.78);
margin-bottom: 4px;
}
.progress-feed-copy {
color: #eff5fb;
margin-bottom: 10px;
}
.progress-feed-inline-editor {
position: relative;
z-index: 2;
width: auto;
max-width: none;
min-width: 0;
margin: 0 0 14px 0;
padding-top: 0;
align-self: stretch;
box-sizing: border-box;
}
.progress-feed-inline-editor iframe {
display: block;
width: 100%;
}
@media (max-width: 1200px) {
.progress-feed-details {
width: 55%;
max-width: 55%;
min-width: 0;
}
}
@media (max-width: 991px) {
.progress-feed-details {
width: 55%;
max-width: 55%;
min-width: 0;
}
.progress-chip-row--top {
justify-content: flex-start;
}
}
</style>
<div id="progress-updates-page" class="panel panel-default">
<div class="panel-heading" style="display:flex;align-items:center;">
<span style="font-size:20px;line-height:1.2;"><?= Yii::t('AnimalManagementModule.base', '<strong>Progress Feed</strong>') ?></span>
</div>
<div class="panel-body">
<div style="display:flex;gap:18px;flex-wrap:wrap;align-items:center;margin-bottom:12px;">
<?= Html::a(
Html::encode($animal->getDisplayName()),
$space->createUrl('/animal_management/animals/view', ['id' => $animal->id]),
['style' => 'font-size:20px;font-weight:700;line-height:1.2;']
) ?>
<?= Html::a(
Yii::t('AnimalManagementModule.base', 'All Animals'),
$space->createUrl('/animal_management/animals/index'),
['style' => 'font-size:16px;line-height:1.2;']
) ?>
<?php if ($canManage): ?>
<?= Html::a('<i class="fa fa-plus"></i> ' . Yii::t('AnimalManagementModule.base', 'Add Progress Update'), '#progress-add-inline', [
'class' => 'btn btn-success btn-sm',
'title' => Yii::t('AnimalManagementModule.base', 'Add Progress Update'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Add Progress Update'),
'data-toggle' => 'collapse',
]) ?>
<?php endif; ?>
</div>
<?php if ($canManage): ?>
<div id="progress-add-inline" class="collapse progress-feed-inline-editor<?= $openProgressAdd ? ' in' : '' ?>" style="margin:0 0 14px 0;">
<iframe
src="<?= Html::encode($space->createUrl('/animal_management/animals/add-progress-update-inline', ['id' => $animal->id, 'inline' => 1, 'returnTo' => 'progress-updates', '_v' => time()])) ?>"
style="width:100%;min-height:860px;border:1px solid rgba(255,255,255,0.22);border-radius:10px;background:transparent;"
loading="lazy"
></iframe>
</div>
<?php endif; ?>
<?php if (empty($progressUpdates)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No progress updates recorded.') ?></div>
<?php else: ?>
<?php foreach ($progressUpdates as $update): ?>
<?php
$progressCustomValues = $update->getCustomFieldDisplayValues($canManage);
$mediaReference = '';
$progressCustomDisplayValues = [];
foreach ($progressCustomValues as $customField) {
if ((string)($customField['field_key'] ?? '') === 'media_reference') {
$mediaReference = trim((string)$customField['value']);
continue;
}
$progressCustomDisplayValues[] = $customField;
}
$hasMediaImage = $mediaReference !== '' && (preg_match('/^https?:\/\//i', $mediaReference) || substr($mediaReference, 0, 1) === '/');
?>
<div id="progress-update-<?= (int)$update->id ?>" class="progress-feed-card">
<?php if ($hasMediaImage): ?>
<img class="progress-feed-cover" src="<?= Html::encode($mediaReference) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Progress media') ?>">
<?php endif; ?>
<div class="progress-feed-overlay"></div>
<div class="progress-feed-content">
<div class="progress-feed-top-row">
<span class="progress-feed-date"><?= Html::encode(DateDisplayHelper::format((string)$update->update_at)) ?></span>
<?php if (!empty($update->weight) || !empty($update->vitals)): ?>
<div class="progress-chip-row progress-chip-row--top">
<?php if (!empty($update->weight)): ?><span class="progress-chip"><?= Yii::t('AnimalManagementModule.base', 'Weight') ?>: <?= Html::encode((string)$update->weight) ?></span><?php endif; ?>
<?php if (!empty($update->vitals)): ?><span class="progress-chip"><?= Yii::t('AnimalManagementModule.base', 'Vitals') ?></span><?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="progress-feed-details">
<div class="progress-feed-header">
<?php if ($canManage): ?>
<?= Html::a(
'<i class="fa fa-pencil"></i>',
'#progress-edit-inline-' . (int)$update->id,
[
'class' => 'btn btn-xs btn-default',
'data-toggle' => 'collapse',
'title' => Yii::t('AnimalManagementModule.base', 'Edit'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Edit'),
]
) ?>
<?php endif; ?>
</div>
<?php if (!empty($update->vitals)): ?><div class="progress-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Vitals') ?></div><div class="progress-feed-copy"><?= nl2br(Html::encode($update->vitals)) ?></div><?php endif; ?>
<?php if (!empty($update->behavior_notes)): ?><div class="progress-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Behavior') ?></div><div class="progress-feed-copy"><?= nl2br(Html::encode($update->behavior_notes)) ?></div><?php endif; ?>
<?php if (!empty($update->meal_plan_changes)): ?><div class="progress-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Meal Plan') ?></div><div class="progress-feed-copy"><?= nl2br(Html::encode($update->meal_plan_changes)) ?></div><?php endif; ?>
<?php if (!empty($update->housing_changes)): ?><div class="progress-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Housing') ?></div><div class="progress-feed-copy"><?= nl2br(Html::encode($update->housing_changes)) ?></div><?php endif; ?>
<?php if (!empty($update->medical_concerns)): ?><div class="progress-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Medical Concerns') ?></div><div class="progress-feed-copy"><?= nl2br(Html::encode($update->medical_concerns)) ?></div><?php endif; ?>
<?php if (!empty($progressCustomDisplayValues)): ?>
<div class="progress-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Additional Fields') ?></div>
<div class="progress-feed-copy">
<?php foreach ($progressCustomDisplayValues as $customField): ?>
<div><strong><?= Html::encode((string)$customField['label']) ?>:</strong> <?= nl2br(Html::encode((string)$customField['value'])) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php if ($canManage): ?>
<div id="progress-edit-inline-<?= (int)$update->id ?>" class="collapse progress-feed-inline-editor<?= $openProgressEditId === (int)$update->id ? ' in' : '' ?>">
<iframe
src="<?= Html::encode($space->createUrl('/animal_management/animals/edit-progress-update', ['id' => $animal->id, 'updateId' => $update->id, 'inline' => 1, 'returnTo' => 'progress-updates', '_v' => time()])) ?>"
style="width:100%;min-height:760px;border:1px solid rgba(255,255,255,0.22);border-radius:10px;background:transparent;"
loading="lazy"
></iframe>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($canManage): ?>
<div class="modal fade" id="<?= Html::encode($progressAddModalId) ?>" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Add Progress Update') ?></h4>
</div>
<div class="modal-body">
<?php $progressForm = ActiveForm::begin([
'action' => $space->createUrl('/animal_management/animals/add-progress-update', ['id' => $animal->id]),
'options' => ['id' => $progressFormId, 'enctype' => 'multipart/form-data'],
]); ?>
<?= Html::hiddenInput('returnTo', 'progress-updates') ?>
<?= $progressForm->errorSummary($progressUpdateForm, ['showAllErrors' => true]) ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Progress Update') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<div class="row">
<div class="col-sm-4"><?= $progressForm->field($progressUpdateForm, 'weight') ?></div>
<div class="col-sm-8"><?= $progressForm->field($progressUpdateForm, 'vitals')->textInput(['maxlength' => 255]) ?></div>
<div class="col-sm-6"><?= $progressForm->field($progressUpdateForm, 'behavior_notes')->textarea(['rows' => 2]) ?></div>
<div class="col-sm-6"><?= $progressForm->field($progressUpdateForm, 'medical_concerns')->textarea(['rows' => 2]) ?></div>
<div class="col-sm-6"><?= $progressForm->field($progressUpdateForm, 'meal_plan_changes')->textarea(['rows' => 2]) ?></div>
<div class="col-sm-6"><?= $progressForm->field($progressUpdateForm, 'housing_changes')->textarea(['rows' => 2]) ?></div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Notes') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $renderCustomField('progress_notes', $progressUpdateForm, $progressCustomDefinitions) ?>
<?= $renderCustomField('routine_updates', $progressUpdateForm, $progressCustomDefinitions) ?>
</div>
</div>
<?php if (!empty($otherProgressCustomDefinitions)): ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Additional Details') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?php foreach ($otherProgressCustomDefinitions as $fieldKey => $definition): ?>
<?= $renderCustomField($fieldKey, $progressUpdateForm, $otherProgressCustomDefinitions) ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Media') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<input type="hidden" id="progress-media-gallery-path" name="progressMediaGalleryPath" value="<?= Html::encode($currentMediaReference) ?>">
<div class="row">
<div class="col-sm-4" style="margin-bottom:8px;">
<div id="progress-media-preview" style="border-radius:8px;overflow:hidden;background:#f2f4f6;height:150px;display:flex;align-items:center;justify-content:center;">
<?php if ($currentMediaReference !== '' && (preg_match('/^https?:\/\//i', $currentMediaReference) || substr($currentMediaReference, 0, 1) === '/')): ?>
<img src="<?= Html::encode($currentMediaReference) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Selected media') ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<i class="fa fa-image fa-2x" style="color:#a7b0b8;"></i>
<?php endif; ?>
</div>
</div>
<div class="col-sm-8">
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#progress-media-modal" style="margin-bottom:8px;">
<i class="fa fa-photo"></i> <?= Yii::t('AnimalManagementModule.base', 'Choose from Gallery or Upload') ?>
</button>
<div class="checkbox" style="margin-top:0;">
<label>
<input type="checkbox" name="removeProgressMedia" value="1">
<?= Yii::t('AnimalManagementModule.base', 'Remove selected media') ?>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default" style="margin-bottom:12px;">
<div class="panel-heading"><strong><?= Yii::t('AnimalManagementModule.base', 'Social Post') ?></strong></div>
<div class="panel-body" style="padding-bottom:8px;">
<?= $progressForm->field($progressUpdateForm, 'post_to_space_feed')->checkbox() ?>
<?= $progressForm->field($progressUpdateForm, 'post_to_animal_feed')->checkbox() ?>
</div>
</div>
<?= Button::save(Yii::t('AnimalManagementModule.base', 'Add Progress Update'))->submit() ?>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>
</div>
<div class="modal fade" id="progress-media-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Select Progress Media') ?></h4>
</div>
<div class="modal-body">
<?php if (empty($galleryItems)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No gallery images available yet.') ?></div>
<?php else: ?>
<div class="row" style="max-height:280px;overflow:auto;margin-bottom:10px;">
<?php foreach ($galleryItems as $galleryItem): ?>
<?php $galleryUrl = trim((string)$galleryItem->getImageUrl()); ?>
<?php if ($galleryUrl === '') { continue; } ?>
<div class="col-xs-6 col-sm-4" style="margin-bottom:8px;">
<button type="button" class="btn btn-default progress-media-select-thumb" data-media-url="<?= Html::encode($galleryUrl) ?>" style="width:100%;padding:3px;">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Gallery image') ?>" style="width:100%;height:120px;object-fit:cover;border-radius:4px;">
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="form-group" style="margin-bottom:0;">
<label class="control-label" for="progressMediaUpload"><?= Yii::t('AnimalManagementModule.base', 'Upload New Image') ?></label>
<input type="file" class="form-control" id="progressMediaUpload" name="progressMediaUpload" form="<?= Html::encode($progressFormId) ?>" accept="image/*">
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php
$this->registerCss(<<<CSS
.progress-media-select-thumb.is-selected {
border-color: #2f7df4 !important;
box-shadow: 0 0 0 2px rgba(47, 125, 244, 0.22);
}
CSS
);
$this->registerJs(<<<JS
(function() {
var pageRootSelector = '#progress-updates-page';
var formSelector = '#{$progressFormId}';
function scrollInlineEditorIntoView(editor) {
var editorNode = $(editor);
if (!editorNode.length) {
return;
}
var sideSpacing = parseFloat(editorNode.css('margin-left'));
if (!(sideSpacing > 0)) {
sideSpacing = parseFloat(editorNode.closest('.panel-body').css('padding-left'));
}
if (!(sideSpacing > 0)) {
sideSpacing = 14;
}
var fixedHeaderHeight = 0;
$('.navbar-fixed-top:visible, #topbar:visible, .topbar:visible, .layout-top-container:visible').each(function() {
var h = $(this).outerHeight() || 0;
if (h > fixedHeaderHeight) {
fixedHeaderHeight = h;
}
});
var topReserve = Math.max(sideSpacing, 14) + Math.max(fixedHeaderHeight, 64) + 28;
var top = Math.max(0, editorNode.offset().top - topReserve);
$('html, body').stop(true).animate({scrollTop: top}, 220);
}
function refreshProgressUpdatesPageRoot() {
return $.get(window.location.href).done(function(html) {
var doc = $('<div></div>').append($.parseHTML(html, document, true));
var nextRoot = doc.find(pageRootSelector).first();
if (!nextRoot.length) {
return;
}
$(pageRootSelector).replaceWith(nextRoot);
if (typeof window.initProgressUpdatesPage === 'function') {
window.initProgressUpdatesPage();
}
});
}
if (!window.__animalProgressUpdatesInlineListenerBound) {
window.__animalProgressUpdatesInlineListenerBound = true;
window.addEventListener('message', function(event) {
var data = event.data || {};
if (!data || typeof data !== 'object' || data.source !== 'animal-inline-editor') {
return;
}
if (data.type === 'cancel') {
if (data.collapseId) {
$('#' + data.collapseId).collapse('hide');
}
return;
}
if (data.type === 'saved') {
if (data.collapseId) {
$('#' + data.collapseId).collapse('hide');
}
refreshProgressUpdatesPageRoot();
}
});
}
window.initProgressUpdatesPage = function() {
$(document)
.off('shown.bs.collapse.progressInlineScroll', pageRootSelector + ' .progress-feed-inline-editor')
.on('shown.bs.collapse.progressInlineScroll', pageRootSelector + ' .progress-feed-inline-editor', function() {
scrollInlineEditorIntoView(this);
});
$(document)
.off('click.progressInlineScroll', pageRootSelector + ' a[href^="#progress-edit-inline-"], ' + pageRootSelector + ' a[href="#progress-add-inline"]')
.on('click.progressInlineScroll', pageRootSelector + ' a[href^="#progress-edit-inline-"], ' + pageRootSelector + ' a[href="#progress-add-inline"]', function() {
var target = $(this).attr('href');
if (!target || target.charAt(0) !== '#') {
return;
}
window.setTimeout(function() {
scrollInlineEditorIntoView($(target));
}, 260);
});
var preopenedEditor = $(pageRootSelector + ' .progress-feed-inline-editor.in').first();
if (preopenedEditor.length) {
window.setTimeout(function() {
scrollInlineEditorIntoView(preopenedEditor);
}, 260);
}
function markSelectedMediaThumb(value) {
$('.progress-media-select-thumb').removeClass('is-selected');
if (!value) {
return;
}
$('.progress-media-select-thumb').each(function() {
if (($(this).attr('data-media-url') || '') === value) {
$(this).addClass('is-selected');
}
});
}
$(document).off('click.progressMediaSelect', '.progress-media-select-thumb').on('click.progressMediaSelect', '.progress-media-select-thumb', function(event) {
event.preventDefault();
var mediaUrl = $(this).attr('data-media-url') || '';
$('#progress-media-gallery-path').val(mediaUrl);
markSelectedMediaThumb(mediaUrl);
$('#progressMediaUpload').val('');
$('input[name="removeProgressMedia"]').prop('checked', false);
if (mediaUrl) {
$('#progress-media-preview').html('<img src="' + mediaUrl + '" alt="Selected media" style="width:100%;height:100%;object-fit:cover;">');
}
$('#progress-media-modal').modal('hide');
});
$(document).off('change.progressMediaUpload', '#progressMediaUpload').on('change.progressMediaUpload', '#progressMediaUpload', function() {
var file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
return;
}
$('#progress-media-gallery-path').val('');
markSelectedMediaThumb('');
$('input[name="removeProgressMedia"]').prop('checked', false);
var reader = new FileReader();
reader.onload = function(e) {
$('#progress-media-preview').html('<img src="' + e.target.result + '" alt="Selected media" style="width:100%;height:100%;object-fit:cover;">');
$('#progress-media-modal').modal('hide');
};
reader.readAsDataURL(file);
});
$(document).off('shown.bs.modal.progressMediaModal', '#progress-media-modal').on('shown.bs.modal.progressMediaModal', '#progress-media-modal', function() {
markSelectedMediaThumb($('#progress-media-gallery-path').val());
});
$(document).off('show.bs.modal.progressMediaStack', '#progress-media-modal').on('show.bs.modal.progressMediaStack', '#progress-media-modal', function() {
var zIndex = 1060 + (10 * $('.modal.in:visible').length);
$(this).css('z-index', zIndex);
window.setTimeout(function() {
$('.modal-backdrop').not('.progress-media-stack').last().css('z-index', zIndex - 1).addClass('progress-media-stack');
}, 0);
});
$(document).off('hidden.bs.modal.progressMediaStack', '#progress-media-modal').on('hidden.bs.modal.progressMediaStack', '#progress-media-modal', function() {
$(this).css('z-index', '');
if ($('#{$progressAddModalId}').is(':visible')) {
$('body').addClass('modal-open');
}
});
$(document).off('submit.progressAjax', formSelector).on('submit.progressAjax', formSelector, function(event) {
event.preventDefault();
var form = this;
var formData = new FormData(form);
var submitButtons = $(form).find('button[type="submit"], input[type="submit"]');
submitButtons.prop('disabled', true);
$.ajax({
url: form.action,
type: 'POST',
data: formData,
processData: false,
contentType: false
}).always(function() {
submitButtons.prop('disabled', false);
}).done(function() {
$('#{$progressAddModalId}').modal('hide');
refreshProgressUpdatesPageRoot();
});
});
};
window.initProgressUpdatesPage();
})();
JS
);
?>

View File

@@ -0,0 +1,31 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\forms\TransferRequestForm;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
use yii\bootstrap\ActiveForm;
/* @var Space $space */
/* @var Animal $animal */
/* @var TransferRequestForm $model */
?>
<div class="panel panel-default">
<div class="panel-heading">
<?= Yii::t('AnimalManagementModule.base', '<strong>Transfer Request</strong> for {animal}', ['animal' => $animal->getDisplayName()]) ?>
</div>
<div class="panel-body">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'to_space_id')->dropDownList($model->getTargetOptions(), ['prompt' => Yii::t('AnimalManagementModule.base', 'Select destination rescue')]) ?>
<?= $form->field($model, 'request_message')->textarea(['rows' => 4]) ?>
<?= $form->field($model, 'conditions_text')->textarea(['rows' => 3]) ?>
<?= Button::save(Yii::t('AnimalManagementModule.base', 'Send Request'))->submit() ?>
<?= Button::asLink(Yii::t('AnimalManagementModule.base', 'Cancel'))
->link($space->createUrl('/animal_management/animals/index')) ?>
<?php ActiveForm::end(); ?>
</div>
</div>

948
views/animals/view.php Normal file
View File

@@ -0,0 +1,948 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\helpers\DateDisplayHelper;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\animal_management\models\AnimalProgressUpdate;
use humhub\modules\animal_management\models\AnimalTransfer;
use humhub\modules\animal_management\models\AnimalTransferEvent;
use humhub\modules\gallery\assets\Assets as GalleryAssets;
use humhub\modules\space\models\Space;
use yii\helpers\Html;
/* @var Space $space */
/* @var Animal $animal */
/* @var bool $canManage */
/* @var AnimalMedicalVisit[] $medicalVisits */
/* @var AnimalProgressUpdate[] $progressUpdates */
/* @var AnimalTransfer[] $transfers */
/* @var AnimalTransferEvent[] $transferEvents */
/* @var AnimalGalleryItem[] $galleryItems */
/* @var array $customFieldValues */
/* @var string $animalCoverImageUrl */
/* @var array $detailHeroFields */
$openMedicalEditId = (int)Yii::$app->request->get('inlineMedicalEdit', 0);
$openProgressEditId = (int)Yii::$app->request->get('inlineProgressEdit', 0);
$coverImageUrl = trim((string)$animalCoverImageUrl);
$hasCoverImage = $coverImageUrl !== '' && (preg_match('/^https?:\/\//i', $coverImageUrl) || substr($coverImageUrl, 0, 1) === '/');
$statusLabel = Animal::statusOptions()[$animal->status] ?? (string)$animal->status;
$detailFieldMap = [
'name' => (string)$animal->getDisplayName(),
'species' => (string)$animal->species,
'breed' => (string)$animal->breed,
'sex' => (string)$animal->sex,
'status' => (string)$statusLabel,
'location_name' => (string)$animal->location_name,
'animal_uid' => (string)$animal->animal_uid,
'public_summary' => trim((string)$animal->public_summary),
'last_medical' => !empty($medicalVisits) ? DateDisplayHelper::format((string)$medicalVisits[0]->visit_at) : '',
];
$heroFieldValues = [];
foreach ($detailHeroFields as $fieldKey) {
$fieldKey = trim((string)$fieldKey);
if ($fieldKey === '' || $fieldKey === 'name' || !array_key_exists($fieldKey, $detailFieldMap)) {
continue;
}
$value = trim((string)$detailFieldMap[$fieldKey]);
if ($value === '') {
continue;
}
$heroFieldValues[] = $value;
}
$customHeroCount = 0;
foreach ($customFieldValues as $customField) {
if ($customHeroCount >= 3) {
break;
}
$label = trim((string)($customField['label'] ?? ''));
$value = trim((string)($customField['value'] ?? ''));
if ($label === '' || $value === '') {
continue;
}
$heroFieldValues[] = $label . ': ' . $value;
$customHeroCount++;
}
if (class_exists(GalleryAssets::class)) {
GalleryAssets::register($this);
}
$uiGalleryId = 'animal-gallery-' . (int)$animal->id;
?>
<div class="panel panel-default">
<div style="position:relative;min-height:320px;overflow:hidden;border-radius:12px;background:#dbe3eb;">
<?php if ($hasCoverImage): ?>
<img src="<?= Html::encode($coverImageUrl) ?>" alt="<?= Html::encode($animal->getDisplayName()) ?>" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#9aa5b1;">
<i class="fa fa-paw fa-4x"></i>
</div>
<?php endif; ?>
<div style="position:absolute;inset:0;background:linear-gradient(180deg, rgba(7,10,16,0.08) 0%, rgba(7,10,16,0.6) 56%, rgba(7,10,16,0.84) 100%);"></div>
<?php if ($canManage): ?>
<div style="position:absolute;top:12px;right:12px;z-index:2;">
<a href="<?= Html::encode($space->createUrl('/animal_management/animals/edit', ['id' => $animal->id])) ?>" class="btn btn-default btn-sm" title="<?= Yii::t('AnimalManagementModule.base', 'Edit Profile') ?>" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Edit Profile') ?>" style="border-radius:999px;background:rgba(255,255,255,0.92);border:0;">
<i class="fa fa-pencil"></i>
</a>
</div>
<?php endif; ?>
<div style="position:absolute;left:14px;right:14px;bottom:14px;color:#fff;z-index:1;">
<div style="font-size:30px;line-height:1.1;font-weight:800;margin-bottom:8px;text-shadow:0 3px 10px rgba(0,0,0,0.45);">
<?= Html::encode($animal->getDisplayName()) ?>
</div>
<?php if (!empty($heroFieldValues)): ?>
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px;">
<?php foreach ($heroFieldValues as $heroFieldValue): ?>
<span style="display:inline-block;background:rgba(15,23,42,0.58);border:1px solid rgba(255,255,255,0.28);padding:3px 10px;border-radius:999px;font-size:12px;">
<?= Html::encode($heroFieldValue) ?>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($animal->public_summary)): ?>
<div style="font-size:13px;line-height:1.35;text-shadow:0 2px 8px rgba(0,0,0,0.45);max-width:960px;">
<?= nl2br(Html::encode((string)$animal->public_summary)) ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php
$this->registerJs(<<<'JS'
(function() {
function getCsrfPayload() {
var csrfParam = $('meta[name="csrf-param"]').attr('content') || '';
var csrfToken = $('meta[name="csrf-token"]').attr('content') || '';
var payload = {};
if (csrfParam && csrfToken) {
payload[csrfParam] = csrfToken;
}
return payload;
}
function scrollInlineEditorIntoView(editor) {
var $editor = $(editor);
if (!$editor.length) {
return;
}
var sideSpacing = parseFloat($editor.css('margin-left'));
if (!(sideSpacing > 0)) {
sideSpacing = parseFloat($editor.closest('.panel-body').css('padding-left'));
}
if (!(sideSpacing > 0)) {
sideSpacing = 14;
}
var fixedHeaderHeight = 0;
$('.navbar-fixed-top:visible, #topbar:visible, .topbar:visible, .layout-top-container:visible').each(function() {
var h = $(this).outerHeight() || 0;
if (h > fixedHeaderHeight) {
fixedHeaderHeight = h;
}
});
var topReserve = Math.max(sideSpacing, 14) + Math.max(fixedHeaderHeight, 64) + 28;
var top = Math.max(0, $editor.offset().top - topReserve);
$('html, body').stop(true).animate({scrollTop: top}, 220);
}
function refreshPanels(selectors) {
selectors = selectors || [];
if (!selectors.length) {
return $.Deferred().resolve().promise();
}
return $.get(window.location.href).done(function(html) {
var $doc = $('<div></div>').append($.parseHTML(html, document, true));
selectors.forEach(function(selector) {
var $next = $doc.find(selector).first();
if ($next.length) {
$(selector).replaceWith($next);
}
});
});
}
window.addEventListener('message', function(event) {
var data = event.data || {};
if (!data || typeof data !== 'object' || data.source !== 'animal-inline-editor') {
return;
}
if (data.type === 'cancel') {
if (data.collapseId) {
$('#' + data.collapseId).collapse('hide');
}
return;
}
if (data.type === 'saved') {
if (data.collapseId) {
$('#' + data.collapseId).collapse('hide');
}
var selectors = $.isArray(data.refreshSelectors) && data.refreshSelectors.length
? data.refreshSelectors
: ['#animal-medical-panel', '#animal-progress-panel', '#animal-gallery-panel'];
refreshPanels(selectors);
}
});
$(document)
.off('shown.bs.collapse.animalInlineScroll', '.animal-inline-editor')
.on('shown.bs.collapse.animalInlineScroll', '.animal-inline-editor', function() {
scrollInlineEditorIntoView(this);
});
$(document).on('submit', '#animal-gallery-upload', function(event) {
event.preventDefault();
var form = this;
var filesInput = form.querySelector('#galleryImages');
var selectedCount = filesInput && filesInput.files ? filesInput.files.length : 0;
if (selectedCount > 10) {
window.alert('You can upload up to 10 images at a time.');
return;
}
var formData = new FormData(form);
$.ajax({
url: form.action,
type: 'POST',
data: formData,
processData: false,
contentType: false
}).done(function() {
var shouldReopenModal = selectedCount > 0;
if (form && typeof form.reset === 'function') {
form.reset();
}
$('#animal-gallery-manage-modal').modal('hide');
refreshPanels(['#animal-gallery-panel']).done(function() {
if (shouldReopenModal) {
$('#animal-gallery-manage-modal').modal('show');
}
});
});
});
$(document).on('click', '.js-ajax-gallery-remove', function(event) {
event.preventDefault();
var $link = $(this);
var confirmText = $link.data('confirmMessage') || $link.data('confirm');
if (confirmText && !window.confirm(confirmText)) {
return;
}
$.post($link.attr('href'), getCsrfPayload()).done(function() {
refreshPanels(['#animal-gallery-panel']);
});
});
})();
JS
, \yii\web\View::POS_END);
?>
<div class="panel panel-default" id="animal-gallery-panel">
<div class="panel-heading" style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
<span><?= Yii::t('AnimalManagementModule.base', '<strong>Gallery</strong>') ?></span>
<?php if ($canManage): ?>
<?= Html::a('<i class="fa fa-plus"></i> ' . Yii::t('AnimalManagementModule.base', 'Add'), '#animal-gallery-manage-modal', [
'class' => 'btn btn-xs btn-success',
'title' => Yii::t('AnimalManagementModule.base', 'Upload Gallery Images'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Upload Gallery Images'),
'data-toggle' => 'modal',
]) ?>
<?php endif; ?>
</div>
<div class="panel-body">
<?php if (empty($galleryItems)): ?>
<div class="text-muted" style="margin-bottom:10px;">
<?= Yii::t('AnimalManagementModule.base', 'No gallery images yet.') ?>
</div>
<?php else: ?>
<div class="row" style="margin-bottom:8px;">
<?php foreach ($galleryItems as $galleryItem): ?>
<?php $galleryUrl = trim((string)$galleryItem->getImageUrl()); ?>
<?php if ($galleryUrl === '') { continue; } ?>
<div class="col-sm-4 col-md-3" style="margin-bottom:10px;">
<div style="position:relative;border-radius:8px;overflow:hidden;background:#f3f5f7;">
<a href="<?= Html::encode($galleryUrl) ?>#.jpeg"
data-type="image"
data-toggle="lightbox"
data-parent="#animal-gallery-panel"
data-ui-gallery="<?= Html::encode($uiGalleryId) ?>"
data-pjax="0"
data-pjax-prevent
aria-label="<?= Yii::t('AnimalManagementModule.base', 'Open image') ?>">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Gallery image') ?>" style="width:100%;height:170px;object-fit:cover;display:block;">
</a>
<?php if ($canManage): ?>
<div style="position:absolute;top:8px;right:8px;">
<?= Html::a('<i class="fa fa-trash"></i>', $space->createUrl('/animal_management/animals/remove-gallery-image', ['id' => $animal->id, 'galleryId' => $galleryItem->id]), [
'class' => 'btn btn-xs btn-default js-ajax-gallery-remove',
'style' => 'background:rgba(255,255,255,0.92);border:0;',
'title' => Yii::t('AnimalManagementModule.base', 'Remove Image'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Remove Image'),
'data-confirm-message' => Yii::t('AnimalManagementModule.base', 'Remove this image from the gallery?'),
]) ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($canManage): ?>
<div class="modal fade" id="animal-gallery-manage-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<?= Yii::t('AnimalManagementModule.base', 'Close') ?>"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?= Yii::t('AnimalManagementModule.base', 'Add Images to Gallery') ?></h4>
</div>
<div class="modal-body">
<?php if (!empty($galleryItems)): ?>
<div class="row" style="margin-bottom:10px;max-height:260px;overflow:auto;">
<?php foreach ($galleryItems as $galleryItem): ?>
<?php $galleryUrl = trim((string)$galleryItem->getImageUrl()); ?>
<?php if ($galleryUrl === '') { continue; } ?>
<div class="col-xs-6 col-sm-3" style="margin-bottom:8px;">
<img src="<?= Html::encode($galleryUrl) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Gallery image') ?>" style="width:100%;height:110px;object-fit:cover;border-radius:6px;">
</div>
<?php endforeach; ?>
</div>
<hr style="margin-top:0;">
<?php endif; ?>
<form id="animal-gallery-upload" method="post" action="<?= Html::encode($space->createUrl('/animal_management/animals/add-gallery-images', ['id' => $animal->id])) ?>" enctype="multipart/form-data">
<?= Html::hiddenInput(Yii::$app->request->csrfParam, Yii::$app->request->getCsrfToken()) ?>
<div class="form-group" style="margin-bottom:8px;">
<label class="control-label" for="galleryImages"><?= Yii::t('AnimalManagementModule.base', 'Upload Images') ?></label>
<input type="file" class="form-control" id="galleryImages" name="galleryImages[]" accept="image/*" multiple>
<div class="help-block" style="margin-bottom:0;"><?= Yii::t('AnimalManagementModule.base', 'You can upload up to 10 images at a time.') ?></div>
</div>
<button type="submit" class="btn btn-primary"><?= Yii::t('AnimalManagementModule.base', 'Upload to Gallery') ?></button>
</form>
</div>
</div>
</div>
</div>
<?php endif; ?>
<style>
.animal-feed-card {
position: relative;
overflow: hidden;
border: 1px solid #d5dfe8;
border-radius: 12px;
background: #223446;
min-height: 240px;
box-shadow: 0 8px 22px rgba(12, 24, 36, 0.16);
}
.animal-feed-cover {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.animal-feed-overlay {
position: absolute;
inset: 0;
background: linear-gradient(110deg, rgba(10, 18, 28, 0.28) 10%, rgba(10, 18, 28, 0.55) 52%, rgba(10, 18, 28, 0.75) 100%);
}
.animal-feed-content {
position: relative;
z-index: 1;
min-height: 240px;
padding: 14px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
.animal-feed-top-row {
width: 100%;
max-width: none;
margin-left: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.animal-feed-date {
font-size: 15px;
font-weight: 700;
color: #ffffff;
margin-right: auto;
}
.animal-feed-details {
width: 55%;
max-width: 55%;
min-width: 0;
margin-left: auto;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.22);
background: rgba(10, 18, 28, 0.5);
backdrop-filter: blur(2px);
padding: 12px;
color: #ecf2f8;
}
.animal-feed-head {
display: flex;
justify-content: flex-end;
align-items: flex-start;
gap: 8px;
margin-bottom: 8px;
}
.animal-feed-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.animal-feed-chips--top {
margin-bottom: 0;
justify-content: flex-end;
}
.animal-feed-chip {
display: inline-block;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.16);
color: #ffffff;
font-size: 12px;
padding: 4px 10px;
}
.animal-feed-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: .04em;
color: rgba(231, 241, 249, 0.78);
margin-bottom: 4px;
}
.animal-feed-copy {
color: #eff5fb;
margin-bottom: 10px;
}
.animal-inline-editor {
position: relative;
z-index: 2;
width: auto;
max-width: none;
min-width: 0;
margin: 0 0 14px 0;
padding-top: 0;
align-self: stretch;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 12px;
background: rgba(10, 18, 28, 0.2);
padding: 8px;
}
.animal-inline-editor iframe {
display: block;
width: 100%;
border: 0;
border-radius: 10px;
background: transparent;
}
@media (max-width: 1200px) {
.animal-feed-details {
width: 55%;
max-width: 55%;
min-width: 0;
}
}
@media (max-width: 991px) {
.animal-feed-details {
width: 55%;
max-width: 55%;
min-width: 0;
}
.animal-feed-chips--top {
justify-content: flex-start;
}
}
</style>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default" id="animal-medical-panel">
<div class="panel-heading" style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
<a href="<?= Html::encode($space->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id])) ?>" style="font-weight:bold;">
<?= Yii::t('AnimalManagementModule.base', 'Medical Visits') ?>
</a>
<?php if ($canManage): ?>
<?= Html::a('<i class="fa fa-plus"></i> ' . Yii::t('AnimalManagementModule.base', 'Add'), $space->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id, 'inlineMedicalAdd' => 1]) . '#medical-add-inline', [
'class' => 'btn btn-xs btn-success',
'title' => Yii::t('AnimalManagementModule.base', 'Add Medical Visit'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Add Medical Visit'),
]) ?>
<?php endif; ?>
</div>
<div class="panel-body">
<?php if (empty($medicalVisits)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No medical visits recorded.') ?></div>
<?php else: ?>
<?php
$hiddenMedicalKeys = [
'second_physician_name',
'second_physician_business_name',
'second_physician_street_address',
'second_physician_city',
'second_physician_state',
'second_physician_zip',
'second_physician_cell_phone',
'second_physician_business_phone',
'second_physician_license_number',
'previous_physicians',
];
$knownMedicalKeys = [
'weight',
'pulse',
'blood_pressure',
'oxygen',
'chronic_conditions',
'acute_conditions',
'special_needs',
'date_of_most_recent_medical_visit',
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
'medical_media_reference',
'media_reference',
];
$medicalVitalLabelOverrides = [
'blood_pressure' => 'BP',
'oxygen' => 'O₂',
];
?>
<?php foreach ($medicalVisits as $visit): ?>
<?php
$visitCustomValues = $visit->getCustomFieldDisplayValues($canManage);
$visitFieldsByKey = [];
$additionalVisitFields = [];
$medicalMedia = '';
foreach ($visitCustomValues as $customField) {
$fieldKey = (string)($customField['field_key'] ?? '');
if (in_array($fieldKey, $hiddenMedicalKeys, true)) {
continue;
}
$fieldValue = trim((string)($customField['value'] ?? ''));
if ($fieldValue === '') {
continue;
}
if ($fieldKey === 'medical_media_reference' || $fieldKey === 'media_reference') {
$medicalMedia = $fieldValue;
continue;
}
if (in_array($fieldKey, $knownMedicalKeys, true)) {
$visitFieldsByKey[$fieldKey] = [
'label' => (string)($medicalVitalLabelOverrides[$fieldKey] ?? ($customField['label'] ?? $fieldKey)),
'value' => $fieldValue,
];
continue;
}
$additionalVisitFields[] = [
'label' => (string)($customField['label'] ?? $fieldKey),
'value' => $fieldValue,
];
}
$hasMedicalMedia = $medicalMedia !== '' && (preg_match('/^https?:\/\//i', $medicalMedia) || substr($medicalMedia, 0, 1) === '/');
$visitDateDisplay = DateDisplayHelper::format((string)$visit->visit_at);
?>
<div class="panel panel-default animal-feed-card" style="margin-bottom:12px;">
<?php if ($hasMedicalMedia): ?>
<img class="animal-feed-cover" src="<?= Html::encode($medicalMedia) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Medical media') ?>">
<?php endif; ?>
<div class="animal-feed-overlay"></div>
<div class="animal-feed-content">
<?php
$vitalKeys = ['weight', 'pulse', 'blood_pressure', 'oxygen'];
$hasVitals = false;
foreach ($vitalKeys as $vitalKey) {
if (!empty($visitFieldsByKey[$vitalKey]['value'])) {
$hasVitals = true;
break;
}
}
?>
<div class="animal-feed-top-row">
<span class="animal-feed-date"><?= Html::encode($visitDateDisplay !== '' ? $visitDateDisplay : (string)$visit->visit_at) ?></span>
<?php if ($hasVitals): ?>
<div class="animal-feed-chips animal-feed-chips--top">
<?php foreach ($vitalKeys as $vitalKey): ?>
<?php if (empty($visitFieldsByKey[$vitalKey]['value'])) { continue; } ?>
<span class="animal-feed-chip"><?= Html::encode($visitFieldsByKey[$vitalKey]['label']) ?>: <?= Html::encode($visitFieldsByKey[$vitalKey]['value']) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="animal-feed-details">
<div class="animal-feed-head">
<?php if ($canManage): ?>
<?= Html::a(
'<i class="fa fa-pencil"></i>',
'#medical-edit-inline-' . (int)$visit->id,
[
'class' => 'btn btn-xs btn-default',
'data-toggle' => 'collapse',
'title' => Yii::t('AnimalManagementModule.base', 'Edit'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Edit'),
]
) ?>
<?php endif; ?>
</div>
<?php if (!empty($visit->provider_name)): ?>
<div class="animal-feed-copy" style="margin-bottom:8px;"><?= Yii::t('AnimalManagementModule.base', 'Provider') ?>: <?= Html::encode((string)$visit->provider_name) ?></div>
<?php endif; ?>
<?php if (!empty($visit->notes)): ?>
<div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Clinical Notes') ?></div>
<div class="animal-feed-copy"><?= nl2br(Html::encode((string)$visit->notes)) ?></div>
<?php endif; ?>
<?php if (!empty($visit->recommendations)): ?>
<div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Recommendations') ?></div>
<div class="animal-feed-copy"><?= nl2br(Html::encode((string)$visit->recommendations)) ?></div>
<?php endif; ?>
<?php
$conditionKeys = ['chronic_conditions', 'acute_conditions', 'special_needs'];
foreach ($conditionKeys as $conditionKey):
if (empty($visitFieldsByKey[$conditionKey]['value'])) {
continue;
}
?>
<div class="animal-feed-label"><?= Html::encode($visitFieldsByKey[$conditionKey]['label']) ?></div>
<div class="animal-feed-copy"><?= nl2br(Html::encode($visitFieldsByKey[$conditionKey]['value'])) ?></div>
<?php endforeach; ?>
<?php if (!empty($visitFieldsByKey['date_of_most_recent_medical_visit']['value'])): ?>
<div class="animal-feed-label"><?= Html::encode($visitFieldsByKey['date_of_most_recent_medical_visit']['label']) ?></div>
<div class="animal-feed-copy"><?= Html::encode(DateDisplayHelper::format((string)$visitFieldsByKey['date_of_most_recent_medical_visit']['value'])) ?></div>
<?php endif; ?>
<?php
$contactKeys = [
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
];
$hasContact = false;
foreach ($contactKeys as $contactKey) {
if (!empty($visitFieldsByKey[$contactKey]['value'])) {
$hasContact = true;
break;
}
}
?>
<?php if ($hasContact): ?>
<?php
$contactLines = [];
if (!empty($visitFieldsByKey['physician_name']['value'])) {
$contactLines[] = (string)$visitFieldsByKey['physician_name']['value'];
}
if (!empty($visitFieldsByKey['physician_business_name']['value'])) {
$contactLines[] = (string)$visitFieldsByKey['physician_business_name']['value'];
}
if (!empty($visitFieldsByKey['physician_street_address']['value'])) {
$contactLines[] = (string)$visitFieldsByKey['physician_street_address']['value'];
}
$cityStateZip = trim(implode(', ', array_filter([
trim((string)($visitFieldsByKey['physician_city']['value'] ?? '')),
trim((string)($visitFieldsByKey['physician_state']['value'] ?? '')),
])));
$zipValue = trim((string)($visitFieldsByKey['physician_zip']['value'] ?? ''));
if ($zipValue !== '') {
$cityStateZip = trim($cityStateZip . ($cityStateZip !== '' ? ' ' : '') . $zipValue);
}
if ($cityStateZip !== '') {
$contactLines[] = $cityStateZip;
}
$phones = array_filter([
trim((string)($visitFieldsByKey['physician_cell_phone']['value'] ?? '')),
trim((string)($visitFieldsByKey['physician_business_phone']['value'] ?? '')),
]);
if (!empty($phones)) {
$contactLines[] = implode(' · ', $phones);
}
if (!empty($visitFieldsByKey['physician_license_number']['value'])) {
$contactLines[] = (string)$visitFieldsByKey['physician_license_number']['value'];
}
?>
<div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Care Contact') ?></div>
<div class="animal-feed-copy">
<?php foreach ($contactLines as $contactLine): ?>
<div><?= Html::encode($contactLine) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($additionalVisitFields)): ?>
<div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Additional Fields') ?></div>
<div class="animal-feed-copy">
<?php foreach ($additionalVisitFields as $additionalField): ?>
<div><strong><?= Html::encode((string)$additionalField['label']) ?>:</strong> <?= nl2br(Html::encode((string)$additionalField['value'])) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php if ($canManage): ?>
<div id="medical-edit-inline-<?= (int)$visit->id ?>" class="collapse animal-inline-editor<?= $openMedicalEditId === (int)$visit->id ? ' in' : '' ?>">
<iframe
src="<?= Html::encode($space->createUrl('/animal_management/animals/edit-medical-visit', ['id' => $animal->id, 'visitId' => $visit->id, 'inline' => 1, '_v' => time()])) ?>"
style="width:100%;min-height:640px;"
loading="lazy"
></iframe>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default" id="animal-progress-panel">
<div class="panel-heading" style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
<a href="<?= Html::encode($space->createUrl('/animal_management/animals/progress-updates', ['id' => $animal->id])) ?>" style="font-weight:bold;">
<?= Yii::t('AnimalManagementModule.base', 'Progress Feed') ?>
</a>
<?php if ($canManage): ?>
<?= Html::a('<i class="fa fa-plus"></i> ' . Yii::t('AnimalManagementModule.base', 'Add'), $space->createUrl('/animal_management/animals/progress-updates', ['id' => $animal->id, 'inlineProgressAdd' => 1]) . '#progress-add-inline', [
'class' => 'btn btn-xs btn-success',
'title' => Yii::t('AnimalManagementModule.base', 'Add Progress Update'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Add Progress Update'),
]) ?>
<?php endif; ?>
</div>
<div class="panel-body">
<?php if (empty($progressUpdates)): ?>
<div class="text-muted" style="margin-bottom:10px;"><?= Yii::t('AnimalManagementModule.base', 'No progress updates recorded.') ?></div>
<?php else: ?>
<?php foreach ($progressUpdates as $update): ?>
<?php
$progressCustomValues = $update->getCustomFieldDisplayValues($canManage);
$mediaReference = '';
$progressCustomDisplayValues = [];
foreach ($progressCustomValues as $customField) {
if ((string)($customField['field_key'] ?? '') === 'media_reference') {
$mediaReference = trim((string)$customField['value']);
continue;
}
$progressCustomDisplayValues[] = $customField;
}
$hasMediaImage = $mediaReference !== '' && (preg_match('/^https?:\/\//i', $mediaReference) || substr($mediaReference, 0, 1) === '/');
?>
<div class="panel panel-default animal-feed-card" style="margin-bottom:12px;">
<?php if ($hasMediaImage): ?>
<img class="animal-feed-cover" src="<?= Html::encode($mediaReference) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Progress media') ?>">
<?php endif; ?>
<div class="animal-feed-overlay"></div>
<div class="animal-feed-content">
<div class="animal-feed-top-row">
<span class="animal-feed-date"><?= Html::encode(DateDisplayHelper::format((string)$update->update_at)) ?></span>
<?php if (!empty($update->weight) || !empty($update->vitals)): ?>
<div class="animal-feed-chips animal-feed-chips--top">
<?php if (!empty($update->weight)): ?><span class="animal-feed-chip"><?= Yii::t('AnimalManagementModule.base', 'Weight') ?>: <?= Html::encode((string)$update->weight) ?></span><?php endif; ?>
<?php if (!empty($update->vitals)): ?><span class="animal-feed-chip"><?= Yii::t('AnimalManagementModule.base', 'Vitals') ?></span><?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="animal-feed-details">
<div class="animal-feed-head">
<?php if ($canManage): ?>
<?= Html::a(
'<i class="fa fa-pencil"></i>',
'#progress-edit-inline-' . (int)$update->id,
[
'class' => 'btn btn-xs btn-default',
'data-toggle' => 'collapse',
'title' => Yii::t('AnimalManagementModule.base', 'Edit'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Edit'),
]
) ?>
<?php endif; ?>
</div>
<?php if (!empty($update->vitals)): ?><div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Vitals') ?></div><div class="animal-feed-copy"><?= nl2br(Html::encode((string)$update->vitals)) ?></div><?php endif; ?>
<?php if (!empty($update->behavior_notes)): ?><div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Behavior') ?></div><div class="animal-feed-copy"><?= nl2br(Html::encode((string)$update->behavior_notes)) ?></div><?php endif; ?>
<?php if (!empty($update->meal_plan_changes)): ?><div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Meal Plan') ?></div><div class="animal-feed-copy"><?= nl2br(Html::encode((string)$update->meal_plan_changes)) ?></div><?php endif; ?>
<?php if (!empty($update->housing_changes)): ?><div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Housing') ?></div><div class="animal-feed-copy"><?= nl2br(Html::encode((string)$update->housing_changes)) ?></div><?php endif; ?>
<?php if (!empty($update->medical_concerns)): ?><div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Medical Concerns') ?></div><div class="animal-feed-copy"><?= nl2br(Html::encode((string)$update->medical_concerns)) ?></div><?php endif; ?>
<?php if (!empty($progressCustomDisplayValues)): ?>
<div class="animal-feed-label"><?= Yii::t('AnimalManagementModule.base', 'Additional Fields') ?></div>
<div class="animal-feed-copy">
<?php foreach ($progressCustomDisplayValues as $customField): ?>
<div><strong><?= Html::encode((string)$customField['label']) ?>:</strong> <?= nl2br(Html::encode((string)$customField['value'])) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php if ($canManage): ?>
<div id="progress-edit-inline-<?= (int)$update->id ?>" class="collapse animal-inline-editor<?= $openProgressEditId === (int)$update->id ? ' in' : '' ?>">
<iframe
src="<?= Html::encode($space->createUrl('/animal_management/animals/edit-progress-update', ['id' => $animal->id, 'updateId' => $update->id, 'inline' => 1, '_v' => time()])) ?>"
style="width:100%;min-height:760px;"
loading="lazy"
></iframe>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
<span><?= Yii::t('AnimalManagementModule.base', '<strong>Transfer Timeline</strong>') ?></span>
<?php if ($canManage): ?>
<?= Html::a('<i class="fa fa-plus"></i> ' . Yii::t('AnimalManagementModule.base', 'Add'), $space->createUrl('/animal_management/animals/transfer', ['id' => $animal->id]), [
'class' => 'btn btn-xs btn-success',
'title' => Yii::t('AnimalManagementModule.base', 'Request Transfer'),
'aria-label' => Yii::t('AnimalManagementModule.base', 'Request Transfer'),
]) ?>
<?php endif; ?>
</div>
<div class="panel-body">
<?php if (empty($transfers)): ?>
<div class="text-muted"><?= Yii::t('AnimalManagementModule.base', 'No transfers yet.') ?></div>
<?php else: ?>
<table class="table table-condensed table-hover" style="margin-bottom:0;">
<thead>
<tr>
<th><?= Yii::t('AnimalManagementModule.base', 'From') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'To') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Status') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Updated') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($transfers as $transfer): ?>
<?php $fromSpace = $transfer->getFromSpace(); $toSpace = $transfer->getToSpace(); ?>
<tr>
<td><?= Html::encode($fromSpace ? $fromSpace->name : Yii::t('AnimalManagementModule.base', 'Unknown')) ?></td>
<td><?= Html::encode($toSpace ? $toSpace->name : Yii::t('AnimalManagementModule.base', 'Unknown')) ?></td>
<td><?= Html::encode(AnimalTransfer::statusOptions()[$transfer->status] ?? $transfer->status) ?></td>
<td><?= Html::encode(DateDisplayHelper::format((string)$transfer->updated_at)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><?= Yii::t('AnimalManagementModule.base', '<strong>Transfer Audit Log</strong>') ?></div>
<div class="panel-body">
<?php if (empty($transferEvents)): ?>
<div class="text-muted"><?= Yii::t('AnimalManagementModule.base', 'No transfer events recorded yet.') ?></div>
<?php else: ?>
<table class="table table-condensed table-hover" style="margin-bottom:0;">
<thead>
<tr>
<th><?= Yii::t('AnimalManagementModule.base', 'When') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'By') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Event') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Status Change') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Message') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($transferEvents as $event): ?>
<?php $actor = $event->createdByUser; ?>
<tr>
<td><?= Html::encode(DateDisplayHelper::format((string)$event->created_at)) ?></td>
<td><?= Html::encode($actor ? $actor->displayName : Yii::t('AnimalManagementModule.base', 'System')) ?></td>
<td><?= Html::encode((string)$event->event_type) ?></td>
<td><?= Html::encode((string)($event->from_status ?: '-') . ' -> ' . (string)($event->to_status ?: '-')) ?></td>
<td><?= Html::encode((string)$event->message) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>

View File

@@ -0,0 +1,104 @@
<?php
use humhub\libs\Html;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\gallery\assets\Assets as GalleryAssets;
use humhub\modules\gallery\helpers\Url;
use humhub\modules\gallery\models\CustomGallery;
use humhub\modules\space\models\Space;
use humhub\widgets\Button;
/* @var CustomGallery $gallery */
/* @var Animal|null $animal */
/* @var AnimalGalleryItem[] $items */
/* @var Space $container */
/* @var bool|string $showMore */
GalleryAssets::register($this);
$animalName = $animal instanceof Animal ? $animal->getDisplayName() : Yii::t('AnimalManagementModule.base', 'Animal');
$backUrl = Url::toGalleryOverview($container);
$animalUrl = $animal instanceof Animal
? $container->createUrl('/animal_management/animals/view', ['id' => (int)$animal->id])
: null;
$uiGalleryId = 'animal-gallery-native-' . (int)$gallery->id;
$descriptionText = preg_replace('/\s*\[animal-gallery:\d+\]\s*/', ' ', (string)$gallery->description);
$descriptionText = trim((string)$descriptionText);
$headerTitle = trim((string)$gallery->title);
$headerTitle = preg_replace('/\s+Gallery\s*$/i', '', $headerTitle);
if ($headerTitle === '') {
$headerTitle = $animalName;
}
?>
<div id="gallery-container" class="panel panel-default">
<div class="panel-heading clearfix" style="background-color: <?= $this->theme->variable('background-color-secondary') ?>;">
<div style="margin-right:40px;" class="pull-left">
<?= Yii::t('GalleryModule.base', '<strong>Gallery</strong> ') . Html::encode($headerTitle) ?>
</div>
<?= Button::back($backUrl, Yii::t('GalleryModule.base', 'Back to overview'))->right()->sm() ?>
</div>
<div class="panel-body">
<?php if ($animalUrl !== null): ?>
<div style="margin-bottom:10px;">
<?= Html::a(Html::encode($animalName), $animalUrl, ['style' => 'font-size:20px;font-weight:700;line-height:1.2;']) ?>
</div>
<?php endif; ?>
<?php if ($descriptionText !== ''): ?>
<div class="row clearfix" style="padding-bottom:8px;">
<div class="col-sm-12 gallery-description">
<i class="fa fa-arrow-circle-right"></i>
<?= Html::encode($descriptionText) ?>
</div>
</div>
<?php endif; ?>
<div id="gallery-list" class="col">
<div id="gallery-media-container" class="row">
<?php if (empty($items)): ?>
<div class="col-sm-12 text-muted" style="margin-bottom:10px;">
<?= Yii::t('AnimalManagementModule.base', 'No gallery images yet.') ?>
</div>
<?php else: ?>
<?php foreach ($items as $item): ?>
<?php
if (!$item instanceof AnimalGalleryItem) {
continue;
}
$imageUrl = trim((string)$item->getImageUrl());
if ($imageUrl === '') {
continue;
}
$title = trim((string)$item->caption);
$altText = $title !== '' ? $title : $animalName;
?>
<div class="col-sm-6 col-md-4 gallery-list-entry" style="margin-bottom:12px;">
<a href="<?= Html::encode($imageUrl) ?>#.jpeg"
data-type="image"
data-toggle="lightbox"
data-parent="#gallery-content"
data-ui-gallery="<?= Html::encode($uiGalleryId) ?>"
data-description="<?= Html::encode($title) ?>"
title="<?= Html::encode($title) ?>"
style="display:block;position:relative;aspect-ratio:4 / 3;background:#d8dee8;border-radius:12px;overflow:hidden;box-shadow:0 8px 24px rgba(15,23,42,0.14);">
<img class="gallery-img" src="<?= Html::encode($imageUrl) ?>" alt="<?= Html::encode($altText) ?>" style="display:none;position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"/>
</a>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php if ($showMore): ?>
<div style="text-align:center;">
<?= Button::primary(Yii::t('GalleryModule.base', 'Show more'))->action('gallery.showMore', (string)$showMore) ?>
</div>
<?php endif; ?>
</div>
</div>

168
views/settings/index.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
use humhub\modules\animal_management\models\forms\DisplaySettingsForm;
use humhub\modules\animal_management\models\forms\FieldDefinitionSettingsForm;
use humhub\widgets\Button;
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
/* @var string|null $subNav */
/* @var int $animalCount */
/* @var FieldDefinitionSettingsForm $fieldSettingsForm */
/* @var DisplaySettingsForm $displaySettingsForm */
?>
<div class="panel panel-default">
<div class="panel-heading"><?= Yii::t('AnimalManagementModule.base', '<strong>Animal Management</strong> Settings') ?></div>
<?php if (!empty($subNav)): ?>
<?= $subNav ?>
<?php endif; ?>
<div class="panel-body">
<p><?= Yii::t('AnimalManagementModule.base', 'Configure intake/profile field definitions used by Animal Management.') ?></p>
<div class="well well-sm" style="margin-bottom:12px;">
<strong><?= Yii::t('AnimalManagementModule.base', 'Current animals in this space') ?>:</strong>
<?= (int)$animalCount ?>
</div>
<h4 style="margin-top:0;"><?= Yii::t('AnimalManagementModule.base', 'Display Settings') ?></h4>
<?php $displayForm = ActiveForm::begin(); ?>
<?= $displayForm->errorSummary($displaySettingsForm, ['showAllErrors' => true]) ?>
<div class="row">
<div class="col-md-8">
<?= $displayForm->field($displaySettingsForm, 'search_block_heading')->textInput(['maxlength' => 190]) ?>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="control-label" style="display:block;"><?= Yii::t('AnimalManagementModule.base', 'Animal Tile Fields') ?></label>
<?= Html::checkboxList(
'DisplaySettingsForm[tile_fields]',
$displaySettingsForm->tile_fields,
DisplaySettingsForm::fieldOptions(),
['separator' => '<br>']
) ?>
<p class="help-block" style="margin-top:6px;"><?= Yii::t('AnimalManagementModule.base', 'ID is hidden by default unless selected here.') ?></p>
</div>
<div class="col-md-6">
<label class="control-label" style="display:block;"><?= Yii::t('AnimalManagementModule.base', 'Animal Detail Hero Fields') ?></label>
<?= Html::checkboxList(
'DisplaySettingsForm[detail_fields]',
$displaySettingsForm->detail_fields,
DisplaySettingsForm::fieldOptions(),
['separator' => '<br>']
) ?>
</div>
</div>
<div style="margin-bottom:16px;">
<?= Button::save(Yii::t('AnimalManagementModule.base', 'Save Display Settings'))->submit() ?>
</div>
<?php ActiveForm::end(); ?>
<?php if (!$fieldSettingsForm->canUseFieldDefinition()): ?>
<div class="alert alert-warning" style="margin-bottom:0;">
<?= Yii::t('AnimalManagementModule.base', 'Field definition storage is unavailable. Enable rescue foundation migrations first.') ?>
</div>
<?php else: ?>
<?php $form = ActiveForm::begin(); ?>
<?php if ($fieldSettingsForm->hasErrors('rows')): ?>
<div class="alert alert-danger">
<?= Html::errorSummary($fieldSettingsForm, ['header' => '', 'footer' => '', 'showAllErrors' => true]) ?>
</div>
<?php endif; ?>
<h4 style="margin-top:0;"><?= Yii::t('AnimalManagementModule.base', 'Configured Fields') ?></h4>
<?php if (empty($fieldSettingsForm->rows)): ?>
<div class="alert alert-info"><?= Yii::t('AnimalManagementModule.base', 'No field definitions found for this module.') ?></div>
<?php else: ?>
<div class="table-responsive" style="margin-bottom:12px;">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th><?= Yii::t('AnimalManagementModule.base', 'Field Key') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Label') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Group') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Input Type') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Required') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Active') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Visibility') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Sort') ?></th>
<th><?= Yii::t('AnimalManagementModule.base', 'Remove') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($fieldSettingsForm->rows as $index => $row): ?>
<?php $isCore = ((int)($row['is_core'] ?? 0)) === 1; ?>
<tr>
<td>
<?= Html::hiddenInput("FieldDefinitionSettingsForm[rows][$index][id]", (int)$row['id']) ?>
<?= Html::hiddenInput("FieldDefinitionSettingsForm[rows][$index][field_key]", (string)$row['field_key']) ?>
<?= Html::encode((string)$row['field_key']) ?>
<?php if ($isCore): ?>
<span class="label label-default" style="margin-left:4px;"><?= Yii::t('AnimalManagementModule.base', 'Core') ?></span>
<?php endif; ?>
</td>
<td><?= Html::textInput("FieldDefinitionSettingsForm[rows][$index][label]", (string)$row['label'], ['class' => 'form-control input-sm']) ?></td>
<td><?= Html::textInput("FieldDefinitionSettingsForm[rows][$index][group_key]", (string)$row['group_key'], ['class' => 'form-control input-sm']) ?></td>
<td>
<?= Html::encode((string)$row['input_type']) ?>
<?= Html::hiddenInput("FieldDefinitionSettingsForm[rows][$index][input_type]", (string)$row['input_type']) ?>
</td>
<td class="text-center" style="vertical-align:middle;">
<?= Html::checkbox("FieldDefinitionSettingsForm[rows][$index][required]", !empty($row['required']), ['disabled' => $isCore]) ?>
<?php if ($isCore): ?>
<?= Html::hiddenInput("FieldDefinitionSettingsForm[rows][$index][required]", !empty($row['required']) ? 1 : 0) ?>
<?php endif; ?>
</td>
<td class="text-center" style="vertical-align:middle;">
<?= Html::checkbox("FieldDefinitionSettingsForm[rows][$index][is_active]", !empty($row['is_active']), ['disabled' => $isCore]) ?>
<?php if ($isCore): ?>
<?= Html::hiddenInput("FieldDefinitionSettingsForm[rows][$index][is_active]", !empty($row['is_active']) ? 1 : 0) ?>
<?php endif; ?>
</td>
<td>
<?= Html::dropDownList(
"FieldDefinitionSettingsForm[rows][$index][visibility]",
(string)$row['visibility'],
FieldDefinitionSettingsForm::visibilityOptions(),
['class' => 'form-control input-sm']
) ?>
</td>
<td><?= Html::input('number', "FieldDefinitionSettingsForm[rows][$index][sort_order]", (int)$row['sort_order'], ['class' => 'form-control input-sm', 'style' => 'max-width:90px;']) ?></td>
<td class="text-center" style="vertical-align:middle;">
<?php if (!$isCore): ?>
<?= Html::checkbox("FieldDefinitionSettingsForm[rows][$index][remove]", false) ?>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<h4><?= Yii::t('AnimalManagementModule.base', 'Add Custom Field') ?></h4>
<div class="row">
<div class="col-md-3"><?= $form->field($fieldSettingsForm, 'new_field_key')->textInput(['placeholder' => 'e.g. coat_color']) ?></div>
<div class="col-md-3"><?= $form->field($fieldSettingsForm, 'new_label') ?></div>
<div class="col-md-2"><?= $form->field($fieldSettingsForm, 'new_input_type')->dropDownList(FieldDefinitionSettingsForm::inputTypeOptions()) ?></div>
<div class="col-md-2"><?= $form->field($fieldSettingsForm, 'new_group_key') ?></div>
<div class="col-md-2"><?= $form->field($fieldSettingsForm, 'new_sort_order')->input('number') ?></div>
</div>
<div class="row">
<div class="col-md-3"><?= $form->field($fieldSettingsForm, 'new_visibility')->dropDownList(FieldDefinitionSettingsForm::visibilityOptions()) ?></div>
<div class="col-md-3"><?= $form->field($fieldSettingsForm, 'new_required')->checkbox() ?></div>
<div class="col-md-6"><?= $form->field($fieldSettingsForm, 'new_options')->textInput(['placeholder' => 'Optional JSON options']) ?></div>
</div>
<?= Button::save(Yii::t('AnimalManagementModule.base', 'Save Field Settings'))->submit() ?>
<?php ActiveForm::end(); ?>
<?php endif; ?>
</div>
</div>

View File

@@ -0,0 +1,42 @@
<?php
use humhub\modules\animal_management\models\Animal;
use yii\helpers\Html;
/* @var Animal[] $animals */
/* @var \humhub\modules\content\components\ContentContainerActiveRecord $contentContainer */
/* @var string $queryValue */
?>
<form method="get" action="<?= Html::encode($contentContainer->createUrl('/animal_management/animals/index')) ?>" style="margin-bottom:10px;">
<div class="input-group">
<input type="text" class="form-control" name="q" value="<?= Html::encode($queryValue) ?>" placeholder="<?= Yii::t('AnimalManagementModule.base', 'Search by name, species, or ID') ?>">
<span class="input-group-btn">
<button class="btn btn-default" type="submit"><?= Yii::t('AnimalManagementModule.base', 'Search') ?></button>
</span>
</div>
</form>
<?php if (empty($animals)): ?>
<div class="text-muted" style="margin-bottom:8px;">
<?= Yii::t('AnimalManagementModule.base', 'No animals found yet.') ?>
</div>
<?php else: ?>
<ul class="list-unstyled" style="margin-bottom:10px;">
<?php foreach ($animals as $animal): ?>
<li style="padding:6px 0;border-bottom:1px solid #f0f0f0;">
<strong><?= Html::encode($animal->getDisplayName()) ?></strong>
<div class="text-muted" style="font-size:12px;">
<?= Html::encode($animal->animal_uid) ?>
<?php if (!empty($animal->species)): ?>
· <?= Html::encode($animal->species) ?>
<?php endif; ?>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<a class="btn btn-primary btn-sm" href="<?= Html::encode($contentContainer->createUrl('/animal_management/animals/index')) ?>">
<?= Yii::t('AnimalManagementModule.base', 'Open Animals') ?>
</a>

View File

@@ -0,0 +1,306 @@
<?php
namespace humhub\modules\animal_management\widgets;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalFieldValue;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\animal_management\models\forms\DisplaySettingsForm;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\rescue_foundation\models\RescueFieldDefinition;
use Yii;
use yii\base\Widget;
class SearchAnimalProfilesBlock extends Widget
{
public ContentContainerActiveRecord $contentContainer;
public int $limit = 10;
public function run()
{
if (!$this->contentContainer->moduleManager->isEnabled('animal_management')) {
return '<div class="well well-sm" style="margin-bottom:0;">'
. Yii::t('AnimalManagementModule.base', 'Animal profiles are not enabled for this rescue.')
. '</div>';
}
$queryValue = trim((string)Yii::$app->request->get('q', ''));
$showAll = ((int)Yii::$app->request->get('animalFeedAll', 0)) === 1;
$countParam = (int)Yii::$app->request->get('animalFeedCount', $this->limit);
if ($countParam < $this->limit) {
$countParam = $this->limit;
}
if ($countParam > 200) {
$countParam = 200;
}
$query = Animal::find()->where(['contentcontainer_id' => $this->contentContainer->contentcontainer_id]);
if ($queryValue !== '') {
$query->andWhere([
'or',
['like', 'animal_uid', $queryValue],
['like', 'name', $queryValue],
['like', 'species', $queryValue],
]);
}
$totalCount = (int)$query->count();
$displayCount = $showAll ? $totalCount : min($countParam, $totalCount);
$settings = Yii::$app->getModule('animal_management')->settings->contentContainer($this->contentContainer);
$heading = trim((string)$settings->get('searchBlockHeading', DisplaySettingsForm::DEFAULT_SEARCH_BLOCK_HEADING));
if ($heading === '') {
$heading = DisplaySettingsForm::DEFAULT_SEARCH_BLOCK_HEADING;
}
$tileFieldsRaw = $settings->get('tileFields', json_encode(DisplaySettingsForm::DEFAULT_TILE_FIELDS));
$tileFields = $this->normalizeDisplayFields($tileFieldsRaw, DisplaySettingsForm::DEFAULT_TILE_FIELDS);
$animals = $query
->orderBy(['updated_at' => SORT_DESC, 'id' => SORT_DESC])
->limit($displayCount)
->all();
$animalIds = array_map(static function (Animal $animal): int {
return (int)$animal->id;
}, $animals);
$latestMedicalVisitByAnimal = $this->resolveLatestMedicalVisits($animalIds);
$animalImageUrls = $this->resolveAnimalImageUrls($animalIds, ['profile_image_url', 'profile_image', 'photo_url', 'image_url', 'photo'], false);
$tileFieldOverrides = $this->resolveDisplayFieldOverrides($animalIds, 'tile_display_fields');
$hasMore = !$showAll && $displayCount < $totalCount;
$nextCount = min($displayCount + $this->limit, $totalCount);
$viewFile = $this->getViewPath() . '/searchAnimalProfilesBlock.php';
if (!is_file($viewFile)) {
return '<div class="well well-sm" style="margin-bottom:0;">'
. Yii::t('AnimalManagementModule.base', 'Animal search is temporarily unavailable.')
. '</div>';
}
return $this->render('searchAnimalProfilesBlock', [
'animals' => $animals,
'contentContainer' => $this->contentContainer,
'queryValue' => $queryValue,
'heading' => $heading,
'tileFields' => $tileFields,
'tileFieldOverrides' => $tileFieldOverrides,
'latestMedicalVisitByAnimal' => $latestMedicalVisitByAnimal,
'animalImageUrls' => $animalImageUrls,
'totalCount' => $totalCount,
'displayCount' => $displayCount,
'hasMore' => $hasMore,
'nextCount' => $nextCount,
'showAll' => $showAll,
]);
}
private function normalizeDisplayFields($raw, array $default): array
{
if (is_string($raw)) {
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return $default;
}
$raw = $decoded;
}
if (!is_array($raw)) {
return $default;
}
$allowed = array_keys(DisplaySettingsForm::fieldOptions());
$normalized = [];
foreach ($raw as $field) {
$field = trim((string)$field);
if ($field === '' || !in_array($field, $allowed, true)) {
continue;
}
if (!in_array($field, $normalized, true)) {
$normalized[] = $field;
}
}
return !empty($normalized) ? $normalized : $default;
}
private function resolveLatestMedicalVisits(array $animalIds): array
{
$animalIds = array_values(array_unique(array_map('intval', $animalIds)));
if (empty($animalIds)) {
return [];
}
$visits = AnimalMedicalVisit::find()
->where(['animal_id' => $animalIds])
->orderBy(['animal_id' => SORT_ASC, 'visit_at' => SORT_DESC, 'id' => SORT_DESC])
->all();
$result = [];
foreach ($visits as $visit) {
$animalId = (int)$visit->animal_id;
if (!isset($result[$animalId])) {
$result[$animalId] = $visit;
}
}
return $result;
}
private function resolveAnimalImageUrls(array $animalIds, array $imageFieldOrder = [], bool $allowGalleryFallback = false): array
{
$animalIds = array_values(array_unique(array_map('intval', $animalIds)));
if (empty($animalIds)) {
return [];
}
if (!class_exists(RescueFieldDefinition::class)
|| Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null
|| Yii::$app->db->schema->getTableSchema('rescue_animal_field_value', true) === null
) {
return [];
}
if (empty($imageFieldOrder)) {
$imageFieldOrder = ['cover_image_url', 'profile_image_url', 'photo_url', 'image_url', 'profile_image', 'photo'];
}
$definitions = RescueFieldDefinition::find()
->select(['id', 'field_key'])
->where([
'module_id' => 'animal_management',
'group_key' => 'animal_profile',
'field_key' => $imageFieldOrder,
'is_active' => 1,
])
->all();
if (empty($definitions)) {
return [];
}
$definitionPriority = [];
foreach ($definitions as $definition) {
$priority = array_search((string)$definition->field_key, $imageFieldOrder, true);
$definitionPriority[(int)$definition->id] = $priority === false ? 999 : (int)$priority;
}
if (empty($definitionPriority)) {
return [];
}
$valueRows = AnimalFieldValue::find()
->where(['animal_id' => $animalIds, 'field_definition_id' => array_keys($definitionPriority)])
->all();
$imageUrls = [];
$chosenPriorityByAnimal = [];
foreach ($valueRows as $valueRow) {
$animalId = (int)$valueRow->animal_id;
$valueText = trim((string)$valueRow->value_text);
if ($valueText === '') {
continue;
}
$priority = $definitionPriority[(int)$valueRow->field_definition_id] ?? 999;
if (!isset($chosenPriorityByAnimal[$animalId]) || $priority < $chosenPriorityByAnimal[$animalId]) {
$chosenPriorityByAnimal[$animalId] = $priority;
$imageUrls[$animalId] = $valueText;
}
}
$missingAnimalIds = [];
foreach ($animalIds as $animalId) {
if (!isset($imageUrls[$animalId])) {
$missingAnimalIds[] = (int)$animalId;
}
}
if ($allowGalleryFallback && !empty($missingAnimalIds) && Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_item', true) !== null) {
$galleryItems = AnimalGalleryItem::find()
->where(['animal_id' => $missingAnimalIds])
->orderBy(['animal_id' => SORT_ASC, 'id' => SORT_DESC])
->all();
foreach ($galleryItems as $galleryItem) {
$animalId = (int)$galleryItem->animal_id;
if (isset($imageUrls[$animalId])) {
continue;
}
$url = trim((string)$galleryItem->getImageUrl());
if ($url === '') {
continue;
}
$imageUrls[$animalId] = $url;
}
}
return $imageUrls;
}
private function resolveDisplayFieldOverrides(array $animalIds, string $fieldKey): array
{
$animalIds = array_values(array_unique(array_map('intval', $animalIds)));
if (empty($animalIds)) {
return [];
}
if (!class_exists(RescueFieldDefinition::class)
|| Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null
|| Yii::$app->db->schema->getTableSchema('rescue_animal_field_value', true) === null
) {
return [];
}
$definition = RescueFieldDefinition::findOne([
'module_id' => 'animal_management',
'group_key' => 'animal_profile',
'field_key' => $fieldKey,
]);
if (!$definition instanceof RescueFieldDefinition) {
return [];
}
$rows = AnimalFieldValue::find()
->where(['animal_id' => $animalIds, 'field_definition_id' => (int)$definition->id])
->all();
$allowed = array_keys(DisplaySettingsForm::fieldOptions());
$result = [];
foreach ($rows as $row) {
$raw = trim((string)$row->value_text);
if ($raw === '') {
continue;
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
$decoded = array_map('trim', explode(',', $raw));
}
$normalized = [];
foreach ($decoded as $field) {
$field = trim((string)$field);
if ($field === '' || !in_array($field, $allowed, true)) {
continue;
}
if (!in_array($field, $normalized, true)) {
$normalized[] = $field;
}
}
if (!empty($normalized)) {
$result[(int)$row->animal_id] = $normalized;
}
}
return $result;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace humhub\modules\animal_management\widgets\stream;
use humhub\modules\animal_management\models\AnimalStreamEntry;
use humhub\modules\content\widgets\stream\WallStreamModuleEntryWidget;
class AnimalStreamEntryWallEntry extends WallStreamModuleEntryWidget
{
/**
* @var AnimalStreamEntry
*/
public $model;
public function renderContent()
{
return $this->render('wall-entry', [
'entry' => $this->model,
]);
}
protected function getTitle()
{
return $this->model->getContentDescription();
}
}

View File

@@ -0,0 +1,228 @@
<?php
use humhub\modules\animal_management\helpers\DateDisplayHelper;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\animal_management\models\AnimalProgressUpdate;
use humhub\modules\animal_management\models\AnimalStreamEntry;
use yii\helpers\Html;
/* @var AnimalStreamEntry $entry */
$animal = $entry->animal;
if (!$animal instanceof Animal) {
return;
}
$medicalVisit = $entry->medicalVisit;
$progressUpdate = $entry->progressUpdate;
$isMedical = (string)$entry->entry_type === AnimalStreamEntry::TYPE_MEDICAL && $medicalVisit instanceof AnimalMedicalVisit;
$isProgress = (string)$entry->entry_type === AnimalStreamEntry::TYPE_PROGRESS && $progressUpdate instanceof AnimalProgressUpdate;
if (!$isMedical && !$isProgress) {
return;
}
$animalName = $animal->getDisplayName();
$mediaReference = '';
$chipRows = [];
$dateText = '';
$detailRows = [];
if ($isProgress) {
$customValues = $progressUpdate->getCustomFieldDisplayValues(true);
$additionalFields = [];
foreach ($customValues as $customField) {
if ((string)($customField['field_key'] ?? '') === 'media_reference') {
$mediaReference = trim((string)$customField['value']);
continue;
}
$additionalFields[] = [
'label' => (string)$customField['label'],
'value' => (string)$customField['value'],
];
}
$dateText = DateDisplayHelper::format((string)$progressUpdate->update_at);
if (trim((string)$progressUpdate->weight) !== '') {
$chipRows[] = Yii::t('AnimalManagementModule.base', 'Weight') . ': ' . trim((string)$progressUpdate->weight);
}
if (trim((string)$progressUpdate->vitals) !== '') {
$chipRows[] = Yii::t('AnimalManagementModule.base', 'Vitals');
}
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Animal'), 'value' => $animalName];
if (!empty($progressUpdate->vitals)) {
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Vitals'), 'value' => (string)$progressUpdate->vitals];
}
if (!empty($progressUpdate->behavior_notes)) {
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Behavior'), 'value' => (string)$progressUpdate->behavior_notes];
}
if (!empty($progressUpdate->meal_plan_changes)) {
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Meal Plan'), 'value' => (string)$progressUpdate->meal_plan_changes];
}
if (!empty($progressUpdate->housing_changes)) {
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Housing'), 'value' => (string)$progressUpdate->housing_changes];
}
if (!empty($progressUpdate->medical_concerns)) {
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Medical Concerns'), 'value' => (string)$progressUpdate->medical_concerns];
}
if (!empty($additionalFields)) {
$text = '';
foreach ($additionalFields as $field) {
$text .= $field['label'] . ': ' . $field['value'] . "\n";
}
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Additional Fields'), 'value' => trim($text)];
}
} elseif ($isMedical) {
$hiddenMedicalKeys = [
'second_physician_name',
'second_physician_business_name',
'second_physician_street_address',
'second_physician_city',
'second_physician_state',
'second_physician_zip',
'second_physician_cell_phone',
'second_physician_business_phone',
'second_physician_license_number',
'previous_physicians',
];
$knownMedicalKeys = [
'weight',
'pulse',
'blood_pressure',
'oxygen',
'chronic_conditions',
'acute_conditions',
'special_needs',
'date_of_most_recent_medical_visit',
'physician_name',
'physician_business_name',
'physician_street_address',
'physician_city',
'physician_state',
'physician_zip',
'physician_cell_phone',
'physician_business_phone',
'physician_license_number',
'medical_media_reference',
'media_reference',
];
$vitalLabelOverrides = [
'blood_pressure' => 'BP',
'oxygen' => 'O₂',
];
$fieldsByKey = [];
$additionalFields = [];
foreach ($medicalVisit->getCustomFieldDisplayValues(true) as $customField) {
$fieldKey = (string)($customField['field_key'] ?? '');
if (in_array($fieldKey, $hiddenMedicalKeys, true)) {
continue;
}
$fieldValue = trim((string)($customField['value'] ?? ''));
if ($fieldValue === '') {
continue;
}
if ($fieldKey === 'medical_media_reference' || $fieldKey === 'media_reference') {
$mediaReference = $fieldValue;
continue;
}
if (in_array($fieldKey, $knownMedicalKeys, true)) {
$fieldsByKey[$fieldKey] = [
'label' => (string)($vitalLabelOverrides[$fieldKey] ?? ($customField['label'] ?? $fieldKey)),
'value' => $fieldValue,
];
continue;
}
$additionalFields[] = [
'label' => (string)($customField['label'] ?? $fieldKey),
'value' => $fieldValue,
];
}
$dateText = DateDisplayHelper::format((string)$medicalVisit->visit_at);
foreach (['weight', 'pulse', 'blood_pressure', 'oxygen'] as $vitalKey) {
if (!empty($fieldsByKey[$vitalKey]['value'])) {
$chipRows[] = $fieldsByKey[$vitalKey]['label'] . ': ' . $fieldsByKey[$vitalKey]['value'];
}
}
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Animal'), 'value' => $animalName];
if (!empty($medicalVisit->provider_name)) {
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Provider'), 'value' => (string)$medicalVisit->provider_name];
}
if (!empty($medicalVisit->notes)) {
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Clinical Notes'), 'value' => (string)$medicalVisit->notes];
}
if (!empty($medicalVisit->recommendations)) {
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Recommendations'), 'value' => (string)$medicalVisit->recommendations];
}
foreach (['chronic_conditions', 'acute_conditions', 'special_needs'] as $conditionKey) {
if (empty($fieldsByKey[$conditionKey]['value'])) {
continue;
}
$detailRows[] = [
'label' => (string)$fieldsByKey[$conditionKey]['label'],
'value' => (string)$fieldsByKey[$conditionKey]['value'],
];
}
if (!empty($fieldsByKey['date_of_most_recent_medical_visit']['value'])) {
$detailRows[] = [
'label' => (string)$fieldsByKey['date_of_most_recent_medical_visit']['label'],
'value' => DateDisplayHelper::format((string)$fieldsByKey['date_of_most_recent_medical_visit']['value']),
];
}
if (!empty($additionalFields)) {
$text = '';
foreach ($additionalFields as $field) {
$text .= $field['label'] . ': ' . $field['value'] . "\n";
}
$detailRows[] = ['label' => Yii::t('AnimalManagementModule.base', 'Additional Fields'), 'value' => trim($text)];
}
}
$hasMediaImage = $mediaReference !== '' && (preg_match('/^https?:\/\//i', $mediaReference) || substr($mediaReference, 0, 1) === '/');
?>
<div style="position:relative;overflow:hidden;border:1px solid #d5dfe8;border-radius:12px;background:#223446;margin:6px 0;min-height:240px;box-shadow:0 8px 22px rgba(12,24,36,0.16);">
<?php if ($hasMediaImage): ?>
<img src="<?= Html::encode($mediaReference) ?>" alt="<?= Yii::t('AnimalManagementModule.base', 'Animal stream media') ?>" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;">
<?php endif; ?>
<div style="position:absolute;inset:0;background:linear-gradient(110deg,rgba(10,18,28,0.28) 10%,rgba(10,18,28,0.55) 52%,rgba(10,18,28,0.75) 100%);"></div>
<div style="position:relative;z-index:1;min-height:240px;padding:14px;display:flex;flex-direction:column;align-items:flex-end;gap:10px;">
<div style="width:100%;display:flex;align-items:center;justify-content:space-between;gap:8px;">
<span style="font-size:15px;font-weight:700;color:#ffffff;margin-right:auto;"><?= Html::encode($dateText) ?></span>
<?php if (!empty($chipRows)): ?>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:0;justify-content:flex-end;">
<?php foreach ($chipRows as $chip): ?>
<span style="display:inline-block;border-radius:999px;border:1px solid rgba(255,255,255,0.3);background:rgba(255,255,255,0.16);color:#ffffff;font-size:12px;padding:4px 10px;"><?= Html::encode($chip) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div style="width:55%;max-width:55%;min-width:0;margin-left:auto;border-radius:10px;border:1px solid rgba(255,255,255,0.22);background:rgba(10,18,28,0.5);backdrop-filter:blur(2px);padding:12px;color:#ecf2f8;">
<div style="color:#ffffff;font-size:13px;font-weight:700;margin-bottom:8px;">
<?= Html::encode($isMedical
? Yii::t('AnimalManagementModule.base', 'Medical Visit')
: Yii::t('AnimalManagementModule.base', 'Progress Update')) ?>
</div>
<?php foreach ($detailRows as $row): ?>
<div style="font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:rgba(231,241,249,0.78);margin-bottom:4px;"><?= Html::encode((string)$row['label']) ?></div>
<div style="color:#eff5fb;margin-bottom:10px;"><?= nl2br(Html::encode((string)$row['value'])) ?></div>
<?php endforeach; ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,95 @@
<?php
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\content\components\ContentContainerActiveRecord;
use yii\helpers\Html;
/* @var Animal[] $animals */
/* @var ContentContainerActiveRecord $contentContainer */
/* @var string $queryValue */
/* @var string $heading */
/* @var array $tileFields */
/* @var array<int, array> $tileFieldOverrides */
/* @var array<int, AnimalMedicalVisit> $latestMedicalVisitByAnimal */
/* @var array<int, string> $animalImageUrls */
/* @var int $totalCount */
/* @var int $displayCount */
/* @var bool $hasMore */
/* @var int $nextCount */
/* @var bool $showAll */
$moduleEnabled = $contentContainer->moduleManager->isEnabled('animal_management');
$currentParams = Yii::$app->request->getQueryParams();
$buildProfileUrl = static function (array $overrides) use ($contentContainer, $currentParams): string {
$params = array_merge($currentParams, $overrides);
return $contentContainer->createUrl('/space_profiles/profile/view', $params);
};
?>
<h4 style="margin-top:0;"><?= Html::encode($heading) ?></h4>
<?php if ($moduleEnabled): ?>
<form method="get" action="<?= Html::encode($contentContainer->createUrl('/space_profiles/profile/view')) ?>" style="margin-bottom:12px;">
<div class="input-group">
<input
type="text"
class="form-control"
name="q"
value="<?= Html::encode($queryValue) ?>"
placeholder="<?= Html::encode(Yii::t('AnimalManagementModule.base', 'Search by name, species, or ID')) ?>"
>
<input type="hidden" name="animalFeedCount" value="10">
<input type="hidden" name="animalFeedAll" value="0">
<span class="input-group-btn">
<button class="btn btn-default" type="submit"><?= Yii::t('AnimalManagementModule.base', 'Search') ?></button>
</span>
</div>
</form>
<?php else: ?>
<div class="well well-sm" style="margin-bottom:12px;">
<?= Yii::t('AnimalManagementModule.base', 'Animal profiles are not enabled for this rescue.') ?>
</div>
<?php endif; ?>
<?php if (empty($animals)): ?>
<div class="text-muted" style="margin-bottom:10px;">
<?= Yii::t('AnimalManagementModule.base', 'No matching animal profiles found.') ?>
</div>
<?php else: ?>
<div class="row" style="margin-bottom:6px;">
<?php foreach ($animals as $animal): ?>
<?php $animalId = (int)$animal->id; ?>
<div class="col-xs-12" style="margin-bottom:12px;">
<?= $this->renderFile(Yii::getAlias('@app/modules/animal_management/views/animals/_tile.php'), [
'animal' => $animal,
'contentContainer' => $contentContainer,
'lastMedical' => $latestMedicalVisitByAnimal[$animalId] ?? null,
'imageUrl' => $animalImageUrls[$animalId] ?? '',
'tileFields' => $tileFieldOverrides[$animalId] ?? $tileFields,
'showMedicalIcon' => true,
]) ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($moduleEnabled): ?>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<?php if ($hasMore): ?>
<a class="btn btn-default btn-sm" href="<?= Html::encode($buildProfileUrl(['animalFeedCount' => $nextCount, 'animalFeedAll' => 0])) ?>">
<?= Yii::t('AnimalManagementModule.base', 'Show More') ?>
</a>
<?php endif; ?>
<?php if (!$showAll && $totalCount > 0): ?>
<a class="btn btn-default btn-sm" href="<?= Html::encode($buildProfileUrl(['animalFeedAll' => 1])) ?>">
<?= Yii::t('AnimalManagementModule.base', 'Show All') ?>
</a>
<?php endif; ?>
<?php if ($totalCount > 0): ?>
<span class="text-muted" style="font-size:12px;">
<?= Yii::t('AnimalManagementModule.base', '{shown} of {total} shown', ['shown' => $displayCount, 'total' => $totalCount]) ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>