registries and registry filters

master
Константин 2023-07-24 22:15:52 +03:00
parent 8a559bb0f9
commit 9f0f5cee3e
12 changed files with 310 additions and 51 deletions

View File

@ -20,10 +20,10 @@ class EntriesController extends Controller {
public function index(Request $request): JsonResponse {
$filters = collect($request->has('filters') ? json_decode($request->get('filters'), true) : [])->filter(function($val) {return $val;});
$registry = Registry::byUuid($request->get('registry'))->first();
$category = Category::byUuid($request->get('category'))->first();
$query = $this->model->query()->where(['registry_id' => $registry->id ?? 0]);
if ($filters->isEmpty()) $query->where(['category_id' => $category->id ?? 0]);
//$registry = Registry::byUuid($request->get('registry'))->first();
//$category = Category::byUuid($request->get('category'))->first();
$query = $this->model->query();
//if ($filters->except('registry')->isEmpty()) $query->where(['category_id' => $category->id ?? 0]);
$service = FiltersService::getService('registryEntries');
$service->applyFilters($query, $filters);
$paginator = $query->paginate(config('app.pagination_limit'));

View File

@ -178,9 +178,9 @@ class Field extends Model {
public function applyOrder(Builder $query, $dir) {
if ($table = FieldType::TABLES[$this->type] ?? null) {
$query->leftJoin("{$table} as {$this->name}", function(JoinClause $join) use($table) {
$join->on('objects.id', '=', "{$this->name}.object_id");
})->where(["{$this->name}.field_id" => $this->id])->orderBy("{$this->name}.value", $dir);
$query->leftJoin("{$table} as prop-{$this->name}", function(JoinClause $join) use($table) {
$join->on('objects.id', '=', "prop-{$this->name}.object_id");
})->where(["prop-{$this->name}.field_id" => $this->id])->orderBy("prop-{$this->name}.value", $dir);
}
}

View File

@ -11,6 +11,7 @@ use App\Models\Objects\Values\IntegerValue;
use App\Models\Objects\Values\RelationValue;
use App\Models\Objects\Values\StringValue;
use App\Models\Objects\Values\TextValue;
use App\Models\Registries\Entry;
use App\Models\User;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Builder;
@ -41,6 +42,10 @@ class NirObject extends Model {
];
public function entries(): MorphToMany {
return $this->morphedByMany(Entry::class, 'objectable');
}
public function objects(): MorphToMany {
return $this->morphToMany(NirObject::class, 'objectable');
}

View File

@ -6,6 +6,7 @@ use App\Models\Asset;
use App\Support\HasObjectsTrait;
use App\Support\RelationValuesTrait;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
@ -56,10 +57,54 @@ class Entry extends Model {
}
public function operations(): MorphToMany {
return $this->objectsByGroup('operations')->reorder()->applyOrders(['order-date' => 'desc']);
return $this->objectsByGroup('operations');
}
public function scopeByStates($query, $states) {
$query->where(function($query) use($states) {
collect($states)->each(function($state) use($query) {
$query->orWhere(function($query) use($state) {
$query->$state();
});
});
});
}
public function scopeAwaiting($query) {
$query->where('active_since', '>', now());
}
public function scopeActive($query) {
$query->where('active_since', '<=', now())->where(function($query) {
$query->whereNull('active_till')->orWhere('active_till', '>', now());
})->notCancelled()->notSuspended();
}
public function scopeExpired($query) {
$query->where('active_till', '<=', now());
}
public function scopeSuspended($query) {
$query->where('suspended_since', '<=', now())->where(function($query) {
$query->whereNull('suspended_till')->orWhere('suspended_till', '>', now());
})->notCancelled();
}
public function scopeNotSuspended($query) {
$query->where(function($query) {
$query->whereNull('suspended_since')->orWhere('suspended_since', '>', now());
})->where(function($query) {
$query->whereNull('suspended_till')->orWhere('suspended_till', '<', now());
});
}
public function scopeCancelled($query) {
$query->where('cancelled_at', '<=', now());
}
public function scopeNotCancelled($query) {
$query->where(function($query) {
$query->whereNull('cancelled_at')->orWhere('cancelled_at', '>', now());
});
}
public function getPropertiesAttribute(): ?Model {
return ($type = $this->registry->parsedType['options']['properties'] ?? null) ? $this->getObject($type, 'properties') : null;
}
@ -76,5 +121,12 @@ class Entry extends Model {
}
public function sortOperations() {
$this->operations()->reorder()->applyOrders(['order-date' => 'desc'])->get()->each(function($operation, $ord) {
$this->objects()->updateExistingPivot($operation, ['ord' => $ord]);
});
}
}

View File

@ -4,11 +4,17 @@ namespace App\Models\Registries;
class RegistryType {
public const SIMPLE = 'simple';
public const CATEGORIZED = 'categorized';
public const RULESET = 'ruleset';
public const LABORATORIES = 'laboratories';
public const CERTIFIERS = 'certifiers';
public const EXPERTS = 'experts';
public const CERTIFICATES = 'certificates';
public const COMPANIES = 'companies';
public const DEVELOPMENTS = 'developments';
public const DISCUSSIONS = 'discussions';
public const RESEARCHES = 'researches';
public const TECHNICAL_CERTIFICATES = 'technical-certificates';
public const TITLES = [
self::SIMPLE => 'Простой реестр',
@ -16,15 +22,26 @@ class RegistryType {
self::LABORATORIES => 'Реестр испытательных лабораторий',
self::CERTIFIERS => 'Реестр органов по сертификации',
self::EXPERTS => 'Реестр экспертов',
self::CERTIFICATES => 'Реестр сертификатов соответствия'
self::CERTIFICATES => 'Реестр сертификатов соответствия',
self::COMPANIES => 'Реестр организаций',
self::DEVELOPMENTS => 'Реестр планов разработки',
self::DISCUSSIONS => 'Реестр публичных обсуждений',
self::RESEARCHES => 'Реестр исследований',
self::TECHNICAL_CERTIFICATES => 'Реестр технических свидетельств'
];
public const OPTIONS = [
self::SIMPLE => [],
self::CATEGORIZED => ['categories' => true],
self::RULESET => ['categories' => true, 'operations' => 'entry-operation-ruleset', 'states' => true],
self::LABORATORIES => ['properties' => 'entry-properties-laboratory', 'states' => true],
self::CERTIFIERS => ['properties' => 'entry-properties-certifier', 'states' => true],
self::EXPERTS => ['categories' => true, 'properties' => 'entry-properties-expert', 'states' => true],
self::CERTIFICATES => ['categories' => true, 'properties' => 'entry-properties-certificate', 'states' => true]
self::EXPERTS => ['categories' => true, 'states' => true],
self::CERTIFICATES => ['categories' => true, 'properties' => 'entry-properties-certificate', 'states' => true],
self::COMPANIES => ['properties' => 'entry-properties-company'],
self::DEVELOPMENTS => ['categorized' => true, 'properties' => 'entry-properties-development'],
self::DISCUSSIONS => ['properties' => 'entry-properties-discussion'],
self::RESEARCHES => ['categories' => true, 'properties' => 'entry-properties-research'],
self::TECHNICAL_CERTIFICATES => ['properties' => 'entry-properties-technical-certificate', 'states' => true]
];
}

View File

@ -3,17 +3,30 @@
namespace App\Services\Filters\Registries;
use App\Models\Dictionaries\Dictionary;
use App\Models\Dictionaries\DictionaryItem;
use App\Models\Objects\Field;
use App\Models\Objects\FieldType;
use App\Models\Objects\ObjectType;
use App\Models\Registries\Category;
use App\Models\Registries\Entry;
use App\Models\Registries\EntryState;
use App\Models\Registries\Registry;
use App\Services\Filters\FiltersService;
use App\Transformers\Dictionaries\DictionaryItemTransformer;
use App\Transformers\Objects\FieldTransformer;
use App\Transformers\Objects\ObjectTypeTransformer;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Spatie\Fractal\Fractal;
class EntryFilters extends FiltersService {
public string $objectRelationName = 'entries';
public function get(Collection $filters): array {
$registry = Registry::byUuid($filters->get('registry'))->firstOrFail();
$types = [];
if ($v = ObjectType::byName($registry->options['operations'] ?? null)->first()) $types[] = $v->uuid;
if ($v = ObjectType::byName($registry->options['properties'] ?? null)->first()) $types[] = $v->uuid;
if ($types) $filters->put('types', $types);
$groups = [
[
'name' => 'common',
@ -21,29 +34,43 @@ class EntryFilters extends FiltersService {
'fields' => $this->nativeFields($filters)
]
];
if ($types) $groups[] = ['name' => 'properties', 'title' => 'Дополнительные характеристики', 'fields' => $this->objectFields($filters)];
//if ($v = $registry->options['properties'] ?? null) $groups[] = ['name' => 'properties', 'title' => 'Особые характеристики', 'fields' => $this->propertiesFields($v, $filters)];
$query = Entry::query();
$this->applyFilters($query, $filters);
$this->applyFilters($query, $filters->put('fake', 'zzz'));
return ['groups' => ['data' => $groups], 'total' => $query->count()];
}
public function nativeFields(Collection $filters): array {
return [
[
'name' => 'listings',
'title' => 'Вхождение в перечень ПП',
'name' => 'registry',
'title' => 'Реестр',
'type' => FieldType::STRING,
'hidden' => true,
'value' => $filters->get('registry')
],
[
'name' => 'types',
'title' => 'Типы связанных объектов',
'type' => FieldType::RELATION,
'represented' => $this->getListings($filters),
'value' => ($val = $filters->get('listings')) ? fractal(DictionaryItem::byUuids($val)->get(), new DictionaryItemTransformer()) : null
]
'hidden' => true,
'value' => $filters->get('types') ? fractal(ObjectType::whereIn('uuid', $filters->get('types'))->get(), new ObjectTypeTransformer()) : null
],
[
'name' => 'state',
'title' => 'Статус',
'type' => FieldType::RELATION,
'represented' => $this->getRelationItems(EntryState::TITLES),
'value' => $this->getRelationValue($filters->get('state'), EntryState::TITLES)
],
];
}
public function operationsFields(Collection $filters): array {
return [];
}
public function propertiesFilters(Collection $filters): array {
return [];
public function objectFields(Collection $filters): array {
return fractal(Field::query()->whereHas('groups.objectType', function($query) use($filters) {
$query->whereIn('uuid', $filters->get('types') ?? []);
})->whereIn('type', [FieldType::RELATION, FieldType::DATE, FieldType::STRING])->get(), new FieldTransformer($filters->all(), $this))->toArray();
}
@ -53,7 +80,9 @@ class EntryFilters extends FiltersService {
public function applyFilters(Builder $query, Collection $filters) {
$this->applyCategoryFilter($query, $filters);
$this->applyNativeFilters($query, $filters);
$this->applyObjectFilters($query, $filters);
$this->applyPermissionsFilters($query);
}
@ -66,14 +95,37 @@ class EntryFilters extends FiltersService {
public function applyNativeFilter(Builder $query, $prop, $value) {
if ($value) {
if ($prop === 'search') $this->applySearchFilter($query, $value, ['name', 'number']);
elseif ($prop === 'registry') $this->applyRelationFilter($query, 'registry', $value);
elseif ($prop === 'state') $query->byStates($value);
}
}
public function applyObjectFilters(Builder $query, Collection $filters) {
if ($filters->get('types') && !$this->isFiltersEmpty($filters, true)) $query->whereHas('objects', function($query) use($filters) {
Field::applyFilters($query, $filters);
});
}
public function applyCategoryFilter(Builder $query, Collection $filters) {
if ($this->isFiltersEmpty($filters)) $query->where(['category_id' => Category::byUuid($filters->get('category'))->first()->id ?? 0]);
}
public function applyPermissionsFilters(Builder $query) {
}
public function isFiltersEmpty(Collection $filters, $exceptNative = false): bool {
if ($exceptNative) $filters = $filters->filter(function($val, $prop) {
return Field::byName($prop)->exists();
});
return $filters->except('registry', 'category', 'types', 'state')->filter(function($val) {
return collect($val)->filter(function($val) {return $val;})->isNotEmpty();
})->isEmpty();
}
}

View File

@ -35,12 +35,14 @@ class OperationForms extends FormsService {
$entry = Entry::byUuid($data['entry'] ?? null)->firstOrFail();
$model = $entry->createObject($entry->registry->options['operations'] ?? null, null, 'operations');
$model->setValues($data);
$entry->sortOperations();
return fractal($model, new ObjectTransformer())->respond();
}
public function update(string $id, array $data): ?JsonResponse {
$model = NirObject::byUuid($id)->firstOrFail();
$model->setValues($data);
$model->entries()->first()->sortOperations();
return fractal($model->fresh(), new ObjectTransformer())->respond();
}
}

View File

@ -32,6 +32,14 @@ class DictionariesTableSeeder extends Seeder {
'listings' => [
'title' => 'Перечни ПП',
'items' => ['pp1521' => 'ПП №1521 от 26.12.2014 г.', 'pp985' => 'ПП № 985 от 04.07.2020 г.', 'pp815' => 'ПП № 815 от 28.05.2021 г.']
],
'activities' => [
'title' => 'Направления деятельности',
'items' => ['products' => 'Продукция', 'services' => 'Работы и услуги', 'management' => 'СМК']
],
'research-types' => [
'title' => 'Виды исследовательских работ',
'items' => ['nir' => 'НИР', 'niokr' => 'НИОКР']
]
];

View File

@ -169,25 +169,122 @@ class FieldsTableSeeder extends Seeder {
'required' => true,
],
'laboratory-name' => [
'title' => 'Наименование лаборатории',
'company-name' => [
'title' => 'Наименование организации',
'type' => FieldType::STRING,
'required' => true
],
'certifier-name' => [
'title' => 'Наименование органа по сертификации',
'activities' => [
'title' => 'Виды деятельности',
'type' => FieldType::RELATION,
'required' => true,
'multiple' => true,
'params' => [
'related' => DictionaryItem::class, 'transformer' => DictionaryItemTransformer::class,
'options' => ['show' => true, 'whereHas' => ['dictionary' => ['name' => 'activities']]],
]
],
'applicant-name' => [
'title' => 'Заявитель',
'type' => FieldType::STRING,
'required' => true
],
'expert-name' => [
'title' => 'ФИО эксперта',
'type' => FieldType::STRING,
'applicant-address' => [
'title' => 'Адрес заявителя',
'type' => FieldType::STRING
],
'applicant-email' => [
'title' => 'Электронная почта заявителя',
'type' => FieldType::STRING
],
'applicant-phone' => [
'title' => 'Телефон заявителя',
'type' => FieldType::STRING
],
'producer-name' => [
'title' => 'Производитель',
'type' => FieldType::STRING
],
'producer-address' => [
'title' => 'Адрес производителя',
'type' => FieldType::STRING
],
'producer-email' => [
'title' => 'Электронная почта производителя',
'type' => FieldType::STRING
],
'producer-phone' => [
'title' => 'Телефон производителя',
'type' => FieldType::STRING
],
'company-address' => [
'title' => 'Адрес',
'type' => FieldType::STRING
],
'company-site' => [
'title' => 'Сайт',
'type' => FieldType::STRING
],
'company-email' => [
'title' => 'Электронная почта',
'type' => FieldType::STRING
],
'company-phone' => [
'title' => 'Телефон',
'type' => FieldType::STRING
],
'primary-developer' => [
'title'=> 'Основной исполнитель',
'type' => FieldType::STRING
],
'funding-source' => [
'title' => 'Источник финансирования',
'type' => FieldType::STRING
],
'plan-year' => [
'title' => 'Год плана',
'type' => FieldType::INTEGER
],
'discussion-start-date' => [
'title' => 'Дата начала обсуждения',
'type' => FieldType::DATE
],
'discussion-finish-date' => [
'title' => 'Дата окончания обсуждения',
'type' => FieldType::DATE
],
'research-type' => [
'title' => 'Вид работы',
'type' => FieldType::RELATION,
'required' => true,
'params' => [
'appearance' => 'radio',
'related' => DictionaryItem::class, 'transformer' => DictionaryItemTransformer::class,
'options' => ['show' => true, 'whereHas' => ['dictionary' => ['name' => 'research-types']]]
]
],
'research-objective' => [
'title' => 'Цель исследования',
'type' => FieldType::TEXT
],
'technical-conclusion' => [
'title' => 'Техническое заключение',
'type' => FieldType::DOCUMENT,
'required' => true
],
'certificate-number' => [
'title' => 'Номер сертификата',
'type' => FieldType::STRING,
'required' => true
'developer-name' => [
'title' => 'Разработчик',
'type' => FieldType::STRING
],
'developer-address' => [
'title' => 'Адрес разработчика',
'type' => FieldType::STRING
],
'operation-type' => [

View File

@ -77,23 +77,34 @@ class ObjectTypeFieldsTableSeeder extends Seeder {
],
'entry-properties-laboratory' => [
'common' => [
'fields' => ['laboratory-name']
]
'common' => ['fields' => ['company-name', 'activities']]
],
'entry-properties-certifier' => [
'common' => [
'fields' => ['certifier-name']
]
'common' => ['fields' => ['company-name', 'activities']]
],
'entry-properties-expert' => [
'common' => [
'fields' => ['expert-name']
]
'common' => ['fields' => []]
],
'entry-properties-certificate' => [
'common' => [
'fields' => ['certificate-number']
'fields' => ['applicant-name', 'applicant-address', 'applicant-email', 'applicant-phone', 'producer-name', 'producer-address', 'producer-email', 'producer-phone']
]
],
'entry-properties-company' => [
'common' => ['fields' => ['company-address', 'company-email', 'company-phone']]
],
'entry-properties-development' => [
'common' => ['fields' => ['operation-type', 'primary-developer', 'funding-source', 'plan-year']]
],
'entry-properties-discussion' => [
'common' => ['fields' => ['discussion-start-date', 'discussion-finish-date', 'funding-source']]
],
'entry-properties-research' => [
'common' => ['fields' => ['research-type', 'research-objective', 'plan-year']]
],
'entry-properties-technical-certificate' => [
'common' => [
'fields' => ['technical-conclusion', 'developer-name', 'developer-address', 'company-site', 'company-email', 'company-phone', 'producer-name', 'producer-address']
]
],

View File

@ -73,6 +73,21 @@ class ObjectTypesTableSeeder extends Seeder {
],
'entry-properties-certificate' => [
'title' => 'Сертификат соответствия'
],
'entry-properties-company' => [
'title' => 'Организация'
],
'entry-properties-development' => [
'title' => 'Разработка'
],
'entry-properties-discussion' => [
'title' => 'Обсуждение'
],
'entry-properties-research' => [
'title' => 'Исследование'
],
'entry-properties-technical-certificate' => [
'title' => 'Техническое свидетельство'
]
]
],

View File

@ -37,8 +37,8 @@ class PagesTableSeeder extends Seeder
'Закупки' => [],
'Противодействие коррупции' => [
'children' => [
'ФЗ, указы, постановления' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::SIMPLE],
'Ведомственные нормативные правовые акты' => [],
'ФЗ, указы, постановления' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::CATEGORIZED],
'Ведомственные нормативные правовые акты' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::SIMPLE],
'Внутренние нормативные документы' => [],
'Антикоррупционная экспертиза' => [],
'Методические материалы' => [],
@ -56,15 +56,15 @@ class PagesTableSeeder extends Seeder
'Нормирование и стандартизация' => [
'children' => [
'Реестр сводов правил' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::RULESET],
'Разработка сводов правил' => [],
'Прикладные исследования' => [],
'Разработка сводов правил' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::DEVELOPMENTS],
'Прикладные исследования' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::RESEARCHES],
'Реестр нормативно-технической документации' => [],
'Методические материалы' => [],
]
],
'Оценка пригодности' => [
'children' => [
'Реестр технических свидетельств' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::SIMPLE],
'Реестр технических свидетельств' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::TECHNICAL_CERTIFICATES],
'Заявка на оформление' => [],
'Предварительная заявка' => [],
]
@ -75,7 +75,7 @@ class PagesTableSeeder extends Seeder
'Секретариат' => [],
'Структура' => [],
'Состав' => [],
'Документы' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::SIMPLE],
'Документы' => ['type' => PageType::REGISTRY, 'registry_type' => RegistryType::CATEGORIZED],
'АИС ТК 465 «Строительство»' => [],
]
],