diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index a3e5c96..072d3ab 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -5,61 +5,31 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; +use App\Models\User; +use Illuminate\Support\Facades\Hash; + class AuthController extends Controller { - // Регистрация нового пользователя - public function register(Request $request) - { - $validator = Validator::make($request->all(), [ - 'name' => 'required|string|max:255', - 'email' => 'required|email|unique:users,email', - 'password' => 'required|string|min:6', - ]); - - if ($validator->fails()) { - return response()->json([ - 'message' => 'Ошибка валидации', - 'errors' => $validator->errors() - ], 422); - } - - $user = \App\Models\User::create([ - 'name' => $request->name, - 'email' => $request->email, - 'password' => bcrypt($request->password), - 'phone' => $request->phone ?? null, - 'role' => 'client', // по умолчанию клиент - ]); - - // Создаём токен для Sanctum - $token = $user->createToken('main-token')->plainTextToken; - - return response()->json([ - 'message' => 'Пользователь создан', - 'user' => $user, - 'token' => $token - ], 201); - } - - // Вход public function login(Request $request) - { - $credentials = $request->only('email', 'password'); +{ + $request->validate([ + 'email' => 'required|email', + 'password' => 'required' + ]); - if (!Auth::attempt($credentials)) { - return response()->json([ - 'message' => 'Неверный email или пароль' - ], 401); - } + $user = \App\Models\User::where('email', $request->email)->first(); - $user = Auth::user(); - $token = $user->createToken('main-token')->plainTextToken; - - return response()->json([ - 'message' => 'Успешный вход', - 'user' => $user, - 'token' => $token - ]); + if (!$user || !Hash::check($request->password, $user->password)) { + return response()->json(['message' => 'Неверный email или пароль'], 401); } + + $token = $user->createToken('auth_token')->plainTextToken; + + return response()->json([ + 'access_token' => $token, + 'token_type' => 'Bearer', + 'user' => $user + ]); +} } \ No newline at end of file diff --git a/app/Http/Controllers/AvailabilitiesController.php b/app/Http/Controllers/AvailabilitiesController.php index 491b9da..a5a1d4a 100644 --- a/app/Http/Controllers/AvailabilitiesController.php +++ b/app/Http/Controllers/AvailabilitiesController.php @@ -2,168 +2,92 @@ namespace App\Http\Controllers; -use App\Models\EmployeeAvailability; -use App\Models\User; use Illuminate\Http\Request; +use App\Models\EmployeeAvailability; 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(); + $availabilities = EmployeeAvailability::with('employee') + ->when($request->has('employee_id'), function ($query) use ($request) { + $query->where('employee_id', $request->employee_id); + }) + ->when($request->has('date'), function ($query) use ($request) { + $query->where('date', $request->date); + }) + ->get(); + return response()->json($availabilities); } - - // POST api/admin/availabilities - создать один слот + public function store(Request $request) { - $request->validate([ + $validated = $request->validate([ 'employee_id' => 'required|exists:users,id', 'date' => 'required|date', - 'starttime' => 'required', - 'endtime' => 'required|after:starttime', - 'isavailable' => 'boolean' + 'start_time' => 'required|date_format:H:i:s', + 'end_time' => 'required|date_format:H:i:s|after:start_time', + 'is_available' => 'boolean' ]); - - $availability = EmployeeAvailability::create($request->all()); + + $availability = EmployeeAvailability::create($validated); + return response()->json($availability, 201); } - - // POST api/admin/availabilities/bulk - создать несколько слотов + public function bulkStore(Request $request) { - $request->validate([ + $validated = $request->validate([ 'employee_id' => 'required|exists:users,id', 'date' => 'required|date', - 'intervals' => 'required|array|min:1' + 'intervals' => 'required|array' ]); - - $availabilities = []; + foreach ($request->intervals as $interval) { - $availability = EmployeeAvailability::create([ + EmployeeAvailability::create([ 'employee_id' => $request->employee_id, 'date' => $request->date, - 'starttime' => $interval['start'], - 'endtime' => $interval['end'], - 'isavailable' => true + 'start_time' => $interval['start'], + 'end_time' => $interval['end'], + 'is_available' => true ]); - $availabilities[] = $availability; } - - return response()->json($availabilities, 201); + + return response()->json(['message' => 'Расписание добавлено']); } - - // DELETE api/admin/availabilities/{id} - удалить слот (брони остаются!) + public function destroy($id) { $availability = EmployeeAvailability::findOrFail($id); $availability->delete(); - - return response()->json(['message' => 'Слот удален из расписания (брони сохранены)']); + + return response()->json(['message' => 'Слот удалён']); } + + // ✅ Единственный метод publicAvailability 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 мин + { + $serviceId = $request->query('service_id'); + $date = $request->query('date'); + + if (!$serviceId || !$date) { + return response()->json([]); } + + $availabilities = EmployeeAvailability::where('date', $date) + ->where('is_available', true) + ->get(); + + $slots = []; + foreach ($availabilities as $avail) { + $slots[] = [ + 'employee_id' => $avail->employee_id, + 'start' => substr($avail->start_time, 0, 5), + 'end' => substr($avail->end_time, 0, 5), + ]; + } + + return response()->json($slots); } - - 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 - ]); -} } \ No newline at end of file diff --git a/app/Http/Controllers/BookingsController.php b/app/Http/Controllers/BookingsController.php index 81c6218..94a7ff1 100644 --- a/app/Http/Controllers/BookingsController.php +++ b/app/Http/Controllers/BookingsController.php @@ -2,165 +2,112 @@ namespace App\Http\Controllers; -use App\Models\Booking; -use App\Models\Services; -use App\Models\EmployeeAvailability; use Illuminate\Http\Request; -use Illuminate\Support\Str; +use App\Models\Booking; +use App\Models\Service; +use App\Models\User; class BookingsController extends Controller { public function store(Request $request) { - $request->validate([ + $validated = $request->validate([ 'service_id' => 'required|exists:services,id', 'employee_id' => 'required|exists:users,id', 'date' => 'required|date', - 'starttime' => 'required' + 'start_time' => 'required|date_format:H:i:s' ]); - $clientId = auth()->id(); - if (!$clientId) { - return response()->json(['error' => 'Авторизация обязательна'], 401); - } + // Получаем длительность услуги + $service = Service::findOrFail($validated['service_id']); + $duration = $service->duration_minutes; - $service = Services::where('id', $request->service_id) - ->where('isactive', true) - ->first(); - if (!$service) { - return response()->json(['error' => 'Услуга неактивна или не найдена'], 400); - } + // Вычисляем end_time + $start = new \DateTime($validated['start_time']); + $end = clone $start; + $end->modify("+$duration minutes"); + $end_time = $end->format('H:i:s'); - $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']) + // Проверяем, свободен ли слот + $conflict = Booking::where('employee_id', $validated['employee_id']) + ->where('booking_date', $validated['date']) + ->where('start_time', '<', $end_time) + ->where('end_time', '>', $validated['start_time']) ->exists(); - if ($bookingExists) { - return response()->json(['error' => 'Слот уже забронирован'], 400); + if ($conflict) { + return response()->json(['message' => 'Слот занят'], 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, + 'booking_number' => 'CL-' . date('Y') . '-' . str_pad(Booking::count() + 1, 4, '0', STR_PAD_LEFT), + 'client_id' => auth()->id(), + 'employee_id' => $validated['employee_id'], + 'service_id' => $validated['service_id'], + 'booking_date' => $validated['date'], + 'start_time' => $validated['start_time'], + 'end_time' => $end_time, 'status' => 'confirmed' ]); - return response()->json([ - 'booking' => $booking, - 'message' => 'Бронирование создано №' . $bookingNumber - ], 201); + return response()->json($booking, 201); } - public function cancel(Request $request, $id) + public function clientIndex() { - $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 - ]); - } - - public function adminCancel(Request $request, $id) - { - $booking = Booking::findOrFail($id); - - if (!auth()->user()->isEmployeeOrAdmin()) { - return response()->json(['error' => 'Доступ только для админов/сотрудников'], 403); - } - - $booking->update([ - 'status' => 'cancelled', - 'cancelledby' => 'admin', - 'cancelreason' => $request->reason ?? null - ]); - - return response()->json([ - 'message' => 'Бронь отменена администратором', - 'booking' => $booking - ]); - } - - public function clientIndex(Request $request) - { - $clientId = auth()->id(); - - $query = Booking::where('client_id', $clientId); - - if ($request->date) { - $query->where('bookingdate', $request->date); - } - if ($request->status) { - $query->where('status', $request->status); - } - - $bookings = $query->with(['service', 'employee']) - ->orderBy('bookingdate', 'desc') - ->orderBy('starttime', 'asc') - ->get(); - + $bookings = Booking::where('client_id', auth()->id())->get(); return response()->json($bookings); } - public function adminIndex(Request $request) + // adminIndex + public function adminIndex() { - if (!auth()->user()->isEmployeeOrAdmin()) { - return response()->json(['error' => 'Доступ запрещен'], 403); - } - - $query = Booking::with(['service', 'client', 'employee']); - - if ($request->date) { - $query->where('bookingdate', $request->date); - } - if ($request->status) { - $query->where('status', $request->status); - } - if ($request->employee_id) { - $query->where('employee_id', $request->employee_id); - } - - $bookings = $query->orderBy('bookingdate', 'desc') - ->orderBy('starttime', 'asc') + $bookings = Booking::with(['client', 'employee', 'service']) + ->orderBy('booking_date', 'desc') ->get(); - return response()->json($bookings); } + + public function cancel($id) + { + $booking = Booking::findOrFail($id); + + if ($booking->client_id !== auth()->id()) { + return response()->json(['message' => 'Нет прав'], 403); + } + + $booking->update([ + 'status' => 'cancelled', + 'cancelled_by' => 'client', + 'cancel_reason' => request('reason') + ]); + + return response()->json(['message' => 'Бронь отменена']); + } + + // adminCancel + public function adminCancel($id) + { + $booking = Booking::findOrFail($id); + $booking->update([ + 'status' => 'cancelled', + 'cancelled_by' => 'admin', + 'cancel_reason' => request('reason') + ]); + + return response()->json(['message' => 'Бронь отменена администратором']); + } + + // Назначение сотрудника + public function assignEmployee(Request $request, $id) + { + $request->validate(['employee_id' => 'required|exists:users,id']); + + $booking = Booking::findOrFail($id); + $booking->employee_id = $request->employee_id; + $booking->save(); + + return response()->json($booking); + } } \ No newline at end of file diff --git a/app/Http/Controllers/ServicesController.php b/app/Http/Controllers/ServicesController.php index f73f57a..fa6d98e 100644 --- a/app/Http/Controllers/ServicesController.php +++ b/app/Http/Controllers/ServicesController.php @@ -2,78 +2,60 @@ namespace App\Http\Controllers; -use App\Models\Services; use Illuminate\Http\Request; +use App\Models\Service; class ServicesController extends Controller { - // GET api/admin/services - список активных услуг public function index() { - $services = Services::where('isactive', true)->get(); + $services = Service::all(); return response()->json($services); } - - // POST api/admin/services - создать услугу + public function store(Request $request) { - $request->validate([ + $validated = $request->validate([ 'name' => 'required|string|max:255', - 'description' => 'required|string', - 'durationminutes' => 'required|integer|min:1|max:500', - 'price' => 'required|numeric|min:0' + 'description' => 'nullable|string', + 'duration_minutes' => 'required|integer', + 'price' => 'required|numeric', + 'is_active' => 'boolean' ]); - - $service = Services::create([ - 'name' => $request->name, - 'description' => $request->description, - 'durationminutes' => $request->durationminutes, - 'price' => $request->price, - 'isactive' => true // по умолчанию активна - ]); - + + $service = Service::create($validated); + return response()->json($service, 201); } - - // PUT api/admin/services/{id} - обновить услугу + public function update(Request $request, $id) { - $service = Services::findOrFail($id); - - $request->validate([ + $service = Service::findOrFail($id); + + $validated = $request->validate([ 'name' => 'required|string|max:255', - 'description' => 'required|string', - 'durationminutes' => 'required|integer|min:1|max:500', - 'price' => 'required|numeric|min:0' + 'description' => 'nullable|string', + 'duration_minutes' => 'required|integer', + 'price' => 'required|numeric', + 'is_active' => 'boolean' ]); - - $service->update([ - 'name' => $request->name, - 'description' => $request->description, - 'durationminutes' => $request->durationminutes, - 'price' => $request->price, - ]); - + + $service->update($validated); + 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); + + public function destroy($id) + { + $service = Service::findOrFail($id); + $service->delete(); + + return response()->json(['message' => 'Услуга удалена']); } - - $service->delete(); - return response()->json(['message' => 'Услуга удалена']); -} + + public function publicIndex() +{ + $services = \App\Models\Service::where('is_active', true)->get(); + return response()->json($services); } +} \ No newline at end of file diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 0000000..b08ecfd --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,48 @@ + [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + + 'api' => [ + \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + ]; + + protected $routeMiddleware = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'signed' => \App\Http\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + + 'role' => \App\Http\Middleware\CheckRole::class, + ]; +} \ No newline at end of file diff --git a/app/Http/Middleware/CheckRole.php b/app/Http/Middleware/CheckRole.php index 9ba5f45..d766439 100644 --- a/app/Http/Middleware/CheckRole.php +++ b/app/Http/Middleware/CheckRole.php @@ -1,5 +1,7 @@ check() || !auth()->user()->isEmployeeOrAdmin()) { - return response()->json(['error' => 'Доступ запрещен'], 403); + if (!$request->user()) { + return response()->json(['message' => 'Unauthorized'], 401); } + + if ($request->user()->role !== $role) { + return response()->json(['message' => 'Access denied'], 403); + } + return $next($request); } -} - +} \ No newline at end of file diff --git a/app/Models/Booking.php b/app/Models/Booking.php index 498825e..46fc45f 100644 --- a/app/Models/Booking.php +++ b/app/Models/Booking.php @@ -1,39 +1,42 @@ belongsTo(Services::class); - } - + // Связь с клиентом public function client() { return $this->belongsTo(User::class, 'client_id'); } - + + // Связь со сотрудником public function employee() { return $this->belongsTo(User::class, 'employee_id'); } -} + + // Связь с услугой + public function service() + { + return $this->belongsTo(Service::class); + } +} \ No newline at end of file diff --git a/app/Models/EmployeeAvailability.php b/app/Models/EmployeeAvailability.php index 1d9c5b0..2dba20f 100644 --- a/app/Models/EmployeeAvailability.php +++ b/app/Models/EmployeeAvailability.php @@ -1,10 +1,16 @@ hasMany(Booking::class); } -} +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index 3597e3f..7b072ab 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,44 +2,24 @@ namespace App\Models; -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 HasApiTokens, HasFactory, Notifiable; + use HasApiTokens, Notifiable; protected $fillable = [ 'name', 'email', 'password', - 'role', + 'role', + 'phone' ]; protected $hidden = [ 'password', 'remember_token', ]; - - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } - - // Проверяет админ или сотрудник - public function isEmployeeOrAdmin() - { - return $this->role == 'employee' || $this->role == 'admin'; - } - - // Для запросов - все сотрудники и админы - public static function scopeEmployeeOrAdmin($query) - { - return $query->whereIn('role', ['employee', 'admin']); - } -} +} \ No newline at end of file diff --git a/composer.json b/composer.json index 6ede6a4..bdebea5 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require": { "php": "^8.2", "laravel/framework": "^12.0", - "laravel/sanctum": "^4.0", + "laravel/sanctum": "^4.2", "laravel/tinker": "^2.10.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 1d8c52f..5e39ef3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d3c16cb86c42230c6c023d9a5d9bcf42", + "content-hash": "8f387a0734f3bf879214e4aa2fca6e2f", "packages": [ { "name": "brick/math", diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index bd3ae51..9662e16 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -6,45 +6,21 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { - /** - * Run the migrations. - */ - public function up(): void + public function up() { - //таблица user Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->string('password'); - $table->enum('role',['client','admin'])->default('client'); + $table->enum('role', ['client', 'employee', 'admin'])->default('client'); $table->string('phone')->nullable(); - $table->timestamps(); - }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); + $table->timestamps(); }); } - /** - * Reverse the migrations. - */ - public function down(): void + public function down() { Schema::dropIfExists('users'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); } -}; +}; \ No newline at end of file diff --git a/database/migrations/2025_11_05_172031_create_services_table.php b/database/migrations/2025_11_05_172031_create_services_table.php index 6e7645a..0f8506b 100644 --- a/database/migrations/2025_11_05_172031_create_services_table.php +++ b/database/migrations/2025_11_05_172031_create_services_table.php @@ -6,30 +6,21 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { - /** - * Run the migrations. - */ - public function up(): void + public function up() { Schema::create('services', function (Blueprint $table) { $table->id(); $table->string('name'); $table->text('description')->nullable(); - $table->decimal('price', 10, 2); $table->integer('duration_minutes'); - $table->unsignedBigInteger('category_id'); - $table->string('image_url')->nullable(); + $table->decimal('price', 10, 2); + $table->boolean('is_active')->default(true); $table->timestamps(); - - $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade'); }); } - /** - * Reverse the migrations. - */ - public function down(): void + public function down() { Schema::dropIfExists('services'); } -}; +}; \ No newline at end of file diff --git a/database/migrations/2026_01_07_135000_create_employee_availabilities_table.php b/database/migrations/2026_01_07_135000_create_employee_availabilities_table.php index 888d26b..441a88f 100644 --- a/database/migrations/2026_01_07_135000_create_employee_availabilities_table.php +++ b/database/migrations/2026_01_07_135000_create_employee_availabilities_table.php @@ -1,24 +1,28 @@ 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->time('start_time'); + $table->time('end_time'); + $table->boolean('is_available')->default(true); + $table->timestamps(); + $table->foreign('employee_id')->references('id')->on('users'); }); } - - public function down() { + + public function down() + { Schema::dropIfExists('employee_availabilities'); } -}; +}; \ No newline at end of file diff --git a/database/migrations/2026_01_07_140000_create_bookings_table.php b/database/migrations/2026_01_07_140000_create_bookings_table.php index 990fcdf..35aeefa 100644 --- a/database/migrations/2026_01_07_140000_create_bookings_table.php +++ b/database/migrations/2026_01_07_140000_create_bookings_table.php @@ -1,35 +1,37 @@ id(); - $table->string('bookingnumber'); + $table->string('booking_number')->unique(); $table->unsignedBigInteger('client_id'); - $table->unsignedBigInteger('employee_id'); + $table->unsignedBigInteger('employee_id'); $table->unsignedBigInteger('service_id'); - $table->date('bookingdate'); - $table->time('starttime'); - $table->time('endtime'); + $table->date('booking_date'); + $table->time('start_time'); + $table->time('end_time'); $table->enum('status', ['confirmed', 'cancelled', 'completed'])->default('confirmed'); - $table->enum('cancelledby', ['client', 'admin'])->nullable(); - $table->text('cancelreason')->nullable(); + $table->enum('cancelled_by', ['client', 'admin'])->nullable(); + $table->text('cancel_reason')->nullable(); $table->timestamps(); - - // Внешние ключи + $table->foreign('client_id')->references('id')->on('users'); - $table->foreign('employee_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'); + + $table->unique(['employee_id', 'booking_date', 'start_time']); }); } - - public function down() { + + public function down() + { Schema::dropIfExists('bookings'); } -}; +}; \ No newline at end of file diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..d80c30e 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,24 +2,71 @@ namespace Database\Seeders; -use App\Models\User; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use App\Models\User; +use App\Models\Service; +use App\Models\EmployeeAvailability; +use App\Models\Booking; class DatabaseSeeder extends Seeder { - use WithoutModelEvents; - - /** - * Seed the application's database. - */ - public function run(): void + public function run() { - // User::factory(10)->create(); + // Создаём админа + User::create([ + 'name' => 'Админ', + 'email' => 'admin@example.com', + 'password' => bcrypt('123'), + 'role' => 'admin' + ]); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + // Создаём сотрудника + User::create([ + 'name' => 'Иван Петров', + 'email' => 'ivan@example.com', + 'password' => bcrypt('2001'), + 'role' => 'employee' + ]); + + // Создаём клиента + User::create([ + 'name' => 'Мария Иванова', + 'email' => 'maria@example.com', + 'password' => bcrypt('2002'), + 'role' => 'client' + ]); + + + + + // Создаём услугу + Service::create([ + 'name' => 'Генеральная уборка', + 'description' => 'Полная уборка помещения.', + 'duration_minutes' => 180, + 'price' => 5000.00, + 'is_active' => true + ]); + + // Создаём расписание для сотрудника + EmployeeAvailability::create([ + 'employee_id' => 2, // Иван Петров + 'date' => '2026-02-10', + 'start_time' => '09:00:00', + 'end_time' => '18:00:00', + 'is_available' => true + ]); + + // Создаём бронирование + Booking::create([ + 'booking_number' => 'CL-2026-0001', + 'client_id' => 3, // Мария Иванова + 'employee_id' => 2, // Иван Петров + 'service_id' => 1, // Генеральная уборка + 'booking_date' => '2026-01-15', + 'start_time' => '10:00:00', + 'end_time' => '13:00:00', + 'status' => 'confirmed' ]); } -} +} \ No newline at end of file diff --git a/public/admin-bookings.html b/public/admin-bookings.html index b75b75e..5ffebec 100644 --- a/public/admin-bookings.html +++ b/public/admin-bookings.html @@ -3,393 +3,263 @@ - Все брони - КлинСервис Админка + Админка — Бронирования -
- -
- Услуги - Расписание - На главную - + +
+ Главная +

📋 Все бронирования

- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - -
- - -
- - - - - - - - - - - - - - - - - -
Код брониКлиент (email)УслугаСотрудникДата и времяСтатусДействия
Загрузка броней...
-
-
- - - - + \ No newline at end of file diff --git a/public/admin-schedule.html b/public/admin-schedule.html index 9271088..c18c809 100644 --- a/public/admin-schedule.html +++ b/public/admin-schedule.html @@ -171,21 +171,53 @@ diff --git a/public/admin-services.html b/public/admin-services.html index f0352aa..a127d25 100644 --- a/public/admin-services.html +++ b/public/admin-services.html @@ -180,45 +180,105 @@ diff --git a/public/booking-confirm.html b/public/booking-confirm.html index 3412435..4b1bf89 100644 --- a/public/booking-confirm.html +++ b/public/booking-confirm.html @@ -128,28 +128,56 @@ diff --git a/public/index.html b/public/index.html index 50406b5..32255c2 100644 --- a/public/index.html +++ b/public/index.html @@ -152,19 +152,20 @@ diff --git a/public/my-bookings.html b/public/my-bookings.html index 11c1101..18e5fce 100644 --- a/public/my-bookings.html +++ b/public/my-bookings.html @@ -1,4 +1,4 @@ - + @@ -212,11 +212,12 @@ const canCancel = booking.status === 'confirmed'; + // ИСПРАВЛЕНО: используем правильные имена полей с подчёркиваниями row.innerHTML = ` - ${booking.bookingnumber} - ${booking.bookingdate} ${booking.starttime.slice(0,5)}–${booking.endtime.slice(0,5)} - ${booking.service ? booking.service.name : 'Удалена'} - ${booking.employee ? booking.employee.name : 'Не назначен'} + ${booking.booking_number} + ${booking.booking_date} ${booking.start_time.slice(0,5)}–${booking.end_time.slice(0,5)} + ${booking.service ? booking.service.name : 'Услуга удалена'} + ${booking.employee ? booking.employee.name : 'Сотрудник не назначен'} ${booking.status} ${canCancel ? @@ -266,7 +267,7 @@ closeModal(); loadBookings(); // Перезагрузить список } else { - alert('Ошибка: ' + data.error); + alert('Ошибка: ' + (data.message || data.error || 'Не удалось отменить бронь')); } } catch (error) { alert('Ошибка сети'); @@ -288,4 +289,4 @@ } - + \ No newline at end of file diff --git a/public/register-login.html b/public/register-login.html index 4e52e5d..8d95a7e 100644 --- a/public/register-login.html +++ b/public/register-login.html @@ -75,7 +75,6 @@
-

Вход

@@ -91,7 +90,6 @@
Нет аккаунта? Зарегистрироваться
- diff --git a/public/services.html b/public/services.html index d07207e..a30ed45 100644 --- a/public/services.html +++ b/public/services.html @@ -142,11 +142,8 @@
- - +
@@ -173,46 +170,242 @@
+ + \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 0c4e253..8b22fbe 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,51 +1,57 @@ user(); })->middleware('auth:sanctum'); -// РЕГИСТРАЦИЯ ТОЛЬКО КЛИЕНТОВ (публичный) -Route::post('/register', [UserController::class, 'register']); - -Route::post('/login', [AuthController::class, 'login']); - -// Существующие роуты categories -Route::get('/categories', [CategoriesController::class, 'index'])->middleware('auth:sanctum'); +// === ПУБЛИЧНЫЕ РОУТЫ === +Route::post('/register', [AuthController::class, 'register']); +Route::post('/login', [AuthController::class, 'login'])->name('login'); +Route::get('/services', [ServicesController::class, 'publicIndex']); +Route::get('/availability', [AvailabilitiesController::class, 'publicAvailability']); Route::get('/categories/{id}', [CategoriesController::class, 'show']); Route::post('/categories', [CategoriesController::class, 'create']); -// ПУБЛИЧНЫЙ API доступности (без авторизации) -Route::get('/availability', [AvailabilitiesController::class, 'publicAvailability']); - -// КЛИЕНТСКИЕ РОУТЫ БРОНИРОВАНИЙ (auth:sanctum) -Route::middleware('auth:sanctum', 'role:admin')->group(function () { - Route::post('/bookings', [BookingsController::class, 'store']); - Route::post('/bookings/{id}/cancel', [BookingsController::class, 'cancel']); - Route::post('/bookings/{id}/cancel', [BookingsController::class, 'adminCancel']); +// === ЗАЩИЩЁННЫЕ РОУТЫ === +Route::middleware('auth:sanctum')->group(function () { + Route::get('/categories', [CategoriesController::class, 'index']); Route::get('/bookings', [BookingsController::class, 'clientIndex']); - Route::get('/bookings', [BookingsController::class, 'adminIndex']); + Route::post('/bookings', [BookingsController::class, 'store']); + Route::post('/bookings/{id}/cancel', [BookingsController::class, 'cancel']); + + // Получить сотрудников + Route::get('/users', function (Request $request) { + $role = $request->query('role', 'employee'); + return \App\Models\User::where('role', $role)->get(); + }); }); -// АДМИН РОУТЫ - ТОЛЬКО employee/admin (role:employee) -Route::middleware(['auth:sanctum', 'role:employee'])->prefix('admin')->group(function () { - // CRUD услуги +// === АДМИНСКИЕ РОУТЫ === +Route::middleware(['auth:sanctum', 'role:admin'])->prefix('admin')->group(function () { + // Услуги 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']); -}); + + // Бронирования админа ← ЭТОТ БЛОК ОБЯЗАТЕЛЕН! + Route::get('/bookings', [BookingsController::class, 'adminIndex']); + Route::post('/bookings/{id}/assign', [BookingsController::class, 'assignEmployee']); + Route::post('/bookings/{id}/cancel', [BookingsController::class, 'adminCancel']); +}); \ No newline at end of file