diff --git a/app/Actions/Article/DeclineArticleAction.php b/app/Actions/Article/DeclineArticleAction.php new file mode 100644 index 00000000..12d75316 --- /dev/null +++ b/app/Actions/Article/DeclineArticleAction.php @@ -0,0 +1,31 @@ +update([ + 'declined_at' => Carbon::now(), + 'reason' => $reason, + 'submitted_at' => null, + ]); + + $article->user->notify(new ArticleDeclinedNotification($article)); + + $article->refresh(); + + return $article; + }); + } +} diff --git a/app/Actions/Article/UpdateArticleAction.php b/app/Actions/Article/UpdateArticleAction.php index ddb508de..e645bf42 100644 --- a/app/Actions/Article/UpdateArticleAction.php +++ b/app/Actions/Article/UpdateArticleAction.php @@ -31,6 +31,10 @@ public function execute(ArticleData $articleData, Article $article): Article ); } + if ($articleData->declined_at) { + $articleData->declined_at = null; + } + $article->update($articleData->toArray()); $article->refresh(); diff --git a/app/Data/ArticleData.php b/app/Data/ArticleData.php index fca6e491..5ea7bfcb 100644 --- a/app/Data/ArticleData.php +++ b/app/Data/ArticleData.php @@ -17,5 +17,6 @@ public function __construct( public ?string $canonical_url = null, public ?Carbon $published_at = null, public ?Carbon $submitted_at = null, + public ?Carbon $declined_at = null, ) {} } diff --git a/app/Filament/Resources/ArticleResource.php b/app/Filament/Resources/ArticleResource.php index a462240a..c58511d2 100644 --- a/app/Filament/Resources/ArticleResource.php +++ b/app/Filament/Resources/ArticleResource.php @@ -5,10 +5,13 @@ namespace App\Filament\Resources; use App\Actions\Article\ApprovedArticleAction; +use App\Actions\Article\DeclineArticleAction; use App\Filament\Resources\ArticleResource\Pages; use App\Models\Article; use Awcodes\FilamentBadgeableColumn\Components\Badge; use Awcodes\FilamentBadgeableColumn\Components\BadgeableColumn; +use Filament\Forms\Components\Textarea; +use Filament\Notifications\Notification; use Filament\Resources\Resource; use Filament\Support\Enums\MaxWidth; use Filament\Tables; @@ -123,15 +126,27 @@ public static function table(Table $table): Table ->label('Décliner') ->icon('heroicon-s-x-mark') ->color('warning') - ->modalHeading(__('Voulez vous décliner cet article')) - ->successNotificationTitle(__('Opération effectuée avec succès')) + ->form([ + Textarea::make('reason') + ->label(__('Raison du refus')) + ->maxLength(255) + ->required(), + ]) + ->modalHeading('Décliner l\'article') + ->modalDescription('Veuillez fournir une raison détaillée pour le refus de cet article. L\'auteur recevra cette explication.') + ->successNotificationTitle('Article décliné avec succès') ->requiresConfirmation() ->modalIcon('heroicon-s-x-mark') - ->action(function ($record): void { + ->action(function (array $data, Article $record): void { Gate::authorize('decline', $record); - $record->declined_at = now(); - $record->save(); + app(DeclineArticleAction::class)->execute($data['reason'], $record); + + Notification::make() + ->title('Article décliné') + ->body('L\'auteur a été notifié de la raison du refus.') + ->success() + ->send(); }), Tables\Actions\Action::make('show') ->icon('untitledui-eye') diff --git a/app/Filament/Resources/ArticleResource/Pages/ListArticles.php b/app/Filament/Resources/ArticleResource/Pages/ListArticles.php index a793cae9..f8be7594 100644 --- a/app/Filament/Resources/ArticleResource/Pages/ListArticles.php +++ b/app/Filament/Resources/ArticleResource/Pages/ListArticles.php @@ -7,6 +7,7 @@ use App\Filament\Resources\ArticleResource; use App\Models\Article; use Closure; +use Filament\Resources\Components\Tab; use Filament\Resources\Pages\ListRecords; final class ListArticles extends ListRecords @@ -17,4 +18,13 @@ public function isTableRecordSelectable(): Closure { return fn (Article $record): bool => $record->isNotPublished(); } + + public function getTabs(): array + { + return [ + 'En attente' => Tab::make()->query(fn ($query) => $query->awaitingApproval()), + 'Apprové' => Tab::make()->query(fn ($query) => $query->published()), + 'Décliné' => Tab::make()->query(fn ($query) => $query->declined()), + ]; + } } diff --git a/app/Models/Article.php b/app/Models/Article.php index 32c697b6..8616bc63 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -32,6 +32,7 @@ * @property bool $is_pinned * @property int $is_sponsored * @property string | null $canonical_url + * @property string | null $reason * @property int | null $tweet_id * @property int $user_id * @property string | null $locale @@ -63,6 +64,7 @@ final class Article extends Model implements HasMedia, ReactableInterface, Sitem 'body', 'slug', 'canonical_url', + 'reason', 'show_toc', 'is_pinned', 'user_id', diff --git a/app/Notifications/ArticleDeclinedNotification.php b/app/Notifications/ArticleDeclinedNotification.php new file mode 100644 index 00000000..d6148720 --- /dev/null +++ b/app/Notifications/ArticleDeclinedNotification.php @@ -0,0 +1,41 @@ +subject(__('emails/article.article_declined.subject')) + ->markdown('emails.article_declined', ['article' => $this->article]); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'article' => $this->article, + 'owner' => $this->article->user->name, + 'email' => $this->article->user->email, + ]; + } +} diff --git a/database/migrations/2025_02_20_112613_add_reason_field_to_articles_table.php b/database/migrations/2025_02_20_112613_add_reason_field_to_articles_table.php new file mode 100644 index 00000000..16b8cd48 --- /dev/null +++ b/database/migrations/2025_02_20_112613_add_reason_field_to_articles_table.php @@ -0,0 +1,17 @@ +string('reason')->nullable()->after('canonical_url'); + }); + } +}; diff --git a/lang/en/emails/article.php b/lang/en/emails/article.php new file mode 100644 index 00000000..6c406acc --- /dev/null +++ b/lang/en/emails/article.php @@ -0,0 +1,20 @@ + [ + 'subject' => 'Your article has been declined', + 'head' => 'Your article :title has been declined for the following reason:', + 'recommandation_body' => 'Don\'t be discouraged! You can:', + 'recommandation_1' => '1. Review your article and the reason for the decline', + 'recommandation_2' => '2. Make the necessary changes', + 'recommandation_3' => '3. Resubmit your article once the corrections are made', + 'help' => 'Our team is here to help you improve your content.', + 'closing' => 'Best regards,', + 'team' => 'The Laravel Cameroon Team', + 'button_update_article' => 'Edit my article', + ], + +]; diff --git a/lang/fr/emails/article.php b/lang/fr/emails/article.php new file mode 100644 index 00000000..1c612c56 --- /dev/null +++ b/lang/fr/emails/article.php @@ -0,0 +1,20 @@ + [ + 'subject' => 'Votre article a été décliné', + 'head' => 'Votre article :title a été décliné pour la raison suivante :', + 'recommandation_body' => 'Ne vous découragez pas ! Vous pouvez :', + 'recommandation_1' => '1. Consulter votre article et la raison du refus', + 'recommandation_2' => '2. Apporter les modifications nécessaires', + 'recommandation_3' => '3. Re-soumettre votre article une fois les corrections effectuées', + 'help' => 'Notre équipe est là pour vous aider à améliorer votre contenu.', + 'closing' => 'Cordialement,', + 'team' => 'L\'équipe Laravel Cameroun', + 'button_update_article' => 'Modifier mon article', + ], + +]; diff --git a/resources/views/emails/article_declined.blade.php b/resources/views/emails/article_declined.blade.php new file mode 100644 index 00000000..e62d4ea3 --- /dev/null +++ b/resources/views/emails/article_declined.blade.php @@ -0,0 +1,28 @@ + + + + +{{ __('emails/article.article_declined.head', ['title' => $article->title]) }} + +{{ $article->reason }} + +{{ __('emails/article.article_declined.recommandation_body') }} + +{{ __('emails/article.article_declined.recommandation_1') }} +{{ __('emails/article.article_declined.recommandation_2') }} +{{ __('emails/article.article_declined.recommandation_3') }} + + +{{ __('emails/article.article_declined.button_update_article') }} + + +{{ __('emails/article.article_declined.help') }} + + + +

+ {{ __('emails/article.article_declined.closing') }}
+ {{ __('emails/article.article_declined.team') }} +

+ +
diff --git a/tests/Feature/Filament/ArticleResourceTest.php b/tests/Feature/Filament/ArticleResourceTest.php index 2d02d951..326af861 100644 --- a/tests/Feature/Filament/ArticleResourceTest.php +++ b/tests/Feature/Filament/ArticleResourceTest.php @@ -35,11 +35,8 @@ $article->refresh(); - expect($article->approved_at) - ->not() - ->toBe(null) - ->and($article->declined_at) - ->toBe(null); + expect($article->approved_at)->toBeInstanceOf(\Carbon\Carbon::class) + ->and($article->declined_at)->toBeNull(); Livewire::test(ArticleResource\Pages\ListArticles::class) ->assertTableActionHidden('approved', $article) @@ -50,15 +47,14 @@ $article = $this->articles->first(); Livewire::test(ArticleResource\Pages\ListArticles::class) - ->callTableAction('declined', $article); + ->callTableAction('declined', $article, data: [ + 'reason' => 'Ce contenu ne respecte pas nos règles éditoriales.', + ]); $article->refresh(); - expect($article->declined_at) - ->not - ->toBe(null) - ->and($article->approved_at) - ->toBe(null); + expect($article->declined_at)->toBeInstanceOf(\Carbon\Carbon::class) + ->and($article->approved_at)->toBeNull(); Livewire::test(ArticleResource\Pages\ListArticles::class) ->assertTableActionHidden('approved', $article)