Prechádzať zdrojové kódy

crud agendamentos basico (sem datas e sem oportunidades)

Gustavo Zanatta 3 týždňov pred
rodič
commit
3d2d5b51a4

+ 70 - 0
app/Http/Controllers/ScheduleController.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\ScheduleRequest;
+use App\Http\Resources\ScheduleResource;
+use App\Services\ScheduleService;
+use Illuminate\Http\JsonResponse;
+
+class ScheduleController extends Controller
+{
+    protected $scheduleService;
+
+    public function __construct(ScheduleService $scheduleService)
+    {
+        $this->scheduleService = $scheduleService;
+    }
+
+    public function index(): JsonResponse
+    {
+        $schedules = $this->scheduleService->getAll();
+        return $this->successResponse(
+            ScheduleResource::collection($schedules),
+        );
+    }
+
+    public function store(ScheduleRequest $request): JsonResponse
+    {
+        try {
+            $schedule = $this->scheduleService->create($request->validated());
+            return $this->successResponse(
+                payload: new ScheduleResource($schedule),
+                message: __("messages.created"),
+                code: 201,
+            );
+        } catch (\Exception $e) {
+            return $this->errorResponse($e->getMessage(), 422);
+        }
+    }
+
+    public function show(string $id): JsonResponse
+    {
+        $schedule = $this->scheduleService->getById($id);
+        return $this->successResponse(
+            new ScheduleResource($schedule),
+        );
+    }
+
+    public function update(ScheduleRequest $request, string $id): JsonResponse
+    {
+        try {
+            $schedule = $this->scheduleService->update($id, $request->validated());
+            return $this->successResponse(
+                payload: new ScheduleResource($schedule),
+                message: __("messages.updated"),
+            );
+        } catch (\Exception $e) {
+            return $this->errorResponse($e->getMessage(), 422);
+        }
+    }
+
+    public function destroy(string $id): JsonResponse
+    {
+        $this->scheduleService->delete($id);
+        return $this->successResponse(
+            message: __("messages.deleted"),
+            code: 204,
+        );
+    }
+}

+ 1 - 1
app/Http/Requests/ProviderWorkingDayRequest.php

@@ -17,7 +17,7 @@ class ProviderWorkingDayRequest extends FormRequest
     {
         return [
             'provider_id' => ['required', 'exists:providers,id'],
-            'day' => ['required', 'integer', 'min:1', 'max:7'],
+            'day' => ['required', 'integer', 'min:0', 'max:6'],
             'period' => ['required', Rule::in([WorkingPeriodEnum::MORNING->value, WorkingPeriodEnum::AFTERNOON->value])],
         ];
     }

+ 64 - 0
app/Http/Requests/ScheduleRequest.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ScheduleRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        $rules = [
+            'client_id' => 'sometimes|required|exists:clients,id',
+            'provider_id' => 'sometimes|required|exists:providers,id',
+            'address_id' => 'sometimes|required|exists:addresses,id',
+            'date' => 'sometimes|required|date',
+            'period_type' => 'sometimes|required|in:2,4,6,8',
+            'schedule_type' => 'sometimes|in:default,custom',
+            'start_time' => 'sometimes|required|date_format:H:i:s',
+            'end_time' => 'sometimes|required|date_format:H:i:s|after:start_time',
+            'status' => 'sometimes|in:pending,accepted,rejected,paid,cancelled,started,finished',
+            'code_verified' => 'sometimes|boolean',
+        ];
+
+        if ($this->isMethod('POST')) {
+            $rules['client_id'] = 'required|exists:clients,id';
+            $rules['provider_id'] = 'required|exists:providers,id';
+            $rules['address_id'] = 'required|exists:addresses,id';
+            $rules['date'] = 'required|date';
+            $rules['period_type'] = 'required|in:2,4,6,8';
+            $rules['start_time'] = 'required|date_format:H:i';
+            $rules['end_time'] = 'required|date_format:H:i|after:start_time';
+            $rules['status'] = 'in:pending';
+        }
+
+        return $rules;
+    }
+
+    public function messages(): array
+    {
+        return [
+            'client_id.required' => 'O cliente é obrigatório.',
+            'client_id.exists' => 'Cliente não encontrado.',
+            'provider_id.required' => 'O prestador é obrigatório.',
+            'provider_id.exists' => 'Prestador não encontrado.',
+            'address_id.required' => 'O endereço é obrigatório.',
+            'address_id.exists' => 'Endereço não encontrado.',
+            'date.required' => 'A data é obrigatória.',
+            'date.date' => 'Data inválida.',
+            'period_type.required' => 'O período é obrigatório.',
+            'period_type.in' => 'Período inválido.',
+            'start_time.required' => 'O horário de início é obrigatório.',
+            'start_time.date_format' => 'Formato de horário inválido.',
+            'end_time.required' => 'O horário de término é obrigatório.',
+            'end_time.date_format' => 'Formato de horário inválido.',
+            'end_time.after' => 'O horário de término deve ser após o horário de início.',
+            'status.in' => 'Status inválido.',
+        ];
+    }
+}

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

@@ -26,6 +26,9 @@ class AddressResource extends JsonResource
             'instructions' => $this->instructions,
             'city_id' => $this->city_id,
             'state_id' => $this->state_id,
+            'address_full' => "{$this->address}" .
+                ($this->complement ? " - {$this->complement}" : '') .
+                ($this->city ? " , {$this->city->name}/{$this->state?->code}" : ''), 
             'city' => $this->whenLoaded('city'),
             'state' => $this->whenLoaded('state'),
             'address_type' => $this->address_type,

+ 39 - 0
app/Http/Resources/ScheduleResource.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class ScheduleResource extends JsonResource
+{
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'client_id' => $this->client_id,
+            'client_name' => $this->client?->user?->name,
+            'provider_id' => $this->provider_id,
+            'provider_name' => $this->provider?->user?->name,
+            'address_id' => $this->address_id,
+            'address_full' => $this->address ? 
+                "{$this->address->address}" . 
+                ($this->address->complement ? " - {$this->address->complement}" : '') .
+                " , {$this->address->city->name}/{$this->address->state->code}" 
+                : null,
+            'address' => new AddressResource($this->whenLoaded('address')),
+            'date' => $this->date?->format('Y-m-d'),
+            'period_type' => $this->period_type,
+            'schedule_type' => $this->schedule_type,
+            'start_time' => $this->start_time,
+            'end_time' => $this->end_time,
+            'status' => $this->status,
+            'total_amount' => $this->total_amount,
+            'code' => $this->code,
+            'code_verified' => $this->code_verified,
+            'created_at' => $this->created_at?->toISOString(),
+            'updated_at' => $this->updated_at?->toISOString(),
+            'deleted_at' => $this->deleted_at?->toISOString(),
+        ];
+    }
+}

+ 48 - 0
app/Models/Schedule.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class Schedule extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $fillable = [
+        'client_id',
+        'provider_id',
+        'address_id',
+        'date',
+        'period_type',
+        'schedule_type',
+        'start_time',
+        'end_time',
+        'status',
+        'total_amount',
+        'code',
+        'code_verified',
+    ];
+
+    protected $casts = [
+        'date' => 'date',
+        'code_verified' => 'boolean',
+        'total_amount' => 'decimal:2',
+    ];
+
+    public function client()
+    {
+        return $this->belongsTo(Client::class);
+    }
+
+    public function provider()
+    {
+        return $this->belongsTo(Provider::class);
+    }
+
+    public function address()
+    {
+        return $this->belongsTo(Address::class);
+    }
+}

+ 144 - 0
app/Services/ScheduleService.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\Schedule;
+use App\Models\Provider;
+use App\Models\ProviderBlockedDay;
+use App\Models\ProviderWorkingDay;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class ScheduleService
+{
+    public function getAll()
+    {
+        return Schedule::with(['client.user', 'provider.user', 'address'])
+            ->orderBy('date', 'desc')
+            ->orderBy('start_time', 'desc')
+            ->get();
+    }
+
+    public function getById($id)
+    {
+        return Schedule::with(['client.user', 'provider.user', 'address'])->findOrFail($id);
+    }
+
+    public function create(array $data)
+    {
+        $data['code'] = str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT);
+        
+        $provider = Provider::findOrFail($data['provider_id']);
+        $data['total_amount'] = $this->calculateAmount($provider, $data['period_type']);
+
+        $this->validateProviderAvailability($data);
+
+        return Schedule::create($data);
+    }
+
+    public function update($id, array $data)
+    {
+        $schedule = Schedule::findOrFail($id);
+
+        if (isset($data['provider_id']) || isset($data['period_type'])) {
+            $providerId = $data['provider_id'] ?? $schedule->provider_id;
+            $periodType = $data['period_type'] ?? $schedule->period_type;
+            $provider = Provider::findOrFail($providerId);
+            $data['total_amount'] = $this->calculateAmount($provider, $periodType);
+        }
+
+        if (isset($data['date']) || isset($data['start_time']) || isset($data['provider_id'])) {
+            $validationData = array_merge($schedule->toArray(), $data);
+            $this->validateProviderAvailability($validationData, $id);
+        }
+
+        $schedule->update($data);
+        return $schedule->fresh(['client.user', 'provider.user', 'address']);
+    }
+
+    public function delete($id)
+    {
+        $schedule = Schedule::findOrFail($id);
+        $schedule->delete();
+        return $schedule;
+    }
+
+    private function calculateAmount(Provider $provider, string $periodType): float
+    {
+        $hourlyRates = [
+            '2' => $provider->value_2_hours ?? 0,
+            '4' => $provider->value_4_hours ?? 0,
+            '6' => $provider->value_6_hours ?? 0,
+            '8' => $provider->value_8_hours ?? 0,
+        ];
+
+        return $hourlyRates[$periodType] ?? 0;
+    }
+
+    private function validateProviderAvailability(array $data, $excludeScheduleId = null)
+    {
+        $provider = Provider::findOrFail($data['provider_id']);
+        $date = Carbon::parse($data['date']);
+        $startTime = $data['start_time'];
+        $endTime = $data['end_time'];
+        
+        $dayOfWeek = $date->dayOfWeek;
+
+        $startHour = (int) substr($startTime, 0, 2);
+        $period = $startHour < 12 ? 'morning' : 'afternoon';
+
+        $workingDay = ProviderWorkingDay::where('provider_id', $data['provider_id'])
+            ->where('day', $dayOfWeek)
+            ->where('period', $period)
+            ->first();
+
+        if (!$workingDay) {
+            throw new \Exception("Prestador não trabalha neste dia/período.");
+        }
+
+        $blockedDay = ProviderBlockedDay::where('provider_id', $data['provider_id'])
+            ->where('date', $date->format('Y-m-d'))
+            ->where(function ($query) use ($startTime, $endTime) {
+                $query->where('period', 'full')
+                    ->orWhere(function ($q) use ($startTime, $endTime) {
+                        $q->where('period', 'partial')
+                            ->where(function ($q2) use ($startTime, $endTime) {
+                                $q2->whereBetween('init_hour', [$startTime, $endTime])
+                                    ->orWhereBetween('end_hour', [$startTime, $endTime])
+                                    ->orWhere(function ($q3) use ($startTime, $endTime) {
+                                        $q3->where('init_hour', '<=', $startTime)
+                                            ->where('end_hour', '>=', $endTime);
+                                    });
+                            });
+                    });
+            })
+            ->first();
+
+        if ($blockedDay) {
+            throw new \Exception("Prestador possui bloqueio neste dia/horário.");
+        }
+
+        $conflictingSchedule = Schedule::where('provider_id', $data['provider_id'])
+            ->where('date', $date->format('Y-m-d'))
+            ->whereIn('status', ['pending', 'accepted', 'paid', 'started'])
+            ->where(function ($query) use ($startTime, $endTime) {
+                $query->whereBetween('start_time', [$startTime, $endTime])
+                    ->orWhereBetween('end_time', [$startTime, $endTime])
+                    ->orWhere(function ($q) use ($startTime, $endTime) {
+                        $q->where('start_time', '<=', $startTime)
+                            ->where('end_time', '>=', $endTime);
+                    });
+            })
+            ->when($excludeScheduleId, function ($query) use ($excludeScheduleId) {
+                $query->where('id', '!=', $excludeScheduleId);
+            })
+            ->first();
+
+        if ($conflictingSchedule) {
+            throw new \Exception("Prestador já possui agendamento neste horário.");
+        }
+
+        return true;
+    }
+}

+ 46 - 0
database/migrations/2026_02_11_175745_create_schedules_table.php

@@ -0,0 +1,46 @@
+<?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::create('schedules', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('client_id')->constrained('clients')->onDelete('cascade');
+            $table->foreignId('provider_id')->constrained('providers')->onDelete('cascade');
+            $table->foreignId('address_id')->constrained('addresses')->onDelete('cascade');
+            $table->date('date');
+            $table->enum('period_type', ['2', '4', '6', '8']);
+            $table->enum('schedule_type', ['default', 'custom'])->default('default');
+            $table->time('start_time');
+            $table->time('end_time');
+            $table->enum('status', ['pending', 'accepted', 'rejected', 'paid', 'cancelled', 'started', 'finished'])->default('pending');
+            $table->decimal('total_amount', 10, 2);
+            $table->string('code', 4);
+            $table->boolean('code_verified')->default(false);
+            $table->timestamps();
+            $table->softDeletes();
+
+            $table->index('client_id');
+            $table->index('provider_id');
+            $table->index('address_id');
+            $table->index('date');
+            $table->index('status');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('schedules');
+    }
+};

+ 6 - 0
database/seeders/PermissionSeeder.php

@@ -82,6 +82,12 @@ class PermissionSeeder extends Seeder
                         "bits" => 271,
                         "children" => [],
                     ],
+                    [
+                        "scope" => "config.schedule",
+                        "description" => "Agendamentos",
+                        "bits" => 271,
+                        "children" => [],
+                    ],
                     [
                         "scope" => "config.country",
                         "description" => "Configurações de Países",

+ 10 - 0
routes/authRoutes/schedule.php

@@ -0,0 +1,10 @@
+<?php
+
+use App\Http\Controllers\ScheduleController;
+use Illuminate\Support\Facades\Route;
+
+Route::get('/schedules', [ScheduleController::class, 'index']);
+Route::get('/schedule/{id}', [ScheduleController::class, 'show']);
+Route::post('/schedule', [ScheduleController::class, 'store']);
+Route::put('/schedule/{id}', [ScheduleController::class, 'update']);
+Route::delete('/schedule/{id}', [ScheduleController::class, 'destroy']);