From 54895702878589c249e5a1105e477ab21c3bf7fd Mon Sep 17 00:00:00 2001 From: deniskorbakov Date: Sun, 27 Oct 2024 06:15:08 +0300 Subject: [PATCH 1/6] feat: create logic for challenge list --- .../Challenge/Request/ChallengeFilterDTO.php | 16 +++++++++++ .../Controllers/Api/ChallengeController.php | 19 +++++++++++++ app/Services/Api/ChallengeService.php | 28 +++++++++++++++++++ routes/api.php | 5 ++++ 4 files changed, 68 insertions(+) create mode 100644 app/DTO/Api/Challenge/Request/ChallengeFilterDTO.php create mode 100644 app/Http/Controllers/Api/ChallengeController.php create mode 100644 app/Services/Api/ChallengeService.php diff --git a/app/DTO/Api/Challenge/Request/ChallengeFilterDTO.php b/app/DTO/Api/Challenge/Request/ChallengeFilterDTO.php new file mode 100644 index 0000000..6b15180 --- /dev/null +++ b/app/DTO/Api/Challenge/Request/ChallengeFilterDTO.php @@ -0,0 +1,16 @@ +challengeService->index($challengeFilterDTO); + } +} diff --git a/app/Services/Api/ChallengeService.php b/app/Services/Api/ChallengeService.php new file mode 100644 index 0000000..d464198 --- /dev/null +++ b/app/Services/Api/ChallengeService.php @@ -0,0 +1,28 @@ +when($challengeFilterDTO->challengeType, function (Builder $query) use ($challengeFilterDTO) { + $query->where('type', '=', $challengeFilterDTO->challengeType->value); + }) + ->when($challengeFilterDTO->startDate, function (Builder $query) use ($challengeFilterDTO) { + $query->where('start_date', '>=', $challengeFilterDTO->startDate); + }) + ->when($challengeFilterDTO->endDate, function (Builder $query) use ($challengeFilterDTO) { + $query->where('end_date', '<=', $challengeFilterDTO->endDate); + }) + ->orderBy('end_date', 'desc') + ->get(); + + return $games->toArray(); + } +} diff --git a/routes/api.php b/routes/api.php index ca05ae4..5ee019d 100755 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Http\Controllers\Api\AuthController; +use App\Http\Controllers\Api\ChallengeController; use App\Http\Controllers\Api\TeamController; use App\Http\Controllers\Api\TelegramController; use App\Http\Controllers\Api\UserController; @@ -27,6 +28,10 @@ Route::get('/{id}/challenges', [TeamController::class, 'challenge'])->name('users.challenge'); Route::get('/{id}/achievements', [TeamController::class, 'achievements'])->name('teams.achievements'); }); + + Route::group(['prefix' => 'challenges'], static function () { + Route::get('/', [ChallengeController::class, 'index'])->name('teams.index'); + }); }); Route::post('sync-telegram', [TelegramController::class, 'syncTelegram'])->name('auth.syncTelegram'); From c468026aaa00b35a2a8f9303e0114422dec78219 Mon Sep 17 00:00:00 2001 From: deniskorbakov Date: Sun, 27 Oct 2024 06:31:07 +0300 Subject: [PATCH 2/6] feat: create logic for show challenge --- .../Challenge/Response/ChallengeShowDTO.php | 27 +++++++++++++++++++ .../Controllers/Api/ChallengeController.php | 8 ++++++ app/Services/Api/ChallengeService.php | 6 +++++ routes/api.php | 1 + 4 files changed, 42 insertions(+) create mode 100644 app/DTO/Api/Challenge/Response/ChallengeShowDTO.php diff --git a/app/DTO/Api/Challenge/Response/ChallengeShowDTO.php b/app/DTO/Api/Challenge/Response/ChallengeShowDTO.php new file mode 100644 index 0000000..834ba4b --- /dev/null +++ b/app/DTO/Api/Challenge/Response/ChallengeShowDTO.php @@ -0,0 +1,27 @@ +users()->get(), + ); + } +} diff --git a/app/Http/Controllers/Api/ChallengeController.php b/app/Http/Controllers/Api/ChallengeController.php index ccc6ca8..1937e2d 100644 --- a/app/Http/Controllers/Api/ChallengeController.php +++ b/app/Http/Controllers/Api/ChallengeController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Api; use App\DTO\Api\Challenge\Request\ChallengeFilterDTO; +use App\Models\Challenge; use App\Services\Api\ChallengeService; class ChallengeController extends Controller @@ -16,4 +17,11 @@ public function index(ChallengeFilterDTO $challengeFilterDTO): array { return $this->challengeService->index($challengeFilterDTO); } + + public function show(int $id): array + { + $challenge = Challenge::query()->findOrFail($id); + + return $this->challengeService->show($challenge); + } } diff --git a/app/Services/Api/ChallengeService.php b/app/Services/Api/ChallengeService.php index d464198..b6f6f81 100644 --- a/app/Services/Api/ChallengeService.php +++ b/app/Services/Api/ChallengeService.php @@ -3,6 +3,7 @@ namespace App\Services\Api; use App\DTO\Api\Challenge\Request\ChallengeFilterDTO; +use App\DTO\Api\Challenge\Response\ChallengeShowDTO; use App\Models\Challenge; use Illuminate\Database\Eloquent\Builder; @@ -25,4 +26,9 @@ public function index(ChallengeFilterDTO $challengeFilterDTO): array return $games->toArray(); } + + public function show(Challenge $challenge): array + { + return ChallengeShowDTO::from($challenge)->toArray(); + } } diff --git a/routes/api.php b/routes/api.php index 5ee019d..5fb6e24 100755 --- a/routes/api.php +++ b/routes/api.php @@ -31,6 +31,7 @@ Route::group(['prefix' => 'challenges'], static function () { Route::get('/', [ChallengeController::class, 'index'])->name('teams.index'); + Route::get('/{id}', [ChallengeController::class, 'show'])->name('teams.show'); }); }); From fa9d98d82d8640fc318f29af2b5b8c1327ee0c88 Mon Sep 17 00:00:00 2001 From: deniskorbakov Date: Sun, 27 Oct 2024 06:42:07 +0300 Subject: [PATCH 3/6] fix: update logic --- app/DTO/Api/User/Response/UserShowDTO.php | 23 +++++++++++++++++++ .../Controllers/Api/ChallengeController.php | 10 ++++++++ app/Http/Controllers/Api/UserController.php | 4 +++- app/Services/Api/UserService.php | 6 +++++ routes/api.php | 2 ++ 5 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 app/DTO/Api/User/Response/UserShowDTO.php diff --git a/app/DTO/Api/User/Response/UserShowDTO.php b/app/DTO/Api/User/Response/UserShowDTO.php new file mode 100644 index 0000000..d8c95ab --- /dev/null +++ b/app/DTO/Api/User/Response/UserShowDTO.php @@ -0,0 +1,23 @@ +id === auth()->id() + ); + } +} diff --git a/app/Http/Controllers/Api/ChallengeController.php b/app/Http/Controllers/Api/ChallengeController.php index 1937e2d..307265c 100644 --- a/app/Http/Controllers/Api/ChallengeController.php +++ b/app/Http/Controllers/Api/ChallengeController.php @@ -24,4 +24,14 @@ public function show(int $id): array return $this->challengeService->show($challenge); } + + public function joinPersonal(): array + { + return []; + } + + public function joinTeam(): array + { + return []; + } } diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php index 50f907f..e8d68f1 100644 --- a/app/Http/Controllers/Api/UserController.php +++ b/app/Http/Controllers/Api/UserController.php @@ -19,7 +19,9 @@ public function __construct( public function show(int $id): array { - return User::query()->findOrFail($id)?->toArray(); + $user = User::query()->findOrFail($id); + + return $this->userService->show($user); } public function update(int $id, UserUpdateDTO $userUpdateDTO): array|JsonResponse diff --git a/app/Services/Api/UserService.php b/app/Services/Api/UserService.php index 940d217..7f51f77 100644 --- a/app/Services/Api/UserService.php +++ b/app/Services/Api/UserService.php @@ -8,6 +8,7 @@ use App\DTO\Api\User\Response\UserAchievementPersonalDTO; use App\DTO\Api\User\Response\UserAchievementTeamsDTO; use App\DTO\Api\User\Response\UserChallengeDTO; +use App\DTO\Api\User\Response\UserShowDTO; use App\DTO\Api\User\Response\UserTeamDTO; use App\Models\Team; use App\Models\User; @@ -16,6 +17,11 @@ class UserService { + public function show(User $user): array + { + return UserShowDTO::from($user)->toArray(); + } + public function achievement(Team $team, User $user): array { $achievementPersonal = $user->achievements()->get(); diff --git a/routes/api.php b/routes/api.php index 5fb6e24..02dbd73 100755 --- a/routes/api.php +++ b/routes/api.php @@ -32,6 +32,8 @@ Route::group(['prefix' => 'challenges'], static function () { Route::get('/', [ChallengeController::class, 'index'])->name('teams.index'); Route::get('/{id}', [ChallengeController::class, 'show'])->name('teams.show'); + Route::post('/{id}/joins/personal', [ChallengeController::class, 'joinPersonal'])->name('teams.joinPersonal'); + Route::post('/{id}/joins/teams', [ChallengeController::class, 'joinTeam'])->name('teams.joinTeam'); }); }); From c9b90a6a5fc6989ff76107881c443519db462702 Mon Sep 17 00:00:00 2001 From: Roman Korchnev Date: Sun, 27 Oct 2024 06:51:02 +0300 Subject: [PATCH 4/6] send message to telegram --- .env.example | 2 ++ app/Filament/Resources/ChallengeResource.php | 11 ++++-- .../Pages/CreateChallenge.php | 1 - .../ChallengeResource/Pages/EditChallenge.php | 14 ++++++++ .../Controllers/Api/TelegramController.php | 30 ++++++++++++++++ composer.json | 3 +- config/cors.php | 34 +++++++++++++++++++ 7 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 config/cors.php diff --git a/.env.example b/.env.example index 54addec..13b4aec 100644 --- a/.env.example +++ b/.env.example @@ -66,3 +66,5 @@ VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" TELEGRAM_BOT_TOKEN= TELEGRAM_SECRET_KEY= + +FRONTEND_URL=https://localhost:3000 diff --git a/app/Filament/Resources/ChallengeResource.php b/app/Filament/Resources/ChallengeResource.php index be2e12a..e49926c 100644 --- a/app/Filament/Resources/ChallengeResource.php +++ b/app/Filament/Resources/ChallengeResource.php @@ -50,6 +50,9 @@ public static function form(Form $form): Form ]) ->disabled( function ($record) { + if ($record === null) { + return false; + } return $record->teams->count() > 0 || $record->users->count() > 0; } ) @@ -57,14 +60,16 @@ function ($record) { ->maxWidth('sm'), Forms\Components\Textarea::make('description') ->label('Описание') + ->required() ->rows(5), Forms\Components\Section::make()->schema([ DateTimePicker::make('start_date') ->label('Дата начала') + ->required() ->default(now()), DateTimePicker::make('end_date') ->label('Дата окончания') - ->nullable(), + ->required(), ])->columns(), Forms\Components\Textarea::make('result') ->label('Результаты завершения челленджа') @@ -84,7 +89,7 @@ function ($record) { ->visibility('public') ->maxWidth('xs') ->label('Изображение'), - Forms\Components\Section::make() + $record !== null ? Forms\Components\Section::make() ->schema([ $record->type === ChallengeType::PERSONAL->value ? Forms\Components\Select::make( 'users') ->label('Участники челленджа') @@ -96,7 +101,7 @@ function ($record) { ->relationship('teams', 'name') ->preload() ->multiple(), - ])->columns(2), + ])->columns(2) : new Forms\Components\Section(), ])->columns(1); } diff --git a/app/Filament/Resources/ChallengeResource/Pages/CreateChallenge.php b/app/Filament/Resources/ChallengeResource/Pages/CreateChallenge.php index 661f11b..63e2cf9 100644 --- a/app/Filament/Resources/ChallengeResource/Pages/CreateChallenge.php +++ b/app/Filament/Resources/ChallengeResource/Pages/CreateChallenge.php @@ -3,7 +3,6 @@ namespace App\Filament\Resources\ChallengeResource\Pages; use App\Filament\Resources\ChallengeResource; -use Filament\Actions; use Filament\Resources\Pages\CreateRecord; class CreateChallenge extends CreateRecord diff --git a/app/Filament/Resources/ChallengeResource/Pages/EditChallenge.php b/app/Filament/Resources/ChallengeResource/Pages/EditChallenge.php index b34f6d4..dcb5410 100644 --- a/app/Filament/Resources/ChallengeResource/Pages/EditChallenge.php +++ b/app/Filament/Resources/ChallengeResource/Pages/EditChallenge.php @@ -3,6 +3,7 @@ namespace App\Filament\Resources\ChallengeResource\Pages; use App\Filament\Resources\ChallengeResource; +use App\Http\Controllers\Api\TelegramController; use Filament\Actions; use Filament\Resources\Pages\EditRecord; @@ -19,6 +20,19 @@ protected function getHeaderActions(): array { return [ Actions\DeleteAction::make(), + Actions\Action::make('Отправить уведомления') + ->action('sendNotification') + ->color('success'), ]; } + + public function sendNotification(TelegramController $controller): void + { + $record = $this->form->getRecord(); + + $url = env('FRONTEND_URL') . "/challenges/{$record->id}"; + $message = "Создан новый челлендж: {$record->name}! \nСкорее присоединяйся по ссылке {$url}"; + + $controller->sendMessageForAll($message); + } } diff --git a/app/Http/Controllers/Api/TelegramController.php b/app/Http/Controllers/Api/TelegramController.php index fb8c0a2..0d9763d 100644 --- a/app/Http/Controllers/Api/TelegramController.php +++ b/app/Http/Controllers/Api/TelegramController.php @@ -32,4 +32,34 @@ public function syncTelegram(Request $request): JsonResponse return response()->json(['message' => 'error'], 404); } } + + public function sendMessageForAll(string $message) + { + $users = User::query() + ->whereNotNull('telegram_id') + ->get(); + + foreach ($users as $user) { + $this->sendMessage($user->telegram_id, $message); + } + } + + public function sendMessage(string $telegramId, string $message) + { + $ch = curl_init(); + curl_setopt_array( + $ch, + array( + CURLOPT_URL => 'https://api.telegram.org/bot' . env('TELEGRAM_BOT_TOKEN') . '/sendMessage', + CURLOPT_POST => TRUE, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_TIMEOUT => 10, + CURLOPT_POSTFIELDS => array( + 'chat_id' => $telegramId, + 'text' => $message, + ), + ) + ); + curl_exec($ch); + } } diff --git a/composer.json b/composer.json index eb40ddf..a6b20d8 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "laravel/framework": "^11.9", "laravel/sanctum": "^4.0", "laravel/tinker": "^2.9", - "spatie/laravel-data": "^4.11" + "spatie/laravel-data": "^4.11", + "ext-curl": "*" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..8a39e6d --- /dev/null +++ b/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, + +]; From 7a117017252761da76aaa7f4e1f1f8a81b210739 Mon Sep 17 00:00:00 2001 From: Roman Korchnev Date: Sun, 27 Oct 2024 07:31:44 +0300 Subject: [PATCH 5/6] added admin statistics --- app/Filament/Pages/Dashboard.php | 14 +++++++ app/Filament/Widgets/StatsOverview.php | 51 ++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 app/Filament/Pages/Dashboard.php create mode 100644 app/Filament/Widgets/StatsOverview.php diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Pages/Dashboard.php new file mode 100644 index 0000000..9102ff4 --- /dev/null +++ b/app/Filament/Pages/Dashboard.php @@ -0,0 +1,14 @@ +pages([]); + } +} diff --git a/app/Filament/Widgets/StatsOverview.php b/app/Filament/Widgets/StatsOverview.php new file mode 100644 index 0000000..abc830a --- /dev/null +++ b/app/Filament/Widgets/StatsOverview.php @@ -0,0 +1,51 @@ +where('created_at', '>=', Carbon::now()->startOfMonth())->count()) + ->chart(self::getCountUsersRegisteredForLastMonth()) + ->color('success'), + Stat::make( + 'Активных пользователей', + User::query()->where('is_confirmed', true)->count() + ), + Stat::make( + 'Всего пользователей', + User::query()->count() + ), + ]; + } + + private function getCountUsersRegisteredForLastMonth(): array + { + $results = []; + $startTime = Carbon::now()->startOfMonth(); + $daysLeft = $startTime->diffInDays(Carbon::now()); + + for ($i = 0; $i < $daysLeft; $i++) { + $endTime = $startTime->copy()->addDay(); + $count = DB::table('users') + ->whereBetween('created_at', [ + $startTime, + $endTime, + ]) + ->count(); + $results[] = $count; + $startTime = $endTime; + } + + return $results; + } +} From 120ccff83cea5ee90622bfbad210f46f35817e73 Mon Sep 17 00:00:00 2001 From: deniskorbakov Date: Sun, 27 Oct 2024 07:50:18 +0300 Subject: [PATCH 6/6] feat: add endpoint for team - is captain --- .../User/Response/UserTeamIsCaptainDTO.php | 30 +++++++++++++++++++ app/Http/Controllers/Api/UserController.php | 7 +++++ app/Services/Api/UserService.php | 7 +++++ routes/api.php | 1 + 4 files changed, 45 insertions(+) create mode 100644 app/DTO/Api/User/Response/UserTeamIsCaptainDTO.php diff --git a/app/DTO/Api/User/Response/UserTeamIsCaptainDTO.php b/app/DTO/Api/User/Response/UserTeamIsCaptainDTO.php new file mode 100644 index 0000000..c8c88a1 --- /dev/null +++ b/app/DTO/Api/User/Response/UserTeamIsCaptainDTO.php @@ -0,0 +1,30 @@ +id, + $team->image, + $team->name, + $team->users->count(), + ); + } +} diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php index e8d68f1..512609f 100644 --- a/app/Http/Controllers/Api/UserController.php +++ b/app/Http/Controllers/Api/UserController.php @@ -17,6 +17,13 @@ public function __construct( ) { } + public function teamIsCaptain(int $id): array + { + $teams = User::query()->findOrFail($id); + + return $this->userService->teamIsCaptain($teams); + } + public function show(int $id): array { $user = User::query()->findOrFail($id); diff --git a/app/Services/Api/UserService.php b/app/Services/Api/UserService.php index 7f51f77..ac54bc7 100644 --- a/app/Services/Api/UserService.php +++ b/app/Services/Api/UserService.php @@ -64,6 +64,13 @@ public function team(User $user): array return UserTeamDTO::collect($team)->toArray(); } + public function teamIsCaptain(User $user): array + { + $team = $user->teams()->where('captain_id', $user->id)->get(); + + return UserTeamDTO::collect($team)->toArray(); + } + public function challenge(User $user): array { $challenges = $user->challenges()->orderBy('end_date', 'desc')->get(); diff --git a/routes/api.php b/routes/api.php index 02dbd73..9733120 100755 --- a/routes/api.php +++ b/routes/api.php @@ -17,6 +17,7 @@ Route::get('/{id}', [UserController::class, 'show'])->name('users.show'); Route::post('/{id}', [UserController::class, 'update'])->name('users.update'); Route::get('/{id}/teams', [UserController::class, 'team'])->name('users.team'); + Route::get('/{id}/teams/is-captain', [UserController::class, 'teamIsCaptain'])->name('users.teamIsCaptain'); Route::get('/{id}/challenges', [UserController::class, 'challenge'])->name('users.challenge'); Route::get('/{id}/achievements', [UserController::class, 'achievement'])->name('users.achievement'); });