From 84e9bf73072a2d378af36cdb5bb7d7ce4031aada Mon Sep 17 00:00:00 2001 From: Xavier Oliveira Date: Tue, 16 Jun 2026 12:25:58 +0100 Subject: [PATCH] Feat: Organize videos by sector --- .../app/Http/Controllers/AuthController.php | 31 ++- .../Http/Controllers/DashboardController.php | 45 +++- .../app/Http/Controllers/SectorController.php | 31 +++ .../app/Http/Controllers/UserController.php | 43 ++- .../app/Http/Controllers/VideosController.php | 244 ++++++++++++------ .../app/Http/Middleware/JwtMiddleware.php | 6 +- .../app/Http/Requests/CreateSectorRequest.php | 41 +++ .../app/Http/Requests/CreateUserRequest.php | 3 + .../app/Http/Requests/CreateVideoRequest.php | 1 + .../app/Http/Requests/UpdateUserRequest.php | 2 + .../app/Http/Requests/UpdateVideoRequest.php | 2 + plataforma-tutorias/app/Models/Sector.php | 29 +++ plataforma-tutorias/app/Models/User.php | 7 + plataforma-tutorias/app/Models/Video.php | 5 + plataforma-tutorias/config/jwt.php | 4 +- ...2026_06_03_162741_create_sector_table.php} | 11 +- ...06_03_172841_add_sector_to_users_table.php | 28 ++ ...06_03_180000_create_video_sector_table.php | 29 +++ .../database/seeders/DatabaseSeeder.php | 1 + .../database/seeders/SectorsTableSeeder.php | 24 ++ plataforma-tutorias/routes/api.php | 6 +- 21 files changed, 477 insertions(+), 116 deletions(-) create mode 100644 plataforma-tutorias/app/Http/Controllers/SectorController.php create mode 100644 plataforma-tutorias/app/Http/Requests/CreateSectorRequest.php create mode 100644 plataforma-tutorias/app/Models/Sector.php rename plataforma-tutorias/database/migrations/{2026_06_02_113520_create_refresh_tokens.php => 2026_06_03_162741_create_sector_table.php} (56%) create mode 100644 plataforma-tutorias/database/migrations/2026_06_03_172841_add_sector_to_users_table.php create mode 100644 plataforma-tutorias/database/migrations/2026_06_03_180000_create_video_sector_table.php create mode 100644 plataforma-tutorias/database/seeders/SectorsTableSeeder.php diff --git a/plataforma-tutorias/app/Http/Controllers/AuthController.php b/plataforma-tutorias/app/Http/Controllers/AuthController.php index a651604..304078f 100644 --- a/plataforma-tutorias/app/Http/Controllers/AuthController.php +++ b/plataforma-tutorias/app/Http/Controllers/AuthController.php @@ -105,16 +105,33 @@ class AuthController extends Controller $user = auth()->user(); $userId = $user->id; $role = $user->role_id; + $userSector = $user->sector_id; if ($role !== 1) { + $stats = Video::query() + ->where('is_active', true) + ->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + }) + ->selectRaw(' + COUNT(*) as active_count, + COUNT(CASE WHEN EXISTS ( + SELECT 1 FROM video_views + WHERE video_views.video_id = videos.id + AND video_views.user_id = ? + ) THEN 1 END) as watched_count + ', [$userId]) + ->first(); + } else { $stats = Video::selectRaw(' - COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_count, - COUNT(CASE WHEN is_active = 1 AND EXISTS ( - SELECT 1 FROM video_views - WHERE video_views.video_id = videos.id - AND video_views.user_id = ? - ) THEN 1 END) as watched_count - ', [$userId]) + COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_count, + COUNT(CASE WHEN is_active = 1 AND EXISTS ( + SELECT 1 FROM video_views + WHERE video_views.video_id = videos.id + AND video_views.user_id = ? + ) THEN 1 END) as watched_count + ', [$userId]) ->first(); } diff --git a/plataforma-tutorias/app/Http/Controllers/DashboardController.php b/plataforma-tutorias/app/Http/Controllers/DashboardController.php index 1e60dbf..46ea451 100644 --- a/plataforma-tutorias/app/Http/Controllers/DashboardController.php +++ b/plataforma-tutorias/app/Http/Controllers/DashboardController.php @@ -22,12 +22,17 @@ class DashboardController extends Controller $userId = $user->id; $role = $user->role_id; + $userSector = $user->sector_id; $videos = Video::select('id', 'title', 'thumbnail', 'is_active', 'order') ->where('is_active', true) ->whereDoesntHave('views', function ($q) use ($user) { $q->where('user_id', $user->id); }) + ->when($role !== 1, fn($q) => $q->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + })) ->orderBy('order', 'asc') ->limit(3) ->get() @@ -41,19 +46,33 @@ class DashboardController extends Controller ]; }); - if ($role === 1) { - $stats = Video::selectRaw('COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_count')->first(); - } else { - $stats = Video::selectRaw(' - COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_count, - COUNT(CASE WHEN is_active = 1 AND EXISTS ( - SELECT 1 FROM video_views - WHERE video_views.video_id = videos.id - AND video_views.user_id = ? - ) THEN 1 END) as watched_count - ', [$userId]) - ->first(); - } + if ($role !== 1) { + $stats = Video::query() + ->where('is_active', true) + ->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + }) + ->selectRaw(' + COUNT(*) as active_count, + COUNT(CASE WHEN EXISTS ( + SELECT 1 FROM video_views + WHERE video_views.video_id = videos.id + AND video_views.user_id = ? + ) THEN 1 END) as watched_count + ', [$userId]) + ->first(); + } else { + $stats = Video::selectRaw(' + COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_count, + COUNT(CASE WHEN is_active = 1 AND EXISTS ( + SELECT 1 FROM video_views + WHERE video_views.video_id = videos.id + AND video_views.user_id = ? + ) THEN 1 END) as watched_count + ', [$userId]) + ->first(); + } $workshops = Workshop::select(['id', 'title', 'image', 'date', 'time_start', 'time_end', 'status']) ->with(['users:id']) diff --git a/plataforma-tutorias/app/Http/Controllers/SectorController.php b/plataforma-tutorias/app/Http/Controllers/SectorController.php new file mode 100644 index 0000000..94c6de1 --- /dev/null +++ b/plataforma-tutorias/app/Http/Controllers/SectorController.php @@ -0,0 +1,31 @@ +get(); + + return response()->json([ + 'message' => 'Setores obtidos com sucesso', + 'data' => $sectors, + 'errors' => null, + ], 200); + } + + public function create(CreateSectorRequest $request) + { + $sector = Sector::create($request->validated()); + return response()->json([ + 'message' => 'Setor criado com sucesso', + 'data' => $sector, + 'errors' => null, + ], 201); + } +} diff --git a/plataforma-tutorias/app/Http/Controllers/UserController.php b/plataforma-tutorias/app/Http/Controllers/UserController.php index 521ce4a..dbaf289 100644 --- a/plataforma-tutorias/app/Http/Controllers/UserController.php +++ b/plataforma-tutorias/app/Http/Controllers/UserController.php @@ -74,12 +74,27 @@ class UserController extends Controller ], 404); } + $sectorName = $user->sector?->name; + $userSector = $user->sector_id; + $role = $user->role_id; + $videosWatched = Video::select('id') ->whereHas('views', function ($query) use ($user) { $query->where('user_id', $user->id); - })->count(); + }) + ->when($role !== 1, fn($q) => $q->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + })) + ->count(); - $videosCount = Video::select('id')->where('is_active', true)->count(); + $videosCount = Video::select('id') + ->when($role !== 1, fn($q) => $q->where('is_active', true)) + ->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + }) + ->count(); $workshopsCount = Workshop::select('id')->where('status', 'pending')->count(); @@ -98,6 +113,7 @@ class UserController extends Controller return response()->json([ 'message' => 'Utilizador obtido com sucesso', 'data' => $user, + 'sectorName' => $sectorName, 'errors' => null, 'videosWatched' => $videosWatched, 'videosCount' => $videosCount, @@ -122,6 +138,7 @@ class UserController extends Controller $role = $user->role_id; $userId = $user->id; + $userSector = $user->sector_id; if ($role === 1) { $nextWorkshops = Workshop::select('id', 'title', 'image', 'date', 'time_start', 'time_end', 'status')->where('status', 'pending')->orderBy('date', 'asc')->orderBy('time_start', 'asc')->limit(3)->get(); @@ -144,6 +161,10 @@ class UserController extends Controller ->whereDoesntHave('views', function ($q) use ($user) { $q->where('user_id', $user->id); }) + ->when($role !== 1, fn($q) => $q->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + })) ->orderBy('order', 'asc') ->limit(3) ->get() @@ -157,20 +178,26 @@ class UserController extends Controller ]; }); - $videosCount = Video::select('id')->where('is_active', true)->count(); + $videosCount = Video::select('id')->where('is_active', true)->when($role !== 1, fn($q) => $q->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + }))->count(); $videosWatched = Video::select('id') ->where('is_active', true) ->whereHas('views', function ($query) use ($user) { $query->where('user_id', $user->id); - })->count(); + })->when($role !== 1, fn($q) => $q->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + }))->count(); $workshopsInscribed = Workshop::select('id', 'title', 'image', 'date', 'time_start', 'time_end', 'status')->with('users:id')->where('status', 'pending')->whereHas('users', function ($query) use ($user) { $query->where('users.id', $user->id); }) - ->orderBy('date', 'asc') - ->orderBy('time_start', 'asc') - ->get(); + ->orderBy('date', 'asc') + ->orderBy('time_start', 'asc') + ->get(); $workshopsCount = Workshop::select('id')->where('status', 'pending')->count(); } @@ -200,6 +227,7 @@ class UserController extends Controller 'email' => $validated['email'], 'password' => Hash::make($validated['password']), 'role_id' => $validated['role_id'], + 'sector_id' => $validated['sector_id'], ]); return response()->json([ @@ -253,6 +281,7 @@ class UserController extends Controller 'name' => $request->name ?: $userToUpdate->name, 'email' => $request->email ?: $userToUpdate->email, 'role_id' => $request->role_id ?: $userToUpdate->role_id, + 'sector_id' => $request->sector_id ?: $userToUpdate->sector_id, ]; if ($request->filled('novaPassword')) { diff --git a/plataforma-tutorias/app/Http/Controllers/VideosController.php b/plataforma-tutorias/app/Http/Controllers/VideosController.php index cec0487..a6ae3be 100644 --- a/plataforma-tutorias/app/Http/Controllers/VideosController.php +++ b/plataforma-tutorias/app/Http/Controllers/VideosController.php @@ -36,6 +36,7 @@ class VideosController extends Controller $userId = $user->id; $role = $user->role_id; + $userSector = $user->sector_id; $search = trim((string) $request->query('search', '')); $categoryId = $request->query('category'); @@ -45,9 +46,7 @@ class VideosController extends Controller $query = Video::select(['id', 'title', 'thumbnail', 'is_active']) ->with('categories:id,name') - ->withCount([ - 'views as watched' => fn($q) => $q->where('user_id', $userId) - ]) + ->with('sectors:id') ->when($search !== '', function ($q) use ($search) { $q->where(function ($sub) use ($search) { @@ -64,6 +63,11 @@ class VideosController extends Controller ->when($role !== 1, fn($q) => $q->where('is_active', true)) + ->when($role !== 1, fn($q) => $q->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + })) + ->when( $role === 1 && $status === 'active', fn($q) => @@ -85,25 +89,43 @@ class VideosController extends Controller // left join para trazer os vídeos vistos pelo utilizador ->leftJoin('video_views as vv', function ($join) use ($userId) { $join->on('vv.video_id', '=', 'videos.id') - ->where('vv.user_id', '=', $userId); + ->where('vv.user_id', '=', $userId); }) ->orderByRaw('CASE WHEN vv.id IS NOT NULL THEN 1 ELSE 0 END ASC') ->orderBy('videos.order', 'ASC') - ->select(['videos.id', 'videos.title', 'videos.thumbnail', 'videos.is_active']) + ->select(['videos.id', 'videos.title', 'videos.thumbnail', 'videos.is_active', 'vv.id as user_view_id']) ->paginate($perPage); // 1 única query para obter o número de vídeos ativos e vistos - $stats = Video::selectRaw(' - COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_count, - COUNT(CASE WHEN is_active = 1 AND EXISTS ( - SELECT 1 FROM video_views - WHERE video_views.video_id = videos.id - AND video_views.user_id = ? - ) THEN 1 END) as watched_count - ', [$userId]) - ->first(); + if ($role !== 1) { + $stats = Video::query() + ->where('is_active', true) + ->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + }) + ->selectRaw(' + COUNT(*) as active_count, + COUNT(CASE WHEN EXISTS ( + SELECT 1 FROM video_views + WHERE video_views.video_id = videos.id + AND video_views.user_id = ? + ) THEN 1 END) as watched_count + ', [$userId]) + ->first(); + } else { + $stats = Video::selectRaw(' + COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_count, + COUNT(CASE WHEN is_active = 1 AND EXISTS ( + SELECT 1 FROM video_views + WHERE video_views.video_id = videos.id + AND video_views.user_id = ? + ) THEN 1 END) as watched_count + ', [$userId]) + ->first(); + } $query->getCollection()->transform(function ($video) { return [ @@ -112,21 +134,38 @@ class VideosController extends Controller 'thumbnail' => $video->thumbnail, 'is_active' => $video->is_active, 'categories' => $video->categories, - 'watched' => $video->views->isNotEmpty(), + 'watched' => $video->user_view_id !== null, ]; }); // Guarda as categorias em cache durante 1 minuto para evitar consultas repetidas - $categories = cache()->remember( - 'active_categories_with_videos', - 60, - fn() => - Category::select('id', 'name') - ->where('is_active', true) - ->whereHas('videos') - ->orderBy('name') - ->get() - ); + if ($role !== 1) { + $categories = cache()->remember( + 'active_categories_with_videos', + 60, + fn() => + Category::select('id', 'name') + ->where('is_active', true) + ->whereHas('videos') + ->whereHas('videos.sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + }) + ->orderBy('name') + ->get() + ); + } else { + $categories = cache()->remember( + 'active_categories_with_videos_admin', + 60, + fn() => + Category::select('id', 'name') + ->where('is_active', true) + ->whereHas('videos') + ->orderBy('name') + ->get() + ); + } return response()->json([ 'message' => 'Vídeos obtidos com sucesso', @@ -148,6 +187,7 @@ class VideosController extends Controller public function search(Request $request) { $user = auth()->user(); + $userSector = $user->sector_id; $search = trim((string) $request->query('search', '')); @@ -159,8 +199,12 @@ class VideosController extends Controller ->where('user_id', $user->id); } ]) - ->when($user->role_id !== 1, function ($query) { - $query->where('is_active', true); + ->when($user->role_id !== 1, function ($query) use ($userSector) { + $query->where('is_active', true) + ->whereHas('sectors', function ($s) use ($userSector) { + $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global"); + }); }) ->where(function ($query) use ($search) { $query->where('title', 'like', "%{$search}%") @@ -198,67 +242,106 @@ class VideosController extends Controller } $userID = $user->id; + $userSector = $user->sector_id; + $isAdmin = $user->role_id === 1; - if ($user->role_id !== 1) { - /* $video = Video::with('categories')->where('is_active', true)->find($id); */ - $video = Video::with([ - 'categories' => function ($query) use ($userID) { + $video = Video::with([ + 'categories' => function ($query) use ($isAdmin) { + if (!$isAdmin) { $query->where('is_active', true); - }, - 'views' => function ($query) use ($userID) { - $query->where('user_id', $userID); } - ])->find($id); + }, + 'sectors', + 'views' => function ($query) use ($userID) { + $query->where('user_id', $userID); + }, + ])->find($id); - $nextVideo = Video::select('id')->where('order', '>', $video->order)->where('is_active', true)->orderBy('order', 'asc')->first(); - $previousVideo = Video::select('id')->where('order', '<', $video->order)->where('is_active', true)->orderBy('order', 'desc')->first(); - - /* Para não mostrar vídeos inactivos para utilizadores não administradores */ - if (!$video || $video->is_active === false) { - return response()->json([ - 'message' => 'Acesso negado', - 'data' => null, - 'errors' => null, - ], 404); - } - } - - $video = Video::with('categories')->find($id); - - if ($video) { - $video->url = Storage::url($video->url); - $video->thumbnail = Storage::url($video->thumbnail); - return response()->json([ - 'message' => 'Vídeo obtido com sucesso', - 'data' => [ - 'id' => $video->id, - 'title' => $video->title, - 'description' => $video->description, - 'url' => $video->url, - 'thumbnail' => $video->thumbnail, - 'duration' => $video->duration, - 'tags' => $video->tags, - 'order' => $video->order, - 'categories' => $video->categories->map(function ($category) { - return [ - 'id' => $category->id, - 'name' => $category->name, - ]; - })->values(), - 'is_active' => $video->is_active, - 'watched' => $video->views->isNotEmpty(), - ], - 'errors' => null, - 'nextVideo' => $nextVideo->id ?? null, - 'previousVideo' => $previousVideo->id ?? null, - ], 200); - } else { + if (!$video) { return response()->json([ 'message' => 'Vídeo não encontrado', 'data' => null, 'errors' => null, ], 404); } + + if (!$isAdmin) { + if (!$video->is_active) { + return response()->json([ + 'message' => 'Acesso negado', + 'data' => null, + 'errors' => null, + ], 404); + } + + if (!$video->sectors->contains(fn ($sector) => $sector->id === $userSector || $sector->slug === 'global')) { + return response()->json([ + 'message' => 'Acesso negado', + 'data' => null, + 'errors' => null, + ], 404); + } + + $nextVideo = Video::select('id') + ->where('order', '>', $video->order) + ->where('is_active', true) + ->whereHas('sectors', fn($s) => $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global")) + ->orderBy('order', 'asc') + ->first(); + + $previousVideo = Video::select('id') + ->where('order', '<', $video->order) + ->where('is_active', true) + ->whereHas('sectors', fn($s) => $s->where('sectors.id', $userSector) + ->orWhere('sectors.slug', "global")) + ->orderBy('order', 'desc') + ->first(); + } else { + $nextVideo = Video::select('id') + ->where('order', '>', $video->order) + ->orderBy('order', 'asc') + ->first(); + + $previousVideo = Video::select('id') + ->where('order', '<', $video->order) + ->orderBy('order', 'desc') + ->first(); + } + + $video->url = Storage::url($video->url); + $video->thumbnail = Storage::url($video->thumbnail); + + return response()->json([ + 'message' => 'Vídeo obtido com sucesso', + 'data' => [ + 'id' => $video->id, + 'title' => $video->title, + 'description' => $video->description, + 'url' => $video->url, + 'thumbnail' => $video->thumbnail, + 'duration' => $video->duration, + 'tags' => $video->tags, + 'order' => $video->order, + 'categories' => $video->categories->map(function ($category) { + return [ + 'id' => $category->id, + 'name' => $category->name, + ]; + })->values(), + 'sectors' => $video->sectors->map(function ($sector) { + return [ + 'id' => $sector->id, + 'name' => $sector->name, + ]; + })->values(), + 'is_active' => $video->is_active, + 'watched' => $video->views->isNotEmpty(), + ], + 'errors' => null, + 'nextVideo' => $nextVideo->id ?? null, + 'previousVideo' => $previousVideo->id ?? null, + ], 200); } @@ -290,6 +373,7 @@ class VideosController extends Controller ]); $video->categories()->sync($request->input('category_ids', [])); + $video->sectors()->sync($request->input('sector_ids', [])); $baseUrl = $request->getSchemeAndHttpHost(); @@ -305,6 +389,7 @@ class VideosController extends Controller 'tags' => $video->tags, 'order' => $video->order, 'categories' => $video->categories->pluck('name'), + 'sectors' => $video->sectors->pluck('name'), 'is_active' => $video->is_active, ], 'errors' => null, @@ -355,10 +440,11 @@ class VideosController extends Controller ]); $videoToUpdate->categories()->sync($request->input('category_ids', [])); + $videoToUpdate->sectors()->sync($request->input('sector_ids', [])); return response()->json([ 'message' => 'Dados do vídeo atualizados com sucesso', - 'data' => $videoToUpdate->load('categories'), + 'data' => $videoToUpdate->load(['categories', 'sectors']), 'errors' => null, ], 200); } catch (\Throwable $th) { diff --git a/plataforma-tutorias/app/Http/Middleware/JwtMiddleware.php b/plataforma-tutorias/app/Http/Middleware/JwtMiddleware.php index 70c5423..9abb661 100644 --- a/plataforma-tutorias/app/Http/Middleware/JwtMiddleware.php +++ b/plataforma-tutorias/app/Http/Middleware/JwtMiddleware.php @@ -14,7 +14,11 @@ class JwtMiddleware try { JWTAuth::parseToken()->authenticate(); } catch (Exception $e) { - return redirect()->route('login'); + return response()->json([ + 'message' => 'Utilizador não autenticado', + 'data' => null, + 'errors' => null, + ], 401); } return $next($request); diff --git a/plataforma-tutorias/app/Http/Requests/CreateSectorRequest.php b/plataforma-tutorias/app/Http/Requests/CreateSectorRequest.php new file mode 100644 index 0000000..abfaaab --- /dev/null +++ b/plataforma-tutorias/app/Http/Requests/CreateSectorRequest.php @@ -0,0 +1,41 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:50|unique:sectors,name', + 'slug' => 'sometimes|string|max:50|unique:sectors,slug', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'O nome do setor é obrigatório', + 'name.max' => 'O nome do setor deve ter no máximo 50 caracteres', + 'slug.required' => 'O slug é obrigatório', + 'slug.max' => 'O slug deve ter no máximo 50 caracteres', + 'slug.unique' => 'O slug já existe', + 'name.unique' => 'O setor já existe', + ]; + } +} diff --git a/plataforma-tutorias/app/Http/Requests/CreateUserRequest.php b/plataforma-tutorias/app/Http/Requests/CreateUserRequest.php index 91d75ea..2dc008a 100644 --- a/plataforma-tutorias/app/Http/Requests/CreateUserRequest.php +++ b/plataforma-tutorias/app/Http/Requests/CreateUserRequest.php @@ -26,6 +26,7 @@ class CreateUserRequest extends FormRequest 'email' => 'required|email|unique:users,email', 'password' => 'required|string|min:6|confirmed', 'role_id' => 'required|exists:roles,id', + 'sector_id' => 'required|exists:sectors,id', ]; } @@ -44,6 +45,8 @@ class CreateUserRequest extends FormRequest 'password.required' => 'A password é obrigatória', 'password.min' => 'A password deve ter pelo menos 6 caracteres', 'password.confirmed' => 'As passwords não coincidem', + 'sector_id.required' => 'Obrigatório selecionar um setor', + 'sector_id.exists' => 'Setor não encontrado', ]; } } \ No newline at end of file diff --git a/plataforma-tutorias/app/Http/Requests/CreateVideoRequest.php b/plataforma-tutorias/app/Http/Requests/CreateVideoRequest.php index 09ad9bf..6e2ae40 100644 --- a/plataforma-tutorias/app/Http/Requests/CreateVideoRequest.php +++ b/plataforma-tutorias/app/Http/Requests/CreateVideoRequest.php @@ -39,6 +39,7 @@ class CreateVideoRequest extends FormRequest 'duration' => 'nullable|string|max:10', 'tags' => 'nullable|string|max:255', // nullable para não ser obrigatório 'category_ids' => 'nullable|array|exists:categories,id', // nullable caso não selecione + 'sector_ids' => 'nullable|array|exists:sectors,id', // nullable caso não selecione 'order' => 'nullable|integer|min:0', ]; } diff --git a/plataforma-tutorias/app/Http/Requests/UpdateUserRequest.php b/plataforma-tutorias/app/Http/Requests/UpdateUserRequest.php index 9c662d0..6805f82 100644 --- a/plataforma-tutorias/app/Http/Requests/UpdateUserRequest.php +++ b/plataforma-tutorias/app/Http/Requests/UpdateUserRequest.php @@ -32,6 +32,7 @@ class UpdateUserRequest extends FormRequest Rule::unique('users', 'email')->ignore($userId), ], 'role_id' => 'sometimes|exists:roles,id', + 'sector_id' => 'sometimes|exists:sectors,id', ]; } @@ -43,6 +44,7 @@ class UpdateUserRequest extends FormRequest 'email.email' => 'O email deve ser um email válido', 'email.unique' => 'O email já está em uso', 'role_id.exists' => 'Cargo não encontrado', + 'sector_id.exists' => 'Setor não encontrado', ]; } } diff --git a/plataforma-tutorias/app/Http/Requests/UpdateVideoRequest.php b/plataforma-tutorias/app/Http/Requests/UpdateVideoRequest.php index 001eb48..24a4fb6 100644 --- a/plataforma-tutorias/app/Http/Requests/UpdateVideoRequest.php +++ b/plataforma-tutorias/app/Http/Requests/UpdateVideoRequest.php @@ -39,6 +39,8 @@ class UpdateVideoRequest extends FormRequest 'tags' => 'sometimes|string|max:100', 'category_ids' => 'sometimes|array', 'category_ids.*' => 'exists:categories,id', + 'sector_ids' => 'sometimes|array', + 'sector_ids.*' => 'exists:sectors,id', 'is_active' => 'sometimes|boolean', 'order' => 'sometimes|integer|min:0', ]; diff --git a/plataforma-tutorias/app/Models/Sector.php b/plataforma-tutorias/app/Models/Sector.php new file mode 100644 index 0000000..076595a --- /dev/null +++ b/plataforma-tutorias/app/Models/Sector.php @@ -0,0 +1,29 @@ +belongsToMany(Video::class, 'video_sector'); + } + + public function users() + { + return $this->hasMany(User::class); + } + +} diff --git a/plataforma-tutorias/app/Models/User.php b/plataforma-tutorias/app/Models/User.php index 980358d..27934e7 100644 --- a/plataforma-tutorias/app/Models/User.php +++ b/plataforma-tutorias/app/Models/User.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Tymon\JWTAuth\Contracts\JWTSubject; +use App\Models\Sector; class User extends Authenticatable implements JWTSubject { @@ -16,6 +17,7 @@ class User extends Authenticatable implements JWTSubject 'email', 'password', 'role_id', + 'sector_id', 'created_at', ]; @@ -47,4 +49,9 @@ class User extends Authenticatable implements JWTSubject { return $this->belongsToMany(Workshop::class, 'user_workshop')->withTimestamps(); } + + public function sector() + { + return $this->belongsTo(Sector::class, 'sector_id', 'id'); + } } \ No newline at end of file diff --git a/plataforma-tutorias/app/Models/Video.php b/plataforma-tutorias/app/Models/Video.php index 0355464..4b07685 100644 --- a/plataforma-tutorias/app/Models/Video.php +++ b/plataforma-tutorias/app/Models/Video.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Models\Category; +use App\Models\Sector; class Video extends Model { @@ -84,4 +85,8 @@ class Video extends Model return $this->views()->where('user_id', $user->id)->exists(); } + public function sectors() + { + return $this->belongsToMany(Sector::class, 'video_sector'); + } } diff --git a/plataforma-tutorias/config/jwt.php b/plataforma-tutorias/config/jwt.php index 5ce9856..9523a9c 100644 --- a/plataforma-tutorias/config/jwt.php +++ b/plataforma-tutorias/config/jwt.php @@ -101,7 +101,7 @@ return [ | */ - 'ttl' => env('JWT_TTL', 15), + 'ttl' => env('JWT_TTL', 1), /* |-------------------------------------------------------------------------- @@ -120,7 +120,7 @@ return [ | */ - 'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), + 'refresh_ttl' => env('JWT_REFRESH_TTL', 1), /* |-------------------------------------------------------------------------- diff --git a/plataforma-tutorias/database/migrations/2026_06_02_113520_create_refresh_tokens.php b/plataforma-tutorias/database/migrations/2026_06_03_162741_create_sector_table.php similarity index 56% rename from plataforma-tutorias/database/migrations/2026_06_02_113520_create_refresh_tokens.php rename to plataforma-tutorias/database/migrations/2026_06_03_162741_create_sector_table.php index 04bda36..d151cbb 100644 --- a/plataforma-tutorias/database/migrations/2026_06_02_113520_create_refresh_tokens.php +++ b/plataforma-tutorias/database/migrations/2026_06_03_162741_create_sector_table.php @@ -11,13 +11,12 @@ return new class extends Migration */ public function up(): void { - Schema::create('refresh_tokens', function (Blueprint $table) { + Schema::create('sectors', function (Blueprint $table) { $table->id(); - $table->foreignId('user_id')->constrained('users'); - $table->string('token_hash'); - $table->dateTime('revoked_at')->nullable(); - $table->dateTime('expires_at'); + $table->string('name'); + $table->string('slug'); $table->timestamps(); + $table->boolean('is_active')->default(true); }); } @@ -26,6 +25,6 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('refresh_tokens'); + Schema::dropIfExists('sectors'); } }; diff --git a/plataforma-tutorias/database/migrations/2026_06_03_172841_add_sector_to_users_table.php b/plataforma-tutorias/database/migrations/2026_06_03_172841_add_sector_to_users_table.php new file mode 100644 index 0000000..9dba9a5 --- /dev/null +++ b/plataforma-tutorias/database/migrations/2026_06_03_172841_add_sector_to_users_table.php @@ -0,0 +1,28 @@ +foreignId('sector_id')->nullable()->constrained('sectors'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['sector_id']); + }); + } +}; diff --git a/plataforma-tutorias/database/migrations/2026_06_03_180000_create_video_sector_table.php b/plataforma-tutorias/database/migrations/2026_06_03_180000_create_video_sector_table.php new file mode 100644 index 0000000..aa3c24e --- /dev/null +++ b/plataforma-tutorias/database/migrations/2026_06_03_180000_create_video_sector_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('video_id')->constrained()->cascadeOnDelete(); + $table->foreignId('sector_id')->constrained()->cascadeOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('video_sector'); + } +}; diff --git a/plataforma-tutorias/database/seeders/DatabaseSeeder.php b/plataforma-tutorias/database/seeders/DatabaseSeeder.php index 31c43fb..886202c 100644 --- a/plataforma-tutorias/database/seeders/DatabaseSeeder.php +++ b/plataforma-tutorias/database/seeders/DatabaseSeeder.php @@ -17,6 +17,7 @@ class DatabaseSeeder extends Seeder $this->call([ RoleSeeder::class, UserSeeder::class, + SectorsTableSeeder::class, ]); } } diff --git a/plataforma-tutorias/database/seeders/SectorsTableSeeder.php b/plataforma-tutorias/database/seeders/SectorsTableSeeder.php new file mode 100644 index 0000000..cb2b71f --- /dev/null +++ b/plataforma-tutorias/database/seeders/SectorsTableSeeder.php @@ -0,0 +1,24 @@ +insert([ + 'name' => 'Global', + 'slug' => 'global', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } +} diff --git a/plataforma-tutorias/routes/api.php b/plataforma-tutorias/routes/api.php index 1e1f05b..882f879 100644 --- a/plataforma-tutorias/routes/api.php +++ b/plataforma-tutorias/routes/api.php @@ -11,6 +11,7 @@ use App\Http\Controllers\WorkshopsController; use App\Http\Middleware\JwtMiddleware; use App\Http\Controllers\VideoViewController; use App\Http\Controllers\DashboardController; +use App\Http\Controllers\SectorController; /* |-------------------------------------------------------------------------- | API Routes @@ -40,14 +41,17 @@ Route::middleware([JwtMiddleware::class])->group(function () { Route::get('/next-videos', [VideosController::class, 'nextVideos']); Route::get('/categories', [CategoryController::class, 'index']); + Route::post('/categories', [CategoryController::class, 'create']); + Route::get('/sectors', [SectorController::class, 'index']); + Route::post('/sectors', [SectorController::class, 'create']); + Route::get('/workshops', [WorkshopsController::class, 'index']); Route::get('/workshop/{id}', [WorkshopsController::class, 'getWorkshop']); Route::get('/workshops-length', [WorkshopsController::class, 'workshopsLength']); Route::get('/workshops-search', [WorkshopsController::class, 'search']); Route::get('/next-workshops', [WorkshopsController::class, 'nextWorkshops']); - Route::post('/categories', [CategoryController::class, 'create']); Route::post('/inscrever/{id}', [WorkshopsController::class, 'inscrever']); Route::delete('/cancelar-inscricao/{id}', [WorkshopsController::class, 'cancelarInscricao']);