Explorar el Código

feat: :sparkles: feat (parceiro / convenio opcao de excluir) adicionada opcao de excluir parceiro / convenio

foi adicionada opcao para excluir parceiro e convenio, sempre mantendo historico, e com opcao para restaurar registros excluidos

fase:dev | origin:escopo
Gustavo Zanatta hace 9 horas
padre
commit
1983581864

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

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

+ 25 - 0
app/Http/Controllers/PartnerAgreementController.php

@@ -31,6 +31,31 @@ class PartnerAgreementController extends Controller
         return $this->successResponse(payload: PartnerAgreementListResource::collection($items));
     }
 
+    public function indexTrashed(Request $request): JsonResponse
+    {
+        $items = $this->service->getTrashed($request->only(['type']));
+
+        return $this->successResponse(payload: $items->map(fn($item) => [
+            'id'           => $item->id,
+            'company_name' => $item->company_name,
+            'deleted_at'   => $item->deleted_at?->format('Y-m-d H:i:s'),
+        ]));
+    }
+
+    public function restore(int $id): JsonResponse
+    {
+        $item = $this->service->restore($id);
+
+        if (!$item) {
+            return $this->errorResponse(message: __('messages.not_found'), code: 404);
+        }
+
+        return $this->successResponse(
+            payload: new PartnerAgreementListResource($item),
+            message: __('messages.updated'),
+        );
+    }
+
     public function indexPaginated(Request $request): JsonResponse
     {
         $filters = $request->only(['search', 'expires_in_days', 'created_month', 'type']);

+ 7 - 4
app/Http/Requests/AppointmentRequest.php

@@ -10,10 +10,13 @@ class AppointmentRequest extends FormRequest
 {
     public function rules(): array
     {
+        $partnerExists = Rule::exists('partner_agreements', 'id')->whereNull('deleted_at');
+        $serviceExists = Rule::exists('partner_agreement_services', 'id')->whereNull('deleted_at');
+
         $rules = [
             'user_id'                      => 'sometimes|integer|exists:users,id',
-            'partner_agreement_id'         => 'sometimes|integer|exists:partner_agreements,id',
-            'partner_agreement_service_id' => 'sometimes|integer|exists:partner_agreement_services,id',
+            'partner_agreement_id'         => ['sometimes', 'integer', $partnerExists],
+            'partner_agreement_service_id' => ['sometimes', 'integer', $serviceExists],
             'date'                         => 'sometimes|date',
             'time'                         => 'sometimes|date_format:H:i',
             'observations'                 => 'sometimes|nullable|string',
@@ -22,8 +25,8 @@ class AppointmentRequest extends FormRequest
 
         if ($this->isMethod('post')) {
             $rules['user_id']                      = 'required|integer|exists:users,id';
-            $rules['partner_agreement_id']         = 'required|integer|exists:partner_agreements,id';
-            $rules['partner_agreement_service_id'] = 'required|integer|exists:partner_agreement_services,id';
+            $rules['partner_agreement_id']         = ['required', 'integer', $partnerExists];
+            $rules['partner_agreement_service_id'] = ['required', 'integer', $serviceExists];
         }
 
         return $rules;

+ 2 - 2
app/Http/Requests/PartnerAgreementRequest.php

@@ -12,11 +12,11 @@ class PartnerAgreementRequest extends FormRequest
 {
     public function rules(): array
     {
-        $cnpjUnique = 'unique:partner_agreements,cnpj';
+        $cnpjUnique = Rule::unique('partner_agreements', 'cnpj')->whereNull('deleted_at');
         if ($this->isMethod('put')) {
             $partnerId = (int) $this->route('id') ?: PartnerAgreement::where('user_id', auth()->id())->value('id');
             if ($partnerId) {
-                $cnpjUnique = 'unique:partner_agreements,cnpj,' . $partnerId;
+                $cnpjUnique->ignore($partnerId);
             }
         }
         $rules = [

+ 2 - 2
app/Models/Appointment.php

@@ -35,11 +35,11 @@ class Appointment extends Model
 
     public function partnerAgreement(): BelongsTo
     {
-        return $this->belongsTo(PartnerAgreement::class);
+        return $this->belongsTo(PartnerAgreement::class)->withTrashed();
     }
 
     public function partnerAgreementService(): BelongsTo
     {
-        return $this->belongsTo(PartnerAgreementService::class);
+        return $this->belongsTo(PartnerAgreementService::class)->withTrashed();
     }
 }

+ 1 - 1
app/Models/PartnerAgreementService.php

@@ -28,7 +28,7 @@ class PartnerAgreementService extends Model
 
     public function partnerAgreement(): BelongsTo
     {
-        return $this->belongsTo(PartnerAgreement::class);
+        return $this->belongsTo(PartnerAgreement::class)->withTrashed();
     }
 
     public function category(): BelongsTo

+ 4 - 0
app/Services/AuthService.php

@@ -29,6 +29,10 @@ class AuthService
             return ["error" => "refused"];
         }
 
+        if ($user->type === UserTypeEnum::PARCEIRO && $user->status === UserStatusEnum::INACTIVE) {
+            return ["error" => "inactive"];
+        }
+
         if ($user->type === UserTypeEnum::ASSOCIADO && is_null($user->first_access_completed_at)) {
             return ["error" => "first_access_required"];
         }

+ 3 - 1
app/Services/NotificationService.php

@@ -3,6 +3,7 @@
 namespace App\Services;
 
 use App\Enums\NotificationRecipientEnum;
+use App\Enums\UserStatusEnum;
 use App\Enums\UserTypeEnum;
 use App\Models\Notification;
 use App\Models\NotificationSend;
@@ -130,7 +131,8 @@ class NotificationService
 
         match ($notification->recipient) {
             NotificationRecipientEnum::ASSOCIADO => $query->where('type', UserTypeEnum::ASSOCIADO->value),
-            NotificationRecipientEnum::PARCEIRO  => $query->where('type', UserTypeEnum::PARCEIRO->value),
+            NotificationRecipientEnum::PARCEIRO  => $query->where('type', UserTypeEnum::PARCEIRO->value)
+                                                          ->where('status', '!=', UserStatusEnum::INACTIVE->value),
             default                              => null,
         };
 

+ 45 - 2
app/Services/PartnerAgreementService.php

@@ -2,11 +2,14 @@
 
 namespace App\Services;
 
+use App\Enums\PartnerAgreementStatusEnum;
+use App\Enums\UserStatusEnum;
 use App\Enums\UserTypeEnum;
 use App\Models\PartnerAgreement;
 use App\Models\User;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Hash;
 
 class PartnerAgreementService
@@ -74,7 +77,11 @@ class PartnerAgreementService
             ->where('user_id', $userId)
             ->first();
 
-        if(!$partner) {
+        if (!$partner) {
+          if (PartnerAgreement::onlyTrashed()->where('user_id', $userId)->exists()) {
+            return null;
+          }
+
           $partner = new PartnerAgreement();
           $partner->company_name = 'Novo Parceiro';
           $partner->user_id = $userId;
@@ -135,6 +142,34 @@ class PartnerAgreementService
         return $model->fresh(['category', 'city', 'state', 'logo', 'media']);
     }
 
+    public function getTrashed(array $filters = []): Collection
+    {
+        $query = PartnerAgreement::onlyTrashed()->orderByDesc('deleted_at');
+
+        if (!empty($filters['type'])) {
+            $query->where('type', $filters['type']);
+        }
+
+        return $query->get(['id', 'company_name', 'type', 'deleted_at']);
+    }
+
+    public function restore(int $id): ?PartnerAgreement
+    {
+        $model = PartnerAgreement::onlyTrashed()->find($id);
+
+        if (!$model) {
+            return null;
+        }
+
+        return DB::transaction(function () use ($model): PartnerAgreement {
+            $model->restore();
+            $model->update(['status' => PartnerAgreementStatusEnum::ACTIVE]);
+            $model->user?->update(['status' => UserStatusEnum::ACTIVE]);
+
+            return $model->fresh();
+        });
+    }
+
     public function delete(int $id): bool
     {
         $model = PartnerAgreement::find($id);
@@ -143,6 +178,14 @@ class PartnerAgreementService
             return false;
         }
 
-        return $model->delete();
+        return DB::transaction(function () use ($model): bool {
+            $model->services()->delete();
+
+            $model->user?->update(['status' => UserStatusEnum::INACTIVE]);
+
+            $model->update(['status' => PartnerAgreementStatusEnum::INACTIVE]);
+
+            return (bool) $model->delete();
+        });
     }
 }

+ 2 - 0
app/Traits/ImportsPartners.php

@@ -23,6 +23,8 @@ trait ImportsPartners
 
             if ($partner->trashed()) {
                 $partner->restore();
+                $partner->services()->onlyTrashed()->restore();
+                $partner->user?->update(['status' => UserStatusEnum::ACTIVE]);
                 $wasModified = true;
             }
 

+ 20 - 0
database/migrations/2026_08_10_000002_make_partner_agreements_cnpj_unique_ignore_trashed.php

@@ -0,0 +1,20 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\DB;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        DB::statement('ALTER TABLE partner_agreements DROP CONSTRAINT IF EXISTS partner_agreements_cnpj_unique');
+        DB::statement('DROP INDEX IF EXISTS partner_agreements_cnpj_unique');
+        DB::statement('CREATE UNIQUE INDEX partner_agreements_cnpj_unique ON partner_agreements (cnpj) WHERE deleted_at IS NULL');
+    }
+
+    public function down(): void
+    {
+        DB::statement('DROP INDEX IF EXISTS partner_agreements_cnpj_unique');
+        DB::statement('ALTER TABLE partner_agreements ADD CONSTRAINT partner_agreements_cnpj_unique UNIQUE (cnpj)');
+    }
+};

+ 1 - 0
lang/en/auth.php

@@ -24,6 +24,7 @@ return [
     '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.',
+    'inactive' => 'This access has been deactivated. Please contact the administration for more information.',
     'password_reset_sent' => 'Verification code sent to your email.',
     'password_reset_invalid' => 'Invalid or expired code.',
     'password_reset_success' => 'Password changed successfully.',

+ 1 - 0
lang/es/auth.php

@@ -24,6 +24,7 @@ return [
     '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.',
+    'inactive' => 'Este acceso fue desactivado. Póngase en contacto con la administración para más información.',
     '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_success' => 'Contraseña cambiada correctamente.',

+ 1 - 0
lang/pt/auth.php

@@ -24,6 +24,7 @@ return [
     '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.',
+    'inactive' => 'Este acesso foi desativado. Entre em contato com a administração para mais informações.',
     'password_reset_sent' => 'Código de verificação enviado para seu e-mail.',
     'password_reset_invalid' => 'Código inválido ou expirado.',
     'password_reset_success' => 'Senha alterada com sucesso.',

+ 3 - 0
routes/authRoutes/partner_agreement.php

@@ -14,6 +14,7 @@ Route::controller(PartnerAgreementController::class)->prefix('partner-agreement'
 
     Route::get('/paginated', 'indexPaginated')->middleware('permission:parceiro.convenio,view');
     Route::get('/expiring',  'indexExpiring') ->middleware('permission:parceiro.convenio,view');
+    Route::get('/trashed',   'indexTrashed')  ->middleware('permission:parceiro.convenio,delete');
 
     Route::post('/', 'store')->middleware('permission:parceiro.convenio,add');
 
@@ -28,6 +29,8 @@ Route::controller(PartnerAgreementController::class)->prefix('partner-agreement'
 
     Route::delete('/{id}', 'destroy')->middleware('permission:parceiro.convenio,delete');
 
+    Route::put('/{id}/restore', 'restore')->middleware('permission:parceiro.convenio,delete');
+
     Route::post('/{id}/logo', 'uploadLogo')->middleware('permission:parceiro.convenio,edit');
     Route::post('/{id}/media', 'uploadMedia')->middleware('permission:parceiro.convenio,edit');
     Route::delete('/{id}/media/{mediaId}', 'deleteMedia')->middleware('permission:parceiro.convenio,edit');