← Назад к вопросам
Менеджер задач с ролями
2.0 Middle🔥 251 комментариев
#Архитектура и паттерны#Безопасность#Фреймворки
Условие
Реализовать менеджер задач на Laravel с системой ролей.
Роли
- Менеджер: видит список всех задач, может менять статус
- Клиент: видит форму создания задачи и свои задачи
Требования
- Регистрация и авторизация через Laravel Auth
- CRUD для задач
- Статусы задач: новая, в работе, завершена
- Пересечение задач у клиента - максимум 4 активных
- Ограничение частоты создания задач (не чаще 1 в минуту)
- Отметка задачи как выполненной
Технологии
Laravel, MySQL, Bootstrap/Tailwind
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
1. Миграции для таблиц
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
// Таблица ролей
Schema::create("roles", function (Blueprint $table) {
$table->id();
$table->string("name", 50)->unique();
$table->timestamps();
});
// Добавляем role_id к пользователям
Schema::table("users", function (Blueprint $table) {
$table->foreignId("role_id")->constrained("roles")->default(2);
});
// Таблица задач
Schema::create("tasks", function (Blueprint $table) {
$table->id();
$table->foreignId("user_id")->constrained("users");
$table->string("title", 255);
$table->text("description")->nullable();
$table->enum("status", ["new", "in_progress", "completed"])->default("new");
$table->timestamp("last_created_at")->nullable();
$table->timestamps();
$table->index("user_id");
$table->index("status");
});
}
public function down(): void {
Schema::dropIfExists("tasks");
Schema::table("users", function (Blueprint $table) {
$table->dropForeignIdFor("roles");
$table->dropColumn("role_id");
});
Schema::dropIfExists("roles");
}
};
2. Модели
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable {
use HasFactory, Notifiable;
protected $fillable = ["name", "email", "password", "role_id"];
protected $hidden = ["password"];
protected $casts = ["email_verified_at" => "datetime"];
public function tasks(): HasMany {
return $this->hasMany(Task::class);
}
public function isManager(): bool {
return $this->role_id === 1;
}
public function isClient(): bool {
return $this->role_id === 2;
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Task extends Model {
protected $fillable = ["user_id", "title", "description", "status", "last_created_at"];
protected $casts = ["created_at" => "datetime", "updated_at" => "datetime"];
public function user(): BelongsTo {
return $this->belongsTo(User::class);
}
public function scopeActive($query) {
return $query->whereIn("status", ["new", "in_progress"]);
}
public function scopeCompleted($query) {
return $query->where("status", "completed");
}
}
3. Сидер для ролей
<?php
namespace Database\Seeders;
use App\Models\Role;
use Illuminate\Database\Seeder;
class RoleSeeder extends Seeder {
public function run(): void {
Role::create(["id" => 1, "name" => "manager"]);
Role::create(["id" => 2, "name" => "client"]);
}
}
4. Service для бизнес-логики
<?php
namespace App\Services;
use App\Models\Task;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Validation\ValidationException;
class TaskService {
const MAX_ACTIVE_TASKS = 4;
const MIN_SECONDS_BETWEEN_CREATION = 60;
public function createTask(User $user, string $title, ?string $description): Task {
// Проверка: максимум активных задач
$activeTasks = Task::where("user_id", $user->id)
->active()
->count();
if ($activeTasks >= self::MAX_ACTIVE_TASKS) {
throw ValidationException::withMessages([
"title" => "Максимум 4 активных задачи. Завершите текущие перед созданием новых."
]);
}
// Проверка: частота создания (не более 1 в минуту)
$lastTask = Task::where("user_id", $user->id)
->orderBy("created_at", "desc")
->first();
if ($lastTask) {
$secondsElapsed = Carbon::now()->diffInSeconds($lastTask->created_at);
if ($secondsElapsed < self::MIN_SECONDS_BETWEEN_CREATION) {
$waitSeconds = self::MIN_SECONDS_BETWEEN_CREATION - $secondsElapsed;
throw ValidationException::withMessages([
"title" => "Подождите $waitSeconds секунд перед созданием новой задачи."
]);
}
}
return Task::create([
"user_id" => $user->id,
"title" => $title,
"description" => $description,
"status" => "new",
"last_created_at" => Carbon::now(),
]);
}
public function updateStatus(Task $task, string $status): Task {
$validStatuses = ["new", "in_progress", "completed"];
if (!in_array($status, $validStatuses)) {
throw ValidationException::withMessages([
"status" => "Неверный статус."
]);
}
$task->update(["status" => $status]);
return $task;
}
}
5. Контроллер для задач
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use App\Services\TaskService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class TaskController extends Controller {
protected $taskService;
public function __construct(TaskService $taskService) {
$this->taskService = $taskService;
$this->middleware("auth");
}
public function index() {
$user = Auth::user();
if ($user->isManager()) {
$tasks = Task::with("user")->paginate(20);
} else {
$tasks = $user->tasks()->paginate(20);
}
return view("tasks.index", ["tasks" => $tasks]);
}
public function create() {
if (!Auth::user()->isClient()) {
abort(403, "Только клиенты могут создавать задачи.");
}
return view("tasks.create");
}
public function store(Request $request) {
$validated = $request->validate([
"title" => "required|string|max:255",
"description" => "nullable|string|max:1000",
]);
try {
$task = $this->taskService->createTask(
Auth::user(),
$validated["title"],
$validated["description"] ?? null
);
return redirect()->route("tasks.index")->with("success", "Задача создана.");
} catch (\Exception $e) {
return back()->withErrors(["title" => $e->getMessage()]);
}
}
public function show(Task $task) {
if (!Auth::user()->isManager() && $task->user_id !== Auth::id()) {
abort(403);
}
return view("tasks.show", ["task" => $task]);
}
public function edit(Task $task) {
if (!Auth::user()->isManager()) {
abort(403, "Только менеджеры могут редактировать.");
}
return view("tasks.edit", ["task" => $task]);
}
public function update(Request $request, Task $task) {
if (!Auth::user()->isManager()) {
abort(403);
}
$validated = $request->validate([
"status" => "required|in:new,in_progress,completed",
]);
$this->taskService->updateStatus($task, $validated["status"]);
return redirect()->route("tasks.show", $task)->with("success", "Статус обновлен.");
}
public function destroy(Task $task) {
if (!Auth::user()->isManager() && $task->user_id !== Auth::id()) {
abort(403);
}
$task->delete();
return redirect()->route("tasks.index")->with("success", "Задача удалена.");
}
}
6. Маршруты
<?php
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;
Route::middleware(["auth"])->group(function () {
Route::resource("tasks", TaskController::class);
});
Auth::routes();
7. Blade шаблон списка (resources/views/tasks/index.blade.php)
@extends("layouts.app")
@section("content")
<div class="container">
@if(Auth::user()->isClient())
<a href="{{ route('tasks.create') }}" class="btn btn-primary mb-3">Создать задачу</a>
@endif
@if($tasks->count())
<table class="table table-striped">
<thead>
<tr>
<th>Заголовок</th>
<th>Статус</th>
<th>Клиент</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@foreach($tasks as $task)
<tr>
<td>{{ $task->title }}</td>
<td>
@if(Auth::user()->isManager())
<form action="{{ route('tasks.update', $task) }}" method="POST" style="display:inline;">
@csrf
@method('PUT')
<select name="status" onchange="this.form.submit()" class="form-select">
<option value="new" @if($task->status == 'new') selected @endif>Новая</option>
<option value="in_progress" @if($task->status == 'in_progress') selected @endif>В работе</option>
<option value="completed" @if($task->status == 'completed') selected @endif>Завершена</option>
</select>
</form>
@else
<span class="badge">{{ ucfirst(str_replace('_', ' ', $task->status)) }}</span>
@endif
</td>
<td>{{ $task->user->name }}</td>
<td>{{ $task->created_at->format('d.m.Y H:i') }}</td>
<td>
<a href="{{ route('tasks.show', $task) }}" class="btn btn-sm btn-info">Просмотр</a>
@if(Auth::user()->isManager())
<form action="{{ route('tasks.destroy', $task) }}" method="POST" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Удалить?')">Удалить</button>
</form>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $tasks->links() }}
@else
<div class="alert alert-info">Нет задач</div>
@endif
</div>
@endsection
8. Тесты
<?php
namespace Tests\Feature;
use App\Models\Task;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TaskTest extends TestCase {
use RefreshDatabase;
public function test_client_can_create_task(): void {
$user = User::factory()->create(["role_id" => 2]);
$response = $this->actingAs($user)
->post(route("tasks.store"), [
"title" => "Test Task",
"description" => "Test"
]);
$response->assertRedirect();
$this->assertDatabaseHas("tasks", ["title" => "Test Task"]);
}
public function test_client_cannot_create_more_than_4_active(): void {
$user = User::factory()->create(["role_id" => 2]);
for ($i = 0; $i < 4; $i++) {
Task::factory()->create(["user_id" => $user->id, "status" => "new"]);
}
$response = $this->actingAs($user)
->post(route("tasks.store"), ["title" => "Test", "description" => "Test"]);
$response->assertSessionHasErrors();
}
public function test_manager_can_change_status(): void {
$manager = User::factory()->create(["role_id" => 1]);
$task = Task::factory()->create(["status" => "new"]);
$response = $this->actingAs($manager)
->put(route("tasks.update", $task), ["status" => "in_progress"]);
$response->assertRedirect();
$this->assertDatabaseHas("tasks", ["id" => $task->id, "status" => "in_progress"]);
}
}
Это полное решение с ролями, ограничениями и управлением статусами.