feat: moderation
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModerationCases;
|
||||
|
||||
use App\Filament\Resources\ModerationCases\Pages\EditModerationCase;
|
||||
use App\Filament\Resources\ModerationCases\Pages\ListModerationCases;
|
||||
use App\Filament\Resources\ModerationCases\Pages\ViewModerationCase;
|
||||
use App\Filament\Resources\ModerationCases\Schemas\ModerationCaseForm;
|
||||
use App\Filament\Resources\ModerationCases\Schemas\ModerationCaseInfolist;
|
||||
use App\Filament\Resources\ModerationCases\Tables\ModerationCasesTable;
|
||||
use App\Models\ModerationCase;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ModerationCaseResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ModerationCase::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('admin.moderation_cases.navigation.label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('admin.moderation_cases.navigation.singular');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('admin.moderation_cases.navigation.plural');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ModerationCaseForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return ModerationCaseInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ModerationCasesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListModerationCases::route('/'),
|
||||
'view' => ViewModerationCase::route('/{record}'),
|
||||
'edit' => EditModerationCase::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModerationCases\Pages;
|
||||
|
||||
use App\Filament\Resources\ModerationCases\ModerationCaseResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateModerationCase extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ModerationCaseResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModerationCases\Pages;
|
||||
|
||||
use App\Filament\Resources\ModerationCases\ModerationCaseResource;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditModerationCase extends EditRecord
|
||||
{
|
||||
protected static string $resource = ModerationCaseResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModerationCases\Pages;
|
||||
|
||||
use App\Filament\Resources\ModerationCases\ModerationCaseResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListModerationCases extends ListRecords
|
||||
{
|
||||
protected static string $resource = ModerationCaseResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModerationCases\Pages;
|
||||
|
||||
use App\Filament\Resources\ModerationCases\ModerationCaseResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewModerationCase extends ViewRecord
|
||||
{
|
||||
protected static string $resource = ModerationCaseResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModerationCases\Schemas;
|
||||
|
||||
use App\Enums\ModerationCaseStatus;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class ModerationCaseForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make(__('admin.moderation_cases.sections.case'))
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('status')
|
||||
->label(__('admin.moderation_cases.fields.status'))
|
||||
->options(ModerationCaseStatus::class)
|
||||
->required(),
|
||||
Select::make('assigned_to')
|
||||
->label(__('admin.moderation_cases.fields.assigned_to'))
|
||||
->relationship('assignedTo', 'name')
|
||||
->searchable()
|
||||
->preload(),
|
||||
TextInput::make('reports_count')
|
||||
->label(__('admin.moderation_cases.fields.reports_count'))
|
||||
->numeric()
|
||||
->disabled()
|
||||
->dehydrated(false),
|
||||
TextInput::make('threshold')
|
||||
->label(__('admin.moderation_cases.fields.threshold'))
|
||||
->numeric()
|
||||
->disabled()
|
||||
->dehydrated(false),
|
||||
]),
|
||||
Section::make(__('admin.moderation_cases.sections.target'))
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('caseable_type')
|
||||
->label(__('admin.moderation_cases.fields.target_type'))
|
||||
->disabled()
|
||||
->dehydrated(false),
|
||||
TextInput::make('caseable_id')
|
||||
->label(__('admin.moderation_cases.fields.caseable_id'))
|
||||
->disabled()
|
||||
->dehydrated(false),
|
||||
]),
|
||||
Section::make(__('admin.moderation_cases.sections.resolution'))
|
||||
->columns(2)
|
||||
->schema([
|
||||
DateTimePicker::make('resolved_at')
|
||||
->label(__('admin.moderation_cases.fields.resolved_at')),
|
||||
Select::make('resolved_by')
|
||||
->label(__('admin.moderation_cases.fields.resolved_by'))
|
||||
->relationship('resolvedBy', 'name')
|
||||
->searchable()
|
||||
->preload(),
|
||||
Textarea::make('resolution_note')
|
||||
->label(__('admin.moderation_cases.fields.resolution_note'))
|
||||
->rows(4)
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModerationCases\Schemas;
|
||||
|
||||
use App\Models\ModerationCase;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class ModerationCaseInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make(__('admin.moderation_cases.sections.case'))
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextEntry::make('id')
|
||||
->label(__('admin.moderation_cases.fields.id'))
|
||||
->copyable(),
|
||||
TextEntry::make('status')
|
||||
->label(__('admin.moderation_cases.fields.status'))
|
||||
->badge(),
|
||||
TextEntry::make('reports_count')
|
||||
->label(__('admin.moderation_cases.fields.reports_count')),
|
||||
TextEntry::make('threshold')
|
||||
->label(__('admin.moderation_cases.fields.threshold')),
|
||||
TextEntry::make('threshold_notified_at')
|
||||
->label(__('admin.moderation_cases.fields.threshold_notified_at'))
|
||||
->dateTime()
|
||||
->placeholder(__('admin.moderation_cases.placeholders.empty')),
|
||||
TextEntry::make('assignedTo.name')
|
||||
->label(__('admin.moderation_cases.fields.assigned_to'))
|
||||
->placeholder(__('admin.moderation_cases.placeholders.empty')),
|
||||
]),
|
||||
Section::make(__('admin.moderation_cases.sections.target'))
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextEntry::make('target_type')
|
||||
->label(__('admin.moderation_cases.fields.target_type'))
|
||||
->state(fn (ModerationCase $record): string => $record->targetLabel())
|
||||
->badge(),
|
||||
TextEntry::make('target_title')
|
||||
->label(__('admin.moderation_cases.fields.target_title'))
|
||||
->state(fn (ModerationCase $record): string => $record->targetTitle()),
|
||||
TextEntry::make('caseable_id')
|
||||
->label(__('admin.moderation_cases.fields.caseable_id'))
|
||||
->copyable(),
|
||||
]),
|
||||
Section::make(__('admin.moderation_cases.sections.resolution'))
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextEntry::make('resolvedBy.name')
|
||||
->label(__('admin.moderation_cases.fields.resolved_by'))
|
||||
->placeholder(__('admin.moderation_cases.placeholders.empty')),
|
||||
TextEntry::make('resolved_at')
|
||||
->label(__('admin.moderation_cases.fields.resolved_at'))
|
||||
->dateTime()
|
||||
->placeholder(__('admin.moderation_cases.placeholders.empty')),
|
||||
TextEntry::make('resolution_note')
|
||||
->label(__('admin.moderation_cases.fields.resolution_note'))
|
||||
->placeholder(__('admin.moderation_cases.placeholders.empty'))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Section::make(__('admin.moderation_cases.sections.metadata'))
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextEntry::make('created_at')
|
||||
->label(__('admin.moderation_cases.fields.created_at'))
|
||||
->dateTime(),
|
||||
TextEntry::make('updated_at')
|
||||
->label(__('admin.moderation_cases.fields.updated_at'))
|
||||
->dateTime(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModerationCases\Tables;
|
||||
|
||||
use App\Enums\ModerationCaseStatus;
|
||||
use App\Enums\ReportStatus;
|
||||
use App\Models\MealPosts;
|
||||
use App\Models\ModerationCase;
|
||||
use App\Models\User;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ModerationCasesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label(__('admin.moderation_cases.fields.id'))
|
||||
->copyable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('target_type')
|
||||
->label(__('admin.moderation_cases.fields.target_type'))
|
||||
->state(fn (ModerationCase $record): string => $record->targetLabel())
|
||||
->badge(),
|
||||
TextColumn::make('target_title')
|
||||
->label(__('admin.moderation_cases.fields.target_title'))
|
||||
->state(fn (ModerationCase $record): string => $record->targetTitle())
|
||||
->searchable(query: function (Builder $query, string $search): Builder {
|
||||
return $query->where(function (Builder $query) use ($search): void {
|
||||
$query
|
||||
->whereHasMorph('caseable', [MealPosts::class], fn (Builder $query): Builder => $query->where('title', 'like', "%{$search}%"))
|
||||
->orWhereHasMorph('caseable', [User::class], fn (Builder $query): Builder => $query->where('name', 'like', "%{$search}%"));
|
||||
});
|
||||
}),
|
||||
TextColumn::make('status')
|
||||
->label(__('admin.moderation_cases.fields.status'))
|
||||
->badge()
|
||||
->sortable(),
|
||||
TextColumn::make('reports_count')
|
||||
->label(__('admin.moderation_cases.fields.reports_count'))
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('threshold')
|
||||
->label(__('admin.moderation_cases.fields.threshold'))
|
||||
->numeric()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('threshold_notified_at')
|
||||
->label(__('admin.moderation_cases.fields.threshold_notified_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->placeholder(__('admin.moderation_cases.placeholders.empty')),
|
||||
TextColumn::make('assignedTo.name')
|
||||
->label(__('admin.moderation_cases.fields.assigned_to'))
|
||||
->placeholder(__('admin.moderation_cases.placeholders.empty'))
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('resolved_at')
|
||||
->label(__('admin.moderation_cases.fields.resolved_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->placeholder(__('admin.moderation_cases.placeholders.empty')),
|
||||
TextColumn::make('created_at')
|
||||
->label(__('admin.moderation_cases.fields.created_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->label(__('admin.moderation_cases.fields.status'))
|
||||
->options(ModerationCaseStatus::class),
|
||||
SelectFilter::make('caseable_type')
|
||||
->label(__('admin.moderation_cases.filters.target_type'))
|
||||
->options([
|
||||
MealPosts::class => __('admin.moderation_cases.targets.meal_post'),
|
||||
User::class => __('admin.moderation_cases.targets.user'),
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
Action::make('markReviewing')
|
||||
->label(__('admin.moderation_cases.actions.mark_reviewing'))
|
||||
->visible(fn (ModerationCase $record): bool => $record->status !== ModerationCaseStatus::REVIEWING)
|
||||
->action(fn (ModerationCase $record): bool => self::markReviewing($record)),
|
||||
Action::make('hideMeal')
|
||||
->label(__('admin.moderation_cases.actions.hide_meal'))
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->visible(fn (ModerationCase $record): bool => $record->caseable_type === MealPosts::class
|
||||
&& $record->caseable instanceof MealPosts
|
||||
&& ! $record->caseable->isHidden())
|
||||
->action(fn (ModerationCase $record): bool => self::hideMeal($record)),
|
||||
Action::make('suspendUser')
|
||||
->label(__('admin.moderation_cases.actions.suspend_user'))
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->visible(fn (ModerationCase $record): bool => $record->caseable_type === User::class
|
||||
&& $record->caseable instanceof User
|
||||
&& ! $record->caseable->isSuspended())
|
||||
->action(fn (ModerationCase $record): bool => self::suspendUser($record)),
|
||||
Action::make('resolve')
|
||||
->label(__('admin.moderation_cases.actions.resolve'))
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->visible(fn (ModerationCase $record): bool => $record->status !== ModerationCaseStatus::RESOLVED)
|
||||
->action(fn (ModerationCase $record): bool => self::closeCase($record, ModerationCaseStatus::RESOLVED, ReportStatus::RESOLVED)),
|
||||
Action::make('reject')
|
||||
->label(__('admin.moderation_cases.actions.reject'))
|
||||
->color('gray')
|
||||
->requiresConfirmation()
|
||||
->visible(fn (ModerationCase $record): bool => $record->status !== ModerationCaseStatus::REJECTED)
|
||||
->action(fn (ModerationCase $record): bool => self::closeCase($record, ModerationCaseStatus::REJECTED, ReportStatus::REJECTED)),
|
||||
])
|
||||
->toolbarActions([
|
||||
//
|
||||
]);
|
||||
}
|
||||
|
||||
private static function markReviewing(ModerationCase $record): bool
|
||||
{
|
||||
$record->forceFill([
|
||||
'status' => ModerationCaseStatus::REVIEWING,
|
||||
'assigned_to' => $record->assigned_to ?: auth()->id(),
|
||||
])->save();
|
||||
|
||||
$record->reports()
|
||||
->where('status', ReportStatus::OPEN->value)
|
||||
->update([
|
||||
'status' => ReportStatus::REVIEWING->value,
|
||||
'reviewed_by' => auth()->id(),
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function hideMeal(ModerationCase $record): bool
|
||||
{
|
||||
$mealPost = $record->caseable;
|
||||
|
||||
if (! $mealPost instanceof MealPosts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mealPost->forceFill([
|
||||
'hidden_at' => now(),
|
||||
'hidden_by' => auth()->id(),
|
||||
'hidden_reason' => __('admin.moderation_cases.actions.hide_meal'),
|
||||
])->save();
|
||||
|
||||
return self::markReviewing($record);
|
||||
}
|
||||
|
||||
private static function suspendUser(ModerationCase $record): bool
|
||||
{
|
||||
$user = $record->caseable;
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user->forceFill([
|
||||
'suspended_at' => now(),
|
||||
'suspended_by' => auth()->id(),
|
||||
'suspended_reason' => __('admin.moderation_cases.actions.suspend_user'),
|
||||
])->save();
|
||||
|
||||
return self::markReviewing($record);
|
||||
}
|
||||
|
||||
private static function closeCase(
|
||||
ModerationCase $record,
|
||||
ModerationCaseStatus $caseStatus,
|
||||
ReportStatus $reportStatus,
|
||||
): bool {
|
||||
$record->forceFill([
|
||||
'status' => $caseStatus,
|
||||
'assigned_to' => $record->assigned_to ?: auth()->id(),
|
||||
'resolved_by' => auth()->id(),
|
||||
'resolved_at' => now(),
|
||||
])->save();
|
||||
|
||||
$record->reports()->update([
|
||||
'status' => $reportStatus->value,
|
||||
'reviewed_by' => auth()->id(),
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user