diff --git a/app/Events/Applications/ApplicationStatusChanged.php b/app/Events/Applications/ApplicationStatusChanged.php new file mode 100644 index 0000000..ebc34c6 --- /dev/null +++ b/app/Events/Applications/ApplicationStatusChanged.php @@ -0,0 +1,25 @@ +application = $application; + $this->status = $status ?? $application->status; + } + + public function broadcastOn(): PrivateChannel { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Http/Controllers/Api/Applications/ApplicationsController.php b/app/Http/Controllers/Api/Applications/ApplicationsController.php new file mode 100644 index 0000000..66ebba3 --- /dev/null +++ b/app/Http/Controllers/Api/Applications/ApplicationsController.php @@ -0,0 +1,48 @@ +model = $model; + } + + public function index(Request $request): JsonResponse { + $filters = collect($request->has('filters') ? json_decode($request->get('filters'), true) : [])->filter(function($val) {return $val;}); + $query = $this->model->query(); + $service = FiltersService::getService('applications'); + $service->applyFilters($query, $filters); + $query->orderBy('id', 'desc'); + $paginator = $query->paginate(config('app.pagination_limit')); + return fractal($paginator, new ApplicationTransformer())->respond(); + } + + public function show(Request $request, $id): JsonResponse { + $query = $this->model->query(); + $model = $query->byUuid($id)->firstOrFail(); + return fractal($model, new ApplicationTransformer())->respond(); + } + + public function store(Request $request) { + } + + public function update(Request $request, $uuid) { + } + + public function destroy(Request $request, $uuid): JsonResponse { + $model = $this->model->byUuid($uuid)->firstOrFail(); + $model->delete(); + return response()->json(null, 204); + } + + +} diff --git a/app/Http/Controllers/Api/Companies/MembersController.php b/app/Http/Controllers/Api/Companies/MembersController.php index a84afa4..819587e 100644 --- a/app/Http/Controllers/Api/Companies/MembersController.php +++ b/app/Http/Controllers/Api/Companies/MembersController.php @@ -24,7 +24,7 @@ class MembersController extends Controller { $query = $this->model->query(); $service = FiltersService::getService('companyMembers'); $service->applyFilters($query, $filters); - $paginator = $query->paginate(2); + $paginator = $query->paginate(config('app.pagination_limit')); return fractal($paginator, new CompanyMemberTransformer())->respond(); } diff --git a/app/Http/Controllers/Api/Forms/FormsController.php b/app/Http/Controllers/Api/Forms/FormsController.php index 5806ef5..1865dfe 100644 --- a/app/Http/Controllers/Api/Forms/FormsController.php +++ b/app/Http/Controllers/Api/Forms/FormsController.php @@ -10,12 +10,16 @@ use App\Services\Forms\FormsService; use App\Transformers\Objects\ObjectTransformer; use App\Transformers\Objects\ObjectTypeTransformer; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class FormsController extends Controller { public function get(Request $request, $target, $type = null, $id = null) { if ($target === 'model') { - return FormsService::getService($type)->form($id, json_decode($request->get('extra_props'), true) ?? []); + $service = FormsService::getService($type); + if ($request->user() || $service->unprotected) return $service->form($id, json_decode($request->get('extra_props'), true) ?? []); + else return response()->json(['error' => 'Unauthenticated.'], 401); + //return FormsService::getService($type)->form($id, json_decode($request->get('extra_props'), true) ?? []); } elseif ($object = NirObject::byUuid($id)->first()) { return fractal($object, new ObjectTransformer())->respond(); } elseif ($objectType = ObjectType::byUuidOrName($type)->first()) { @@ -43,6 +47,14 @@ class FormsController extends Controller { } } + public function getFeedbackFormSupport(Request $request): ?JsonResponse { + return ($objectType = ObjectType::byName('feedback-form-support')->first()) ? fractal($objectType, new ObjectTypeTransformer())->respond() : null; + } + public function saveFeedbackFormSupport(Request $request): ?JsonResponse { + return FormsService::getService('feedback-form-support')->save($request->all()); + } + + public function filters(Request $request, $type) { $filters = collect(json_decode($request->get('filters', []), true)); diff --git a/app/Listeners/Applications/SendApplicationStatusChangedNotifications.php b/app/Listeners/Applications/SendApplicationStatusChangedNotifications.php new file mode 100644 index 0000000..17af9b4 --- /dev/null +++ b/app/Listeners/Applications/SendApplicationStatusChangedNotifications.php @@ -0,0 +1,44 @@ + [ApplicationStatus::COMPLETED], + 'addresses' => [ApplicationStatus::PROCESSING] + ]; + + public function __construct() { + } + + public function handle(ApplicationStatusChanged $event) { + if (in_array($event->status, $this->notifiableStatuses['addresses'])) $this->notifyAddressees($event->application, $event->status); + if (in_array($event->status, $this->notifiableStatuses['applicant'])) $this->notifyApplicant($event->application, $event->status); + } + + public function notifyAddressees(Application $application, $status) { + $application->addressees->each(function(CompanyMember $member) use($application, $status) { + $this->notify($member->user, $application, $status); + }); + } + + public function notifyApplicant(Application $application, $status) { + $this->notify($application->submitter, $application, $status); + } + + public function notify(User $recipient, Application $application, $status) { + try { + Mail::to($recipient->email)->send(new NotifyApplicationStatusChanged($application, $recipient, $status)); + } catch (\Exception $exception) {} + } + + +} diff --git a/app/Mail/Applications/NotifyApplicationStatusChanged.php b/app/Mail/Applications/NotifyApplicationStatusChanged.php new file mode 100644 index 0000000..35e2537 --- /dev/null +++ b/app/Mail/Applications/NotifyApplicationStatusChanged.php @@ -0,0 +1,29 @@ +application = $application; + $this->recipient = $recipient; + $this->status = $status; + } + + public function build(): NotifyApplicationStatusChanged { + $subject = $this->application->title; + return $this->subject($subject)->view("mail.applications.status-changed"); + } +} diff --git a/app/Models/Applications/Application.php b/app/Models/Applications/Application.php new file mode 100644 index 0000000..da20988 --- /dev/null +++ b/app/Models/Applications/Application.php @@ -0,0 +1,96 @@ +belongsTo(User::class); + } + + public function product(): BelongsTo { + return $this->belongsTo(Product::class); + } + + public function conclusions(): HasMany { + return $this->hasMany(Conclusion::class); + } + + + + public function getTitleAttribute(): string { + return "Заявка №{$this->number} от " . $this->created_at->format('d.m.Y'); + } + + public function getParsedStatusAttribute(): array { + return ['name' => $this->status, 'title' => ApplicationStatus::TITLES[$this->status] ?? null]; + } + + public function getPropertiesAttribute(): ?Model { + return $this->getObject('application-properties', 'properties'); + } + + public function getAddresseesAttribute() { + return CompanyMember::query()->mainCompany()->whereHas('objects', function($query) { + Field::applyFilters($query, collect(['types' => 'company-member-properties', 'moderate-permissions' => 'applications'])); + })->get(); + } + + + public function submit(): bool { + $res = $this->update(['status' => ApplicationStatus::PROCESSING]); + event(new ApplicationStatusChanged($this)); + return $res; + } + public function complete(): bool { + $res = $this->update(['status' => ApplicationStatus::COMPLETED]); + event(new ApplicationStatusChanged($this)); + return $res; + } + + + public function setNumber() { + if (!$this->number) $this->update(['number' => self::generateNumber()]); + } + + public static function generateNumber($start = 100, $digits = 5): string { + $res = intval(static::query()->max('number') ?? $start) + 1; + while (strlen($res) < $digits) { + $res = "0{$res}"; + } + return trim($res); + } + + +} diff --git a/app/Models/Applications/ApplicationStatus.php b/app/Models/Applications/ApplicationStatus.php new file mode 100644 index 0000000..189a82f --- /dev/null +++ b/app/Models/Applications/ApplicationStatus.php @@ -0,0 +1,17 @@ + 'Направлено', + self::PROCESSING => 'В работе', + self::COMPLETED => 'Выполнено', + self::CANCELED =>'Отозвана заявителем' + ]; +} \ No newline at end of file diff --git a/app/Models/Applications/Conclusion.php b/app/Models/Applications/Conclusion.php new file mode 100644 index 0000000..b99dadd --- /dev/null +++ b/app/Models/Applications/Conclusion.php @@ -0,0 +1,39 @@ +belongsTo(Application::class); + } + + public function author(): BelongsTo { + return $this->belongsTo(User::class); + } + + +} diff --git a/app/Models/Companies/CompanyMember.php b/app/Models/Companies/CompanyMember.php index cd76a5f..b78d956 100644 --- a/app/Models/Companies/CompanyMember.php +++ b/app/Models/Companies/CompanyMember.php @@ -4,6 +4,7 @@ namespace App\Models\Companies; use App\Models\Advisories\AdvisoryMember; use App\Models\User; +use App\Support\HasObjectsTrait; use App\Support\RelationValuesTrait; use App\Support\UuidScopeTrait; use Illuminate\Database\Eloquent\Model; @@ -13,7 +14,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Auth; class CompanyMember extends Model { - use UuidScopeTrait, SoftDeletes, RelationValuesTrait; + use UuidScopeTrait, SoftDeletes, HasObjectsTrait, RelationValuesTrait; protected $dates = [ 'deleted_at', @@ -55,6 +56,17 @@ class CompanyMember extends Model { } + public function scopeMainCompany($query) { + return $query->whereHas('company', function($query) { + $query->where(['is_main' => 1]); + }); + } + + + + public function getPropertiesAttribute(): ?Model { + return $this->getObject('company-member-properties'); + } public function getIsMeAttribute(): ?bool { return ($user = Auth::user()) ? ($user->id === $this->user_id) : null; diff --git a/app/Models/Objects/Field.php b/app/Models/Objects/Field.php index 56f1186..848c946 100644 --- a/app/Models/Objects/Field.php +++ b/app/Models/Objects/Field.php @@ -144,13 +144,14 @@ class Field extends Model { public static function applyFilters(Builder $query, Collection $filters, ?FiltersService $service = null) { if ($types = $filters->get('types')) { + $types = is_array($types) ? $types : [$types]; $filters->forget('types'); $query->whereHas('type', function($query) use($types) { - $query->whereIn('uuid', is_array($types) ? $types : [$types]); + $query->whereIn('uuid', $types)->orWhereIn('name', $types); }); $filters->filter(function($value) {return $value;})->each(function($value, $prop) use($query, $types, $service) { $field = Field::query()->where(['name' => $prop])->whereHas('groups.objectType', function($query) use($types) { - $query->whereIn('uuid', is_array($types) ? $types : [$types]); + $query->whereIn('uuid', $types)->orWhereIn('name', $types); })->first(); if ($field) $field->applyFilter($query, $value); }); diff --git a/app/Models/Pages/Page.php b/app/Models/Pages/Page.php index e037bd9..c3c86e5 100644 --- a/app/Models/Pages/Page.php +++ b/app/Models/Pages/Page.php @@ -2,8 +2,10 @@ namespace App\Models\Pages; +use App\Models\Objects\Field; use App\Models\Publications\Publication; use App\Models\Registries\Registry; +use App\Models\User; use App\Support\HasObjectsTrait; use App\Support\RelationValuesTrait; use App\Support\UuidScopeTrait; @@ -94,6 +96,12 @@ class Page extends Model { + public function isEditable(User $user): bool { + return $user->isModerator && $user->membership()->whereHas('objects', function($query) { + Field::applyFilters($query, collect(['types' => 'company-member-properties', 'moderate-pages' => $this->parents->add($this)->pluck('uuid')->all()])); + })->exists(); + } + public function addSection($typeName, $ord = null): ?Model { return $this->createObject($typeName, $ord, 'sections'); } diff --git a/app/Models/Products/Product.php b/app/Models/Products/Product.php new file mode 100644 index 0000000..82da331 --- /dev/null +++ b/app/Models/Products/Product.php @@ -0,0 +1,39 @@ +hasMany(Application::class); + } + + + public function getApplicationAttribute() { + return $this->applications()->first(); + } + +} diff --git a/app/Models/User.php b/app/Models/User.php index 22470d5..bed2f0b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,13 +4,23 @@ namespace App\Models; use App\Events\UserRegistered; use App\Mail\PasswordResetRequested; +use App\Models\Advisories\Advisory; +use App\Models\Advisories\AdvisoryMember; +use App\Models\Advisories\AdvisoryMemberRank; +use App\Models\Applications\Conclusion; +use App\Models\Companies\Company; +use App\Models\Companies\CompanyMember; +use App\Models\Companies\CompanyMemberRole; +use App\Models\Objects\Field; use App\Support\HasRolesUuid; use App\Support\HasSocialLogin; use App\Support\RelationValuesTrait; use App\Support\UuidScopeTrait; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -55,6 +65,22 @@ class User extends Authenticatable { return $this->belongsTo(Asset::class, 'asset_id'); } + public function companies(): BelongsToMany { + return $this->belongsToMany(Company::class, 'company_members'); + } + + public function membership(): HasMany { + return $this->hasMany(CompanyMember::class); + } + public function advisoryMembership(): HasManyThrough { + return $this->hasManyThrough(AdvisoryMember::class, CompanyMember::class); + } + + + public function applicationConclusions(): HasMany { + return $this->hasMany(Conclusion::class, 'author_id'); + } + public function getInitialsAttribute(): string { return collect(explode(' ', $this->name))->slice(0, 2)->map(function($item) { @@ -67,14 +93,34 @@ class User extends Authenticatable { } public function getIsAdminAttribute(): bool { - return $this->hasRole('Administrator'); + return $this->hasRole('Administrator') || $this->isMainCompanyAdmin; + } + public function getIsModeratorAttribute(): bool { + return $this->membership()->where(['role' => CompanyMemberRole::MODERATOR])->mainCompany()->exists(); + } + public function getIsMainCompanyAdminAttribute(): bool { + return $this->membership()->where(['role' => CompanyMemberRole::ADMINISTRATOR])->mainCompany()->exists(); + } + public function getIsMainCompanyMemberAttribute(): bool { + return $this->companies()->where(['is_main' => 1])->exists(); + } + public function getIsExpertAttribute(): bool { + return $this->membership()->mainCompany()->whereHas('objects', function($query) { + Field::applyFilters($query, collect(['types' => 'company-member-properties', 'moderate-permissions' => 'applications'])); + })->exists(); } - public function getIsPrivilegedAttribute() { - return $this->isAdmin; + public function getPrivilegesAttribute(): array { + return [ + 'admin' => $this->isAdmin, + 'main_company_member' => $this->isMainCompanyMember, + 'is_expert' => $this->isExpert + ]; } + + public static function getByData($data, $triggerEvent = false) { $result = false; if ($email = trim($data['email'] ?? null)) { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 30aeedb..da0ae6c 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,8 @@ namespace App\Providers; use App\Models\Advisories\Advisory; use App\Models\Advisories\AdvisoryCompany; use App\Models\Advisories\AdvisoryMember; +use App\Models\Applications\Application; +use App\Models\Applications\Conclusion; use App\Models\Asset; use App\Models\Companies\Address; use App\Models\Companies\BankDetails; @@ -20,6 +22,7 @@ use App\Models\Objects\NirObject; use App\Models\Objects\ObjectType; use App\Models\Pages\Page; use App\Models\Permission; +use App\Models\Products\Product; use App\Models\Publications\Publication; use App\Models\Registries\Category; use App\Models\Registries\Entry; @@ -84,7 +87,11 @@ class AppServiceProvider extends ServiceProvider 'advisory' => Advisory::class, 'advisory-member' => AdvisoryMember::class, - 'advisory-company' => AdvisoryCompany::class + 'advisory-company' => AdvisoryCompany::class, + + 'application' => Application::class, + 'application-conclusion' => Conclusion::class, + 'product' => Product::class ]); } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 0c4f8b8..cf65602 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,9 +2,11 @@ namespace App\Providers; +use App\Events\Applications\ApplicationStatusChanged; use App\Events\FeedbackSender; use App\Events\PasswordRecovered; use App\Events\UserRegistered; +use App\Listeners\Applications\SendApplicationStatusChangedNotifications; use App\Listeners\SendFeedbackMessage; use App\Listeners\SendPasswordRecoveredNotification; use App\Listeners\SendRegistrationNotification; @@ -25,6 +27,10 @@ class EventServiceProvider extends ServiceProvider { ], FeedbackSender::class => [ SendFeedbackMessage::class + ], + + ApplicationStatusChanged::class => [ + SendApplicationStatusChangedNotifications::class ] ]; diff --git a/app/Services/Filters/Applications/ApplicationFilters.php b/app/Services/Filters/Applications/ApplicationFilters.php new file mode 100644 index 0000000..69f8990 --- /dev/null +++ b/app/Services/Filters/Applications/ApplicationFilters.php @@ -0,0 +1,104 @@ + 'common', + 'title' => 'Общие параметры', + 'fields' => $this->nativeFields($filters) + ] + ]; + $query = Application::query(); + $this->applyFilters($query, $filters); + return ['groups' => ['data' => $groups], 'total' => $query->count()]; + } + + public function nativeFields(Collection $filters): array { + return [ + [ + 'name' => 'created_at', + 'title' => 'Дата формирования заявки', + 'type' => FieldType::DATE, + 'range' => $this->getDatesRange('created_at', $filters), + 'value' => $filters->get('created_at') + ], + [ + 'name' => 'status', + 'title' => 'Статус', + 'type' => FieldType::RELATION, + 'represented' => $this->getStatuses($filters), + 'value' => $this->getRelationValue($filters->get('status'), ApplicationStatus::TITLES) + ], + [ + 'name' => 'expert', + 'title' => 'Эксперт', + 'type' => FieldType::RELATION, + 'represented' => $this->getExperts($filters), + 'value' => ($val = $filters->get('expert')) ? fractal(User::byUuids($val)->get(), new UserTransformer()) : null + ] + ]; + } + + public function getDatesRange($prop, Collection $filters): array { + $query = Application::query(); + $this->applyFilters($query, $filters->except($prop)); + return ['min' => ($v = $query->min($prop)) ? Date::create($v)->format('Y-m-d') : null, 'max' => ($v = $query->max($prop)) ? Date::create($v)->format('Y-m-d') : null]; + } + + + public function getStatuses(Collection $filters): array { + $query = Application::query()->whereNotNull('status')->distinct('status'); + $this->applyFilters($query, $filters->except('status')); + $res = $query->get('status')->map(function($item) {return $item->status;})->all(); + return $this->getRelationItems(collect(ApplicationStatus::TITLES)->only($res)->all()); + } + + + public function getExperts(Collection $filters): Fractal { + return fractal(User::query()->whereHas('applicationConclusions.application', function($query) use($filters) { + $this->applyFilters($query, $filters->except('expert')); + })->orderBy('name')->get(), new UserTransformer()); + } + + + public function applyFilters(Builder $query, Collection $filters) { + $this->applyNativeFilters($query, $filters); + $this->applyPermissionsFilters($query); + } + + public function applyNativeFilters(Builder $query, Collection $filters) { + $filters->each(function($value, $prop) use($query) { + $this->applyNativeFilter($query, $prop, $value); + }); + } + + public function applyNativeFilter(Builder $query, $prop, $value) { + if ($value) { + if ($prop === 'search') $this->applySearchFilter($query, $value, ['number', 'applicant', ['submitter' => ['name']], ['product' => ['name', 'purpose', 'normative', 'producer']]]); + elseif ($prop === 'created_at') $this->applyDateFilter($query, 'created_at', $value); + elseif ($prop === 'status') $query->whereIn('status', is_array($value) ? $value : [$value]); + elseif ($prop === 'expert') $this->applyRelationFilter($query, 'conclusions.author', $value); + } + } + + public function applyPermissionsFilters(Builder $query) { + $user = Auth::user(); + if (!$user->isExpert && !$user->isAdmin) $query->where(['submitter_id' => $user->id ?? null]); + } + +} diff --git a/app/Services/Filters/Applications/ApplicationFiltersServices.php b/app/Services/Filters/Applications/ApplicationFiltersServices.php new file mode 100644 index 0000000..b651333 --- /dev/null +++ b/app/Services/Filters/Applications/ApplicationFiltersServices.php @@ -0,0 +1,9 @@ + ApplicationFilters::class + ]; +} diff --git a/app/Services/Filters/FiltersService.php b/app/Services/Filters/FiltersService.php index 3a11212..b80e27f 100644 --- a/app/Services/Filters/FiltersService.php +++ b/app/Services/Filters/FiltersService.php @@ -2,6 +2,7 @@ namespace App\Services\Filters; +use App\Services\Filters\Applications\ApplicationFiltersServices; use App\Services\Filters\Companies\CompanyFiltersServices; use App\Services\Filters\Registries\RegistryFiltersServices; use Illuminate\Database\Eloquent\Builder; @@ -11,7 +12,8 @@ use Illuminate\Support\Facades\Date; class FiltersService { public static array $services = [ RegistryFiltersServices::class, - CompanyFiltersServices::class + CompanyFiltersServices::class, + ApplicationFiltersServices::class ]; diff --git a/app/Services/Forms/Applications/ApplicationForms.php b/app/Services/Forms/Applications/ApplicationForms.php new file mode 100644 index 0000000..80a7b0d --- /dev/null +++ b/app/Services/Forms/Applications/ApplicationForms.php @@ -0,0 +1,126 @@ + 'Формирование заявки', 'update' => 'Редактирование заявки']; + + public function form(?string $id = null, array $data = []): array { + $model = Application::byUuid($id)->first(); + return [ + 'title' => $this->formTitle($model), + 'frames' => [ + ['title' => 'Общие сведения', 'groups' => $this->form1Groups($model)], + ['title' => 'Документация', 'groups' => $this->form2Groups($model)] + ] + ]; + } + + public function form1Groups(?Application $model): array { + $groups = [ + ['name' => 'common', 'title' => '', 'fields' => $this->commonGroupFields($model)] + ]; + return ['data' => $groups]; + } + + public function form2Groups(?Application $model): array { + $groups = [ + ['name' => 'documents', 'title' => '', 'fields' => $this->documentsGroupFields($model)] + ]; + return ['data' => $groups]; + } + + public function commonGroupFields(?Application $model): array { + $fields = [ + [ + 'name' => 'applicant', + 'title' => 'Организация-заявитель', + 'type' => FieldType::STRING, + 'required' => true, + 'value' => $model->applicant ?? null + ], + [ + 'name' => 'product_name', + 'title' => 'Hаименование продукции', + 'type' => FieldType::TEXT, + 'required' => true, + 'value' => $model->product->name ?? null + ], + [ + 'name' => 'product_purpose', + 'title' => 'Hазначение продукции', + 'type' => FieldType::TEXT, + 'required' => true, + 'value' => $model->product->purpose ?? null + ], + [ + 'name' => 'product_usage', + 'title' => 'Область применения продукции', + 'type' => FieldType::TEXT, + 'required' => true, + 'value' => $model->product->usage ?? null + ], + [ + 'name' => 'normative', + 'title' => 'Нормативно-технический документ', + 'type' => FieldType::STRING, + 'value' => $model->product->normative ?? null + ], + [ + 'name' => 'producer', + 'title' => 'Изготовитель/разработчик', + 'type' => FieldType::TEXT, + 'required' => true, + 'value' => $model->product->producer ?? null + ] + ]; + return ['data' => $fields]; + } + + public function documentsGroupFields(?Application $model): array { + $fields = [ + [ + 'name' => 'documents', + 'title' => 'Документация', + 'type' => FieldType::DOCUMENT, + 'multiple' => true, + 'value' => $model ? fractal($model->properties->getValue('documents'), new AssetTransformer()) : null + ] + ]; + return ['data' => $fields]; + } + + + + public function store(array $data): ?JsonResponse { + $product = Product::create(); + $model = Application::create(['submitter_id' => Auth::user()->id, 'product_id' => $product->id, 'number' => Application::generateNumber(), 'status' => ApplicationStatus::PROCESSING]); + $this->updateData($model, $data); + $model->submit(); + return fractal($model, new ApplicationTransformer())->respond(); + } + + public function update(string $id, array $data): ?JsonResponse { + $model = Application::byUuid($id)->firstOrFail(); + $this->updateData($model, $data); + return fractal($model, new ApplicationTransformer())->respond(); + } + + public function updateData(Application $model, array $data) { + $model->update(['applicant' => $data['applicant'] ?? null]); + $model->product->update(['name' => $data['product_name'] ?? null, 'purpose' => $data['product_purpose'] ?? null, 'usage' => $data['product_usage'] ?? null, + 'normative' => $data['normative'] ?? null, 'producer' => $data['producer'] ?? null]); + $model->properties->setValue('documents', $data['documents'] ?? null); + } + +} diff --git a/app/Services/Forms/Applications/ApplicationFormsServices.php b/app/Services/Forms/Applications/ApplicationFormsServices.php new file mode 100644 index 0000000..ea9e1be --- /dev/null +++ b/app/Services/Forms/Applications/ApplicationFormsServices.php @@ -0,0 +1,10 @@ + ApplicationForms::class, + 'applicationConclusion' => ConclusionForms::class + ]; +} diff --git a/app/Services/Forms/Applications/ConclusionForms.php b/app/Services/Forms/Applications/ConclusionForms.php new file mode 100644 index 0000000..0e85c96 --- /dev/null +++ b/app/Services/Forms/Applications/ConclusionForms.php @@ -0,0 +1,61 @@ + 'Ответ на заявку', 'update' => 'Редактирование ответа']; + + public function form(?string $id = null, array $data = []): array { + $model = Conclusion::byUuid($id)->first(); + return [ + 'title' => $this->formTitle($model), + 'frames' => [ + ['title' => 'Общие сведения', 'groups' => $this->form1Groups($model)], + ] + ]; + } + + public function form1Groups(?Conclusion $model): array { + $groups = [ + ['name' => 'common', 'title' => '', 'fields' => $this->commonGroupFields($model)] + ]; + return ['data' => $groups]; + } + + public function commonGroupFields(?Conclusion $model): array { + $fields = [ + [ + 'name' => 'message', + 'title' => 'Мнение эксперта', + 'type' => FieldType::HTML, + 'required' => true, + 'value' => $model->message ?? null + ] + ]; + return ['data' => $fields]; + } + + + + public function store(array $data): ?JsonResponse { + $application = Application::byUuid($data['application'] ?? null)->firstOrFail(); + $model = $application->conclusions()->create(['author_id' => Auth::user()->id, 'message' => $data['message'] ?? null]); + $application->complete(); + return fractal($model, new ConclusionTransformer())->respond(); + } + + public function update(string $id, array $data): ?JsonResponse { + $model = Conclusion::byUuid($id)->firstOrFail(); + $model->update(['message' => $data['message'] ?? null]); + return fractal($model, new ConclusionTransformer())->respond(); + } + +} diff --git a/app/Services/Forms/Companies/CompanyMemberForms.php b/app/Services/Forms/Companies/CompanyMemberForms.php index fdb00f2..b04e755 100644 --- a/app/Services/Forms/Companies/CompanyMemberForms.php +++ b/app/Services/Forms/Companies/CompanyMemberForms.php @@ -6,12 +6,14 @@ use App\Models\Companies\CompanyMember; use App\Models\Companies\CompanyMemberRank; use App\Models\Companies\CompanyMemberRole; use App\Models\Companies\Department; +use App\Models\Dictionaries\Dictionary; use App\Models\Objects\FieldType; use App\Models\Pages\Page; use App\Models\User; use App\Services\Forms\FormsService; use App\Transformers\Companies\CompanyMemberTransformer; use App\Transformers\Companies\DepartmentTransformer; +use App\Transformers\Dictionaries\DictionaryItemTransformer; use App\Transformers\Pages\PageTransformer; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; @@ -20,20 +22,35 @@ class CompanyMemberForms extends FormsService { public array $formTitles = ['create' => 'Добавление сотрудника организации', 'update' => 'Редактирование сотрудника']; public function form(?string $id = null, array $data = []): array { + $department = Department::byUuid($data['department'] ?? null)->first(); $model = CompanyMember::byUuid($id)->first(); + $company = $model->company ?? $department->company; + $user = Auth::user(); + $frames = [['title' => 'Общие сведения', 'groups' => $this->form1Groups($model)]]; + if (($company->is_main ?? null) && $user->isAdmin) $frames[] = ['title' => 'Полномочия', 'groups' => $this->form2Groups($model)]; + return ['title' => $this->formTitle($model), 'frames' => $frames]; + } + + public function form1Groups(?CompanyMember $model): array { $groups = [ - [ - 'name' => 'common', - 'fields' => $this->commonGroupFields($model), + ['name' => 'common', 'title' => '', 'fields' => $this->commonGroupFields($model)] + ]; + return ['data' => $groups]; + } + + public function form2Groups(?CompanyMember $model): array { + $groups = [ + ['name' => 'permissions', 'title' => '', 'fields' => $this->permissionsGroupFields($model), 'dynamic' => [ - ['field' => 'role', 'hide' => ['pages']], - ['field' => 'role', 'value' => CompanyMemberRole::MODERATOR, 'show' => ['pages']] + ['field' => 'role', 'hide' => ['moderate-pages']], + ['field' => 'role', 'value' => CompanyMemberRole::MODERATOR, 'show' => ['moderate-pages']] ] ] ]; - return ['title' => $this->formTitle($model), 'data' => $groups]; + return ['data' => $groups]; } + public function commonGroupFields(?CompanyMember $model): array { $fields = [ [ @@ -47,7 +64,7 @@ class CompanyMemberForms extends FormsService { [ 'name' => 'name', 'title' => 'ФИО', - 'type' => FieldType::TEXT, + 'type' => FieldType::STRING, 'required' => true, 'value' => $model->user->name ?? null ], @@ -75,23 +92,6 @@ class CompanyMemberForms extends FormsService { 'title' => 'Внутренний телефон', 'type' => FieldType::STRING, 'value' => $model->intercom ?? null - ], - [ - 'name' => 'role', - 'title' => 'Роль', - 'type' => FieldType::RELATION, - 'required' => true, - 'appearance' => 'radio', - 'options' => $this->getRelationItems(CompanyMemberRole::TITLES), - 'value' => $this->getRelationValue(CompanyMemberRole::TITLES, $model->role ?? null) - ], - [ - 'name' => 'pages', - 'title' => 'Разделы сайта', - 'type' => FieldType::RELATION, - 'multiple' => true, - 'options' => fractal(Page::all(), new PageTransformer()), - 'value' => null ] /* [ @@ -124,12 +124,46 @@ class CompanyMemberForms extends FormsService { return ['data' => $fields]; } + public function permissionsGroupFields(?CompanyMember $model): array { + $fields = [ + [ + 'name' => 'role', + 'title' => 'Роль', + 'type' => FieldType::RELATION, + 'required' => true, + 'appearance' => 'radio', + 'options' => $this->getRelationItems(CompanyMemberRole::TITLES), + 'value' => $this->getRelationValue(CompanyMemberRole::TITLES, $model->role ?? null) + ], + [ + 'name' => 'moderate-pages', + 'title' => 'Разделы сайта', + 'type' => FieldType::RELATION, + 'multiple' => true, + 'options' => fractal(Page::all(), new PageTransformer()), + 'value' => $model ? fractal($model->properties->getValue('moderate-pages'), new PageTransformer()) : null + ], + [ + 'name' => 'moderate-permissions', + 'title' => 'Права', + 'type' => FieldType::RELATION, + 'multiple' => true, + 'appearance' => 'checkbox', + 'options' => fractal(Dictionary::byName('moderate-permissions')->first()->items, new DictionaryItemTransformer()), + 'value' => $model ? fractal($model->properties->getValue('moderate-permissions'), new DictionaryItemTransformer()) : null + ], + ]; + + return ['data' => $fields]; + } + public function store(array $data): ?JsonResponse { if (($department = Department::byUuid($data['department'] ?? null)->first()) && ($user = User::getByData(collect($data)->except('role')->all(), true))) { $model = $department->addMember($user, $data['position'] ?? null); $model->update(['position' => $data['position'] ?? null, 'role' => $data['role'] ?? null, 'room' => $data['room'] ?? null, 'intercom' => $data['intercom'] ?? null]); + $model->properties->setValues($data); return fractal($model, new CompanyMemberTransformer())->respond(); } } @@ -140,6 +174,7 @@ class CompanyMemberForms extends FormsService { if ($department = Department::byUuid($data['department'] ?? null)->first()) $model->update(['department_id' => $department->id]); $model->user->update(['name' => $data['name'] ?? null, 'phone' => $data['phone'] ?? null]); if (($email = $data['email'] ?? null) && !User::query()->where(['email' => $email])->exists()) $model->user->update(['email' => $data['email']]); + $model->properties->setValues($data); return fractal($model->fresh(), new CompanyMemberTransformer())->respond(); } diff --git a/app/Services/Forms/FormsService.php b/app/Services/Forms/FormsService.php index d14ece8..b85a00c 100644 --- a/app/Services/Forms/FormsService.php +++ b/app/Services/Forms/FormsService.php @@ -3,6 +3,7 @@ namespace App\Services\Forms; use App\Services\Forms\Advisories\AdvisoryFormsServices; +use App\Services\Forms\Applications\ApplicationFormsServices; use App\Services\Forms\Companies\CompanyFormsServices; use App\Services\Forms\Feedback\FeedbackFormsServices; use App\Services\Forms\Pages\PageFormsServices; @@ -20,7 +21,8 @@ class FormsService { UserFormsServices::class, FeedbackFormsServices::class, AdvisoryFormsServices::class, - CompanyFormsServices::class + CompanyFormsServices::class, + ApplicationFormsServices::class ]; public function __construct() { diff --git a/app/Services/PermissionsService.php b/app/Services/PermissionsService.php index c2e5de4..d25fe43 100644 --- a/app/Services/PermissionsService.php +++ b/app/Services/PermissionsService.php @@ -2,7 +2,9 @@ namespace App\Services; +use App\Models\Applications\Application; use App\Models\Objects\NirObject; +use App\Models\Pages\Page; use App\Models\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Auth; @@ -12,7 +14,9 @@ class PermissionsService { private ?User $user; private array $rules = [ - NirObject::class => 'nirObject' + NirObject::class => 'nirObject', + Application::class => 'application', + Page::class => 'page' ]; public function __construct(Model $model, ?User $user = null) { @@ -21,11 +25,14 @@ class PermissionsService { } public function get(): array { - $rule = $this->rules[get_class($this->model)] ?? null; - $func = "{$rule}Permissions"; - if ($this->user->isPrivileged) return ['anything' => true]; - elseif (method_exists($this, $func)) return $this->$func(); - else return []; + $result = []; + if ($this->user) { + $rule = $this->rules[get_class($this->model)] ?? null; + $func = "{$rule}Permissions"; + $result = method_exists($this, $func) ? $this->$func() : []; + if ($this->user->isAdmin) $result['anything'] = true; + } + return $result; } @@ -33,4 +40,12 @@ class PermissionsService { return ['edit' => $this->model->owner_id === $this->user->id]; } + public function applicationPermissions(): array { + return ['edit' => $this->model->submitter_id === $this->user->id, 'reply' => $this->user->isExpert]; + } + + public function pagePermissions(): array { + return ['edit' => $this->model->isEditable($this->user)]; + } + } \ No newline at end of file diff --git a/app/Transformers/Applications/ApplicationTransformer.php b/app/Transformers/Applications/ApplicationTransformer.php new file mode 100644 index 0000000..bc56de5 --- /dev/null +++ b/app/Transformers/Applications/ApplicationTransformer.php @@ -0,0 +1,54 @@ + $model->uuid, + 'title' => $model->title, + 'status' => $model->parsedStatus, + 'number' => $model->number, + 'applicant' => $model->applicant, + 'created_at' => $model->created_at ? $model->created_at->toIso8601String() : null, + 'updated_at' => $model->updated_at ? $model->updated_at->toIso8601String() : null + ]; + } + + public function includeSubmitter(Application $model): ?Item { + return $model->submitter ? $this->item($model->submitter, new UserTransformer()) : null; + } + + public function includeProduct(Application $model): ?Item { + return $model->product ? $this->item($model->product, new ProductTransformer()) : null; + } + + public function includeProperties(Application $model): ?Item { + return $model->properties ? $this->item($model->properties, new ObjectTransformer()) : null; + } + + public function includeConclusions(Application $model): Collection { + return $this->collection($model->conclusions, new ConclusionTransformer()); + } + + public function includePermissions(Application $model): Primitive { + return $this->primitive((new PermissionsService($model))->get()); + } + +} \ No newline at end of file diff --git a/app/Transformers/Applications/ConclusionTransformer.php b/app/Transformers/Applications/ConclusionTransformer.php new file mode 100644 index 0000000..7ebf6c6 --- /dev/null +++ b/app/Transformers/Applications/ConclusionTransformer.php @@ -0,0 +1,34 @@ + $model->uuid, + 'message' => $model->message, + 'created_at' => $model->created_at ? $model->created_at->toIso8601String() : null, + 'updated_at' => $model->updated_at ? $model->updated_at->toIso8601String() : null + ]; + } + + public function includeAuthor(Conclusion $model): ?Item { + return $model->author ? $this->item($model->author, new UserTransformer()) : null; + } + + public function includeApplication(Conclusion $model): ?Item { + return $model->application ? $this->item($model->application, new ApplicationTransformer()) : null; + } + +} \ No newline at end of file diff --git a/app/Transformers/Products/ProductTransformer.php b/app/Transformers/Products/ProductTransformer.php new file mode 100644 index 0000000..325da45 --- /dev/null +++ b/app/Transformers/Products/ProductTransformer.php @@ -0,0 +1,32 @@ + $model->uuid, + 'name' => $model->name, + 'purpose' => $model->purpose, + 'usage' => $model->usage, + 'normative' => $model->normative, + 'producer' => $model->producer + ]; + } + + public function includeApplications(Product $model): Collection { + return $this->collection($model->applications, new ApplicationTransformer()); + } + +} \ No newline at end of file diff --git a/app/Transformers/Users/UserTransformer.php b/app/Transformers/Users/UserTransformer.php index 562ef3d..57dd25b 100644 --- a/app/Transformers/Users/UserTransformer.php +++ b/app/Transformers/Users/UserTransformer.php @@ -4,13 +4,15 @@ namespace App\Transformers\Users; use App\Models\User; use App\Transformers\Assets\AssetTransformer; +use App\Transformers\Companies\CompanyMemberTransformer; use League\Fractal\Resource\Collection; use League\Fractal\Resource\Item; +use League\Fractal\Resource\Primitive; use League\Fractal\TransformerAbstract; class UserTransformer extends TransformerAbstract { protected array $defaultIncludes = []; - protected array $availableIncludes = ['avatar', 'roles']; + protected array $availableIncludes = ['avatar', 'roles', 'membership', 'privileges']; public function transform(User $model): array { return [ @@ -33,4 +35,12 @@ class UserTransformer extends TransformerAbstract { return $this->collection($model->roles, new RoleTransformer()); } + public function includeMembership(User $model): Collection { + return $this->collection($model->membership, new CompanyMemberTransformer()); + } + + public function includePrivileges(User $model): Primitive { + return $this->primitive($model->privileges); + } + } diff --git a/database/migrations/2023_07_31_152001_create_applications_table.php b/database/migrations/2023_07_31_152001_create_applications_table.php new file mode 100644 index 0000000..2830775 --- /dev/null +++ b/database/migrations/2023_07_31_152001_create_applications_table.php @@ -0,0 +1,38 @@ +id(); + $table->char('uuid', 36)->index()->unique(); + $table->integer('submitter_id')->index()->nullable(); + $table->integer('product_id')->index()->nullable(); + $table->string('status')->index()->nullable(); + $table->string('number')->index()->nullable(); + $table->string('applicant', 750)->index()->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('applications'); + } +} diff --git a/database/migrations/2023_07_31_153559_create_products_table.php b/database/migrations/2023_07_31_153559_create_products_table.php new file mode 100644 index 0000000..b19d8db --- /dev/null +++ b/database/migrations/2023_07_31_153559_create_products_table.php @@ -0,0 +1,38 @@ +id(); + $table->char('uuid', 36)->index()->unique(); + $table->text('name')->nullable(); + $table->text('purpose')->nullable(); + $table->text('usage')->nullable(); + $table->text('normative')->nullable(); + $table->text('producer')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('products'); + } +} diff --git a/database/migrations/2023_07_31_155054_create_application_conclusions_table.php b/database/migrations/2023_07_31_155054_create_application_conclusions_table.php new file mode 100644 index 0000000..dcb7a65 --- /dev/null +++ b/database/migrations/2023_07_31_155054_create_application_conclusions_table.php @@ -0,0 +1,36 @@ +id(); + $table->char('uuid', 36)->index()->unique(); + $table->integer('application_id')->index()->nullable(); + $table->integer('author_id')->index()->nullable(); + $table->text('message')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('application_conclusions'); + } +} diff --git a/database/seeders/Dictionaries/DictionariesTableSeeder.php b/database/seeders/Dictionaries/DictionariesTableSeeder.php index f806181..5b62cfc 100644 --- a/database/seeders/Dictionaries/DictionariesTableSeeder.php +++ b/database/seeders/Dictionaries/DictionariesTableSeeder.php @@ -40,6 +40,10 @@ class DictionariesTableSeeder extends Seeder { 'research-types' => [ 'title' => 'Виды исследовательских работ', 'items' => ['nir' => 'НИР', 'niokr' => 'НИОКР'] + ], + 'moderate-permissions' => [ + 'title' => 'Права', + 'items' => ['applications' => 'Рассмотрение предварительных заявок'] ] ]; diff --git a/database/seeders/Objects/FieldsTableSeeder.php b/database/seeders/Objects/FieldsTableSeeder.php index c16cb9a..6fe0bfb 100644 --- a/database/seeders/Objects/FieldsTableSeeder.php +++ b/database/seeders/Objects/FieldsTableSeeder.php @@ -6,8 +6,10 @@ use App\Models\Dictionaries\DictionaryItem; use App\Models\Objects\Field; use App\Models\Objects\FieldType; use App\Models\Objects\ObjectType; +use App\Models\Pages\Page; use App\Transformers\Dictionaries\DictionaryItemTransformer; use App\Transformers\Objects\ObjectTypeTransformer; +use App\Transformers\Pages\PageTransformer; use Illuminate\Database\Seeder; class FieldsTableSeeder extends Seeder { @@ -344,6 +346,26 @@ class FieldsTableSeeder extends Seeder { 'type' => FieldType::STRING, 'required' => true, ], + + 'moderate-pages' => [ + 'title' => 'Модерируемые разделы сайта', + 'type' => FieldType::RELATION, + 'multiple' => true, + 'params' => [ + 'related' => Page::class, 'transformer' => PageTransformer::class, + 'options' => ['show' => true] + ] + ], + 'moderate-permissions' => [ + 'title' => 'Права', + 'type' => FieldType::RELATION, + 'multiple' => true, + 'params' => [ + 'appearance' => 'checkbox', + 'related' => DictionaryItem::class, 'transformer' => DictionaryItemTransformer::class, + 'options' => ['show' => true, 'whereHas' => ['dictionary' => ['name' => 'moderate-permissions']]] + ] + ] ]; public function run() { diff --git a/database/seeders/Objects/ObjectTypeFieldsTableSeeder.php b/database/seeders/Objects/ObjectTypeFieldsTableSeeder.php index 6ba6242..635da7e 100644 --- a/database/seeders/Objects/ObjectTypeFieldsTableSeeder.php +++ b/database/seeders/Objects/ObjectTypeFieldsTableSeeder.php @@ -112,6 +112,18 @@ class ObjectTypeFieldsTableSeeder extends Seeder { 'common' => [ 'fields' => ['operation-type', 'order-name', 'order-date', 'order-document', 'listings', 'active-since', 'active-till', 'developer'] ] + ], + + 'company-member-properties' => [ + 'common' => [ + 'fields' => ['moderate-pages', 'moderate-permissions'] + ] + ], + + 'application-properties' => [ + 'common' => [ + 'fields' => ['documents'] + ] ] ]; diff --git a/database/seeders/Objects/ObjectTypesTableSeeder.php b/database/seeders/Objects/ObjectTypesTableSeeder.php index bab991d..9cca279 100644 --- a/database/seeders/Objects/ObjectTypesTableSeeder.php +++ b/database/seeders/Objects/ObjectTypesTableSeeder.php @@ -99,6 +99,14 @@ class ObjectTypesTableSeeder extends Seeder { 'title' => 'Действие со сводом правил' ] ] + ], + + 'company-member-properties' => [ + 'title' => 'Параметры сотрудника организации' + ], + + 'application-properties' => [ + 'title' => 'Параметры заявки' ] ]; diff --git a/resources/views/mail/applications/status-changed.blade.php b/resources/views/mail/applications/status-changed.blade.php new file mode 100644 index 0000000..81b8669 --- /dev/null +++ b/resources/views/mail/applications/status-changed.blade.php @@ -0,0 +1,9 @@ +@extends('mail.layouts.layout') + +@section('content') + @if($status === \App\Models\Applications\ApplicationStatus::PROCESSING) +
{{$application->title}} поступила на рассмотрение
+ @elseif($status === \App\Models\Applications\ApplicationStatus::COMPLETED) +{{$application->title}} выполнена.
+ @endif +@endsection \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 6e6d6e9..6170b7f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -25,11 +25,6 @@ Route::get('publications/{id}', 'Api\Publications\PublicationsController@show'); Route::apiResource('object-types', 'Api\Objects\ObjectTypesController'); -Route::group(['prefix' => 'forms'], function() { - Route::get('/{target}/{type?}/{id?}', 'Api\Forms\FormsController@get'); - Route::post('/{target}/{type?}/{id?}', 'Api\Forms\FormsController@save'); -}); - Route::group(['prefix' => 'registries'], function() { Route::get('/categories', 'Api\Registries\CategoriesController@index'); @@ -47,6 +42,10 @@ Route::group(['prefix' => 'registries'], function() { }); }); + +Route::get('forms/object/feedback-form-support', 'Api\Forms\FormsController@getFeedbackFormSupport'); +Route::post('forms/model/feedback-form-support', 'Api\Forms\FormsController@saveFeedbackFormSupport'); + Route::get('filters/{type}', 'Api\Forms\FormsController@filters'); Route::group(['middleware' => ['auth:api']], function() { @@ -74,6 +73,11 @@ Route::group(['middleware' => ['auth:api']], function() { Route::put('objects/move/{id}', 'Api\Objects\ObjectsController@move'); Route::apiResource('objects', 'Api\Objects\ObjectsController'); + Route::group(['prefix' => 'forms'], function() { + Route::get('/{target}/{type?}/{id?}', 'Api\Forms\FormsController@get'); + Route::post('/{target}/{type?}/{id?}', 'Api\Forms\FormsController@save'); + }); + Route::get('dadata/{inn}', 'Api\Companies\CompaniesController@getDataByInn'); Route::put('publications/published/{id}', 'Api\Publications\PublicationsController@published'); @@ -88,4 +92,5 @@ Route::group(['middleware' => ['auth:api']], function() { Route::apiResource('advisory-companies', 'Api\Advisories\AdvisoryCompaniesController'); Route::apiResource('advisory-members', 'Api\Advisories\AdvisoryMembersController'); + Route::apiResource('applications', 'Api\Applications\ApplicationsController'); }); \ No newline at end of file