10-04-2026
This commit is contained in:
parent
4d6b4930b2
commit
4bb89aad8c
836 changed files with 52961 additions and 5950 deletions
155
app/Models/CabinetTabletSetting.php
Normal file
155
app/Models/CabinetTabletSetting.php
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class CabinetTabletSetting extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\CabinetTabletSettingFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'store_status',
|
||||
'notice_headline',
|
||||
'notice_subtext',
|
||||
'override_open_today',
|
||||
'override_close_today',
|
||||
'next_appointment_date',
|
||||
'next_appointment_time',
|
||||
'hours_monday_open', 'hours_monday_close',
|
||||
'hours_tuesday_open', 'hours_tuesday_close',
|
||||
'hours_wednesday_open', 'hours_wednesday_close',
|
||||
'hours_thursday_open', 'hours_thursday_close',
|
||||
'hours_friday_open', 'hours_friday_close',
|
||||
'hours_saturday_open', 'hours_saturday_close',
|
||||
'hours_sunday_open', 'hours_sunday_close',
|
||||
'contact_phone',
|
||||
'contact_email',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'next_appointment_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
private const DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
|
||||
private const GERMAN_DAY_LABELS = [
|
||||
'monday' => 'Montag',
|
||||
'tuesday' => 'Dienstag',
|
||||
'wednesday' => 'Mittwoch',
|
||||
'thursday' => 'Donnerstag',
|
||||
'friday' => 'Freitag',
|
||||
'saturday' => 'Samstag',
|
||||
'sunday' => 'Sonntag',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get or create the singleton settings row.
|
||||
*/
|
||||
public static function current(): self
|
||||
{
|
||||
return static::firstOrCreate(['id' => 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get opening hours as display strings for the frontend (e.g. "10:00 – 18:00" or "Geschlossen").
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getHoursArray(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (self::DAYS as $day) {
|
||||
$open = $this->{"hours_{$day}_open"};
|
||||
$close = $this->{"hours_{$day}_close"};
|
||||
$result[$day] = ($open && $close) ? "{$open} – {$close}" : 'Geschlossen';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the effective store status based on opening hours and current Berlin time.
|
||||
*
|
||||
* Returns the display status ('open', 'closed', 'notice'), the next opening time when
|
||||
* closed, and today's closing time when open.
|
||||
*
|
||||
* @return array{status: string, today_close: string|null, next_open: array{label: string, time: string}|null}
|
||||
*/
|
||||
public function computeStatus(): array
|
||||
{
|
||||
if ($this->store_status === 'notice' || $this->store_status === 'warning') {
|
||||
return ['status' => $this->store_status, 'today_close' => null, 'next_open' => null];
|
||||
}
|
||||
|
||||
if ($this->store_status === 'closed') {
|
||||
$now = Carbon::now('Europe/Berlin');
|
||||
|
||||
return ['status' => 'closed', 'today_close' => null, 'next_open' => $this->findNextOpenTime($now, true)];
|
||||
}
|
||||
|
||||
// Auto mode: compute from opening hours
|
||||
$now = Carbon::now('Europe/Berlin');
|
||||
$dayKey = strtolower($now->englishDayOfWeek);
|
||||
|
||||
$openTime = $this->override_open_today ?: $this->{"hours_{$dayKey}_open"};
|
||||
$closeTime = $this->override_close_today ?: $this->{"hours_{$dayKey}_close"};
|
||||
$currentHHMM = $now->format('H:i');
|
||||
|
||||
if ($openTime && $closeTime && $currentHHMM >= $openTime && $currentHHMM < $closeTime) {
|
||||
return ['status' => 'open', 'today_close' => $closeTime, 'next_open' => null];
|
||||
}
|
||||
|
||||
return ['status' => 'closed', 'today_close' => null, 'next_open' => $this->findNextOpenTime($now, false)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next upcoming opening time from the given Berlin datetime.
|
||||
*
|
||||
* @return array{label: string, time: string}|null
|
||||
*/
|
||||
private function findNextOpenTime(Carbon $now, bool $skipToday): ?array
|
||||
{
|
||||
// Check if today still has an upcoming opening (we might be before opening time)
|
||||
if (! $skipToday) {
|
||||
$dayKey = strtolower($now->englishDayOfWeek);
|
||||
$todayOpen = $this->override_open_today ?: $this->{"hours_{$dayKey}_open"};
|
||||
|
||||
if ($todayOpen && $now->format('H:i') < $todayOpen) {
|
||||
return ['label' => 'Heute', 'time' => $todayOpen];
|
||||
}
|
||||
}
|
||||
|
||||
// Look ahead up to 7 days
|
||||
for ($i = 1; $i <= 7; $i++) {
|
||||
$checkDate = $now->copy()->addDays($i);
|
||||
$dayKey = strtolower($checkDate->englishDayOfWeek);
|
||||
$openTime = $this->{"hours_{$dayKey}_open"};
|
||||
|
||||
if ($openTime) {
|
||||
$label = $i === 1 ? 'Morgen' : self::GERMAN_DAY_LABELS[$dayKey];
|
||||
|
||||
return ['label' => $label, 'time' => $openTime];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear override times (called by midnight scheduler).
|
||||
*/
|
||||
public function clearOverrides(): void
|
||||
{
|
||||
$this->update([
|
||||
'override_open_today' => null,
|
||||
'override_close_today' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
74
app/Models/CmsArticle.php
Normal file
74
app/Models/CmsArticle.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
|
||||
class CmsArticle extends Model
|
||||
{
|
||||
use HasFactory, HasTranslations;
|
||||
|
||||
protected $table = 'cms_articles';
|
||||
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'title',
|
||||
'subtitle',
|
||||
'image',
|
||||
'category',
|
||||
'date_label',
|
||||
'read_time',
|
||||
'author',
|
||||
'content',
|
||||
'is_published',
|
||||
'order',
|
||||
];
|
||||
|
||||
/** @var array<string> */
|
||||
public array $translatable = [
|
||||
'title',
|
||||
'subtitle',
|
||||
'content',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'author' => 'array',
|
||||
'is_published' => 'boolean',
|
||||
'order' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function scopePublished(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_published', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('order')->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toFrontendArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'slug' => $this->slug,
|
||||
'title' => $this->title,
|
||||
'subtitle' => $this->subtitle,
|
||||
'image' => $this->image,
|
||||
'category' => $this->category,
|
||||
'date' => $this->date_label,
|
||||
'readTime' => $this->read_time,
|
||||
'author' => $this->author ?? [],
|
||||
'content' => $this->content ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
111
app/Models/CmsProject.php
Normal file
111
app/Models/CmsProject.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Helpers\PriceHelper;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
|
||||
class CmsProject extends Model
|
||||
{
|
||||
use HasFactory, HasTranslations;
|
||||
|
||||
protected $table = 'cms_projects';
|
||||
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'title',
|
||||
'location',
|
||||
'status',
|
||||
'launch_date',
|
||||
'price_from_aed',
|
||||
'currency',
|
||||
'image',
|
||||
'highlights',
|
||||
'quick_facts',
|
||||
'investment_case',
|
||||
'gallery',
|
||||
'location_info',
|
||||
'contact',
|
||||
'investor_trust',
|
||||
'furniture_benefit',
|
||||
'is_published',
|
||||
'order',
|
||||
];
|
||||
|
||||
/** @var array<string> */
|
||||
public array $translatable = [
|
||||
'title',
|
||||
'location',
|
||||
'highlights',
|
||||
'investment_case',
|
||||
'location_info',
|
||||
'contact',
|
||||
'investor_trust',
|
||||
'furniture_benefit',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'launch_date' => 'date',
|
||||
'price_from_aed' => 'integer',
|
||||
'highlights' => 'array',
|
||||
'quick_facts' => 'array',
|
||||
'investment_case' => 'array',
|
||||
'gallery' => 'array',
|
||||
'location_info' => 'array',
|
||||
'contact' => 'array',
|
||||
'is_published' => 'boolean',
|
||||
'order' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function scopePublished(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_published', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('order')->orderByDesc('launch_date');
|
||||
}
|
||||
|
||||
public function getFormattedPrice(string $prefix = 'ab'): string
|
||||
{
|
||||
if (! $this->price_from_aed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return PriceHelper::formatAed($this->price_from_aed, $prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array compatible with the existing Blade views
|
||||
* (immobilien.blade.php and immobilien-show.blade.php).
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toFrontendArray(): array
|
||||
{
|
||||
return [
|
||||
'slug' => $this->slug,
|
||||
'title' => $this->title,
|
||||
'location' => $this->location,
|
||||
'status' => $this->status,
|
||||
'launch_date' => $this->launch_date?->format('d.m.Y'),
|
||||
'price_from' => $this->getFormattedPrice(),
|
||||
'image' => $this->image,
|
||||
'highlights' => $this->highlights ?? [],
|
||||
'quick_facts' => $this->quick_facts ?? [],
|
||||
'investment_case' => $this->investment_case ?? [],
|
||||
'gallery' => $this->gallery ?? [],
|
||||
'location_info' => $this->location_info ?? [],
|
||||
'contact' => $this->contact ?? [],
|
||||
'investor_trust' => $this->investor_trust ?? [],
|
||||
'furniture_benefit' => $this->furniture_benefit ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
33
app/Models/Display.php
Normal file
33
app/Models/Display.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Display extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\DisplayFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'location',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function versions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(DisplayVersion::class, 'display_display_version')
|
||||
->withPivot('sort_order')
|
||||
->orderByPivot('sort_order');
|
||||
}
|
||||
}
|
||||
153
app/Models/DisplayMedia.php
Normal file
153
app/Models/DisplayMedia.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DisplayMedia extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\DisplayMediaFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'display_media';
|
||||
|
||||
protected $fillable = [
|
||||
'filename',
|
||||
'disk',
|
||||
'path',
|
||||
'external_url',
|
||||
'source_type',
|
||||
'type',
|
||||
'mime_type',
|
||||
'file_size',
|
||||
'thumbnail_path',
|
||||
'alt_text',
|
||||
'title',
|
||||
'collection',
|
||||
'metadata',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'metadata' => 'array',
|
||||
'file_size' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ACCESSORS
|
||||
// ========================================
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
if ($this->isExternal()) {
|
||||
return $this->external_url;
|
||||
}
|
||||
|
||||
return Storage::disk($this->disk)->url($this->path);
|
||||
}
|
||||
|
||||
public function getThumbnailUrl(): ?string
|
||||
{
|
||||
if ($this->thumbnail_path) {
|
||||
return Storage::disk($this->disk)->url($this->thumbnail_path);
|
||||
}
|
||||
|
||||
if ($this->isUpload() && $this->isImage()) {
|
||||
return $this->getUrl();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TYPE CHECKS
|
||||
// ========================================
|
||||
|
||||
public function isUpload(): bool
|
||||
{
|
||||
return $this->source_type === 'upload';
|
||||
}
|
||||
|
||||
public function isExternal(): bool
|
||||
{
|
||||
return $this->source_type === 'external';
|
||||
}
|
||||
|
||||
public function isImage(): bool
|
||||
{
|
||||
return $this->type === 'image';
|
||||
}
|
||||
|
||||
public function isVideo(): bool
|
||||
{
|
||||
return $this->type === 'video';
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DISPLAY HELPERS
|
||||
// ========================================
|
||||
|
||||
public function getHumanFileSize(): string
|
||||
{
|
||||
if ($this->file_size === 0) {
|
||||
return $this->isExternal() ? 'Extern' : '0 B';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$size = $this->file_size;
|
||||
$unitIndex = 0;
|
||||
|
||||
while ($size >= 1024 && $unitIndex < count($units) - 1) {
|
||||
$size /= 1024;
|
||||
$unitIndex++;
|
||||
}
|
||||
|
||||
return round($size, 1).' '.$units[$unitIndex];
|
||||
}
|
||||
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return $this->title ?: $this->filename;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SCOPES
|
||||
// ========================================
|
||||
|
||||
public function scopeImages(Builder $query): Builder
|
||||
{
|
||||
return $query->where('type', 'image');
|
||||
}
|
||||
|
||||
public function scopeVideos(Builder $query): Builder
|
||||
{
|
||||
return $query->where('type', 'video');
|
||||
}
|
||||
|
||||
public function scopeUploads(Builder $query): Builder
|
||||
{
|
||||
return $query->where('source_type', 'upload');
|
||||
}
|
||||
|
||||
public function scopeExternals(Builder $query): Builder
|
||||
{
|
||||
return $query->where('source_type', 'external');
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeInCollection(Builder $query, string $collection): Builder
|
||||
{
|
||||
return $query->where('collection', $collection);
|
||||
}
|
||||
}
|
||||
67
app/Models/DisplayVersion.php
Normal file
67
app/Models/DisplayVersion.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\DisplayVersionType;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class DisplayVersion extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\DisplayVersionFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'type',
|
||||
'settings',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => DisplayVersionType::class,
|
||||
'settings' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(DisplayVersionItem::class)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function displays(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Display::class, 'display_display_version')
|
||||
->withPivot('sort_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<DisplayVersionItem, $this>
|
||||
*/
|
||||
public function activeItems(?string $itemType = null): HasMany
|
||||
{
|
||||
$query = $this->items()->where('is_active', true);
|
||||
|
||||
if ($itemType) {
|
||||
$query->where('item_type', $itemType);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function scopeOfType(Builder $query, DisplayVersionType $type): Builder
|
||||
{
|
||||
return $query->where('type', $type);
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
}
|
||||
41
app/Models/DisplayVersionItem.php
Normal file
41
app/Models/DisplayVersionItem.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class DisplayVersionItem extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\DisplayVersionItemFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'display_version_id',
|
||||
'item_type',
|
||||
'content',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'content' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function version(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DisplayVersion::class, 'display_version_id');
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true)->orderBy('sort_order');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue