Compare commits

..

3 Commits

42 changed files with 1030 additions and 164 deletions

View File

@ -3,8 +3,13 @@
namespace Modules\Admin\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Modules\User\Models\User;
use Modules\User\Models\Role;
use Modules\Main\Models\City;
use Modules\Post\Models\Post;
use Modules\Post\Models\PostCity;
use App\Http\Controllers\Controller;
use Modules\Post\Models\PostCategory;
@ -15,8 +20,7 @@ class AdminPostsController extends Controller
public function index()
{
$posts = Post::orderBy('id', 'desc');
if (!auth()->user()->isAdmin() && auth()->user()->isCityManager())
{
if (!auth()->user()->isAdmin() && auth()->user()->isCityManager()) {
}
$posts = $posts->get();
@ -35,53 +39,65 @@ public function create()
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required',
'category' => 'required',
'name' => 'required',
'category' => 'required',
'short_text' => 'max:500',
'text' => 'required',
'imageFile' => 'required|mimes:jpg,bmp,png'
'text' => 'required',
'imageFile' => 'required|mimes:jpg,bmp,png'
]);
if (!Auth::user()->hasRole(Role::SUPER_ADMIN)) {
if (!Auth::user()->hasRole(Role::CITY_MANAGER)) {
return back();
}
if ($request->has('cities')) {
$availableCities = GetAvailableCities()->pluck('id')->toArray();
foreach ($request->cities as $cityId) {
if (!in_array($cityId, $availableCities)) {
return back();
}
}
} else {
return back();
}
}
$path = $request->file('imageFile')->store('posts', ['disk' => 'public']);
$request['image'] = $path;
$post = Post::create(
$request->only(['name', 'short_text', 'text', 'category', 'image'])
);
if (array_key_exists('cities', $request->all()) && is_array($request['cities']))
{
foreach ($request->cities as $cityId)
{
$post->cities()->create([
if (array_key_exists('cities', $request->all()) && is_array($request['cities'])) {
foreach ($request->cities as $cityId) {
PostCity::create([
'post_id' => $post->id,
'city_id' => $cityId,
]);
}
}
return to_route('admin.posts');
return back()->withSuccess('Новость добавлена успешно');
}
public function edit(Post $post)
{
return view('admin::posts.edit', [
'categories' => PostCategory::cases(),
'post' => $post
'post' => $post
]);
}
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'name' => 'required',
'category' => 'required',
'name' => 'required',
'category' => 'required',
'short_text' => 'max:500',
'text' => 'required',
'text' => 'required',
]);
if ($request->file('imageFile'))
{
if ($request->file('imageFile')) {
$path = $request->file('imageFile')->store('posts', ['disk' => 'public']);
$request['image'] = $path;
}
else
{
} else {
$reuqest['image'] = $post->image;
}
$post = $post->update(

View File

@ -0,0 +1,24 @@
<?php
namespace Modules\Admin\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Gate;
use Modules\User\Models\User;
use Modules\User\Models\Role;
class PostCreatePolicyAuthorization
{
public function handle(Request $request, Closure $next): Response
{
if (!Auth::user()->can('viewAdminPath', User::class) && !Auth::user()->hasRole(Role::CITY_MANAGER))
{
abort(403, 'Unauthorized action.');
}
return $next($request);
}
}

View File

@ -3,11 +3,17 @@
use Illuminate\Support\Facades\Route;
use Modules\Admin\Http\Controllers\AdminController;
use Modules\Admin\Http\Middleware\AdminPolicyAuthorization;
use Modules\Admin\Http\Middleware\PostCreatePolicyAuthorization;
use Modules\Admin\Http\Middleware\GloabalAdminPathsPolicyAuthorization;
Route::get('/admin', [Modules\Admin\Http\Controllers\AdminController::class, 'index'])->name('admin.index');
Route::post('/admin/set', [Modules\Admin\Http\Controllers\AdminController::class, 'setSuperAdmin'])->name('admin.setSuperAdmin');
Route::middleware(['auth', PostCreatePolicyAuthorization::class])->group(function ()
{
Route::post('/admin/posts/store', [Modules\Admin\Http\Controllers\AdminPostsController::class, 'store'])->name('admin.posts.store');
Route::post('/admin/post/{post}/update', [Modules\Admin\Http\Controllers\AdminPostsController::class, 'update'])->name('admin.posts.update');
});
Route::middleware(['auth', AdminPolicyAuthorization::class])->group(function ()
{
@ -54,9 +60,7 @@
Route::get('/admin/posts', [Modules\Admin\Http\Controllers\AdminPostsController::class, 'index'])->name('admin.posts');
Route::get('/admin/posts/create', [Modules\Admin\Http\Controllers\AdminPostsController::class, 'create'])->name('admin.posts.create');
Route::post('/admin/posts/store', [Modules\Admin\Http\Controllers\AdminPostsController::class, 'store'])->name('admin.posts.store');
Route::get('/admin/post/{post}/edit', [Modules\Admin\Http\Controllers\AdminPostsController::class, 'edit'])->name('admin.posts.edit');
Route::post('/admin/post/{post}/update', [Modules\Admin\Http\Controllers\AdminPostsController::class, 'update'])->name('admin.posts.update');
Route::post('/admin/post/{post}/delete', [Modules\Admin\Http\Controllers\AdminPostsController::class, 'delete'])->name('admin.posts.delete');
Route::get('/admin/docs', [Modules\Admin\Http\Controllers\AdminDocsController::class, 'index'])->name('admin.docs');

View File

@ -1,55 +1,5 @@
@php($title = 'Новости')
@extends('layouts.admin')
@section('content')
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
<script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
<h4 class="fw-bold">Добавить новость</h4>
<form action="{{ route('admin.posts.store') }}" method="post" enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label for="titleFormControlTextarea" class="form-label">Заголовок</label>
<textarea class="form-control" id="titleFormControlTextarea1" name="name" rows="2"></textarea>
@error('name')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="row">
<div class="col-6 mb-3">
<label for="categoryFormControlSelect" class="form-label">Категория</label>
<select class="form-select" id="categoryFormControlSelect" name="category" aria-label="">
@foreach ($categories as $category)
<option value="{{ $category->value }}">{{ __($category->name) }}</option>
@endforeach
</select>
@error('category')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="col-6 mb-3">
<label for="formFile" class="form-label">Заставка новости</label>
<input class="form-control" type="file" id="formFile" name="imageFile">
@error('imageFile')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label for="shortTextFormControlTextarea" class="form-label">Анонс</label>
<textarea class="form-control" id="shortTextFormControlTextarea1" name="short_text" rows="3"></textarea>
@error('short_text')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="shortTextFormControlTextarea" class="form-label">Основной текст</label>
<textarea class="form-control d-none" id="textFormControlTextarea" name="text" rows="15"></textarea>
<trix-editor input="textFormControlTextarea" class="overflow-auto" style="height:300px"></trix-editor>
@error('text')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<input type="text" name="cities[0]" value="1" />
<button type="submit" class="btn btn-primary">Сохранить</button>
</form>
@include('admin::posts.path.create-form')
@endsection

View File

@ -1,54 +1,5 @@
@php($title = 'Новости')
@extends('layouts.admin')
@section('content')
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
@vite(['trix.umd.min.js'])
<h4 class="fw-bold">Добавить новость</h4>
<form action="{{ route('admin.posts.update', ['post' => $post]) }}" method="post" enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label for="titleFormControlTextarea" class="form-label">Заголовок</label>
<textarea class="form-control" id="titleFormControlTextarea1" name="name" rows="2">{{ $post->name }}</textarea>
@error('name')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="row">
<div class="col mb-3">
<label for="categoryFormControlSelect" class="form-label">Категория</label>
<select class="form-select" id="categoryFormControlSelect" name="category" aria-label="">
@foreach ($categories as $category)
<option value="{{ $category->value }}" {{ $post->category == $category->value ? 'selected' : '' }}>
{{ __($category->name) }}</option>
@endforeach
</select>
@error('category')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="col mb-3">
<label for="formFile" class="form-label">Заставка новости</label>
<input class="form-control" type="file" id="formFile" name="imageFile">
@error('imageFile')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label for="shortTextFormControlTextarea" class="form-label">Анонс</label>
<textarea class="form-control" id="shortTextFormControlTextarea1" name="short_text" rows="3">{{ $post->short_text }}</textarea>
@error('short_text')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="shortTextFormControlTextarea" class="form-label">Основной текст</label>
<textarea class="form-control d-none" id="textFormControlTextarea" name="text" rows="15">{{ $post->text }}</textarea>
<trix-editor input="textFormControlTextarea" class="overflow-auto" style="height:300px"></trix-editor>
@error('text')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn btn-primary">Сохранить</button>
</form>
@include('admin::posts.path.edit-form')
@endsection

View File

@ -0,0 +1,64 @@
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
<script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
<h4 class="fw-bold">Добавить новость</h4>
<form action="{{ route('admin.posts.store') }}" method="post" enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label for="titleFormControlTextarea" class="form-label">Заголовок</label>
<textarea class="form-control" id="titleFormControlTextarea1" name="name" rows="2"></textarea>
@error('name')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="row text-dark">
<label class="form-label mb-1">Города для публикации</label>
<div class="hstack gap-2 mb-2">
@if($availableCities = GetAvailableCities())
@foreach($availableCities as $key=>$city)
<div class="form-check">
<input name="cities[{{ $key }}]" class="btn-check" type="checkbox" value="{{ $city->id }}" id="city_{{ $city->id }}">
<label class="btn bg-light" for="city_{{ $city->id }}">
{{ $city->name }}
</label>
</div>
@endforeach
@endif
</div>
</div>
<div class="row">
<div class="col-6 mb-3">
<label for="categoryFormControlSelect" class="form-label">Категория</label>
<select class="form-select" id="categoryFormControlSelect" name="category" aria-label="">
@foreach (Modules\Post\Models\PostCategory::cases() as $category)
<option value="{{ $category->value }}">{{ __($category->name) }}</option>
@endforeach
</select>
@error('category')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="col-6 mb-3">
<label for="formFile" class="form-label">Заставка новости</label>
<input class="form-control" type="file" id="formFile" name="imageFile">
@error('imageFile')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label for="shortTextFormControlTextarea" class="form-label">Анонс</label>
<textarea class="form-control" id="shortTextFormControlTextarea1" name="short_text" rows="3"></textarea>
@error('short_text')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="shortTextFormControlTextarea" class="form-label">Основной текст</label>
<textarea class="form-control d-none" id="textFormControlTextarea" name="text" rows="15"></textarea>
<trix-editor input="textFormControlTextarea" class="overflow-auto" style="height:300px"></trix-editor>
@error('text')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn btn-primary">Сохранить</button>
</form>

View File

@ -0,0 +1,53 @@
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
<script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
<h4 class="fw-bold">Добавить новость</h4>
<form action="{{ route('admin.posts.update', ['post' => $post]) }}" method="post" enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label for="titleFormControlTextarea" class="form-label">Заголовок</label>
<textarea class="form-control" id="titleFormControlTextarea1" name="name" rows="2">{{ $post->name }}</textarea>
@error('name')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="row">
<div class="col mb-3">
<label for="categoryFormControlSelect" class="form-label">Категория</label>
<select class="form-select" id="categoryFormControlSelect" name="category" aria-label="">
@foreach ($categories as $category)
<option value="{{ $category->value }}" {{ $post->category == $category->value ? 'selected' : '' }}>
{{ __($category->name) }}
</option>
@endforeach
</select>
@error('category')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="col mb-3">
<label for="formFile" class="form-label">Заставка новости</label>
<input class="form-control" type="file" id="formFile" name="imageFile">
@error('imageFile')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label for="shortTextFormControlTextarea" class="form-label">Анонс</label>
<textarea class="form-control" id="shortTextFormControlTextarea1" name="short_text"
rows="3">{{ $post->short_text }}</textarea>
@error('short_text')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="shortTextFormControlTextarea" class="form-label">Основной текст</label>
<textarea class="form-control d-none" id="textFormControlTextarea" name="text"
rows="15">{{ $post->text }}</textarea>
<trix-editor input="textFormControlTextarea" class="overflow-auto" style="height:300px"></trix-editor>
@error('text')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn btn-primary">Сохранить</button>
</form>

View File

@ -5,8 +5,6 @@
Route::middleware(['auth'])->group(function ()
{
Route::get('/docs', [DocsController::class, 'index'])->name('docs.index');
Route::get('/docs/{document}/download', [DocsController::class, 'download'])->name('docs.download');
});

View File

@ -0,0 +1,5 @@
<?php
return [
];

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invites', function (Blueprint $table)
{
$table->id();
$table->uuid('hash');
$table->foreignId('company_id')->references('id')->on('companies')->onDelete('cascade');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('invites');
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invite_agents', function (Blueprint $table)
{
$table->id();
$table->foreignId('invite_id')->references('id')->on('invites');
$table->foreignId('agent_id')->references('id')->on('agents');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('invite_agents');
}
};

View File

@ -0,0 +1,125 @@
<?php
namespace Modules\Invite\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Modules\Invite\Models\InviteAgent;
use Ramsey\Uuid\Uuid;
use Modules\Invite\Models\Invite;
use Modules\User\Models\User;
use Modules\User\Models\Role;
use Modules\User\Models\UserRole;
use Modules\Main\Models\Company\Company;
use Modules\Main\Models\Company\CompanyAdmin;
use Modules\Main\Models\Agent\Agent;
use Modules\CityManager\Models\CityManager;
class InviteController extends Controller
{
public function index(Company $company = null)
{
$companies = [];
//$companiesIds = UserRole::where('user_id', auth()->user()->id)->pluck('company_id')->toArray();
if (!$company) {
if (auth()->user()->hasRole(Role::CITY_MANAGER)) {
$citiesIds = CityManager::where('user_id', auth()->user()->id)->pluck('city_id')->toArray();
foreach (Company::whereIn('city_id', $citiesIds)->get() as $company) {
$companies[] = $company;
}
}
if (auth()->user()->hasRole(Role::COMPANY_ADMIN)) {
$companyAdmin = CompanyAdmin::where('user_id', auth()->user()->id)->first();
$companies[] = $companyAdmin->company;
}
if (count($companies) > 1) {
return view('invite::companies', [
'companies' => $companies
]);
} else {
return to_route('company.invites', [
'company' => $companies[0]
]);
}
}
return view('invite::index', [
'company' => $company,
'invites' => Invite::where('company_id', $company->id)->get()
]);
}
public function create(Request $request, Company $company) {
Invite::create([
'hash' => Uuid::uuid4(),
'company_id' => $company->id
]);
return back();
}
public function open(Request $request, $hash) {
if (!$invite = Invite::where('hash', $hash)->first()) {
return back();
}
return view('invite::form', [
'invite' => $invite
]);
return back();
}
public function process(Request $request, $hash) {
if (!$invite = Invite::where('hash', $hash)->first()) {
return back();
}
$user = User::where('email', $request->email)->orWhere('phone', $request->phone)->first();
if ($user) {
if (UserRole::where('user_id', Role::AGENT)->get()) {
return back()->withInput()->withErrors('Агент с данным телефоном или электронной почтой уже зарегистрирован');
}
} else {
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'phone' => $request->phone
]);
}
$agent = Agent::create([
'company_id' => $invite->company_id,
'user_id' => $user->id
]);
if (!$agent && $user->wasRecentlyCreated) {
$user->delete();
return back()->withInput()->withErrors('При создании учетной записи возникла ошибка. Попробуйте сделать это немного позже');
}
$registration = InviteAgent::create([
'invite_id' => $invite->id,
'agent_id' => $agent->id
]);
if (!$registration) {
$agent->forceDelete();
return back()->withInput()->withErrors('При создании учетной записи возникла ошибка. Попробуйте сделать это немного позже');
}
$user->setForcedPassword();
return to_route('company.invite.success', [
'hash' => $invite->hash
]);
}
public function success(Request $request, $hash) {
if (!$invite = Invite::where('hash', $hash)->first()) {
return back();
}
return view('invite::success', [
'invite' => $invite
]);
}
public function delete (Invite $invite) {
$invite->delete();
return back()->withSuccess('Ссылка-приглашение была успешно удалена');
}
public function agents(Invite $invite) {
return view('invite::agents', [
'invite' => $invite
]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Modules\Invite\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Modules\Main\Models\Company\Company;
class Invite extends Model
{
use HasFactory;
protected $fillable = [
'hash',
'company_id'
];
public function company()
{
return $this->belongsTo(Company::class, 'company_id');
}
public function registrations()
{
return $this->HasMany(InviteAgent::class, 'invite_id');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Modules\Invite\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Modules\Main\Models\Agent\Agent;
class InviteAgent extends Model
{
use HasFactory;
protected $touches = ['invite'];
protected $fillable = [
'invite_id',
'agent_id'
];
public function agent()
{
return $this->belongsTo(Agent::class, 'agent_id');
}
public function invite()
{
return $this->belongsTo(Invite::class, 'invite_id');
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Modules\Invite\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;
use Livewire\Livewire;
class ModuleServiceProvider extends ServiceProvider
{
protected string $moduleName = 'Invite';
public function register()
{
$this->app->register(RouteServiceProvider::class);
}
public function boot()
{
$this->registerViews();
$this->registerLivewireViews();
$this->registerMigrations();
$this->registerConfig();
$this->registerComponent();
$this->registerLivewire();
}
protected function registerViews()
{
$moduleViewsPath = __DIR__ . '/../Views';
$this->loadViewsFrom(
$moduleViewsPath,
strtolower($this->moduleName)
);
}
protected function registerLivewireViews()
{
$moduleViewsPath = __DIR__ . '/../Views/livewire';
$this->loadViewsFrom(
$moduleViewsPath,
strtolower($this->moduleName)
);
}
protected function registerMigrations()
{
$this->loadMigrationsFrom(
app_path('Modules/' . $this->moduleName . '/Database/Migrations')
);
}
protected function registerConfig()
{
$path = app_path('Modules/' . $this->moduleName . '/Config/config.php');
$this->mergeConfigFrom(
$path,
strtolower($this->moduleName)
);
}
protected function registerLivewire()
{
//Livewire::component('<name>', \Modules\<NAME>\Http\Livewire\<NAME>::class);
}
protected function registerComponent()
{
//Blade::component('document', \Modules\Docs\Http\Components\DocumentComponent::class);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Modules\Invite\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
public function map()
{
$this->registerWebRoutes();
}
protected function registerWebRoutes()
{
//Add Web Routes with web Guard
Route::middleware('web')
//Set Default Controllers Namespace
->namespace('Modules\\Invite\\Http\\Controllers')
->group(app_path('Modules/Invite/Routes/web.php'));
}
}

View File

@ -0,0 +1,17 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Invite\Http\Controllers\InviteController;
Route::middleware(['auth'])->group(function ()
{
Route::get('company/{company}/invites', [InviteController::class, 'index'])->name('company.invites');
Route::get('company/invites', [InviteController::class, 'index'])->name('company.invites.select');
Route::post('company/{company}/create', [InviteController::class, 'create'])->name('company.invites.create');
Route::get('company/invite/{hash}', [InviteController::class, 'open'])->name('company.invite.open');
Route::post('company/invite/{hash}', [InviteController::class, 'process'])->name('company.invite.process');
Route::get('company/invite/{hash}/success', [InviteController::class, 'success'])->name('company.invite.success');
Route::get('company/invite/{invite}/agents', [InviteController::class, 'agents'])->name('company.invite.agents');
Route::post('company/invite/{invite}/delete', [InviteController::class, 'delete'])->name('company.invite.delete');
});

View File

@ -0,0 +1,40 @@
@php($title = 'Зарегистрированные по приглашению агенты')
@extends('layouts.app')
@section('content')
<h4>{{ $invite->company->name }}</h4>
<div class="d-none mb-3 sticky-top bg-light rounded-3">
<div class="p-2 w-100"></div>
<div class="p-2 flex-shrink-1">
<form class="d-none" method="post" action="{{ route('company.invites.create', ['company' => 1]) }}">
@csrf
<input class="btn btn-primary" type="submit" value="Создать приглашение" />
</form>
</div>
</div>
<h5>Зарегистрированные по приглашению агенты</h5>
<div class="row g-2 w-100">
@foreach ($invite->registrations()->orderBy('created_at', 'desc')->get() as $inviteRegistration)
<div class="d-flex flex-row w-100 gap-3 bg-light rounded-3 p-2">
<div class="col flex-fill">
{{ $inviteRegistration->agent->user?->name }}
</div>
<div class="col-2">
{{ $inviteRegistration->created_at->diffForHumans() }}
</div>
<div class="col-1 d-none">
<div class="dropdown d-none d-md-block" style="">
<button class="btn btn-light" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" data-popper-placement="bottom-start">
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endsection

View File

@ -0,0 +1,45 @@
@php($title = 'Приглашения агентов')
@extends('layouts.app')
@section('content')
<div class="d-flex mb-3 sticky-top bg-light rounded-3">
<div class="p-2 w-100"></div>
<div class="p-2 flex-shrink-1">
<form class="" method="post" action="{{ route('company.invites.create', ['company' => 1]) }}">
@csrf
<input class="btn btn-primary" type="submit" value="Создать приглашение" />
</form>
</div>
</div>
<h5>Доступные агентства</h5>
<div class="row g-2 w-100">
@foreach ($companies as $company)
<a href="{{ route('company.invites', ['company' => $company]) }}"
class="p-2 bg-light rounded icon-link icon-link-hover w-100 hstack gap-2 text-decoration-none">
<span class="col-8 text-dark">{{ $company->name }}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-chevron-right ms-auto" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708" />
</svg>
</a>
@endforeach
</div>
<script>
const copyBtns = document.querySelectorAll('.copy-invite-btn');
Array.from(copyBtns).forEach(btn => {
btn.addEventListener('click', function (event) {
targetId = '#' + event.target.dataset.target;
navigator.clipboard.writeText(document.querySelector(targetId).value)
.then(() => {
alert('Ссылка скопирована в буфер обмена');
})
.catch(error => {
console.error(`Ошибка копирования: ${error}`)
});
});
});
</script>
</div>
@endsection

View File

@ -0,0 +1,18 @@
<div class="document-card d-flex border border-1 rounded-3 overflow-hidden">
<div class="p-2 w-100 p-2 bg-light">
<h4>{{ $document->name }}</h4>
<div class="fw-semibold">
{{ $document->description }}
</div>
<div class="fw-lighter text-secondary">Загружен: {{ $document->created_at->diffForHumans() }}</div>
</div>
<div class="download d-flex align-items-center">
<a href="{{ route('docs.download', ['document' => $document]) }}" class="m-auto text-white">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor"
class="bi bi-arrow-down-circle" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.5 4.5a.5.5 0 0 0-1 0v5.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293z" />
</svg>
</a>
</div>
</div>

View File

@ -0,0 +1,32 @@
@php($title = 'Приглашения агентов')
@extends('layouts.guest')
@section('content')
<form class="modal-content" action="{{ route('company.invite.process', ['hash' => $invite->hash]) }}" method="post">
<h1 class="mb-3 fs-5" id="exampleModalLabel">Форма регистрации агента</h1>
<div class="modal-body">
@csrf
в агентство<br><div class="badge bg-primary">{{ $invite->company->name }}</div>
<input type="hidden" class="form-control" id="company_id" name="company_id" value="{{ $invite->company->id }}">
<div class="mb-3 mt-3">
<label for="agentName" class="form-label">Полное имя (ФИО)</label>
<input type="text" class="form-control" id="agentName" name="name" value="{{ old('name') }}">
</div>
<div class="mb-3">
<label for="agentEmail" class="form-label">Электронная почта</label>
<input type="text" class="form-control" id="agentEmail" name="email" value="{{ old('email') }}">
</div>
<div class="mb-3">
<label for="agentPhone" class="form-label">Телефон</label>
<input type="tel" class="form-control" id="agentPhone" name="phone" value="{{ old('phone') }}">
</div>
</div>
<div class="modal-footer gap-3">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<input type="submit" class="btn btn-primary" value="Зарегистрироваться">
</div>
</form>
@endsection

View File

@ -0,0 +1,75 @@
@php($title = 'Приглашения агентов')
@extends('layouts.app')
@section('content')
<div class="d-flex mb-3 sticky-top bg-light rounded-3">
<div class="p-2 w-100"></div>
<div class="p-2 flex-shrink-1">
<form class="" method="post" action="{{ route('company.invites.create', ['company' => 1]) }}">
@csrf
<input class="btn btn-primary" type="submit" value="Создать приглашение" />
</form>
</div>
</div>
<h5>Действующие приглашения для<br><b>{{ $company->name }}</b></h5>
<div class="row g-2 w-100">
@foreach ($invites as $invite)
<div class="d-flex flex-row w-100 gap-3 bg-light rounded-3 p-2 align-items-center">
<div class="col flex-fill">
<div class="input-group">
<input disabled id="invite_{{ $invite->hash }}" value="{{ route('company.invite.open', ['hash' => $invite->hash]) }}" type="text"
class="form-control" placeholder="Recipients username" aria-label="Recipients username"
aria-describedby="basic-addon2">
<button data-target="invite_{{ $invite->hash }}" class="copy-invite-btn btn btn-secondary border-secondry" type="button" id="button-addon2"><i
class="bi bi-clipboard"></i></button>
</div>
</div>
<div class="col-3 d-flex">
Зарегистрировалось:&nbsp;
<div class="d-block rounded rounded-circle bg-secondary text-light text-center"
style="width:1.4rem;height:1.4rem;">
{{ $invite->registrations()->count() }}
</div>
</div>
<div class="col-2">
Создано:<br>{{ $invite->created_at->diffForHumans() }}
</div>
<div class="col-2">
Обновлено:<br>{{ $invite->updated_at->diffForHumans() }}
</div>
<div class="col text-end">
<div class="dropdown d-none d-md-block" style="">
<button class="btn btn-light" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" data-popper-placement="bottom-start">
<a class="dropdown-item"
href="{{ route('company.invite.agents', ['invite' => $invite]) }}">Агенты</a>
<form method="post" action="{{ route('company.invite.delete', ['invite' => $invite]) }}">
@csrf
<button class="dropdown-item" type="submit">Удалить</button>
</form>
</div>
</div>
</div>
</div>
@endforeach
</div>
<script>
const copyBtns = document.querySelectorAll('.copy-invite-btn');
Array.from(copyBtns).forEach(btn => {
btn.addEventListener('click', function (event) {
targetId = '#' + event.target.dataset.target;
navigator.clipboard.writeText(document.querySelector(targetId).value)
.then(() => {
alert('Ссылка скопирована в буфер обмена');
})
.catch(error => {
console.error(`Ошибка копирования: ${error}`)
});
});
});
</script>
</div>
@endsection

View File

@ -0,0 +1,14 @@
@php($title = 'Приглашения агентов')
@extends('layouts.guest')
@section('content')
<div class="p-5 rounded-3 bg-success text-center text-light d-flex flex-column justify-content-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" class="bi bi-hand-thumbs-up-fill" viewBox="0 0 16 16">
<path d="M6.956 1.745C7.021.81 7.908.087 8.864.325l.261.066c.463.116.874.456 1.012.965.22.816.533 2.511.062 4.51a10 10 0 0 1 .443-.051c.713-.065 1.669-.072 2.516.21.518.173.994.681 1.2 1.273.184.532.16 1.162-.234 1.733q.086.18.138.363c.077.27.113.567.113.856s-.036.586-.113.856c-.039.135-.09.273-.16.404.169.387.107.819-.003 1.148a3.2 3.2 0 0 1-.488.901c.054.152.076.312.076.465 0 .305-.089.625-.253.912C13.1 15.522 12.437 16 11.5 16H8c-.605 0-1.07-.081-1.466-.218a4.8 4.8 0 0 1-.97-.484l-.048-.03c-.504-.307-.999-.609-2.068-.722C2.682 14.464 2 13.846 2 13V9c0-.85.685-1.432 1.357-1.615.849-.232 1.574-.787 2.132-1.41.56-.627.914-1.28 1.039-1.639.199-.575.356-1.539.428-2.59z"/>
</svg>
<div>
Регистрация прошла успешно. На указанную электронную почту направлено сообщение с логином и паролем для доступа к агентсткому кабинету {{ $invite->company->name }}
</div>
</div>
@endsection

View File

@ -4,6 +4,7 @@
use Modules\Main\Models\Company\Company;
use Modules\Main\Models\Company\CompanyAdmin;
use Modules\CityManager\Models\CityManager;
use Modules\Main\Models\City;
use Modules\Main\Models\Agent\Agent;
use Modules\Main\Models\Complex;
use Modules\User\Models\Role;
@ -62,7 +63,8 @@ function GetAvailableAgents($resultType = 'Collection')
return $agents->get();
} else {
return $agents;
};
}
;
}
}
@ -77,7 +79,7 @@ function GetAvailableCompanies($resultType = 'Collection')
if ($adminCompany = AdminCompanyOfUser()) {
$companiesIds[] = $adminCompany->id;
}
if ($cityManager = CityManager::where('user_id', auth()->user()->id)) {
if ($cityManager = CityManager::where('user_id', auth()->user()->id)) {
if ($cityManager->count()) {
$companies = Company::whereIn('city_id', $cityManager->pluck('city_id'));
if ($companies->count()) {
@ -93,7 +95,8 @@ function GetAvailableCompanies($resultType = 'Collection')
return $companies->get();
} else {
return $companies;
};
}
;
}
}
@ -123,7 +126,33 @@ function GetAvailableComplexes()
if (!function_exists('GetAvailableCities')) {
function GetAvailableCities()
{
$agents = [];
$user = auth()->user();
if ($user->hasRole(Role::SUPER_ADMIN)) {
return City::all();
}
if ($user->hasRole(Role::CITY_MANAGER)) {
$cititesOfManager = CityManager::where('user_id', $user->id)->pluck('city_id');
if ($cititesOfManager->count()) {
return City::whereIn('id', $cititesOfManager)->get();
}
return false;
}
if ($user->hasRole(Role::COMPANY_ADMIN)) {
$adminCompany = AdminCompanyOfUser();
if ($adminCompany->count()) {
return City::where('id', $adminCompany->city_id)->get();
}
return false;
}
if ($user->hasRole(Role::AGENT)) {
$agentCompany = AgentCompanyOfUser();
if ($agentCompany->count()) {
return City::where('id', $agentCompany->city_id)->get();
}
return false;
}
return false;
/*$agents = [];
if ($adminCompany = AdminCompanyOfUser()) {
$agents = Agent::where('company_id', $adminCompany->id);
} else {
@ -136,6 +165,6 @@ function GetAvailableCities()
}
$agents->with('company:id,name');
$agents->with('user:id,name');
return $agents->get();
return $agents->get();*/
}
}

View File

@ -14,7 +14,7 @@
class CreateAgentController extends Controller
{
public function __invoke(Request $request)
public function __invoke(Request $request, )
{
$company = Company::findOrFail($request->company_id);
if ($request->user()->cannot('update', $company))

View File

@ -0,0 +1,28 @@
<?php
namespace Modules\Main\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class CityScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*/
public function apply(Builder $builder, Model $model): void
{
//dd(GetAvailableCities());
if ($cities = GetAvailableCities()) { //получаю доступные пользователю города
$citiesIds = [];
foreach ($cities as $city) {
$citiesIds[] = $city->id;
}
$builder->whereHas('cities', fn($q) => $q->whereIn('cities.id', $citiesIds));
$builder->OrDoesntHave('cities');
} else {
$builder->where('id', 0);//не знаю, как сделать, чтобы выбор был гарантированно пустой
}
}
}
?>

View File

@ -29,12 +29,16 @@ public function boot()
$this->registerLivewire();
$this->registerHelpers();
try {
define('DESIGN_PARAMETERS', Design::getParameters());
if (!defined('DESIGN_PARAMETERS')) {
define('DESIGN_PARAMETERS', Design::getParameters());
}
if (array_key_exists('title', DESIGN_PARAMETERS)) {
\Config::set('app.name', DESIGN_PARAMETERS['title']);
}
} catch (\Exception $e) {
define('DESIGN_PARAMETERS', []);
if (!defined('DESIGN_PARAMETERS')) {
define('DESIGN_PARAMETERS', []);
}
}
;
}

View File

@ -6,6 +6,8 @@
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Modules\Main\Models\CityAppend;
use Modules\Main\Models\City;
use Modules\User\Models\User;
trait CityAppendTrait
{
@ -14,9 +16,13 @@ public function cities(): MorphMany
return $this->MorphMany(CityAppend::class, 'append');
}
public function byCity(Builder $query, $cityId)
public function byCity()
{
//return $query->where('category', $category);
$user = null;
settype($user, User::class);
$user = auth()->user();
return true;
}
}

View File

@ -17,11 +17,18 @@
<label class="btn p-2 fs-5" for="option7">Уволенные</label>
</div>
<div class="ms-auto p-2">
<button type="button" class="btn btn-primary py-2 px-3 fs-5" data-bs-toggle="modal"
data-bs-target="#createAgentModal">
<i class="bi bi-person-plus"></i>
</button>
<div class="ms-auto p-2 d-flex gap-2">
<div class="dropdown">
<button class="btn btn-primary fs-5 py-2 px-3 dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-person-plus"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#createAgentModal">Добавить вручную</a></li>
<li><a class="dropdown-item" href="{{ route('company.invites.select') }}">Приглашения</a></li>
</ul>
</div>
</div>
</form>
@if (!$status || $status == 'all' || $status == $statuses::ACTIVE)
@ -39,4 +46,4 @@
@endif
</div>
@livewire('company.agent.create', ['containerId' => 'createAgentModal'])
@endsection
@endsection

View File

@ -1,12 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Docs\Http\Controllers\DocsController;
Route::middleware(['auth'])->group(function ()
{
Route::get('/doc', [DocsController::class, 'index'])->name('docs.index');
Route::get('/docs/{document}/download', [DocsController::class, 'download'])->name('docs.download');
});

View File

@ -25,5 +25,7 @@ public function open(Post $post)
'post' => $post
]);
}
public function create() {
return view('post::form.create');
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Modules\Post\Http\Livewire;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Livewire\Attributes\On;
class PostCreator extends Component
{
public $categories = [];
protected function rules()
{
return [
];
}
protected function messages()
{
return [
];
}
public function mount()
{
}
public function updated($propertyName)
{
}
public function render()
{
return view(
'post::form.index'
);
}
public function rendered()
{
$this->dispatch('phoneInputAdded');
}
public function resetData()
{
$this->mount();
}
public function back()
{
$this->status = FormStatus::IN_PROCESS;
}
public function save()
{
$hasErrors = false;
foreach ($this->selectedObjects as $complexId=>$room) {
if ($this->createDeal($complexId, $room)) {
unset($this->selectedObjects[$complexId]);
} else {
$hasErrors = true;
}
}
if ($hasErrors) {
return $this->status = FormStatus::ERROR;
}
$this->status = FormStatus::SUCCESS;
}
private function createDeal($complexId, $room)
{
if (
!$deal = Deal::create([
'agent_id' => $this->agentId,
'complex_id' => $complexId,
'plan7_data' => (is_array($room) && array_key_exists('id', $room)) ? json_encode($room) : null
])
) {
return false;
}
foreach ($this->contacts as $contact) {
if (
!$newUser = Client::updateOrCreate(
['phone' => $contact['phones'][0]],
[
'name' => trim($contact['firstName'] . ' ' . $contact['secondName']),
'phone' => $contact['phones'][0]
]
)
) {
return false;
}
if (
!$dealClient = DealClients::firstOrCreate([
'deal_id' => $deal->id,
'client_id' => $newUser->id
])
) {
return false;
}
}
$this->dispatch('clientCreated');
return true;
}
}

View File

@ -3,16 +3,18 @@
namespace Modules\Post\Models;
use Modules\Main\Models\City;
use Modules\Post\Models\PostCity;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Modules\Main\Traits\CityAppendTrait;
use Modules\Main\Models\Scopes\CityScope;
class Post extends Model
{
use HasFactory;
use CityAppendTrait;
protected $fillable = [
'name',
@ -21,4 +23,21 @@ class Post extends Model
'category',
'image'
];
protected static function booted(): void
{
static::addGlobalScope(new CityScope);
}
public function cities(): HasManyThrough
{
return $this->hasManyThrough(
City::class,
PostCity::class,
'post_id',
'id',
'id',
'city_id'
);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Modules\Post\Models;
use Modules\Main\Models\City;
use Illuminate\Database\Eloquent\Model;
use Modules\Main\Models\Scopes\CityScope;
class PostCity extends Model
{
protected $fillable = [
'post_id',
'city_id',
];
}

View File

@ -63,7 +63,7 @@ protected function registerLivewire()
{
Livewire::component('posts.list', \Modules\Post\Http\Livewire\PostsList::class);
Livewire::component('post.card', \Modules\Post\Http\Livewire\PostCard::class);
Livewire::component('post.form', \Modules\Post\Http\Livewire\PostCreator::class);
}
protected function registerComponent()

View File

@ -5,4 +5,5 @@
Route::middleware(['auth'])->group(function ()
{
Route::get('/news', [Modules\Post\Http\Controllers\PostController::class, 'index'])->name('posts');
Route::get('/news/create', [Modules\Post\Http\Controllers\PostController::class, 'create'])->name('post.create');
});

View File

@ -0,0 +1,7 @@
@extends('layouts.app')
@section('content')
<div class="container">
@include('admin::posts.path.create-form')
</div>
@endsection

View File

@ -49,5 +49,15 @@
<div>
@livewire('posts.list', $filter)
</div>
</div>
<div class="position-fixed bottom-0 end-0 me-0 me-md-5" style="z-index:2000">
<a class="m-2 me-md-5 btn btn-primary rounded-circle d-flex justify-content-center align-items-center"
style="width:4rem;height:4rem" href="{{ route('post.create') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-plus-lg"
viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2" />
</svg>
</a>
</div>
</div>
@endsection

View File

@ -1,5 +1,4 @@
@extends('layouts.guest')
@section('content')
<div class="">
<div class="">

View File

@ -15,33 +15,28 @@
|
*/
Route::get('/', function ()
{
Route::get('/', function () {
return view(view: 'welcome');
})->name('welcome');
Auth::routes();
Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request)
{
Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
$request->fulfill();
return redirect('/home');
})->middleware(['auth', 'signed'])->name('verification.verify');
Route::middleware(['auth'])->group(function ()
{
Route::middleware(['auth'])->group(function () {
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::get('/clients/table', [App\Http\Controllers\ClientsTableController::class, 'index'])->name('clients.table');
});
//МАКЕТЫ
Route::get('projects', function ()
{
Route::get('projects', function () {
return view(view: 'makets.projects');
});
Route::get('profile', function ()
{
Route::get('profile', function () {
return view(view: 'user.profile');
});

View File

@ -16,7 +16,7 @@ export default defineConfig({
'resources/css/docs.css',
'resources/css/multiselect.css',
'resources/js/phone-format.js',
'resources/js/trix.umd.min.js',
'resources/js/trix-min.js',
'resources/fonts/TikTokSans.ttf',
'resources/woff/bootstrap-icons.woff2',
],