tenant admin startseite schicker gestaltet und super-admin und tenant admin (filament) aufgesplittet.

Es gibt nun task collections und vordefinierte tasks für alle. Onboarding verfeinert und webseite-carousel gefixt (logging später entfernen!)
This commit is contained in:
Codex Agent
2025-10-14 15:17:52 +02:00
parent 64a5411fb9
commit 1a4bdb1fe1
92 changed files with 6027 additions and 515 deletions

View File

@@ -10,7 +10,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Spatie\Translatable\HasTranslations;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
class BlogPost extends Model
{
@@ -54,7 +60,15 @@ class BlogPost extends Model
{
return Attribute::get(function () {
$markdown = $this->getTranslation('content', app()->getLocale());
$converter = new CommonMarkConverter();
$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new StrikethroughExtension());
$environment->addExtension(new TaskListExtension());
$converter = new MarkdownConverter($environment);
return $converter->convert($markdown);
});
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -18,6 +19,11 @@ class Emotion extends Model
'description' => 'array',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function eventTypes(): BelongsToMany
{
return $this->belongsToMany(EventType::class, 'emotion_event_type', 'emotion_id', 'event_type_id');

View File

@@ -6,10 +6,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Znck\Eloquent\Relations\BelongsToThrough as BelongsToThroughRelation;
use Znck\Eloquent\Traits\BelongsToThrough;
class Photo extends Model
{
use HasFactory;
use BelongsToThrough;
protected $table = 'photos';
protected $guarded = [];
@@ -47,5 +50,12 @@ class Photo extends Model
{
return $this->hasMany(PhotoLike::class);
}
}
public function tenant(): BelongsToThroughRelation
{
return $this->belongsToThrough(
Tenant::class,
Event::class
);
}
}

View File

@@ -2,11 +2,14 @@
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;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class Task extends Model
{
@@ -38,6 +41,59 @@ class Task extends Model
return $this->belongsTo(TaskCollection::class, 'collection_id');
}
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function sourceTask(): BelongsTo
{
return $this->belongsTo(Task::class, 'source_task_id');
}
public function derivedTasks(): HasMany
{
return $this->hasMany(Task::class, 'source_task_id');
}
public function sourceCollection(): BelongsTo
{
return $this->belongsTo(TaskCollection::class, 'source_collection_id');
}
public function scopeForTenant(Builder $query, ?int $tenantId): Builder
{
return $query->where(function (Builder $innerQuery) use ($tenantId) {
$innerQuery->whereNull('tenant_id');
if ($tenantId) {
$innerQuery->orWhere('tenant_id', $tenantId);
}
});
}
protected static function booted(): void
{
static::creating(function (Task $task) {
if (! $task->slug) {
$task->slug = static::generateSlug(
$task->title['en'] ?? $task->title['de'] ?? 'task'
);
}
});
}
protected static function generateSlug(string $base): string
{
$slugBase = Str::slug($base) ?: 'task';
do {
$slug = $slugBase . '-' . Str::random(6);
} while (static::where('slug', $slug)->exists());
return $slug;
}
public function assignedEvents(): BelongsToMany
{
return $this->belongsToMany(Event::class, 'event_task', 'task_id', 'event_id')

View File

@@ -4,7 +4,10 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Arr;
class TaskCollection extends Model
{
@@ -14,10 +17,40 @@ class TaskCollection extends Model
protected $fillable = [
'tenant_id',
'name',
'description',
'slug',
'name_translations',
'description_translations',
'event_type_id',
'source_collection_id',
'is_default',
'position',
];
protected $casts = [
'name_translations' => 'array',
'description_translations' => 'array',
];
public function eventType(): BelongsTo
{
return $this->belongsTo(EventType::class);
}
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function sourceCollection(): BelongsTo
{
return $this->belongsTo(TaskCollection::class, 'source_collection_id');
}
public function derivedCollections(): HasMany
{
return $this->hasMany(TaskCollection::class, 'source_collection_id');
}
public function tasks(): BelongsToMany
{
return $this->belongsToMany(
@@ -25,7 +58,7 @@ class TaskCollection extends Model
'task_collection_task',
'task_collection_id',
'task_id'
);
)->withPivot(['sort_order']);
}
public function events(): BelongsToMany
@@ -35,7 +68,49 @@ class TaskCollection extends Model
'event_task_collection',
'task_collection_id',
'event_id'
);
)->withPivot(['sort_order'])->withTimestamps();
}
public function scopeGlobal($query)
{
return $query->whereNull('tenant_id');
}
public function scopeForTenant($query, ?int $tenantId)
{
return $query->where(function ($inner) use ($tenantId) {
$inner->whereNull('tenant_id');
if ($tenantId) {
$inner->orWhere('tenant_id', $tenantId);
}
});
}
public function getNameAttribute(): string
{
return $this->resolveTranslation('name_translations');
}
public function getDescriptionAttribute(): ?string
{
$value = $this->resolveTranslation('description_translations');
return $value ?: null;
}
protected function resolveTranslation(string $attribute, ?string $locale = null): string
{
$translations = $this->{$attribute} ?? [];
if (is_string($translations)) {
$translations = json_decode($translations, true) ?: [];
}
$locale = $locale ?? app()->getLocale();
return $translations[$locale]
?? Arr::first($translations)
?? '';
}
}

View File

@@ -116,7 +116,11 @@ class User extends Authenticatable implements MustVerifyEmail, HasName, Filament
return false;
}
return in_array($this->role, ['tenant_admin', 'super_admin'], true);
return match ($panel->getId()) {
'superadmin' => $this->role === 'super_admin',
'admin' => $this->role === 'tenant_admin',
default => false,
};
}
public function canAccessTenant(Model $tenant): bool