feat: moderation

This commit is contained in:
2026-05-22 11:56:17 +02:00
parent f5b460c650
commit 1c472e46db
43 changed files with 1836 additions and 3 deletions
+26
View File
@@ -11,6 +11,8 @@ 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\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class MealPosts extends Model
@@ -31,6 +33,9 @@ class MealPosts extends Model
'type',
'ai_generated',
'diet_type',
'hidden_at',
'hidden_by',
'hidden_reason',
];
protected $attributes = [
@@ -60,6 +65,26 @@ class MealPosts extends Model
->withTimestamps();
}
public function reports(): MorphMany
{
return $this->morphMany(Report::class, 'reportable');
}
public function moderationCase(): MorphOne
{
return $this->morphOne(ModerationCase::class, 'caseable');
}
public function hiddenBy(): BelongsTo
{
return $this->belongsTo(User::class, 'hidden_by');
}
public function isHidden(): bool
{
return $this->hidden_at !== null;
}
protected function casts(): array
{
return [
@@ -68,6 +93,7 @@ class MealPosts extends Model
'type' => MealPostType::class,
'diet_type' => DietType::class,
'ai_generated' => 'boolean',
'hidden_at' => 'datetime',
];
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Models;
use App\Enums\ModerationCaseStatus;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class ModerationCase extends Model
{
/** @use HasFactory<\Database\Factories\ModerationCaseFactory> */
use HasFactory, HasUlids;
protected $fillable = [
'caseable_type',
'caseable_id',
'status',
'reports_count',
'threshold',
'threshold_notified_at',
'assigned_to',
'resolved_by',
'resolved_at',
'resolution_note',
];
public function caseable(): MorphTo
{
return $this->morphTo();
}
public function reports(): HasMany
{
return $this->hasMany(Report::class);
}
public function assignedTo(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_to');
}
public function resolvedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'resolved_by');
}
public function targetType(): string
{
return match ($this->caseable_type) {
MealPosts::class => 'meal_post',
User::class => 'user',
default => 'unknown',
};
}
public function targetLabel(): string
{
return match ($this->caseable_type) {
MealPosts::class => __('admin.moderation_cases.targets.meal_post'),
User::class => __('admin.moderation_cases.targets.user'),
default => __('admin.moderation_cases.targets.unknown'),
};
}
public function targetTitle(): string
{
$target = $this->caseable;
return match (true) {
$target instanceof MealPosts => $target->title,
$target instanceof User => $target->name,
default => (string) $this->caseable_id,
};
}
protected function casts(): array
{
return [
'status' => ModerationCaseStatus::class,
'reports_count' => 'integer',
'threshold' => 'integer',
'threshold_notified_at' => 'datetime',
'resolved_at' => 'datetime',
];
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Models;
use App\Enums\ReportReason;
use App\Enums\ReportStatus;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Report extends Model
{
/** @use HasFactory<\Database\Factories\ReportFactory> */
use HasFactory, HasUlids;
protected $fillable = [
'moderation_case_id',
'reporter_id',
'reportable_type',
'reportable_id',
'reason',
'details',
'status',
'reviewed_by',
'reviewed_at',
];
public function moderationCase(): BelongsTo
{
return $this->belongsTo(ModerationCase::class);
}
public function reporter(): BelongsTo
{
return $this->belongsTo(User::class, 'reporter_id');
}
public function reviewedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'reviewed_by');
}
public function reportable(): MorphTo
{
return $this->morphTo();
}
protected function casts(): array
{
return [
'reason' => ReportReason::class,
'status' => ReportStatus::class,
'reviewed_at' => 'datetime',
];
}
}
+42
View File
@@ -13,8 +13,11 @@ use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Contracts\Translation\HasLocalePreference;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@@ -50,6 +53,9 @@ class User extends Authenticatable implements FilamentUser, HasLocalePreference,
'pace_preference',
'sex',
'date_of_birth',
'suspended_at',
'suspended_by',
'suspended_reason',
];
/**
@@ -83,6 +89,7 @@ class User extends Authenticatable implements FilamentUser, HasLocalePreference,
'sex' => UserSex::class,
'role' => UserRole::class,
'date_of_birth' => 'immutable_date',
'suspended_at' => 'datetime',
];
}
@@ -106,6 +113,41 @@ class User extends Authenticatable implements FilamentUser, HasLocalePreference,
return $this->hasOne(StravaConnection::class);
}
public function reportsMade(): HasMany
{
return $this->hasMany(Report::class, 'reporter_id');
}
public function receivedReports(): MorphMany
{
return $this->morphMany(Report::class, 'reportable');
}
public function moderationCase(): MorphOne
{
return $this->morphOne(ModerationCase::class, 'caseable');
}
public function assignedModerationCases(): HasMany
{
return $this->hasMany(ModerationCase::class, 'assigned_to');
}
public function resolvedModerationCases(): HasMany
{
return $this->hasMany(ModerationCase::class, 'resolved_by');
}
public function suspendedBy(): BelongsTo
{
return $this->belongsTo(self::class, 'suspended_by');
}
public function isSuspended(): bool
{
return $this->suspended_at !== null;
}
public function routeNotificationForExpoPush(): array
{
return $this->deviceTokens()