разработка функции публикации новостей от менеджеров

This commit is contained in:
developer 2026-04-24 18:37:41 +08:00
parent 484e364831
commit 54207dcf50
17 changed files with 317 additions and 136 deletions

View File

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

View File

@ -1,55 +1,5 @@
@php($title = 'Новости') @php($title = 'Новости')
@extends('layouts.admin') @extends('layouts.admin')
@section('content') @section('content')
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css"> @include('admin::posts.path.create-form')
<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>
@endsection @endsection

View File

@ -1,54 +1,5 @@
@php($title = 'Новости') @php($title = 'Новости')
@extends('layouts.admin') @extends('layouts.admin')
@section('content') @section('content')
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css"> @include('admin::posts.path.edit-form')
@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>
@endsection @endsection

View File

@ -0,0 +1,61 @@
<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="">
@if($availableCities = GetAvailableCities())
@foreach($availableCities as $key=>$city)
<div class="form-check">
<input name="cities[{{ $key }}]" class="form-check-input" type="checkbox" value="{{ $city->id }}" id="city_ {{ $city->id }}">
<label class="form-check-label" for="city_{{ $city->id }}">
{{ $city->name }}
</label>
</div>
@endforeach
@endif
</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

@ -131,7 +131,7 @@ function GetAvailableCities()
return City::all(); return City::all();
} }
if ($user->hasRole(Role::CITY_MANAGER)) { if ($user->hasRole(Role::CITY_MANAGER)) {
$cititesOfManager = CityManager::where('user_id', $user->id)->pluck('city_id')->get(); $cititesOfManager = CityManager::where('user_id', $user->id)->pluck('city_id');
if ($cititesOfManager->count()) { if ($cititesOfManager->count()) {
return City::whereIn('id', $cititesOfManager)->get(); return City::whereIn('id', $cititesOfManager)->get();
} }

View File

@ -12,21 +12,17 @@ class CityScope implements Scope
*/ */
public function apply(Builder $builder, Model $model): void public function apply(Builder $builder, Model $model): void
{ {
if ($model->cities()->count()) { //если города для новости установлены //dd(GetAvailableCities());
if ($cities = GetAvailableCities()) { //получаю доступные пользователю города if ($cities = GetAvailableCities()) { //получаю доступные пользователю города
$citiesIds = []; $citiesIds = [];
foreach ($cities as $city) { foreach ($cities as $city) {
$citiesIds[] = $city->id; $citiesIds[] = $city->id;
} }
$builder->whereHas('cities', fn($q) => $q->whereIn('cities.id', $citiesIds)); $builder->whereHas('cities', fn($q) => $q->whereIn('cities.id', $citiesIds));
$builder->OrDoesntHave('cities');
} else { } else {
$builder->where('id', 0);//не знаю, как сделать, чтобы выбор был гарантированно пустой $builder->where('id', 0);//не знаю, как сделать, чтобы выбор был гарантированно пустой
} }
} else {
}
//$builder->where('created_at', '<', now()->minus(years: 2000));
} }
} }
?> ?>

View File

@ -25,5 +25,7 @@ public function open(Post $post)
'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

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

View File

@ -5,4 +5,5 @@
Route::middleware(['auth'])->group(function () Route::middleware(['auth'])->group(function ()
{ {
Route::get('/news', [Modules\Post\Http\Controllers\PostController::class, 'index'])->name('posts'); 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> <div>
@livewire('posts.list', $filter) @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> </div>
@endsection @endsection

View File

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