VerbFilter::class, 'actions' => [ 'delete' => ['post'], 'add-medical-visit' => ['post'], 'add-progress-update' => ['post'], 'add-gallery-images' => ['post'], 'remove-gallery-image' => ['post'], 'transfer-respond' => ['post'], 'transfer-complete' => ['post'], 'transfer-cancel' => ['post'], ], ]; return $behaviors; } 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']], ]; } public function actionIndex() { $queryValue = trim((string)Yii::$app->request->get('q', '')); $statusFilter = trim((string)Yii::$app->request->get('status', '')); $speciesFilter = trim((string)Yii::$app->request->get('species', '')); if ($this->contentContainer instanceof Space) { GalleryIntegrationService::syncSpaceAnimalGalleries($this->contentContainer); } $viewMode = trim((string)Yii::$app->request->get('view', 'tiles')); if (!in_array($viewMode, ['tiles', 'table'], true)) { $viewMode = 'tiles'; } $sortKey = trim((string)Yii::$app->request->get('sort', 'updated_at')); $sortDirection = strtolower(trim((string)Yii::$app->request->get('direction', 'desc'))) === 'asc' ? 'asc' : 'desc'; $availableSorts = [ 'animal_uid' => 'animal_uid', 'name' => 'name', 'species' => 'species', 'status' => 'status', 'updated_at' => 'updated_at', 'last_medical' => 'last_medical', ]; if (!isset($availableSorts[$sortKey])) { $sortKey = 'updated_at'; } $availableColumns = [ 'animal_uid' => Yii::t('AnimalManagementModule.base', 'ID'), 'name' => Yii::t('AnimalManagementModule.base', 'Name'), 'species' => Yii::t('AnimalManagementModule.base', 'Species'), 'status' => Yii::t('AnimalManagementModule.base', 'Status'), 'last_medical' => Yii::t('AnimalManagementModule.base', 'Last Medical Visit'), 'updated_at' => Yii::t('AnimalManagementModule.base', 'Updated'), ]; $defaultColumns = ['animal_uid', 'name', 'species', 'status', 'last_medical', 'updated_at']; $colsRaw = Yii::$app->request->get('cols', implode(',', $defaultColumns)); if (is_array($colsRaw)) { $selectedColumns = array_values(array_intersect(array_keys($availableColumns), $colsRaw)); } else { $selectedColumns = array_values(array_intersect(array_keys($availableColumns), array_map('trim', explode(',', (string)$colsRaw)))); } if (empty($selectedColumns)) { $selectedColumns = $defaultColumns; } if (!in_array('name', $selectedColumns, true)) { $selectedColumns[] = 'name'; } $query = Animal::find() ->where(['contentcontainer_id' => $this->contentContainer->contentcontainer_id]); if ($queryValue !== '') { $query->andWhere([ 'or', ['like', 'animal_uid', $queryValue], ['like', 'name', $queryValue], ['like', 'species', $queryValue], ['like', 'breed', $queryValue], ]); } if ($statusFilter !== '' && isset(Animal::statusOptions()[$statusFilter])) { $query->andWhere(['status' => $statusFilter]); } if ($speciesFilter !== '') { $query->andWhere(['species' => $speciesFilter]); } if ($sortKey !== 'last_medical') { $sortColumn = $availableSorts[$sortKey]; $query->orderBy([ $sortColumn => $sortDirection === 'asc' ? SORT_ASC : SORT_DESC, 'id' => SORT_DESC, ]); } else { $query->orderBy(['updated_at' => SORT_DESC, 'id' => SORT_DESC]); } $animals = $query->all(); $speciesOptions = Animal::find() ->select('species') ->where(['contentcontainer_id' => $this->contentContainer->contentcontainer_id]) ->andWhere(['not', ['species' => null]]) ->andWhere(['!=', 'species', '']) ->distinct() ->orderBy(['species' => SORT_ASC]) ->column(); $speciesOptions = array_values(array_filter(array_map(static function ($species) { return trim((string)$species); }, $speciesOptions))); $latestMedicalVisitByAnimal = []; $animalIds = array_map(static function (Animal $animal): int { return (int)$animal->id; }, $animals); $animalImageUrls = $this->resolveAnimalImageUrls($animalIds, ['profile_image_url', 'profile_image', 'photo_url', 'image_url', 'photo'], false); $tileFieldOverrides = $this->resolveDisplayFieldOverrides($animalIds, 'tile_display_fields'); if (!empty($animalIds)) { $visits = AnimalMedicalVisit::find() ->where(['animal_id' => $animalIds]) ->orderBy(['animal_id' => SORT_ASC, 'visit_at' => SORT_DESC, 'id' => SORT_DESC]) ->all(); foreach ($visits as $visit) { $animalId = (int)$visit->animal_id; if (!isset($latestMedicalVisitByAnimal[$animalId])) { $latestMedicalVisitByAnimal[$animalId] = $visit; } } } if ($sortKey === 'last_medical') { usort($animals, static function (Animal $a, Animal $b) use ($latestMedicalVisitByAnimal, $sortDirection): int { $aVisit = $latestMedicalVisitByAnimal[(int)$a->id] ?? null; $bVisit = $latestMedicalVisitByAnimal[(int)$b->id] ?? null; $aTs = $aVisit ? strtotime((string)$aVisit->visit_at) : 0; $bTs = $bVisit ? strtotime((string)$bVisit->visit_at) : 0; if ($aTs === false) { $aTs = 0; } if ($bTs === false) { $bTs = 0; } $cmp = $aTs <=> $bTs; if ($cmp === 0) { $cmp = strcmp((string)$a->updated_at, (string)$b->updated_at); } return $sortDirection === 'asc' ? $cmp : -$cmp; }); } $incomingTransfers = AnimalTransfer::find() ->where(['to_contentcontainer_id' => $this->contentContainer->contentcontainer_id]) ->andWhere(['status' => [AnimalTransfer::STATUS_REQUESTED, AnimalTransfer::STATUS_ACCEPTED]]) ->orderBy(['updated_at' => SORT_DESC, 'id' => SORT_DESC]) ->all(); $outgoingTransfers = AnimalTransfer::find() ->where(['from_contentcontainer_id' => $this->contentContainer->contentcontainer_id]) ->orderBy(['updated_at' => SORT_DESC, 'id' => SORT_DESC]) ->limit(30) ->all(); $transferAnimalIds = []; foreach (array_merge($incomingTransfers, $outgoingTransfers) as $transfer) { if ($transfer instanceof AnimalTransfer) { $transferAnimalIds[] = (int)$transfer->animal_id; } } $transferAnimalImageUrls = $this->resolveAnimalImageUrls( $transferAnimalIds, ['cover_image_url', 'profile_image_url', 'profile_image', 'photo_url', 'image_url', 'photo'], true ); return $this->render('index', [ 'animals' => $animals, 'queryValue' => $queryValue, 'statusFilter' => $statusFilter, 'speciesFilter' => $speciesFilter, 'viewMode' => $viewMode, 'sortKey' => $sortKey, 'sortDirection' => $sortDirection, 'availableColumns' => $availableColumns, 'selectedColumns' => $selectedColumns, 'speciesOptions' => $speciesOptions, 'latestMedicalVisitByAnimal' => $latestMedicalVisitByAnimal, 'animalImageUrls' => $animalImageUrls, 'tileFields' => $this->getTileFieldSettings(), 'tileFieldOverrides' => $tileFieldOverrides, 'space' => $this->contentContainer, 'canManage' => $this->canManageAnimals(), 'incomingTransfers' => $incomingTransfers, 'outgoingTransfers' => $outgoingTransfers, 'transferAnimalImageUrls' => $transferAnimalImageUrls, ]); } public function actionView(int $id) { $animal = $this->findAnimal($id); $canManage = $this->canManageAnimals(); if ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($animal, $this->contentContainer); } $medicalVisitsQuery = $animal->getMedicalVisits(); $medicalSchema = Yii::$app->db->schema->getTableSchema(AnimalMedicalVisit::tableName(), true); if ($medicalSchema !== null && isset($medicalSchema->columns['post_to_animal_feed'])) { $medicalVisitsQuery->andWhere(['post_to_animal_feed' => 1]); } $medicalVisits = $medicalVisitsQuery->orderBy(['visit_at' => SORT_DESC, 'id' => SORT_DESC])->all(); $progressUpdatesQuery = $animal->getProgressUpdates(); $progressSchema = Yii::$app->db->schema->getTableSchema(AnimalProgressUpdate::tableName(), true); if ($progressSchema !== null && isset($progressSchema->columns['post_to_animal_feed'])) { $progressUpdatesQuery->andWhere(['post_to_animal_feed' => 1]); } $progressUpdates = $progressUpdatesQuery->orderBy(['update_at' => SORT_DESC, 'id' => SORT_DESC])->all(); $transfers = $animal->getTransfers()->orderBy(['created_at' => SORT_DESC, 'id' => SORT_DESC])->all(); $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] ?? '')); $detailHeroFields = $this->getDetailHeroFieldSettings(); $heroOverrides = $this->resolveDisplayFieldOverrides([(int)$animal->id], 'hero_display_fields'); if (!empty($heroOverrides[(int)$animal->id])) { $detailHeroFields = $heroOverrides[(int)$animal->id]; } return $this->render('view', [ 'space' => $this->contentContainer, 'animal' => $animal, 'canManage' => $canManage, 'medicalVisits' => $medicalVisits, 'progressUpdates' => $progressUpdates, 'transfers' => $transfers, 'transferEvents' => $transferEvents, 'galleryItems' => $galleryItems, 'customFieldValues' => $customFieldValues, 'animalCoverImageUrl' => $animalCoverImageUrl, 'detailHeroFields' => $detailHeroFields, ]); } public function actionMedicalVisits(int $id) { $animal = $this->findAnimal($id); if ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($animal, $this->contentContainer); } $canManage = $this->canManageAnimals(); $this->ensureMedicalMediaFieldDefinition(); $medicalVisitForm = new AnimalMedicalVisitForm(['animal' => $animal]); $medicalVisits = $animal->getMedicalVisits()->orderBy(['visit_at' => SORT_DESC, 'id' => SORT_DESC])->all(); $galleryItems = $animal->getGalleryItems()->orderBy(['id' => SORT_DESC])->limit(120)->all(); return $this->render('medical-visits', [ 'space' => $this->contentContainer, 'animal' => $animal, 'canManage' => $canManage, 'medicalVisitForm' => $medicalVisitForm, 'medicalVisits' => $medicalVisits, 'galleryItems' => $galleryItems, ]); } public function actionProgressUpdates(int $id) { $animal = $this->findAnimal($id); if ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($animal, $this->contentContainer); } $canManage = $this->canManageAnimals(); $progressUpdateForm = new AnimalProgressUpdateForm(['animal' => $animal]); $progressUpdates = $animal->getProgressUpdates()->orderBy(['update_at' => SORT_DESC, 'id' => SORT_DESC])->all(); $galleryItems = $animal->getGalleryItems()->orderBy(['id' => SORT_DESC])->limit(120)->all(); return $this->render('progress-updates', [ 'space' => $this->contentContainer, 'animal' => $animal, 'canManage' => $canManage, 'progressUpdateForm' => $progressUpdateForm, 'progressUpdates' => $progressUpdates, 'galleryItems' => $galleryItems, ]); } public function actionCreate() { $model = new AnimalForm(['contentContainer' => $this->contentContainer]); if (Yii::$app->request->isPost) { $model->load(Yii::$app->request->post()); $model->coverImageFile = UploadedFile::getInstanceByName('AnimalForm[coverImageFile]') ?: UploadedFile::getInstance($model, 'coverImageFile'); $model->profileImageFile = UploadedFile::getInstanceByName('AnimalForm[profileImageFile]') ?: UploadedFile::getInstance($model, 'profileImageFile'); } if (Yii::$app->request->isPost && $model->save()) { $savedAnimal = $model->getAnimal(); if ($savedAnimal instanceof Animal) { $this->syncPrimaryImagesToGallery($savedAnimal); if ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($savedAnimal, $this->contentContainer); } } $this->view->success(Yii::t('AnimalManagementModule.base', 'Animal created.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/index')); } return $this->render('create', [ 'model' => $model, 'space' => $this->contentContainer, 'isEdit' => false, 'animal' => null, ]); } public function actionEdit(int $id) { $animal = $this->findAnimal($id); $model = new AnimalForm(['contentContainer' => $this->contentContainer]); $model->setAnimal($animal); if (Yii::$app->request->isPost) { $model->load(Yii::$app->request->post()); $model->coverImageFile = UploadedFile::getInstanceByName('AnimalForm[coverImageFile]') ?: UploadedFile::getInstance($model, 'coverImageFile'); $model->profileImageFile = UploadedFile::getInstanceByName('AnimalForm[profileImageFile]') ?: UploadedFile::getInstance($model, 'profileImageFile'); } if (Yii::$app->request->isPost && $model->save()) { $savedAnimal = $model->getAnimal(); if ($savedAnimal instanceof Animal) { $this->syncPrimaryImagesToGallery($savedAnimal); if ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($savedAnimal, $this->contentContainer); } } $this->view->success(Yii::t('AnimalManagementModule.base', 'Animal updated.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id])); } return $this->render('create', [ 'model' => $model, 'space' => $this->contentContainer, 'isEdit' => true, 'animal' => $animal, ]); } public function actionDelete(int $id) { if (!$this->canManageAnimals()) { throw new ForbiddenHttpException('You are not allowed to delete animals.'); } $animal = $this->findAnimal($id); $localGalleryPaths = $this->collectAnimalLocalGalleryImagePaths((int)$animal->id); if ($animal->delete() === false) { $this->view->error(Yii::t('AnimalManagementModule.base', 'Could not delete animal.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $id])); } $deletedFiles = 0; foreach ($localGalleryPaths as $path) { $absolutePath = Yii::getAlias('@webroot') . $path; if (!is_file($absolutePath)) { continue; } if (@unlink($absolutePath)) { $deletedFiles++; } } if ($this->contentContainer instanceof Space) { GalleryIntegrationService::syncSpaceAnimalGalleries($this->contentContainer); } if ($deletedFiles > 0) { $this->view->success(Yii::t('AnimalManagementModule.base', 'Animal deleted and {count} gallery image(s) removed.', ['count' => $deletedFiles])); } else { $this->view->success(Yii::t('AnimalManagementModule.base', 'Animal deleted.')); } return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/index')); } public function actionTransfer(int $id) { $animal = $this->findAnimal($id); $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.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id])); } return $this->render('transfer', [ 'space' => $this->contentContainer, 'animal' => $animal, 'model' => $form, ]); } public function actionAddGalleryImages(int $id) { $animal = $this->findAnimal($id); $uploadedFiles = UploadedFile::getInstancesByName('galleryImages'); $maxUploadCount = 10; if (empty($uploadedFiles)) { $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])); } $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 ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($animal, $this->contentContainer); } $this->view->success(Yii::t('AnimalManagementModule.base', '{count} image(s) added to gallery.', ['count' => $added])); } else { $this->view->error(Yii::t('AnimalManagementModule.base', 'No gallery images were added. Check image type/size requirements.')); } return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id]) . '#animal-gallery'); } public function actionRemoveGalleryImage(int $id, int $galleryId) { $animal = $this->findAnimal($id); $item = AnimalGalleryItem::findOne(['id' => $galleryId, 'animal_id' => (int)$animal->id]); if (!$item instanceof AnimalGalleryItem) { throw new NotFoundHttpException('Gallery image not found.'); } $path = trim((string)$item->file_path); $deletePhysicalFile = false; if ($path !== '' && substr($path, 0, 1) === '/') { $stillReferencedByGallery = AnimalGalleryItem::find() ->where(['animal_id' => (int)$animal->id, 'file_path' => $path]) ->andWhere(['!=', 'id', (int)$item->id]) ->exists(); $stillReferencedByPrimaryImage = $this->isPrimaryImagePathReferenced((int)$animal->id, $path); $deletePhysicalFile = !$stillReferencedByGallery && !$stillReferencedByPrimaryImage; } if ($item->delete() !== false) { if ($this->contentContainer instanceof Space) { GalleryIntegrationService::ensureAnimalGallery($animal, $this->contentContainer); } if ($deletePhysicalFile) { $absolute = Yii::getAlias('@webroot') . $path; if (is_file($absolute)) { @unlink($absolute); } } $this->view->success(Yii::t('AnimalManagementModule.base', 'Gallery image removed.')); } else { $this->view->error(Yii::t('AnimalManagementModule.base', 'Could not remove gallery image.')); } return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id]) . '#animal-gallery'); } public function actionAddMedicalVisit(int $id) { $animal = $this->findAnimal($id); $this->ensureMedicalMediaFieldDefinition(); $form = new AnimalMedicalVisitForm(['animal' => $animal]); $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'view')); if ($form->load(Yii::$app->request->post())) { $this->applyMedicalMediaInput($form, $animal); if ($form->save()) { $this->view->success(Yii::t('AnimalManagementModule.base', 'Medical visit added.')); } else { $message = Yii::t('AnimalManagementModule.base', 'Could not save medical visit.'); if ($form->hasErrors()) { $message .= ' ' . implode(' ', array_values($form->getFirstErrors())); } $this->view->error($message); } } else { $this->view->error(Yii::t('AnimalManagementModule.base', 'Could not save medical visit.')); } return $this->redirectToAnimalPage((int)$animal->id, $returnTo); } public function actionAddMedicalVisitInline(int $id) { $animal = $this->findAnimal($id); $this->ensureMedicalMediaFieldDefinition(); $form = new AnimalMedicalVisitForm(['animal' => $animal]); $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'medical-visits')); $isInline = (int)Yii::$app->request->get('inline', 1) === 1; if ($form->load(Yii::$app->request->post())) { $this->applyMedicalMediaInput($form, $animal); if ($form->save()) { $this->view->success(Yii::t('AnimalManagementModule.base', 'Medical visit added.')); if ($isInline) { return $this->renderAjax('inline-edit-result', [ 'collapseId' => 'medical-add-inline', 'refreshSelectors' => ['#animal-medical-panel', '#animal-gallery-panel'], ]); } return $this->redirectToAnimalPage((int)$animal->id, $returnTo); } } $galleryItems = $animal->getGalleryItems()->orderBy(['id' => SORT_DESC])->limit(120)->all(); $params = [ 'space' => $this->contentContainer, 'animal' => $animal, 'model' => $form, 'returnTo' => $returnTo, 'galleryItems' => $galleryItems, 'isInline' => $isInline, ]; if ($isInline) { return $this->renderAjax('add-medical-visit', $params); } return $this->render('add-medical-visit', $params); } public function actionEditMedicalVisit(int $id, int $visitId) { $animal = $this->findAnimal($id); $visit = $animal->getMedicalVisits()->andWhere(['id' => $visitId])->one(); $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'view')); $isInline = (int)Yii::$app->request->get('inline', 0) === 1; if (!$visit instanceof \humhub\modules\animal_management\models\AnimalMedicalVisit) { throw new NotFoundHttpException('Medical visit not found.'); } $this->ensureMedicalMediaFieldDefinition(); $form = new AnimalMedicalVisitForm(['animal' => $animal]); $form->setMedicalVisit($visit); if ($form->load(Yii::$app->request->post())) { $this->applyMedicalMediaInput($form, $animal); if ($form->save()) { $this->view->success(Yii::t('AnimalManagementModule.base', 'Medical visit updated.')); if ($isInline) { return $this->renderAjax('inline-edit-result', [ 'collapseId' => 'medical-edit-inline-' . (int)$visit->id, 'refreshSelectors' => ['#animal-medical-panel', '#animal-gallery-panel'], ]); } return $this->redirectToAnimalPage((int)$animal->id, $returnTo); } } $galleryItems = $animal->getGalleryItems()->orderBy(['id' => SORT_DESC])->limit(120)->all(); $params = [ 'space' => $this->contentContainer, 'animal' => $animal, 'model' => $form, 'medicalVisit' => $visit, 'returnTo' => $returnTo, 'galleryItems' => $galleryItems, 'isInline' => $isInline, ]; if ($isInline) { return $this->renderAjax('edit-medical-visit', $params); } return $this->render('edit-medical-visit', $params); } public function actionAddProgressUpdate(int $id) { $animal = $this->findAnimal($id); $form = new AnimalProgressUpdateForm(['animal' => $animal]); $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'view')); if ($form->load(Yii::$app->request->post())) { $this->applyProgressMediaInput($form, $animal); if ($form->save()) { $this->view->success(Yii::t('AnimalManagementModule.base', 'Progress update added.')); } else { $message = Yii::t('AnimalManagementModule.base', 'Could not save progress update.'); if ($form->hasErrors()) { $message .= ' ' . implode(' ', array_values($form->getFirstErrors())); } $this->view->error($message); } } else { $this->view->error(Yii::t('AnimalManagementModule.base', 'Could not save progress update.')); } return $this->redirectToAnimalPage((int)$animal->id, $returnTo); } public function actionAddProgressUpdateInline(int $id) { $animal = $this->findAnimal($id); $form = new AnimalProgressUpdateForm(['animal' => $animal]); $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'progress-updates')); $isInline = (int)Yii::$app->request->get('inline', 1) === 1; if ($form->load(Yii::$app->request->post())) { $this->applyProgressMediaInput($form, $animal); if ($form->save()) { $this->view->success(Yii::t('AnimalManagementModule.base', 'Progress update added.')); if ($isInline) { return $this->renderAjax('inline-edit-result', [ 'collapseId' => 'progress-add-inline', 'refreshSelectors' => ['#animal-progress-panel', '#animal-gallery-panel'], ]); } return $this->redirectToAnimalPage((int)$animal->id, $returnTo); } } $galleryItems = $animal->getGalleryItems()->orderBy(['id' => SORT_DESC])->limit(120)->all(); $params = [ 'space' => $this->contentContainer, 'animal' => $animal, 'model' => $form, 'returnTo' => $returnTo, 'galleryItems' => $galleryItems, 'isInline' => $isInline, ]; if ($isInline) { return $this->renderAjax('add-progress-update', $params); } return $this->render('add-progress-update', $params); } public function actionEditProgressUpdate(int $id, int $updateId) { $animal = $this->findAnimal($id); $update = $animal->getProgressUpdates()->andWhere(['id' => $updateId])->one(); $returnTo = (string)Yii::$app->request->post('returnTo', Yii::$app->request->get('returnTo', 'view')); $isInline = (int)Yii::$app->request->get('inline', 0) === 1; if (!$update instanceof \humhub\modules\animal_management\models\AnimalProgressUpdate) { throw new NotFoundHttpException('Progress update not found.'); } $form = new AnimalProgressUpdateForm(['animal' => $animal]); $form->setProgressUpdate($update); if ($form->load(Yii::$app->request->post())) { $this->applyProgressMediaInput($form, $animal); if ($form->save()) { $this->view->success(Yii::t('AnimalManagementModule.base', 'Progress update updated.')); if ($isInline) { return $this->renderAjax('inline-edit-result', [ 'collapseId' => 'progress-edit-inline-' . (int)$update->id, 'refreshSelectors' => ['#animal-progress-panel', '#animal-gallery-panel'], ]); } return $this->redirectToAnimalPage((int)$animal->id, $returnTo); } Yii::warning([ 'message' => 'Progress update save failed.', 'animal_id' => (int)$animal->id, 'progress_update_id' => (int)$update->id, 'is_inline' => $isInline, 'errors' => $form->getErrors(), ], 'animal_management.progress_media'); } elseif (Yii::$app->request->isPost) { Yii::warning([ 'message' => 'Progress update form load failed on POST.', 'animal_id' => (int)$animal->id, 'progress_update_id' => (int)$update->id, 'is_inline' => $isInline, 'post_keys' => array_keys((array)Yii::$app->request->post()), ], 'animal_management.progress_media'); } $galleryItems = $animal->getGalleryItems()->orderBy(['id' => SORT_DESC])->limit(120)->all(); $params = [ 'space' => $this->contentContainer, 'animal' => $animal, 'model' => $form, 'progressUpdate' => $update, 'returnTo' => $returnTo, 'galleryItems' => $galleryItems, 'isInline' => $isInline, ]; if ($isInline) { return $this->renderAjax('edit-progress-update', $params); } return $this->render('edit-progress-update', $params); } public function actionTransferRespond(int $id, string $decision) { $transfer = AnimalTransfer::findOne([ 'id' => $id, 'to_contentcontainer_id' => $this->contentContainer->contentcontainer_id, ]); if (!$transfer instanceof AnimalTransfer) { throw new NotFoundHttpException('Transfer request not found.'); } if ($transfer->status !== AnimalTransfer::STATUS_REQUESTED) { $this->view->error(Yii::t('AnimalManagementModule.base', 'This transfer can no longer be responded to.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/index')); } if ($decision === 'accept') { if ($transfer->markAccepted()) { $originator = Yii::$app->user->getIdentity(); TransferNotifier::notifyStatusChange($transfer, AnimalTransferEvent::EVENT_ACCEPTED, $originator); $this->view->success(Yii::t('AnimalManagementModule.base', 'Transfer request accepted.')); } else { $this->view->error(Yii::t('AnimalManagementModule.base', 'Could not accept transfer request.')); } } elseif ($decision === 'decline') { if ($transfer->markDeclined()) { $originator = Yii::$app->user->getIdentity(); TransferNotifier::notifyStatusChange($transfer, AnimalTransferEvent::EVENT_DECLINED, $originator); $animal = $transfer->animal; if ($animal instanceof Animal && $animal->status === Animal::STATUS_TRANSFER_PENDING) { $hasOpenTransfers = AnimalTransfer::find() ->where(['animal_id' => $animal->id, 'status' => [AnimalTransfer::STATUS_REQUESTED, AnimalTransfer::STATUS_ACCEPTED]]) ->exists(); if (!$hasOpenTransfers) { $animal->status = Animal::STATUS_ACTIVE; $animal->save(false, ['status', 'updated_at']); } } $this->view->success(Yii::t('AnimalManagementModule.base', 'Transfer request declined.')); } else { $this->view->error(Yii::t('AnimalManagementModule.base', 'Could not decline transfer request.')); } } else { $this->view->error(Yii::t('AnimalManagementModule.base', 'Unknown transfer decision.')); } return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/index')); } public function actionTransferComplete(int $id) { $transfer = AnimalTransfer::findOne([ 'id' => $id, 'to_contentcontainer_id' => $this->contentContainer->contentcontainer_id, 'status' => AnimalTransfer::STATUS_ACCEPTED, ]); if (!$transfer instanceof AnimalTransfer) { throw new NotFoundHttpException('Accepted transfer not found.'); } $animal = $transfer->animal; if (!$animal instanceof Animal) { throw new NotFoundHttpException('Animal not found.'); } $animal->contentcontainer_id = $this->contentContainer->contentcontainer_id; $animal->status = Animal::STATUS_ACTIVE; if (!$animal->save(false, ['contentcontainer_id', 'status', 'updated_at'])) { $this->view->error(Yii::t('AnimalManagementModule.base', 'Could not move animal ownership.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/index')); } if (!$transfer->markCompleted()) { $this->view->error(Yii::t('AnimalManagementModule.base', 'Could not complete transfer.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/index')); } $originator = Yii::$app->user->getIdentity(); TransferNotifier::notifyStatusChange($transfer, AnimalTransferEvent::EVENT_COMPLETED, $originator); $openTransfers = AnimalTransfer::find() ->where(['animal_id' => $animal->id]) ->andWhere(['status' => [AnimalTransfer::STATUS_REQUESTED, AnimalTransfer::STATUS_ACCEPTED]]) ->andWhere(['!=', 'id', $transfer->id]) ->all(); foreach ($openTransfers as $openTransfer) { if (!$openTransfer instanceof AnimalTransfer) { continue; } $openTransfer->markCancelled(Yii::t('AnimalManagementModule.base', 'Transfer cancelled because another transfer was completed.')); } $this->view->success(Yii::t('AnimalManagementModule.base', 'Transfer completed. Animal ownership moved to this rescue.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animal->id])); } public function actionTransferCancel(int $id) { $transfer = AnimalTransfer::findOne([ 'id' => $id, 'from_contentcontainer_id' => $this->contentContainer->contentcontainer_id, ]); if (!$transfer instanceof AnimalTransfer) { throw new NotFoundHttpException('Transfer request not found.'); } if (!in_array($transfer->status, [AnimalTransfer::STATUS_REQUESTED, AnimalTransfer::STATUS_ACCEPTED], true)) { $this->view->error(Yii::t('AnimalManagementModule.base', 'This transfer can no longer be cancelled.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/index')); } if (!$transfer->markCancelled(Yii::t('AnimalManagementModule.base', 'Transfer cancelled by source rescue.'))) { $this->view->error(Yii::t('AnimalManagementModule.base', 'Could not cancel transfer request.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/index')); } $originator = Yii::$app->user->getIdentity(); TransferNotifier::notifyStatusChange($transfer, AnimalTransferEvent::EVENT_CANCELLED, $originator); $animal = $transfer->animal; if ($animal instanceof Animal && $animal->status === Animal::STATUS_TRANSFER_PENDING) { $hasOpenTransfers = AnimalTransfer::find() ->where(['animal_id' => $animal->id, 'status' => [AnimalTransfer::STATUS_REQUESTED, AnimalTransfer::STATUS_ACCEPTED]]) ->exists(); if (!$hasOpenTransfers) { $animal->status = Animal::STATUS_ACTIVE; $animal->save(false, ['status', 'updated_at']); } } $this->view->success(Yii::t('AnimalManagementModule.base', 'Transfer request cancelled.')); return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/index')); } private function findAnimal(int $id): Animal { $animal = Animal::findOne(['id' => $id, 'contentcontainer_id' => $this->contentContainer->contentcontainer_id]); if (!$animal instanceof Animal) { throw new NotFoundHttpException('Animal not found.'); } return $animal; } private function collectAnimalLocalGalleryImagePaths(int $animalId): array { if (Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_item', true) === null) { return []; } $items = AnimalGalleryItem::find()->where(['animal_id' => $animalId])->all(); $paths = []; foreach ($items as $item) { $path = trim((string)$item->file_path); if ($path === '' || substr($path, 0, 1) !== '/') { continue; } $paths[$path] = true; } return array_keys($paths); } private function redirectToAnimalPage(int $animalId, string $returnTo): Response { if ($returnTo === 'medical-visits') { return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/medical-visits', ['id' => $animalId])); } if ($returnTo === 'progress-updates') { return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/progress-updates', ['id' => $animalId])); } return $this->redirect($this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animalId])); } private function canManageAnimals(): bool { if ($this->contentContainer->can(ManageAnimals::class)) { return true; } if ($this->contentContainer instanceof Space && $this->contentContainer->isAdmin()) { return true; } return false; } private function getTileFieldSettings(): array { $settings = Yii::$app->getModule('animal_management')->settings->contentContainer($this->contentContainer); $raw = $settings->get('tileFields', json_encode(DisplaySettingsForm::DEFAULT_TILE_FIELDS)); return $this->normalizeDisplayFields($raw, DisplaySettingsForm::DEFAULT_TILE_FIELDS); } private function getDetailHeroFieldSettings(): array { $settings = Yii::$app->getModule('animal_management')->settings->contentContainer($this->contentContainer); $raw = $settings->get('detailHeroFields', json_encode(DisplaySettingsForm::DEFAULT_DETAIL_FIELDS)); return $this->normalizeDisplayFields($raw, DisplaySettingsForm::DEFAULT_DETAIL_FIELDS); } private function normalizeDisplayFields($raw, array $default): array { if (is_string($raw)) { $decoded = json_decode($raw, true); if (!is_array($decoded)) { return $default; } $raw = $decoded; } if (!is_array($raw)) { return $default; } $allowed = array_keys(DisplaySettingsForm::fieldOptions()); $normalized = []; foreach ($raw as $field) { $field = trim((string)$field); if ($field === '' || !in_array($field, $allowed, true)) { continue; } if (!in_array($field, $normalized, true)) { $normalized[] = $field; } } return !empty($normalized) ? $normalized : $default; } private function resolveAnimalImageUrls(array $animalIds, array $imageFieldOrder = [], bool $allowGalleryFallback = false): array { $animalIds = array_values(array_unique(array_map('intval', $animalIds))); if (empty($animalIds)) { return []; } if (!class_exists(RescueFieldDefinition::class) || Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null || Yii::$app->db->schema->getTableSchema('rescue_animal_field_value', true) === null ) { return []; } if (empty($imageFieldOrder)) { $imageFieldOrder = ['cover_image_url', 'profile_image_url', 'photo_url', 'image_url', 'profile_image', 'photo']; } $definitions = RescueFieldDefinition::find() ->select(['id', 'field_key']) ->where([ 'module_id' => 'animal_management', 'group_key' => 'animal_profile', 'field_key' => $imageFieldOrder, 'is_active' => 1, ]) ->all(); if (empty($definitions)) { return []; } $definitionPriority = []; foreach ($definitions as $definition) { $priority = array_search((string)$definition->field_key, $imageFieldOrder, true); if ($priority === false) { $priority = 999; } $definitionPriority[(int)$definition->id] = (int)$priority; } if (empty($definitionPriority)) { return []; } $valueRows = AnimalFieldValue::find() ->where(['animal_id' => $animalIds, 'field_definition_id' => array_keys($definitionPriority)]) ->all(); $imageUrls = []; $chosenPriorityByAnimal = []; foreach ($valueRows as $valueRow) { $animalId = (int)$valueRow->animal_id; $valueText = trim((string)$valueRow->value_text); if ($valueText === '') { continue; } $priority = $definitionPriority[(int)$valueRow->field_definition_id] ?? 999; if (!isset($chosenPriorityByAnimal[$animalId]) || $priority < $chosenPriorityByAnimal[$animalId]) { $chosenPriorityByAnimal[$animalId] = $priority; $imageUrls[$animalId] = $valueText; } } $missingAnimalIds = []; foreach ($animalIds as $animalId) { if (!isset($imageUrls[$animalId])) { $missingAnimalIds[] = (int)$animalId; } } if ($allowGalleryFallback && !empty($missingAnimalIds) && Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_item', true) !== null) { $galleryItems = AnimalGalleryItem::find() ->where(['animal_id' => $missingAnimalIds]) ->orderBy(['animal_id' => SORT_ASC, 'id' => SORT_DESC]) ->all(); foreach ($galleryItems as $galleryItem) { $animalId = (int)$galleryItem->animal_id; if (isset($imageUrls[$animalId])) { continue; } $url = trim((string)$galleryItem->getImageUrl()); if ($url === '') { continue; } $imageUrls[$animalId] = $url; } } return $imageUrls; } private function resolveDisplayFieldOverrides(array $animalIds, string $fieldKey): array { $animalIds = array_values(array_unique(array_map('intval', $animalIds))); if (empty($animalIds)) { return []; } if (!class_exists(RescueFieldDefinition::class) || Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null || Yii::$app->db->schema->getTableSchema('rescue_animal_field_value', true) === null ) { return []; } $definition = RescueFieldDefinition::findOne([ 'module_id' => 'animal_management', 'group_key' => 'animal_profile', 'field_key' => $fieldKey, ]); if (!$definition instanceof RescueFieldDefinition) { return []; } $rows = AnimalFieldValue::find() ->where(['animal_id' => $animalIds, 'field_definition_id' => (int)$definition->id]) ->all(); $allowed = array_keys(DisplaySettingsForm::fieldOptions()); $result = []; foreach ($rows as $row) { $raw = trim((string)$row->value_text); if ($raw === '') { continue; } $decoded = json_decode($raw, true); if (!is_array($decoded)) { $decoded = array_map('trim', explode(',', $raw)); } $normalized = []; foreach ($decoded as $field) { $field = trim((string)$field); if ($field === '' || !in_array($field, $allowed, true)) { continue; } if (!in_array($field, $normalized, true)) { $normalized[] = $field; } } if (!empty($normalized)) { $result[(int)$row->animal_id] = $normalized; } } return $result; } private function storeGalleryUpload(Animal $animal, UploadedFile $file): ?string { $random = Yii::$app->security->generateRandomString(8); $extension = strtolower((string)$file->extension); $fileName = 'gallery-' . time() . '-' . $random . '.' . $extension; $candidateDirs = [ '/uploads/animal-management/gallery/' . (int)$animal->id, '/uploads/animal-management-runtime/gallery/' . (int)$animal->id, '/uploads/animal-media/gallery/' . (int)$animal->id, '/assets/animal-management-runtime/gallery/' . (int)$animal->id, ]; foreach ($candidateDirs as $relativeDir) { $absoluteDir = Yii::getAlias('@webroot') . $relativeDir; try { FileHelper::createDirectory($absoluteDir, 0775, true); } catch (\Throwable $e) { Yii::warning([ 'message' => $e->getMessage(), 'relative_dir' => $relativeDir, 'absolute_dir' => $absoluteDir, ], 'animal_management.gallery_upload_dir'); continue; } if (!is_dir($absoluteDir) || !is_writable($absoluteDir)) { continue; } $absolutePath = $absoluteDir . '/' . $fileName; if ($file->saveAs($absolutePath)) { return $relativeDir . '/' . $fileName; } } Yii::warning([ 'message' => 'Could not save uploaded gallery file in any candidate directory.', 'animal_id' => (int)$animal->id, 'file_name' => $fileName, 'candidate_dirs' => $candidateDirs, ], 'animal_management.gallery_upload_dir'); return null; } private function computeImageContentHash(string $pathOrUrl): ?string { $pathOrUrl = trim($pathOrUrl); if ($pathOrUrl === '') { return null; } $absolutePath = $pathOrUrl; if (substr($absolutePath, 0, 1) === '/') { $absolutePath = Yii::getAlias('@webroot') . $absolutePath; } if (!is_file($absolutePath) || !is_readable($absolutePath)) { return null; } $hash = @hash_file('sha1', $absolutePath); return is_string($hash) && $hash !== '' ? $hash : null; } private function syncPrimaryImagesToGallery(Animal $animal): void { if (Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null || Yii::$app->db->schema->getTableSchema('rescue_animal_field_value', true) === null || Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_item', true) === null ) { return; } $definitions = RescueFieldDefinition::find() ->select(['id', 'field_key']) ->where([ 'module_id' => 'animal_management', 'group_key' => 'animal_profile', 'field_key' => ['cover_image_url', 'profile_image_url'], ]) ->all(); if (empty($definitions)) { return; } $byDefinitionId = []; foreach ($definitions as $definition) { $byDefinitionId[(int)$definition->id] = (string)$definition->field_key; } $fieldRows = AnimalFieldValue::find() ->where([ 'animal_id' => (int)$animal->id, 'field_definition_id' => array_keys($byDefinitionId), ]) ->all(); if (empty($fieldRows)) { return; } $existingItems = AnimalGalleryItem::find()->where(['animal_id' => (int)$animal->id])->all(); $existingByPath = []; $existingHashes = []; foreach ($existingItems as $existingItem) { $path = trim((string)$existingItem->file_path); if ($path !== '') { $existingByPath[$path] = true; } $url = trim((string)$existingItem->getImageUrl()); if ($url !== '') { $existingByPath[$url] = true; } $hash = $this->computeImageContentHash($url !== '' ? $url : $path); if ($hash !== null) { $existingHashes[$hash] = true; } } foreach ($fieldRows as $fieldRow) { $imagePath = trim((string)$fieldRow->value_text); if ($imagePath === '') { continue; } if (!$this->isResolvableImageUrl($imagePath)) { continue; } if (isset($existingByPath[$imagePath])) { continue; } $hash = $this->computeImageContentHash($imagePath); if ($hash !== null && isset($existingHashes[$hash])) { continue; } $fieldKey = $byDefinitionId[(int)$fieldRow->field_definition_id] ?? 'upload'; $item = new AnimalGalleryItem(); $item->animal_id = (int)$animal->id; $item->file_path = $imagePath; $item->source_type = $fieldKey === 'cover_image_url' ? 'cover' : 'profile'; $item->created_by = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id; if ($item->save()) { $existingByPath[$imagePath] = true; if ($hash !== null) { $existingHashes[$hash] = true; } } else { Yii::warning($item->getErrors(), 'animal_management.primary_image_gallery_sync'); } } } private function isPrimaryImagePathReferenced(int $animalId, string $path): bool { if (Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null || Yii::$app->db->schema->getTableSchema('rescue_animal_field_value', true) === null ) { return false; } $definitionIds = RescueFieldDefinition::find() ->select('id') ->where([ 'module_id' => 'animal_management', 'group_key' => 'animal_profile', 'field_key' => ['cover_image_url', 'profile_image_url'], ]) ->column(); if (empty($definitionIds)) { return false; } return AnimalFieldValue::find() ->where([ 'animal_id' => $animalId, 'field_definition_id' => $definitionIds, 'value_text' => $path, ]) ->exists(); } private function isResolvableImageUrl(string $pathOrUrl): bool { $pathOrUrl = trim($pathOrUrl); if ($pathOrUrl === '') { return false; } if (preg_match('/^https?:\/\//i', $pathOrUrl)) { return true; } if (substr($pathOrUrl, 0, 1) !== '/') { return false; } $absolutePath = Yii::getAlias('@webroot') . $pathOrUrl; return is_file($absolutePath) && is_readable($absolutePath); } private function applyProgressMediaInput(AnimalProgressUpdateForm $form, Animal $animal): void { $removeProgressMedia = (int)Yii::$app->request->post('removeProgressMedia', 0) === 1; if ($removeProgressMedia) { $form->customFields['media_reference'] = ''; return; } $selectedGalleryPath = trim((string)Yii::$app->request->post('progressMediaGalleryPath', '')); if ($selectedGalleryPath === '') { $selectedGalleryPath = trim((string)Yii::$app->request->post('AnimalProgressUpdateForm.progressMediaGalleryPath', '')); } if ($selectedGalleryPath === '') { $formPayload = Yii::$app->request->post('AnimalProgressUpdateForm', []); if (is_array($formPayload) && isset($formPayload['progressMediaGalleryPath'])) { $selectedGalleryPath = trim((string)$formPayload['progressMediaGalleryPath']); } } $uploadedMedia = UploadedFile::getInstanceByName('progressMediaUpload'); if (!$uploadedMedia instanceof UploadedFile) { $uploadedMedia = UploadedFile::getInstanceByName('AnimalProgressUpdateForm[progressMediaUpload]'); } if (!$uploadedMedia instanceof UploadedFile) { $uploadedCandidates = UploadedFile::getInstancesByName('progressMediaUpload'); if (!empty($uploadedCandidates) && $uploadedCandidates[0] instanceof UploadedFile) { $uploadedMedia = $uploadedCandidates[0]; } } if ($uploadedMedia instanceof UploadedFile) { $allowedExtensions = array_map('strtolower', UploadStandards::imageExtensions()); $allowedMimeTypes = array_map('strtolower', UploadStandards::imageMimeTypes()); $maxBytes = (int)UploadStandards::IMAGE_MAX_BYTES; $extension = strtolower((string)$uploadedMedia->extension); $mimeType = strtolower((string)$uploadedMedia->type); if (!in_array($extension, $allowedExtensions, true) || ($mimeType !== '' && !in_array($mimeType, $allowedMimeTypes, true)) || $uploadedMedia->size > $maxBytes ) { $form->addError('customFields', Yii::t('AnimalManagementModule.base', 'Progress media image must be a valid image within size limits.')); return; } $storedPath = $this->storeGalleryUpload($animal, $uploadedMedia); if ($storedPath === null) { Yii::warning([ 'message' => 'storeGalleryUpload returned null for progress media.', 'animal_id' => (int)$animal->id, 'progress_update_id' => $form->progressUpdate instanceof AnimalProgressUpdate ? (int)$form->progressUpdate->id : null, 'uploaded_media_name' => (string)$uploadedMedia->name, ], 'animal_management.progress_media'); $form->addError('customFields', Yii::t('AnimalManagementModule.base', 'Could not upload progress media image.')); return; } $this->addImagePathToGalleryIfMissing($animal, $storedPath, 'progress'); $form->customFields['media_reference'] = $storedPath; return; } if ( $selectedGalleryPath !== '' && (preg_match('/^https?:\/\//i', $selectedGalleryPath) || substr($selectedGalleryPath, 0, 1) === '/') ) { $this->addImagePathToGalleryIfMissing($animal, $selectedGalleryPath, 'progress'); $form->customFields['media_reference'] = $selectedGalleryPath; return; } // Preserve existing media reference when no explicit media action was posted. if ($form->progressUpdate instanceof AnimalProgressUpdate) { $existingMedia = $this->resolveProgressMediaReferenceFromRecord($form->progressUpdate); if ($existingMedia !== '') { $form->customFields['media_reference'] = $existingMedia; } } } private function resolveProgressMediaReferenceFromRecord(AnimalProgressUpdate $record): string { foreach ($record->getCustomFieldDisplayValues(true) as $customField) { if ((string)($customField['field_key'] ?? '') !== 'media_reference') { continue; } $value = trim((string)($customField['value'] ?? '')); if ($value !== '') { return $value; } } return ''; } private function applyMedicalMediaInput(AnimalMedicalVisitForm $form, Animal $animal): void { if ((int)Yii::$app->request->post('removeMedicalMedia', 0) === 1) { $form->customFields['medical_media_reference'] = ''; $form->customFields['media_reference'] = ''; return; } $selectedGalleryPath = trim((string)Yii::$app->request->post('medicalMediaGalleryPath', '')); $uploadedMedia = UploadedFile::getInstanceByName('medicalMediaUpload'); if ($uploadedMedia instanceof UploadedFile) { $allowedExtensions = array_map('strtolower', UploadStandards::imageExtensions()); $allowedMimeTypes = array_map('strtolower', UploadStandards::imageMimeTypes()); $maxBytes = (int)UploadStandards::IMAGE_MAX_BYTES; $extension = strtolower((string)$uploadedMedia->extension); $mimeType = strtolower((string)$uploadedMedia->type); if (!in_array($extension, $allowedExtensions, true) || ($mimeType !== '' && !in_array($mimeType, $allowedMimeTypes, true)) || $uploadedMedia->size > $maxBytes ) { $form->addError('customFields', Yii::t('AnimalManagementModule.base', 'Medical media image must be a valid image within size limits.')); return; } $storedPath = $this->storeGalleryUpload($animal, $uploadedMedia); if ($storedPath === null) { $form->addError('customFields', Yii::t('AnimalManagementModule.base', 'Could not upload medical media image.')); return; } $this->addImagePathToGalleryIfMissing($animal, $storedPath, 'medical'); $form->customFields['medical_media_reference'] = $storedPath; $form->customFields['media_reference'] = $storedPath; return; } if ( $selectedGalleryPath !== '' && (preg_match('/^https?:\/\//i', $selectedGalleryPath) || substr($selectedGalleryPath, 0, 1) === '/') ) { $this->addImagePathToGalleryIfMissing($animal, $selectedGalleryPath, 'medical'); $form->customFields['medical_media_reference'] = $selectedGalleryPath; $form->customFields['media_reference'] = $selectedGalleryPath; } } private function ensureMedicalMediaFieldDefinition(): void { if (!class_exists(RescueFieldDefinition::class) || Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null ) { return; } $definition = RescueFieldDefinition::findOne([ 'module_id' => 'animal_management', 'field_key' => 'medical_media_reference', ]); if ($definition instanceof RescueFieldDefinition) { $needsSave = false; if ((string)$definition->group_key !== 'animal_medical_visit') { $definition->group_key = 'animal_medical_visit'; $needsSave = true; } if ((int)$definition->is_active !== 1) { $definition->is_active = 1; $needsSave = true; } if ((string)$definition->visibility !== 'public') { $definition->visibility = 'public'; $needsSave = true; } if ((string)$definition->label === '') { $definition->label = 'Media'; $needsSave = true; } if ($needsSave) { $definition->updated_at = date('Y-m-d H:i:s'); $definition->save(); } return; } $definition = new RescueFieldDefinition(); $definition->module_id = 'animal_management'; $definition->group_key = 'animal_medical_visit'; $definition->field_key = 'medical_media_reference'; $definition->label = 'Media'; $definition->input_type = 'text'; $definition->required = 0; $definition->is_core = 1; $definition->is_active = 1; $definition->visibility = 'public'; $definition->options = '{}'; $definition->sort_order = 220; $definition->created_at = date('Y-m-d H:i:s'); $definition->updated_at = date('Y-m-d H:i:s'); $definition->save(); } private function addImagePathToGalleryIfMissing(Animal $animal, string $imagePath, string $sourceType = 'upload'): void { $imagePath = trim($imagePath); if ($imagePath === '') { return; } $existingItems = AnimalGalleryItem::find()->where(['animal_id' => (int)$animal->id])->all(); $incomingHash = $this->computeImageContentHash($imagePath); foreach ($existingItems as $existingItem) { $existingPath = trim((string)$existingItem->file_path); $existingUrl = trim((string)$existingItem->getImageUrl()); if ($existingPath === $imagePath || $existingUrl === $imagePath) { return; } if ($incomingHash !== null) { $existingHash = $this->computeImageContentHash($existingUrl !== '' ? $existingUrl : $existingPath); if ($existingHash !== null && hash_equals($incomingHash, $existingHash)) { return; } } } if (substr($imagePath, 0, 1) !== '/') { return; } $item = new AnimalGalleryItem(); $item->animal_id = (int)$animal->id; $item->file_path = $imagePath; $item->source_type = $sourceType; $item->created_by = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id; $item->save(); } }