From 039c12233e2f94e3d0e78cb7af6964a0dad7fc5a Mon Sep 17 00:00:00 2001 From: Kelin Rescue Hub Date: Thu, 9 Apr 2026 14:11:34 -0400 Subject: [PATCH] chore: sync module from working instance and add install guide --- INSTALL.md | 55 + controllers/AnimalsController.php | 412 ++++- controllers/SettingsController.php | 33 + events/AnimalTileRenderEvent.php | 41 + events/AnimalTileSizeEvent.php | 32 + services/AnimalStreamPublisherService.php | 2 + services/ModuleSetupService.php | 72 + views/animals/_tablet_quick_donate_extras.php | 184 ++ views/animals/_tile.php | 1532 ++++++++++++++++- views/animals/_transfer_tile.php | 36 +- views/animals/add-gallery-images-inline.php | 305 ++++ views/animals/add-medical-visit.php | 171 +- views/animals/add-progress-update.php | 140 +- views/animals/create.php | 61 +- views/animals/edit-medical-visit.php | 170 +- views/animals/edit-progress-update.php | 140 +- views/animals/index.php | 108 +- views/animals/inline-edit-result.php | 53 +- views/animals/transfer.php | 188 +- views/animals/view.php | 1044 ++++++++++- views/settings/index.php | 21 + widgets/SearchAnimalProfilesBlock.php | 95 +- widgets/views/searchAnimalProfilesBlock.php | 76 +- 23 files changed, 4577 insertions(+), 394 deletions(-) create mode 100644 INSTALL.md create mode 100644 events/AnimalTileRenderEvent.php create mode 100644 events/AnimalTileSizeEvent.php create mode 100644 services/ModuleSetupService.php create mode 100644 views/animals/_tablet_quick_donate_extras.php create mode 100644 views/animals/add-gallery-images-inline.php diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..2c39045 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,55 @@ +# Animal Management Installation Guide + +This guide installs the `animal_management` module in a reusable way for any HumHub instance. + +## 1. Requirements + +- HumHub `1.14+` +- Module directory access on the target instance +- Optional but recommended: `rescue_foundation` module + +## 2. Clone into HumHub Modules Directory + +The folder name must be exactly `animal_management`. + +```bash +git clone https://gitea.kelinreij.duckdns.org/humhub-modules/animal-management.git \ + /var/www/localhost/htdocs/protected/modules/animal_management +``` + +If the folder already exists: + +```bash +cd /var/www/localhost/htdocs/protected/modules/animal_management +git pull +``` + +## 3. Enable the Module + +In HumHub UI: + +1. Go to `Administration` -> `Modules`. +2. Enable `Animal Management`. +3. Enable it per space where needed. + +## 4. Run Migrations + +From the HumHub app host/container: + +```bash +php /var/www/localhost/htdocs/protected/yii migrate/up \ + --include-module-migrations=1 --interactive=0 +``` + +## 5. Verify + +1. Open a space where the module is enabled. +2. Confirm Animals views load. +3. Confirm medical/progress/transfer flows render without errors. + +## Docker Example + +```bash +docker exec humhub php /var/www/localhost/htdocs/protected/yii migrate/up \ + --include-module-migrations=1 --interactive=0 +``` diff --git a/controllers/AnimalsController.php b/controllers/AnimalsController.php index 8979c4b..6eb5c23 100644 --- a/controllers/AnimalsController.php +++ b/controllers/AnimalsController.php @@ -42,6 +42,8 @@ class AnimalsController extends ContentContainerController 'add-medical-visit' => ['post'], 'add-progress-update' => ['post'], 'add-gallery-images' => ['post'], + 'add-gallery-images-inline' => ['get', 'post'], + 'transfer-inline' => ['get', 'post'], 'remove-gallery-image' => ['post'], 'transfer-respond' => ['post'], 'transfer-complete' => ['post'], @@ -55,7 +57,7 @@ class AnimalsController extends ContentContainerController protected function getAccessRules() { return [ - [ContentContainerControllerAccess::RULE_USER_GROUP_ONLY => [Space::USERGROUP_OWNER, Space::USERGROUP_ADMIN, Space::USERGROUP_MODERATOR], 'actions' => ['create', 'edit', 'delete', 'transfer', 'add-medical-visit', 'add-medical-visit-inline', 'edit-medical-visit', 'add-progress-update', 'add-progress-update-inline', 'edit-progress-update', 'add-gallery-images', 'remove-gallery-image', 'transfer-respond', 'transfer-complete', 'transfer-cancel']], + [ContentContainerControllerAccess::RULE_USER_GROUP_ONLY => [Space::USERGROUP_OWNER, Space::USERGROUP_ADMIN, Space::USERGROUP_MODERATOR], 'actions' => ['create', 'edit', 'delete', 'transfer', 'transfer-inline', 'add-medical-visit', 'add-medical-visit-inline', 'edit-medical-visit', 'add-progress-update', 'add-progress-update-inline', 'edit-progress-update', 'add-gallery-images', 'add-gallery-images-inline', 'remove-gallery-image', 'transfer-respond', 'transfer-complete', 'transfer-cancel']], ]; } @@ -68,9 +70,13 @@ class AnimalsController extends ContentContainerController GalleryIntegrationService::syncSpaceAnimalGalleries($this->contentContainer); } $viewMode = trim((string)Yii::$app->request->get('view', 'tiles')); - if (!in_array($viewMode, ['tiles', 'table'], true)) { + if (!in_array($viewMode, ['tiles', 'tiles2', 'rows', 'tablet', 'table'], true)) { $viewMode = 'tiles'; } + $isFocusModeRequest = trim((string)Yii::$app->request->get('focus', '')) === '1'; + if ($isFocusModeRequest) { + $viewMode = 'tablet'; + } $sortKey = trim((string)Yii::$app->request->get('sort', 'updated_at')); $sortDirection = strtolower(trim((string)Yii::$app->request->get('direction', 'desc'))) === 'asc' ? 'asc' : 'desc'; @@ -165,6 +171,33 @@ class AnimalsController extends ContentContainerController return (int)$animal->id; }, $animals); + $animalDonationGoalsByAnimal = []; + $donationGoalClass = 'humhub\\modules\\donations\\models\\DonationGoal'; + if (!empty($animalIds) + && $this->contentContainer instanceof Space + && $this->contentContainer->moduleManager->isEnabled('donations') + && class_exists($donationGoalClass) + && Yii::$app->db->schema->getTableSchema($donationGoalClass::tableName(), true) !== null + ) { + $donationGoals = $donationGoalClass::find() + ->where([ + 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, + 'goal_type' => $donationGoalClass::TYPE_ANIMAL, + ]) + ->andWhere(['target_animal_id' => $animalIds]) + ->orderBy(['is_active' => SORT_DESC, 'id' => SORT_DESC]) + ->all(); + + foreach ($donationGoals as $goal) { + $animalId = (int)$goal->target_animal_id; + if ($animalId <= 0 || isset($animalDonationGoalsByAnimal[$animalId])) { + continue; + } + + $animalDonationGoalsByAnimal[$animalId] = $goal; + } + } + $animalImageUrls = $this->resolveAnimalImageUrls($animalIds, ['profile_image_url', 'profile_image', 'photo_url', 'image_url', 'photo'], false); $tileFieldOverrides = $this->resolveDisplayFieldOverrides($animalIds, 'tile_display_fields'); @@ -245,6 +278,7 @@ class AnimalsController extends ContentContainerController 'animalImageUrls' => $animalImageUrls, 'tileFields' => $this->getTileFieldSettings(), 'tileFieldOverrides' => $tileFieldOverrides, + 'animalDonationGoalsByAnimal' => $animalDonationGoalsByAnimal, 'space' => $this->contentContainer, 'canManage' => $this->canManageAnimals(), 'incomingTransfers' => $incomingTransfers, @@ -257,6 +291,10 @@ class AnimalsController extends ContentContainerController { $animal = $this->findAnimal($id); $canManage = $this->canManageAnimals(); + $layoutMode = trim((string)Yii::$app->request->get('layout', 'default')); + if (!in_array($layoutMode, ['default', 'tablet'], true)) { + $layoutMode = 'default'; + } if ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($animal, $this->contentContainer); @@ -279,8 +317,10 @@ class AnimalsController extends ContentContainerController $transferEvents = $animal->getTransferEvents()->orderBy(['id' => SORT_DESC])->limit(100)->all(); $galleryItems = $animal->getGalleryItems()->orderBy(['id' => SORT_DESC])->limit(120)->all(); $customFieldValues = $animal->getCustomFieldDisplayValues($canManage); - $animalImageUrls = $this->resolveAnimalImageUrls([(int)$animal->id], ['cover_image_url', 'image_url', 'photo_url', 'photo'], false); - $animalCoverImageUrl = trim((string)($animalImageUrls[(int)$animal->id] ?? '')); + $animalCoverImageUrls = $this->resolveAnimalImageUrls([(int)$animal->id], ['cover_image_url', 'image_url', 'photo_url', 'photo'], false); + $animalProfileImageUrls = $this->resolveAnimalImageUrls([(int)$animal->id], ['profile_image_url', 'profile_image', 'image_url', 'photo_url', 'photo'], false); + $animalCoverImageUrl = trim((string)($animalCoverImageUrls[(int)$animal->id] ?? '')); + $animalProfileImageUrl = trim((string)($animalProfileImageUrls[(int)$animal->id] ?? '')); $detailHeroFields = $this->getDetailHeroFieldSettings(); $heroOverrides = $this->resolveDisplayFieldOverrides([(int)$animal->id], 'hero_display_fields'); if (!empty($heroOverrides[(int)$animal->id])) { @@ -298,7 +338,9 @@ class AnimalsController extends ContentContainerController 'galleryItems' => $galleryItems, 'customFieldValues' => $customFieldValues, 'animalCoverImageUrl' => $animalCoverImageUrl, + 'animalProfileImageUrl' => $animalProfileImageUrl, 'detailHeroFields' => $detailHeroFields, + 'layoutMode' => $layoutMode, ]); } @@ -352,6 +394,7 @@ class AnimalsController extends ContentContainerController public function actionCreate() { $model = new AnimalForm(['contentContainer' => $this->contentContainer]); + $intakeGoalPayload = (array)Yii::$app->request->post('IntakeGoal', []); if (Yii::$app->request->isPost) { $model->load(Yii::$app->request->post()); @@ -368,6 +411,11 @@ class AnimalsController extends ContentContainerController if ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($savedAnimal, $this->contentContainer); } + + $goalError = $this->createIntakeAnimalGoal($savedAnimal, $intakeGoalPayload); + if ($goalError !== null) { + $this->view->error($goalError); + } } $this->view->success(Yii::t('AnimalManagementModule.base', 'Animal created.')); @@ -379,6 +427,7 @@ class AnimalsController extends ContentContainerController 'space' => $this->contentContainer, 'isEdit' => false, 'animal' => null, + 'showIntakeGoalSection' => $this->canUseIntakeGoalSection(), ]); } @@ -460,6 +509,7 @@ class AnimalsController extends ContentContainerController public function actionTransfer(int $id) { $animal = $this->findAnimal($id); + $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'view')); $form = new TransferRequestForm([ 'animal' => $animal, @@ -468,107 +518,74 @@ class AnimalsController extends ContentContainerController if ($form->load(Yii::$app->request->post()) && $form->save()) { $this->view->success(Yii::t('AnimalManagementModule.base', 'Transfer request sent.')); - return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id])); + return $this->redirectToAnimalPage((int)$animal->id, $returnTo); } return $this->render('transfer', [ 'space' => $this->contentContainer, 'animal' => $animal, 'model' => $form, + 'isInline' => false, + 'returnTo' => $returnTo, ]); } + public function actionTransferInline(int $id) + { + $animal = $this->findAnimal($id); + $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'view')); + $isInline = (int)Yii::$app->request->get('inline', 1) === 1; + + $form = new TransferRequestForm([ + 'animal' => $animal, + 'sourceSpace' => $this->contentContainer, + ]); + + if ($form->load(Yii::$app->request->post()) && $form->save()) { + $this->view->success(Yii::t('AnimalManagementModule.base', 'Transfer request sent.')); + if ($isInline) { + return $this->renderAjax('inline-edit-result', [ + 'collapseId' => 'transfer-add-inline', + 'refreshSelectors' => ['#animal-transfer-panel'], + ]); + } + return $this->redirectToAnimalPage((int)$animal->id, $returnTo); + } + + $params = [ + 'space' => $this->contentContainer, + 'animal' => $animal, + 'model' => $form, + 'isInline' => $isInline, + 'returnTo' => $returnTo, + ]; + + if ($isInline) { + return $this->renderAjax('transfer', $params); + } + + return $this->render('transfer', $params); + } + public function actionAddGalleryImages(int $id) { $animal = $this->findAnimal($id); - $uploadedFiles = UploadedFile::getInstancesByName('galleryImages'); - $maxUploadCount = 10; + $result = $this->processGalleryUploads($animal); - if (empty($uploadedFiles)) { + if (!$result['hadSelection']) { $this->view->error(Yii::t('AnimalManagementModule.base', 'No gallery images were selected.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id]) . '#animal-gallery'); } - if (count($uploadedFiles) > $maxUploadCount) { - $uploadedFiles = array_slice($uploadedFiles, 0, $maxUploadCount); - $this->view->info(Yii::t('AnimalManagementModule.base', 'Only the first {count} selected images were processed.', ['count' => $maxUploadCount])); + if ($result['wasLimited']) { + $this->view->info(Yii::t('AnimalManagementModule.base', 'Only the first {count} selected images were processed.', ['count' => $result['maxUploadCount']])); } - $allowedExtensions = array_map('strtolower', UploadStandards::imageExtensions()); - $allowedMimeTypes = array_map('strtolower', UploadStandards::imageMimeTypes()); - $maxBytes = (int)UploadStandards::IMAGE_MAX_BYTES; - $existingItems = AnimalGalleryItem::find()->where(['animal_id' => (int)$animal->id])->all(); - $existingHashes = []; - foreach ($existingItems as $existingItem) { - $existingUrl = trim((string)$existingItem->getImageUrl()); - if ($existingUrl === '') { - continue; - } - - $hash = $this->computeImageContentHash($existingUrl); - if ($hash !== null) { - $existingHashes[$hash] = true; - } - } - - $added = 0; - foreach ($uploadedFiles as $uploadedFile) { - if (!$uploadedFile instanceof UploadedFile) { - continue; - } - - $extension = strtolower((string)$uploadedFile->extension); - $mimeType = strtolower((string)$uploadedFile->type); - - if (!in_array($extension, $allowedExtensions, true)) { - continue; - } - - if ($mimeType !== '' && !in_array($mimeType, $allowedMimeTypes, true)) { - continue; - } - - if ($uploadedFile->size > $maxBytes) { - continue; - } - - $storedPath = $this->storeGalleryUpload($animal, $uploadedFile); - if ($storedPath === null) { - continue; - } - - $storedHash = $this->computeImageContentHash($storedPath); - - $exactExists = AnimalGalleryItem::find() - ->where(['animal_id' => (int)$animal->id, 'file_path' => $storedPath]) - ->exists(); - - if ($exactExists || ($storedHash !== null && isset($existingHashes[$storedHash]))) { - $absolute = Yii::getAlias('@webroot') . $storedPath; - if (is_file($absolute)) { - @unlink($absolute); - } - continue; - } - - $item = new AnimalGalleryItem(); - $item->animal_id = (int)$animal->id; - $item->file_path = $storedPath; - $item->source_type = 'upload'; - $item->created_by = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id; - if ($item->save()) { - $added++; - if ($storedHash !== null) { - $existingHashes[$storedHash] = true; - } - } - } - - if ($added > 0) { + if ($result['added'] > 0) { if ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($animal, $this->contentContainer); } - $this->view->success(Yii::t('AnimalManagementModule.base', '{count} image(s) added to gallery.', ['count' => $added])); + $this->view->success(Yii::t('AnimalManagementModule.base', '{count} image(s) added to gallery.', ['count' => $result['added']])); } else { $this->view->error(Yii::t('AnimalManagementModule.base', 'No gallery images were added. Check image type/size requirements.')); } @@ -576,6 +593,63 @@ class AnimalsController extends ContentContainerController return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id]) . '#animal-gallery'); } + public function actionAddGalleryImagesInline(int $id) + { + $animal = $this->findAnimal($id); + $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'view')); + $isInline = (int)Yii::$app->request->get('inline', 1) === 1; + $maxUploadCount = 10; + $errorMessage = ''; + $infoMessage = ''; + + if (Yii::$app->request->isPost) { + $result = $this->processGalleryUploads($animal); + + if (!$result['hadSelection']) { + $errorMessage = Yii::t('AnimalManagementModule.base', 'No gallery images were selected.'); + } else { + if ($result['wasLimited']) { + $infoMessage = Yii::t('AnimalManagementModule.base', 'Only the first {count} selected images were processed.', ['count' => $result['maxUploadCount']]); + } + + if ($result['added'] > 0) { + if ($this->contentContainer instanceof Space) { + GalleryIntegrationService::ensureAnimalGallery($animal, $this->contentContainer); + } + $this->view->success(Yii::t('AnimalManagementModule.base', '{count} image(s) added to gallery.', ['count' => $result['added']])); + if ($isInline) { + return $this->renderAjax('inline-edit-result', [ + 'collapseId' => 'gallery-add-inline', + 'refreshSelectors' => ['#animal-gallery-panel'], + ]); + } + return $this->redirectToAnimalPage((int)$animal->id, $returnTo); + } + + $errorMessage = Yii::t('AnimalManagementModule.base', 'No gallery images were added. Check image type/size requirements.'); + } + } + + $galleryItems = $animal->getGalleryItems()->orderBy(['id' => SORT_DESC])->limit(120)->all(); + + $params = [ + 'space' => $this->contentContainer, + 'animal' => $animal, + 'galleryItems' => $galleryItems, + 'isInline' => $isInline, + 'returnTo' => $returnTo, + 'maxUploadCount' => $maxUploadCount, + 'errorMessage' => $errorMessage, + 'infoMessage' => $infoMessage, + ]; + + if ($isInline) { + return $this->renderAjax('add-gallery-images-inline', $params); + } + + return $this->render('add-gallery-images-inline', $params); + } + public function actionRemoveGalleryImage(int $id, int $galleryId) { $animal = $this->findAnimal($id); @@ -646,6 +720,9 @@ class AnimalsController extends ContentContainerController $animal = $this->findAnimal($id); $this->ensureMedicalMediaFieldDefinition(); $form = new AnimalMedicalVisitForm(['animal' => $animal]); + if (trim((string)$form->visit_at) === '') { + $form->visit_at = date('Y-m-d\TH:i'); + } $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'medical-visits')); $isInline = (int)Yii::$app->request->get('inline', 1) === 1; @@ -1258,6 +1335,105 @@ class AnimalsController extends ContentContainerController return $result; } + private function processGalleryUploads(Animal $animal): array + { + $uploadedFiles = UploadedFile::getInstancesByName('galleryImages'); + $maxUploadCount = 10; + $hadSelection = !empty($uploadedFiles); + $wasLimited = false; + + if (!$hadSelection) { + return [ + 'hadSelection' => false, + 'wasLimited' => false, + 'maxUploadCount' => $maxUploadCount, + 'added' => 0, + ]; + } + + if (count($uploadedFiles) > $maxUploadCount) { + $uploadedFiles = array_slice($uploadedFiles, 0, $maxUploadCount); + $wasLimited = true; + } + + $allowedExtensions = array_map('strtolower', UploadStandards::imageExtensions()); + $allowedMimeTypes = array_map('strtolower', UploadStandards::imageMimeTypes()); + $maxBytes = (int)UploadStandards::IMAGE_MAX_BYTES; + $existingItems = AnimalGalleryItem::find()->where(['animal_id' => (int)$animal->id])->all(); + $existingHashes = []; + foreach ($existingItems as $existingItem) { + $existingUrl = trim((string)$existingItem->getImageUrl()); + if ($existingUrl === '') { + continue; + } + + $hash = $this->computeImageContentHash($existingUrl); + if ($hash !== null) { + $existingHashes[$hash] = true; + } + } + + $added = 0; + foreach ($uploadedFiles as $uploadedFile) { + if (!$uploadedFile instanceof UploadedFile) { + continue; + } + + $extension = strtolower((string)$uploadedFile->extension); + $mimeType = strtolower((string)$uploadedFile->type); + + if (!in_array($extension, $allowedExtensions, true)) { + continue; + } + + if ($mimeType !== '' && !in_array($mimeType, $allowedMimeTypes, true)) { + continue; + } + + if ($uploadedFile->size > $maxBytes) { + continue; + } + + $storedPath = $this->storeGalleryUpload($animal, $uploadedFile); + if ($storedPath === null) { + continue; + } + + $storedHash = $this->computeImageContentHash($storedPath); + + $exactExists = AnimalGalleryItem::find() + ->where(['animal_id' => (int)$animal->id, 'file_path' => $storedPath]) + ->exists(); + + if ($exactExists || ($storedHash !== null && isset($existingHashes[$storedHash]))) { + $absolute = Yii::getAlias('@webroot') . $storedPath; + if (is_file($absolute)) { + @unlink($absolute); + } + continue; + } + + $item = new AnimalGalleryItem(); + $item->animal_id = (int)$animal->id; + $item->file_path = $storedPath; + $item->source_type = 'upload'; + $item->created_by = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id; + if ($item->save()) { + $added++; + if ($storedHash !== null) { + $existingHashes[$storedHash] = true; + } + } + } + + return [ + 'hadSelection' => true, + 'wasLimited' => $wasLimited, + 'maxUploadCount' => $maxUploadCount, + 'added' => $added, + ]; + } + private function storeGalleryUpload(Animal $animal, UploadedFile $file): ?string { $random = Yii::$app->security->generateRandomString(8); @@ -1707,4 +1883,74 @@ class AnimalsController extends ContentContainerController $item->created_by = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id; $item->save(); } + + private function canUseIntakeGoalSection(): bool + { + if (!($this->contentContainer instanceof Space)) { + return false; + } + + if (!$this->contentContainer->moduleManager->isEnabled('donations')) { + return false; + } + + $goalFormClass = 'humhub\\modules\\donations\\models\\forms\\DonationGoalForm'; + $goalClass = 'humhub\\modules\\donations\\models\\DonationGoal'; + + if (!class_exists($goalFormClass) || !class_exists($goalClass)) { + return false; + } + + return Yii::$app->db->schema->getTableSchema($goalClass::tableName(), true) !== null; + } + + private function createIntakeAnimalGoal(Animal $animal, array $payload): ?string + { + if ((int)($payload['enabled'] ?? 0) !== 1) { + return null; + } + + if (!$this->canUseIntakeGoalSection()) { + return Yii::t('AnimalManagementModule.base', 'Animal was created, but donation goal setup is not available in this space.'); + } + + $goalFormClass = 'humhub\\modules\\donations\\models\\forms\\DonationGoalForm'; + $goalClass = 'humhub\\modules\\donations\\models\\DonationGoal'; + + $targetAmount = max(0.0, (float)($payload['target_amount'] ?? 0)); + if ($targetAmount <= 0) { + return Yii::t('AnimalManagementModule.base', 'Animal was created, but the intake donation goal target must be greater than zero.'); + } + + $title = trim((string)($payload['title'] ?? '')); + if ($title === '') { + $title = Yii::t('AnimalManagementModule.base', '{animalName} Care Fund', ['animalName' => $animal->getDisplayName()]); + } + + $description = trim((string)($payload['description'] ?? '')); + $imagePath = trim((string)($payload['image_path'] ?? '')); + $isActive = (int)($payload['is_active'] ?? 1) === 1; + + $goalForm = new $goalFormClass(); + $goalForm->contentContainer = $this->contentContainer; + $goalForm->goal_type = $goalClass::TYPE_ANIMAL; + $goalForm->target_animal_id = (int)$animal->id; + $goalForm->title = $title; + $goalForm->description = $description; + $goalForm->target_amount = $targetAmount; + $goalForm->is_active = $isActive; + + if ($imagePath !== '') { + $goalForm->imageGalleryPath = $imagePath; + } + + $savedGoal = $goalForm->save(); + if ($savedGoal === null) { + $firstErrors = $goalForm->getFirstErrors(); + $errorText = !empty($firstErrors) ? implode(' ', array_values($firstErrors)) : Yii::t('AnimalManagementModule.base', 'Unknown donation goal error.'); + return Yii::t('AnimalManagementModule.base', 'Animal was created, but donation goal could not be saved: {error}', ['error' => $errorText]); + } + + return null; + } } diff --git a/controllers/SettingsController.php b/controllers/SettingsController.php index 93cbf5b..f304445 100644 --- a/controllers/SettingsController.php +++ b/controllers/SettingsController.php @@ -5,11 +5,13 @@ 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\animal_management\services\ModuleSetupService; 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; +use yii\web\BadRequestHttpException; class SettingsController extends ContentContainerController { @@ -53,6 +55,37 @@ class SettingsController extends ContentContainerController 'animalCount' => (int)$animalCount, 'fieldSettingsForm' => $fieldSettingsForm, 'displaySettingsForm' => $displaySettingsForm, + 'space' => $this->contentContainer, ]); } + + public function actionSetup() + { + if (!Yii::$app->request->isPost) { + throw new BadRequestHttpException('Invalid request method.'); + } + + if (!$this->contentContainer instanceof Space) { + $this->view->error(Yii::t('AnimalManagementModule.base', 'Setup can only be run inside a space.')); + return $this->redirect($this->contentContainer->createUrl('/animal_management/settings')); + } + + try { + $result = ModuleSetupService::runForSpace($this->contentContainer); + $appliedCount = count($result['applied'] ?? []); + + if ($appliedCount > 0) { + $this->view->success(Yii::t('AnimalManagementModule.base', 'Setup completed. Applied {count} migration(s).', [ + 'count' => $appliedCount, + ])); + } else { + $this->view->success(Yii::t('AnimalManagementModule.base', 'Setup completed. No pending migrations were found.')); + } + } catch (\Throwable $e) { + Yii::error($e, 'animal_management.setup'); + $this->view->error(Yii::t('AnimalManagementModule.base', 'Setup failed. Please check logs and try again.')); + } + + return $this->redirect($this->contentContainer->createUrl('/animal_management/settings')); + } } diff --git a/events/AnimalTileRenderEvent.php b/events/AnimalTileRenderEvent.php new file mode 100644 index 0000000..d364ed6 --- /dev/null +++ b/events/AnimalTileRenderEvent.php @@ -0,0 +1,41 @@ +htmlFragments[] = $html; + } + + public function getHtml(): string + { + if (empty($this->htmlFragments)) { + return ''; + } + + return implode("\n", $this->htmlFragments); + } +} diff --git a/events/AnimalTileSizeEvent.php b/events/AnimalTileSizeEvent.php new file mode 100644 index 0000000..e732e66 --- /dev/null +++ b/events/AnimalTileSizeEvent.php @@ -0,0 +1,32 @@ +additionalHeightPx += $heightPx; + } + + public function getAdditionalHeightPx(): int + { + return max(0, $this->additionalHeightPx); + } +} diff --git a/services/AnimalStreamPublisherService.php b/services/AnimalStreamPublisherService.php index b396516..a2c47c8 100644 --- a/services/AnimalStreamPublisherService.php +++ b/services/AnimalStreamPublisherService.php @@ -61,6 +61,8 @@ class AnimalStreamPublisherService $entry->medical_visit_id = $medicalVisitId; $entry->progress_update_id = $progressUpdateId; $entry->content->container = $space; + // Respect each space's configured stream visibility policy. + $entry->content->visibility = (int)$space->getDefaultContentVisibility(); if (!$entry->save()) { Yii::warning([ diff --git a/services/ModuleSetupService.php b/services/ModuleSetupService.php new file mode 100644 index 0000000..dcda16b --- /dev/null +++ b/services/ModuleSetupService.php @@ -0,0 +1,72 @@ +getModule('animal_management')->settings->contentContainer($space); + if ($settings->get('searchBlockHeading') === null) { + $settings->set('searchBlockHeading', 'Animals Available for Intake & Transfer'); + $result['defaultsApplied'] = ['searchBlockHeading']; + } else { + $result['defaultsApplied'] = []; + } + + return $result; + } + + private static function applyModuleMigrations(): array + { + $migrationDir = dirname(__DIR__) . '/migrations'; + $files = glob($migrationDir . '/m*.php') ?: []; + sort($files, SORT_NATURAL); + + $existingVersions = Yii::$app->db->createCommand('SELECT version FROM migration')->queryColumn(); + $history = array_fill_keys($existingVersions, true); + + $applied = []; + $skipped = []; + + foreach ($files as $file) { + $version = pathinfo($file, PATHINFO_FILENAME); + if (isset($history[$version])) { + $skipped[] = $version; + continue; + } + + if (!class_exists($version, false)) { + require_once $file; + } + + if (!class_exists($version, false)) { + throw new \RuntimeException('Migration class not found: ' . $version); + } + + $migration = new $version(); + $ok = method_exists($migration, 'safeUp') ? $migration->safeUp() : $migration->up(); + if ($ok === false) { + throw new \RuntimeException('Migration failed: ' . $version); + } + + Yii::$app->db->createCommand()->insert('migration', [ + 'version' => $version, + 'apply_time' => time(), + ])->execute(); + + $applied[] = $version; + $history[$version] = true; + } + + return [ + 'applied' => $applied, + 'skipped' => $skipped, + ]; + } +} diff --git a/views/animals/_tablet_quick_donate_extras.php b/views/animals/_tablet_quick_donate_extras.php new file mode 100644 index 0000000..6137f15 --- /dev/null +++ b/views/animals/_tablet_quick_donate_extras.php @@ -0,0 +1,184 @@ + +
+
+
+ +
+ +
+ +
+
+ + <?= Html::encode((string)$donationRow['donor']) ?> + + + + + +
+ + 'animal-tile-donation-history-name'] + ) ?> + + + + +
+
+ +
+ +
+ +
+ + __ANIMAL_TILE_QUICK_DONATE_FORM_SLOT__ + + +
+ 0 ? 'checked' : '' ?>> + + + + +
+ 'multipart/form-data', + 'style' => 'margin:0;', + ]) ?> + request->csrfParam, Yii::$app->request->getCsrfToken()) ?> + + + id) ?> + + +
+
+
+ + +
+
+ +
+
+ + 'form-control input-sm', + 'maxlength' => 190, + 'required' => true, + ]) ?> +
+ +
+ +
+ $ + 0 ? (string)round($existingGoalTargetAmount) : '', [ + 'class' => 'form-control input-sm', + 'step' => '1', + 'min' => '1', + 'inputmode' => 'numeric', + 'required' => true, + ]) ?> +
+
+
+ +
+ + 'form-control input-sm', + 'rows' => 2, + ]) ?> +
+ +
+ +
+
+ + + + + +
+ +
+ + $galleryUrl): ?> + id . '-' . (int)$galleryIndex; ?> +
+ + > + +
+ + +
+ +
+
+ + $fileFieldId, + 'class' => 'js-animal-donation-upload animal-tile-goal-upload-input', + 'data-preview' => '#' . $previewId, + 'data-editor-bg' => '#' . $quickDonatePanelId, + 'accept' => 'image/*', + ]) ?> + +
+ +
+
+ +
diff --git a/views/animals/_tile.php b/views/animals/_tile.php index 5a494b0..93b1ded 100644 --- a/views/animals/_tile.php +++ b/views/animals/_tile.php @@ -1,10 +1,20 @@ user->isGuest) { + $canManageTile = $contentContainer->permissionManager->can(new ManageAnimals()); +} +$showInlineAddControls = $canManageTile; $imageUrl = trim((string)$imageUrl); $hasImage = $imageUrl !== '' && (preg_match('/^https?:\/\//i', $imageUrl) || substr($imageUrl, 0, 1) === '/'); @@ -53,29 +74,346 @@ if ($summaryText !== '') { $animalViewUrl = $contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id]); $medicalUrl = $contentContainer->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id]); +$createInlineGoalUrl = $contentContainer->createUrl('/donations/settings/create-animal-goal-inline', [ + 'animalId' => (int)$animal->id, +]); + +$toggleInputId = 'animal-donation-toggle-' . (int)$animal->id; +$inlineFormId = 'animal-donation-inline-' . (int)$animal->id; +$fileFieldId = 'animal-donation-image-file-' . (int)$animal->id; +$previewId = 'animal-donation-image-preview-' . (int)$animal->id; + +$galleryItems = []; +if (class_exists(AnimalGalleryItem::class)) { + $galleryItems = $animal->getGalleryItems() + ->orderBy(['id' => SORT_DESC]) + ->limit(8) + ->all(); +} + +$galleryUrls = []; +foreach ($galleryItems as $galleryItem) { + if (!$galleryItem instanceof AnimalGalleryItem) { + continue; + } + + $url = trim((string)$galleryItem->getImageUrl()); + if ($url === '') { + continue; + } + + $galleryUrls[] = $url; +} + +$overlayStackId = 'animal-tile-overlay-stack-' . (int)$animal->id; +$inlineMenuId = 'animal-tile-inline-menu-' . (int)$animal->id; +$inlinePanelWrapId = 'animal-tile-inline-panel-wrap-' . (int)$animal->id; +$tileUiGalleryId = 'animal-tile-gallery-' . (int)$animal->id; +$inlinePanelRadioName = 'animal-tile-inline-panel-' . (int)$animal->id; +$inlinePanelNoneId = 'animal-tile-inline-none-' . (int)$animal->id; +$inlinePanelIds = [ + 'gallery' => 'animal-tile-inline-gallery-' . (int)$animal->id, + 'files' => 'animal-tile-inline-files-' . (int)$animal->id, + 'medical-progress' => 'animal-tile-inline-medical-progress-' . (int)$animal->id, + 'transfer' => 'animal-tile-inline-transfer-' . (int)$animal->id, +]; +$galleryColsRadioName = 'animal-tile-gallery-cols-' . (int)$animal->id; +$galleryCols2Id = 'animal-tile-gallery-cols-2-' . (int)$animal->id; +$galleryCols3Id = 'animal-tile-gallery-cols-3-' . (int)$animal->id; +$formModeRadioName = 'animal-tile-form-mode-' . (int)$animal->id; +$formModeNoneId = 'animal-tile-form-none-' . (int)$animal->id; +$formModeMedicalId = 'animal-tile-form-medical-' . (int)$animal->id; +$formModeProgressId = 'animal-tile-form-progress-' . (int)$animal->id; +$galleryAddToggleId = 'animal-tile-gallery-add-toggle-' . (int)$animal->id; +$galleryFormWrapId = 'animal-tile-gallery-form-wrap-' . (int)$animal->id; +$galleryListWrapId = 'animal-tile-gallery-list-' . (int)$animal->id; +$transferAddToggleId = 'animal-tile-transfer-add-toggle-' . (int)$animal->id; +$transferFormWrapId = 'animal-tile-transfer-form-wrap-' . (int)$animal->id; +$transferHistoryWrapId = 'animal-tile-transfer-history-wrap-' . (int)$animal->id; +$medicalFormWrapId = 'animal-tile-medical-form-wrap-' . (int)$animal->id; +$progressFormWrapId = 'animal-tile-progress-form-wrap-' . (int)$animal->id; +$donorHistoryToggleId = 'donation-goal-toggle-' . (int)$animal->id; +$quickDonateToggleId = 'donation-goal-quick-toggle-' . (int)$animal->id; +$goalCardId = 'donation-goal-card-' . (int)$animal->id; +$donorPanelId = 'donation-goal-donors-panel-' . (int)$animal->id; +$quickDonatePanelId = 'donation-goal-quick-panel-' . (int)$animal->id; +$goalEditorToggleId = 'donation-goal-editor-toggle-' . (int)$animal->id; + +$cfilesEnabled = false; +$cfilesBrowseUrl = ''; +$recentMedicalVisits = []; +$recentProgressUpdates = []; +$recentTransferEvents = []; +$recentTransfersById = []; +$medicalAddInlineUrl = $contentContainer->createUrl('/animal_management/animals/add-medical-visit-inline', ['id' => (int)$animal->id, 'inline' => 1, 'returnTo' => 'medical-visits']); +$progressAddInlineUrl = $contentContainer->createUrl('/animal_management/animals/add-progress-update-inline', ['id' => (int)$animal->id, 'inline' => 1, 'returnTo' => 'progress-updates']); +$galleryAddInlineUrl = $contentContainer->createUrl('/animal_management/animals/add-gallery-images-inline', ['id' => (int)$animal->id, 'inline' => 1, 'returnTo' => 'view']); +$transferAddInlineUrl = $contentContainer->createUrl('/animal_management/animals/transfer-inline', ['id' => (int)$animal->id, 'inline' => 1, 'returnTo' => 'view']); + +$donationHistoryRows = []; +$existingGoalIdForHistory = is_object($existingDonationGoal) ? (int)($existingDonationGoal->id ?? 0) : 0; + +if ($tileLayoutMode === 'tablet') { + if ($contentContainer instanceof Space) { + $cfilesEnabled = $contentContainer->moduleManager->isEnabled('cfiles'); + } + if ($cfilesEnabled) { + $cfilesBrowseUrl = $contentContainer->createUrl('/cfiles/browse'); + } + + $recentMedicalVisits = $animal->getMedicalVisits() + ->orderBy(['visit_at' => SORT_DESC, 'id' => SORT_DESC]) + ->limit(6) + ->all(); + + if (class_exists(AnimalProgressUpdate::class)) { + $recentProgressUpdates = $animal->getProgressUpdates() + ->orderBy(['update_at' => SORT_DESC, 'id' => SORT_DESC]) + ->limit(6) + ->all(); + } + + if (class_exists(AnimalTransferEvent::class)) { + $recentTransferEvents = $animal->getTransferEvents() + ->orderBy(['id' => SORT_DESC]) + ->limit(18) + ->all(); + + $transferIds = []; + foreach ($recentTransferEvents as $recentTransferEvent) { + if ($recentTransferEvent instanceof AnimalTransferEvent) { + $transferIds[] = (int)$recentTransferEvent->transfer_id; + } + } + $transferIds = array_values(array_unique(array_filter($transferIds))); + + if (!empty($transferIds)) { + $recentTransfersById = AnimalTransfer::find() + ->where(['id' => $transferIds]) + ->indexBy('id') + ->all(); + } + } + + if ($existingGoalIdForHistory > 0) { + $transactionClass = 'humhub\\modules\\donations\\models\\DonationTransaction'; + if (class_exists($transactionClass) && Yii::$app->db->schema->getTableSchema($transactionClass::tableName(), true) !== null) { + $transactions = $transactionClass::find() + ->where([ + 'contentcontainer_id' => (int)$contentContainer->contentcontainer_id, + 'goal_id' => $existingGoalIdForHistory, + 'status' => $transactionClass::STATUS_SUCCEEDED, + ]) + ->orderBy(['id' => SORT_DESC]) + ->limit(120) + ->all(); + + foreach ($transactions as $transaction) { + $amount = max(0.0, (float)($transaction->amount ?? 0)); + if ($amount <= 0) { + continue; + } + + $donorLabel = Yii::t('DonationsModule.base', 'Anonymous'); + $donorUrl = ''; + $donorAvatarUrl = ''; + $createdAtRaw = trim((string)($transaction->created_at ?? '')); + $createdAtTs = $createdAtRaw !== '' ? strtotime($createdAtRaw) : false; + $whenText = $createdAtTs !== false + ? date('m/d/y g:i A', $createdAtTs) + : DateDisplayHelper::format($createdAtRaw); + if ((int)($transaction->is_anonymous ?? 0) !== 1 && (int)($transaction->donor_user_id ?? 0) > 0) { + $userClass = 'humhub\\modules\\user\\models\\User'; + if (class_exists($userClass)) { + $donor = $userClass::findOne(['id' => (int)$transaction->donor_user_id]); + if ($donor !== null) { + $donorLabel = (string)$donor->getDisplayName(); + if (method_exists($donor, 'getUrl')) { + $donorUrl = (string)$donor->getUrl(); + } elseif (method_exists($donor, 'createUrl')) { + $donorUrl = (string)$donor->createUrl('/user/profile'); + } + if (method_exists($donor, 'getProfileImage')) { + $profileImage = $donor->getProfileImage(); + if ($profileImage !== null && method_exists($profileImage, 'getUrl')) { + $donorAvatarUrl = (string)$profileImage->getUrl(); + } + } + } + } + } + + $donationHistoryRows[] = [ + 'when' => $whenText, + 'donor' => $donorLabel, + 'donorUrl' => $donorUrl, + 'avatarUrl' => $donorAvatarUrl, + 'amount' => '$' . number_format($amount, 2), + ]; + } + } + } +} +$defaultGoalTitle = Yii::t('DonationsModule.base', '{animalName} Care Fund', [ + 'animalName' => (string)$animal->getDisplayName(), +]); + +$existingGoalId = is_object($existingDonationGoal) ? (int)($existingDonationGoal->id ?? 0) : 0; +$existingGoalTitle = trim((string)(is_object($existingDonationGoal) ? ($existingDonationGoal->title ?? '') : '')); +$existingGoalTargetAmount = is_object($existingDonationGoal) ? (float)($existingDonationGoal->target_amount ?? 0) : 0.0; +$existingGoalDescription = trim((string)(is_object($existingDonationGoal) ? ($existingDonationGoal->description ?? '') : '')); +$existingGoalImage = trim((string)(is_object($existingDonationGoal) ? ($existingDonationGoal->image_path ?? '') : '')); +$existingGoalActive = is_object($existingDonationGoal) ? (int)($existingDonationGoal->is_active ?? 1) : 1; + +if ($existingGoalTitle === '') { + $existingGoalTitle = $defaultGoalTitle; +} + +$tileRenderEvent = new AnimalTileRenderEvent([ + 'animal' => $animal, + 'contentContainer' => $contentContainer, + 'existingDonationGoal' => $existingDonationGoal, + 'showDonationSettingsButton' => $showDonationSettingsButton, + 'donationToggleInputId' => $toggleInputId, + 'donationInlineFormId' => $inlineFormId, +]); +Event::trigger(AnimalTileRenderEvent::class, AnimalTileRenderEvent::EVENT_RENDER_OVERLAY, $tileRenderEvent); +$tileOverlayAddonHtml = $tileRenderEvent->getHtml(); + +if ($tileLayoutMode === 'tablet' && $tileOverlayAddonHtml !== '') { + $tabletQuickDonateExtrasHtml = trim((string)$this->render('_tablet_quick_donate_extras', [ + 'donationHistoryRows' => $donationHistoryRows, + 'showDonationSettingsButton' => $showDonationSettingsButton, + 'goalEditorToggleId' => $goalEditorToggleId, + 'existingGoalId' => $existingGoalId, + 'createInlineGoalUrl' => $createInlineGoalUrl, + 'existingGoalTitle' => $existingGoalTitle, + 'existingGoalTargetAmount' => $existingGoalTargetAmount, + 'existingGoalDescription' => $existingGoalDescription, + 'existingGoalImage' => $existingGoalImage, + 'galleryUrls' => $galleryUrls, + 'animal' => $animal, + 'existingGoalActive' => $existingGoalActive, + 'fileFieldId' => $fileFieldId, + 'previewId' => $previewId, + 'quickDonateToggleId' => $quickDonateToggleId, + 'quickDonatePanelId' => $quickDonatePanelId, + ])); + + if ($tabletQuickDonateExtrasHtml !== '') { + $quickPanelPattern = '/(
]*>)(\s*)(\s*<\/div>)/s'; + $replaceCount = 0; + $replacedOverlayHtml = preg_replace_callback( + $quickPanelPattern, + static function (array $matches) use ($tabletQuickDonateExtrasHtml): string { + $layoutHtml = str_replace('__ANIMAL_TILE_QUICK_DONATE_FORM_SLOT__', $matches[2], $tabletQuickDonateExtrasHtml); + return $matches[1] + . $layoutHtml + . $matches[3]; + }, + $tileOverlayAddonHtml, + 1, + $replaceCount + ); + if ($replaceCount > 0 && is_string($replacedOverlayHtml)) { + $tileOverlayAddonHtml = $replacedOverlayHtml; + } + } +} + +$tileSizeEvent = new AnimalTileSizeEvent([ + 'animal' => $animal, + 'contentContainer' => $contentContainer, + 'existingDonationGoal' => $existingDonationGoal, +]); +Event::trigger(AnimalTileSizeEvent::class, AnimalTileSizeEvent::EVENT_RESOLVE_SIZE, $tileSizeEvent); +$extraTileHeightPx = $tileSizeEvent->getAdditionalHeightPx(); + +// Fallback: read donations setting directly in case size hook listeners are not yet active in cache. +if ($extraTileHeightPx <= 0 + && $contentContainer instanceof Space + && $contentContainer->moduleManager->isEnabled('donations') +) { + $donationsModule = Yii::$app->getModule('donations', false); + if ($donationsModule !== null) { + $extraTileHeightPx = max(0, (int)$donationsModule->settings->contentContainer($contentContainer)->get('animal_tile_extra_height_px', 0)); + } +} + +$tileBaseHeightPx = 260; +if ($tileLayoutMode === 'rows') { + $tileBaseHeightPx = 210; +} elseif ($tileLayoutMode === 'tiles2') { + $tileBaseHeightPx = 360; +} elseif ($tileLayoutMode === 'tablet') { + $tileBaseHeightPx = 560; +} +$tileTotalHeightPx = $tileBaseHeightPx + max(0, (int)$extraTileHeightPx); +$tileMediaStyle = 'display:block;position:relative;background:#d8dee8;height:' . (int)$tileTotalHeightPx . 'px;overflow:visible;border-radius:12px;'; +if ($tileLayoutMode === 'tablet') { + $tileMediaStyle = 'display:block;position:relative;background:#d8dee8;min-height:520px;height:min(84vh,860px);overflow:visible;border-radius:12px;'; +} +$tileImageFitMode = 'cover'; +$overlayPointerEvents = $tileLayoutMode === 'tablet' ? 'auto' : 'none'; +$overlayVerticalStyle = $tileLayoutMode === 'tablet' ? 'top:12px;' : ''; + +$inlineFormBackgroundStyle = 'display:none;position:absolute;left:12px;right:12px;top:12px;z-index:5;max-height:calc(100% - 24px);overflow:auto;padding:12px;border:1px solid rgba(213,222,234,0.9);border-radius:10px;background:#fff;'; +if ($existingGoalImage !== '') { + $inlineFormBackgroundStyle = 'display:none;position:absolute;left:12px;right:12px;top:12px;z-index:5;max-height:calc(100% - 24px);overflow:auto;padding:12px;border:1px solid rgba(213,222,234,0.9);border-radius:10px;' + . 'background-image:linear-gradient(rgba(255,255,255,0.35),rgba(255,255,255,0.45)),url(' . Html::encode($existingGoalImage) . ');' + . 'background-size:cover;background-position:center;'; +} ?> -
-
- - - <?= Html::encode($animal->getDisplayName()) ?> - -
+
+ + + -
+ + + + + + + + + + + + + + + + + +
+ + + <?= Html::encode($animal->getDisplayName()) ?> + +
+ +
- + getDisplayName()) ?> + + + + + - - - - -
-
+
@@ -84,10 +422,1168 @@ $medicalUrl = $contentContainer->createUrl('/animal_management/animals/medical-v
-
+
+ +
+ +
+ + + + + +
+ + + + + + + + + + + +
+
+ + +
+
+
+ +
+ + 'multipart/form-data', + 'style' => 'margin:0;', + ]) ?> + request->csrfParam, Yii::$app->request->getCsrfToken()) ?> + + + id) ?> + + +
+ + 'form-control input-sm animal-donation-inline-input', + 'maxlength' => 190, + 'required' => true, + ]) ?> +
+ +
+ + 0 ? (string)$existingGoalTargetAmount : '', [ + 'class' => 'form-control input-sm animal-donation-inline-input', + 'step' => '0.01', + 'min' => '0', + 'required' => true, + ]) ?> +
+ +
+ + 'form-control input-sm animal-donation-inline-input', + 'rows' => 2, + ]) ?> +
+ +
+ +
+ + + + + +
+ + + + +
+ +
+ + + $fileFieldId, + 'class' => 'form-control input-sm js-animal-donation-upload animal-donation-inline-input', + 'data-preview' => '#' . $previewId, + 'data-editor-bg' => '#' . $inlineFormId, + 'accept' => 'image/*', + ]) ?> +
+ +
+ 0 ? Yii::t('DonationsModule.base', 'Update Goal') : Yii::t('DonationsModule.base', 'Save Goal'), ['class' => 'btn btn-primary btn-sm animal-donation-inline-btn']) ?> + +
+ +
+
+
+ +registerCss("#{$toggleInputId}:checked ~ #{$inlineFormId}{display:block !important;}\n" + . ".animal-tile-gallery-grid{align-content:start;grid-auto-flow:row;grid-auto-rows:auto;}\n" + . ".animal-tile-gallery-grid{scrollbar-width:none;-ms-overflow-style:none;}\n" + . ".animal-tile-gallery-grid::-webkit-scrollbar{width:0;height:0;}\n" + . ".animal-tile-gallery-item{display:block;line-height:0;border-radius:8px;overflow:visible;border:1px solid rgba(255,255,255,0.22);background:rgba(15,23,42,0.36);width:100%;margin:0;}\n" + . ".animal-tile-gallery-image{width:100%;height:500px;object-fit:cover;display:block;}\n" + . ".animal-tile-inline-icon{display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;border-radius:999px;background:transparent;border:0;color:rgba(248,250,252,0.9);font-size:20px;line-height:1;cursor:pointer;opacity:0.66;transition:opacity 120ms ease,background 120ms ease,color 120ms ease;}\n" + . ".animal-tile-inline-icon-close{display:none;}\n" + . ".animal-tile-inline-icon:hover{opacity:1;color:#fff;background:rgba(255,255,255,0.14);}\n" + . ".animal-tile-gallery-cols-toggle{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:transparent;border:0;color:rgba(248,250,252,0.86);font-size:16px;font-weight:700;line-height:1;cursor:pointer;opacity:0.62;transition:opacity 120ms ease,background 120ms ease,color 120ms ease;}\n" + . ".animal-tile-gallery-cols-toggle:hover{opacity:1;color:#fff;background:rgba(255,255,255,0.14);}\n" + . ".animal-tile-goal-header-action{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:999px;border:1px solid rgba(255,255,255,0.24);background:rgba(255,255,255,0.08);color:#f8fafc;cursor:pointer;font-size:14px;line-height:1;padding:0;margin:0;transition:background 120ms ease,border-color 120ms ease,opacity 120ms ease;}\n" + . ".animal-tile-goal-header-action:hover{background:rgba(255,255,255,0.2);border-color:rgba(255,255,255,0.42);}\n" + . ".animal-tile-goal-upload-input{position:absolute !important;left:-10000px !important;width:1px !important;height:1px !important;opacity:0 !important;pointer-events:none !important;}\n" + . ".animal-tile-goal-upload-btn{display:block;width:120px;margin:6px auto 0 auto;padding:6px 10px;border-radius:999px;border:1px solid rgba(255,255,255,0.24);background:rgba(255,255,255,0.08);color:#f8fafc;text-align:center;cursor:pointer;font-size:12px;font-weight:700;}\n" + . ".animal-tile-goal-upload-btn:hover{background:rgba(255,255,255,0.18);}\n" + . "#{$quickDonatePanelId}{width:auto !important;max-width:min(96vw,1600px) !important;overflow:visible !important;align-self:center !important;}\n" + . "#{$quickDonatePanelId} .animal-tile-quick-donate-layout{display:grid;grid-template-columns:minmax(260px,1fr) max-content;gap:10px;align-items:start;}\n" + . "#{$quickDonatePanelId} .animal-tile-quick-donate-layout.has-goal-settings{grid-template-columns:minmax(260px,1fr) max-content minmax(500px,1fr);}\n" + . "#{$quickDonatePanelId} .animal-tile-quick-donate-layout > form{margin:0;display:flex;flex-direction:column;align-items:center;text-align:center;min-width:max-content;border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:8px;background:rgba(15,23,42,0.14);box-sizing:border-box;}\n" + . "#{$quickDonatePanelId} .animal-tile-quick-donate-layout .animal-tile-quick-donate-history-col{min-width:260px;border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:8px;background:rgba(15,23,42,0.14);display:flex;flex-direction:column;min-height:0;overflow:hidden;max-height:min(58vh,540px);}\n" + . "#{$quickDonatePanelId} .animal-tile-quick-donate-layout .animal-tile-quick-donate-goal-col{min-width:500px;border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:8px;background:rgba(15,23,42,0.14);display:flex;flex-direction:column;justify-content:flex-start;min-height:0;overflow:hidden;max-height:min(58vh,540px);}\n" + . "#{$quickDonatePanelId} .animal-tile-quick-donate-layout > form input[name=\"amount\"]{width:100px !important;max-width:100px !important;min-width:100px !important;background:rgba(54,209,124,0.26) !important;border:1px solid rgba(54,209,124,0.62) !important;border-radius:999px !important;color:#f8fafc !important;box-shadow:none !important;font-size:24px !important;font-weight:700 !important;line-height:1.15 !important;text-align:center !important;padding:4px 10px !important;appearance:textfield !important;-moz-appearance:textfield !important;overflow:hidden !important;}\n" + . "#{$quickDonatePanelId} .animal-tile-quick-donate-layout > form input[name=\"amount\"]::-webkit-outer-spin-button,#{$quickDonatePanelId} .animal-tile-quick-donate-layout > form input[name=\"amount\"]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0;}\n" + . "#{$quickDonatePanelId} .animal-tile-quick-donate-layout > form input[name=\"amount\"]::placeholder{color:rgba(248,250,252,0.82) !important;}\n" + . ".animal-tile-goal-title-target-row{width:100%;}\n" + . ".animal-tile-goal-selected-image{width:90px;height:90px;display:flex;align-items:center;justify-content:center;padding:3px;border-radius:12px;background:linear-gradient(120deg,rgba(56,189,248,0.7),rgba(52,211,153,0.72),rgba(99,102,241,0.72),rgba(56,189,248,0.7));background-size:220% 220%;animation:animalTileGoalImageBorderFlow 4.6s linear infinite;box-shadow:0 6px 14px rgba(2,8,23,0.28);flex:0 0 auto;}\n" + . ".animal-tile-goal-selected-image-thumb{width:100%;height:100%;object-fit:cover;border-radius:9px;border:1px solid rgba(248,250,252,0.72);display:block;}\n" + . ".animal-tile-goal-selected-image-empty{display:flex;align-items:center;justify-content:center;width:100%;height:100%;font-size:11px;line-height:1.2;text-align:center;color:rgba(248,250,252,0.92);background:rgba(15,23,42,0.42);border-radius:9px;padding:4px;}\n" + . ".animal-tile-goal-image-list{min-height:64px;}\n" + . ".animal-tile-donation-history-item{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid rgba(255,255,255,0.22);border-radius:10px;padding:10px 12px;background:rgba(15,23,42,0.24);margin-bottom:8px;}\n" + . ".animal-tile-donation-history-meta{display:flex;align-items:center;gap:10px;min-width:0;flex:1 1 auto;}\n" + . ".animal-tile-donation-history-avatar{width:42px;height:42px;border-radius:999px;object-fit:cover;border:1px solid rgba(255,255,255,0.36);flex:0 0 auto;}\n" + . ".animal-tile-donation-history-avatar-fallback{display:inline-flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.12);color:#f8fafc;font-size:16px;}\n" + . ".animal-tile-donation-history-identity{display:flex;flex-direction:column;gap:2px;min-width:0;}\n" + . ".animal-tile-donation-history-name{font-size:18px;font-weight:700;line-height:1.2;color:#f8fafc;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;}\n" + . ".animal-tile-donation-history-name:hover{text-decoration:underline;color:#ffffff;}\n" + . ".animal-tile-donation-history-when{font-size:12px;line-height:1.2;color:rgba(226,232,240,0.86);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\n" + . ".animal-tile-donation-history-amount{font-size:18px;font-weight:700;color:#f8fafc;white-space:nowrap;padding-left:8px;}\n" + . "#{$inlinePanelWrapId} .animal-tile-goal-editor-fields .form-control,#{$quickDonatePanelId} .animal-tile-goal-editor-fields .form-control{font-size:18px !important;line-height:1.3 !important;}\n" + . "#{$inlinePanelWrapId} .animal-tile-goal-editor-fields .input-group-addon,#{$quickDonatePanelId} .animal-tile-goal-editor-fields .input-group-addon{font-size:18px !important;font-weight:700;line-height:1.1;}\n" + . "@keyframes animalTileGoalImageBorderFlow{0%{background-position:0% 50%;}50%{background-position:100% 50%;}100%{background-position:0% 50%;}}\n" + . "#{$inlinePanelWrapId} .form-control{background:rgba(255,255,255,0.05) !important;border-color:rgba(255,255,255,0.14) !important;color:#f8fafc !important;}\n" + . "#{$quickDonatePanelId} .form-control{background:rgba(255,255,255,0.05) !important;border-color:rgba(255,255,255,0.14) !important;color:#f8fafc !important;}\n" + . "#{$inlinePanelWrapId} .form-control::placeholder{color:rgba(226,232,240,0.84) !important;}\n" + . "#{$quickDonatePanelId} .form-control::placeholder{color:rgba(226,232,240,0.84) !important;}\n" + . "#{$inlinePanelWrapId} .text-muted{color:rgba(226,232,240,0.86) !important;}\n" + . "#{$quickDonatePanelId} .text-muted{color:rgba(226,232,240,0.86) !important;}\n" + . "#{$inlinePanelWrapId} .animal-donation-gallery-radio{position:absolute;opacity:0;pointer-events:none;}\n" + . "#{$quickDonatePanelId} .animal-donation-gallery-radio{position:absolute;opacity:0;pointer-events:none;}\n" + . "#{$inlinePanelWrapId} .animal-donation-gallery-radio:checked + .animal-donation-inline-gallery-item{border-color:#f8fafc !important;box-shadow:0 0 0 2px rgba(248,250,252,0.36) !important;}\n" + . "#{$quickDonatePanelId} .animal-donation-gallery-radio:checked + .animal-donation-inline-gallery-item{border-color:#f8fafc !important;box-shadow:0 0 0 2px rgba(248,250,252,0.36) !important;}\n" + . "#{$inlinePanelWrapId} a{color:#f8fafc;}\n" + . ".animal-tile-goal-editor-toggle{position:absolute;left:-10000px;opacity:0;pointer-events:none;}\n" + . ".animal-tile-goal-editor-toggle:not(:checked) + .animal-tile-set-goal-btn + .animal-tile-goal-editor-fields{display:none !important;}\n" + . ".animal-tile-goal-editor-toggle:checked + .animal-tile-set-goal-btn{display:none !important;}\n" + . ".animal-tile-goal-editor-toggle:not(:checked) + .animal-tile-goal-editor-fields{display:none !important;}\n" + . ".animal-tile-scroll-region{scrollbar-width:none;-ms-overflow-style:none;}\n" + . ".animal-tile-scroll-region::-webkit-scrollbar{width:0;height:0;}\n" + . "[data-inline-tile-panel=\"transfer\"]{scrollbar-width:none;-ms-overflow-style:none;}\n" + . "[data-inline-tile-panel=\"transfer\"]::-webkit-scrollbar{width:0;height:0;}\n" + . ".animal-tile-item-edit-toggle{position:absolute;left:-10000px;opacity:0;pointer-events:none;}\n" + . ".animal-tile-item-editor{display:none;}\n" + . ".animal-tile-item-edit-toggle:checked + .animal-tile-item-card{display:none !important;}\n" + . ".animal-tile-item-edit-toggle:checked + .animal-tile-item-card + .animal-tile-item-editor{display:block !important;}\n" + . ".animal-tile-feed-card{position:relative;overflow:hidden;border:1px solid rgba(255,255,255,0.12);border-radius:12px;background:rgba(34,52,70,0.18);min-height:220px;box-shadow:0 8px 22px rgba(12,24,36,0.12);}\n" + . ".animal-tile-feed-cover{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}\n" + . ".animal-tile-feed-overlay{position:absolute;inset:0;background:linear-gradient(110deg, rgba(10,18,28,0.08) 10%, rgba(10,18,28,0.24) 52%, rgba(10,18,28,0.42) 100%);}\n" + . ".animal-tile-feed-content{position:relative;z-index:1;min-height:220px;padding:12px;display:flex;flex-direction:column;gap:10px;}\n" + . ".animal-tile-feed-top-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}\n" + . ".animal-tile-item-edit-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:999px;border:1px solid rgba(255,255,255,0.24);background:rgba(255,255,255,0.12);color:#f8fafc;cursor:pointer;font-size:12px;line-height:1;margin:0;}\n" + . ".animal-tile-item-edit-btn:hover{background:rgba(255,255,255,0.24);border-color:rgba(255,255,255,0.42);}\n" + . ".animal-tile-section-title{font-size:20px;font-weight:700;line-height:1.15;color:#f8fafc;text-decoration:none;text-shadow:0 2px 8px rgba(0,0,0,0.35);}\n" + . ".animal-tile-inline-add-action{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:30px;padding:0 12px;border-radius:999px;border:1px solid rgba(30,164,95,0.62);background:linear-gradient(90deg,#36d17c,#1ea45f);color:#f8fafc;cursor:pointer;font-size:12px;font-weight:700;line-height:1;box-shadow:0 2px 8px rgba(10,40,24,0.32);margin:0;text-decoration:none;}\n" + . ".animal-tile-inline-add-action:hover{background:linear-gradient(90deg,#42db88,#22b366);border-color:rgba(54,209,124,0.88);color:#ffffff;}\n" + . ".animal-tile-inline-iframe-close{position:absolute;top:9px;right:8px;z-index:12;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:999px;border:1px solid rgba(255,255,255,0.36);background:rgba(15,23,42,0.72);color:#f8fafc;cursor:pointer;line-height:1;font-size:12px;box-shadow:0 2px 8px rgba(0,0,0,0.28);}\n" + . ".animal-tile-inline-iframe-close:hover{background:rgba(30,41,59,0.86);border-color:rgba(255,255,255,0.5);}\n" + . ".animal-tile-feed-date{font-size:16px;font-weight:700;color:#ffffff;}\n" + . ".animal-tile-feed-details{border-radius:10px;border:1px solid rgba(255,255,255,0.12);background:rgba(10,18,28,0.2);padding:10px;color:#ecf2f8;}\n" + . ".animal-tile-feed-label{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:rgba(231,241,249,0.78);margin-bottom:3px;}\n" + . ".animal-tile-feed-copy{color:#eff5fb;margin-bottom:8px;font-size:12px;line-height:1.35;}\n" + . ".animal-tile-transfer-event{border:1px solid #d5dfe8;border-radius:10px;padding:8px;background:linear-gradient(180deg,#2a3e53 0%,#233447 100%);box-shadow:0 6px 16px rgba(15,23,42,0.18);}\n"); +$css = + ".animal-profile-transfer-badge { display: inline-block; padding: 3px 8px; border-radius: 999px; font-size: 11px; font-weight: 800; letter-spacing: .02em; text-transform: uppercase; }\n" + . ".animal-profile-transfer-badge-requested { background: #ffe600; color: #111; }\n" + . ".animal-profile-transfer-badge-accepted { background: #dcfce7; color: #166534; }\n" + . ".animal-profile-transfer-badge-declined { background: #fee2e2; color: #991b1b; }\n" + . ".animal-profile-transfer-badge-completed { background: #dbeafe; color: #1d4ed8; }\n" + . ".animal-profile-transfer-badge-cancelled { background: #e5e7eb; color: #374151; }\n" + . ".animal-profile-transfer-badge-default { background: rgba(255, 255, 255, 0.2); color: #f8fafc; }\n" + . ".animal-tile-transfer-badge-accepted{background:#dcfce7;color:#166534;}\n" + . ".animal-tile-transfer-badge-declined{background:#fee2e2;color:#991b1b;}\n" + . ".animal-tile-transfer-badge-completed{background:#dbeafe;color:#1d4ed8;}\n" + . ".animal-tile-transfer-badge-cancelled{background:#e5e7eb;color:#374151;}\n" + . ".animal-tile-transfer-badge-default{background:rgba(255,255,255,0.2);color:#f8fafc;}\n" + . ".animal-profile-transfer-event-head { display: flex; justify-content: flex-end; gap: 8px; margin-bottom: 6px; }\n" + . ".animal-profile-transfer-route-row { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: start; gap: 12px; margin-bottom: 10px; }\n" + . ".animal-profile-transfer-rescue-col { display: flex; flex-direction: column; align-items: center; text-align: center; min-width: 0; gap: 5px; }\n" + . ".animal-profile-transfer-rescue-avatar { width: 54px; height: 54px; border-radius: 999px; object-fit: cover; border: 1px solid rgba(255, 255, 255, 0.38); box-shadow: 0 3px 10px rgba(2, 8, 23, 0.28); display: block; background: rgba(255, 255, 255, 0.12); }\n" + . ".animal-profile-transfer-rescue-avatar-fallback { display: inline-flex; align-items: center; justify-content: center; color: #f8fafc; font-size: 20px; }\n" + . ".animal-profile-transfer-rescue-name { color: #f8fafc; font-size: 14px; font-weight: 700; line-height: 1.2; text-decoration: none; max-width: 100%; overflow-wrap: anywhere; }\n" + . ".animal-profile-transfer-rescue-name:hover { text-decoration: underline; color: #ffffff; }\n" + . ".animal-profile-transfer-rescue-date { color: rgba(226, 232, 240, 0.92); font-size: 12px; line-height: 1.2; }\n" + . ".animal-profile-transfer-route-arrow { align-self: center; justify-self: center; font-size: 20px; color: rgba(248, 250, 252, 0.9); line-height: 1; padding-top: 8px; }\n" + . ".animal-profile-transfer-event-lines { display: flex; flex-direction: column; gap: 4px; }\n" + . ".animal-profile-transfer-event-line { font-size: 14px; line-height: 1.35; color: #f8fafc; display: flex; align-items: center; justify-content: space-between; gap: 10px; backdrop-filter: brightness(0.7); padding: 8px 12px; border-radius: 10px; }\n" + . ".animal-profile-transfer-event-actor { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1 1 auto; }\n" + . ".animal-profile-transfer-event-avatar { width: 32px; height: 32px; border-radius: 999px; object-fit: cover; border: 1px solid rgba(255, 255, 255, 0.36); flex: 0 0 auto; display: block; background: rgba(255, 255, 255, 0.1); }\n" + . ".animal-profile-transfer-event-avatar-fallback { display: inline-flex; align-items: center; justify-content: center; color: #f8fafc; font-size: 14px; }\n" + . ".animal-profile-transfer-event-identity { display: flex; flex-direction: column; gap: 1px; min-width: 0; }\n" + . ".animal-profile-transfer-event-actor-name { color: #f8fafc; font-size: 13px; font-weight: 700; text-decoration: none; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n" + . ".animal-profile-transfer-event-actor-name:hover { text-decoration: underline; color: #ffffff; }\n" + . ".animal-profile-transfer-event-line-time { opacity: 0.9; font-weight: 500; font-size: 11px; line-height: 1.2; }\n" + . ".animal-profile-transfer-event-action { margin-left: auto; text-align: right; font-size: 13px; font-weight: 700; color: #e2e8f0; white-space: nowrap; flex: 0 0 auto; }\n" + . ".animal-profile-transfer-time { font-size: 16px; font-weight: 700; color: #f8fafc; }\n" + . ".animal-profile-transfer-badge { display: inline-block; padding: 3px 8px; border-radius: 999px; font-size: 11px; font-weight: 800; letter-spacing: .02em; text-transform: uppercase; }\n" + . ".animal-profile-transfer-badge-requested { background: #ffe600; color: #111; }\n" + . ".animal-profile-transfer-badge-accepted { background: #dcfce7; color: #166534; }\n" + . ".animal-profile-transfer-badge-declined { background: #fee2e2; color: #991b1b; }\n" + . ".animal-profile-transfer-badge-completed { background: #dbeafe; color: #1d4ed8; }\n" + . ".animal-profile-transfer-badge-cancelled { background: #e5e7eb; color: #374151; }\n" + . ".animal-profile-transfer-badge-default { background: rgba(255, 255, 255, 0.2); color: #f8fafc; }\n" + . ".animal-tile-transfer-badge-accepted{background:#dcfce7;color:#166534;}\n" + . ".animal-tile-transfer-badge-declined{background:#fee2e2;color:#991b1b;}\n" + . ".animal-tile-transfer-badge-completed{background:#dbeafe;color:#1d4ed8;}\n" + . ".animal-tile-transfer-badge-cancelled{background:#e5e7eb;color:#374151;}\n" + . ".animal-tile-transfer-badge-default{background:rgba(255,255,255,0.2);color:#f8fafc;}\n" + . ".animal-tile-transfer-badge-requested{background:#ffe600;color:#111;}\n"; +$this->registerCss($css); +$this->registerCss("#{$inlineFormId} .animal-donation-inline-input{background:rgba(255,255,255,0.34);border-color:rgba(165,178,195,0.72);color:#16202a;}\n" + . "#{$inlineFormId} .animal-donation-inline-input::placeholder{color:rgba(31,41,55,0.72);}\n" + . "#{$inlineFormId} .animal-donation-inline-gallery{background:rgba(255,255,255,0.16);padding:6px;border-radius:6px;pointer-events:auto;}\n" + . "#{$inlineFormId} .animal-donation-gallery-radio{position:absolute;opacity:0;pointer-events:none;}\n" + . "#{$inlineFormId} .animal-donation-gallery-radio:checked + .animal-donation-inline-gallery-item{border-color:#1f78c1 !important;box-shadow:0 0 0 2px rgba(31,120,193,0.32) !important;}\n" + . "#{$inlineFormId} .animal-donation-inline-btn{opacity:0.82;}"); + +$uploadPendingText = Json::htmlEncode(Yii::t('DonationsModule.base', 'Image will be uploaded with this goal.')); +$noImageText = Json::htmlEncode(Yii::t('DonationsModule.base', 'No image selected.')); +$inlineJsNamespace = 'animalDonationInlineToggle' . (int)$animal->id; + +$this->registerJs(<<' + $('
').text($noImageText).html() + ''); + return; + } + + preview.html(''); + } + + var inlineToggleSelector = '#$toggleInputId'; + var inlineFormSelector = '#$inlineFormId'; + + syncInlineEditor(inlineToggleSelector, inlineFormSelector); + + $(document).off('change.$inlineJsNamespace').on('change.$inlineJsNamespace', inlineToggleSelector, function() { + syncInlineEditor(inlineToggleSelector, inlineFormSelector); + }); + + $(document).off('change.animalDonationGallery').on('change.animalDonationGallery', '.animal-donation-gallery-radio', function() { + var radio = $(this); + if (!radio.is(':checked')) { + return; + } + + var previewSelector = radio.data('preview'); + var editorSelector = radio.data('editor-bg'); + var url = radio.val() || ''; + + setPreview(previewSelector, url); + setEditorBackground(editorSelector, url); + }); + + $(document).off('change.animalDonationUpload').on('change.animalDonationUpload', '.js-animal-donation-upload', function() { + var previewSelector = $(this).data('preview'); + var editorSelector = $(this).data('editor-bg'); + if (!previewSelector) { + return; + } + + var form = $(this).closest('form'); + form.find('.animal-donation-gallery-radio').prop('checked', false); + + var previousObjectUrl = $(this).data('object-url') || ''; + if (previousObjectUrl && window.URL && window.URL.revokeObjectURL) { + window.URL.revokeObjectURL(previousObjectUrl); + } + + if (this.files && this.files.length > 0) { + var objectUrl = ''; + if (window.URL && window.URL.createObjectURL) { + objectUrl = window.URL.createObjectURL(this.files[0]); + $(this).data('object-url', objectUrl); + } + + if (objectUrl) { + setPreview(previewSelector, objectUrl); + setEditorBackground(editorSelector, objectUrl); + } else { + $(previewSelector).text($uploadPendingText); + } + } else { + $(this).data('object-url', ''); + var selectedRadio = form.find('.animal-donation-gallery-radio:checked').first(); + var fallbackUrl = selectedRadio.length ? (selectedRadio.val() || '') : ''; + setPreview(previewSelector, fallbackUrl); + setEditorBackground(editorSelector, fallbackUrl); + } + }); +})(); +JS, \yii\web\View::POS_READY, 'animal-donation-inline-' . (int)$animal->id); + +if ($tileLayoutMode === 'tablet') { + $tabletBehaviorJs = <<<'JS' +(function() { + var animalId = ANIMAL_ID_TOKEN; + var inlinePanelNoneSelector = '#INLINE_PANEL_NONE_ID_TOKEN'; + var formModeNoneSelector = '#FORM_MODE_NONE_ID_TOKEN'; + var inlineMenuSelector = '#INLINE_MENU_ID_TOKEN'; + var inlinePanelWrapSelector = '#INLINE_PANEL_WRAP_ID_TOKEN'; + + function triggerChange(element) { + if (!element) { + return; + } + + var event; + if (typeof Event === 'function') { + event = new Event('change', { bubbles: true }); + } else { + event = document.createEvent('Event'); + event.initEvent('change', true, true); + } + element.dispatchEvent(event); + } + + function setChecked(selector, value) { + var element = document.querySelector(selector); + if (!element) { + return false; + } + + element.checked = !!value; + triggerChange(element); + return true; + } + + function setCheckedById(id, value) { + var element = document.getElementById(id); + if (!element) { + return false; + } + + element.checked = !!value; + triggerChange(element); + return true; + } + + function closeInlineEditorByFrameControl(collapseId) { + var frameNodeList = document.querySelectorAll(inlinePanelWrapSelector + ' iframe[data-animal-inline-collapse-id]'); + if (!frameNodeList || frameNodeList.length === 0) { + return false; + } + + var matchedFrame = null; + Array.prototype.some.call(frameNodeList, function(frameElement) { + var frameCollapseId = String(frameElement.getAttribute('data-animal-inline-collapse-id') || ''); + if (frameCollapseId !== collapseId) { + return false; + } + matchedFrame = frameElement; + return true; + }); + + if (!matchedFrame || !matchedFrame.parentElement) { + return false; + } + + var closeControl = matchedFrame.parentElement.querySelector('.animal-tile-inline-iframe-close[for]'); + if (!closeControl) { + return false; + } + + var targetId = String(closeControl.getAttribute('for') || ''); + if (!targetId) { + return false; + } + + var targetElement = document.getElementById(targetId); + if (!targetElement) { + return false; + } + + if ((targetElement.type || '').toLowerCase() === 'radio') { + targetElement.checked = true; + } else { + targetElement.checked = false; + } + triggerChange(targetElement); + return true; + } + + document.addEventListener('click', function(e) { + var target = e.target; + if (!target || !target.closest) { + return; + } + + var trigger = target.closest(inlineMenuSelector + ' .animal-tile-inline-icon[data-panel-id]'); + if (!trigger) { + return; + } + + var panelId = String(trigger.getAttribute('data-panel-id') || ''); + if (!panelId) { + return; + } + + var panelInput = document.getElementById(panelId); + if (!panelInput) { + return; + } + + if (panelInput.checked) { + setChecked(inlinePanelNoneSelector, true); + e.preventDefault(); + if (typeof e.stopImmediatePropagation === 'function') { + e.stopImmediatePropagation(); + } + e.stopPropagation(); + return; + } + + // Rebind iframe watchers after panel changes and lazy iframe loads. + window.setTimeout(bindInlineResultFrameWatchers, 0); + window.setTimeout(bindInlineResultFrameWatchers, 250); + }, false); + + function closeInlineEditorByPayload(collapseId) { + collapseId = String(collapseId || ''); + if (!collapseId) { + return; + } + + if (collapseId === 'medical-add-inline' || collapseId === 'progress-add-inline') { + setChecked(formModeNoneSelector, true); + return; + } + + if (collapseId === 'gallery-add-inline') { + if (!setCheckedById('GALLERY_ADD_TOGGLE_ID_TOKEN', false)) { + closeInlineEditorByFrameControl(collapseId); + } + return; + } + + if (collapseId === 'transfer-add-inline') { + if (!setCheckedById('TRANSFER_ADD_TOGGLE_ID_TOKEN', false)) { + closeInlineEditorByFrameControl(collapseId); + } + return; + } + + var medicalPrefix = 'medical-edit-inline-'; + if (collapseId.indexOf(medicalPrefix) === 0) { + var medicalVisitId = collapseId.substring(medicalPrefix.length); + if (!setCheckedById('animal-tile-medical-edit-toggle-' + animalId + '-' + medicalVisitId, false)) { + closeInlineEditorByFrameControl(collapseId); + } + return; + } + + var progressPrefix = 'progress-edit-inline-'; + if (collapseId.indexOf(progressPrefix) === 0) { + var progressUpdateId = collapseId.substring(progressPrefix.length); + if (!setCheckedById('animal-tile-progress-edit-toggle-' + animalId + '-' + progressUpdateId, false)) { + closeInlineEditorByFrameControl(collapseId); + } + return; + } + + closeInlineEditorByFrameControl(collapseId); + } + + function handleInlineResultFrame(frameElement) { + if (!frameElement) { + return; + } + + var collapseId = String(frameElement.getAttribute('data-animal-inline-collapse-id') || ''); + if (!collapseId) { + return; + } + + var frameDocument = null; + try { + frameDocument = frameElement.contentDocument || (frameElement.contentWindow ? frameElement.contentWindow.document : null); + } catch (err) { + return; + } + + if (!frameDocument || !frameDocument.body) { + return; + } + + var hasSavedMarker = !!frameDocument.querySelector('[data-animal-inline-result="saved"]'); + if (!hasSavedMarker) { + return; + } + + closeInlineEditorByPayload(collapseId); + } + + function bindInlineResultFrameWatchers() { + var frameNodeList = document.querySelectorAll(inlinePanelWrapSelector + ' iframe[data-animal-inline-collapse-id]'); + if (!frameNodeList || frameNodeList.length === 0) { + return; + } + + Array.prototype.forEach.call(frameNodeList, function(frameElement) { + if (!frameElement || frameElement.getAttribute('data-animal-inline-watch-bound') === '1') { + return; + } + + frameElement.setAttribute('data-animal-inline-watch-bound', '1'); + frameElement.addEventListener('load', function() { + handleInlineResultFrame(frameElement); + }); + + handleInlineResultFrame(frameElement); + }); + } + + bindInlineResultFrameWatchers(); + + window.addEventListener('message', function(event) { + var data = event && event.data ? event.data : null; + if (typeof data === 'string') { + try { + data = JSON.parse(data); + } catch (err) { + return; + } + } + + if (!data || typeof data !== 'object' || data.source !== 'animal-inline-editor') { + return; + } + + var collapseId = String(data.collapseId || ''); + if (data.type === 'cancel' || data.type === 'saved') { + closeInlineEditorByPayload(collapseId); + } + }); +})(); +JS; + + $tabletBehaviorJs = str_replace('ANIMAL_ID_TOKEN', (string)(int)$animal->id, $tabletBehaviorJs); + $tabletBehaviorJs = str_replace('INLINE_PANEL_NONE_ID_TOKEN', Html::encode($inlinePanelNoneId), $tabletBehaviorJs); + $tabletBehaviorJs = str_replace('FORM_MODE_NONE_ID_TOKEN', Html::encode($formModeNoneId), $tabletBehaviorJs); + $tabletBehaviorJs = str_replace('INLINE_MENU_ID_TOKEN', Html::encode($inlineMenuId), $tabletBehaviorJs); + $tabletBehaviorJs = str_replace('INLINE_PANEL_WRAP_ID_TOKEN', Html::encode($inlinePanelWrapId), $tabletBehaviorJs); + $tabletBehaviorJs = str_replace('GALLERY_ADD_TOGGLE_ID_TOKEN', Html::encode($galleryAddToggleId), $tabletBehaviorJs); + $tabletBehaviorJs = str_replace('TRANSFER_ADD_TOGGLE_ID_TOKEN', Html::encode($transferAddToggleId), $tabletBehaviorJs); + $this->registerJs($tabletBehaviorJs, \yii\web\View::POS_READY, 'animal-tablet-inline-behavior-' . (int)$animal->id); + + $inlineTileCss = '#'. Html::encode($inlinePanelNoneId) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . '{display:none !important;}'; + $inlineTileCss .= '#'. Html::encode($galleryCols2Id) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' .animal-tile-gallery-grid{grid-template-columns:repeat(2,minmax(0,1fr)) !important;column-gap:8px !important;row-gap:12px !important;}'; + $inlineTileCss .= '#'. Html::encode($galleryCols3Id) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' .animal-tile-gallery-grid{grid-template-columns:repeat(3,minmax(0,1fr)) !important;column-gap:8px !important;row-gap:10px !important;}'; + $inlineTileCss .= '#'. Html::encode($galleryCols2Id) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' label[for="' . Html::encode($galleryCols2Id) . '"]{opacity:1;background:rgba(255,255,255,0.2);color:#fff;}'; + $inlineTileCss .= '#'. Html::encode($galleryCols3Id) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' label[for="' . Html::encode($galleryCols3Id) . '"]{opacity:1;background:rgba(255,255,255,0.2);color:#fff;}'; + $inlineTileCss .= '#'. Html::encode($galleryCols2Id) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' label[for="' . Html::encode($galleryCols3Id) . '"],#'. Html::encode($galleryCols3Id) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' label[for="' . Html::encode($galleryCols2Id) . '"]{opacity:0.55;background:transparent;color:rgba(248,250,252,0.86);}'; + $inlineTileCss .= '#'. Html::encode($inlinePanelWrapId) . ' iframe[data-animal-inline-files]{height:250px;}'; + $inlineTileCss .= '#' . Html::encode($overlayStackId) . '{padding-bottom:58px;box-sizing:border-box;}'; + $inlineTileCss .= '#' . Html::encode($inlineMenuId) . '{position:absolute;left:0;right:0;bottom:0;z-index:6;margin-top:0 !important;}'; + $inlineTileCss .= '#' . Html::encode($formModeMedicalId) . ':checked ~ div #' . Html::encode($medicalFormWrapId) . '{display:block !important;}'; + $inlineTileCss .= '#' . Html::encode($formModeProgressId) . ':checked ~ div #' . Html::encode($progressFormWrapId) . '{display:block !important;}'; + $inlineTileCss .= '#' . Html::encode($formModeMedicalId) . ':checked ~ div #' . Html::encode($goalCardId) . ',#' . Html::encode($formModeProgressId) . ':checked ~ div #' . Html::encode($goalCardId) . '{display:none !important;}'; + $inlineTileCss .= '#' . Html::encode($formModeMedicalId) . ':checked ~ div #' . Html::encode($donorPanelId) . ',#' . Html::encode($formModeProgressId) . ':checked ~ div #' . Html::encode($donorPanelId) . '{display:none !important;opacity:0 !important;pointer-events:none !important;}'; + $inlineTileCss .= '#' . Html::encode($formModeMedicalId) . ':checked ~ div #' . Html::encode($quickDonatePanelId) . ',#' . Html::encode($formModeProgressId) . ':checked ~ div #' . Html::encode($quickDonatePanelId) . '{display:none !important;opacity:0 !important;pointer-events:none !important;}'; + $inlineTileCss .= '#' . Html::encode($formModeMedicalId) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ',#' . Html::encode($formModeProgressId) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . '{max-height:calc(100% - 56px) !important;overflow:auto !important;}'; + $inlineTileCss .= '#' . Html::encode($galleryAddToggleId) . ':checked ~ div #' . Html::encode($galleryFormWrapId) . '{display:flex !important;flex-direction:column !important;flex:1 1 auto !important;min-height:0 !important;height:100% !important;}'; + $inlineTileCss .= '#' . Html::encode($galleryAddToggleId) . ':checked ~ div #' . Html::encode($galleryFormWrapId) . ' iframe[data-animal-inline-collapse-id="gallery-add-inline"]{flex:1 1 auto !important;min-height:0 !important;height:100% !important;}'; + $inlineTileCss .= '#' . Html::encode($galleryAddToggleId) . ':checked ~ div #' . Html::encode($galleryListWrapId) . '{display:none !important;}'; + $inlineTileCss .= '#' . Html::encode($inlinePanelWrapId) . ' #' . Html::encode($galleryListWrapId) . '{display:block;flex:1 1 auto;min-height:0;overflow:hidden;}'; + $inlineTileCss .= '#' . Html::encode($inlinePanelWrapId) . ' #' . Html::encode($galleryListWrapId) . ' .animal-tile-gallery-grid{height:100%;min-height:0;overflow-y:auto;overflow-x:hidden;}'; + $inlineTileCss .= '#' . Html::encode($transferAddToggleId) . ':checked ~ div #' . Html::encode($transferFormWrapId) . '{display:block !important;}'; + $inlineTileCss .= '#' . Html::encode($transferAddToggleId) . ':checked ~ div #' . Html::encode($transferHistoryWrapId) . '{display:none !important;}'; + + foreach ($inlinePanelIds as $panelKey => $panelId) { + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($overlayStackId) . '{top:12px !important;bottom:12px !important;display:flex !important;flex-direction:column !important;justify-content:flex-start !important;}'; + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . '{display:block !important;width:100%;max-width:100%;flex:1 1 auto;min-height:0;overflow:hidden;}'; + if ($panelKey === 'files') { + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . '{overflow:auto;}'; + } + if ($panelKey === 'gallery') { + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' [data-inline-tile-panel="gallery"]{display:grid !important;grid-template-rows:auto minmax(0,1fr) !important;width:100% !important;height:100% !important;min-height:0 !important;overflow:hidden !important;}'; + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' #' . Html::encode($galleryListWrapId) . '{height:100% !important;min-height:0 !important;overflow:hidden !important;}'; + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' #' . Html::encode($galleryListWrapId) . ' .animal-tile-gallery-grid{height:100% !important;min-height:0 !important;overflow-y:auto !important;overflow-x:hidden !important;}'; + } else { + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($inlinePanelWrapId) . ' [data-inline-tile-panel="' . Html::encode($panelKey) . '"]{display:block !important;width:100%;min-width:0;height:100%;overflow:auto;}'; + } + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($inlineMenuId) . ' label[for="' . Html::encode($panelId) . '"]{opacity:1;background:rgba(255,255,255,0.2);color:#fff;}'; + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($inlineMenuId) . ' .animal-tile-inline-icon-open[data-panel-id="' . Html::encode($panelId) . '"]{display:none !important;}'; + $inlineTileCss .= '#' . Html::encode($panelId) . ':checked ~ div #' . Html::encode($inlineMenuId) . ' .animal-tile-inline-icon-close[data-panel-id-close="' . Html::encode($panelId) . '"]{display:inline-flex !important;opacity:1;background:rgba(255,255,255,0.2);color:#fff;}'; + } + + $this->registerCss($inlineTileCss); +} +?> diff --git a/views/animals/_transfer_tile.php b/views/animals/_transfer_tile.php index 0bb94b8..dc11e19 100644 --- a/views/animals/_transfer_tile.php +++ b/views/animals/_transfer_tile.php @@ -33,6 +33,30 @@ switch ($transfer->status) { $statusTextColor = '#d1d5db'; break; } + +static $transferActionButtonStylePrinted = false; +if (!$transferActionButtonStylePrinted): + $transferActionButtonStylePrinted = true; + ?> + + ?>
@@ -69,18 +93,16 @@ switch ($transfer->status) { Yii::t('AnimalManagementModule.base', 'Accept'), $space->createUrl('/animal_management/animals/transfer-respond', ['id' => $transfer->id, 'decision' => 'accept']), [ - 'class' => 'btn btn-xs btn-default', + 'class' => 'btn btn-xs btn-default animal-transfer-action-btn', 'data-method' => 'post', - 'style' => 'border-radius:999px;border:0;background:rgba(255,255,255,0.92);font-weight:600;' ] ) ?> createUrl('/animal_management/animals/transfer-respond', ['id' => $transfer->id, 'decision' => 'decline']), [ - 'class' => 'btn btn-xs btn-default', + 'class' => 'btn btn-xs btn-default animal-transfer-action-btn', 'data-method' => 'post', - 'style' => 'border-radius:999px;border:0;background:rgba(255,255,255,0.92);font-weight:600;' ] ) ?> status === AnimalTransfer::STATUS_ACCEPTED): ?> @@ -88,9 +110,8 @@ switch ($transfer->status) { Yii::t('AnimalManagementModule.base', 'Complete Transfer'), $space->createUrl('/animal_management/animals/transfer-complete', ['id' => $transfer->id]), [ - 'class' => 'btn btn-xs btn-default', + 'class' => 'btn btn-xs btn-default animal-transfer-action-btn', 'data-method' => 'post', - 'style' => 'border-radius:999px;border:0;background:rgba(255,255,255,0.92);font-weight:600;' ] ) ?> status, [AnimalTransfer::STATUS_REQUESTED, AnimalTransfer::STATUS_ACCEPTED], true)): ?> @@ -98,9 +119,8 @@ switch ($transfer->status) { Yii::t('AnimalManagementModule.base', 'Cancel Request'), $space->createUrl('/animal_management/animals/transfer-cancel', ['id' => $transfer->id]), [ - 'class' => 'btn btn-xs btn-default', + 'class' => 'btn btn-xs btn-default animal-transfer-action-btn', 'data-method' => 'post', - 'style' => 'border-radius:999px;border:0;background:rgba(255,255,255,0.92);font-weight:600;' ] ) ?> diff --git a/views/animals/add-gallery-images-inline.php b/views/animals/add-gallery-images-inline.php new file mode 100644 index 0000000..b2840e4 --- /dev/null +++ b/views/animals/add-gallery-images-inline.php @@ -0,0 +1,305 @@ +request->get('showTopCancel', '0') === '1'; +$returnTo = (string)($returnTo ?? 'view'); +$maxUploadCount = max(1, (int)($maxUploadCount ?? 10)); +$errorMessage = trim((string)($errorMessage ?? '')); +$infoMessage = trim((string)($infoMessage ?? '')); +$formId = 'animal-gallery-inline-upload-form'; +$submitUrl = $space->createUrl('/animal_management/animals/add-gallery-images-inline', [ + 'id' => (int)$animal->id, + 'inline' => $isInline ? 1 : 0, + 'returnTo' => $returnTo, +]); + +$this->registerCss(<< .panel-body { + position: relative; + z-index: 1; + background: rgba(10, 18, 28, 0.2); +} + +.inline-add-shell, +.inline-add-shell .panel-body, +.inline-add-shell .control-label, +.inline-add-shell .help-block, +.inline-add-shell h4 { + color: #eef5fb; +} + +.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 .animal-inline-top-save-action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.36); + background: rgba(15, 23, 42, 0.72); + color: #f8fafc; + cursor: pointer; + line-height: 1; + font-size: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28); +} + +.inline-add-shell .animal-inline-top-save-action:hover, +.inline-add-shell .animal-inline-top-save-action:focus { + background: rgba(30, 41, 59, 0.86); + border-color: rgba(255, 255, 255, 0.5); + color: #f8fafc; + outline: none; +} + +.inline-add-shell .animal-gallery-inline-existing-grid { + margin: 0; +} + +.inline-add-shell .animal-gallery-inline-existing-item { + display: block; + width: 100%; + aspect-ratio: 1 / 1; + min-height: 70px; + object-fit: cover; + border-radius: 6px; +} + +.inline-add-shell .animal-gallery-inline-existing-wrap { + min-height: 220px; + max-height: 320px; + overflow-y: auto; + overflow-x: hidden; + margin: 6px 0 10px 0; + padding-right: 4px; +} + +.inline-add-shell .animal-gallery-inline-file-input { + position: absolute; + left: -10000px; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.inline-add-shell .animal-gallery-inline-file-trigger { + margin: 0; +} + +.inline-add-shell .animal-gallery-inline-file-name { + display: inline-block; + margin-left: 8px; + color: rgba(233, 242, 250, 0.9); + font-size: 12px; + vertical-align: middle; + max-width: calc(100% - 150px); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +CSS +); + +if ($isInline) { + $this->registerCss(<< .panel:first-child { + margin-top: 0 !important; +} + +body, +.inline-add-shell, +.inline-add-shell > .panel-body, +#{$formId} { + height: 100% !important; +} + +#{$formId} { + display: flex; + flex-direction: column; +} + +#{$formId} .animal-gallery-inline-existing-wrap { + flex: 1 1 auto; + min-height: 0; + max-height: none; +} +CSS + ); +} +?> + +
+
+
> + request->csrfParam, Yii::$app->request->getCsrfToken()) ?> + + + +
+ ', [ + 'class' => 'animal-inline-top-save-action', + 'title' => Yii::t('AnimalManagementModule.base', 'Upload to Gallery'), + 'form' => $formId, + ]) ?> + + ', [ + 'id' => 'gallery-inline-add-cancel-icon', + 'class' => 'animal-inline-top-save-action', + 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), + 'type' => 'button', + ]) ?> + +
+ + +

+ + +
+ + +
+ + +
+ +
+ + + +
+
$maxUploadCount]) ?>
+
+ + + + + + + + + +
+
+
+ + 'animal-inline-editor', + 'type' => 'cancel', + 'collapseId' => 'gallery-add-inline', + ]); + + $this->registerJs(<< diff --git a/views/animals/add-medical-visit.php b/views/animals/add-medical-visit.php index a47ba09..5ed8e7a 100644 --- a/views/animals/add-medical-visit.php +++ b/views/animals/add-medical-visit.php @@ -17,6 +17,7 @@ use yii\helpers\Json; /* @var bool $isInline */ $isInline = isset($isInline) ? (bool)$isInline : false; +$showTopCancel = (string)Yii::$app->request->get('showTopCancel', '0') === '1'; $hiddenMedicalKeys = [ 'second_physician_name', @@ -135,6 +136,15 @@ $medicalMediaPath = trim((string)($model->customFields['medical_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'; +$visitAtInputValue = trim((string)$model->visit_at); +if ($visitAtInputValue === '') { + $visitAtInputValue = date('Y-m-d\TH:i'); +} else { + $visitAtTimestamp = strtotime($visitAtInputValue); + if ($visitAtTimestamp !== false) { + $visitAtInputValue = date('Y-m-d\TH:i', $visitAtTimestamp); + } +} $this->registerCss(<< .panel-heading a { + color: inherit; + display: block; + text-decoration: none; +} + +.inline-add-shell .panel.panel-default > .panel-heading a:hover, +.inline-add-shell .panel.panel-default > .panel-heading a:focus { + color: #ffffff; + text-decoration: none; +} + .inline-add-shell, .inline-add-shell .panel-body, .inline-add-shell .control-label, @@ -204,6 +226,31 @@ $medicalFormId = 'add-medical-visit-inline-form'; .inline-add-shell select.form-control option { color: #0f1b2a; } + +.inline-add-shell .animal-inline-top-save-action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.36); + background: rgba(15, 23, 42, 0.72); + color: #f8fafc; + cursor: pointer; + line-height: 1; + font-size: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28); +} + +.inline-add-shell .animal-inline-top-save-action:hover, +.inline-add-shell .animal-inline-top-save-action:focus { + background: rgba(30, 41, 59, 0.86); + border-color: rgba(255, 255, 255, 0.5); + color: #f8fafc; + outline: none; +} CSS ); @@ -213,6 +260,13 @@ html, body { margin: 0 !important; padding: 0 !important; background: transparent !important; + scrollbar-width: none; + -ms-overflow-style: none; +} + +html::-webkit-scrollbar, body::-webkit-scrollbar { + width: 0; + height: 0; } body > .panel:first-child { @@ -243,18 +297,20 @@ CSS 'medical-media-gallery-path']) ?> -
+
', [ - 'class' => 'btn btn-default btn-sm', + 'class' => 'animal-inline-top-save-action', 'title' => Yii::t('AnimalManagementModule.base', 'Save Medical Visit'), 'form' => $medicalFormId, ]) ?> - ', [ - 'type' => 'button', - 'class' => 'btn btn-default btn-sm', - 'id' => 'medical-inline-add-cancel-icon', - 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), - ]) ?> + + ', [ + 'id' => 'medical-inline-add-cancel-icon', + 'class' => 'animal-inline-top-save-action', + 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), + 'type' => 'button', + ]) ?> +
@@ -289,48 +345,77 @@ CSS
-
-
+
+ +
+
-
-
+
+ +
+
-
-
+
+ +
+
-
-
+
+ +
+
-
-
+
+ +
+
@@ -355,23 +441,23 @@ CSS
-
-
+
+ +
+
submit() ?> - - 'button', - 'class' => 'btn btn-default', - 'id' => 'medical-inline-add-cancel', - ]) ?> - + link(($returnTo ?? 'medical-visits') === 'medical-visits' ? $space->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id]) @@ -507,11 +593,32 @@ if ($isInline) { ]); $this->registerJs(<<request->get('showTopCancel', '0') === '1'; $renderCustomField = static function (string $fieldKey, AnimalProgressUpdateForm $formModel, array $definitions): string { if (!isset($definitions[$fieldKey])) { @@ -132,6 +133,18 @@ $this->registerCss(<< .panel-heading a { + color: inherit; + display: block; + text-decoration: none; +} + +.inline-add-shell .panel.panel-default > .panel-heading a:hover, +.inline-add-shell .panel.panel-default > .panel-heading a:focus { + color: #ffffff; + text-decoration: none; +} + .inline-add-shell, .inline-add-shell .panel-body, .inline-add-shell .control-label, @@ -164,6 +177,31 @@ $this->registerCss(<< .panel:first-child { @@ -195,26 +240,33 @@ CSS -
+
', [ - 'class' => 'btn btn-default btn-sm', + 'class' => 'animal-inline-top-save-action', 'title' => Yii::t('AnimalManagementModule.base', 'Save Progress Update'), 'form' => $progressFormId, ]) ?> - ', [ - 'type' => 'button', - 'class' => 'btn btn-default btn-sm', - 'id' => 'progress-inline-add-cancel-icon', - 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), - ]) ?> + + ', [ + 'id' => 'progress-inline-add-cancel-icon', + 'class' => 'animal-inline-top-save-action', + 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), + 'type' => 'button', + ]) ?> +
errorSummary($model, ['showAllErrors' => true]) ?>
-
-
+
+ + + +
+
+
@@ -239,12 +291,18 @@ CSS
+
-
-
+
+ + + +
+
+
field($model, 'weight') ?>
field($model, 'vitals')->textInput(['maxlength' => 255]) ?>
@@ -253,24 +311,37 @@ CSS
field($model, 'meal_plan_changes')->textarea(['rows' => 2]) ?>
field($model, 'housing_changes')->textarea(['rows' => 2]) ?>
+
-
-
+
+ + + +
+
+
+
-
-
+
+ + + +
+
+
$definition): ?> +
@@ -316,13 +387,7 @@ CSS
submit() ?> - - 'button', - 'class' => 'btn btn-default', - 'id' => 'progress-inline-add-cancel', - ]) ?> - + link(($returnTo ?? 'progress-updates') === 'progress-updates' ? $space->createUrl('/animal_management/animals/progress-updates', ['id' => $animal->id]) @@ -434,11 +499,32 @@ if ($isInline) { ]); $this->registerJs(<<request->post('IntakeGoal', []); +if (!is_array($intakeGoalInput)) { + $intakeGoalInput = []; +} +$intakeGoalEnabled = !empty($isEdit) ? false : ((int)($intakeGoalInput['enabled'] ?? 0) === 1); +$intakeGoalTitle = trim((string)($intakeGoalInput['title'] ?? '')); +$intakeGoalTargetAmount = trim((string)($intakeGoalInput['target_amount'] ?? '')); +$intakeGoalDescription = trim((string)($intakeGoalInput['description'] ?? '')); +$intakeGoalIsActive = !empty($isEdit) ? true : ((int)($intakeGoalInput['is_active'] ?? 1) === 1); ?>
@@ -215,6 +227,41 @@ use yii\helpers\Html;
+ +
+
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +
@@ -274,7 +321,7 @@ use yii\helpers\Html; submit() ?> + : Yii::t('AnimalManagementModule.base', 'Complete Intake'))->submit() ?> link( !empty($isEdit) && $animal instanceof Animal ? $space->createUrl('/animal_management/animals/view', ['id' => $animal->id]) @@ -306,7 +353,7 @@ use yii\helpers\Html; !empty($isEdit) ? Yii::t('AnimalManagementModule.base', 'Save Changes') - : Yii::t('AnimalManagementModule.base', 'Create Animal'), + : Yii::t('AnimalManagementModule.base', 'Complete Intake'), ]) ?>
@@ -350,7 +397,7 @@ use yii\helpers\Html; !empty($isEdit) ? Yii::t('AnimalManagementModule.base', 'Save Changes') - : Yii::t('AnimalManagementModule.base', 'Create Animal'), + : Yii::t('AnimalManagementModule.base', 'Complete Intake'), ]) ?>
@@ -521,6 +568,14 @@ $this->registerJs(<<request->get('showTopCancel', '0') === '1'; $hiddenMedicalKeys = [ 'second_physician_name', @@ -166,6 +167,18 @@ $this->registerCss(<< .panel-heading a { + color: inherit; + display: block; + text-decoration: none; +} + +.inline-editor-shell .panel.panel-default > .panel-heading a:hover, +.inline-editor-shell .panel.panel-default > .panel-heading a:focus { + color: #ffffff; + text-decoration: none; +} + .inline-editor-shell, .inline-editor-shell .panel-body, .inline-editor-shell .control-label, @@ -198,6 +211,31 @@ $this->registerCss(<< .panel:first-child { @@ -229,18 +274,20 @@ CSS
Edit Medical Visit') ?> - + ', [ - 'class' => 'btn btn-default btn-sm', + 'class' => 'animal-inline-top-save-action', 'title' => Yii::t('AnimalManagementModule.base', 'Save Medical Visit'), 'form' => $medicalFormId, ]) ?> - ', [ - 'type' => 'button', - 'class' => 'btn btn-default btn-sm', - 'id' => 'medical-inline-cancel-icon', - 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), - ]) ?> + + ', [ + 'id' => 'medical-inline-cancel-icon', + 'class' => 'animal-inline-top-save-action', + 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), + 'type' => 'button', + ]) ?> +
@@ -259,48 +306,77 @@ CSS errorSummary($model, ['showAllErrors' => true]) ?>
-
-
+
+ + + +
+
+
field($model, 'visit_at')->input('datetime-local') ?>
field($model, 'provider_name') ?>
field($model, 'notes')->textarea(['rows' => 3]) ?> field($model, 'recommendations')->textarea(['rows' => 3]) ?> +
-
-
+
+ + + +
+
+
+
-
-
+
+ + + +
+
+
+
-
-
+
+ + + +
+
+
+
-
-
+
+ + + +
+
+
@@ -323,12 +399,18 @@ CSS
+
-
-
+
+ + + +
+
+
@@ -340,6 +422,7 @@ CSS
+
@@ -353,23 +436,23 @@ CSS
-
-
+
+ + + +
+
+
$definition): ?> +
submit() ?> - - 'button', - 'class' => 'btn btn-default', - 'id' => 'medical-inline-cancel', - ]) ?> - + link(($returnTo ?? 'view') === 'medical-visits' ? $space->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id]) @@ -486,11 +569,32 @@ if ($isInline) { ]); $this->registerJs(<<request->get('showTopCancel', '0') === '1'; $renderCustomField = static function (string $fieldKey, AnimalProgressUpdateForm $formModel, array $definitions): string { if (!isset($definitions[$fieldKey])) { @@ -126,6 +127,18 @@ $this->registerCss(<< .panel-heading a { + color: inherit; + display: block; + text-decoration: none; +} + +.inline-editor-shell .panel.panel-default > .panel-heading a:hover, +.inline-editor-shell .panel.panel-default > .panel-heading a:focus { + color: #ffffff; + text-decoration: none; +} + .inline-editor-shell, .inline-editor-shell .panel-body, .inline-editor-shell .control-label, @@ -158,6 +171,31 @@ $this->registerCss(<< .panel:first-child { @@ -182,18 +227,20 @@ CSS
Edit Progress Update') ?> - + ', [ - 'class' => 'btn btn-default btn-sm', + 'class' => 'animal-inline-top-save-action', 'title' => Yii::t('AnimalManagementModule.base', 'Save Progress Update'), 'form' => $progressFormId, ]) ?> - ', [ - 'type' => 'button', - 'class' => 'btn btn-default btn-sm', - 'id' => 'progress-inline-cancel-icon', - 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), - ]) ?> + + ', [ + 'id' => 'progress-inline-cancel-icon', + 'class' => 'animal-inline-top-save-action', + 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), + 'type' => 'button', + ]) ?> +
@@ -211,8 +258,13 @@ CSS errorSummary($model, ['showAllErrors' => true]) ?>
-
-
+
+ + + +
+
+
field($model, 'weight') ?>
field($model, 'vitals')->textInput(['maxlength' => 255]) ?>
@@ -221,31 +273,49 @@ CSS
field($model, 'meal_plan_changes')->textarea(['rows' => 2]) ?>
field($model, 'housing_changes')->textarea(['rows' => 2]) ?>
+
-
-
+
+ + + +
+
+
+
-
-
+
+ + + +
+
+
$definition): ?> +
-
-
+
+ + + +
+
+
@@ -270,6 +340,7 @@ CSS
+
@@ -314,13 +385,7 @@ CSS
submit() ?> - - 'button', - 'class' => 'btn btn-default', - 'id' => 'progress-inline-cancel', - ]) ?> - + link(($returnTo ?? 'view') === 'progress-updates' ? $space->createUrl('/animal_management/animals/progress-updates', ['id' => $animal->id]) @@ -413,11 +478,32 @@ if ($isInline) { ]); $this->registerJs(<< $latestMedicalVisitByAnimal */ /* @var array $animalImageUrls */ /* @var array $transferAnimalImageUrls */ +/* @var array $animalDonationGoalsByAnimal */ /* @var array $tileFields */ /* @var array $tileFieldOverrides */ /* @var Space $space */ /* @var bool $canManage */ +$isTabletFocusMode = ($viewMode === 'tablet') && ((string)Yii::$app->request->get('focus', '') === '1'); + $currentParams = [ 'q' => $queryValue, 'status' => $statusFilter, @@ -40,6 +43,13 @@ $currentParams = [ $buildUrl = static function (array $overrides) use ($space, $currentParams): string { $params = array_merge($currentParams, $overrides); + + if (($params['view'] ?? '') !== 'tablet') { + unset($params['focus']); + } elseif (($params['focus'] ?? '') !== '1') { + unset($params['focus']); + } + return $space->createUrl('/animal_management/animals/index', $params); }; @@ -47,16 +57,70 @@ $sortUrl = static function (string $column) use ($buildUrl, $sortKey, $sortDirec $nextDirection = ($sortKey === $column && $sortDirection === 'asc') ? 'desc' : 'asc'; return $buildUrl(['sort' => $column, 'direction' => $nextDirection, 'view' => 'table']); }; + +$showDonationSettingsLinks = $canManage && $space->moduleManager->isEnabled('donations'); ?> -
+ + + + +
Animals') ?> - ', $buildUrl(['view' => 'tiles']), [ + ', $buildUrl(['view' => 'tablet', 'focus' => '1']), [ + 'class' => 'btn btn-default btn-sm' . ($viewMode === 'tablet' ? ' active' : ''), + 'title' => Yii::t('AnimalManagementModule.base', 'Tablet View'), + 'aria-label' => Yii::t('AnimalManagementModule.base', 'Tablet View'), + ]) ?> + ', $buildUrl(['view' => 'rows']), [ + 'class' => 'btn btn-default btn-sm' . ($viewMode === 'rows' ? ' active' : ''), + 'title' => Yii::t('AnimalManagementModule.base', 'Row View'), + 'aria-label' => Yii::t('AnimalManagementModule.base', 'Row View'), + ]) ?> + ', $buildUrl(['view' => 'tiles2']), [ + 'class' => 'btn btn-default btn-sm' . ($viewMode === 'tiles2' ? ' active' : ''), + 'title' => Yii::t('AnimalManagementModule.base', '2-Column View'), + 'aria-label' => Yii::t('AnimalManagementModule.base', '2-Column View'), + ]) ?> + ', $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'), + 'title' => Yii::t('AnimalManagementModule.base', '3-Column View'), + 'aria-label' => Yii::t('AnimalManagementModule.base', '3-Column View'), ]) ?> ', $buildUrl(['view' => 'table']), [ 'class' => 'btn btn-default btn-sm' . ($viewMode === 'table' ? ' active' : ''), @@ -76,6 +140,9 @@ $sortUrl = static function (string $column) use ($buildUrl, $sortKey, $sortDirec
+ + + @@ -125,12 +192,22 @@ $sortUrl = static function (string $column) use ($buildUrl, $sortKey, $sortDirec
- + +
id; ?> -
+
render('_tile', [ 'animal' => $animal, 'contentContainer' => $space, @@ -138,6 +215,9 @@ $sortUrl = static function (string $column) use ($buildUrl, $sortKey, $sortDirec 'imageUrl' => $animalImageUrls[$animalId] ?? '', 'tileFields' => $tileFieldOverrides[$animalId] ?? $tileFields, 'showMedicalIcon' => true, + 'showDonationSettingsButton' => $showDonationSettingsLinks, + 'existingDonationGoal' => $animalDonationGoalsByAnimal[$animalId] ?? null, + 'tileLayoutMode' => $viewMode, ]) ?>
@@ -205,6 +285,13 @@ $sortUrl = static function (string $column) use ($buildUrl, $sortKey, $sortDirec ->link($space->createUrl('/animal_management/animals/medical-visits', ['id' => $animal->id])) ?> link($space->createUrl('/animal_management/animals/progress-updates', ['id' => $animal->id])) ?> + + link($space->createUrl('/donations/settings', [ + 'goalType' => 'animal', + 'targetAnimalId' => (int)$animal->id, + ])) ?> + link($space->createUrl('/animal_management/animals/edit', ['id' => $animal->id])) ?> @@ -223,10 +310,11 @@ $sortUrl = static function (string $column) use ($buildUrl, $sortKey, $sortDirec
-

- -
- + +

+
-
+
@@ -25,8 +25,59 @@ $jsonPayload = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNI $this->registerJs(<<request->get('showTopCancel', '0') === '1'; +$returnTo = (string)($returnTo ?? 'view'); +$transferFormId = 'animal-transfer-request-form'; + +$this->registerCss(<< .panel-body { + position: relative; + z-index: 1; + background: rgba(10, 18, 28, 0.2); +} + +.animal-transfer-inline-shell, +.animal-transfer-inline-shell .panel-body, +.animal-transfer-inline-shell .panel-heading, +.animal-transfer-inline-shell .control-label, +.animal-transfer-inline-shell .help-block { + color: #eef5fb; +} + +.animal-transfer-inline-shell .panel-heading { + background: rgba(15, 23, 42, 0.4); + border-color: rgba(255, 255, 255, 0.22); + margin-left: 20px; + margin-bottom: 20px; + padding: 10px 20px; + max-width: fit-content; + border-radius: 10px; +} + +.animal-transfer-inline-shell .panel-heading strong { + color: #f8fafc; +} + +.animal-transfer-inline-shell .form-control { + background: rgba(10, 18, 28, 0.56); + border-color: rgba(255, 255, 255, 0.44); + color: #f3f8ff; +} + +.animal-transfer-inline-shell .form-control::placeholder { + color: rgba(243, 248, 255, 0.72); +} + +.animal-transfer-inline-shell select.form-control option { + color: #0f1b2a; +} + +.animal-transfer-inline-shell .animal-inline-top-save-action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.36); + background: rgba(15, 23, 42, 0.72); + color: #f8fafc; + cursor: pointer; + line-height: 1; + font-size: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28); +} + +.animal-transfer-inline-shell .animal-inline-top-save-action:hover, +.animal-transfer-inline-shell .animal-inline-top-save-action:focus { + background: rgba(30, 41, 59, 0.86); + border-color: rgba(255, 255, 255, 0.5); + color: #f8fafc; + outline: none; +} +CSS +); + +if ($isInline) { + $this->registerCss(<< .panel:first-child { + margin-top: 0 !important; +} +CSS + ); +} ?> -
+
+ +
+ ', [ + 'class' => 'animal-inline-top-save-action', + 'title' => Yii::t('AnimalManagementModule.base', 'Send Request'), + 'form' => $transferFormId, + ]) ?> + + ', [ + 'id' => 'transfer-inline-add-cancel-icon', + 'class' => 'animal-inline-top-save-action', + 'title' => Yii::t('AnimalManagementModule.base', 'Cancel'), + 'type' => 'button', + ]) ?> + +
+ +
Transfer Request for {animal}', ['animal' => $animal->getDisplayName()]) ?>
- + $transferFormId]; + if (!$isInline) { + $formOptions['target'] = '_top'; + } + $form = ActiveForm::begin(['options' => $formOptions]); + ?> + + + + errorSummary($model, ['showAllErrors' => true]) ?> field($model, 'to_space_id')->dropDownList($model->getTargetOptions(), ['prompt' => Yii::t('AnimalManagementModule.base', 'Select destination rescue')]) ?> field($model, 'request_message')->textarea(['rows' => 4]) ?> field($model, 'conditions_text')->textarea(['rows' => 3]) ?> - submit() ?> - link($space->createUrl('/animal_management/animals/index')) ?> + + submit() ?> + link($space->createUrl('/animal_management/animals/index')) ?> +
+ + 'animal-inline-editor', + 'type' => 'cancel', + 'collapseId' => 'transfer-add-inline', + ]); + + $this->registerJs(<< diff --git a/views/animals/view.php b/views/animals/view.php index 0ba89a6..1bd7b64 100644 --- a/views/animals/view.php +++ b/views/animals/view.php @@ -7,9 +7,12 @@ 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\animal_management\events\AnimalTileRenderEvent; use humhub\modules\gallery\assets\Assets as GalleryAssets; use humhub\modules\space\models\Space; +use yii\base\Event; use yii\helpers\Html; +use yii\helpers\Json; /* @var Space $space */ /* @var Animal $animal */ @@ -21,13 +24,32 @@ use yii\helpers\Html; /* @var AnimalGalleryItem[] $galleryItems */ /* @var array $customFieldValues */ /* @var string $animalCoverImageUrl */ +/* @var string $animalProfileImageUrl */ /* @var array $detailHeroFields */ +/* @var string $layoutMode */ $openMedicalEditId = (int)Yii::$app->request->get('inlineMedicalEdit', 0); $openProgressEditId = (int)Yii::$app->request->get('inlineProgressEdit', 0); +$layoutMode = isset($layoutMode) ? trim((string)$layoutMode) : 'default'; +$isTabletLayout = $layoutMode === 'tablet'; + +$medicalVisitsUrl = $space->createUrl('/animal_management/animals/medical-visits', ['id' => (int)$animal->id]); +$progressUpdatesUrl = $space->createUrl('/animal_management/animals/progress-updates', ['id' => (int)$animal->id]); +$transferRequestUrl = $space->createUrl('/animal_management/animals/transfer', ['id' => (int)$animal->id]); +$tabletBackUrl = $space->createUrl('/animal_management/animals/index', ['view' => 'tablet']); + +$transferById = []; +foreach ($transfers as $transfer) { + if ($transfer instanceof AnimalTransfer) { + $transferById[(int)$transfer->id] = $transfer; + } +} + $coverImageUrl = trim((string)$animalCoverImageUrl); +$profileImageUrl = trim((string)($animalProfileImageUrl ?? '')); $hasCoverImage = $coverImageUrl !== '' && (preg_match('/^https?:\/\//i', $coverImageUrl) || substr($coverImageUrl, 0, 1) === '/'); +$hasProfileImage = $profileImageUrl !== '' && (preg_match('/^https?:\/\//i', $profileImageUrl) || substr($profileImageUrl, 0, 1) === '/'); $statusLabel = Animal::statusOptions()[$animal->status] ?? (string)$animal->status; $detailFieldMap = [ @@ -78,12 +100,112 @@ if (class_exists(GalleryAssets::class)) { } $uiGalleryId = 'animal-gallery-' . (int)$animal->id; + +$showDonationSettingsButton = $canManage && $space->moduleManager->isEnabled('donations'); +$existingDonationGoal = null; +$profileTileOverlayAddonHtml = ''; + +if ($space->moduleManager->isEnabled('donations')) { + $donationGoalClass = 'humhub\\modules\\donations\\models\\DonationGoal'; + if (class_exists($donationGoalClass) + && Yii::$app->db->schema->getTableSchema($donationGoalClass::tableName(), true) !== null + ) { + $existingDonationGoal = $donationGoalClass::find() + ->where([ + 'contentcontainer_id' => (int)$space->contentcontainer_id, + 'goal_type' => $donationGoalClass::TYPE_ANIMAL, + 'target_animal_id' => (int)$animal->id, + ]) + ->orderBy(['is_active' => SORT_DESC, 'id' => SORT_DESC]) + ->one(); + } +} + +$profileDonationToggleInputId = 'animal-profile-donation-toggle-' . (int)$animal->id; +$profileDonationInlineFormId = 'animal-profile-donation-inline-' . (int)$animal->id; +$profileDonationFileFieldId = 'animal-profile-donation-image-file-' . (int)$animal->id; +$profileDonationPreviewId = 'animal-profile-donation-image-preview-' . (int)$animal->id; +$profileCreateInlineGoalUrl = $space->createUrl('/donations/settings/create-animal-goal-inline', [ + 'animalId' => (int)$animal->id, +]); + +$profileGalleryUrls = []; +foreach ($galleryItems as $galleryItem) { + if (!$galleryItem instanceof AnimalGalleryItem) { + continue; + } + + $url = trim((string)$galleryItem->getImageUrl()); + if ($url === '') { + continue; + } + + $profileGalleryUrls[] = $url; +} +$profileGalleryUrls = array_slice($profileGalleryUrls, 0, 8); + +$profileDefaultGoalTitle = Yii::t('DonationsModule.base', '{animalName} Care Fund', [ + 'animalName' => (string)$animal->getDisplayName(), +]); + +$profileExistingGoalId = is_object($existingDonationGoal) ? (int)($existingDonationGoal->id ?? 0) : 0; +$profileExistingGoalTitle = trim((string)(is_object($existingDonationGoal) ? ($existingDonationGoal->title ?? '') : '')); +$profileExistingGoalTargetAmount = is_object($existingDonationGoal) ? (float)($existingDonationGoal->target_amount ?? 0) : 0.0; +$profileExistingGoalDescription = trim((string)(is_object($existingDonationGoal) ? ($existingDonationGoal->description ?? '') : '')); +$profileExistingGoalImage = trim((string)(is_object($existingDonationGoal) ? ($existingDonationGoal->image_path ?? '') : '')); +$profileExistingGoalActive = is_object($existingDonationGoal) ? (int)($existingDonationGoal->is_active ?? 1) : 1; + +if ($profileExistingGoalTitle === '') { + $profileExistingGoalTitle = $profileDefaultGoalTitle; +} + +if (class_exists(AnimalTileRenderEvent::class) && $space->moduleManager->isEnabled('donations')) { + $profileTileRenderEvent = new AnimalTileRenderEvent([ + 'animal' => $animal, + 'contentContainer' => $space, + 'existingDonationGoal' => $existingDonationGoal, + 'showDonationSettingsButton' => $showDonationSettingsButton, + 'donationToggleInputId' => $profileDonationToggleInputId, + 'donationInlineFormId' => $profileDonationInlineFormId, + ]); + + Event::trigger(AnimalTileRenderEvent::class, AnimalTileRenderEvent::EVENT_RENDER_OVERLAY, $profileTileRenderEvent); + $profileTileOverlayAddonHtml = $profileTileRenderEvent->getHtml(); +} + +$profileInlineFormBackgroundStyle = 'padding:12px;border-top:1px solid #e6edf5;display:none;background:#fff;'; +if ($profileExistingGoalImage !== '') { + $profileInlineFormBackgroundStyle = 'padding:12px;border-top:1px solid #e6edf5;display:none;' + . 'background-image:linear-gradient(rgba(255,255,255,0.35),rgba(255,255,255,0.45)),url(' . Html::encode($profileExistingGoalImage) . ');' + . 'background-size:cover;background-position:center;'; +} ?> -
-
- - <?= Html::encode($animal->getDisplayName()) ?> +
+ + + + +
+ +
+ + + +
+ + + + <?= Html::encode($animal->getDisplayName()) ?> + + + <?= Html::encode($animal->getDisplayName()) ?> +
@@ -93,13 +215,13 @@ $uiGalleryId = 'animal-gallery-' . (int)$animal->id; -
+
getDisplayName()) ?>
@@ -118,11 +240,239 @@ $uiGalleryId = 'animal-gallery-' . (int)$animal->id; public_summary)) ?>
+ + +
+ +
+
+ + +
+
+
+ +
+ + 'multipart/form-data', + 'style' => 'margin:0;', + ]) ?> + request->csrfParam, Yii::$app->request->getCsrfToken()) ?> + + + id) ?> + + +
+ + 'form-control input-sm animal-profile-donation-inline-input', + 'maxlength' => 190, + 'required' => true, + ]) ?> +
+ +
+ + 0 ? (string)$profileExistingGoalTargetAmount : '', [ + 'class' => 'form-control input-sm animal-profile-donation-inline-input', + 'step' => '0.01', + 'min' => '0', + 'required' => true, + ]) ?> +
+ +
+ + 'form-control input-sm animal-profile-donation-inline-input', + 'rows' => 2, + ]) ?> +
+ +
+ +
+ + + + + +
+ + + + +
+ +
+ + + $profileDonationFileFieldId, + 'class' => 'form-control input-sm js-animal-profile-donation-upload animal-profile-donation-inline-input', + 'data-preview' => '#' . $profileDonationPreviewId, + 'data-editor-bg' => '#' . $profileDonationInlineFormId, + 'accept' => 'image/*', + ]) ?> +
+ +
+ 0 ? Yii::t('DonationsModule.base', 'Update Goal') : Yii::t('DonationsModule.base', 'Save Goal'), ['class' => 'btn btn-primary btn-sm']) ?> + +
+ +
+
+
registerCss("#{$profileDonationToggleInputId}:checked ~ #{$profileDonationInlineFormId}{display:block !important;}"); + $this->registerCss("#{$profileDonationInlineFormId} .animal-profile-donation-inline-input{background:rgba(255,255,255,0.34);border-color:rgba(165,178,195,0.72);color:#16202a;}\n" + . "#{$profileDonationInlineFormId} .animal-profile-donation-inline-input::placeholder{color:rgba(31,41,55,0.72);}\n" + . "#{$profileDonationInlineFormId} .animal-profile-donation-inline-gallery{background:rgba(255,255,255,0.16);padding:6px;border-radius:6px;pointer-events:auto;}\n" + . "#{$profileDonationInlineFormId} .animal-profile-donation-gallery-radio{position:absolute;opacity:0;pointer-events:none;}\n" + . "#{$profileDonationInlineFormId} .animal-profile-donation-gallery-radio:checked + label{border-color:#1f78c1 !important;box-shadow:0 0 0 2px rgba(31,120,193,0.32) !important;}"); + + $profileUploadPendingText = Json::htmlEncode(Yii::t('DonationsModule.base', 'Image will be uploaded with this goal.')); + $profileNoImageText = Json::htmlEncode(Yii::t('DonationsModule.base', 'No image selected.')); + $profileInlineJsNamespace = 'animalProfileDonationInlineToggle' . (int)$animal->id; + + $this->registerJs(<<'); + } + + var profileInlineToggleSelector = '#$profileDonationToggleInputId'; + var profileInlineFormSelector = '#$profileDonationInlineFormId'; + + syncProfileInlineEditor(profileInlineToggleSelector, profileInlineFormSelector); + + $(document).off('change.$profileInlineJsNamespace').on('change.$profileInlineJsNamespace', profileInlineToggleSelector, function() { + syncProfileInlineEditor(profileInlineToggleSelector, profileInlineFormSelector); + }); + + $(document).off('change.animalProfileDonationGallery').on('change.animalProfileDonationGallery', '.animal-profile-donation-gallery-radio', function() { + var radio = $(this); + if (!radio.is(':checked')) { + return; + } + + var previewSelector = radio.data('preview'); + var editorSelector = radio.data('editor-bg'); + var url = radio.val() || ''; + + setProfilePreview(previewSelector, url); + setProfileEditorBackground(editorSelector, url); + }); + + $(document).off('change.animalProfileDonationUpload').on('change.animalProfileDonationUpload', '.js-animal-profile-donation-upload', function() { + var previewSelector = $(this).data('preview'); + var editorSelector = $(this).data('editor-bg'); + if (!previewSelector) { + return; + } + + var form = $(this).closest('form'); + form.find('.animal-profile-donation-gallery-radio').prop('checked', false); + + var previousObjectUrl = $(this).data('object-url') || ''; + if (previousObjectUrl && window.URL && window.URL.revokeObjectURL) { + window.URL.revokeObjectURL(previousObjectUrl); + } + + if (this.files && this.files.length > 0) { + var objectUrl = ''; + if (window.URL && window.URL.createObjectURL) { + objectUrl = window.URL.createObjectURL(this.files[0]); + $(this).data('object-url', objectUrl); + } + + if (objectUrl) { + setProfilePreview(previewSelector, objectUrl); + setProfileEditorBackground(editorSelector, objectUrl); + } else { + $(previewSelector).text($profileUploadPendingText); + } + } else { + $(this).data('object-url', ''); + var selectedRadio = form.find('.animal-profile-donation-gallery-radio:checked').first(); + var fallbackUrl = selectedRadio.length ? (selectedRadio.val() || '') : ''; + setProfilePreview(previewSelector, fallbackUrl); + setProfileEditorBackground(editorSelector, fallbackUrl); + } + }); +})(); +JS, \yii\web\View::POS_READY, 'animal-profile-donation-inline-' . (int)$animal->id); +} + $this->registerJs(<<<'JS' (function() { function getCsrfPayload() { @@ -209,9 +559,29 @@ $this->registerJs(<<<'JS' $(document) .off('shown.bs.collapse.animalInlineScroll', '.animal-inline-editor') .on('shown.bs.collapse.animalInlineScroll', '.animal-inline-editor', function() { + var $content = $(this).closest('.animal-feed-content'); + if ($content.length) { + $content.addClass('animal-feed-editing'); + } scrollInlineEditorIntoView(this); }); + $(document) + .off('hidden.bs.collapse.animalInlineScroll', '.animal-inline-editor') + .on('hidden.bs.collapse.animalInlineScroll', '.animal-inline-editor', function() { + var $content = $(this).closest('.animal-feed-content'); + if ($content.length) { + $content.removeClass('animal-feed-editing'); + } + }); + + $('.animal-inline-editor.in').each(function() { + var $content = $(this).closest('.animal-feed-content'); + if ($content.length) { + $content.addClass('animal-feed-editing'); + } + }); + $(document).on('submit', '#animal-gallery-upload', function(event) { event.preventDefault(); var form = this; @@ -260,14 +630,15 @@ $this->registerJs(<<<'JS' })(); JS , \yii\web\View::POS_END); + ?> -