From ae5ab2554b95da5090933db70b830973a2f67886 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=92=D0=BB=D0=B0=D0=B4=D0=B8=D0=BC=D0=B8=D1=80?=
<ваш@email.com>
Date: Mon, 12 Jan 2026 14:25:15 +0000
Subject: [PATCH] commit 12.01
---
app/Http/Controllers/AuthController.php | 70 +--
.../Controllers/AvailabilitiesController.php | 184 ++-----
app/Http/Controllers/BookingsController.php | 207 +++----
app/Http/Controllers/ServicesController.php | 88 ++-
app/Http/Kernel.php | 48 ++
app/Http/Middleware/CheckRole.php | 14 +-
app/Models/Booking.php | 41 +-
app/Models/EmployeeAvailability.php | 20 +-
app/Models/{Services.php => Service.php} | 12 +-
app/Models/User.php | 30 +-
composer.json | 2 +-
composer.lock | 2 +-
.../0001_01_01_000000_create_users_table.php | 34 +-
...025_11_05_172031_create_services_table.php | 19 +-
...0_create_employee_availabilities_table.php | 24 +-
...026_01_07_140000_create_bookings_table.php | 38 +-
database/seeders/DatabaseSeeder.php | 73 ++-
public/admin-bookings.html | 515 +++++++-----------
public/admin-schedule.html | 56 +-
public/admin-services.html | 126 +++--
public/booking-confirm.html | 70 ++-
public/index.html | 25 +-
public/my-bookings.html | 15 +-
public/register-login.html | 153 ++----
public/services.html | 275 ++++++++--
routes/api.php | 58 +-
26 files changed, 1116 insertions(+), 1083 deletions(-)
create mode 100644 app/Http/Kernel.php
rename app/Models/{Services.php => Service.php} (65%)
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 @@
- Все брони - КлинСервис Админка
+ Админка — Бронирования
-