coursework tasks 1 to 10

This commit is contained in:
Владимир
2026-01-07 11:55:53 +00:00
parent 9f637e6be7
commit bbe639b604
13 changed files with 545 additions and 51 deletions

View File

@@ -0,0 +1,169 @@
<?php
namespace App\Http\Controllers;
use App\Models\EmployeeAvailability;
use App\Models\User;
use Illuminate\Http\Request;
class AvailabilitiesController extends Controller
{
// GET api/admin/availabilities?employee_id=5&date=2025-06-15
public function index(Request $request)
{
$query = EmployeeAvailability::query();
if ($request->employee_id) {
$query->where('employee_id', $request->employee_id);
}
if ($request->date) {
$query->where('date', $request->date);
}
$availabilities = $query->get();
return response()->json($availabilities);
}
// POST api/admin/availabilities - создать один слот
public function store(Request $request)
{
$request->validate([
'employee_id' => 'required|exists:users,id',
'date' => 'required|date',
'starttime' => 'required',
'endtime' => 'required|after:starttime',
'isavailable' => 'boolean'
]);
$availability = EmployeeAvailability::create($request->all());
return response()->json($availability, 201);
}
// POST api/admin/availabilities/bulk - создать несколько слотов
public function bulkStore(Request $request)
{
$request->validate([
'employee_id' => 'required|exists:users,id',
'date' => 'required|date',
'intervals' => 'required|array|min:1'
]);
$availabilities = [];
foreach ($request->intervals as $interval) {
$availability = EmployeeAvailability::create([
'employee_id' => $request->employee_id,
'date' => $request->date,
'starttime' => $interval['start'],
'endtime' => $interval['end'],
'isavailable' => true
]);
$availabilities[] = $availability;
}
return response()->json($availabilities, 201);
}
// DELETE api/admin/availabilities/{id} - удалить слот (брони остаются!)
public function destroy($id)
{
$availability = EmployeeAvailability::findOrFail($id);
$availability->delete();
return response()->json(['message' => 'Слот удален из расписания (брони сохранены)']);
}
public function publicAvailability(Request $request)
{
$serviceId = $request->query('service_id');
$date = $request->query('date');
if (!$serviceId || !$date) {
return response()->json(['error' => 'service_id и date обязательны'], 400);
}
// Найти услугу и получить длительность
$service = \App\Models\Services::find($serviceId);
if (!$service) {
return response()->json(['error' => 'Услуга не найдена'], 404);
}
$durationMinutes = $service->durationminutes;
// Найти сотрудников с расписанием на эту дату
$availabilities = \App\Models\EmployeeAvailability::where('date', $date)
->where('isavailable', true)
->with('employee') // связь с User
->get();
$freeSlots = [];
foreach ($availabilities as $availability) {
$employeeId = $availability->employee_id;
// Найти занятые слоты этого сотрудника
$bookings = \App\Models\Booking::where('employee_id', $employeeId)
->where('bookingdate', $date)
->where('status', '!=', 'cancelled')
->pluck('starttime', 'endtime');
// Генерировать возможные слоты с учетом duration
$start = new \DateTime($availability->starttime);
$end = new \DateTime($availability->endtime);
$current = clone $start;
while ($current < $end) {
$slotEnd = clone $current;
$slotEnd->modify("+{$durationMinutes} minutes");
// Проверить пересечение с бронями
$isFree = true;
foreach ($bookings as $bookingStart => $bookingEnd) {
$bookingStartTime = new \DateTime($bookingStart);
$bookingEndTime = new \DateTime($bookingEnd);
if ($current < $bookingEndTime && $slotEnd > $bookingStartTime) {
$isFree = false;
break;
}
}
if ($isFree && $slotEnd <= $end) {
$freeSlots[] = [
'employee_id' => $employeeId,
'start' => $current->format('H:i'),
'end' => $slotEnd->format('H:i')
];
}
$current->modify('+30 minutes'); // шаг 30 мин
}
}
return response()->json($freeSlots);
}
public function cancel(Request $request, $id)
{
$booking = Booking::findOrFail($id);
// Проверка: только автор брони может отменить
if ($booking->client_id != auth()->id()) {
return response()->json(['error' => 'Можете отменить только свою бронь'], 403);
}
// Проверка: нельзя отменить уже отмененную/выполненную
if ($booking->status != 'confirmed') {
return response()->json(['error' => 'Можно отменить только подтвержденные брони'], 400);
}
// Обновить статус
$booking->update([
'status' => 'cancelled',
'cancelledby' => 'client',
'cancelreason' => $request->reason ?? null
]);
return response()->json([
'message' => 'Бронь отменена',
'booking' => $booking
]);
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers;
use App\Models\Booking;
use App\Models\Services;
use App\Models\EmployeeAvailability;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class BookingsController extends Controller
{
// POST api/bookings - создание брони (ТОЛЬКО клиенты)
public function store(Request $request)
{
$request->validate([
'service_id' => 'required|exists:services,id',
'employee_id' => 'required|exists:users,id',
'date' => 'required|date',
'starttime' => 'required'
]);
$clientId = auth()->id();
if (!$clientId) {
return response()->json(['error' => 'Авторизация обязательна'], 401);
}
// Проверить активную услугу
$service = Services::where('id', $request->service_id)
->where('isactive', true)
->first();
if (!$service) {
return response()->json(['error' => 'Услуга неактивна или не найдена'], 400);
}
$durationMinutes = $service->durationminutes;
$endtime = date('H:i:s', strtotime($request->starttime . " +{$durationMinutes} minutes"));
// Проверить доступность сотрудника
$availability = EmployeeAvailability::where('employee_id', $request->employee_id)
->where('date', $request->date)
->where('starttime', '<=', $request->starttime)
->where('endtime', '>=', $endtime)
->where('isavailable', true)
->first();
if (!$availability) {
return response()->json(['error' => 'Сотрудник недоступен в это время'], 400);
}
// Проверить уникальность слота
$bookingExists = Booking::where('employee_id', $request->employee_id)
->where('bookingdate', $request->date)
->where('starttime', $request->starttime)
->whereIn('status', ['confirmed', 'completed'])
->exists();
if ($bookingExists) {
return response()->json(['error' => 'Слот уже забронирован'], 400);
}
// Создать бронь
$bookingNumber = 'CL-' . date('Y') . '-' . str_pad(Booking::count() + 1, 4, '0', STR_PAD_LEFT);
$booking = Booking::create([
'bookingnumber' => $bookingNumber,
'client_id' => $clientId,
'employee_id' => $request->employee_id,
'service_id' => $request->service_id,
'bookingdate' => $request->date,
'starttime' => $request->starttime,
'endtime' => $endtime,
'status' => 'confirmed'
]);
return response()->json([
'booking' => $booking,
'message' => 'Бронирование создано №' . $bookingNumber
], 201);
}
// POST api/bookings/{id}/cancel - отмена клиентом
public function cancel(Request $request, $id)
{
$booking = Booking::findOrFail($id);
// Только автор брони может отменить
if ($booking->client_id != auth()->id()) {
return response()->json(['error' => 'Можете отменить только свою бронь'], 403);
}
// Только confirmed брони
if ($booking->status != 'confirmed') {
return response()->json(['error' => 'Можно отменить только подтвержденные'], 400);
}
$booking->update([
'status' => 'cancelled',
'cancelledby' => 'client',
'cancelreason' => $request->reason ?? null
]);
return response()->json([
'message' => 'Бронь отменена',
'booking' => $booking
]);
}
}

View File

@@ -2,9 +2,78 @@
namespace App\Http\Controllers;
use App\Models\Services;
use Illuminate\Http\Request;
class ServicesController extends Controller
{
//
// GET api/admin/services - список активных услуг
public function index()
{
$services = Services::where('isactive', true)->get();
return response()->json($services);
}
// POST api/admin/services - создать услугу
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'description' => 'required|string',
'durationminutes' => 'required|integer|min:1|max:500',
'price' => 'required|numeric|min:0'
]);
$service = Services::create([
'name' => $request->name,
'description' => $request->description,
'durationminutes' => $request->durationminutes,
'price' => $request->price,
'isactive' => true // по умолчанию активна
]);
return response()->json($service, 201);
}
// PUT api/admin/services/{id} - обновить услугу
public function update(Request $request, $id)
{
$service = Services::findOrFail($id);
$request->validate([
'name' => 'required|string|max:255',
'description' => 'required|string',
'durationminutes' => 'required|integer|min:1|max:500',
'price' => 'required|numeric|min:0'
]);
$service->update([
'name' => $request->name,
'description' => $request->description,
'durationminutes' => $request->durationminutes,
'price' => $request->price,
]);
return response()->json($service);
}
// DELETE api/admin/services/{id} - только если нет активных броней
public function destroy($id)
{
$service = Services::findOrFail($id);
// ПРОВЕРКА: нельзя удалить услугу с активными бронями
$activeBookings = \App\Models\Booking::where('service_id', $id)
->where('status', '!=', 'cancelled')
->exists();
if ($activeBookings) {
return response()->json([
'error' => 'Нельзя удалить услугу с активными бронями'
], 400);
}
$service->delete();
return response()->json(['message' => 'Услуга удалена']);
}
}

View File

@@ -5,32 +5,30 @@ namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use SebastianBergmann\CodeCoverage\Report\Xml\Project;
class UserController extends Controller
{
public function create(Request $request)
{
$user = new User();
$name = $request->get(key:'name');
$password = Hash::make($request->get(key:'password'));
$email = $request->get(key: 'email');
// /api/register - ТОЛЬКО клиенты (role = client)
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:6'
]);
$user->name = $name;
$user->email = $email;
$user->password = $password;
$user->save();
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'role' => 'client' // ТОЛЬКО клиенты
]);
dispatch(function() use ($user) {
$project= new Project();
$project->title = 'default project';
$project->description = 'test';
$project->creator_user_id = $user->id;
$project->save();
});
return ['toker' => $user->createToken('frotend')];
}
$token = $user->createToken('client-token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token
], 201);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckRole
{
public function handle(Request $request, Closure $next, $role)
{
if (!auth()->check() || !auth()->user()->isEmployeeOrAdmin()) {
return response()->json(['error' => 'Доступ запрещен'], 403);
}
return $next($request);
}
}

23
app/Models/Booking.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
// бронирование ĸлиентов
class Booking extends Model {
use HasFactory;
protected $table = 'bookings';
protected $fillable = [
'bookingnumber',
'client_id',
'employee_id',
'service_id',
'bookingdate',
'starttime',
'endtime',
'status',
'cancelledby',
'cancelreason'
];
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
// расписание сотрудниĸов
class EmployeeAvailability extends Model {
use HasFactory;
protected $table = 'employee_availabilities';
protected $fillable = ['employee_id','date','starttime','endtime','isavailable'];
}

View File

@@ -2,9 +2,24 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Services extends Model
class Services extends Model // услуги ĸлининга
{
//
use HasFactory;
protected $fillable = [
'name',
'description',
'durationminutes',
'price',
'isactive',
];
// Простая связь с bookings, если нужно
public function bookings()
{
return $this->hasMany(Booking::class);
}
}

View File

@@ -2,43 +2,27 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
class User extends Authenticatable // все пользователи системы
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
'role',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
@@ -46,4 +30,16 @@ class User extends Authenticatable
'password' => 'hashed',
];
}
// Проверяет админ или сотрудник
public function isEmployeeOrAdmin()
{
return $this->role == 'employee' || $this->role == 'admin';
}
// Для запросов - все сотрудники и админы
public static function scopeEmployeeOrAdmin($query)
{
return $query->whereIn('role', ['employee', 'admin']);
}
}

View File

@@ -11,9 +11,11 @@ return Application::configure(basePath: dirname(__DIR__))
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role:employee' => \App\Http\Middleware\CheckRole::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();

View File

@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up() {
Schema::create('employee_availabilities', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('employee_id');
$table->date('date');
$table->time('starttime');
$table->time('endtime');
$table->boolean('isavailable')->default(true);
$table->timestamps(); // автоматическое создание created_at и updated_at
$table->foreign('employee_id')->references('id')->on('users');
});
}
public function down() {
Schema::dropIfExists('employee_availabilities');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up() {
Schema::create('bookings', function (Blueprint $table) {
$table->id();
$table->string('bookingnumber');
$table->unsignedBigInteger('client_id');
$table->unsignedBigInteger('employee_id');
$table->unsignedBigInteger('service_id');
$table->date('bookingdate');
$table->time('starttime');
$table->time('endtime');
$table->enum('status', ['confirmed', 'cancelled', 'completed'])->default('confirmed');
$table->enum('cancelledby', ['client', 'admin'])->nullable();
$table->text('cancelreason')->nullable();
$table->timestamps();
// Внешние ключи
$table->foreign('client_id')->references('id')->on('users');
$table->foreign('employee_id')->references('id')->on('users');
$table->foreign('service_id')->references('id')->on('services');
// УНИКАЛЬНЫЙ ИНДЕКС
$table->unique(['employee_id', 'bookingdate', 'starttime'], 'unique_booking_slot');
});
}
public function down() {
Schema::dropIfExists('bookings');
}
};

View File

@@ -1,16 +1,45 @@
<?php
use App\Http\Controllers\UserController;
use App\Http\Controllers\ServicesController;
use App\Http\Controllers\BookingsController;
use App\Http\Controllers\AvailabilitiesController;
use App\Http\Controllers\CategoriesController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CategoriesController;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
// РЕГИСТРАЦИЯ ТОЛЬКО КЛИЕНТОВ (публичный)
Route::post('/register', [UserController::class, 'register']);
// Существующие роуты categories
Route::get('/categories', [CategoriesController::class, 'index'])->middleware('auth:sanctum');
Route::get('/categories/{id}', [CategoriesController::class, 'show']);
Route::post( '/categories', [CategoriesController::class, 'create']);
Route::post( '/users', [UserController::class, 'create']);
Route::post('/categories', [CategoriesController::class, 'create']);
// ПУБЛИЧНЫЙ API доступности (без авторизации)
Route::get('/availability', [AvailabilitiesController::class, 'publicAvailability']);
// КЛИЕНТСКИЕ РОУТЫ БРОНИРОВАНИЙ (auth:sanctum)
Route::middleware('auth:sanctum')->group(function () {
Route::post('/bookings', [BookingsController::class, 'store']); // Пункт 9
Route::post('/bookings/{id}/cancel', [BookingsController::class, 'cancel']); // Пункт 10
});
// АДМИН РОУТЫ - ТОЛЬКО employee/admin (role:employee)
Route::middleware(['auth:sanctum', 'role:employee'])->prefix('admin')->group(function () {
// CRUD услуги
Route::get('/services', [ServicesController::class, 'index']);
Route::post('/services', [ServicesController::class, 'store']);
Route::put('/services/{id}', [ServicesController::class, 'update']);
Route::delete('/services/{id}', [ServicesController::class, 'destroy']);
// CRUD расписание
Route::get('/availabilities', [AvailabilitiesController::class, 'index']);
Route::post('/availabilities', [AvailabilitiesController::class, 'store']);
Route::post('/availabilities/bulk', [AvailabilitiesController::class, 'bulkStore']);
Route::delete('/availabilities/{id}', [AvailabilitiesController::class, 'destroy']);
});