7 Commits e6730879a0 ... 6e62c09810

Autor SHA1 Mensagem Data
  Gustavo Zanatta 6e62c09810 feat: ✨ feat (primeiro acesso associado) criado fluxo de primeiro acesso do associado há 14 horas atrás
  Gustavo Zanatta 1fdb7fb02d feat: ✨ feat (afastamentos) criada funcao de importar afastamentos há 1 dia atrás
  Gustavo Zanatta 1e9fde8823 feat: :sparkles: feat (agendamento) criada flag de auto aprovado há 1 dia atrás
  Gustavo Zanatta f936d9154e feat: :sparkles: feat (agendamentos associado) adicionada informacoes dos agendamentos do associado há 1 dia atrás
  Gustavo Zanatta 209ed0f05b feat: :sparkles: feat (associados layout adm) adicionada opção de aprovar / recusar associado há 1 dia atrás
  Gustavo Zanatta f109656b8d feat: :sparkles: feat (importacao) alterada geracao de email há 1 dia atrás
  Gustavo Zanatta 084a5be212 feat: :sparkles: feat (dependentes do associado) adicionada aprovacao de dependentes + correcao no status há 1 dia atrás
42 ficheiros alterados com 735 adições e 41 exclusões
  1. 1 0
      app/Enums/UserStatusEnum.php
  2. 10 2
      app/Http/Controllers/AppointmentController.php
  3. 20 0
      app/Http/Controllers/AssociadoImportController.php
  4. 8 0
      app/Http/Controllers/AuthController.php
  5. 39 0
      app/Http/Controllers/FirstAccessController.php
  6. 15 0
      app/Http/Controllers/UserController.php
  7. 33 0
      app/Http/Controllers/UserDependentController.php
  8. 15 0
      app/Http/Requests/FirstAccessCheckRequest.php
  9. 40 0
      app/Http/Requests/FirstAccessRegisterRequest.php
  10. 14 6
      app/Http/Requests/LandingPageRegisterRequest.php
  11. 3 0
      app/Http/Resources/AppointmentResource.php
  12. 10 0
      app/Http/Resources/UserDependentResource.php
  13. 1 0
      app/Http/Resources/UserResource.php
  14. 1 0
      app/Jobs/BaseImportJob.php
  15. 18 0
      app/Jobs/SyncAfastamentosJob.php
  16. 9 3
      app/Models/Appointment.php
  17. 1 0
      app/Models/ImportLog.php
  18. 1 0
      app/Models/User.php
  19. 21 8
      app/Services/AppointmentService.php
  20. 61 9
      app/Services/AssociadoImportService.php
  21. 9 0
      app/Services/AuthService.php
  22. 3 0
      app/Services/DashboardService.php
  23. 173 0
      app/Services/FirstAccessService.php
  24. 11 10
      app/Services/LandingPageService.php
  25. 50 0
      app/Services/UserDependentService.php
  26. 12 0
      app/Services/UserService.php
  27. 22 0
      database/migrations/2026_07_28_000001_add_on_leave_to_import_logs_table.php
  28. 28 0
      database/migrations/2026_07_28_000001_make_email_nullable_in_users_table.php
  29. 25 0
      database/migrations/2026_07_28_000002_add_approval_fields_to_appointments_table.php
  30. 22 0
      database/migrations/2026_07_29_000001_add_first_access_completed_at_to_users_table.php
  31. 6 0
      database/seeders/PermissionSeeder.php
  32. 2 0
      lang/en/auth.php
  33. 7 1
      lang/en/messages.php
  34. 2 0
      lang/es/auth.php
  35. 7 1
      lang/es/messages.php
  36. 2 0
      lang/pt/auth.php
  37. 7 1
      lang/pt/messages.php
  38. 1 0
      routes/authRoutes/appointment.php
  39. 1 0
      routes/authRoutes/associado_import.php
  40. 2 0
      routes/authRoutes/user.php
  41. 6 0
      routes/authRoutes/user_dependent.php
  42. 16 0
      routes/noAuthRoutes/first_access.php

+ 1 - 0
app/Enums/UserStatusEnum.php

@@ -12,4 +12,5 @@ enum UserStatusEnum: string
     case INACTIVE = 'inactive';
     case INACTIVE = 'inactive';
     case PENDING = 'pending';
     case PENDING = 'pending';
     case ON_LEAVE = 'on_leave';
     case ON_LEAVE = 'on_leave';
+    case REFUSED = 'refused';
 }
 }

+ 10 - 2
app/Http/Controllers/AppointmentController.php

@@ -34,6 +34,12 @@ class AppointmentController extends Controller
         return $this->successResponse(payload: AppointmentResource::collection($items));
         return $this->successResponse(payload: AppointmentResource::collection($items));
     }
     }
 
 
+    public function getByUser(int $id): JsonResponse
+    {
+        $items = $this->service->getAllByUser($id);
+        return $this->successResponse(payload: AppointmentResource::collection($items));
+    }
+
     public function store(AppointmentRequest $request): JsonResponse
     public function store(AppointmentRequest $request): JsonResponse
     {
     {
         $data = $request->validated();
         $data = $request->validated();
@@ -42,8 +48,10 @@ class AppointmentController extends Controller
 
 
         if ($creatingForOther) {
         if ($creatingForOther) {
             $data['status'] = AppointmentStatusEnum::CONFIRMADO;
             $data['status'] = AppointmentStatusEnum::CONFIRMADO;
+            $data['auto_approved'] = true;
         } elseif ($authUser->status === UserStatusEnum::ACTIVE) {
         } elseif ($authUser->status === UserStatusEnum::ACTIVE) {
             $data['status'] = AppointmentStatusEnum::CONFIRMADO;
             $data['status'] = AppointmentStatusEnum::CONFIRMADO;
+            $data['auto_approved'] = true;
         }
         }
 
 
         $item = $this->service->create($data);
         $item = $this->service->create($data);
@@ -67,7 +75,7 @@ class AppointmentController extends Controller
 
 
     public function update(AppointmentRequest $request, int $id): JsonResponse
     public function update(AppointmentRequest $request, int $id): JsonResponse
     {
     {
-        $item = $this->service->update($id, $request->validated());
+        $item = $this->service->update($id, $request->validated(), Auth::id());
         return $this->successResponse(
         return $this->successResponse(
             payload: new AppointmentResource($item),
             payload: new AppointmentResource($item),
             message: __('messages.updated'),
             message: __('messages.updated'),
@@ -114,7 +122,7 @@ class AppointmentController extends Controller
 
 
     public function approve(AppointmentApproveRequest $request, int $id): JsonResponse
     public function approve(AppointmentApproveRequest $request, int $id): JsonResponse
     {
     {
-        $item = $this->service->approve($id, $request->date, $request->time);
+        $item = $this->service->approve($id, $request->date, $request->time, Auth::id());
         return $this->successResponse(payload: new AppointmentResource($item), message: __('messages.updated'));
         return $this->successResponse(payload: new AppointmentResource($item), message: __('messages.updated'));
     }
     }
 
 

+ 20 - 0
app/Http/Controllers/AssociadoImportController.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Http\Controllers;
 namespace App\Http\Controllers;
 
 
+use App\Jobs\SyncAfastamentosJob;
 use App\Jobs\SyncAssociadosJob;
 use App\Jobs\SyncAssociadosJob;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
@@ -29,4 +30,23 @@ class AssociadoImportController extends Controller
             code: 202,
             code: 202,
         );
         );
     }
     }
+
+    public function importAfastamentos(Request $request): JsonResponse
+    {
+        $request->validate([
+            'file' => 'required|file|mimes:xlsx|max:10240',
+        ]);
+
+        $importId = Str::uuid()->toString();
+        $filePath = Storage::putFileAs('imports', $request->file('file'), $importId . '.xlsx');
+
+        Cache::put($importId, ['status' => 'pending'], now()->addDay());
+
+        SyncAfastamentosJob::dispatch($filePath, $importId, auth()->id());
+
+        return $this->successResponse(
+            payload: ['import_id' => $importId],
+            code: 202,
+        );
+    }
 }
 }

+ 8 - 0
app/Http/Controllers/AuthController.php

@@ -31,6 +31,14 @@ class AuthController extends Controller
             return $this->errorResponse(message: __("auth.wrong_type"), code: 403);
             return $this->errorResponse(message: __("auth.wrong_type"), code: 403);
         }
         }
 
 
+        if (isset($result["error"]) && $result["error"] === "refused") {
+            return $this->errorResponse(message: __("auth.refused"), code: 403);
+        }
+
+        if (isset($result["error"]) && $result["error"] === "first_access_required") {
+            return $this->errorResponse(message: __("auth.first_access_required"), code: 403);
+        }
+
         $cookieName = $this->getCookieName($request);
         $cookieName = $this->getCookieName($request);
 
 
         return $this->successResponse(
         return $this->successResponse(

+ 39 - 0
app/Http/Controllers/FirstAccessController.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\FirstAccessCheckRequest;
+use App\Http\Requests\FirstAccessRegisterRequest;
+use App\Http\Resources\UserResource;
+use App\Services\FirstAccessService;
+use Illuminate\Http\JsonResponse;
+
+class FirstAccessController extends Controller
+{
+    public function __construct(protected FirstAccessService $service) {}
+
+    public function check(FirstAccessCheckRequest $request): JsonResponse
+    {
+        $result = $this->service->check($request->validated()['registration']);
+
+        $message = $result['status'] === FirstAccessService::STATUS_DONE
+            ? __('messages.first_access.already_done')
+            : null;
+
+        return $this->successResponse(payload: $result, message: $message);
+    }
+
+    public function register(FirstAccessRegisterRequest $request): JsonResponse
+    {
+        $user = $this->service->register(
+            $request->validated(),
+            $request->file('photo'),
+        );
+
+        return $this->successResponse(
+            payload: new UserResource($user),
+            message: __('messages.first_access.register_success'),
+            code: 201,
+        );
+    }
+}

+ 15 - 0
app/Http/Controllers/UserController.php

@@ -118,6 +118,7 @@ class UserController extends Controller
             'name'           => 'sometimes|string|max:255',
             'name'           => 'sometimes|string|max:255',
             'cpf'            => 'sometimes|string|max:20',
             'cpf'            => 'sometimes|string|max:20',
             'email'          => 'sometimes|email|max:255',
             'email'          => 'sometimes|email|max:255',
+            'phone'          => 'sometimes|string|max:20',
             'position_id'    => 'sometimes|nullable|integer|exists:positions,id',
             'position_id'    => 'sometimes|nullable|integer|exists:positions,id',
             'sector_id'      => 'sometimes|nullable|integer|exists:sectors,id',
             'sector_id'      => 'sometimes|nullable|integer|exists:sectors,id',
         ]);
         ]);
@@ -158,4 +159,18 @@ class UserController extends Controller
             message: __("messages.updated"),
             message: __("messages.updated"),
         );
         );
     }
     }
+
+    public function refuse(int $id): JsonResponse
+    {
+        $item = $this->service->refuse($id);
+
+        if (!$item) {
+            return $this->errorResponse(message: __('messages.not_found'), code: 404);
+        }
+
+        return $this->successResponse(
+            payload: new UserResource($item),
+            message: __("messages.updated"),
+        );
+    }
 }
 }

+ 33 - 0
app/Http/Controllers/UserDependentController.php

@@ -6,6 +6,7 @@ use App\Http\Requests\UserDependentRequest;
 use App\Http\Resources\UserDependentResource;
 use App\Http\Resources\UserDependentResource;
 use App\Services\UserDependentService;
 use App\Services\UserDependentService;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
 
 
 class UserDependentController extends Controller
 class UserDependentController extends Controller
 {
 {
@@ -17,6 +18,20 @@ class UserDependentController extends Controller
         return $this->successResponse(payload: UserDependentResource::collection($items));
         return $this->successResponse(payload: UserDependentResource::collection($items));
     }
     }
 
 
+    public function indexPaginated(Request $request): JsonResponse
+    {
+        $filters = $request->only(['status', 'search']);
+        $perPage = min((int) $request->get('per_page', 10), 100);
+        $paginator = $this->service->getAllPaginated($filters, $perPage);
+
+        return $this->successResponse(payload: [
+            'data'  => UserDependentResource::collection($paginator->items()),
+            'total' => $paginator->total(),
+            'from'  => $paginator->firstItem() ?? 0,
+            'to'    => $paginator->lastItem() ?? 0,
+        ]);
+    }
+
     public function store(UserDependentRequest $request): JsonResponse
     public function store(UserDependentRequest $request): JsonResponse
     {
     {
         $item = $this->service->create($request->validated());
         $item = $this->service->create($request->validated());
@@ -47,4 +62,22 @@ class UserDependentController extends Controller
         $this->service->delete($id);
         $this->service->delete($id);
         return $this->successResponse(message: __('messages.deleted'), code: 204);
         return $this->successResponse(message: __('messages.deleted'), code: 204);
     }
     }
+
+    public function approve(int $id): JsonResponse
+    {
+        $item = $this->service->approve($id);
+        if (!$item) {
+            return $this->errorResponse(message: __('messages.not_found'), code: 404);
+        }
+        return $this->successResponse(payload: new UserDependentResource($item), message: __('messages.updated'));
+    }
+
+    public function refuse(int $id): JsonResponse
+    {
+        $item = $this->service->refuse($id);
+        if (!$item) {
+            return $this->errorResponse(message: __('messages.not_found'), code: 404);
+        }
+        return $this->successResponse(payload: new UserDependentResource($item), message: __('messages.updated'));
+    }
 }
 }

+ 15 - 0
app/Http/Requests/FirstAccessCheckRequest.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class FirstAccessCheckRequest extends FormRequest
+{
+    public function rules(): array
+    {
+        return [
+            'registration' => 'required|string|max:255',
+        ];
+    }
+}

+ 40 - 0
app/Http/Requests/FirstAccessRegisterRequest.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Http\Requests;
+
+use App\Enums\UserTypeEnum;
+use App\Models\User;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Rules\Password;
+
+class FirstAccessRegisterRequest extends FormRequest
+{
+    public function rules(): array
+    {
+        $userId = $this->existingUserId();
+
+        return [
+            'token'        => 'required|string',
+            'registration' => ['required', 'string', 'max:255', Rule::unique('users', 'registration')->ignore($userId)],
+            'name'         => 'required|string|max:255',
+            'cpf'          => ['required', 'string', 'max:14', Rule::unique('users', 'cpf')->ignore($userId)],
+            'email'        => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($userId)],
+            'phone'        => 'required|string|max:20',
+            'position_id'  => 'required|integer|exists:positions,id',
+            'sector_id'    => 'required|integer|exists:sectors,id',
+            'password'     => ['required', 'confirmed', Password::min(8)->mixedCase()->numbers()],
+            'photo'        => 'required|image|max:5120',
+        ];
+    }
+
+    /**
+     * Associado que já existe com este crachá — as regras de unicidade ignoram o próprio registro.
+     */
+    private function existingUserId(): ?int
+    {
+        return User::where('type', UserTypeEnum::ASSOCIADO)
+            ->where('registration', $this->input('registration'))
+            ->value('id');
+    }
+}

+ 14 - 6
app/Http/Requests/LandingPageRegisterRequest.php

@@ -9,12 +9,20 @@ class LandingPageRegisterRequest extends FormRequest
     public function rules(): array
     public function rules(): array
     {
     {
         return [
         return [
-            'name'           => 'required|string|max:255',
-            'cpf'            => 'required|string|max:14|unique:users,cpf',
-            'email'          => 'required|email|unique:users,email',
-            'phone'          => 'required|string|max:20',
-            'position_label' => 'required|string|max:255',
-            'sector_label'   => 'required|string|max:255',
+            'name'         => 'required|string|max:255',
+            'cpf'          => 'required|string|max:14|unique:users,cpf',
+            'registration' => 'required|string|max:255|unique:users,registration',
+            'email'        => 'required|email|unique:users,email',
+            'phone'        => 'required|string|max:20',
+            'position_id'  => 'required|integer|exists:positions,id',
+            'sector_id'    => 'required|integer|exists:sectors,id',
+        ];
+    }
+
+    public function messages(): array
+    {
+        return [
+            'registration.unique' => __('messages.landing.registration_taken'),
         ];
         ];
     }
     }
 }
 }

+ 3 - 0
app/Http/Resources/AppointmentResource.php

@@ -24,6 +24,9 @@ class AppointmentResource extends JsonResource
             'observations'                 => $this->observations,
             'observations'                 => $this->observations,
             'requested_at'                 => $this->requested_at?->format('Y-m-d H:i:s'),
             'requested_at'                 => $this->requested_at?->format('Y-m-d H:i:s'),
             'status'                       => $this->status,
             'status'                       => $this->status,
+            'auto_approved'                => $this->auto_approved,
+            'approved_by_user_id'          => $this->approved_by_user_id,
+            'approved_by_user'             => $this->whenLoaded('approvedByUser', fn() => new UserResource($this->approvedByUser)),
             'created_at'                   => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
             'created_at'                   => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
             'updated_at'                   => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
             'updated_at'                   => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
         ];
         ];

+ 10 - 0
app/Http/Resources/UserDependentResource.php

@@ -16,6 +16,16 @@ class UserDependentResource extends JsonResource
             'name'                => $this->name,
             'name'                => $this->name,
             'kinship'             => $this->kinship,
             'kinship'             => $this->kinship,
             'status'              => $this->status,
             'status'              => $this->status,
+            'responsible_user'    => $this->whenLoaded('responsibleUser', fn() => [
+                'id'       => $this->responsibleUser->id,
+                'name'     => $this->responsibleUser->name,
+                'position' => $this->responsibleUser->relationLoaded('position')
+                    ? $this->responsibleUser->position?->name
+                    : null,
+                'sector'   => $this->responsibleUser->relationLoaded('sector')
+                    ? $this->responsibleUser->sector?->name
+                    : null,
+            ]),
             'created_at'          => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
             'created_at'          => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
             'updated_at'          => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
             'updated_at'          => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
         ];
         ];

+ 1 - 0
app/Http/Resources/UserResource.php

@@ -36,6 +36,7 @@ class UserResource extends JsonResource
             'photo_url'                  => $this->photo_path
             'photo_url'                  => $this->photo_path
                                                 ? Storage::disk('s3')->temporaryUrl($this->photo_path, now()->addHours(24))
                                                 ? Storage::disk('s3')->temporaryUrl($this->photo_path, now()->addHours(24))
                                                 : null,
                                                 : null,
+            'first_access_completed_at'  => $this->first_access_completed_at?->format('Y-m-d H:i:s'),
             'on_leave_at'                => $this->on_leave_at?->format('Y-m-d H:i:s'),
             'on_leave_at'                => $this->on_leave_at?->format('Y-m-d H:i:s'),
             'on_leave_by_user_id'        => $this->on_leave_by_user_id,
             'on_leave_by_user_id'        => $this->on_leave_by_user_id,
             'first_access_at'            => $this->access_logs_min_accessed_at
             'first_access_at'            => $this->access_logs_min_accessed_at

+ 1 - 0
app/Jobs/BaseImportJob.php

@@ -40,6 +40,7 @@ abstract class BaseImportJob implements ShouldQueue
                 'created'     => $stats['created'] ?? 0,
                 'created'     => $stats['created'] ?? 0,
                 'updated'     => $stats['updated'] ?? 0,
                 'updated'     => $stats['updated'] ?? 0,
                 'inactivated' => $stats['inactivated'] ?? 0,
                 'inactivated' => $stats['inactivated'] ?? 0,
+                'on_leave'    => $stats['on_leave'] ?? 0,
                 'imported_at' => now(),
                 'imported_at' => now(),
             ]);
             ]);
 
 

+ 18 - 0
app/Jobs/SyncAfastamentosJob.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Services\AssociadoImportService;
+
+class SyncAfastamentosJob extends BaseImportJob
+{
+    protected function getImportType(): string
+    {
+        return 'afastamento';
+    }
+
+    protected function runSync(string $filePath): array
+    {
+        return app(AssociadoImportService::class)->syncAfastamentosFromExcel($filePath, $this->userId);
+    }
+}

+ 9 - 3
app/Models/Appointment.php

@@ -16,9 +16,10 @@ class Appointment extends Model
     protected function casts(): array
     protected function casts(): array
     {
     {
         return [
         return [
-            'date'         => 'date',
-            'requested_at' => 'datetime',
-            'status'       => AppointmentStatusEnum::class,
+            'date'          => 'date',
+            'requested_at'  => 'datetime',
+            'status'        => AppointmentStatusEnum::class,
+            'auto_approved' => 'boolean',
         ];
         ];
     }
     }
 
 
@@ -27,6 +28,11 @@ class Appointment extends Model
         return $this->belongsTo(User::class);
         return $this->belongsTo(User::class);
     }
     }
 
 
+    public function approvedByUser(): BelongsTo
+    {
+        return $this->belongsTo(User::class, 'approved_by_user_id');
+    }
+
     public function partnerAgreement(): BelongsTo
     public function partnerAgreement(): BelongsTo
     {
     {
         return $this->belongsTo(PartnerAgreement::class);
         return $this->belongsTo(PartnerAgreement::class);

+ 1 - 0
app/Models/ImportLog.php

@@ -14,6 +14,7 @@ class ImportLog extends Model
         'created',
         'created',
         'updated',
         'updated',
         'inactivated',
         'inactivated',
+        'on_leave',
         'imported_at',
         'imported_at',
     ];
     ];
 
 

+ 1 - 0
app/Models/User.php

@@ -77,6 +77,7 @@ class User extends Authenticatable
             "language"          => LanguageEnum::class,
             "language"          => LanguageEnum::class,
             "status"            => UserStatusEnum::class,
             "status"            => UserStatusEnum::class,
             "admission_date"    => "date",
             "admission_date"    => "date",
+            "first_access_completed_at" => "datetime",
             "expiry_date"       => "date",
             "expiry_date"       => "date",
             "excluded_at"       => "datetime",
             "excluded_at"       => "datetime",
             "on_leave_at"       => "datetime",
             "on_leave_at"       => "datetime",

+ 21 - 8
app/Services/AppointmentService.php

@@ -65,7 +65,7 @@ class AppointmentService
         ], $model->user_id);
         ], $model->user_id);
     }
     }
 
 
-    public function update(int $id, array $data): ?Appointment
+    public function update(int $id, array $data, ?int $approvingUserId = null): ?Appointment
     {
     {
         $model = Appointment::find($id);
         $model = Appointment::find($id);
 
 
@@ -73,6 +73,15 @@ class AppointmentService
             return null;
             return null;
         }
         }
 
 
+        if (
+            isset($data['status'])
+            && $data['status'] === AppointmentStatusEnum::CONFIRMADO->value
+            && $model->status !== AppointmentStatusEnum::CONFIRMADO
+        ) {
+            $data['auto_approved'] = false;
+            $data['approved_by_user_id'] = $approvingUserId;
+        }
+
         $model->update($data);
         $model->update($data);
         return $model->fresh(['user', 'partnerAgreement', 'partnerAgreementService']);
         return $model->fresh(['user', 'partnerAgreement', 'partnerAgreementService']);
     }
     }
@@ -122,14 +131,16 @@ class AppointmentService
         return $query->paginate($perPage);
         return $query->paginate($perPage);
     }
     }
 
 
-    public function approve(int $id, string $date, string $time): ?Appointment
+    public function approve(int $id, string $date, string $time, ?int $approvedByUserId = null): ?Appointment
     {
     {
         $model = Appointment::find($id);
         $model = Appointment::find($id);
         if (!$model) return null;
         if (!$model) return null;
         $model->update([
         $model->update([
-            'status' => AppointmentStatusEnum::CONFIRMADO,
-            'date'   => $date,
-            'time'   => $time,
+            'status'              => AppointmentStatusEnum::CONFIRMADO,
+            'date'                => $date,
+            'time'                => $time,
+            'auto_approved'       => false,
+            'approved_by_user_id' => $approvedByUserId,
         ]);
         ]);
         $this->notificationService->createAutoForUser([
         $this->notificationService->createAutoForUser([
             'title'     => 'Agendamento confirmado',
             'title'     => 'Agendamento confirmado',
@@ -161,9 +172,11 @@ class AppointmentService
         $model = Appointment::whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))->find($id);
         $model = Appointment::whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))->find($id);
         if (!$model) return null;
         if (!$model) return null;
         $model->update([
         $model->update([
-            'status' => AppointmentStatusEnum::CONFIRMADO,
-            'date'   => $date,
-            'time'   => $time,
+            'status'              => AppointmentStatusEnum::CONFIRMADO,
+            'date'                => $date,
+            'time'                => $time,
+            'auto_approved'       => false,
+            'approved_by_user_id' => $userId,
         ]);
         ]);
         $this->notificationService->createAutoForUser([
         $this->notificationService->createAutoForUser([
             'title'     => 'Agendamento confirmado',
             'title'     => 'Agendamento confirmado',

+ 61 - 9
app/Services/AssociadoImportService.php

@@ -68,14 +68,9 @@ class AssociadoImportService
                     $updated++;
                     $updated++;
                 }
                 }
             } else {
             } else {
-                $firstName = $this->extractFirstName($name);
-                $email     = "{$firstName}_{$registration}@serprati.com";
-                $password  = "{$firstName}2026";
-
                 User::create([
                 User::create([
                     'name'           => $name,
                     'name'           => $name,
-                    'email'          => $email,
-                    'password'       => Hash::make($password),
+                    'password'       => Hash::make(Str::random(32)),
                     'type'           => UserTypeEnum::ASSOCIADO,
                     'type'           => UserTypeEnum::ASSOCIADO,
                     'status'         => UserStatusEnum::ACTIVE,
                     'status'         => UserStatusEnum::ACTIVE,
                     'registration'   => $registration,
                     'registration'   => $registration,
@@ -104,9 +99,66 @@ class AssociadoImportService
         ];
         ];
     }
     }
 
 
-    private function extractFirstName(string $fullName): string
+    public function syncAfastamentosFromExcel(string $filePath, int $byUserId): array
     {
     {
-        $firstName = explode(' ', trim($fullName))[0];
-        return strtolower(Str::ascii($firstName));
+        $import = new AssociadoImport();
+        Excel::import($import, $filePath);
+        $rows = $import->rows ?? collect();
+
+        $today      = Carbon::today()->toDateString();
+        $expiryDate = Carbon::now()->endOfMonth()->toDateString();
+
+        $existing = User::where('type', UserTypeEnum::ASSOCIADO)
+            ->whereNotNull('registration')
+            ->get()
+            ->keyBy('registration');
+
+        $seenRegistrations = [];
+        $created  = 0;
+        $onLeave  = 0;
+
+        foreach ($rows as $row) {
+            $registration = $row['registration'];
+            $name         = $row['name'];
+
+            if (isset($seenRegistrations[$registration])) {
+                continue;
+            }
+            $seenRegistrations[$registration] = true;
+
+            if ($existing->has($registration)) {
+                $user = $existing->get($registration);
+
+                if ($user->status !== UserStatusEnum::ON_LEAVE) {
+                    $user->status              = UserStatusEnum::ON_LEAVE;
+                    $user->on_leave_at         = now();
+                    $user->on_leave_by_user_id = $byUserId;
+                    $user->save();
+
+                    $onLeave++;
+                }
+            } else {
+                User::create([
+                    'name'                 => $name,
+                    'password'             => Hash::make(Str::random(32)),
+                    'type'                 => UserTypeEnum::ASSOCIADO,
+                    'status'               => UserStatusEnum::ON_LEAVE,
+                    'registration'         => $registration,
+                    'admission_date'       => $today,
+                    'expiry_date'          => $expiryDate,
+                    'on_leave_at'          => now(),
+                    'on_leave_by_user_id'  => $byUserId,
+                ]);
+
+                $created++;
+            }
+        }
+
+        return [
+            'total'    => $rows->count(),
+            'created'  => $created,
+            'on_leave' => $onLeave,
+        ];
     }
     }
+
 }
 }

+ 9 - 0
app/Services/AuthService.php

@@ -5,6 +5,7 @@ namespace App\Services;
 use App\Models\User;
 use App\Models\User;
 use App\Models\PersonalAccessToken;
 use App\Models\PersonalAccessToken;
 use App\Models\UserAccessLog;
 use App\Models\UserAccessLog;
+use App\Enums\UserStatusEnum;
 use App\Enums\UserTypeEnum;
 use App\Enums\UserTypeEnum;
 use Carbon\Carbon;
 use Carbon\Carbon;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Auth;
@@ -24,6 +25,14 @@ class AuthService
             return null;
             return null;
         }
         }
 
 
+        if ($user->type === UserTypeEnum::ASSOCIADO && $user->status === UserStatusEnum::REFUSED) {
+            return ["error" => "refused"];
+        }
+
+        if ($user->type === UserTypeEnum::ASSOCIADO && is_null($user->first_access_completed_at)) {
+            return ["error" => "first_access_required"];
+        }
+
         Auth::login($user);
         Auth::login($user);
 
 
         if ($user->type->value !== $tipo) {
         if ($user->type->value !== $tipo) {

+ 3 - 0
app/Services/DashboardService.php

@@ -2,11 +2,13 @@
 
 
 namespace App\Services;
 namespace App\Services;
 
 
+use App\Enums\UserDependentStatusEnum;
 use App\Enums\UserStatusEnum;
 use App\Enums\UserStatusEnum;
 use App\Enums\UserTypeEnum;
 use App\Enums\UserTypeEnum;
 use App\Models\PartnerAgreement;
 use App\Models\PartnerAgreement;
 use App\Models\User;
 use App\Models\User;
 use App\Models\UserAccessLog;
 use App\Models\UserAccessLog;
+use App\Models\UserDependent;
 use App\Models\Appointment;
 use App\Models\Appointment;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Auth;
 
 
@@ -23,6 +25,7 @@ class DashboardService
         ->count(),
         ->count(),
       'novos_mes'           => PartnerAgreement::whereMonth('created_at', now()->month)->whereYear('created_at', now()->year)->count(),
       'novos_mes'           => PartnerAgreement::whereMonth('created_at', now()->month)->whereYear('created_at', now()->year)->count(),
       'associados_pendentes' => User::where('type', UserTypeEnum::ASSOCIADO)->where('status', UserStatusEnum::PENDING)->count(),
       'associados_pendentes' => User::where('type', UserTypeEnum::ASSOCIADO)->where('status', UserStatusEnum::PENDING)->count(),
+      'dependentes_pendentes' => UserDependent::where('status', UserDependentStatusEnum::PENDING)->count(),
       'ultimos_acessos'     => UserAccessLog::distinct('user_id')->count('user_id'),
       'ultimos_acessos'     => UserAccessLog::distinct('user_id')->count('user_id'),
       'associados_com_acesso_app' => UserAccessLog::whereHas('user', fn ($q) => $q->where('type', UserTypeEnum::ASSOCIADO))
       'associados_com_acesso_app' => UserAccessLog::whereHas('user', fn ($q) => $q->where('type', UserTypeEnum::ASSOCIADO))
         ->distinct('user_id')
         ->distinct('user_id')

+ 173 - 0
app/Services/FirstAccessService.php

@@ -0,0 +1,173 @@
+<?php
+
+namespace App\Services;
+
+use App\Enums\LanguageEnum;
+use App\Enums\UserStatusEnum;
+use App\Enums\UserTypeEnum;
+use App\Models\User;
+use Illuminate\Contracts\Encryption\DecryptException;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Facades\Crypt;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Validation\ValidationException;
+
+class FirstAccessService
+{
+    public const STATUS_NOT_FOUND = 'not_found';
+    public const STATUS_PENDING   = 'pending_first_access';
+    public const STATUS_DONE      = 'already_done';
+
+    private const TOKEN_TTL_MINUTES = 15;
+
+    private const PROFILE_FIELDS = ['name', 'cpf', 'email', 'phone', 'position_id', 'sector_id'];
+
+    public function __construct(protected MediaService $mediaService) {}
+
+    /**
+     * Consulta um crachá e diz se o associado pode seguir para o formulário de primeiro acesso.
+     */
+    public function check(string $registration): array
+    {
+        $user = $this->findByRegistration($registration);
+
+        if ($user?->first_access_completed_at) {
+            return ['status' => self::STATUS_DONE];
+        }
+
+        return [
+            'status'         => $user ? self::STATUS_PENDING : self::STATUS_NOT_FOUND,
+            'token'          => $this->issueToken($registration),
+            'registration'   => $registration,
+            'user'           => $user ? $this->prefill($user) : null,
+            'missing_fields' => $user ? $this->missingFields($user) : self::PROFILE_FIELDS,
+        ];
+    }
+
+    /**
+     * Conclui o primeiro acesso: cria o associado (ou completa o existente), define a senha e a foto.
+     */
+    public function register(array $data, UploadedFile $photo): User
+    {
+        $registration = $data['registration'];
+
+        $this->assertValidToken($data['token'] ?? null, $registration);
+
+        $user = $this->findByRegistration($registration);
+
+        if ($user?->first_access_completed_at) {
+            throw ValidationException::withMessages([
+                'registration' => __('messages.first_access.already_done'),
+            ]);
+        }
+
+        return DB::transaction(function () use ($data, $photo, $registration, $user): User {
+            if ($user) {
+                // Associado já existente (importação ou landing page): completa apenas o que falta,
+                // sem sobrescrever o que a administração já cadastrou, e mantém o status atual.
+                foreach (self::PROFILE_FIELDS as $field) {
+                    if ($this->isEmpty($user->{$field})) {
+                        $user->{$field} = $data[$field];
+                    }
+                }
+            } else {
+                $user = new User([
+                    'registration' => $registration,
+                    'type'         => UserTypeEnum::ASSOCIADO,
+                    'status'       => UserStatusEnum::PENDING,
+                    'language'     => LanguageEnum::PORTUGUESE,
+                ]);
+
+                foreach (self::PROFILE_FIELDS as $field) {
+                    $user->{$field} = $data[$field];
+                }
+            }
+
+            $user->password                  = $data['password'];
+            $user->first_access_completed_at = now();
+            $user->save();
+
+            $this->mediaService->uploadUserAvatar($photo, $user);
+
+            return $user->fresh();
+        });
+    }
+
+    private function findByRegistration(string $registration): ?User
+    {
+        return User::where('type', UserTypeEnum::ASSOCIADO)
+            ->where('registration', $registration)
+            ->first();
+    }
+
+    private function prefill(User $user): array
+    {
+        return [
+            'name'        => $user->name,
+            'cpf'         => $user->cpf,
+            'email'       => $user->email,
+            'phone'       => $user->phone,
+            'position_id' => $user->position_id,
+            'sector_id'   => $user->sector_id,
+            'photo_url'   => $user->photo_path
+                ? Storage::disk('s3')->temporaryUrl($user->photo_path, now()->addHours(24))
+                : null,
+        ];
+    }
+
+    private function missingFields(User $user): array
+    {
+        $missing = array_values(array_filter(
+            self::PROFILE_FIELDS,
+            fn(string $field): bool => $this->isEmpty($user->{$field}),
+        ));
+
+        if (!$user->photo_path) {
+            $missing[] = 'photo';
+        }
+
+        return $missing;
+    }
+
+    private function isEmpty(mixed $value): bool
+    {
+        return $value === null || $value === '';
+    }
+
+    private function issueToken(string $registration): string
+    {
+        return Crypt::encryptString(json_encode([
+            'registration' => $registration,
+            'expires_at'   => now()->addMinutes(self::TOKEN_TTL_MINUTES)->timestamp,
+        ]));
+    }
+
+    /**
+     * Garante que o formulário enviado corresponde ao crachá consultado no passo anterior.
+     */
+    private function assertValidToken(?string $token, string $registration): void
+    {
+        $invalid = ValidationException::withMessages([
+            'token' => __('messages.first_access.invalid_token'),
+        ]);
+
+        if (!$token) {
+            throw $invalid;
+        }
+
+        try {
+            $payload = json_decode(Crypt::decryptString($token), true);
+        } catch (DecryptException) {
+            throw $invalid;
+        }
+
+        if (
+            !is_array($payload) ||
+            ($payload['registration'] ?? null) !== $registration ||
+            ($payload['expires_at'] ?? 0) < now()->timestamp
+        ) {
+            throw $invalid;
+        }
+    }
+}

+ 11 - 10
app/Services/LandingPageService.php

@@ -13,16 +13,17 @@ class LandingPageService
     public function registerUser(array $data): User
     public function registerUser(array $data): User
     {
     {
         return User::create([
         return User::create([
-            'name'           => $data['name'],
-            'email'          => $data['email'],
-            'cpf'            => $data['cpf'],
-            'phone'          => $data['phone'],
-            'position_label' => $data['position_label'],
-            'sector_label'   => $data['sector_label'],
-            'status'         => UserStatusEnum::PENDING,
-            'type'           => UserTypeEnum::ASSOCIADO,
-            'language'       => LanguageEnum::PORTUGUESE,
-            'password'       => bcrypt(Str::random(32)),
+            'name'         => $data['name'],
+            'email'        => $data['email'],
+            'cpf'          => $data['cpf'],
+            'registration' => $data['registration'],
+            'phone'        => $data['phone'],
+            'position_id'  => $data['position_id'],
+            'sector_id'    => $data['sector_id'],
+            'status'       => UserStatusEnum::PENDING,
+            'type'         => UserTypeEnum::ASSOCIADO,
+            'language'     => LanguageEnum::PORTUGUESE,
+            'password'     => bcrypt(Str::random(32)),
         ]);
         ]);
     }
     }
 }
 }

+ 50 - 0
app/Services/UserDependentService.php

@@ -2,8 +2,10 @@
 
 
 namespace App\Services;
 namespace App\Services;
 
 
+use App\Enums\UserDependentStatusEnum;
 use App\Models\UserDependent;
 use App\Models\UserDependent;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Pagination\LengthAwarePaginator;
 
 
 class UserDependentService
 class UserDependentService
 {
 {
@@ -14,6 +16,28 @@ class UserDependentService
             ->get();
             ->get();
     }
     }
 
 
+    public function getAllPaginated(array $filters = [], int $perPage = 10): LengthAwarePaginator
+    {
+        $query = UserDependent::with(['responsibleUser.position', 'responsibleUser.sector'])
+            ->orderBy('created_at', 'asc');
+
+        if (!empty($filters['status'])) {
+            $query->where('status', $filters['status']);
+        }
+
+        if (!empty($filters['search'])) {
+            $term = '%' . mb_strtolower($filters['search']) . '%';
+            $query->where(function ($q) use ($term) {
+                $q->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term])
+                  ->orWhereHas('responsibleUser', function ($q) use ($term) {
+                      $q->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]);
+                  });
+            });
+        }
+
+        return $query->paginate($perPage);
+    }
+
     public function findById(int $id): ?UserDependent
     public function findById(int $id): ?UserDependent
     {
     {
         return UserDependent::find($id);
         return UserDependent::find($id);
@@ -21,9 +45,35 @@ class UserDependentService
 
 
     public function create(array $data): UserDependent
     public function create(array $data): UserDependent
     {
     {
+        $data['status'] = UserDependentStatusEnum::PENDING->value;
+
         return UserDependent::create($data);
         return UserDependent::create($data);
     }
     }
 
 
+    public function approve(int $id): ?UserDependent
+    {
+        $model = $this->findById($id);
+
+        if (!$model) {
+            return null;
+        }
+
+        $model->update(['status' => UserDependentStatusEnum::APPROVED]);
+        return $model->fresh();
+    }
+
+    public function refuse(int $id): ?UserDependent
+    {
+        $model = $this->findById($id);
+
+        if (!$model) {
+            return null;
+        }
+
+        $model->update(['status' => UserDependentStatusEnum::REFUSED]);
+        return $model->fresh();
+    }
+
     public function update(int $id, array $data): ?UserDependent
     public function update(int $id, array $data): ?UserDependent
     {
     {
         $model = $this->findById($id);
         $model = $this->findById($id);

+ 12 - 0
app/Services/UserService.php

@@ -126,6 +126,18 @@ class UserService
         return $model->fresh();
         return $model->fresh();
     }
     }
 
 
+    public function refuse(int $id): ?User
+    {
+        $model = $this->findById($id);
+        if (!$model) return null;
+
+        $model->update([
+            'status' => UserStatusEnum::REFUSED,
+        ]);
+
+        return $model->fresh();
+    }
+
     public function getUserTypes(): array
     public function getUserTypes(): array
     {
     {
         return UserTypeEnum::toArray();
         return UserTypeEnum::toArray();

+ 22 - 0
database/migrations/2026_07_28_000001_add_on_leave_to_import_logs_table.php

@@ -0,0 +1,22 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('import_logs', function (Blueprint $table) {
+            $table->unsignedInteger('on_leave')->default(0)->after('inactivated');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('import_logs', function (Blueprint $table) {
+            $table->dropColumn('on_leave');
+        });
+    }
+};

+ 28 - 0
database/migrations/2026_07_28_000001_make_email_nullable_in_users_table.php

@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->string('email')->nullable()->change();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->string('email')->nullable(false)->change();
+        });
+    }
+};

+ 25 - 0
database/migrations/2026_07_28_000002_add_approval_fields_to_appointments_table.php

@@ -0,0 +1,25 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('appointments', function (Blueprint $table) {
+            $table->boolean('auto_approved')->default(false)->after('status');
+            $table->foreignId('approved_by_user_id')->nullable()->after('auto_approved')
+                ->constrained('users')->nullOnDelete();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('appointments', function (Blueprint $table) {
+            $table->dropConstrainedForeignId('approved_by_user_id');
+            $table->dropColumn('auto_approved');
+        });
+    }
+};

+ 22 - 0
database/migrations/2026_07_29_000001_add_first_access_completed_at_to_users_table.php

@@ -0,0 +1,22 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->timestamp('first_access_completed_at')->nullable()->after('status');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->dropColumn('first_access_completed_at');
+        });
+    }
+};

+ 6 - 0
database/seeders/PermissionSeeder.php

@@ -173,6 +173,12 @@ class PermissionSeeder extends Seeder
                         "bits" => Permission::CRUD,
                         "bits" => Permission::CRUD,
                         "children" => [],
                         "children" => [],
                     ],
                     ],
+                    [
+                        "scope" => "associado.dependente_aprovacao",
+                        "description" => "Aprovação de Dependentes (Admin)",
+                        "bits" => Permission::VIEW | Permission::EDIT,
+                        "children" => [],
+                    ],
                     [
                     [
                        "scope" => "associado.loja",
                        "scope" => "associado.loja",
                         "description" => "Loja do Associado",
                         "description" => "Loja do Associado",

+ 2 - 0
lang/en/auth.php

@@ -22,6 +22,8 @@ return [
     'unauthorized' => 'Unauthorized',
     'unauthorized' => 'Unauthorized',
     'session_expired' => 'Session expired',
     'session_expired' => 'Session expired',
     'wrong_type' => 'User found, but the selected type is incorrect. Please select the correct login type and try again.',
     'wrong_type' => 'User found, but the selected type is incorrect. Please select the correct login type and try again.',
+    'refused' => 'Your registration was refused. Please contact the administration for more information.',
+    'first_access_required' => 'You have not completed your first access yet. Use the "My First Access" option to finish your registration.',
     'password_reset_sent' => 'Verification code sent to your email.',
     'password_reset_sent' => 'Verification code sent to your email.',
     'password_reset_invalid' => 'Invalid or expired code.',
     'password_reset_invalid' => 'Invalid or expired code.',
     'password_reset_success' => 'Password changed successfully.',
     'password_reset_success' => 'Password changed successfully.',

+ 7 - 1
lang/en/messages.php

@@ -20,6 +20,12 @@ return [
     'not_found'               => 'Record not found',
     'not_found'               => 'Record not found',
     'unauthorized'            => 'Unauthorized action',
     'unauthorized'            => 'Unauthorized action',
     'landing'                 => [
     'landing'                 => [
-        'register_success' => 'Association request sent successfully!',
+        'register_success'   => 'Association request sent successfully!',
+        'registration_taken' => 'There is already an account with this badge. Use the "My First Access" option on the login screen.',
+    ],
+    'first_access'            => [
+        'register_success' => 'Registration completed successfully!',
+        'already_done'     => 'The first access for this badge has already been completed. Please sign in or use "Forgot your password?".',
+        'invalid_token'    => 'Session expired. Please enter your badge again to continue.',
     ],
     ],
 ];
 ];

+ 2 - 0
lang/es/auth.php

@@ -22,6 +22,8 @@ return [
     'unauthorized' => 'No autorizado',
     'unauthorized' => 'No autorizado',
     'session_expired' => 'Sesión caducada',
     'session_expired' => 'Sesión caducada',
     'wrong_type' => 'Usuario encontrado, pero el tipo seleccionado es incorrecto. Seleccione el tipo de inicio de sesión correcto e inténtelo de nuevo.',
     'wrong_type' => 'Usuario encontrado, pero el tipo seleccionado es incorrecto. Seleccione el tipo de inicio de sesión correcto e inténtelo de nuevo.',
+    'refused' => 'Su registro fue rechazado. Comuníquese con la administración para más información.',
+    'first_access_required' => 'Aún no realizó su primer acceso. Use la opción "Mi Primer Acceso" para completar su registro.',
     'password_reset_sent' => 'Código de verificación enviado a su correo electrónico.',
     'password_reset_sent' => 'Código de verificación enviado a su correo electrónico.',
     'password_reset_invalid' => 'Código inválido o caducado.',
     'password_reset_invalid' => 'Código inválido o caducado.',
     'password_reset_success' => 'Contraseña cambiada correctamente.',
     'password_reset_success' => 'Contraseña cambiada correctamente.',

+ 7 - 1
lang/es/messages.php

@@ -20,6 +20,12 @@ return [
     'not_found'               => 'Registro no encontrado',
     'not_found'               => 'Registro no encontrado',
     'unauthorized'            => 'Acción no autorizada',
     'unauthorized'            => 'Acción no autorizada',
     'landing'                 => [
     'landing'                 => [
-        'register_success' => '¡Solicitud de asociación enviada con éxito!',
+        'register_success'   => '¡Solicitud de asociación enviada con éxito!',
+        'registration_taken' => 'Ya existe un registro con esta credencial. Use la opción "Mi Primer Acceso" en el inicio de sesión.',
+    ],
+    'first_access'            => [
+        'register_success' => '¡Registro completado con éxito!',
+        'already_done'     => 'El primer acceso de esta credencial ya fue realizado. Inicie sesión normalmente o use "¿Olvidó su contraseña?".',
+        'invalid_token'    => 'Sesión expirada. Ingrese la credencial nuevamente para continuar.',
     ],
     ],
 ];
 ];

+ 2 - 0
lang/pt/auth.php

@@ -22,6 +22,8 @@ return [
     'unauthorized' => 'Não autorizado',
     'unauthorized' => 'Não autorizado',
     'session_expired' => 'Sessão expirada',
     'session_expired' => 'Sessão expirada',
     'wrong_type' => 'Usuário encontrado, mas o tipo selecionado está incorreto. Selecione o login correto e tente novamente.',
     'wrong_type' => 'Usuário encontrado, mas o tipo selecionado está incorreto. Selecione o login correto e tente novamente.',
+    'refused' => 'Seu cadastro foi recusado. Entre em contato com a administração para mais informações.',
+    'first_access_required' => 'Você ainda não realizou seu primeiro acesso. Use a opção "Meu Primeiro Acesso" para concluir seu cadastro.',
     'password_reset_sent' => 'Código de verificação enviado para seu e-mail.',
     'password_reset_sent' => 'Código de verificação enviado para seu e-mail.',
     'password_reset_invalid' => 'Código inválido ou expirado.',
     'password_reset_invalid' => 'Código inválido ou expirado.',
     'password_reset_success' => 'Senha alterada com sucesso.',
     'password_reset_success' => 'Senha alterada com sucesso.',

+ 7 - 1
lang/pt/messages.php

@@ -20,6 +20,12 @@ return [
     'not_found'               => 'Registro não encontrado',
     'not_found'               => 'Registro não encontrado',
     'unauthorized'            => 'Ação não autorizada',
     'unauthorized'            => 'Ação não autorizada',
     'landing'                 => [
     'landing'                 => [
-        'register_success' => 'Solicitação de associação enviada com sucesso!',
+        'register_success'   => 'Solicitação de associação enviada com sucesso!',
+        'registration_taken' => 'Já existe um cadastro com este crachá. Use a opção "Meu Primeiro Acesso" no login.',
+    ],
+    'first_access'            => [
+        'register_success' => 'Cadastro concluído com sucesso!',
+        'already_done'     => 'O primeiro acesso deste crachá já foi realizado. Faça login normalmente ou use "Esqueceu a senha?".',
+        'invalid_token'    => 'Sessão expirada. Informe o crachá novamente para continuar.',
     ],
     ],
 ];
 ];

+ 1 - 0
routes/authRoutes/appointment.php

@@ -12,6 +12,7 @@ Route::controller(AppointmentController::class)->prefix('appointment')->group(fu
 
 
     Route::get('/admin/counters', 'getAdminCounters')->middleware('permission:agendamento,view');
     Route::get('/admin/counters', 'getAdminCounters')->middleware('permission:agendamento,view');
     Route::get('/admin/list', 'getAdminAppointmentsPaginated')->middleware('permission:agendamento,view');
     Route::get('/admin/list', 'getAdminAppointmentsPaginated')->middleware('permission:agendamento,view');
+    Route::get('/admin/user/{id}', 'getByUser')->middleware('permission:agendamento,view');
     Route::put('/{id}/approve', 'approve')->middleware('permission:agendamento,edit');
     Route::put('/{id}/approve', 'approve')->middleware('permission:agendamento,edit');
     Route::put('/{id}/reject', 'reject')->middleware('permission:agendamento,edit');
     Route::put('/{id}/reject', 'reject')->middleware('permission:agendamento,edit');
 
 

+ 1 - 0
routes/authRoutes/associado_import.php

@@ -5,4 +5,5 @@ use Illuminate\Support\Facades\Route;
 
 
 Route::controller(AssociadoImportController::class)->prefix('user')->group(function () {
 Route::controller(AssociadoImportController::class)->prefix('user')->group(function () {
     Route::post('/import/associados', 'importAndSync')->middleware('permission:config.user,add');
     Route::post('/import/associados', 'importAndSync')->middleware('permission:config.user,add');
+    Route::post('/import/afastamentos', 'importAfastamentos')->middleware('permission:config.user,add');
 });
 });

+ 2 - 0
routes/authRoutes/user.php

@@ -29,4 +29,6 @@ Route::controller(UserController::class)->prefix('user')->group(function () {
     Route::put('/{id}/on-leave', 'setOnLeave')->middleware('permission:config.user,edit');
     Route::put('/{id}/on-leave', 'setOnLeave')->middleware('permission:config.user,edit');
 
 
     Route::put('/{id}/approve', 'approve')->middleware('permission:config.user,edit');
     Route::put('/{id}/approve', 'approve')->middleware('permission:config.user,edit');
+
+    Route::put('/{id}/refuse', 'refuse')->middleware('permission:config.user,edit');
 });
 });

+ 6 - 0
routes/authRoutes/user_dependent.php

@@ -6,11 +6,17 @@ use Illuminate\Support\Facades\Route;
 Route::controller(UserDependentController::class)->prefix('user-dependent')->group(function () {
 Route::controller(UserDependentController::class)->prefix('user-dependent')->group(function () {
     Route::get('/user/{userId}', 'indexByUser')->middleware('permission:associado.dependente,view');
     Route::get('/user/{userId}', 'indexByUser')->middleware('permission:associado.dependente,view');
 
 
+    Route::get('/paginated', 'indexPaginated')->middleware('permission:associado.dependente_aprovacao,view');
+
     Route::post('/', 'store')->middleware('permission:associado.dependente,add');
     Route::post('/', 'store')->middleware('permission:associado.dependente,add');
 
 
     Route::get('/{id}', 'show')->middleware('permission:associado.dependente,view');
     Route::get('/{id}', 'show')->middleware('permission:associado.dependente,view');
 
 
     Route::put('/{id}', 'update')->middleware('permission:associado.dependente,edit');
     Route::put('/{id}', 'update')->middleware('permission:associado.dependente,edit');
 
 
+    Route::put('/{id}/approve', 'approve')->middleware('permission:associado.dependente_aprovacao,edit');
+
+    Route::put('/{id}/refuse', 'refuse')->middleware('permission:associado.dependente_aprovacao,edit');
+
     Route::delete('/{id}', 'destroy')->middleware('permission:associado.dependente,delete');
     Route::delete('/{id}', 'destroy')->middleware('permission:associado.dependente,delete');
 });
 });

+ 16 - 0
routes/noAuthRoutes/first_access.php

@@ -0,0 +1,16 @@
+<?php
+
+use App\Http\Controllers\FirstAccessController;
+use App\Http\Controllers\PositionController;
+use App\Http\Controllers\SectorController;
+use Illuminate\Support\Facades\Route;
+
+Route::middleware('throttle:10,1')->group(function () {
+    Route::controller(FirstAccessController::class)->prefix('first-access')->group(function () {
+        Route::post('/check', 'check');
+        Route::post('/register', 'register');
+    });
+});
+
+Route::get('/public/positions', [PositionController::class, 'index']);
+Route::get('/public/sectors', [SectorController::class, 'index']);