*/ use HasFactory, HasUniqueSlug, SoftDeletes; /** * @return list */ protected function slugScopeAttributes(): array { return ['portal']; } protected function slugFallback(): string { return 'company'; } protected static function booted(): void { static::addGlobalScope(new PortalScope); } protected $fillable = [ 'portal', 'owner_user_id', 'type', 'name', 'slug', 'address', 'country_code', 'phone', 'fax', 'email', 'website', 'logo_path', 'logo_variants', 'boilerplate', 'is_active', 'gone_at', 'is_active_before_gone', 'disable_footer_code', 'legacy_portal', 'legacy_id', ]; protected function casts(): array { return [ 'portal' => Portal::class, 'type' => CompanyType::class, 'logo_variants' => 'array', 'is_active' => 'boolean', 'gone_at' => 'datetime', 'is_active_before_gone' => 'boolean', 'disable_footer_code' => 'boolean', 'deleted_at' => 'datetime', ]; } /** * Firmensuche für die Admin-Combobox: Volltext (name/email/slug) bei * unterstützenden Treibern und Suchbegriffen ab 3 Zeichen, sonst LIKE. * Bei leerem Begriff bleibt die Query unverändert. Single Source of Truth * für die zuvor in mehreren Editoren duplizierte Such- und Treiberlogik. */ public function scopeSearchByTerm(Builder $query, string $term): Builder { $term = Portal::stripTrailingAbbreviation($term); if (! filled($term)) { return $query; } $supportsFullText = mb_strlen($term) >= 3 && in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true); return $query->where(function (Builder $query) use ($term, $supportsFullText): void { if ($supportsFullText) { $query->whereFullText(['name', 'email', 'slug'], $term); return; } $query->where('name', 'like', '%'.$term.'%') ->orWhere('slug', 'like', '%'.$term.'%'); }); } /** * Firmen, auf die ein User zugreifen darf: als Owner (owner_user_id) oder * als Mitglied über den company_user-Pivot. Single Source of Truth für die * zuvor in mehreren API-Controllern und Services duplizierte Closure. Das * Scope-Verhalten (mit/ohne globale Scopes) bestimmt der Aufrufer. */ public function scopeAccessibleBy(Builder $query, User $user): Builder { return $query->where(function (Builder $query) use ($user): void { $query->where('owner_user_id', $user->id) ->orWhereHas('users', fn (Builder $users) => $users->whereKey($user->id)); }); } /** * Mit 410 entfernter Newsroom: die Newsroom-URL bleibt bestehen und liefert * einen Tombstone statt eines 404. Gegenstück zur vollständigen Löschung * (Soft Delete). */ public function isGone(): bool { return $this->gone_at !== null; } /** * Normalisierte Firmen-E-Mail — der Schlüssel der Newsroom-Konsolidierung. * * Die eine Stelle, die festlegt, was „dieselbe Adresse" heißt. * Klassifizierer, Bericht, Zusammenführung und laufende Zuordnung müssen * exakt gleich normalisieren, sonst finden sie einander nicht. */ public static function emailKey(?string $email): ?string { $key = mb_strtolower(trim((string) $email)); return $key === '' ? null : $key; } /** * @return BelongsTo */ public function owner(): BelongsTo { return $this->belongsTo(User::class, 'owner_user_id'); } /** * @return BelongsToMany */ public function users(): BelongsToMany { return $this->belongsToMany(User::class) ->withPivot('role') ->withTimestamps(); } /** * @return HasMany */ public function contacts(): HasMany { return $this->hasMany(Contact::class); } /** * @return HasMany */ public function pressReleases(): HasMany { return $this->hasMany(PressRelease::class); } /** * @return BelongsToMany */ public function userPaymentOptions(): BelongsToMany { return $this->belongsToMany(UserPaymentOption::class, 'user_payment_option_company') ->withPivot('is_active') ->withTimestamps(); } public function logoUrl(): ?string { if (blank($this->logo_path)) { return null; } $logoPath = trim((string) $this->logo_path); $legacyPortal = $this->legacy_portal ?: $this->portal?->value; $logoFilename = basename((string) (parse_url($logoPath, PHP_URL_PATH) ?: $logoPath)); $candidates = []; if (Str::startsWith($logoPath, '/storage/')) { $candidates[] = Str::after($logoPath, '/storage/'); } elseif (! Str::startsWith($logoPath, ['http://', 'https://'])) { $candidates[] = ltrim($logoPath, '/'); } if (filled($logoFilename)) { $candidates[] = "company-logos/{$legacyPortal}/{$this->id}/{$logoFilename}"; $candidates[] = "{$legacyPortal}/company/{$logoFilename}"; } foreach (array_unique($candidates) as $candidate) { if (Storage::disk('public')->exists($candidate)) { return asset('storage/'.$candidate); } } if (Str::startsWith($logoPath, ['http://', 'https://']) && blank($this->legacy_portal)) { return $logoPath; } return null; } /** * Absolute Website-URL inklusive Schema. Bestandsdaten enthalten oft * Adressen wie „www.example.de“ — als roher href würde der Browser * daraus einen relativen Link auf der aktuellen Seite machen. */ public function websiteUrl(): ?string { if (blank($this->website)) { return null; } $website = trim((string) $this->website); return Str::startsWith($website, ['http://', 'https://']) ? $website : 'https://'.$website; } }