أُنشئ من Tokal/Test
Initial backend upload
هذا الالتزام موجود في:
102
app/Http/Controllers/Api/AdminController.php
Normal file
102
app/Http/Controllers/Api/AdminController.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Reservation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
// LIST USERS (admin)
|
||||
public function users(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'per_page' => 'nullable|integer|min:1|max:200',
|
||||
]);
|
||||
|
||||
$perPage = $validated['per_page'] ?? 50;
|
||||
|
||||
$paginator = User::query()
|
||||
->with(['roles:id,name'])
|
||||
->orderByDesc('id')
|
||||
->paginate($perPage);
|
||||
|
||||
$data = collect($paginator->items())->map(function (User $user) {
|
||||
return [
|
||||
'id' => $user->id,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'email' => $user->email,
|
||||
'phone' => $user->phone,
|
||||
'roles' => $user->roles->pluck('name')->values(),
|
||||
'created_at' => $user->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// LIST RESERVATIONS (admin)
|
||||
public function reservations(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'per_page' => 'nullable|integer|min:1|max:200',
|
||||
]);
|
||||
|
||||
$perPage = $validated['per_page'] ?? 50;
|
||||
|
||||
$paginator = Reservation::query()
|
||||
->with([
|
||||
'venue:id,name,type,address_text',
|
||||
'customer:id,first_name,last_name,email,phone',
|
||||
])
|
||||
->orderByDesc('id')
|
||||
->paginate($perPage);
|
||||
|
||||
$data = collect($paginator->items())->map(function (Reservation $reservation) {
|
||||
return [
|
||||
'id' => $reservation->id,
|
||||
'code' => $reservation->code,
|
||||
'reservation_date' => $reservation->reservation_date,
|
||||
'reservation_time' => $reservation->reservation_time,
|
||||
'party_size' => $reservation->party_size,
|
||||
'status' => $reservation->status,
|
||||
'rejection_reason' => $reservation->rejection_reason,
|
||||
'created_at' => $reservation->created_at,
|
||||
'venue' => $reservation->venue ? [
|
||||
'id' => $reservation->venue->id,
|
||||
'name' => $reservation->venue->name,
|
||||
'type' => $reservation->venue->type,
|
||||
'address_text' => $reservation->venue->address_text,
|
||||
] : null,
|
||||
'customer' => $reservation->customer ? [
|
||||
'id' => $reservation->customer->id,
|
||||
'first_name' => $reservation->customer->first_name,
|
||||
'last_name' => $reservation->customer->last_name,
|
||||
'email' => $reservation->customer->email,
|
||||
'phone' => $reservation->customer->phone,
|
||||
] : null,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
154
app/Http/Controllers/Api/AuthController.php
Normal file
154
app/Http/Controllers/Api/AuthController.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
// REGISTER
|
||||
public function register(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'first_name' => 'nullable|string|max:255',
|
||||
'last_name' => 'nullable|string|max:255',
|
||||
'email' => 'required|email|unique:users',
|
||||
'phone' => 'required|string|unique:users',
|
||||
'password' => 'required|string|min:6',
|
||||
]);
|
||||
|
||||
$emailPrefix = explode('@', $validated['email'])[0] ?? 'User';
|
||||
$firstName = trim($validated['first_name'] ?? '');
|
||||
$lastName = trim($validated['last_name'] ?? '');
|
||||
if ($firstName === '') {
|
||||
$firstName = 'User';
|
||||
}
|
||||
if ($lastName === '') {
|
||||
$lastName = $emailPrefix;
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
$token = $user->createToken('mobile_token')->plainTextToken;
|
||||
|
||||
$defaultRole = Role::firstOrCreate(['name' => 'customer']);
|
||||
$user->roles()->syncWithoutDetaching([$defaultRole->id]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'User registered successfully',
|
||||
'token' => $token,
|
||||
'user' => [
|
||||
...$user->toArray(),
|
||||
'roles' => $user->roles()->pluck('name')->values(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// LOGIN
|
||||
public function login(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
$user = User::where('email', $request->email)->first();
|
||||
|
||||
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||
return response()->json([
|
||||
'message' => 'Invalid credentials'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Backfill customer role for legacy users that were created before roles assignment.
|
||||
if (! $user->roles()->exists()) {
|
||||
$defaultRole = Role::firstOrCreate(['name' => 'customer']);
|
||||
$user->roles()->syncWithoutDetaching([$defaultRole->id]);
|
||||
}
|
||||
|
||||
$token = $user->createToken('auth_token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Login successful',
|
||||
'token' => $token,
|
||||
'user' => [
|
||||
...$user->toArray(),
|
||||
'roles' => $user->roles()->pluck('name')->values(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// LOGOUT
|
||||
public function logout(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user) {
|
||||
return response()->json(['message' => 'Unauthenticated.'], 401);
|
||||
}
|
||||
|
||||
$token = $user->currentAccessToken();
|
||||
if ($token) {
|
||||
$token->delete();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Logout successful',
|
||||
]);
|
||||
}
|
||||
|
||||
public function me(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
if (! $user) {
|
||||
return response()->json(['message' => 'Unauthenticated.'], 401);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'user' => [
|
||||
...$user->toArray(),
|
||||
'roles' => $user->roles()->pluck('name')->values(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateProfile(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
if (! $user) {
|
||||
return response()->json(['message' => 'Unauthenticated.'], 401);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'phone' => 'required|string|max:50|unique:users,phone,' . $user->id,
|
||||
]);
|
||||
|
||||
$user->first_name = $validated['first_name'];
|
||||
$user->last_name = $validated['last_name'];
|
||||
$user->phone = $validated['phone'];
|
||||
$user->save();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Profile updated successfully',
|
||||
'user' => [
|
||||
...$user->toArray(),
|
||||
'roles' => $user->roles()->pluck('name')->values(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
69
app/Http/Controllers/Api/FeedbackController.php
Normal file
69
app/Http/Controllers/Api/FeedbackController.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\UserFeedback;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FeedbackController extends Controller
|
||||
{
|
||||
// CREATE FEEDBACK (customer)
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'message' => 'required|string|min:5|max:2000',
|
||||
]);
|
||||
|
||||
$feedback = UserFeedback::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'message' => $validated['message'],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Feedback sent successfully',
|
||||
'data' => [
|
||||
'id' => $feedback->id,
|
||||
'created_at' => $feedback->created_at,
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
// LIST FEEDBACK (admin)
|
||||
public function adminIndex(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
]);
|
||||
|
||||
$perPage = $validated['per_page'] ?? 20;
|
||||
$paginator = UserFeedback::query()
|
||||
->with(['user:id,first_name,last_name,email'])
|
||||
->orderByDesc('id')
|
||||
->paginate($perPage);
|
||||
|
||||
$data = collect($paginator->items())->map(function (UserFeedback $item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'message' => $item->message,
|
||||
'created_at' => $item->created_at,
|
||||
'user' => [
|
||||
'id' => $item->user?->id,
|
||||
'first_name' => $item->user?->first_name,
|
||||
'last_name' => $item->user?->last_name,
|
||||
'email' => $item->user?->email,
|
||||
],
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
47
app/Http/Controllers/Api/NotificationController.php
Normal file
47
app/Http/Controllers/Api/NotificationController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Notification;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
// LIST NOTIFICATIONS (auth)
|
||||
public function index(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'per_page' => 'nullable|integer|min:1|max:50',
|
||||
]);
|
||||
|
||||
$perPage = $validated['per_page'] ?? 10;
|
||||
|
||||
$paginator = Notification::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->orderByDesc('id')
|
||||
->paginate($perPage);
|
||||
|
||||
$data = collect($paginator->items())->map(function ($notification) {
|
||||
return [
|
||||
'id' => $notification->id,
|
||||
'type' => $notification->type,
|
||||
'title' => $notification->title,
|
||||
'body' => $notification->body,
|
||||
'data' => $notification->data_json,
|
||||
'is_read' => $notification->is_read,
|
||||
'created_at' => $notification->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
617
app/Http/Controllers/Api/ReservationController.php
Normal file
617
app/Http/Controllers/Api/ReservationController.php
Normal file
@@ -0,0 +1,617 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Reservation;
|
||||
use App\Models\Notification;
|
||||
use App\Models\ReservationStatusHistory;
|
||||
use App\Models\ReservationFee;
|
||||
use App\Models\PricingRule;
|
||||
use App\Models\ReservationTableAssignment;
|
||||
use App\Models\VenueTable;
|
||||
use App\Models\ReservationReminder;
|
||||
use App\Models\Venue;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ReservationController extends Controller
|
||||
{
|
||||
// LIST VENDOR RESERVATIONS (vendor)
|
||||
public function vendorReservations(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'per_page' => 'nullable|integer|min:1|max:50',
|
||||
]);
|
||||
|
||||
$perPage = $validated['per_page'] ?? 10;
|
||||
|
||||
$query = Reservation::query()
|
||||
->whereHas('venue', function ($q) use ($request) {
|
||||
$q->where('vendor_id', $request->user()->id);
|
||||
})
|
||||
->with([
|
||||
'venue:id,name,type,address_text',
|
||||
'customer:id,first_name,last_name,phone',
|
||||
])
|
||||
->orderByDesc('id');
|
||||
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
$data = collect($paginator->items())->map(function ($reservation) {
|
||||
return [
|
||||
'id' => $reservation->id,
|
||||
'code' => $reservation->code,
|
||||
'reservation_date' => $reservation->reservation_date,
|
||||
'reservation_time' => $reservation->reservation_time,
|
||||
'party_size' => $reservation->party_size,
|
||||
'status' => $reservation->status,
|
||||
'rejection_reason' => $reservation->rejection_reason,
|
||||
'venue' => $reservation->venue ? [
|
||||
'id' => $reservation->venue->id,
|
||||
'name' => $reservation->venue->name,
|
||||
'type' => $reservation->venue->type,
|
||||
'address_text' => $reservation->venue->address_text,
|
||||
] : null,
|
||||
'customer' => $reservation->customer ? [
|
||||
'id' => $reservation->customer->id,
|
||||
'first_name' => $reservation->customer->first_name,
|
||||
'last_name' => $reservation->customer->last_name,
|
||||
'phone' => $reservation->customer->phone,
|
||||
] : null,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// APPROVE RESERVATION (vendor)
|
||||
public function approve(Request $request, int $id)
|
||||
{
|
||||
$reservation = null;
|
||||
$table = null;
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($request, $id, &$reservation, &$table) {
|
||||
$reservation = Reservation::query()
|
||||
->where('id', $id)
|
||||
->whereHas('venue', function ($q) use ($request) {
|
||||
$q->where('vendor_id', $request->user()->id);
|
||||
})
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (! $reservation) {
|
||||
throw new \RuntimeException('Reservation not found.', 404);
|
||||
}
|
||||
|
||||
$table = $reservation->tableAssignment?->table;
|
||||
if (! $table) {
|
||||
$table = $this->findAvailableTable($reservation, true, true);
|
||||
if (! $table) {
|
||||
throw new \RuntimeException('No available table for this reservation.', 409);
|
||||
}
|
||||
|
||||
ReservationTableAssignment::create([
|
||||
'reservation_id' => $reservation->id,
|
||||
'venue_table_id' => $table->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->updateStatus($reservation, 'approved', $request->user()->id);
|
||||
$this->notifyCustomer(
|
||||
$reservation,
|
||||
'reservation_approved',
|
||||
'Reservation approved',
|
||||
'Your reservation has been approved.'
|
||||
);
|
||||
});
|
||||
} catch (\RuntimeException $e) {
|
||||
$status = $e->getCode() >= 400 ? $e->getCode() : 409;
|
||||
return response()->json(['message' => $e->getMessage()], $status);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Reservation approved successfully',
|
||||
'reservation' => [
|
||||
'id' => $reservation->id,
|
||||
'status' => $reservation->status,
|
||||
'rejection_reason' => $reservation->rejection_reason,
|
||||
'table' => [
|
||||
'id' => $table->id,
|
||||
'name' => $table->name,
|
||||
'capacity' => $table->capacity,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// REJECT RESERVATION (vendor)
|
||||
public function reject(Request $request, int $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'reason' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$reservation = $this->findVendorReservation($request, $id);
|
||||
if (! $reservation) {
|
||||
return response()->json(['message' => 'Reservation not found.'], 404);
|
||||
}
|
||||
|
||||
$this->updateStatus($reservation, 'rejected', $request->user()->id, $validated['reason']);
|
||||
$this->notifyCustomer(
|
||||
$reservation,
|
||||
'reservation_rejected',
|
||||
'Reservation rejected',
|
||||
$validated['reason']
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Reservation rejected',
|
||||
'reservation' => [
|
||||
'id' => $reservation->id,
|
||||
'status' => $reservation->status,
|
||||
'rejection_reason' => $reservation->rejection_reason,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// CANCEL RESERVATION (customer)
|
||||
public function cancelByCustomer(Request $request, int $id)
|
||||
{
|
||||
$reservation = Reservation::query()
|
||||
->where('customer_id', $request->user()->id)
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $this->canCancel($reservation)) {
|
||||
return response()->json([
|
||||
'message' => 'Reservation cannot be cancelled.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (! in_array($reservation->status, ['pending', 'approved'], true)) {
|
||||
return response()->json([
|
||||
'message' => 'Reservation cannot be cancelled.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$this->updateStatus($reservation, 'cancelled_by_customer', $request->user()->id);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Reservation cancelled successfully',
|
||||
'reservation' => [
|
||||
'id' => $reservation->id,
|
||||
'status' => $reservation->status,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// CANCEL RESERVATION (vendor)
|
||||
public function cancelByVendor(Request $request, int $id)
|
||||
{
|
||||
$reservation = $this->findVendorReservation($request, $id);
|
||||
if (! $reservation) {
|
||||
return response()->json(['message' => 'Reservation not found.'], 404);
|
||||
}
|
||||
|
||||
if ($reservation->status === 'completed') {
|
||||
return response()->json([
|
||||
'message' => 'Reservation cannot be cancelled.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$this->updateStatus($reservation, 'cancelled_by_venue', $request->user()->id);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Reservation cancelled by venue',
|
||||
'reservation' => [
|
||||
'id' => $reservation->id,
|
||||
'status' => $reservation->status,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function findVendorReservation(Request $request, int $id): ?Reservation
|
||||
{
|
||||
return Reservation::query()
|
||||
->whereHas('venue', function ($q) use ($request) {
|
||||
$q->where('vendor_id', $request->user()->id);
|
||||
})
|
||||
->find($id);
|
||||
}
|
||||
// LIST MY RESERVATIONS (customer)
|
||||
public function myReservations(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'per_page' => 'nullable|integer|min:1|max:50',
|
||||
]);
|
||||
|
||||
$perPage = $validated['per_page'] ?? 10;
|
||||
|
||||
$query = Reservation::query()
|
||||
->where('customer_id', $request->user()->id)
|
||||
->with([
|
||||
'venue:id,name,type,description,address_text,lat,lng',
|
||||
])
|
||||
->orderByDesc('id');
|
||||
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
$data = collect($paginator->items())->map(function ($reservation) {
|
||||
return [
|
||||
'id' => $reservation->id,
|
||||
'code' => $reservation->code,
|
||||
'reservation_date' => $reservation->reservation_date,
|
||||
'reservation_time' => $reservation->reservation_time,
|
||||
'party_size' => $reservation->party_size,
|
||||
'status' => $reservation->status,
|
||||
'venue' => $reservation->venue ? [
|
||||
'id' => $reservation->venue->id,
|
||||
'name' => $reservation->venue->name,
|
||||
'type' => $reservation->venue->type,
|
||||
'address_text' => $reservation->venue->address_text,
|
||||
] : null,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// CREATE RESERVATION (customer)
|
||||
public function store(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
if ($user->isBlocked()) {
|
||||
return response()->json(['message' => 'You are temporarily blocked from making reservations.'], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'venue_id' => 'required|integer|exists:venues,id',
|
||||
'reservation_date' => 'required|date_format:Y-m-d',
|
||||
'reservation_time' => 'required|date_format:H:i',
|
||||
'party_size' => 'required|integer|min:1|max:50',
|
||||
]);
|
||||
|
||||
$venue = Venue::where('id', $validated['venue_id'])
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (! $venue) {
|
||||
return response()->json(['message' => 'Venue not found.'], 404);
|
||||
}
|
||||
|
||||
$reservation = null;
|
||||
$fee = null;
|
||||
$table = null;
|
||||
$reminder = null;
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($validated, $user, &$reservation, &$fee, &$table, &$reminder) {
|
||||
$reservation = Reservation::create([
|
||||
'code' => $this->generateCode(),
|
||||
'customer_id' => $user->id,
|
||||
'venue_id' => $validated['venue_id'],
|
||||
'reservation_date' => $validated['reservation_date'],
|
||||
'reservation_time' => $validated['reservation_time'],
|
||||
'party_size' => $validated['party_size'],
|
||||
'status' => 'pending',
|
||||
'rejection_reason' => null,
|
||||
]);
|
||||
|
||||
$table = $this->findAvailableTable($reservation, true, true);
|
||||
if (! $table) {
|
||||
throw new \RuntimeException('No available table for this reservation.');
|
||||
}
|
||||
|
||||
ReservationTableAssignment::create([
|
||||
'reservation_id' => $reservation->id,
|
||||
'venue_table_id' => $table->id,
|
||||
]);
|
||||
|
||||
$fee = $this->createReservationFee($reservation);
|
||||
$reminder = $this->createReservationReminder($reservation);
|
||||
$this->logStatusChange($reservation, null, 'pending', $user->id);
|
||||
});
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['message' => $e->getMessage()], 409);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'reservation' => [
|
||||
'id' => $reservation->id,
|
||||
'code' => $reservation->code,
|
||||
'status' => $reservation->status,
|
||||
'reservation_date' => $reservation->reservation_date,
|
||||
'reservation_time' => $reservation->reservation_time,
|
||||
'party_size' => $reservation->party_size,
|
||||
],
|
||||
'fee' => [
|
||||
'price_per_person' => $fee->price_per_person,
|
||||
'party_size' => $fee->party_size,
|
||||
'total_amount' => $fee->total_amount,
|
||||
'currency' => $fee->currency,
|
||||
],
|
||||
'table' => [
|
||||
'id' => $table->id,
|
||||
'name' => $table->name,
|
||||
'capacity' => $table->capacity,
|
||||
],
|
||||
'reminder' => [
|
||||
'send_at' => $reminder->send_at,
|
||||
],
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
private function generateCode(): string
|
||||
{
|
||||
do {
|
||||
$code = Str::upper(Str::random(8));
|
||||
} while (Reservation::where('code', $code)->exists());
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function createReservationFee(Reservation $reservation): ReservationFee
|
||||
{
|
||||
$venue = $reservation->venue ?: Venue::find($reservation->venue_id);
|
||||
|
||||
$rule = $this->getPricingRule($venue);
|
||||
$pricePerPerson = $rule ? (float) $rule->price_per_person : 0.0;
|
||||
$total = $pricePerPerson * (int) $reservation->party_size;
|
||||
$currency = env('APP_CURRENCY', 'USD');
|
||||
|
||||
return ReservationFee::create([
|
||||
'reservation_id' => $reservation->id,
|
||||
'pricing_rule_id' => $rule?->id,
|
||||
'price_per_person' => $pricePerPerson,
|
||||
'party_size' => $reservation->party_size,
|
||||
'total_amount' => $total,
|
||||
'currency' => $currency,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getPricingRule(?Venue $venue): ?PricingRule
|
||||
{
|
||||
if (! $venue) {
|
||||
return PricingRule::where('scope', 'global')->where('is_active', true)->latest('id')->first();
|
||||
}
|
||||
|
||||
$venueRule = PricingRule::where('scope', 'venue')
|
||||
->where('venue_id', $venue->id)
|
||||
->where('is_active', true)
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
if ($venueRule) {
|
||||
return $venueRule;
|
||||
}
|
||||
|
||||
$typeRule = PricingRule::where('scope', 'type')
|
||||
->where('venue_type', $venue->type)
|
||||
->where('is_active', true)
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
if ($typeRule) {
|
||||
return $typeRule;
|
||||
}
|
||||
|
||||
return PricingRule::where('scope', 'global')->where('is_active', true)->latest('id')->first();
|
||||
}
|
||||
|
||||
private function findAvailableTable(Reservation $reservation, bool $includePending, bool $lockRows = false): ?VenueTable
|
||||
{
|
||||
$durationMinutes = (int) env('RESERVATION_DURATION_MINUTES', 90);
|
||||
if ($durationMinutes < 30) {
|
||||
$durationMinutes = 90;
|
||||
}
|
||||
|
||||
$start = Carbon::parse($reservation->reservation_date . ' ' . $reservation->reservation_time);
|
||||
$end = $start->copy()->addMinutes($durationMinutes);
|
||||
|
||||
$tablesQuery = VenueTable::query()
|
||||
->where('venue_id', $reservation->venue_id)
|
||||
->where('is_active', true)
|
||||
->where('capacity', '>=', $reservation->party_size)
|
||||
->orderBy('capacity');
|
||||
|
||||
if ($lockRows) {
|
||||
$tablesQuery->lockForUpdate();
|
||||
}
|
||||
|
||||
$tables = $tablesQuery->get();
|
||||
|
||||
if ($tables->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tableIds = $tables->pluck('id')->all();
|
||||
|
||||
$assignedQuery = ReservationTableAssignment::query()
|
||||
->whereIn('venue_table_id', $tableIds)
|
||||
->whereHas('reservation', function ($q) use ($reservation, $includePending) {
|
||||
$statuses = ['approved', 'completed'];
|
||||
if ($includePending) {
|
||||
$statuses[] = 'pending';
|
||||
}
|
||||
$q->where('venue_id', $reservation->venue_id)
|
||||
->whereIn('status', $statuses);
|
||||
})
|
||||
->with('reservation');
|
||||
|
||||
if ($lockRows) {
|
||||
$assignedQuery->lockForUpdate();
|
||||
}
|
||||
|
||||
$assigned = $assignedQuery->get();
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$conflict = false;
|
||||
foreach ($assigned as $assign) {
|
||||
if ($assign->venue_table_id !== $table->id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rStart = Carbon::parse($assign->reservation->reservation_date . ' ' . $assign->reservation->reservation_time);
|
||||
$rEnd = $rStart->copy()->addMinutes($durationMinutes);
|
||||
|
||||
if ($start < $rEnd && $end > $rStart) {
|
||||
$conflict = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $conflict) {
|
||||
return $table;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function createReservationReminder(Reservation $reservation): ReservationReminder
|
||||
{
|
||||
$minutes = (int) env('RESERVATION_REMINDER_MINUTES', 60);
|
||||
if ($minutes < 1) {
|
||||
$minutes = 60;
|
||||
}
|
||||
|
||||
$dateTime = Carbon::parse($reservation->reservation_date . ' ' . $reservation->reservation_time);
|
||||
$sendAt = $dateTime->copy()->subMinutes($minutes);
|
||||
|
||||
return ReservationReminder::create([
|
||||
'reservation_id' => $reservation->id,
|
||||
'send_at' => $sendAt,
|
||||
'sent_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
// MARK NO-SHOW (vendor)
|
||||
public function markNoShow(Request $request, int $id)
|
||||
{
|
||||
$reservation = $this->findVendorReservation($request, $id);
|
||||
if (! $reservation) {
|
||||
return response()->json(['message' => 'Reservation not found.'], 404);
|
||||
}
|
||||
|
||||
$this->updateStatus($reservation, 'no_show', $request->user()->id);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Reservation marked as no_show',
|
||||
'reservation' => [
|
||||
'id' => $reservation->id,
|
||||
'status' => $reservation->status,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// MARK COMPLETED (vendor)
|
||||
public function markCompleted(Request $request, int $id)
|
||||
{
|
||||
$reservation = $this->findVendorReservation($request, $id);
|
||||
if (! $reservation) {
|
||||
return response()->json(['message' => 'Reservation not found.'], 404);
|
||||
}
|
||||
|
||||
$this->updateStatus($reservation, 'completed', $request->user()->id);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Reservation marked as completed',
|
||||
'reservation' => [
|
||||
'id' => $reservation->id,
|
||||
'status' => $reservation->status,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function updateStatus(Reservation $reservation, string $newStatus, int $changedByUserId, ?string $rejectionReason = null): void
|
||||
{
|
||||
$oldStatus = $reservation->status;
|
||||
|
||||
$reservation->status = $newStatus;
|
||||
$reservation->rejection_reason = $newStatus === 'rejected' ? $rejectionReason : null;
|
||||
$reservation->save();
|
||||
|
||||
$this->logStatusChange($reservation, $oldStatus, $newStatus, $changedByUserId);
|
||||
|
||||
if ($this->isFakeBooking($newStatus)) {
|
||||
$this->applyStrike($reservation->customer);
|
||||
}
|
||||
}
|
||||
|
||||
private function notifyCustomer(Reservation $reservation, string $type, string $title, string $body): void
|
||||
{
|
||||
Notification::create([
|
||||
'user_id' => $reservation->customer_id,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'data_json' => [
|
||||
'reservation_id' => $reservation->id,
|
||||
'venue_id' => $reservation->venue_id,
|
||||
'status' => $reservation->status,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function logStatusChange(Reservation $reservation, ?string $oldStatus, string $newStatus, int $changedByUserId): void
|
||||
{
|
||||
ReservationStatusHistory::create([
|
||||
'reservation_id' => $reservation->id,
|
||||
'old_status' => $oldStatus,
|
||||
'new_status' => $newStatus,
|
||||
'changed_by_user_id' => $changedByUserId,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function isFakeBooking(string $status): bool
|
||||
{
|
||||
return in_array($status, ['no_show'], true);
|
||||
}
|
||||
|
||||
private function applyStrike($customer): void
|
||||
{
|
||||
if (! $customer) {
|
||||
return;
|
||||
}
|
||||
|
||||
$customer->strike_count = $customer->strike_count + 1;
|
||||
|
||||
if ($customer->strike_count >= 9) {
|
||||
$customer->blocked_permanent = true;
|
||||
} elseif ($customer->strike_count >= 6) {
|
||||
$customer->blocked_until = now()->addDays(30);
|
||||
} elseif ($customer->strike_count >= 3) {
|
||||
$customer->blocked_until = now()->addDays(7);
|
||||
}
|
||||
|
||||
$customer->save();
|
||||
}
|
||||
|
||||
private function canCancel(Reservation $reservation): bool
|
||||
{
|
||||
$dateTime = Carbon::parse($reservation->reservation_date . ' ' . $reservation->reservation_time);
|
||||
return now()->lt($dateTime->subHour());
|
||||
}
|
||||
}
|
||||
429
app/Http/Controllers/Api/VenueController.php
Normal file
429
app/Http/Controllers/Api/VenueController.php
Normal file
@@ -0,0 +1,429 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Models\Venue;
|
||||
use App\Models\VenueTable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class VenueController extends Controller
|
||||
{
|
||||
// LIST VENUES (customer)
|
||||
public function index(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'type' => 'nullable|in:restaurant,cafe',
|
||||
'search' => 'nullable|string|max:255',
|
||||
'per_page' => 'nullable|integer|min:1|max:50',
|
||||
]);
|
||||
|
||||
$query = Venue::query()
|
||||
->select([
|
||||
'id',
|
||||
'name',
|
||||
'type',
|
||||
'description',
|
||||
'address_text',
|
||||
'lat',
|
||||
'lng',
|
||||
'amenities',
|
||||
'image_urls',
|
||||
'offers',
|
||||
]);
|
||||
|
||||
$query->where('is_active', true);
|
||||
|
||||
if (! empty($validated['type'])) {
|
||||
$query->where('type', $validated['type']);
|
||||
}
|
||||
|
||||
if (! empty($validated['search'])) {
|
||||
$query->where('name', 'like', '%' . $validated['search'] . '%');
|
||||
}
|
||||
|
||||
$perPage = $validated['per_page'] ?? 10;
|
||||
|
||||
$paginator = $query->orderByDesc('id')->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'data' => $paginator->items(),
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// SHOW VENUE (public)
|
||||
public function show(int $id)
|
||||
{
|
||||
$venue = Venue::query()
|
||||
->select([
|
||||
'id',
|
||||
'name',
|
||||
'type',
|
||||
'description',
|
||||
'address_text',
|
||||
'lat',
|
||||
'lng',
|
||||
'amenities',
|
||||
'image_urls',
|
||||
'offers',
|
||||
])
|
||||
->where('is_active', true)
|
||||
->findOrFail($id);
|
||||
|
||||
return response()->json([
|
||||
'data' => $venue,
|
||||
]);
|
||||
}
|
||||
|
||||
// CREATE VENUE (vendor only)
|
||||
public function store(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:restaurant,cafe',
|
||||
'description' => 'nullable|string',
|
||||
'address_text' => 'nullable|string|max:255',
|
||||
'lat' => 'nullable|numeric',
|
||||
'lng' => 'nullable|numeric',
|
||||
'phone' => 'nullable|string|max:50',
|
||||
'amenities' => 'nullable|array',
|
||||
'amenities.*' => 'string|max:100',
|
||||
'image_urls' => 'nullable|array|max:10',
|
||||
'image_urls.*' => 'url|max:2048',
|
||||
'offers' => 'nullable|array|max:20',
|
||||
'offers.*.title' => 'required_with:offers|string|max:120',
|
||||
'offers.*.description' => 'nullable|string|max:500',
|
||||
'offers.*.image_url' => 'nullable|url|max:2048',
|
||||
'offers.*.is_active' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$venue = Venue::create([
|
||||
'vendor_id' => $user->id,
|
||||
'name' => $validated['name'],
|
||||
'type' => $validated['type'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'address_text' => $validated['address_text'] ?? null,
|
||||
'lat' => $validated['lat'] ?? null,
|
||||
'lng' => $validated['lng'] ?? null,
|
||||
'is_active' => true,
|
||||
'phone' => $validated['phone'] ?? null,
|
||||
'amenities' => $validated['amenities'] ?? [],
|
||||
'image_urls' => $validated['image_urls'] ?? [],
|
||||
'offers' => $this->sanitizeOffers($validated['offers'] ?? []),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Venue created successfully',
|
||||
'venue' => $venue,
|
||||
], 201);
|
||||
}
|
||||
|
||||
// LIST VENUES (admin)
|
||||
public function adminIndex(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
]);
|
||||
|
||||
$query = Venue::query()
|
||||
->select([
|
||||
'id',
|
||||
'vendor_id',
|
||||
'name',
|
||||
'type',
|
||||
'description',
|
||||
'address_text',
|
||||
'lat',
|
||||
'lng',
|
||||
'is_active',
|
||||
'phone',
|
||||
'amenities',
|
||||
'image_urls',
|
||||
'offers',
|
||||
])
|
||||
->withCount([
|
||||
'tables as table_count' => function ($q) {
|
||||
$q->where('is_active', true);
|
||||
},
|
||||
]);
|
||||
|
||||
if (! empty($validated['search'])) {
|
||||
$query->where('name', 'like', '%' . $validated['search'] . '%');
|
||||
}
|
||||
|
||||
$perPage = $validated['per_page'] ?? 20;
|
||||
$paginator = $query->orderByDesc('id')->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'data' => $paginator->items(),
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// CREATE VENUE (admin)
|
||||
public function adminStore(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'vendor_id' => 'nullable|integer|exists:users,id',
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:restaurant,cafe',
|
||||
'description' => 'nullable|string',
|
||||
'address_text' => 'nullable|string|max:255',
|
||||
'lat' => 'nullable|numeric',
|
||||
'lng' => 'nullable|numeric',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'phone' => 'nullable|string|max:50',
|
||||
'amenities' => 'nullable|array',
|
||||
'amenities.*' => 'string|max:100',
|
||||
'image_urls' => 'nullable|array|max:10',
|
||||
'image_urls.*' => 'url|max:2048',
|
||||
'images' => 'nullable|array|max:10',
|
||||
'images.*' => 'file|image|max:5120',
|
||||
'offers' => 'nullable|array|max:20',
|
||||
'offers.*.title' => 'required_with:offers|string|max:120',
|
||||
'offers.*.description' => 'nullable|string|max:500',
|
||||
'offers.*.image_url' => 'nullable|url|max:2048',
|
||||
'offers.*.is_active' => 'nullable|boolean',
|
||||
'table_count' => 'nullable|integer|min:1|max:300',
|
||||
'vendor_email' => 'nullable|email|unique:users,email',
|
||||
'vendor_phone' => 'nullable|string|unique:users,phone',
|
||||
'vendor_password' => 'nullable|string|min:6',
|
||||
]);
|
||||
|
||||
$vendorId = $validated['vendor_id'] ?? null;
|
||||
|
||||
if (! $vendorId) {
|
||||
$hasVendorCreds = ! empty($validated['vendor_email']) &&
|
||||
! empty($validated['vendor_phone']) &&
|
||||
! empty($validated['vendor_password']);
|
||||
|
||||
if ($hasVendorCreds) {
|
||||
$vendorRole = Role::firstOrCreate(['name' => 'vendor']);
|
||||
$vendor = User::create([
|
||||
'first_name' => 'Vendor',
|
||||
'last_name' => 'User',
|
||||
'email' => $validated['vendor_email'],
|
||||
'phone' => $validated['vendor_phone'],
|
||||
'password' => Hash::make($validated['vendor_password']),
|
||||
]);
|
||||
$vendor->roles()->syncWithoutDetaching([$vendorRole->id]);
|
||||
$vendorId = $vendor->id;
|
||||
} else {
|
||||
$vendorId = $this->resolveVendorId();
|
||||
}
|
||||
}
|
||||
|
||||
if (! $vendorId) {
|
||||
return response()->json([
|
||||
'message' => 'Provide vendor account data (email, phone, password) or vendor_id.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$venue = null;
|
||||
DB::transaction(function () use ($validated, $vendorId, &$venue, $request) {
|
||||
$imageUrls = $validated['image_urls'] ?? [];
|
||||
$imageUrls = $this->appendUploadedImages($request, $imageUrls);
|
||||
|
||||
$venue = Venue::create([
|
||||
'vendor_id' => $vendorId,
|
||||
'name' => $validated['name'],
|
||||
'type' => $validated['type'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'address_text' => $validated['address_text'] ?? null,
|
||||
'lat' => $validated['lat'] ?? null,
|
||||
'lng' => $validated['lng'] ?? null,
|
||||
'is_active' => $validated['is_active'] ?? true,
|
||||
'phone' => $validated['phone'] ?? null,
|
||||
'amenities' => $validated['amenities'] ?? [],
|
||||
'image_urls' => $imageUrls,
|
||||
'offers' => $this->sanitizeOffers($validated['offers'] ?? []),
|
||||
]);
|
||||
|
||||
$this->syncVenueTables($venue, (int) ($validated['table_count'] ?? 4));
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Venue created successfully',
|
||||
'venue' => $venue->loadCount([
|
||||
'tables as table_count' => function ($q) {
|
||||
$q->where('is_active', true);
|
||||
},
|
||||
]),
|
||||
], 201);
|
||||
}
|
||||
|
||||
// UPDATE VENUE (admin)
|
||||
public function adminUpdate(Request $request, int $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'vendor_id' => 'nullable|integer|exists:users,id',
|
||||
'name' => 'sometimes|required|string|max:255',
|
||||
'type' => 'sometimes|required|in:restaurant,cafe',
|
||||
'description' => 'nullable|string',
|
||||
'address_text' => 'nullable|string|max:255',
|
||||
'lat' => 'nullable|numeric',
|
||||
'lng' => 'nullable|numeric',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'phone' => 'nullable|string|max:50',
|
||||
'amenities' => 'nullable|array',
|
||||
'amenities.*' => 'string|max:100',
|
||||
'image_urls' => 'nullable|array|max:10',
|
||||
'image_urls.*' => 'url|max:2048',
|
||||
'images' => 'nullable|array|max:10',
|
||||
'images.*' => 'file|image|max:5120',
|
||||
'offers' => 'nullable|array|max:20',
|
||||
'offers.*.title' => 'required_with:offers|string|max:120',
|
||||
'offers.*.description' => 'nullable|string|max:500',
|
||||
'offers.*.image_url' => 'nullable|url|max:2048',
|
||||
'offers.*.is_active' => 'nullable|boolean',
|
||||
'table_count' => 'nullable|integer|min:1|max:300',
|
||||
]);
|
||||
|
||||
$venue = Venue::findOrFail($id);
|
||||
DB::transaction(function () use (&$venue, $validated, $request) {
|
||||
if (array_key_exists('offers', $validated)) {
|
||||
$validated['offers'] = $this->sanitizeOffers($validated['offers'] ?? []);
|
||||
}
|
||||
|
||||
$baseImageUrls = array_key_exists('image_urls', $validated)
|
||||
? ($validated['image_urls'] ?? [])
|
||||
: ($venue->image_urls ?? []);
|
||||
$validated['image_urls'] = $this->appendUploadedImages($request, $baseImageUrls);
|
||||
|
||||
$venue->fill($validated);
|
||||
$venue->save();
|
||||
|
||||
if (array_key_exists('table_count', $validated)) {
|
||||
$this->syncVenueTables($venue, (int) $validated['table_count']);
|
||||
}
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Venue updated successfully',
|
||||
'venue' => $venue->loadCount([
|
||||
'tables as table_count' => function ($q) {
|
||||
$q->where('is_active', true);
|
||||
},
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
// DELETE VENUE (admin)
|
||||
public function adminDestroy(int $id)
|
||||
{
|
||||
$venue = Venue::findOrFail($id);
|
||||
$venue->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Venue deleted successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveVendorId(): ?int
|
||||
{
|
||||
$vendor = User::query()
|
||||
->whereHas('roles', function ($q) {
|
||||
$q->where('name', 'vendor');
|
||||
})
|
||||
->first();
|
||||
|
||||
return $vendor?->id;
|
||||
}
|
||||
|
||||
private function syncVenueTables(Venue $venue, int $targetCount): void
|
||||
{
|
||||
$targetCount = max(1, min($targetCount, 300));
|
||||
|
||||
$activeTables = VenueTable::query()
|
||||
->where('venue_id', $venue->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$currentCount = $activeTables->count();
|
||||
|
||||
if ($currentCount < $targetCount) {
|
||||
for ($i = $currentCount + 1; $i <= $targetCount; $i++) {
|
||||
VenueTable::create([
|
||||
'venue_id' => $venue->id,
|
||||
'seating_area_id' => null,
|
||||
'name' => 'Table ' . $i,
|
||||
'capacity' => 4,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($currentCount > $targetCount) {
|
||||
$toDisable = $activeTables->slice($targetCount);
|
||||
foreach ($toDisable as $table) {
|
||||
$table->is_active = false;
|
||||
$table->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sanitizeOffers(array $offers): array
|
||||
{
|
||||
return collect($offers)
|
||||
->filter(fn ($offer) => is_array($offer))
|
||||
->map(function (array $offer) {
|
||||
return [
|
||||
'title' => trim((string) ($offer['title'] ?? '')),
|
||||
'description' => trim((string) ($offer['description'] ?? '')),
|
||||
'image_url' => trim((string) ($offer['image_url'] ?? '')),
|
||||
'is_active' => array_key_exists('is_active', $offer)
|
||||
? (bool) $offer['is_active']
|
||||
: true,
|
||||
];
|
||||
})
|
||||
->filter(fn (array $offer) => $offer['title'] !== '')
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function appendUploadedImages(Request $request, array $imageUrls): array
|
||||
{
|
||||
$urls = collect($imageUrls)
|
||||
->map(fn ($url) => trim((string) $url))
|
||||
->filter(fn ($url) => $url !== '')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($request->hasFile('images')) {
|
||||
foreach ($request->file('images') as $image) {
|
||||
$path = $image->store('venues', 'public');
|
||||
$urls[] = rtrim($request->getSchemeAndHttpHost(), '/') . '/storage/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
$urls = collect($urls)->unique()->values()->all();
|
||||
if (count($urls) > 10) {
|
||||
throw ValidationException::withMessages([
|
||||
'images' => ['Maximum 10 images allowed per venue.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
31
app/Http/Middleware/RoleMiddleware.php
Normal file
31
app/Http/Middleware/RoleMiddleware.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RoleMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next, ...$roles): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user) {
|
||||
return response()->json(['message' => 'Unauthenticated.'], 401);
|
||||
}
|
||||
|
||||
if (empty($roles)) {
|
||||
return response()->json(['message' => 'Forbidden.'], 403);
|
||||
}
|
||||
|
||||
foreach ($roles as $role) {
|
||||
if ($user->hasRole($role)) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Forbidden.'], 403);
|
||||
}
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم