diff --git a/Dockerfile b/Dockerfile index e6037ab..fc2a8bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,39 @@ -FROM php:8.3-fpm +FROM php:8.2-cli -RUN apt-get update && apt-get install -y --no-install-recommends \ - libpq-dev \ - libicu-dev \ - libpng-dev \ - libzip-dev \ - libjpeg62-turbo-dev \ - libfreetype6-dev \ - unzip \ +# Install system dependencies +RUN apt-get update && apt-get install -y \ git \ curl \ + libpng-dev \ + libonig-dev \ + libxml2-dev \ zip \ + unzip \ + && apt-get clean \ && rm -rf /var/lib/apt/lists/* -RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ - && docker-php-ext-install -j"$(nproc)" pdo_pgsql pgsql intl gd zip +# Install PHP extensions +RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd -COPY --from=composer:2 /usr/bin/composer /usr/bin/composer +# Install composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer +# Set working directory WORKDIR /var/www -COPY composer.json composer.lock ./ -RUN composer install --no-dev --prefer-dist --no-interaction --no-scripts --optimize-autoloader +# Copy existing application directory contents +COPY . /var/www -COPY . . +# Install dependencies +RUN composer install --no-interaction --no-dev --optimize-autoloader -RUN chown -R www-data:www-data storage bootstrap/cache \ - && chmod -R ug+rw storage bootstrap/cache \ - && chmod +x docker/entrypoint.sh +# Set permissions +RUN chown -R www-data:www-data /var/www \ + && chmod -R 755 /var/www/storage \ + && chmod -R 755 /var/www/bootstrap/cache -ENTRYPOINT ["docker/entrypoint.sh"] +# Expose port 8000 +EXPOSE 8000 -EXPOSE 9000 - -CMD ["php-fpm"] +# Start Laravel development server +CMD php artisan serve --host=0.0.0.0 --port=8000 diff --git a/app/Http/Controllers/Api/ReservationController.php b/app/Http/Controllers/Api/ReservationController.php index cdebe26..091b155 100644 --- a/app/Http/Controllers/Api/ReservationController.php +++ b/app/Http/Controllers/Api/ReservationController.php @@ -12,10 +12,13 @@ use App\Models\ReservationTableAssignment; use App\Models\VenueTable; use App\Models\ReservationReminder; use App\Models\Venue; +use App\Models\UserBlock; +use App\Models\UserStrike; use Illuminate\Http\Request; use Illuminate\Support\Str; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class ReservationController extends Controller { @@ -393,7 +396,7 @@ class ReservationController extends Controller private function getPricingRule(?Venue $venue): ?PricingRule { if (! $venue) { - return PricingRule::where('scope', 'global')->where('is_active', true)->latest('id')->first(); + return PricingRule::where('scope', 'global_type')->where('is_active', true)->latest('id')->first(); } $venueRule = PricingRule::where('scope', 'venue') @@ -406,7 +409,7 @@ class ReservationController extends Controller return $venueRule; } - $typeRule = PricingRule::where('scope', 'type') + $typeRule = PricingRule::where('scope', 'global_type') ->where('venue_type', $venue->type) ->where('is_active', true) ->latest('id') @@ -416,7 +419,7 @@ class ReservationController extends Controller return $typeRule; } - return PricingRule::where('scope', 'global')->where('is_active', true)->latest('id')->first(); + return PricingRule::where('scope', 'global_type')->where('is_active', true)->latest('id')->first(); } private function findAvailableTable(Reservation $reservation, bool $includePending, bool $lockRows = false): ?VenueTable @@ -501,6 +504,7 @@ class ReservationController extends Controller return ReservationReminder::create([ 'reservation_id' => $reservation->id, + 'user_id' => $reservation->customer_id, 'send_at' => $sendAt, 'sent_at' => null, ]); @@ -563,10 +567,11 @@ class ReservationController extends Controller { Notification::create([ 'user_id' => $reservation->customer_id, - 'type' => $type, + 'type' => 'reservation_status', 'title' => $title, 'body' => $body, 'data_json' => [ + 'event' => $type, 'reservation_id' => $reservation->id, 'venue_id' => $reservation->venue_id, 'status' => $reservation->status, @@ -596,17 +601,80 @@ class ReservationController extends Controller return; } - $customer->strike_count = $customer->strike_count + 1; + if (Schema::hasColumn('users', 'strike_count') + && Schema::hasColumn('users', 'blocked_until') + && Schema::hasColumn('users', 'blocked_permanent')) { + $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); + 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(); + return; } - $customer->save(); + if (! Schema::hasTable('user_strikes') || ! Schema::hasTable('user_blocks')) { + return; + } + + UserStrike::create([ + 'user_id' => $customer->id, + 'type' => 'no_show', + 'created_at' => now(), + ]); + + $strikeCount = UserStrike::query() + ->where('user_id', $customer->id) + ->where('type', 'no_show') + ->count(); + + UserBlock::query() + ->where('user_id', $customer->id) + ->where('is_active', true) + ->update(['is_active' => false]); + + if ($strikeCount >= 9) { + UserBlock::create([ + 'user_id' => $customer->id, + 'level' => 'permanent', + 'reason' => 'Exceeded no-show threshold', + 'blocked_until' => null, + 'created_by' => 'system', + 'is_active' => true, + 'created_at' => now(), + ]); + return; + } + + if ($strikeCount >= 6) { + UserBlock::create([ + 'user_id' => $customer->id, + 'level' => 'month', + 'reason' => 'Exceeded no-show threshold', + 'blocked_until' => now()->addDays(30), + 'created_by' => 'system', + 'is_active' => true, + 'created_at' => now(), + ]); + return; + } + + if ($strikeCount >= 3) { + UserBlock::create([ + 'user_id' => $customer->id, + 'level' => 'week', + 'reason' => 'Exceeded no-show threshold', + 'blocked_until' => now()->addDays(7), + 'created_by' => 'system', + 'is_active' => true, + 'created_at' => now(), + ]); + } } private function canCancel(Reservation $reservation): bool diff --git a/app/Http/Controllers/Api/VenueController.php b/app/Http/Controllers/Api/VenueController.php index 4033dc1..d0bc18a 100644 --- a/app/Http/Controllers/Api/VenueController.php +++ b/app/Http/Controllers/Api/VenueController.php @@ -3,13 +3,17 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Models\Amenity; +use App\Models\Offer; use App\Models\Role; use App\Models\User; use App\Models\Venue; +use App\Models\VenueImage; use App\Models\VenueTable; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Schema; use Illuminate\Validation\ValidationException; class VenueController extends Controller @@ -23,19 +27,8 @@ class VenueController extends Controller '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 = Venue::query()->select($this->venueSelectColumns()); + $this->applyVenueRelations($query); $query->where('is_active', true); @@ -50,9 +43,12 @@ class VenueController extends Controller $perPage = $validated['per_page'] ?? 10; $paginator = $query->orderByDesc('id')->paginate($perPage); + $data = collect($paginator->items()) + ->map(fn (Venue $venue) => $this->serializeVenue($venue)) + ->values(); return response()->json([ - 'data' => $paginator->items(), + 'data' => $data, 'meta' => [ 'current_page' => $paginator->currentPage(), 'per_page' => $paginator->perPage(), @@ -66,23 +62,14 @@ class VenueController extends Controller public function show(int $id) { $venue = Venue::query() - ->select([ - 'id', - 'name', - 'type', - 'description', - 'address_text', - 'lat', - 'lng', - 'amenities', - 'image_urls', - 'offers', - ]) + ->select($this->venueSelectColumns()) ->where('is_active', true) ->findOrFail($id); + $venue->loadMissing($this->venueRelations()); + return response()->json([ - 'data' => $venue, + 'data' => $this->serializeVenue($venue), ]); } @@ -111,24 +98,31 @@ class VenueController extends Controller 'offers.*.is_active' => 'nullable|boolean', ]); + $offers = $this->sanitizeOffers($validated['offers'] ?? []); + $imageUrls = $this->appendUploadedImages($request, $validated['image_urls'] ?? []); $venue = Venue::create([ $ownerColumn => $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, + 'lat' => $validated['lat'] ?? 0, + 'lng' => $validated['lng'] ?? 0, 'is_active' => true, 'phone' => $validated['phone'] ?? null, - 'amenities' => $validated['amenities'] ?? [], - 'image_urls' => $validated['image_urls'] ?? [], - 'offers' => $this->sanitizeOffers($validated['offers'] ?? []), + ...$this->inlineVenuePayload($validated['amenities'] ?? [], $imageUrls, $offers), ]); + $this->syncStructuredVenueData( + $venue, + $validated['amenities'] ?? [], + $imageUrls, + $offers + ); + $venue = $this->reloadVenueForResponse($venue->id); return response()->json([ 'message' => 'Venue created successfully', - 'venue' => $venue, + 'venue' => $this->serializeVenue($venue), ], 201); } @@ -143,25 +137,15 @@ class VenueController extends Controller $query = Venue::query() ->select([ - 'id', DB::raw($ownerColumn . ' as vendor_id'), - 'name', - 'type', - 'description', - 'address_text', - 'lat', - 'lng', - 'is_active', - 'phone', - 'amenities', - 'image_urls', - 'offers', + ...$this->venueSelectColumns(), ]) ->withCount([ 'tables as table_count' => function ($q) { $q->where('is_active', true); }, ]); + $this->applyVenueRelations($query); if (! empty($validated['search'])) { $query->where('name', 'like', '%' . $validated['search'] . '%'); @@ -169,9 +153,12 @@ class VenueController extends Controller $perPage = $validated['per_page'] ?? 20; $paginator = $query->orderByDesc('id')->paginate($perPage); + $data = collect($paginator->items()) + ->map(fn (Venue $venue) => $this->serializeVenue($venue, true)) + ->values(); return response()->json([ - 'data' => $paginator->items(), + 'data' => $data, 'meta' => [ 'current_page' => $paginator->currentPage(), 'per_page' => $paginator->perPage(), @@ -225,7 +212,7 @@ class VenueController extends Controller 'last_name' => 'User', 'email' => $validated['vendor_email'], 'phone' => $validated['vendor_phone'], - 'password' => Hash::make($validated['vendor_password']), + 'password_hash' => Hash::make($validated['vendor_password']), ]); $vendor->roles()->syncWithoutDetaching([$vendorRole->id]); $vendorId = $vendor->id; @@ -245,6 +232,7 @@ class VenueController extends Controller $ownerColumn = Venue::ownerColumn(); $imageUrls = $validated['image_urls'] ?? []; $imageUrls = $this->appendUploadedImages($request, $imageUrls); + $offers = $this->sanitizeOffers($validated['offers'] ?? []); $venue = Venue::create([ $ownerColumn => $vendorId, @@ -252,25 +240,28 @@ class VenueController extends Controller 'type' => $validated['type'], 'description' => $validated['description'] ?? null, 'address_text' => $validated['address_text'] ?? null, - 'lat' => $validated['lat'] ?? null, - 'lng' => $validated['lng'] ?? null, + 'lat' => $validated['lat'] ?? 0, + 'lng' => $validated['lng'] ?? 0, 'is_active' => $validated['is_active'] ?? true, 'phone' => $validated['phone'] ?? null, - 'amenities' => $validated['amenities'] ?? [], - 'image_urls' => $imageUrls, - 'offers' => $this->sanitizeOffers($validated['offers'] ?? []), + ...$this->inlineVenuePayload($validated['amenities'] ?? [], $imageUrls, $offers), ]); + $this->syncStructuredVenueData( + $venue, + $validated['amenities'] ?? [], + $imageUrls, + $offers + ); $this->syncVenueTables($venue, (int) ($validated['table_count'] ?? 4)); }); + $venue = $this->reloadVenueForResponse($venue->id); return response()->json([ 'message' => 'Venue created successfully', - 'venue' => $venue->loadCount([ - 'tables as table_count' => function ($q) { - $q->where('is_active', true); - }, - ]), + 'venue' => $this->serializeVenue($venue->loadCount([ + 'tables as table_count' => fn ($q) => $q->where('is_active', true), + ]), true), ], 201); } @@ -307,26 +298,62 @@ class VenueController extends Controller $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); + $hasAmenityInput = array_key_exists('amenities', $validated); + $hasOfferInput = array_key_exists('offers', $validated); + $hasImageInput = array_key_exists('image_urls', $validated) || $request->hasFile('images'); + $resolvedImageUrls = $hasImageInput + ? $this->appendUploadedImages( + $request, + array_key_exists('image_urls', $validated) + ? ($validated['image_urls'] ?? []) + : $this->resolveImageUrls($venue) + ) + : null; - $venue->fill($validated); + $payload = collect($validated)->only([ + 'vendor_id', + 'name', + 'type', + 'description', + 'address_text', + 'lat', + 'lng', + 'is_active', + 'phone', + ])->all(); + + if ($this->hasInlineVenueArrays()) { + if ($hasAmenityInput) { + $payload['amenities'] = $validated['amenities'] ?? []; + } + if ($hasOfferInput) { + $payload['offers'] = $validated['offers'] ?? []; + } + if ($hasImageInput) { + $payload['image_urls'] = $resolvedImageUrls ?? []; + } + } + + $venue->fill($payload); $venue->save(); + $this->syncStructuredVenueData( + $venue, + $hasAmenityInput ? ($validated['amenities'] ?? []) : null, + $hasImageInput ? ($resolvedImageUrls ?? []) : null, + $hasOfferInput ? ($validated['offers'] ?? []) : null + ); if (array_key_exists('table_count', $validated)) { $this->syncVenueTables($venue, (int) $validated['table_count']); } }); + $venue = $this->reloadVenueForResponse($venue->id)->loadCount([ + 'tables as table_count' => fn ($q) => $q->where('is_active', true), + ]); return response()->json([ 'message' => 'Venue updated successfully', - 'venue' => $venue->loadCount([ - 'tables as table_count' => function ($q) { - $q->where('is_active', true); - }, - ]), + 'venue' => $this->serializeVenue($venue, true), ]); } @@ -369,7 +396,7 @@ class VenueController extends Controller VenueTable::create([ 'venue_id' => $venue->id, 'seating_area_id' => null, - 'name' => 'Table ' . $i, + VenueTable::labelColumn() => 'Table ' . $i, 'capacity' => 4, 'is_active' => true, ]); @@ -429,4 +456,207 @@ class VenueController extends Controller return $urls; } + + private function venueSelectColumns(): array + { + $columns = [ + 'id', + 'name', + 'type', + 'description', + 'address_text', + 'lat', + 'lng', + 'is_active', + 'phone', + ]; + + if ($this->hasInlineVenueArrays()) { + $columns[] = 'amenities'; + $columns[] = 'image_urls'; + $columns[] = 'offers'; + } + + return $columns; + } + + private function venueRelations(): array + { + if (! $this->hasStructuredVenueData()) { + return []; + } + + return [ + 'images:id,venue_id,url,sort_order', + 'offersRelation:id,venue_id,title,description,image_url,is_active,start_at,end_at', + 'amenitiesRelation:id,name', + ]; + } + + private function applyVenueRelations($query): void + { + $relations = $this->venueRelations(); + if (! empty($relations)) { + $query->with($relations); + } + } + + private function reloadVenueForResponse(int $venueId): Venue + { + $query = Venue::query()->select($this->venueSelectColumns()); + $this->applyVenueRelations($query); + + return $query->findOrFail($venueId); + } + + private function serializeVenue(Venue $venue, bool $includeAdminFields = false): array + { + $data = [ + 'id' => $venue->id, + 'name' => $venue->name, + 'type' => $venue->type, + 'description' => $venue->description, + 'address_text' => $venue->address_text, + 'lat' => $venue->lat, + 'lng' => $venue->lng, + 'phone' => $venue->phone, + 'is_active' => (bool) $venue->is_active, + 'amenities' => $this->resolveAmenities($venue), + 'image_urls' => $this->resolveImageUrls($venue), + 'offers' => $this->resolveOffers($venue), + ]; + + if ($includeAdminFields) { + $ownerColumn = Venue::ownerColumn(); + $data['vendor_id'] = $venue->vendor_id ?? $venue->{$ownerColumn} ?? null; + $data['table_count'] = $venue->table_count ?? null; + } + + return $data; + } + + private function resolveAmenities(Venue $venue): array + { + if ($this->hasInlineVenueArrays()) { + return is_array($venue->amenities) ? $venue->amenities : []; + } + + if ($this->hasStructuredVenueData()) { + return ($venue->amenitiesRelation ?? collect()) + ->pluck('name') + ->values() + ->all(); + } + + return []; + } + + private function resolveImageUrls(Venue $venue): array + { + if ($this->hasInlineVenueArrays()) { + return is_array($venue->image_urls) ? $venue->image_urls : []; + } + + if ($this->hasStructuredVenueData()) { + return ($venue->images ?? collect()) + ->sortBy('sort_order') + ->pluck('url') + ->values() + ->all(); + } + + return []; + } + + private function resolveOffers(Venue $venue): array + { + if ($this->hasInlineVenueArrays()) { + return is_array($venue->offers) ? $venue->offers : []; + } + + if ($this->hasStructuredVenueData()) { + return ($venue->offersRelation ?? collect()) + ->map(fn (Offer $offer) => [ + 'title' => $offer->title, + 'description' => $offer->description, + 'image_url' => $offer->image_url, + 'is_active' => (bool) $offer->is_active, + ]) + ->values() + ->all(); + } + + return []; + } + + private function hasInlineVenueArrays(): bool + { + return Schema::hasColumn('venues', 'amenities') + && Schema::hasColumn('venues', 'image_urls') + && Schema::hasColumn('venues', 'offers'); + } + + private function hasStructuredVenueData(): bool + { + return Schema::hasTable('venue_images') + && Schema::hasTable('offers') + && Schema::hasTable('venue_amenities') + && Schema::hasTable('amenities'); + } + + private function inlineVenuePayload(array $amenities, array $imageUrls, array $offers): array + { + if (! $this->hasInlineVenueArrays()) { + return []; + } + + return [ + 'amenities' => $amenities, + 'image_urls' => $imageUrls, + 'offers' => $offers, + ]; + } + + private function syncStructuredVenueData(Venue $venue, ?array $amenities, ?array $imageUrls, ?array $offers): void + { + if (! $this->hasStructuredVenueData()) { + return; + } + + if ($amenities !== null) { + $amenityIds = collect($amenities) + ->map(fn ($name) => trim((string) $name)) + ->filter(fn ($name) => $name !== '') + ->map(fn ($name) => Amenity::firstOrCreate(['name' => $name])->id) + ->values() + ->all(); + $venue->amenitiesRelation()->sync($amenityIds); + } + + if ($imageUrls !== null) { + VenueImage::query()->where('venue_id', $venue->id)->delete(); + foreach (array_values($imageUrls) as $index => $url) { + VenueImage::create([ + 'venue_id' => $venue->id, + 'url' => $url, + 'sort_order' => $index, + ]); + } + } + + if ($offers !== null) { + Offer::query()->where('venue_id', $venue->id)->delete(); + foreach ($offers as $offer) { + Offer::create([ + 'venue_id' => $venue->id, + 'title' => $offer['title'], + 'description' => $offer['description'] ?: null, + 'image_url' => $offer['image_url'] ?: null, + 'is_active' => $offer['is_active'] ?? true, + 'start_at' => now(), + 'end_at' => now()->addDays(30), + ]); + } + } + } } diff --git a/app/Models/Amenity.php b/app/Models/Amenity.php new file mode 100644 index 0000000..c65da63 --- /dev/null +++ b/app/Models/Amenity.php @@ -0,0 +1,17 @@ + 'boolean', + 'start_at' => 'datetime', + 'end_at' => 'datetime', + ]; +} diff --git a/app/Models/ReservationStatusHistory.php b/app/Models/ReservationStatusHistory.php index 934d379..8c2b32a 100644 --- a/app/Models/ReservationStatusHistory.php +++ b/app/Models/ReservationStatusHistory.php @@ -4,21 +4,43 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Facades\Schema; class ReservationStatusHistory extends Model { use HasFactory; public $timestamps = false; + protected static ?string $resolvedTable = null; protected $fillable = [ 'reservation_id', 'old_status', 'new_status', 'changed_by_user_id', + 'note', 'created_at', ]; + public function __construct(array $attributes = []) + { + parent::__construct($attributes); + $this->table = self::tableName(); + } + + public static function tableName(): string + { + if (self::$resolvedTable !== null) { + return self::$resolvedTable; + } + + self::$resolvedTable = Schema::hasTable('reservation_status_histories') + ? 'reservation_status_histories' + : 'reservation_status_history'; + + return self::$resolvedTable; + } + public function reservation() { return $this->belongsTo(Reservation::class); diff --git a/app/Models/ReservationTableAssignment.php b/app/Models/ReservationTableAssignment.php index 259eab7..ea29575 100644 --- a/app/Models/ReservationTableAssignment.php +++ b/app/Models/ReservationTableAssignment.php @@ -4,15 +4,19 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Facades\Schema; class ReservationTableAssignment extends Model { use HasFactory; public $timestamps = false; + protected static ?string $resolvedTable = null; + protected static ?string $resolvedTableForeignKey = null; protected $fillable = [ 'reservation_id', + 'table_id', 'venue_table_id', 'assigned_at', ]; @@ -21,6 +25,49 @@ class ReservationTableAssignment extends Model 'assigned_at' => 'datetime', ]; + public function __construct(array $attributes = []) + { + parent::__construct($attributes); + $this->table = self::tableName(); + } + + public static function tableName(): string + { + if (self::$resolvedTable !== null) { + return self::$resolvedTable; + } + + self::$resolvedTable = Schema::hasTable('reservation_table_assignments') + ? 'reservation_table_assignments' + : 'reservation_tables'; + + return self::$resolvedTable; + } + + public static function tableForeignKey(): string + { + if (self::$resolvedTableForeignKey !== null) { + return self::$resolvedTableForeignKey; + } + + self::$resolvedTableForeignKey = Schema::hasColumn(self::tableName(), 'venue_table_id') + ? 'venue_table_id' + : 'table_id'; + + return self::$resolvedTableForeignKey; + } + + public function setVenueTableIdAttribute($value): void + { + $this->attributes[self::tableForeignKey()] = $value; + } + + public function getVenueTableIdAttribute() + { + $column = self::tableForeignKey(); + return $this->attributes[$column] ?? null; + } + public function reservation() { return $this->belongsTo(Reservation::class); @@ -28,6 +75,6 @@ class ReservationTableAssignment extends Model public function table() { - return $this->belongsTo(VenueTable::class, 'venue_table_id'); + return $this->belongsTo(VenueTable::class, self::tableForeignKey()); } } diff --git a/app/Models/User.php b/app/Models/User.php index 9baf853..e5d49a2 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -12,11 +12,15 @@ use App\Models\Reservation; use Illuminate\Support\Carbon; use App\Models\Notification; use App\Models\UserFeedback; +use App\Models\UserBlock; +use Illuminate\Support\Facades\Schema; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; + protected static ?bool $hasLegacyBlockColumns = null; + protected static ?bool $hasUserBlocksTable = null; /** * The attributes that are mass assignable. @@ -86,14 +90,53 @@ class User extends Authenticatable public function isBlocked(): bool { - if ($this->blocked_permanent) { - return true; + if ($this->hasLegacyBlockColumns()) { + if ($this->blocked_permanent) { + return true; + } + + if ($this->blocked_until && Carbon::parse($this->blocked_until)->isFuture()) { + return true; + } + + return false; } - if ($this->blocked_until && Carbon::parse($this->blocked_until)->isFuture()) { - return true; + if (! $this->hasUserBlocksTable()) { + return false; } - return false; + return UserBlock::query() + ->where('user_id', $this->id) + ->where('is_active', true) + ->where(function ($q) { + $q->whereNull('blocked_until') + ->orWhere('blocked_until', '>', now()); + }) + ->exists(); + } + + private function hasLegacyBlockColumns(): bool + { + if (self::$hasLegacyBlockColumns !== null) { + return self::$hasLegacyBlockColumns; + } + + self::$hasLegacyBlockColumns = Schema::hasColumn('users', 'blocked_until') + && Schema::hasColumn('users', 'blocked_permanent') + && Schema::hasColumn('users', 'strike_count'); + + return self::$hasLegacyBlockColumns; + } + + private function hasUserBlocksTable(): bool + { + if (self::$hasUserBlocksTable !== null) { + return self::$hasUserBlocksTable; + } + + self::$hasUserBlocksTable = Schema::hasTable('user_blocks'); + + return self::$hasUserBlocksTable; } } diff --git a/app/Models/UserBlock.php b/app/Models/UserBlock.php new file mode 100644 index 0000000..91f9a1d --- /dev/null +++ b/app/Models/UserBlock.php @@ -0,0 +1,32 @@ + 'datetime', + 'is_active' => 'boolean', + ]; +} diff --git a/app/Models/UserStrike.php b/app/Models/UserStrike.php new file mode 100644 index 0000000..45c8da6 --- /dev/null +++ b/app/Models/UserStrike.php @@ -0,0 +1,23 @@ +hasMany(VenueTable::class); } + + public function images() + { + return $this->hasMany(VenueImage::class); + } + + public function offersRelation() + { + return $this->hasMany(Offer::class); + } + + public function amenitiesRelation() + { + return $this->belongsToMany(Amenity::class, 'venue_amenities', 'venue_id', 'amenity_id'); + } } diff --git a/app/Models/VenueImage.php b/app/Models/VenueImage.php new file mode 100644 index 0000000..d580c29 --- /dev/null +++ b/app/Models/VenueImage.php @@ -0,0 +1,21 @@ +