Forráskód Böngészése

certificati e document membri s3

FabioFratini 7 hónapja
szülő
commit
201b7b20b0

+ 212 - 68
app/Http/Livewire/Member.php

@@ -144,6 +144,8 @@ class Member extends Component
     public $filterCertScaduto = 0;
     public $filterCertInScadenza = 0;
     public $already_existing = false;
+    private $fileService;
+
     protected $rules = [
         'first_name' => 'required',
         'last_name' => 'required',
@@ -408,45 +410,91 @@ class Member extends Component
 
     public function removeDocument($idx, $type)
     {
-        if ($type == 'father') {
-            unset($this->father_document_files[$idx]);
-        } elseif ($type == 'mother') {
-            unset($this->mother_document_files[$idx]);
-        } else {
-            unset($this->document_files[$idx]);
+        try {
+            if ($type == 'father') {
+                if (isset($this->father_document_files[$idx])) {
+                    $filePath = $this->father_document_files[$idx];
+                    $this->fileService->deleteFile($filePath);
+                    unset($this->father_document_files[$idx]);
+                    $this->father_document_files = array_values($this->father_document_files);
+                }
+            } elseif ($type == 'mother') {
+                if (isset($this->mother_document_files[$idx])) {
+                    $filePath = $this->mother_document_files[$idx];
+                    $this->fileService->deleteFile($filePath);
+                    unset($this->mother_document_files[$idx]);
+                    $this->mother_document_files = array_values($this->mother_document_files);
+                }
+            } else {
+                if (isset($this->document_files[$idx])) {
+                    $filePath = $this->document_files[$idx];
+                    $this->fileService->deleteFile($filePath);
+                    unset($this->document_files[$idx]);
+                    $this->document_files = array_values($this->document_files);
+                }
+            }
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error removing document: ' . $e->getMessage());
         }
     }
 
+
     public function updatedDocuments()
     {
-        foreach ($this->documents as $document) {
-            $name = $document->getClientOriginalName();
-            $document->storeAs('public', $name);
-            $this->document_files[] = $name;
+        try {
+            foreach ($this->documents as $document) {
+                if ($this->dataId > 0) {
+                    $s3Path = $this->fileService->uploadDocument($document, $this->dataId, 'self');
+                    $this->document_files[] = $s3Path;
+                } else {
+                    $name = $document->getClientOriginalName();
+                    $document->storeAs('public', $name);
+                    $this->document_files[] = $name;
+                }
+            }
+            $this->documents = [];
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error uploading documents: ' . $e->getMessage());
         }
-        $this->documents = [];
     }
 
     public function updatedFatherDocuments()
     {
-        foreach ($this->father_documents as $document) {
-            $name = $document->getClientOriginalName();
-            $document->storeAs('public', $name);
-            $this->father_document_files[] = $name;
+        try {
+            foreach ($this->father_documents as $document) {
+                if ($this->dataId > 0) {
+                    $s3Path = $this->fileService->uploadDocument($document, $this->dataId, 'father');
+                    $this->father_document_files[] = $s3Path;
+                } else {
+                    $name = $document->getClientOriginalName();
+                    $document->storeAs('public', $name);
+                    $this->father_document_files[] = $name;
+                }
+            }
+            $this->father_documents = [];
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error uploading father documents: ' . $e->getMessage());
         }
-        $this->father_documents = [];
     }
 
     public function updatedMotherDocuments()
     {
-        foreach ($this->mother_documents as $document) {
-            $name = $document->getClientOriginalName();
-            $document->storeAs('public', $name);
-            $this->mother_document_files[] = $name;
+        try {
+            foreach ($this->mother_documents as $document) {
+                if ($this->dataId > 0) {
+                    $s3Path = $this->fileService->uploadDocument($document, $this->dataId, 'mother');
+                    $this->mother_document_files[] = $s3Path;
+                } else {
+                    $name = $document->getClientOriginalName();
+                    $document->storeAs('public', $name);
+                    $this->mother_document_files[] = $name;
+                }
+            }
+            $this->mother_documents = [];
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error uploading mother documents: ' . $e->getMessage());
         }
-        $this->mother_documents = [];
     }
-
     public function resetCategoryFields()
     {
         $this->category_category_id = null;
@@ -523,6 +571,11 @@ class Member extends Component
         $this->birthCities = \App\Models\City::where('province_id', 178)->orderBy('name')->orderBy('name')->get();*/
     }
 
+    public function boot(){
+        $this->fileService = app(\App\Services\MemberFileService::class);
+
+    }
+
     public function updated()
     {
         if ($this->isSaving) {
@@ -926,16 +979,14 @@ class Member extends Component
         }
         try {
 
-            $name = '';
+            $imageName = '';
             if ($this->image) {
-                $name = md5($this->image . microtime()) . '.' . $this->image->extension();
-                $this->image->storeAs('public', $name);
+                $imageName = md5($this->image . microtime()) . '.' . $this->image->extension();
+                $this->image->storeAs('public', $imageName);
             }
 
             $docs = implode("|", $this->document_files);
-
             $father_docs = implode("|", $this->father_document_files);
-
             $mother_docs = implode("|", $this->mother_document_files);
 
 
@@ -981,10 +1032,18 @@ class Member extends Component
                 'phone2' => $this->phone2,
                 'phone3' => $this->phone3,
                 'email' => strtolower($this->email),
-                'image' => $name,
+                'image' => $imageName,
                 'to_complete' => false,
                 'enabled' => $this->enabled
             ]);
+            $this->fileService->createMemberFolders($member->id);
+
+            if ($this->image) {
+                $s3ImagePath = $this->fileService->uploadProfileImage($this->image, $member->id);
+                $member->update(['image' => $s3ImagePath]);
+            }
+            $this->migrateTemporaryFiles($member->id);
+
 
             session()->flash('success, Tesserato creato');
             updateMemberData($member->id);
@@ -1000,6 +1059,104 @@ class Member extends Component
         }
     }
 
+    private function migrateTemporaryFiles($memberId)
+    {
+        try {
+            $updatedPaths = [];
+
+            // Migrate document files
+            $newDocumentFiles = [];
+            foreach ($this->document_files as $filePath) {
+                if (strpos($filePath, '/members/') === false) {
+                    // This is a temporary local file, move to S3
+                    $localPath = storage_path('app/public/' . $filePath);
+                    if (file_exists($localPath)) {
+                        $uploadedFile = new \Illuminate\Http\UploadedFile(
+                            $localPath,
+                            basename($filePath),
+                            mime_content_type($localPath),
+                            filesize($localPath),
+                            0,
+                            true
+                        );
+                        $s3Path = $this->fileService->uploadDocument($uploadedFile, $memberId, 'self');
+                        $newDocumentFiles[] = $s3Path;
+                        // Delete temporary file
+                        unlink($localPath);
+                    }
+                } else {
+                    $newDocumentFiles[] = $filePath;
+                }
+            }
+            if (!empty($newDocumentFiles)) {
+                $updatedPaths['document_files'] = implode('|', $newDocumentFiles);
+            }
+
+            // Migrate father document files
+            $newFatherFiles = [];
+            foreach ($this->father_document_files as $filePath) {
+                if (strpos($filePath, '/members/') === false) {
+                    $localPath = storage_path('app/public/' . $filePath);
+                    if (file_exists($localPath)) {
+                        $uploadedFile = new \Illuminate\Http\UploadedFile(
+                            $localPath,
+                            basename($filePath),
+                            mime_content_type($localPath),
+                            filesize($localPath),
+                            0,
+                            true
+                        );
+                        $s3Path = $this->fileService->uploadDocument($uploadedFile, $memberId, 'father');
+                        $newFatherFiles[] = $s3Path;
+                        unlink($localPath);
+                    }
+                } else {
+                    $newFatherFiles[] = $filePath;
+                }
+            }
+            if (!empty($newFatherFiles)) {
+                $updatedPaths['father_document_files'] = implode('|', $newFatherFiles);
+            }
+
+            // Migrate mother document files
+            $newMotherFiles = [];
+            foreach ($this->mother_document_files as $filePath) {
+                if (strpos($filePath, '/members/') === false) {
+                    $localPath = storage_path('app/public/' . $filePath);
+                    if (file_exists($localPath)) {
+                        $uploadedFile = new \Illuminate\Http\UploadedFile(
+                            $localPath,
+                            basename($filePath),
+                            mime_content_type($localPath),
+                            filesize($localPath),
+                            0,
+                            true
+                        );
+                        $s3Path = $this->fileService->uploadDocument($uploadedFile, $memberId, 'mother');
+                        $newMotherFiles[] = $s3Path;
+                        unlink($localPath);
+                    }
+                } else {
+                    $newMotherFiles[] = $filePath;
+                }
+            }
+            if (!empty($newMotherFiles)) {
+                $updatedPaths['mother_document_files'] = implode('|', $newMotherFiles);
+            }
+
+            // Update member with new S3 paths
+            if (!empty($updatedPaths)) {
+                \App\Models\Member::whereId($memberId)->update($updatedPaths);
+            }
+        } catch (\Exception $e) {
+            Log::error('Error migrating temporary files', [
+                'member_id' => $memberId,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+
     public function duplicate($id)
     {
         $member = \App\Models\Member::findOrFail($id);
@@ -1179,12 +1336,12 @@ class Member extends Component
 
         try {
 
-            $name = '';
+            $imagePath = $this->image_old; // Keep existing if no new image
             if ($this->image) {
-                $name = md5($this->image . microtime()) . '.' . $this->image->extension();
-                $this->image->storeAs('public', $name);
+                $imagePath = $this->fileService->uploadProfileImage($this->image, $this->dataId);
             }
 
+
             $docs = implode("|", $this->document_files);
             $father_docs = implode("|", $this->father_document_files);
             $mother_docs = implode("|", $this->mother_document_files);
@@ -1226,7 +1383,7 @@ class Member extends Component
                 'nation_id' => $this->nation_id > 0 ? $this->nation_id : null,
                 'province_id' => $this->province_id > 0 ? $this->province_id : null,
                 'city_id' => $this->city_id > 0 ? $this->city_id : null,
-                'image' => $name != '' ? $name : $this->image_old,
+                'image' => $imagePath,
                 'phone' => $this->phone,
                 'phone2' => $this->phone2,
                 'phone3' => $this->phone3,
@@ -1751,43 +1908,26 @@ class Member extends Component
 
     public function storeCertificate()
     {
-
         $this->validate(['certificate_expire_date' => 'required']);
-        // $this->validate();
-        try {
-
-            $name = '';
-            try {
 
-                if ($this->certificate_filename) {
-                    $name = md5($this->certificate_filename . microtime()) . '.' . $this->certificate_filename->extension();
-                    $this->certificate_filename->storeAs('public', $name);
-                }
-            } catch (\Exception $ex) {
-                //session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        try {
+            $certificatePath = '';
+            if ($this->certificate_filename) {
+                $certificatePath = $this->fileService->uploadCertificate($this->certificate_filename, $this->dataId);
             }
 
             if ($this->dataId > -1) {
                 \App\Models\MemberCertificate::create([
                     'member_id' => $this->dataId,
                     'type' => $this->certificate_type,
-                    'filename' => $name,
+                    'filename' => $certificatePath,
                     'expire_date' => $this->certificate_expire_date,
                     'status' => $this->certificate_status
                 ]);
                 updateMemberData($this->dataId);
             }
-            /*else
-            {
-                $this->certificateTmp = new \App\Models\MemberCertificate();
-                $this->certificateTmp->type = $this->certificate_type;
-                $this->certificateTmp->filename = $name;
-                $this->certificateTmp->expire_date = $this->certificate_expire_date;
-                $this->certificateTmp->status = $this->certificate_status;
-                $this->certificateTmp->status = $this->certificate_status;
-                // s    $this->member_certificates[] = $certificateTmp;
-            }*/
-            session()->flash('success, Tesserato creato');
+
+            session()->flash('success', 'Certificato creato');
             $this->resetCertificateFields();
             $this->addCertificate = false;
         } catch (\Exception $ex) {
@@ -1818,28 +1958,23 @@ class Member extends Component
     public function updateCertificate()
     {
         $this->validate(['certificate_expire_date' => 'required']);
-        try {
 
-            $name = '';
-            try {
-
-                if ($this->certificate_filename) {
-                    $name = md5($this->certificate_filename . microtime()) . '.' . $this->certificate_filename->extension();
-                    $this->certificate_filename->storeAs('public', $name);
-                }
-            } catch (\Exception $ex) {
-                //session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        try {
+            $certificatePath = $this->certificate_filename_old; // Keep existing if no new file
+            if ($this->certificate_filename) {
+                $certificatePath = $this->fileService->uploadCertificate($this->certificate_filename, $this->dataId);
             }
 
             \App\Models\MemberCertificate::whereId($this->cardCertificateId)->update([
                 'member_id' => $this->dataId,
                 'type' => $this->certificate_type,
-                'filename' => $name != '' ? $name : $this->certificate_filename_old,
+                'filename' => $certificatePath,
                 'expire_date' => $this->certificate_expire_date,
                 'status' => $this->certificate_status
             ]);
+
             updateMemberData($this->dataId);
-            session()->flash('success', 'Tesserato aggiornato');
+            session()->flash('success', 'Certificato aggiornato');
             $this->resetCertificateFields();
             $this->updateCertificate = false;
         } catch (\Exception $ex) {
@@ -1847,6 +1982,15 @@ class Member extends Component
         }
     }
 
+    public function getFileUrl($filePath)
+    {
+        if (!$filePath) {
+            return null;
+        }
+
+        return $this->fileService->getFileUrl($filePath);
+    }
+
     public function cancelCertificate()
     {
         $this->addCertificate = false;

+ 424 - 0
app/Services/MemberFileService.php

@@ -0,0 +1,424 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Http\UploadedFile;
+use Exception;
+
+class MemberFileService
+{
+    /**
+     * The storage disk to use for file operations
+     */
+    private $disk;
+
+    /**
+     * Allowed file extensions for different file types
+     */
+    private const ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif'];
+    private const ALLOWED_DOCUMENT_EXTENSIONS = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'];
+
+    /**
+     * Maximum file sizes in KB
+     */
+    private const MAX_IMAGE_SIZE = 2048; // 2MB
+    private const MAX_DOCUMENT_SIZE = 10240; // 10MB
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->disk = Storage::disk('s3');
+    }
+
+    /**
+     * Upload member profile image
+     *
+     * @param UploadedFile $imageFile
+     * @param int $memberId
+     * @return string
+     * @throws Exception
+     */
+    public function uploadProfileImage(UploadedFile $imageFile, int $memberId): string
+    {
+        try {
+            $currentClient = session('currentClient', 'default');
+
+            // Validate file
+            $this->validateFile($imageFile, 'image');
+
+            // Delete existing profile image
+            $this->deleteExistingProfileImage($memberId);
+
+            // Create new filename
+            $extension = strtolower($imageFile->getClientOriginalExtension());
+            $filename = 'profile_' . $memberId . '_' . time() . '.' . $extension;
+
+            // Upload to S3
+            $s3Path = $currentClient . '/members/' . $memberId . '/images/' . $filename;
+
+            $uploaded = $this->disk->putFileAs(
+                $currentClient . '/members/' . $memberId . '/images',
+                $imageFile,
+                $filename,
+                'private'
+            );
+
+            if (!$uploaded) {
+                throw new Exception('Failed to upload profile image to S3');
+            }
+
+            Log::info("Profile image uploaded", [
+                'member_id' => $memberId,
+                'path' => $s3Path,
+                'size' => $imageFile->getSize()
+            ]);
+
+            return $s3Path;
+
+        } catch (Exception $e) {
+            Log::error("Error uploading profile image", [
+                'member_id' => $memberId,
+                'error' => $e->getMessage()
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * Upload single document file
+     *
+     * @param UploadedFile $documentFile
+     * @param int $memberId
+     * @param string $documentType ('self', 'father', 'mother')
+     * @return string
+     * @throws Exception
+     */
+    public function uploadDocument(UploadedFile $documentFile, int $memberId, string $documentType = 'self'): string
+    {
+        try {
+            $currentClient = session('currentClient', 'default');
+
+            // Validate file
+            $this->validateFile($documentFile, 'document');
+
+            // Create filename
+            $originalName = pathinfo($documentFile->getClientOriginalName(), PATHINFO_FILENAME);
+            $extension = strtolower($documentFile->getClientOriginalExtension());
+            $filename = $documentType . '_' . $originalName . '_' . time() . '.' . $extension;
+
+            // Upload to S3
+            $s3Path = $currentClient . '/members/' . $memberId . '/documents/' . $filename;
+
+            $uploaded = $this->disk->putFileAs(
+                $currentClient . '/members/' . $memberId . '/documents',
+                $documentFile,
+                $filename,
+                'private'
+            );
+
+            if (!$uploaded) {
+                throw new Exception('Failed to upload document to S3: ' . $originalName);
+            }
+
+            Log::info("Document uploaded", [
+                'member_id' => $memberId,
+                'type' => $documentType,
+                'path' => $s3Path,
+                'original_name' => $documentFile->getClientOriginalName()
+            ]);
+
+            return $s3Path;
+
+        } catch (Exception $e) {
+            Log::error("Error uploading document", [
+                'member_id' => $memberId,
+                'type' => $documentType,
+                'error' => $e->getMessage()
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * Upload certificate file
+     *
+     * @param UploadedFile $certificateFile
+     * @param int $memberId
+     * @return string
+     * @throws Exception
+     */
+    public function uploadCertificate(UploadedFile $certificateFile, int $memberId): string
+    {
+        try {
+            $currentClient = session('currentClient', 'default');
+
+            // Validate file
+            $this->validateFile($certificateFile, 'document');
+
+            // Create filename
+            $extension = strtolower($certificateFile->getClientOriginalExtension());
+            $filename = 'certificate_' . $memberId . '_' . time() . '.' . $extension;
+
+            // Upload to S3
+            $s3Path = $currentClient . '/members/' . $memberId . '/certificates/' . $filename;
+
+            $uploaded = $this->disk->putFileAs(
+                $currentClient . '/members/' . $memberId . '/certificates',
+                $certificateFile,
+                $filename,
+                'private'
+            );
+
+            if (!$uploaded) {
+                throw new Exception('Failed to upload certificate to S3');
+            }
+
+            Log::info("Certificate uploaded", [
+                'member_id' => $memberId,
+                'path' => $s3Path,
+                'size' => $certificateFile->getSize()
+            ]);
+
+            return $s3Path;
+
+        } catch (Exception $e) {
+            Log::error("Error uploading certificate", [
+                'member_id' => $memberId,
+                'error' => $e->getMessage()
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * Get file URL for display
+     *
+     * @param string $filePath
+     * @param string $expiresIn
+     * @return string|null
+     */
+    public function getFileUrl(string $filePath, string $expiresIn = '+1 hour'): ?string
+    {
+        if (!$filePath) {
+            return null;
+        }
+
+        // Handle legacy local paths - return asset URL
+        if (!$this->isS3Path($filePath)) {
+            return asset('storage/' . $filePath);
+        }
+
+        try {
+            if (!$this->disk->exists($filePath)) {
+                Log::warning("File not found", ['path' => $filePath]);
+                return null;
+            }
+
+            return $this->disk->temporaryUrl($filePath, now()->add($expiresIn));
+
+        } catch (Exception $e) {
+            Log::error("Error generating file URL", [
+                'path' => $filePath,
+                'error' => $e->getMessage()
+            ]);
+            return null;
+        }
+    }
+
+    /**
+     * Delete file from S3
+     *
+     * @param string $filePath
+     * @return bool
+     */
+    public function deleteFile(string $filePath): bool
+    {
+        // Don't try to delete local files
+        if (!$this->isS3Path($filePath)) {
+            return false;
+        }
+
+        try {
+            if ($this->disk->exists($filePath)) {
+                $this->disk->delete($filePath);
+                Log::info("File deleted", ['path' => $filePath]);
+                return true;
+            }
+            return false;
+
+        } catch (Exception $e) {
+            Log::error("Error deleting file", [
+                'path' => $filePath,
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * Check if path is S3 path
+     */
+    private function isS3Path(string $path): bool
+    {
+        return strpos($path, '/members/') !== false ||
+               strpos($path, session('currentClient', 'default')) === 0;
+    }
+
+    /**
+     * Delete existing profile image for member
+     *
+     * @param int $memberId
+     */
+    private function deleteExistingProfileImage(int $memberId): void
+    {
+        try {
+            $currentClient = session('currentClient', 'default');
+            $imagePath = $currentClient . '/members/' . $memberId . '/images/';
+
+            // List all files in the member's image directory
+            $files = $this->disk->files($imagePath);
+
+            foreach ($files as $file) {
+                if (strpos(basename($file), 'profile_' . $memberId) === 0) {
+                    $this->disk->delete($file);
+                    Log::info("Deleted existing profile image", [
+                        'member_id' => $memberId,
+                        'file' => $file
+                    ]);
+                }
+            }
+
+        } catch (Exception $e) {
+            Log::warning("Error deleting existing profile image", [
+                'member_id' => $memberId,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * Validate uploaded file
+     *
+     * @param UploadedFile $file
+     * @param string $type ('image' or 'document')
+     * @throws Exception
+     */
+    private function validateFile(UploadedFile $file, string $type): void
+    {
+        // Check if file is valid
+        if (!$file->isValid()) {
+            throw new Exception('Invalid file upload');
+        }
+
+        $extension = strtolower($file->getClientOriginalExtension());
+        $allowedExtensions = $type === 'image' ? self::ALLOWED_IMAGE_EXTENSIONS : self::ALLOWED_DOCUMENT_EXTENSIONS;
+        $maxSize = $type === 'image' ? self::MAX_IMAGE_SIZE : self::MAX_DOCUMENT_SIZE;
+
+        // Check file extension
+        if (!in_array($extension, $allowedExtensions)) {
+            throw new Exception("File type not allowed. Allowed types: " . implode(', ', $allowedExtensions));
+        }
+
+        // Check file size
+        if ($file->getSize() > ($maxSize * 1024)) {
+            throw new Exception("File size exceeds maximum allowed size of {$maxSize}KB");
+        }
+
+        // Check mime type for images
+        if ($type === 'image') {
+            $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif'];
+            if (!in_array($file->getMimeType(), $allowedMimeTypes)) {
+                throw new Exception('Invalid image file type');
+            }
+        }
+    }
+
+    /**
+     * Create member folder structure in S3
+     *
+     * @param int $memberId
+     */
+    public function createMemberFolders(int $memberId): void
+    {
+        try {
+            $currentClient = session('currentClient', 'default');
+
+            $folders = [
+                $currentClient . '/members/' . $memberId . '/images/.gitkeep',
+                $currentClient . '/members/' . $memberId . '/documents/.gitkeep',
+                $currentClient . '/members/' . $memberId . '/certificates/.gitkeep'
+            ];
+
+            foreach ($folders as $folder) {
+                if (!$this->disk->exists($folder)) {
+                    $this->disk->put($folder, '');
+                }
+            }
+
+            Log::info("Created member folder structure", ['member_id' => $memberId]);
+
+        } catch (Exception $e) {
+            Log::error("Error creating member folders", [
+                'member_id' => $memberId,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * Get file info
+     *
+     * @param string $filePath
+     * @return array|null
+     */
+    public function getFileInfo(string $filePath): ?array
+    {
+        if (!$filePath) {
+            return null;
+        }
+
+        // Handle legacy local paths
+        if (!$this->isS3Path($filePath)) {
+            $localPath = storage_path('app/public/' . $filePath);
+            if (file_exists($localPath)) {
+                return [
+                    'path' => $filePath,
+                    'name' => basename($filePath),
+                    'size' => filesize($localPath),
+                    'last_modified' => filemtime($localPath),
+                    'url' => $this->getFileUrl($filePath),
+                    'exists' => true,
+                    'storage_type' => 'local'
+                ];
+            }
+            return null;
+        }
+
+        if (!$this->disk->exists($filePath)) {
+            return null;
+        }
+
+        try {
+            return [
+                'path' => $filePath,
+                'name' => basename($filePath),
+                'size' => $this->disk->size($filePath),
+                'last_modified' => $this->disk->lastModified($filePath),
+                'url' => $this->getFileUrl($filePath),
+                'exists' => true,
+                'storage_type' => 's3'
+            ];
+
+        } catch (Exception $e) {
+            Log::error("Error getting file info", [
+                'path' => $filePath,
+                'error' => $e->getMessage()
+            ]);
+            return null;
+        }
+    }
+}

+ 210 - 0
app/Traits/HandlesS3Files.php

@@ -0,0 +1,210 @@
+<?php
+
+namespace App\Traits;
+
+use App\Services\MemberFileService;
+use Illuminate\Support\Facades\Log;
+
+trait HandlesS3Files
+{
+    /**
+     * Get file service instance
+     */
+    protected function getFileService(): MemberFileService
+    {
+        return app(MemberFileService::class);
+    }
+
+    /**
+     * Get file URL for display
+     */
+    public function getFileUrl(?string $filePath, string $expiresIn = '+1 hour'): ?string
+    {
+        if (!$filePath) {
+            return null;
+        }
+
+        return $this->getFileService()->getFileUrl($filePath, $expiresIn);
+    }
+
+    /**
+     * Check if file exists in S3
+     */
+    public function fileExists(?string $filePath): bool
+    {
+        if (!$filePath) {
+            return false;
+        }
+
+        try {
+            return \Illuminate\Support\Facades\Storage::disk('s3')->exists($filePath);
+        } catch (\Exception $e) {
+            Log::warning("Error checking file existence", [
+                'path' => $filePath,
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * Get file info
+     */
+    public function getFileInfo(?string $filePath): ?array
+    {
+        if (!$filePath) {
+            return null;
+        }
+
+        return $this->getFileService()->getFileInfo($filePath);
+    }
+
+    /**
+     * Delete file from S3
+     */
+    public function deleteFile(string $filePath): bool
+    {
+        return $this->getFileService()->deleteFile($filePath);
+    }
+
+    /**
+     * Get file name from path
+     */
+    public function getFileName(?string $filePath): ?string
+    {
+        if (!$filePath) {
+            return null;
+        }
+
+        return basename($filePath);
+    }
+
+    /**
+     * Check if path is S3 path
+     */
+    public function isS3Path(?string $path): bool
+    {
+        if (!$path) {
+            return false;
+        }
+
+        return strpos($path, '/members/') !== false ||
+               strpos($path, session('currentClient', 'default')) === 0;
+    }
+
+    /**
+     * Convert file paths array to string for database storage
+     */
+    public function filePathsToString(array $paths): string
+    {
+        return implode('|', array_filter($paths));
+    }
+
+    /**
+     * Convert file paths string from database to array
+     */
+    public function stringToFilePaths(?string $pathsString): array
+    {
+        if (!$pathsString) {
+            return [];
+        }
+
+        return array_filter(explode('|', $pathsString));
+    }
+
+    /**
+     * Get multiple file URLs
+     */
+    public function getMultipleFileUrls(array $filePaths): array
+    {
+        $urls = [];
+        foreach ($filePaths as $path) {
+            $url = $this->getFileUrl($path);
+            if ($url) {
+                $urls[$path] = $url;
+            }
+        }
+        return $urls;
+    }
+
+    /**
+     * Clean up files for deleted member
+     */
+    public function cleanupMemberFiles(int $memberId): void
+    {
+        try {
+            $currentClient = session('currentClient', 'default');
+            $memberPath = $currentClient . '/members/' . $memberId . '/';
+
+            $disk = \Illuminate\Support\Facades\Storage::disk('s3');
+
+            // Delete all files in member's folder
+            $files = $disk->allFiles($memberPath);
+            foreach ($files as $file) {
+                $disk->delete($file);
+            }
+
+            // Delete empty directories
+            $directories = $disk->allDirectories($memberPath);
+            foreach (array_reverse($directories) as $directory) {
+                $disk->deleteDirectory($directory);
+            }
+
+            Log::info("Cleaned up files for deleted member", ['member_id' => $memberId]);
+
+        } catch (\Exception $e) {
+            Log::error("Error cleaning up member files", [
+                'member_id' => $memberId,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * Validate file upload
+     */
+    public function validateFileUpload($file, string $type = 'document'): array
+    {
+        $errors = [];
+
+        if (!$file) {
+            $errors[] = 'No file provided';
+            return $errors;
+        }
+
+        if (!$file->isValid()) {
+            $errors[] = 'Invalid file upload';
+            return $errors;
+        }
+
+        $extension = strtolower($file->getClientOriginalExtension());
+        $size = $file->getSize();
+
+        if ($type === 'image') {
+            $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
+            $maxSize = 2048 * 1024; // 2MB
+
+            if (!in_array($extension, $allowedExtensions)) {
+                $errors[] = 'Invalid image format. Allowed: ' . implode(', ', $allowedExtensions);
+            }
+
+            if ($size > $maxSize) {
+                $errors[] = 'Image too large. Maximum size: 2MB';
+            }
+
+        } elseif ($type === 'document') {
+            $allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'];
+            $maxSize = 10240 * 1024; // 10MB
+
+            if (!in_array($extension, $allowedExtensions)) {
+                $errors[] = 'Invalid document format. Allowed: ' . implode(', ', $allowedExtensions);
+            }
+
+            if ($size > $maxSize) {
+                $errors[] = 'Document too large. Maximum size: 10MB';
+            }
+        }
+
+        return $errors;
+    }
+}

+ 86 - 23
resources/views/livewire/member.blade.php

@@ -11,9 +11,19 @@
                             <div class="avatar--wrapper d-flex align-items-center justify-content-between w-50">
 
                                 <figure class="m-0 avatar--wrapper_img">
-                                    @if ($currentMember->image != '')
-                                        <img src="{{ asset('storage/app/public/'.$currentMember->image) }}" style="max-width:200px">
-                                    @endif
+                                       @if ($currentMember->image != '')
+                                            @php
+                                                $fileService = app(App\Services\MemberFileService::class);
+                                                $imageUrl = $fileService->getFileUrl($currentMember->image);
+                                            @endphp
+                                            @if($imageUrl)
+                                                <img src="{{ $imageUrl }}" style="max-width:200px" alt="Profile Image">
+                                            @else
+                                                <div style="width: 200px; height: 150px; background: #f0f0f0; display: flex; align-items: center; justify-content: center;">
+                                                    <span style="color: #999;">Image not available</span>
+                                                </div>
+                                            @endif
+                                        @endif
                                 </figure>
                             </div>
                         </header>
@@ -601,10 +611,16 @@
                                                             @error('image') <span class="error">{{ $message }}</span> @enderror
                                                             <input class="form-control" type="file" wire:model="image">
                                                             @if ($image)
-                                                                <img src="{{ $image->temporaryUrl() }}" style="max-width:200px">
+                                                                <img src="{{ $image->temporaryUrl() }}" style="max-width:200px" alt="Preview">
                                                             @endif
                                                             @if ($image_old)
-                                                                <img src="{{ asset('storage/app/public/'.$image_old) }}" style="max-width:200px">
+                                                                @php
+                                                                    $fileService = app(App\Services\MemberFileService::class);
+                                                                    $imageUrl = $fileService->getFileUrl($image_old);
+                                                                @endphp
+                                                                @if($imageUrl)
+                                                                    <img src="{{ $imageUrl }}" style="max-width:200px" alt="Current Image">
+                                                                @endif
                                                             @endif
                                                         </div>
                                                     </div>
@@ -736,19 +752,28 @@
                                                         <input class="form-control" type="file" wire:model="documents" multiple><br>
                                                         <label for="document_files" class="form-label">Caricati</label>
                                                         @if ($document_files !== null && count(array_filter($document_files)) > 0)
-                                                            @foreach ($document_files as $idx => $d)
-                                                                @if (!empty($d))
-                                                                    <div class="row">
-                                                                        <div class="col-6">
-                                                                            <a href="{{ asset('storage/app/public/'.$d) }}" target="_blank" class="form-label">{{$d}}</a>
-                                                                        </div>
-                                                                        <div class="col-6">
-                                                                            <a wire:click="removeDocument({{$idx}},'self')" class="form-label">(elimina)</a><br>
-                                                                        </div>
+                                                        @foreach ($document_files as $idx => $d)
+                                                            @if (!empty($d))
+                                                                <div class="row">
+                                                                    <div class="col-6">
+                                                                        @php
+                                                                            $fileService = app(App\Services\MemberFileService::class);
+                                                                            $fileUrl = $fileService->getFileUrl($d);
+                                                                            $fileName = basename($d);
+                                                                        @endphp
+                                                                        @if($fileUrl)
+                                                                            <a href="{{ $fileUrl }}" target="_blank" class="form-label">{{ $fileName }}</a>
+                                                                        @else
+                                                                            <span class="form-label text-muted">{{ $fileName }} (not available)</span>
+                                                                        @endif
                                                                     </div>
-                                                                @endif
-                                                            @endforeach
-                                                        @endif
+                                                                    <div class="col-6">
+                                                                        <a wire:click="removeDocument({{$idx}},'self')" class="form-label" style="cursor: pointer;">(elimina)</a><br>
+                                                                    </div>
+                                                                </div>
+                                                            @endif
+                                                        @endforeach
+                                                    @endif
                                                     </div>
                                                 </div>
 
@@ -795,10 +820,19 @@
                                                                     @if (!empty($d))
                                                                         <div class="row">
                                                                             <div class="col-6">
-                                                                                <a href="{{ asset('storage/app/public/'.$d) }}" target="_blank" class="form-label">{{$d}}</a>
+                                                                                @php
+                                                                                    $fileService = app(App\Services\MemberFileService::class);
+                                                                                    $fileUrl = $fileService->getFileUrl($d);
+                                                                                    $fileName = basename($d);
+                                                                                @endphp
+                                                                                @if($fileUrl)
+                                                                                    <a href="{{ $fileUrl }}" target="_blank" class="form-label">{{ $fileName }}</a>
+                                                                                @else
+                                                                                    <span class="form-label text-muted">{{ $fileName }} (not available)</span>
+                                                                                @endif
                                                                             </div>
                                                                             <div class="col-6">
-                                                                                <a wire:click="removeDocument({{$idx}},'father')" class="form-label">(elimina)</a><br>
+                                                                                <a wire:click="removeDocument({{$idx}},'father')" class="form-label" style="cursor: pointer;">(elimina)</a><br>
                                                                             </div>
                                                                         </div>
                                                                     @endif
@@ -848,10 +882,19 @@
                                                                 @if (!empty($d))
                                                                     <div class="row">
                                                                         <div class="col-6">
-                                                                            <a href="{{ asset('storage/app/public/'.$d) }}" target="_blank" class="form-label">{{$d}}</a>
+                                                                            @php
+                                                                                $fileService = app(App\Services\MemberFileService::class);
+                                                                                $fileUrl = $fileService->getFileUrl($d);
+                                                                                $fileName = basename($d);
+                                                                            @endphp
+                                                                            @if($fileUrl)
+                                                                                <a href="{{ $fileUrl }}" target="_blank" class="form-label">{{ $fileName }}</a>
+                                                                            @else
+                                                                                <span class="form-label text-muted">{{ $fileName }} (not available)</span>
+                                                                            @endif
                                                                         </div>
                                                                         <div class="col-6">
-                                                                            <a wire:click="removeDocument({{$idx}},'mother')" class="form-label">(elimina)</a><br>
+                                                                            <a wire:click="removeDocument({{$idx}},'mother')" class="form-label" style="cursor: pointer;">(elimina)</a><br>
                                                                         </div>
                                                                     </div>
                                                                 @endif
@@ -879,7 +922,19 @@
                                                                         <tr>
                                                                             <td>{{$member_certificate->type == 'A' ? 'Agonistico' : 'Non agonistico'}}</td>
                                                                             <td>{{$member_certificate->expire_date ? date("d/m/Y", strtotime($member_certificate->expire_date)) : ''}}</td>
-                                                                            <td>{!!$member_certificate->filename != '' ? '<a href="/storage/app/public/' . $member_certificate->filename . '" target="_blank">Visualizza</a>' : ''!!}</td>
+                                                                            <td>
+                                                                                @if($member_certificate->filename != '')
+                                                                                    @php
+                                                                                        $fileService = app(App\Services\MemberFileService::class);
+                                                                                        $certificateUrl = $fileService->getFileUrl($member_certificate->filename);
+                                                                                    @endphp
+                                                                                    @if($certificateUrl)
+                                                                                        <a href="{{ $certificateUrl }}" target="_blank">Visualizza</a>
+                                                                                    @else
+                                                                                        <span class="text-muted">File non disponibile</span>
+                                                                                    @endif
+                                                                                @endif
+                                                                            </td>
                                                                             <td>
                                                                                 <button type="button" class="btn" wire:click="editCertificate({{ $member_certificate->id }})" data-bs-toggle="popover"  data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Modifica"><i class="fa-regular fa-pen-to-square"></i></button>
                                                                                 <button type="button" class="btn" onclick="confirm('Sei sicuro?') || event.stopImmediatePropagation()" wire:click="deleteCertificate({{ $member_certificate->id }})" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Elimina"><i class="fa-regular fa-trash-can"></i></button>
@@ -911,7 +966,15 @@
                                                                             <input class="form-control" type="file" wire:model="certificate_filename">
                                                                         </div>
                                                                         <p class="caption text-center mt-1">Formati consentiti: .jpg, .pdf, .docx</p>
-                                                                        {!!$certificate_filename_old != '' ? '<br><a href="/storage/app/public/' . $certificate_filename_old . '" target="_blank">Visualizza</a>' : ''!!}
+                                                                        @if($certificate_filename_old != '')
+                                                                            @php
+                                                                                $fileService = app(App\Services\MemberFileService::class);
+                                                                                $certificateUrl = $fileService->getFileUrl($certificate_filename_old);
+                                                                            @endphp
+                                                                            @if($certificateUrl)
+                                                                                <br><a href="{{ $certificateUrl }}" target="_blank">Visualizza certificato corrente</a>
+                                                                            @endif
+                                                                        @endif
                                                                     </div>
                                                                 </form>
                                                             </div>