Initial import of animal_management module

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

View File

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