أُنشئ من Tokal/Test
Fix API reliability and CORS config for hosted deployment
هذا الالتزام موجود في:
@@ -2,7 +2,7 @@ APP_NAME=Laravel
|
|||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost
|
APP_URL=https://tabeleymvp2-8b111c91640a.hosted.ghaymah.systems
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
@@ -26,12 +26,15 @@ DB_PORT=5432
|
|||||||
DB_DATABASE=tabeley_db
|
DB_DATABASE=tabeley_db
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=secret
|
DB_PASSWORD=secret
|
||||||
|
DB_SSLMODE=prefer
|
||||||
|
|
||||||
SESSION_DRIVER=file
|
SESSION_DRIVER=file
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
SESSION_ENCRYPT=false
|
SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
SESSION_DOMAIN=null
|
SESSION_DOMAIN=null
|
||||||
|
SANCTUM_STATEFUL_DOMAINS=localhost,localhost:3000,localhost:5173,127.0.0.1,127.0.0.1:8000,tabeleymvp2-8b111c91640a.hosted.ghaymah.systems
|
||||||
|
CORS_ALLOWED_ORIGINS=https://tabeleymvp2-8b111c91640a.hosted.ghaymah.systems
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM php:8.2-cli
|
FROM php:8.2-fpm
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
@@ -8,9 +8,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
libonig-dev \
|
libonig-dev \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
zip \
|
zip \
|
||||||
unzip \
|
unzip
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install PHP extensions
|
# Install PHP extensions
|
||||||
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
|
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
|
||||||
@@ -29,11 +27,7 @@ RUN composer install --no-interaction --no-dev --optimize-autoloader
|
|||||||
|
|
||||||
# Set permissions
|
# Set permissions
|
||||||
RUN chown -R www-data:www-data /var/www \
|
RUN chown -R www-data:www-data /var/www \
|
||||||
&& chmod -R 755 /var/www/storage \
|
&& chmod -R 755 /var/www/storage
|
||||||
&& chmod -R 755 /var/www/bootstrap/cache
|
|
||||||
|
|
||||||
# Expose port 8000
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Start Laravel development server
|
|
||||||
CMD php artisan serve --host=0.0.0.0 --port=8000
|
CMD php artisan serve --host=0.0.0.0 --port=8000
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class SendReservationReminders extends Command
|
|||||||
$reservations = Reservation::query()
|
$reservations = Reservation::query()
|
||||||
->where('status', 'approved')
|
->where('status', 'approved')
|
||||||
->whereRaw(
|
->whereRaw(
|
||||||
"STR_TO_DATE(CONCAT(reservation_date,' ',reservation_time), '%Y-%m-%d %H:%i:%s') BETWEEN ? AND ?",
|
"(reservation_date::text || ' ' || reservation_time::text)::timestamp BETWEEN ? AND ?",
|
||||||
[$now->format('Y-m-d H:i:s'), $windowEnd->format('Y-m-d H:i:s')]
|
[$now->format('Y-m-d H:i:s'), $windowEnd->format('Y-m-d H:i:s')]
|
||||||
)
|
)
|
||||||
->get();
|
->get();
|
||||||
@@ -42,13 +42,14 @@ class SendReservationReminders extends Command
|
|||||||
|
|
||||||
ReservationReminder::create([
|
ReservationReminder::create([
|
||||||
'reservation_id' => $reservation->id,
|
'reservation_id' => $reservation->id,
|
||||||
|
'user_id' => $reservation->customer_id,
|
||||||
'send_at' => $sendAt,
|
'send_at' => $sendAt,
|
||||||
'sent_at' => now(),
|
'sent_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Notification::create([
|
Notification::create([
|
||||||
'user_id' => $reservation->customer_id,
|
'user_id' => $reservation->customer_id,
|
||||||
'type' => 'reservation_reminder',
|
'type' => 'reminder',
|
||||||
'title' => 'Reservation reminder',
|
'title' => 'Reservation reminder',
|
||||||
'body' => 'Your reservation is coming up in about ' . $minutes . ' minutes.',
|
'body' => 'Your reservation is coming up in about ' . $minutes . ' minutes.',
|
||||||
'data_json' => [
|
'data_json' => [
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class AdminController extends Controller
|
|||||||
// LIST USERS (admin)
|
// LIST USERS (admin)
|
||||||
public function users(Request $request)
|
public function users(Request $request)
|
||||||
{
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'per_page' => 'nullable|integer|min:1|max:200',
|
'per_page' => 'nullable|integer|min:1|max:200',
|
||||||
]);
|
]);
|
||||||
@@ -49,6 +51,8 @@ class AdminController extends Controller
|
|||||||
// LIST RESERVATIONS (admin)
|
// LIST RESERVATIONS (admin)
|
||||||
public function reservations(Request $request)
|
public function reservations(Request $request)
|
||||||
{
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'per_page' => 'nullable|integer|min:1|max:200',
|
'per_page' => 'nullable|integer|min:1|max:200',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -29,9 +29,49 @@ class FeedbackController extends Controller
|
|||||||
], 201);
|
], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CREATE FEEDBACK (public landing page)
|
||||||
|
public function storePublic(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'message' => 'required|string|min:5|max:2000',
|
||||||
|
'name' => 'nullable|string|max:120',
|
||||||
|
'email' => 'nullable|email|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$details = [];
|
||||||
|
if (! empty($validated['name'])) {
|
||||||
|
$details[] = 'Name: ' . trim($validated['name']);
|
||||||
|
}
|
||||||
|
if (! empty($validated['email'])) {
|
||||||
|
$details[] = 'Email: ' . trim($validated['email']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = trim($validated['message']);
|
||||||
|
if (! empty($details)) {
|
||||||
|
$message .= "\n\n[Landing Page]\n" . implode("\n", $details);
|
||||||
|
} else {
|
||||||
|
$message .= "\n\n[Landing Page]";
|
||||||
|
}
|
||||||
|
|
||||||
|
$feedback = UserFeedback::create([
|
||||||
|
'user_id' => null,
|
||||||
|
'message' => $message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Feedback sent successfully',
|
||||||
|
'data' => [
|
||||||
|
'id' => $feedback->id,
|
||||||
|
'created_at' => $feedback->created_at,
|
||||||
|
],
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
// LIST FEEDBACK (admin)
|
// LIST FEEDBACK (admin)
|
||||||
public function adminIndex(Request $request)
|
public function adminIndex(Request $request)
|
||||||
{
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'per_page' => 'nullable|integer|min:1|max:100',
|
'per_page' => 'nullable|integer|min:1|max:100',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ class ReservationController extends Controller
|
|||||||
// CREATE RESERVATION (customer)
|
// CREATE RESERVATION (customer)
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
if ($user->isBlocked()) {
|
if ($user->isBlocked()) {
|
||||||
return response()->json(['message' => 'You are temporarily blocked from making reservations.'], 403);
|
return response()->json(['message' => 'You are temporarily blocked from making reservations.'], 403);
|
||||||
@@ -319,7 +321,8 @@ class ReservationController extends Controller
|
|||||||
'rejection_reason' => null,
|
'rejection_reason' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$table = $this->findAvailableTable($reservation, true, true);
|
// Avoid long lock waits in high-latency environments; use optimistic read.
|
||||||
|
$table = $this->findAvailableTable($reservation, true, false);
|
||||||
if (! $table) {
|
if (! $table) {
|
||||||
throw new \RuntimeException('No available table for this reservation.');
|
throw new \RuntimeException('No available table for this reservation.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use App\Models\Venue;
|
|||||||
use App\Models\VenueImage;
|
use App\Models\VenueImage;
|
||||||
use App\Models\VenueTable;
|
use App\Models\VenueTable;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@@ -18,6 +19,9 @@ use Illuminate\Validation\ValidationException;
|
|||||||
|
|
||||||
class VenueController extends Controller
|
class VenueController extends Controller
|
||||||
{
|
{
|
||||||
|
private ?bool $hasInlineVenueArraysCache = null;
|
||||||
|
private ?bool $hasStructuredVenueDataCache = null;
|
||||||
|
|
||||||
// LIST VENUES (customer)
|
// LIST VENUES (customer)
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
@@ -129,6 +133,8 @@ class VenueController extends Controller
|
|||||||
// LIST VENUES (admin)
|
// LIST VENUES (admin)
|
||||||
public function adminIndex(Request $request)
|
public function adminIndex(Request $request)
|
||||||
{
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
|
||||||
$ownerColumn = Venue::ownerColumn();
|
$ownerColumn = Venue::ownerColumn();
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'search' => 'nullable|string|max:255',
|
'search' => 'nullable|string|max:255',
|
||||||
@@ -145,7 +151,7 @@ class VenueController extends Controller
|
|||||||
$q->where('is_active', true);
|
$q->where('is_active', true);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
$this->applyVenueRelations($query);
|
// Keep admin list lightweight; loading full relations here can timeout.
|
||||||
|
|
||||||
if (! empty($validated['search'])) {
|
if (! empty($validated['search'])) {
|
||||||
$query->where('name', 'like', '%' . $validated['search'] . '%');
|
$query->where('name', 'like', '%' . $validated['search'] . '%');
|
||||||
@@ -171,6 +177,8 @@ class VenueController extends Controller
|
|||||||
// CREATE VENUE (admin)
|
// CREATE VENUE (admin)
|
||||||
public function adminStore(Request $request)
|
public function adminStore(Request $request)
|
||||||
{
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'vendor_id' => 'nullable|integer|exists:users,id',
|
'vendor_id' => 'nullable|integer|exists:users,id',
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
@@ -268,6 +276,8 @@ class VenueController extends Controller
|
|||||||
// UPDATE VENUE (admin)
|
// UPDATE VENUE (admin)
|
||||||
public function adminUpdate(Request $request, int $id)
|
public function adminUpdate(Request $request, int $id)
|
||||||
{
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'vendor_id' => 'nullable|integer|exists:users,id',
|
'vendor_id' => 'nullable|integer|exists:users,id',
|
||||||
'name' => 'sometimes|required|string|max:255',
|
'name' => 'sometimes|required|string|max:255',
|
||||||
@@ -542,6 +552,10 @@ class VenueController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasStructuredVenueData()) {
|
if ($this->hasStructuredVenueData()) {
|
||||||
|
if (! $venue->relationLoaded('amenitiesRelation')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return ($venue->amenitiesRelation ?? collect())
|
return ($venue->amenitiesRelation ?? collect())
|
||||||
->pluck('name')
|
->pluck('name')
|
||||||
->values()
|
->values()
|
||||||
@@ -558,6 +572,10 @@ class VenueController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasStructuredVenueData()) {
|
if ($this->hasStructuredVenueData()) {
|
||||||
|
if (! $venue->relationLoaded('images')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return ($venue->images ?? collect())
|
return ($venue->images ?? collect())
|
||||||
->sortBy('sort_order')
|
->sortBy('sort_order')
|
||||||
->pluck('url')
|
->pluck('url')
|
||||||
@@ -575,6 +593,10 @@ class VenueController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasStructuredVenueData()) {
|
if ($this->hasStructuredVenueData()) {
|
||||||
|
if (! $venue->relationLoaded('offersRelation')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return ($venue->offersRelation ?? collect())
|
return ($venue->offersRelation ?? collect())
|
||||||
->map(fn (Offer $offer) => [
|
->map(fn (Offer $offer) => [
|
||||||
'title' => $offer->title,
|
'title' => $offer->title,
|
||||||
@@ -591,17 +613,48 @@ class VenueController extends Controller
|
|||||||
|
|
||||||
private function hasInlineVenueArrays(): bool
|
private function hasInlineVenueArrays(): bool
|
||||||
{
|
{
|
||||||
return Schema::hasColumn('venues', 'amenities')
|
if ($this->hasInlineVenueArraysCache !== null) {
|
||||||
&& Schema::hasColumn('venues', 'image_urls')
|
return $this->hasInlineVenueArraysCache;
|
||||||
&& Schema::hasColumn('venues', 'offers');
|
}
|
||||||
|
|
||||||
|
$this->hasInlineVenueArraysCache = $this->rememberSchemaFlag(
|
||||||
|
'schema:venues:inline-arrays:v1',
|
||||||
|
fn () => Schema::hasColumn('venues', 'amenities')
|
||||||
|
&& Schema::hasColumn('venues', 'image_urls')
|
||||||
|
&& Schema::hasColumn('venues', 'offers')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->hasInlineVenueArraysCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function hasStructuredVenueData(): bool
|
private function hasStructuredVenueData(): bool
|
||||||
{
|
{
|
||||||
return Schema::hasTable('venue_images')
|
if ($this->hasStructuredVenueDataCache !== null) {
|
||||||
&& Schema::hasTable('offers')
|
return $this->hasStructuredVenueDataCache;
|
||||||
&& Schema::hasTable('venue_amenities')
|
}
|
||||||
&& Schema::hasTable('amenities');
|
|
||||||
|
$this->hasStructuredVenueDataCache = $this->rememberSchemaFlag(
|
||||||
|
'schema:venues:structured-data:v1',
|
||||||
|
fn () => Schema::hasTable('venue_images')
|
||||||
|
&& Schema::hasTable('offers')
|
||||||
|
&& Schema::hasTable('venue_amenities')
|
||||||
|
&& Schema::hasTable('amenities')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->hasStructuredVenueDataCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function rememberSchemaFlag(string $cacheKey, callable $resolver): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (bool) Cache::remember(
|
||||||
|
$cacheKey,
|
||||||
|
now()->addMinutes(30),
|
||||||
|
fn () => (bool) $resolver()
|
||||||
|
);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return (bool) $resolver();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function inlineVenuePayload(array $amenities, array $imageUrls, array $offers): array
|
private function inlineVenuePayload(array $amenities, array $imageUrls, array $offers): array
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
class Notification extends Model
|
class Notification extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
class PricingRule extends Model
|
class PricingRule extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'scope',
|
'scope',
|
||||||
|
|||||||
@@ -8,16 +8,28 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
class ReservationFee extends Model
|
class ReservationFee extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'reservation_id',
|
'reservation_id',
|
||||||
'pricing_rule_id',
|
'pricing_rule_id',
|
||||||
'price_per_person',
|
'price_per_person',
|
||||||
'party_size',
|
'party_size',
|
||||||
|
'total_fee',
|
||||||
'total_amount',
|
'total_amount',
|
||||||
'currency',
|
'currency',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function setTotalAmountAttribute($value): void
|
||||||
|
{
|
||||||
|
$this->attributes['total_fee'] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalAmountAttribute()
|
||||||
|
{
|
||||||
|
return $this->attributes['total_fee'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
public function reservation()
|
public function reservation()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Reservation::class);
|
return $this->belongsTo(Reservation::class);
|
||||||
|
|||||||
@@ -8,18 +8,32 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
class ReservationReminder extends Model
|
class ReservationReminder extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'reservation_id',
|
'reservation_id',
|
||||||
|
'user_id',
|
||||||
|
'remind_at',
|
||||||
'send_at',
|
'send_at',
|
||||||
'sent_at',
|
'sent_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
'remind_at' => 'datetime',
|
||||||
'send_at' => 'datetime',
|
'send_at' => 'datetime',
|
||||||
'sent_at' => 'datetime',
|
'sent_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function setSendAtAttribute($value): void
|
||||||
|
{
|
||||||
|
$this->attributes['remind_at'] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSendAtAttribute()
|
||||||
|
{
|
||||||
|
return $this->attributes['remind_at'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
public function reservation()
|
public function reservation()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Reservation::class);
|
return $this->belongsTo(Reservation::class);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
class Role extends Model
|
class Role extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
class SeatingArea extends Model
|
class SeatingArea extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'venue_id',
|
'venue_id',
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'is_active',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function venue()
|
public function venue()
|
||||||
|
|||||||
@@ -4,19 +4,48 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class VenueTable extends Model
|
class VenueTable extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
protected static ?string $labelColumn = null;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'venue_id',
|
'venue_id',
|
||||||
'seating_area_id',
|
'seating_area_id',
|
||||||
'name',
|
'name',
|
||||||
|
'table_number',
|
||||||
'capacity',
|
'capacity',
|
||||||
'is_active',
|
'is_active',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static function labelColumn(): string
|
||||||
|
{
|
||||||
|
if (self::$labelColumn !== null) {
|
||||||
|
return self::$labelColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$labelColumn = Schema::hasColumn('venue_tables', 'name')
|
||||||
|
? 'name'
|
||||||
|
: 'table_number';
|
||||||
|
|
||||||
|
return self::$labelColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNameAttribute($value): void
|
||||||
|
{
|
||||||
|
$this->attributes[self::labelColumn()] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNameAttribute()
|
||||||
|
{
|
||||||
|
$column = self::labelColumn();
|
||||||
|
return $this->attributes[$column] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
public function venue()
|
public function venue()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Venue::class);
|
return $this->belongsTo(Venue::class);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use Illuminate\Foundation\Application;
|
|||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
use Illuminate\Auth\AuthenticationException;
|
use Illuminate\Auth\AuthenticationException;
|
||||||
|
use Illuminate\Http\Middleware\HandleCors;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
@@ -13,6 +14,8 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->append(HandleCors::class);
|
||||||
|
|
||||||
// Treat all requests as API-style: don't redirect guests, return 401/403.
|
// Treat all requests as API-style: don't redirect guests, return 401/403.
|
||||||
$middleware->redirectGuestsTo(fn () => null);
|
$middleware->redirectGuestsTo(fn () => null);
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
|
|||||||
27
config/cors.php
Normal file
27
config/cors.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$allowedOrigins = array_values(array_filter(array_map(
|
||||||
|
static fn ($origin) => trim($origin),
|
||||||
|
explode(',', (string) env('CORS_ALLOWED_ORIGINS', 'https://tabeleymvp2-8b111c91640a.hosted.ghaymah.systems'))
|
||||||
|
)));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||||
|
|
||||||
|
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||||
|
|
||||||
|
'allowed_origins' => $allowedOrigins,
|
||||||
|
|
||||||
|
'allowed_origins_patterns' => [
|
||||||
|
'#^http://localhost(:\d+)?$#',
|
||||||
|
'#^http://127\.0\.0\.1(:\d+)?$#',
|
||||||
|
],
|
||||||
|
|
||||||
|
'allowed_headers' => ['*'],
|
||||||
|
|
||||||
|
'exposed_headers' => [],
|
||||||
|
|
||||||
|
'max_age' => 0,
|
||||||
|
|
||||||
|
'supports_credentials' => false,
|
||||||
|
];
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('user_feedback')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create('user_feedback', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
$table->text('message');
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->timestamp('updated_at')->useCurrent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_feedback');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('user_feedback') || ! Schema::hasColumn('user_feedback', 'user_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::statement('ALTER TABLE user_feedback ALTER COLUMN user_id DROP NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('user_feedback') || ! Schema::hasColumn('user_feedback', 'user_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('user_feedback')->whereNull('user_id')->delete();
|
||||||
|
DB::statement('ALTER TABLE user_feedback ALTER COLUMN user_id SET NOT NULL');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ set_env DB_PORT "${DB_PORT}"
|
|||||||
set_env DB_DATABASE "${DB_DATABASE}"
|
set_env DB_DATABASE "${DB_DATABASE}"
|
||||||
set_env DB_USERNAME "${DB_USERNAME}"
|
set_env DB_USERNAME "${DB_USERNAME}"
|
||||||
set_env DB_PASSWORD "${DB_PASSWORD}"
|
set_env DB_PASSWORD "${DB_PASSWORD}"
|
||||||
|
set_env DB_SSLMODE "${DB_SSLMODE}"
|
||||||
set_env DB_URL "${DB_URL}"
|
set_env DB_URL "${DB_URL}"
|
||||||
set_env DATABASE_URL "${DATABASE_URL}"
|
set_env DATABASE_URL "${DATABASE_URL}"
|
||||||
set_env APP_ENV "${APP_ENV}"
|
set_env APP_ENV "${APP_ENV}"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Route::middleware(['auth:sanctum', 'role:vendor'])->patch('/reservations/{id}/co
|
|||||||
|
|
||||||
Route::middleware(['auth:sanctum'])->get('/notifications', [NotificationController::class, 'index']);
|
Route::middleware(['auth:sanctum'])->get('/notifications', [NotificationController::class, 'index']);
|
||||||
Route::middleware(['auth:sanctum', 'role:customer'])->post('/feedback', [FeedbackController::class, 'store']);
|
Route::middleware(['auth:sanctum', 'role:customer'])->post('/feedback', [FeedbackController::class, 'store']);
|
||||||
|
Route::middleware(['throttle:20,1'])->post('/public/feedback', [FeedbackController::class, 'storePublic']);
|
||||||
Route::middleware(['auth:sanctum', 'role:admin'])->get('/admin/feedback', [FeedbackController::class, 'adminIndex']);
|
Route::middleware(['auth:sanctum', 'role:admin'])->get('/admin/feedback', [FeedbackController::class, 'adminIndex']);
|
||||||
Route::middleware(['auth:sanctum', 'role:admin'])->get('/admin/users', [AdminController::class, 'users']);
|
Route::middleware(['auth:sanctum', 'role:admin'])->get('/admin/users', [AdminController::class, 'users']);
|
||||||
Route::middleware(['auth:sanctum', 'role:admin'])->get('/admin/reservations', [AdminController::class, 'reservations']);
|
Route::middleware(['auth:sanctum', 'role:admin'])->get('/admin/reservations', [AdminController::class, 'reservations']);
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم