Răsfoiți Sursa

refactor: :recycle: refactor (images) refatoracao geral imagens com S3

foi retatorada toda a parte de imagens para fazer upload corretamente e anexar na S3 que foi criada + corrigida exibicao das imagens com url presigned da aws em todas as telas

fase:dev | origin:escopo
Gustavo Zanatta 2 săptămâni în urmă
părinte
comite
996d3f6172

+ 1 - 0
app/Http/Requests/ProviderRequest.php

@@ -67,6 +67,7 @@ class ProviderRequest extends FormRequest
             'daily_price_4h'         => 'sometimes|nullable|numeric',
             'daily_price_2h'         => 'sometimes|nullable|numeric',
             'profile_media_id'       => 'sometimes|nullable|exists:media,id',
+            'avatar'                 => 'sometimes|file|image|mimes:jpg,jpeg,png,webp|max:5120',
             'recipient_name'         => 'sometimes|string|max:255',
             'recipient_email'        => 'sometimes|email|max:255',
             'recipient_description'  => 'sometimes|nullable|string',

+ 19 - 19
app/Http/Requests/RegisterProviderRequest.php

@@ -19,25 +19,25 @@ class RegisterProviderRequest extends FormRequest
       'rg' => 'required|string|max:20',
       'birth_date' => 'required|date|before:today',
 
-      'recipient_name'        => 'required|string|max:255',
-      'recipient_email'       => 'required|email|max:255',
-      'recipient_description' => 'required|string',
-      'recipient_document'    => 'required|string|max:20',
-      'recipient_type'        => ['required', Rule::in(['individual', 'company'])],
-      'recipient_code'        => 'required|string|max:255',
+      'recipient_name'        => 'sometimes|string|max:255',
+      'recipient_email'       => 'sometimes|email|max:255',
+      'recipient_description' => 'sometimes|string',
+      'recipient_document'    => 'sometimes|string|max:20',
+      'recipient_type'        => ['sometimes', Rule::in(['individual', 'company'])],
+      'recipient_code'        => 'sometimes|string|max:255',
 
-      'recipient_payment_mode' => ['required', Rule::in(['bank_transfer'])],
+      'recipient_payment_mode' => ['sometimes', Rule::in(['bank_transfer'])],
 
-      'recipient_default_bank_account'                     => 'required|array',
-      'recipient_default_bank_account.holder_name'         => 'required|string|max:255',
-      'recipient_default_bank_account.holder_type'         => ['required', Rule::in(['individual', 'company'])],
-      'recipient_default_bank_account.holder_document'     => 'required|string|max:20',
-      'recipient_default_bank_account.bank'                => 'required|string|max:20',
-      'recipient_default_bank_account.branch_number'       => 'required|string|max:20',
+      'recipient_default_bank_account'                     => 'sometimes|array',
+      'recipient_default_bank_account.holder_name'         => 'sometimes|string|max:255',
+      'recipient_default_bank_account.holder_type'         => ['sometimes', Rule::in(['individual', 'company'])],
+      'recipient_default_bank_account.holder_document'     => 'sometimes|string|max:20',
+      'recipient_default_bank_account.bank'                => 'sometimes|string|max:20',
+      'recipient_default_bank_account.branch_number'       => 'sometimes|string|max:20',
       'recipient_default_bank_account.branch_check_digit'  => 'sometimes|nullable|string|max:10',
-      'recipient_default_bank_account.account_number'      => 'required|string|max:20',
-      'recipient_default_bank_account.account_check_digit' => 'required|string|max:10',
-      'recipient_default_bank_account.type'                => ['required', Rule::in(['checking', 'savings'])],
+      'recipient_default_bank_account.account_number'      => 'sometimes|string|max:20',
+      'recipient_default_bank_account.account_check_digit' => 'sometimes|string|max:10',
+      'recipient_default_bank_account.type'                => ['sometimes', Rule::in(['checking', 'savings'])],
       'recipient_default_bank_account.metadata'            => 'sometimes|array',
       'recipient_default_bank_account.pix_key'             => 'sometimes|nullable|string|max:255',
 
@@ -77,9 +77,9 @@ class RegisterProviderRequest extends FormRequest
       'working_days.*.day' => 'required|integer|min:0|max:6',
       'working_days.*.period' => ['required', Rule::in([WorkingPeriodEnum::MORNING->value, WorkingPeriodEnum::AFTERNOON->value])],
 
-      'selfie_base64' => 'required|string',
-      'document_front_base64' => 'required|string',
-      'document_back_base64' => 'required|string',
+      'selfie'          => 'required|file|image|mimes:jpg,jpeg,png,webp|max:5120',
+      'document_front'  => 'required|file|image|mimes:jpg,jpeg,png,webp|max:10240',
+      'document_back'   => 'required|file|image|mimes:jpg,jpeg,png,webp|max:10240',
     ];
 
     if (!$this->has('email')) {

+ 2 - 0
app/Http/Requests/ReviewRequest.php

@@ -27,6 +27,8 @@ class ReviewRequest extends FormRequest
             'block_provider'     => ['sometimes', 'boolean'],
             'block_client'       => ['sometimes', 'boolean'],
             'favorite_provider'  => ['sometimes', 'boolean'],
+            'photos'             => ['sometimes', 'array', 'max:5'],
+            'photos.*'           => ['file', 'image', 'mimes:jpg,jpeg,png,webp', 'max:10240'],
         ];
 
         if ($this->isMethod('POST')) {

+ 1 - 0
app/Http/Requests/UpdateMeRequest.php

@@ -21,6 +21,7 @@ class UpdateMeRequest extends FormRequest
             'phone'    => 'sometimes|string|nullable',
             'language' => 'sometimes|string|nullable',
             'document' => 'sometimes|string|nullable',
+            'avatar'   => 'sometimes|file|image|mimes:jpg,jpeg,png,webp|max:5120',
         ];
     }
 }

+ 9 - 7
app/Http/Resources/ClientResource.php

@@ -15,13 +15,15 @@ class ClientResource extends JsonResource
     public function toArray(Request $request): array
     {
         return [
-            'id'         => $this->id,
-            'document'   => $this->document,
-            'user_id'    => $this->user_id,
-            'user'       => new UserResource($this->whenLoaded('user')),
-            'created_at' => $this->created_at,
-            'updated_at' => $this->updated_at,
-            'deleted_at' => $this->deleted_at,
+            'id'              => $this->id,
+            'document'        => $this->document,
+            'user_id'         => $this->user_id,
+            'profile_media_id' => $this->profile_media_id,
+            'profile_media'   => new MediaResource($this->whenLoaded('profileMedia')),
+            'user'            => new UserResource($this->whenLoaded('user')),
+            'created_at'      => $this->created_at,
+            'updated_at'      => $this->updated_at,
+            'deleted_at'      => $this->deleted_at,
         ];
     }
 }

+ 2 - 1
app/Http/Resources/ProviderResource.php

@@ -6,6 +6,7 @@ use Carbon\Carbon;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
+use App\Http\Resources\MediaResource;
 use App\Models\Provider;
 
 class ProviderResource extends JsonResource
@@ -31,7 +32,7 @@ class ProviderResource extends JsonResource
             'profile_media_id'               => $this->profile_media_id,
             'recipient_id'                   => $this->recipient_id,
             'recipient_default_bank_account' => $this->recipient_default_bank_account,
-            'profile_media'                  => $this->profileMedia,
+            'profile_media'                  => $this->profileMedia ? new MediaResource($this->profileMedia) : null,
             'created_at'                     => Carbon::parse($this->created_at)->format('d/m/Y H:i'),
             'updated_at'                     => Carbon::parse($this->updated_at)->format('d/m/Y H:i'),
         ];

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

@@ -4,6 +4,7 @@ namespace App\Http\Resources;
 
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Support\Facades\Storage;
 
 class ReviewResource extends JsonResource
 {
@@ -30,6 +31,15 @@ class ReviewResource extends JsonResource
                     ];
                 });
             }),
+            'photos'     => $this->whenLoaded('reviewMedia', function () {
+                return $this->reviewMedia->map(fn ($rm) => [
+                    'id'     => $rm->media_id,
+                    'origin' => $rm->origin,
+                    'url'    => $rm->media?->path
+                        ? Storage::temporaryUrl($rm->media->path, now()->addMinutes(60))
+                        : null,
+                ]);
+            }),
             'created_at' => $this->created_at?->format('Y-m-d H:i'),
             'updated_at' => $this->updated_at?->format('Y-m-d H:i'),
         ];

+ 3 - 2
app/Http/Resources/UserResource.php

@@ -6,6 +6,7 @@ use Carbon\Carbon;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
 use Illuminate\Http\Resources\Json\JsonResource;
+use App\Http\Resources\ProviderResource;
 
 class UserResource extends JsonResource
 {
@@ -31,8 +32,8 @@ class UserResource extends JsonResource
             'client_id'               => $this->client?->id,
             'client_document'         => $this->client?->document,
             'registration_complete'   => $this->registration_complete,
-            'provider'                => $this->whenLoaded('provider'),
-            'client'                  => $this->whenLoaded('client'),
+            'provider'                => new ProviderResource($this->whenLoaded('provider')),
+            'client'                  => new ClientResource($this->whenLoaded('client')),
             'created_at'              => Carbon::parse($this->created_at)->format('Y-m-d H:i'),
             'updated_at'              => Carbon::parse($this->updated_at)->format('Y-m-d H:i'),
         ];

+ 9 - 0
app/Models/Client.php

@@ -6,12 +6,15 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\HasOne;
 use Illuminate\Database\Eloquent\SoftDeletes;
 
 /**
  * @property int $id
  * @property string|null $document
  * @property int $user_id
+ * @property int|null $profile_media_id
+ * @property-read \App\Models\Media|null $profileMedia
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at
@@ -53,6 +56,7 @@ class Client extends Model
         'external_customer_id',
         'external_customer_code',
         'user_id',
+        'profile_media_id',
     ];
 
     protected $casts = [
@@ -66,6 +70,11 @@ class Client extends Model
         return $this->belongsTo(User::class)->select('id', 'name', 'email', 'phone');
     }
 
+    public function profileMedia(): BelongsTo
+    {
+        return $this->belongsTo(Media::class, 'profile_media_id');
+    }
+
     public function blockedByProviders(): HasMany
     {
         return $this->hasMany(ProviderClientBlock::class);

+ 16 - 6
app/Models/Provider.php

@@ -26,12 +26,13 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property float|null $daily_price_4h
  * @property float|null $daily_price_2h
  * @property int|null $profile_media_id
+ * @property int|null $document_front_media_id
+ * @property int|null $document_back_media_id
+ * @property-read \App\Models\Media|null $documentFrontMedia
+ * @property-read \App\Models\Media|null $documentBackMedia
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at
- * @property string|null $selfie_media_base64
- * @property string|null $document_front_media_base64
- * @property string|null $document_back_media_base64
  * @property ApprovalStatusEnum $approval_status
  * @property string|null $recipient_id
  * @property string|null $recipient_name
@@ -67,8 +68,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDailyPrice8h($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDeletedAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDocument($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDocumentBackMediaBase64($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDocumentFrontMediaBase64($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDocumentBackMediaId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDocumentFrontMediaId($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDocumentVerified($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereId($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereProfileMediaId($value)
@@ -85,7 +86,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereRecipientTransferSettings($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereRecipientType($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereRg($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereSelfieMediaBase64($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereSelfieVerified($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereTotalServices($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereUpdatedAt($value)
@@ -140,6 +140,16 @@ class Provider extends Model
     return $this->belongsTo(Media::class, "profile_media_id");
   }
 
+  public function documentFrontMedia(): BelongsTo
+  {
+    return $this->belongsTo(Media::class, 'document_front_media_id');
+  }
+
+  public function documentBackMedia(): BelongsTo
+  {
+    return $this->belongsTo(Media::class, 'document_back_media_id');
+  }
+
   /**
    * @return HasMany
      */

+ 7 - 0
app/Models/Review.php

@@ -19,6 +19,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at
+ * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReviewMedia> $reviewMedia
+ * @property-read int|null $review_media_count
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ImprovementType> $improvements
  * @property-read int|null $improvements_count
  * @property-read \App\Models\Client|null $originClient
@@ -82,6 +84,11 @@ class Review extends Model
         return $this->hasMany(ReviewImprovement::class);
     }
 
+    public function reviewMedia(): HasMany
+    {
+        return $this->hasMany(ReviewMedia::class);
+    }
+
     public function improvements(): BelongsToMany
     {
         return $this->belongsToMany(

+ 31 - 0
app/Models/ReviewMedia.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * @property int $id
+ * @property int $review_id
+ * @property int $media_id
+ * @property string $origin
+ * @property-read \App\Models\Media $media
+ * @property-read \App\Models\Review $review
+ */
+class ReviewMedia extends Model
+{
+    protected $table = 'review_media';
+
+    protected $fillable = ['review_id', 'media_id', 'origin'];
+
+    public function review(): BelongsTo
+    {
+        return $this->belongsTo(Review::class);
+    }
+
+    public function media(): BelongsTo
+    {
+        return $this->belongsTo(Media::class);
+    }
+}

+ 8 - 2
app/Services/CustomScheduleService.php

@@ -14,6 +14,7 @@ use App\Services\DistanceService;
 use Carbon\Carbon;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
 
 class CustomScheduleService
 {
@@ -254,6 +255,7 @@ class CustomScheduleService
 
         $opportunities = Schedule::with([
             'client.user',
+            'client.profileMedia',
             'address',
             'customSchedule.serviceType',
             'customSchedule.specialities',
@@ -521,9 +523,13 @@ class CustomScheduleService
         $grouped = $schedules->groupBy('client_id')->map(function ($clientSchedules) {
             $firstSchedule = $clientSchedules->first();
 
+            $clientPhotoPath = $firstSchedule->client->profileMedia?->path;
             return [
-                'client_id'   => $firstSchedule->client_id,
-                'client_name' => $firstSchedule->client->user->name ?? 'N/A',
+                'client_id'      => $firstSchedule->client_id,
+                'client_name'    => $firstSchedule->client->user->name ?? 'N/A',
+                'customer_photo' => $clientPhotoPath
+                    ? Storage::temporaryUrl($clientPhotoPath, now()->addMinutes(60))
+                    : null,
                 'schedules'   => $clientSchedules->map(function ($schedule) {
                     $customSchedule = $schedule->customSchedule;
 

+ 90 - 3
app/Services/DashboardService.php

@@ -28,7 +28,7 @@ class DashboardService
     {
         $user = Auth::user();
 
-        $cliente = Client::where('user_id', $user->id)->first();
+        $cliente = Client::with('profileMedia')->where('user_id', $user->id)->first();
 
         $headerBar = [
             'rating'        => $cliente->average_rating,
@@ -47,6 +47,9 @@ class DashboardService
 
         $summaryInfos = [
             'name'             => $user->name,
+            'profile_photo'    => $cliente->profileMedia?->path
+                ? Storage::temporaryUrl($cliente->profileMedia->path, now()->addMinutes(60))
+                : null,
             'address'          => $address,
             'pending_services' => Schedule::where('client_id', $cliente->id)
                 ->where('status', 'pending')
@@ -60,6 +63,7 @@ class DashboardService
             ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
             ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
+            ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->where('schedules.date', '>=', now()->toDateString())
             ->select(
                 'schedules.id',
@@ -73,11 +77,19 @@ class DashboardService
                 'schedules.schedule_type',
                 'schedules.address_id',
                 'custom_schedules.address_type as custom_address_type',
+                'provider_media.path as provider_photo_path',
             )
             ->orderBy('schedules.date', 'asc')
             ->limit(5)
             ->get();
 
+        $nextSchedules->each(function ($item) {
+            $item->provider_photo = $item->provider_photo_path
+                ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
+                : null;
+            unset($item->provider_photo_path);
+        });
+
         $lastDoneSchedules = Schedule::where('schedules.client_id', $cliente->id)
             ->where('schedules.status', 'finished')
             ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
@@ -87,16 +99,25 @@ class DashboardService
                     ->where('provider_address.source', 'provider')
                     ->orderBy('provider_address.is_primary', 'desc');
             })
+            ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->select(
                 'schedules.id',
                 'schedules.provider_id',
                 'provider_user.name as provider_name',
                 'provider_address.district as provider_district',
+                'provider_media.path as provider_photo_path',
             )
             ->orderBy('schedules.date', 'desc')
             ->limit(5)
             ->get();
 
+        $lastDoneSchedules->each(function ($item) {
+            $item->provider_photo = $item->provider_photo_path
+                ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
+                : null;
+            unset($item->provider_photo_path);
+        });
+
         $favoriteProviders = ClientFavoriteProvider::where('client_favorite_providers.client_id', $cliente->id)
             ->leftJoin('providers', 'providers.id', '=', 'client_favorite_providers.provider_id')
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
@@ -105,16 +126,25 @@ class DashboardService
                     ->where('provider_address.source', 'provider')
                     ->orderBy('provider_address.is_primary', 'desc');
             })
+            ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->select(
                 'providers.id as provider_id',
                 'provider_user.name as provider_name',
                 'providers.average_rating',
                 'provider_address.district as provider_district',
+                'provider_media.path as provider_photo_path',
             )
             ->orderBy('client_favorite_providers.created_at', 'desc')
             ->limit(5)
             ->get();
 
+        $favoriteProviders->each(function ($item) {
+            $item->provider_photo = $item->provider_photo_path
+                ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
+                : null;
+            unset($item->provider_photo_path);
+        });
+
         $blockedProviderIds       = ScheduleBusinessRules::getBlockedProviderIdsForClient($cliente->id);
         $providersWithWorkingDays = ScheduleBusinessRules::getProviderIdsWithWorkingDays();
 
@@ -139,6 +169,7 @@ class DashboardService
           ORDER BY source_id, is_primary DESC
         ) as provider_address
       "), 'provider_address.source_id', '=', 'providers.id')
+            ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->whereNotNull('provider_address.id')
             ->where('provider_address.city_id', $clientPrimaryAddress?->city_id)
             ->whereNotIn('providers.id', $blockedProviderIds)
@@ -165,15 +196,24 @@ class DashboardService
           AND schedules.provider_id = providers.id
         ) as total_reviews"),
                 $providersCloseDistanceSelect,
+                'provider_media.path as provider_photo_path',
             )
             ->get();
 
+        $providersClose->each(function ($item) {
+            $item->provider_photo = $item->provider_photo_path
+                ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
+                : null;
+            unset($item->provider_photo_path);
+        });
+
         $pendingSchedules = Schedule::with('address:district,address,number,source_id,source,id,address_type')
             ->where('schedules.client_id', $cliente->id)
             ->whereIn('schedules.status', ['pending', 'accepted'])
             ->where('schedules.schedule_type', 'default')
             ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
+            ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->select(
                 'schedules.id',
                 'schedules.provider_id',
@@ -188,11 +228,19 @@ class DashboardService
           WHEN (now() - schedules.created_at) < INTERVAL '1 hour' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 60), 'min')
           WHEN (now() - schedules.created_at) < INTERVAL '1 day' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 3600), 'h')
           ELSE CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 86400), 'd')
-        END as time_since_request")
+        END as time_since_request"),
+                'provider_media.path as provider_photo_path',
             )
             ->orderBy('schedules.date', 'asc')
             ->get();
 
+        $pendingSchedules->each(function ($item) {
+            $item->provider_photo = $item->provider_photo_path
+                ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
+                : null;
+            unset($item->provider_photo_path);
+        });
+
         $proposalsDistanceSelect = DistanceService::sqlExpression(
             $clientPrimaryAddress?->latitude !== null ? (float) $clientPrimaryAddress->latitude : null,
             $clientPrimaryAddress?->longitude !== null ? (float) $clientPrimaryAddress->longitude : null,
@@ -210,6 +258,7 @@ class DashboardService
                 AND deleted_at IS NULL
                 ORDER BY source_id, is_primary DESC
             ) as provider_address"), 'provider_address.source_id', '=', 'providers.id')
+            ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->where('schedules.client_id', $cliente->id)
             ->where('schedules.schedule_type', 'custom')
             ->where('schedules.status', 'pending')
@@ -233,9 +282,17 @@ class DashboardService
 
                 'users.name as provider_name',
                 $proposalsDistanceSelect,
+                'provider_media.path as provider_photo_path',
             ])
             ->get();
 
+        $schedulesProposals->each(function ($item) {
+            $item->provider_photo = $item->provider_photo_path
+                ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
+                : null;
+            unset($item->provider_photo_path);
+        });
+
         $todaySchedules = Schedule::with('address:district,address,number,source_id,source,id,address_type')
             ->where('schedules.client_id', $cliente->id)
             ->whereIn('schedules.status', ['accepted', 'paid', 'started', 'finished'])
@@ -342,7 +399,7 @@ class DashboardService
     {
         $user = Auth::user();
 
-        $provider = Provider::where('user_id', $user->id)->first();
+        $provider = Provider::with('profileMedia')->where('user_id', $user->id)->first();
 
         $headerBar = [
             'rating'         => $provider->average_rating,
@@ -354,6 +411,9 @@ class DashboardService
 
         $summaryInfos = [
             'name'             => $user->name,
+            'profile_photo'    => $provider->profileMedia?->path
+                ? Storage::temporaryUrl($provider->profileMedia->path, now()->addMinutes(60))
+                : null,
             'address'          => $address,
             'pending_services' => Schedule::where('provider_id', $provider->id)->where('status', 'pending')->count(),
         ];
@@ -375,6 +435,7 @@ class DashboardService
             ->where('schedules.status', 'pending')
             ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
             ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')
+            ->leftJoin('media as client_media', 'client_media.id', '=', 'clients.profile_media_id')
             ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
             ->select(
                 'schedules.id',
@@ -390,6 +451,7 @@ class DashboardService
                 'schedules.address_id',
                 'schedules.status',
                 'custom_schedules.offers_meal',
+                'client_media.path as customer_photo_path',
                 DB::raw("CASE
           WHEN (now() - schedules.created_at) < INTERVAL '1 day' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 3600), ' hours ago')
           ELSE CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 86400), ' days ago')
@@ -402,6 +464,10 @@ class DashboardService
         $providerLng = $address?->longitude !== null ? (float) $address->longitude : null;
 
         $solicitations->each(function ($solicitation) use ($providerLat, $providerLng) {
+            $solicitation->customer_photo = $solicitation->customer_photo_path
+                ? Storage::temporaryUrl($solicitation->customer_photo_path, now()->addMinutes(60))
+                : null;
+            unset($solicitation->customer_photo_path);
             $solicitation->distance_km = DistanceService::calculate(
                 $providerLat,
                 $providerLng,
@@ -416,6 +482,7 @@ class DashboardService
             ->whereDate('schedules.date', now()->toDateString())
             ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
             ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')
+            ->leftJoin('media as client_media', 'client_media.id', '=', 'clients.profile_media_id')
             ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
             ->select(
                 'schedules.id',
@@ -432,6 +499,7 @@ class DashboardService
                 'schedules.code_verified',
                 'schedules.code',
                 'custom_schedules.offers_meal',
+                'client_media.path as customer_photo_path',
                 DB::raw("EXISTS(
           SELECT 1 FROM reviews
           WHERE reviews.schedule_id = schedules.id
@@ -443,12 +511,20 @@ class DashboardService
             ->orderBy('schedules.start_time', 'asc')
             ->get();
 
+        $todayServices->each(function ($s) {
+            $s->customer_photo = $s->customer_photo_path
+                ? Storage::temporaryUrl($s->customer_photo_path, now()->addMinutes(60))
+                : null;
+            unset($s->customer_photo_path);
+        });
+
         $nextSchedules = Schedule::with('address:district,address,number,source_id,source,id,latitude,longitude')
             ->where('schedules.provider_id', $provider->id)
             ->whereIn('schedules.status', ['accepted', 'paid'])
             ->whereDate('schedules.date', '>=', now()->toDateString())
             ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
             ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')
+            ->leftJoin('media as client_media', 'client_media.id', '=', 'clients.profile_media_id')
             ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
             ->select(
                 'schedules.id',
@@ -462,11 +538,16 @@ class DashboardService
                 'schedules.schedule_type',
                 'schedules.status',
                 'custom_schedules.offers_meal',
+                'client_media.path as customer_photo_path',
             )
             ->orderBy('schedules.date', 'asc')
             ->get();
 
         $nextSchedules->each(function ($schedule) use ($providerLat, $providerLng) {
+            $schedule->customer_photo = $schedule->customer_photo_path
+                ? Storage::temporaryUrl($schedule->customer_photo_path, now()->addMinutes(60))
+                : null;
+            unset($schedule->customer_photo_path);
             $schedule->distance_km = DistanceService::calculate(
                 $providerLat,
                 $providerLng,
@@ -507,6 +588,12 @@ class DashboardService
 
         $opportunities = $this->customScheduleService->getAvailableOpportunities($provider->id);
 
+        $opportunities->each(function ($o) {
+            $o->customer_photo = $o->client?->profileMedia?->path
+                ? Storage::temporaryUrl($o->client->profileMedia->path, now()->addMinutes(60))
+                : null;
+        });
+
         return [
             'headerBar'      => $headerBar,
             'summaryInfos'   => $summaryInfos,

+ 27 - 0
app/Services/MediaService.php

@@ -3,10 +3,15 @@
 namespace App\Services;
 
 use App\Models\Media;
+use App\Traits\RemoveArchiveS3;
+use App\Traits\UploadsFile;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Http\UploadedFile;
 
 class MediaService
 {
+    use UploadsFile, RemoveArchiveS3;
+
     public function getAll(): Collection
     {
         return Media::query()
@@ -48,4 +53,26 @@ class MediaService
 
         return $model->delete();
     }
+
+    public function createFromFile(UploadedFile $file, string $folder, string $source, int $sourceId, ?string $filename = null): Media
+    {
+        $path = $this->uploadFile($file, $folder, $filename);
+
+        return Media::create([
+            'source'    => $source,
+            'source_id' => $sourceId,
+            'name'      => $file->getClientOriginalName(),
+            'path'      => $path,
+        ]);
+    }
+
+    public function replaceFile(UploadedFile $newFile, string $folder, string $source, int $sourceId, ?Media $old = null, ?string $filename = null): Media
+    {
+        if ($old) {
+            $this->removeArchiveByPath($old->path);
+            $old->delete();
+        }
+
+        return $this->createFromFile($newFile, $folder, $source, $sourceId, $filename);
+    }
 }

+ 48 - 7
app/Services/ProviderService.php

@@ -16,12 +16,14 @@ use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Http\UploadedFile;
 
 class ProviderService
 {
     public function __construct(
         private readonly AuthService $authService,
         private readonly PagarmeRecipientService $pagarmeRecipientService,
+        private readonly MediaService $mediaService,
     ) {}
 
   public function getAll(): Collection
@@ -60,11 +62,23 @@ class ProviderService
       return null;
     }
 
-        $model->update($data);
-
-        return $model->fresh(['user', 'profileMedia']);
+    if (isset($data['avatar']) && $data['avatar'] instanceof UploadedFile) {
+      $media = $this->mediaService->replaceFile(
+        newFile:  $data['avatar'],
+        folder:   "provider/avatar/{$model->id}",
+        source:   'provider',
+        sourceId: $model->id,
+        old:      $model->profileMedia,
+      );
+      $data['profile_media_id'] = $media->id;
+      unset($data['avatar']);
     }
 
+    $model->update($data);
+
+    return $model->fresh(['user', 'profileMedia']);
+  }
+
     public function updateBankAccount(int $id, array $bankAccountData): ?Provider
     {
         $provider = $this->findById($id);
@@ -166,13 +180,40 @@ class ProviderService
       $provider->daily_price_4h = $data['daily_price_4h'] ?? null;
       $provider->daily_price_2h = $data['daily_price_2h'] ?? null;
       $provider->approval_status = ApprovalStatusEnum::PENDING->value;
-      $provider->selfie_media_base64 = $data['selfie_base64'] ?? null;
-      $provider->document_front_media_base64 = $data['document_front_base64'] ?? null;
-      $provider->document_back_media_base64 = $data['document_back_base64'] ?? null;
       $provider->save();
       $provider->refresh();
 
-      $this->pagarmeRecipientService->createRecipientForProvider($provider, $data);
+      $selfie = $this->mediaService->createFromFile(
+          file:     $data['selfie'],
+          folder:   "provider/avatar/{$provider->id}",
+          source:   'provider',
+          sourceId: $provider->id,
+      );
+      $provider->profile_media_id = $selfie->id;
+
+      $front = $this->mediaService->createFromFile(
+          file:     $data['document_front'],
+          folder:   "provider/documentos/{$provider->id}",
+          source:   'provider_document',
+          sourceId: $provider->id,
+          filename: 'frente.'.$data['document_front']->getClientOriginalExtension(),
+      );
+      $provider->document_front_media_id = $front->id;
+
+      $back = $this->mediaService->createFromFile(
+          file:     $data['document_back'],
+          folder:   "provider/documentos/{$provider->id}",
+          source:   'provider_document',
+          sourceId: $provider->id,
+          filename: 'verso.'.$data['document_back']->getClientOriginalExtension(),
+      );
+      $provider->document_back_media_id = $back->id;
+
+      $provider->save();
+
+      if (!empty($data['recipient_code'])) {
+          $this->pagarmeRecipientService->createRecipientForProvider($provider, $data);
+      }
       $this->createProviderAddress($provider->id, $data);
       $this->createProviderServicesTypes($provider->id, $data);
       $this->createProviderWorkingDays($provider->id, $data);

+ 23 - 1
app/Services/ReviewService.php

@@ -8,6 +8,7 @@ use App\Models\ClientProviderBlock;
 use App\Models\Provider;
 use App\Models\ProviderClientBlock;
 use App\Models\Review;
+use App\Models\ReviewMedia;
 use App\Models\Schedule;
 use Exception;
 use Illuminate\Support\Facades\DB;
@@ -15,6 +16,7 @@ use Illuminate\Support\Facades\Log;
 
 class ReviewService
 {
+    public function __construct(private readonly MediaService $mediaService) {}
     public function getAll()
     {
         return Review::with(['schedule.client.user', 'schedule.provider.user', 'reviewsImprovements.improvementType'])
@@ -119,9 +121,29 @@ class ReviewService
                 }
             }
 
+            if (! empty($data['photos'])) {
+                $schedule  = Schedule::find($data['schedule_id']);
+                $origin    = $data['origin'];
+
+                foreach ($data['photos'] as $photo) {
+                    $media = $this->mediaService->createFromFile(
+                        file:     $photo,
+                        folder:   "review/{$schedule->id}/{$origin}",
+                        source:   'review',
+                        sourceId: $review->id,
+                    );
+
+                    ReviewMedia::create([
+                        'review_id' => $review->id,
+                        'media_id'  => $media->id,
+                        'origin'    => $origin,
+                    ]);
+                }
+            }
+
             DB::commit();
 
-            return $review;
+            return $review->load('reviewMedia.media');
         } catch (Exception $e) {
             DB::rollBack();
             Log::error('Error creating review: '.$e->getMessage());

+ 19 - 3
app/Services/UserService.php

@@ -7,15 +7,18 @@ use App\Models\Address;
 use App\Models\Client;
 use App\Models\User;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Http\UploadedFile;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
 class UserService
 {
+    public function __construct(private readonly MediaService $mediaService) {}
+
     public function me(): ?User
     {
-        return User::with(['provider', 'client'])->find(Auth::id());
+        return User::with(['provider.profileMedia', 'client.profileMedia'])->find(Auth::id());
     }
 
     public function getAll(): Collection
@@ -62,7 +65,7 @@ class UserService
         try {
             DB::beginTransaction();
 
-            $user = User::with(['client'])->findOrFail(Auth::id());
+            $user = User::with(['client.profileMedia'])->findOrFail(Auth::id());
 
             $userFields = array_filter(
                 array_intersect_key($data, array_flip(['name', 'email', 'phone', 'language'])),
@@ -81,6 +84,19 @@ class UserService
                 $client->save();
             }
 
+            if (isset($data['avatar']) && $data['avatar'] instanceof UploadedFile) {
+                $client  = $user->client ?? Client::create(['user_id' => $user->id]);
+                $media   = $this->mediaService->replaceFile(
+                    newFile:   $data['avatar'],
+                    folder:    "client/avatar/{$client->id}",
+                    source:    'client',
+                    sourceId:  $client->id,
+                    old:       $client->profileMedia,
+                );
+                $client->profile_media_id = $media->id;
+                $client->save();
+            }
+
             $user->load('client');
 
             $registrationComplete = ! empty($user->name)
@@ -95,7 +111,7 @@ class UserService
 
             DB::commit();
 
-            return $user->fresh(['client']);
+            return $user->fresh(['client.profileMedia']);
         } catch (\Exception $e) {
             DB::rollBack();
 

+ 8 - 9
app/Traits/RemoveArchiveS3.php

@@ -14,15 +14,14 @@ trait RemoveArchiveS3
      */
     public function removeArchiveByPath(string $path): bool
     {
-        if (Storage::exists($path)) {
-            $success = Storage::delete($path);
-            if ($success) {
-                return true;
-            } else {
-                throw new Exception('Deletion was not possible');
-            }
-        } else {
-            throw new Exception('File not found');
+        if (!Storage::exists($path)) {
+            return false;
         }
+
+        if (!Storage::delete($path)) {
+            throw new Exception('Deletion was not possible');
+        }
+
+        return true;
     }
 }

+ 21 - 0
app/Traits/UploadsFile.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Traits;
+
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+
+trait UploadsFile
+{
+    public function uploadFile(UploadedFile $file, string $folder, ?string $filename = null): string
+    {
+        $extension = $file->getClientOriginalExtension();
+        $name      = $filename ?? Str::random(20).'.'.$extension;
+        $path      = $folder.'/'.$name;
+
+        Storage::disk('s3')->put($path, file_get_contents($file));
+
+        return $path;
+    }
+}

+ 27 - 0
database/migrations/2026_05_27_142855_add_profile_media_id_to_clients_table.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
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('clients', function (Blueprint $table) {
+            $table->unsignedBigInteger('profile_media_id')->nullable()->after('user_id');
+            $table->foreign('profile_media_id')->references('id')->on('media')->nullOnDelete();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('clients', function (Blueprint $table) {
+            $table->dropForeign(['profile_media_id']);
+            $table->dropColumn('profile_media_id');
+        });
+    }
+};

+ 30 - 0
database/migrations/2026_05_27_143114_add_document_media_ids_to_providers_table.php

@@ -0,0 +1,30 @@
+<?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('providers', function (Blueprint $table) {
+            $table->unsignedBigInteger('document_front_media_id')->nullable()->after('profile_media_id');
+            $table->unsignedBigInteger('document_back_media_id')->nullable()->after('document_front_media_id');
+            $table->foreign('document_front_media_id')->references('id')->on('media')->nullOnDelete();
+            $table->foreign('document_back_media_id')->references('id')->on('media')->nullOnDelete();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->dropForeign(['document_front_media_id']);
+            $table->dropForeign(['document_back_media_id']);
+            $table->dropColumn(['document_front_media_id', 'document_back_media_id']);
+        });
+    }
+};

+ 27 - 0
database/migrations/2026_05_27_143114_remove_base64_columns_from_providers_table.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
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->dropColumn(['selfie_media_base64', 'document_front_media_base64', 'document_back_media_base64']);
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->longText('selfie_media_base64')->nullable();
+            $table->longText('document_front_media_base64')->nullable();
+            $table->longText('document_back_media_base64')->nullable();
+        });
+    }
+};

+ 33 - 0
database/migrations/2026_05_27_143343_create_review_media_table.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
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('review_media', function (Blueprint $table) {
+            $table->id();
+            $table->unsignedBigInteger('review_id');
+            $table->unsignedBigInteger('media_id');
+            $table->string('origin'); // client | provider
+            $table->timestamps();
+
+            $table->foreign('review_id')->references('id')->on('reviews')->cascadeOnDelete();
+            $table->foreign('media_id')->references('id')->on('media')->cascadeOnDelete();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('review_media');
+    }
+};

+ 1 - 1
routes/authRoutes/user.php

@@ -4,7 +4,7 @@ use App\Http\Controllers\UserController;
 use Illuminate\Support\Facades\Route;
 
 Route::get('/user/me',      [UserController::class, 'me']);
-Route::put('/me',           [UserController::class, 'updateMe'])->middleware('permission:config.user,edit');
+Route::match(['put', 'post'], '/me', [UserController::class, 'updateMe'])->middleware('permission:config.user,edit');
 Route::get('/user',         [UserController::class, 'index'])->middleware('permission:config.user,view');
 Route::post('/user',        [UserController::class, 'store'])->middleware('permission:config.user,add');
 Route::get('/user/{id}',    [UserController::class, 'show'])->middleware('permission:config.user,view');