← Назад к вопросам

Менеджер задач с ролями

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"]);
    }
}

Это полное решение с ролями, ограничениями и управлением статусами.

Менеджер задач с ролями | PrepBro