Просмотр исходного кода

Merge branch 'feature/GINC-GAB-suporte' of Softpar/sfp_api_laravel_ginastica_cerebro into development

Gabriel Alves 3 недель назад
Родитель
Сommit
60fa1f4f5c

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

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Services\SupportReplyService;
+use App\Http\Requests\SupportReplyRequest;
+use App\Http\Resources\SupportReplyResource;
+use Illuminate\Http\JsonResponse;
+
+class SupportReplyController extends Controller
+{
+    public function __construct(
+        protected SupportReplyService $service,
+    ) {}
+
+    public function index(int $ticketId): JsonResponse
+    {
+        $replies = $this->service->getByTicket($ticketId);
+        return $this->successResponse(payload: SupportReplyResource::collection($replies));
+    }
+
+    public function store(SupportReplyRequest $request, int $ticketId): JsonResponse
+    {
+        $reply = $this->service->create($ticketId, auth()->id(), $request->validated()['reply']);
+        return $this->successResponse(payload: new SupportReplyResource($reply), message: __('messages.created'), code: 201);
+    }
+
+    public function update(SupportReplyRequest $request, int $ticketId, int $id): JsonResponse
+    {
+        $reply = $this->service->update($ticketId, $id, $request->validated()['reply']);
+        return $this->successResponse(payload: new SupportReplyResource($reply), message: __('messages.updated'));
+    }
+
+    public function destroy(int $ticketId, int $id): JsonResponse
+    {
+        $this->service->delete($ticketId, $id);
+        return $this->successResponse(message: __('messages.deleted'), code: 204);
+    }
+}

+ 92 - 4
app/Http/Controllers/SupportTicketController.php

@@ -15,14 +15,71 @@ public function __construct(
 
     public function index(): JsonResponse
     {
-        $items = $this->service->getAll();
-        return $this->successResponse(payload: SupportTicketResource::collection($items));
+        $user = auth()->user();
+        $query = \App\Models\SupportTicket::query()
+            ->with(['applicantUnit', 'targetUnit'])
+            ->orderBy('created_at', 'desc');
+
+        if ($this->isMatriz($user)) {
+            $query->where(fn($q) => $q->where('origin', '!=', 'unit')->orWhere('scope', '!=', 'internal'));
+        } else {
+            $query->visibleToUnit($user->units()->first()?->id);
+        }
+
+        return $this->successResponse(
+            payload: SupportTicketResource::collection($query->get())
+        );
     }
 
     public function store(SupportTicketRequest $request): JsonResponse
     {
-        $item = $this->service->create($request->validated());
-        return $this->successResponse(payload: new SupportTicketResource($item), message: __('messages.created'), code: 201);
+        $data = $request->validated();
+        $user = auth()->user();
+        $isMatriz = $this->isMatriz($user);
+
+        $data['origin'] = $isMatriz ? 'matriz' : 'unit';
+        $data['applicant_user_id'] = $user->id;
+        $data['responsable_user_id'] = $user->id;
+        $data['applicant_unit_id'] = $isMatriz ? null : $user->units()->first()?->id;
+        $data['status'] = 'in_progress';
+
+        // Broadcast: Matriz para todas as unidades
+        if ($isMatriz && ($data['scope'] ?? null) === 'all') {
+            $batchId = (string) \Illuminate\Support\Str::uuid();
+            $unitIds = \App\Models\Unit::query()->pluck('id');
+            $created = [];
+            foreach ($unitIds as $unitId) {
+                $created[] = $this->service->create(array_merge($data, [
+                    'target_unit_id' => $unitId,
+                    'batch_id' => $batchId,
+                ]));
+            }
+            return $this->successResponse(
+                payload: SupportTicketResource::collection($created),
+                message: __('messages.created'),
+                code: 201
+            );
+        }
+
+        // Resolução de target_unit_id por cenário
+        if ($isMatriz) {
+            if (($data['scope'] ?? null) === 'internal') {
+                $data['target_unit_id'] = null;
+            }
+            // scope='specific' → target_unit_id já veio do request
+        } else {
+            // Franchisee
+            $data['target_unit_id'] = (($data['scope'] ?? null) === 'internal')
+                ? $user->units()->first()?->id
+                : null; // 'specific' do Franchisee = "para Matriz"
+        }
+
+        $item = $this->service->create($data);
+        return $this->successResponse(
+            payload: new SupportTicketResource($item),
+            message: __('messages.created'),
+            code: 201
+        );
     }
 
     public function show(int $id): JsonResponse
@@ -33,13 +90,44 @@ public function show(int $id): JsonResponse
 
     public function update(SupportTicketRequest $request, int $id): JsonResponse
     {
+        $user = auth()->user();
+        $item = $this->service->findById($id);
+
+        if (!$this->canManage($user, $item)) {
+            return $this->errorResponse(message: __('messages.unauthorized'), code: 403);
+        }
+
         $item = $this->service->update($id, $request->validated());
         return $this->successResponse(payload: new SupportTicketResource($item), message: __('messages.updated'));
     }
 
     public function destroy(int $id): JsonResponse
     {
+        $user = auth()->user();
+        $item = $this->service->findById($id);
+
+        if (!$this->canManage($user, $item)) {
+            return $this->errorResponse(message: __('messages.unauthorized'), code: 403);
+        }
+
         $this->service->delete($id);
         return $this->successResponse(message: __('messages.deleted'), code: 204);
     }
+
+    private function isMatriz(\App\Models\User $user): bool
+    {
+        return $user->user_type === 'ADMIN';
+    }
+
+    private function canManage(\App\Models\User $user, \App\Models\SupportTicket $ticket): bool
+    {
+        if ($this->isMatriz($user)) {
+            return true;
+        }
+
+        // Franchisee só pode gerenciar tickets internos que ela mesma criou
+        return $ticket->origin === 'unit'
+            && $ticket->scope === 'internal'
+            && $ticket->applicant_unit_id === $user->units()->first()?->id;
+    }
 }

+ 20 - 0
app/Http/Requests/SupportReplyRequest.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class SupportReplyRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        return [
+            'reply' => 'required|string',
+        ];
+    }
+}

+ 10 - 21
app/Http/Requests/SupportTicketRequest.php

@@ -8,27 +8,16 @@ class SupportTicketRequest extends FormRequest
 {
     public function rules(): array
     {
-        $rules = [
-            // Add your validation rules here
-            //'field' => 'sometimes|string|max:255',
-        ];
-
-        // Different rules for creation
-        //if ($this->isMethod('POST')) {
-            // Make fields required if needed
-            // $rules['field'] = 'required|string|max:255';
-        //}
+        $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH');
 
-        return $rules;
+        return [
+            'title'       => ($isUpdate ? 'sometimes' : 'required') . '|string|max:255',
+            'severity'    => ($isUpdate ? 'sometimes' : 'required') . '|string|in:alta,normal,baixa',
+            'scope'       => ($isUpdate ? 'sometimes' : 'required') . '|string|in:all,internal,specific',
+            'target_unit_id' => 'nullable|integer|exists:units,id',
+            'sector'      => 'nullable|string|max:255',
+            'description' => 'nullable|string',
+            'status'      => 'sometimes|string|in:in_progress,resolved,unresolved',
+        ];
     }
-
-    /**
-    * Add custom messages when needed
-    * public function messages(): array
-    * {
-    *   return [
-    *        'field.required' => __('message.algo'),
-    *    ];
-    * }
-    */
 }

+ 21 - 0
app/Http/Resources/SupportReplyResource.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Carbon\Carbon;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class SupportReplyResource extends JsonResource
+{
+    public function toArray(Request $request): array
+    {
+        return [
+            'id'         => $this->id,
+            'ticket_id'  => $this->ticket_id,
+            'reply'      => $this->reply,
+            'user_name'  => $this->user?->name,
+            'created_at' => Carbon::parse($this->created_at)->format('d/m/Y H:i'),
+        ];
+    }
+}

+ 17 - 19
app/Http/Resources/SupportTicketResource.php

@@ -2,7 +2,6 @@
 
 namespace App\Http\Resources;
 
-use Carbon\Carbon;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
@@ -10,27 +9,26 @@
 
 class SupportTicketResource extends JsonResource
 {
-    /**
-     * Transform the resource into an array.
-     *
-     * @return array<string, mixed>
-     */
     public function toArray(Request $request): array
     {
         return [
-            'id' => $this->id,
-            'name' => $this->name,
-            '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'),
-            // Add your fields here
-
-            // Conditional fields
-            // $this->mergeWhen($request->user()?->isAdmin(), [
-            //     'internal_notes' => $this->internal_notes,
-            // ]),
-
-            // Relationships
-            // 'user' => new UserResource($this->whenLoaded('user')),
+            'id'                  => $this->id,
+            'title'               => $this->title,
+            'description'         => $this->description,
+            'severity'            => $this->severity,
+            'scope'               => $this->scope,
+            'sector'              => $this->sector,
+            'status'              => $this->status,
+            'origin'              => $this->origin,
+            'applicant_user_id'   => $this->applicant_user_id,
+            'responsable_user_id' => $this->responsable_user_id,
+            'applicant_unit_id'   => $this->applicant_unit_id,
+            'target_unit_id'      => $this->target_unit_id,
+            'applicant_unit_name' => $this->whenLoaded('applicantUnit', fn() => $this->applicantUnit?->fantasy_name),
+            'target_unit_name'    => $this->whenLoaded('targetUnit', fn() => $this->targetUnit?->fantasy_name),
+            'batch_id'            => $this->batch_id,
+            'created_at'          => $this->created_at?->format('d/m/Y H:i'),
+            'updated_at'          => $this->updated_at?->format('d/m/Y H:i'),
         ];
     }
 

+ 31 - 0
app/Models/SupportReply.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+
+class SupportReply extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $table = 'support_replies';
+
+    protected $guarded = ['id'];
+
+    protected $casts = [
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+    ];
+
+    public function ticket()
+    {
+        return $this->belongsTo(SupportTicket::class, 'ticket_id');
+    }
+
+    public function user()
+    {
+        return $this->belongsTo(User::class, 'user_id');
+    }
+}

+ 26 - 2
app/Models/SupportTicket.php

@@ -53,10 +53,34 @@ class SupportTicket extends Model
 
     // Relationships
 
-    // Business Logic Methods
+    public function applicantUnit()
+    {
+        return $this->belongsTo(\App\Models\Unit::class, 'applicant_unit_id');
+    }
 
-    // Custom Finders
+    public function targetUnit()
+    {
+        return $this->belongsTo(\App\Models\Unit::class, 'target_unit_id');
+    }
+
+    public function applicantUser()
+    {
+        return $this->belongsTo(\App\Models\User::class, 'applicant_user_id');
+    }
+
+    public function responsableUser()
+    {
+        return $this->belongsTo(\App\Models\User::class, 'responsable_user_id');
+    }
 
     // Query Scopes
 
+    public function scopeVisibleToUnit($query, int $unitId)
+    {
+        return $query->where(function ($q) use ($unitId) {
+            $q->where('target_unit_id', $unitId)
+              ->orWhere('applicant_unit_id', $unitId);
+        });
+    }
+
 }

+ 41 - 0
app/Services/SupportReplyService.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\SupportReply;
+use Illuminate\Database\Eloquent\Collection;
+
+class SupportReplyService
+{
+    public function getByTicket(int $ticketId): Collection
+    {
+        return SupportReply::with('user')
+            ->where('ticket_id', $ticketId)
+            ->orderBy('created_at', 'asc')
+            ->get();
+    }
+
+    public function create(int $ticketId, int $userId, string $reply): SupportReply
+    {
+        $model = SupportReply::create([
+            'ticket_id' => $ticketId,
+            'user_id'   => $userId,
+            'reply'     => $reply,
+        ]);
+
+        return $model->load('user');
+    }
+
+    public function update(int $ticketId, int $id, string $reply): SupportReply
+    {
+        $model = SupportReply::where('ticket_id', $ticketId)->findOrFail($id);
+        $model->update(['reply' => $reply]);
+        return $model->load('user');
+    }
+
+    public function delete(int $ticketId, int $id): bool
+    {
+        $model = SupportReply::where('ticket_id', $ticketId)->findOrFail($id);
+        return $model->delete();
+    }
+}

+ 33 - 0
database/migrations/2026_05_18_165110_modify_support_tickets_add_scope_sector.php

@@ -0,0 +1,33 @@
+<?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('support_tickets', function (Blueprint $table) {
+            $table->unsignedBigInteger('unit_id')->nullable()->change();
+            $table->unsignedBigInteger('student_id')->nullable()->change();
+            $table->unsignedBigInteger('support_status_id')->nullable()->change();
+            $table->unsignedBigInteger('responsable_user_id')->nullable()->change();
+
+            $table->string('scope')->nullable()->after('unit_id');
+            $table->string('sector')->nullable()->after('description');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('support_tickets', function (Blueprint $table) {
+            $table->dropColumn(['scope', 'sector']);
+
+            $table->unsignedBigInteger('unit_id')->nullable(false)->change();
+            $table->unsignedBigInteger('student_id')->nullable(false)->change();
+            $table->unsignedBigInteger('support_status_id')->nullable(false)->change();
+            $table->unsignedBigInteger('responsable_user_id')->nullable(false)->change();
+        });
+    }
+};

+ 27 - 0
database/migrations/2026_05_18_170854_modify_support_tickets_replace_status.php

@@ -0,0 +1,27 @@
+<?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('support_tickets', function (Blueprint $table) {
+            $table->dropForeign(['support_status_id']);
+            $table->dropColumn('support_status_id');
+
+            $table->string('status')->default('in_progress')->after('description');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('support_tickets', function (Blueprint $table) {
+            $table->dropColumn('status');
+
+            $table->foreignId('support_status_id')->nullable()->constrained('support_statuses');
+        });
+    }
+};

+ 28 - 0
database/migrations/2026_05_18_173440_create_support_replies_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
+{
+    public function up(): void
+    {
+        Schema::create('support_replies', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('ticket_id')->constrained('support_tickets')->cascadeOnDelete();
+            $table->foreignId('user_id')->constrained('users');
+            $table->text('reply');
+            $table->timestamps();
+            $table->softDeletes();
+
+            $table->index('ticket_id');
+            $table->index('user_id');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('support_replies');
+    }
+};

+ 34 - 0
database/migrations/2026_05_19_120000_alter_support_tickets_add_origin_targets_batch.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Facades\DB;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('support_tickets', function (Blueprint $table) {
+            $table->enum('origin', ['matriz', 'unit'])->default('matriz')->after('id');
+            $table->unsignedBigInteger('applicant_unit_id')->nullable()->after('origin');
+            $table->unsignedBigInteger('target_unit_id')->nullable()->after('applicant_unit_id');
+            $table->uuid('batch_id')->nullable()->index()->after('target_unit_id');
+
+            $table->foreign('applicant_unit_id')->references('id')->on('units')->nullOnDelete();
+            $table->foreign('target_unit_id')->references('id')->on('units')->nullOnDelete();
+        });
+
+        // Backfill: tickets legacy assumem origem matriz e target = unit_id antigo
+        DB::statement("UPDATE support_tickets SET origin = 'matriz', target_unit_id = unit_id WHERE origin IS NULL OR target_unit_id IS NULL");
+    }
+
+    public function down(): void
+    {
+        Schema::table('support_tickets', function (Blueprint $table) {
+            $table->dropForeign(['applicant_unit_id']);
+            $table->dropForeign(['target_unit_id']);
+            $table->dropColumn(['origin', 'applicant_unit_id', 'target_unit_id', 'batch_id']);
+        });
+    }
+};

+ 11 - 0
routes/authRoutes/support_reply.php

@@ -0,0 +1,11 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\SupportReplyController;
+
+Route::controller(SupportReplyController::class)->prefix('support-ticket/{ticketId}/replies')->group(function () {
+    Route::get('/', 'index')->middleware('permission:support-ticket,view');
+    Route::post('/', 'store')->middleware('permission:support-ticket,add');
+    Route::put('/{id}', 'update')->middleware('permission:support-ticket,edit');
+    Route::delete('/{id}', 'destroy')->middleware('permission:support-ticket,delete');
+});