Quellcode durchsuchen

ricevuta multiclient e logo s3

FabioFratini vor 8 Monaten
Ursprung
Commit
d95ae779ee

+ 67 - 37
app/Http/Livewire/Azienda.php

@@ -7,8 +7,10 @@ use Livewire\WithFileUploads;
 use Illuminate\Support\Str;
 
 use App\Models\Azienda as AziendaModel;
+use App\Services\LogoUploadServices;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
+
 class Azienda extends Component
 {
     use WithFileUploads;
@@ -215,6 +217,7 @@ class Azienda extends Component
     {
         $this->validate();
         Log::info('Saving discipline: ' . json_encode(implode('; ', $this->selectedDisciplines)));
+
         try {
             $data = [
                 'ragione_sociale' => $this->ragione_sociale,
@@ -245,47 +248,41 @@ class Azienda extends Component
                 'codice_fiscale' => $this->codice_fiscale,
                 'codice_sdi' => $this->codice_sdi,
             ];
-            Log::info('Data logo: ' . json_encode(value: $this->temp_logo));
-            if ($this->temp_logo) {
-                $folderName = Str::slug($this->nome_associazione);
-                Log::info('Folder name: ' . $folderName);
-                $path = 'img/' . $folderName;
-                $fullPath = storage_path('app/public/' . $path);
-                Log::info('Full path: ' . $fullPath);
-                Log::info('Computed Full path: ' . $fullPath);
-
-                Log::info('Directory exists check: ' . (file_exists($fullPath) ? 'Yes' : 'No'));
-
-                if (!file_exists($fullPath)) {
-                    $result = mkdir($fullPath, 0755, true);
-                    if ($result === false) {
-                        Log::error('Failed to create directory: ' . $fullPath);
-                        session()->flash('error', 'Errore durante la creazione della cartella del logo.');
-                        return;
-                    }
-                    Log::info('Directory created: ' . $fullPath);
-                }
 
-                try {
-                    $logoPath = $this->temp_logo->store($path, 'public');
-                    $data['logo'] = $logoPath;
-                    Log::info('Logo path: ' . $logoPath);
-                } catch (\Exception $e) {
-                    Log::error('Error saving logo: ' . $e->getMessage());
-                    session()->flash('error', 'Errore durante il salvataggio del logo: ' . $e->getMessage());
-                    return;
-                }
-            }
+                      // Create or update azienda first
             if ($this->azienda) {
                 $this->azienda->update($data);
-                session()->flash('message', 'Dati aziendali aggiornati con successo!');
             } else {
                 $this->azienda = AziendaModel::create($data);
-                session()->flash('message', 'Dati aziendali creati con successo!');
             }
 
+            // Handle logo upload using the service
+            if ($this->temp_logo) {
+                try {
+                    Log::info('Starting logo upload with service');
+                    $logoService = app(LogoUploadServices::class);
+                    $logoPath = $logoService->uploadLogo($this->temp_logo, $this->azienda);
+                    Log::info('Logo uploaded successfully to: ' . $logoPath);
+
+                    // Reset temp logo after successful upload
+                    $this->temp_logo = null;
+
+                } catch (\Exception $e) {
+                    Log::error('Error uploading logo via service: ' . $e->getMessage());
+                    session()->flash('error', 'Errore durante il caricamento del logo: ' . $e->getMessage());
+                    return;
+                }
+            }
+
+            session()->flash('message', $this->azienda->wasRecentlyCreated ?
+                'Dati aziendali creati con successo!' :
+                'Dati aziendali aggiornati con successo!'
+            );
+
             $this->update = false;
+
         } catch (\Exception $ex) {
+            Log::error('Error in save method: ' . $ex->getMessage());
             session()->flash('error', 'Errore: ' . $ex->getMessage());
         }
     }
@@ -339,13 +336,46 @@ class Azienda extends Component
         $this->loadDisciplines();
     }
 
-    public function removeLogo()
+      public function removeLogo()
+    {
+        if ($this->azienda) {
+            try {
+                $logoService = app(LogoUploadServices::class);
+                if ($logoService->deleteLogo($this->azienda)) {
+                    $this->logo = null;
+                    session()->flash('message', 'Logo rimosso con successo!');
+                    Log::info('Logo removed successfully for azienda: ' . $this->azienda->id);
+                } else {
+                    session()->flash('error', 'Nessun logo da rimuovere.');
+                }
+            } catch (\Exception $e) {
+                Log::error('Error removing logo: ' . $e->getMessage());
+                session()->flash('error', 'Errore durante la rimozione del logo: ' . $e->getMessage());
+            }
+        }
+    }
+
+    /**
+     * Get logo URL for display
+     */
+    public function getLogoUrlProperty()
     {
         if ($this->azienda && $this->azienda->logo) {
-            Storage::disk('public')->delete($this->azienda->logo);
-            $this->azienda->logo = null;
-            $this->azienda->save();
-            session()->flash('message', 'Logo rimosso con successo!');
+            $logoService = app(LogoUploadServices::class);
+            return $logoService->getLogoUrl($this->azienda);
+        }
+        return null;
+    }
+
+    /**
+     * Check if logo exists
+     */
+    public function getHasLogoProperty()
+    {
+        if ($this->azienda) {
+            $logoService = app(LogoUploadServices::class);
+            return $logoService->logoExists($this->azienda);
         }
+        return false;
     }
 }

+ 350 - 0
app/Services/LogoUploadServices.php

@@ -0,0 +1,350 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Http\UploadedFile;
+use Exception;
+
+class LogoUploadServices
+{
+    /**
+     * The storage disk to use for file operations
+     */
+    private $disk;
+
+    /**
+     * Allowed file extensions for logos
+     */
+    private const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
+
+    /**
+     * Maximum file size in KB
+     */
+    private const MAX_FILE_SIZE = 2048;
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->disk = Storage::disk('s3');
+    }
+
+    /**
+     * Upload logo and manage client sub-folder
+     *
+     * @param UploadedFile $logoFile
+     * @param mixed $azienda
+     * @return string
+     * @throws Exception
+     */
+    public function uploadLogo(UploadedFile $logoFile, $azienda): string
+    {
+        try {
+            $currentClient = 'iao';
+
+            if (!$currentClient) {
+                throw new Exception('No current client found in session');
+            }
+
+            $this->validateLogoFile($logoFile);
+
+            $this->ensureClientFolderExists($currentClient);
+
+            $this->deleteExistingLogo($currentClient);
+
+            $logoPath = $this->uploadNewLogo($logoFile, $currentClient);
+
+            $azienda->logo = $logoPath;
+            $azienda->save();
+
+            Log::info("Logo uploaded successfully", [
+                'client' => $currentClient,
+                'path' => $logoPath,
+                'azienda_id' => $azienda->id ?? null
+            ]);
+
+            return $logoPath;
+
+        } catch (Exception $e) {
+            Log::error('Error uploading logo', [
+                'message' => $e->getMessage(),
+                'client' => session('currentClient'),
+                'azienda_id' => $azienda->id ?? null
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * Validate the uploaded logo file
+     *
+     * @param UploadedFile $logoFile
+     * @throws Exception
+     */
+    private function validateLogoFile(UploadedFile $logoFile): void
+    {
+        // Check if file is valid
+        if (!$logoFile->isValid()) {
+            throw new Exception('Invalid file upload');
+        }
+
+        // Check file size
+        if ($logoFile->getSize() > (self::MAX_FILE_SIZE * 1024)) {
+            throw new Exception('File size exceeds maximum allowed size of ' . self::MAX_FILE_SIZE . 'KB');
+        }
+
+        // Check file extension
+        $extension = strtolower($logoFile->getClientOriginalExtension());
+        if (!in_array($extension, self::ALLOWED_EXTENSIONS)) {
+            throw new Exception('File type not allowed. Allowed types: ' . implode(', ', self::ALLOWED_EXTENSIONS));
+        }
+
+        // Check mime type
+        $mimeType = $logoFile->getMimeType();
+        $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+        if (!in_array($mimeType, $allowedMimeTypes)) {
+            throw new Exception('Invalid file type');
+        }
+    }
+
+    /**
+     * Ensure client folder exists in the bucket
+     *
+     * @param string $clientName
+     * @throws Exception
+     */
+    private function ensureClientFolderExists(string $clientName): void
+    {
+        try {
+            $files = $this->disk->files($clientName);
+
+            if (empty($files)) {
+                $placeholderPath = $clientName . '/.gitkeep';
+                $this->disk->put($placeholderPath, '');
+
+                Log::info("Created client folder", ['client' => $clientName]);
+            }
+
+        } catch (Exception $e) {
+            Log::error("Error creating client folder", [
+                'client' => $clientName,
+                'error' => $e->getMessage()
+            ]);
+            throw new Exception("Failed to create client folder: " . $e->getMessage());
+        }
+    }
+
+    /**
+     * Delete all existing logo files for the client
+     *
+     * @param string $clientName
+     */
+    private function deleteExistingLogo(string $clientName): void
+    {
+        try {
+            foreach (self::ALLOWED_EXTENSIONS as $extension) {
+                $logoPath = $clientName . '/logo.' . $extension;
+
+                if ($this->disk->exists($logoPath)) {
+                    $this->disk->delete($logoPath);
+                    Log::info("Deleted existing logo", [
+                        'client' => $clientName,
+                        'file' => "logo.{$extension}"
+                    ]);
+                }
+            }
+
+        } catch (Exception $e) {
+            Log::warning("Error deleting existing logo", [
+                'client' => $clientName,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * Upload the new logo file
+     *
+     * @param UploadedFile $logoFile
+     * @param string $clientName
+     * @return string
+     * @throws Exception
+     */
+    private function uploadNewLogo(UploadedFile $logoFile, string $clientName): string
+    {
+        try {
+            $extension = strtolower($logoFile->getClientOriginalExtension());
+
+            $fileName = 'logo.' . $extension;
+
+            $logoPath = $this->disk->putFileAs(
+                $clientName,
+                $logoFile,
+                $fileName,
+                'private'
+            );
+
+            if (!$logoPath) {
+                throw new Exception('Failed to upload file to storage');
+            }
+
+            Log::info("Logo file uploaded", [
+                'client' => $clientName,
+                'path' => $logoPath,
+                'size' => $logoFile->getSize()
+            ]);
+
+            return $logoPath;
+
+        } catch (Exception $e) {
+            Log::error("Error uploading logo file", [
+                'client' => $clientName,
+                'error' => $e->getMessage()
+            ]);
+            throw new Exception("Failed to upload logo: " . $e->getMessage());
+        }
+    }
+
+    /**
+     * Get a temporary URL for the logo
+     *
+     * @param mixed $azienda
+     * @param string $expiresIn
+     * @return string|null
+     */
+    public function getLogoUrl($azienda, string $expiresIn = '+1 hour'): ?string
+    {
+        if (!$azienda->logo) {
+            return null;
+        }
+
+        try {
+            if (!$this->disk->exists($azienda->logo)) {
+                Log::warning("Logo file not found", ['path' => $azienda->logo]);
+                return null;
+            }
+
+            return $this->disk->temporaryUrl($azienda->logo, now()->add($expiresIn));
+
+        } catch (Exception $e) {
+            Log::error("Error generating logo URL", [
+                'path' => $azienda->logo,
+                'error' => $e->getMessage()
+            ]);
+            return null;
+        }
+    }
+
+    /**
+     * Check if logo file exists
+     *
+     * @param mixed $azienda
+     * @return bool
+     */
+    public function logoExists($azienda): bool
+    {
+        if (!$azienda->logo) {
+            return false;
+        }
+
+        return $this->disk->exists($azienda->logo);
+    }
+
+    /**
+     * Delete logo file and update database
+     *
+     * @param mixed $azienda
+     * @return bool
+     */
+    public function deleteLogo($azienda): bool
+    {
+        try {
+            if ($azienda->logo && $this->disk->exists($azienda->logo)) {
+                $this->disk->delete($azienda->logo);
+
+                Log::info("Logo deleted", [
+                    'path' => $azienda->logo,
+                    'azienda_id' => $azienda->id ?? null
+                ]);
+
+                $azienda->logo = null;
+                $azienda->save();
+
+                return true;
+            }
+
+            return false;
+
+        } catch (Exception $e) {
+            Log::error("Error deleting logo", [
+                'path' => $azienda->logo ?? 'unknown',
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * Get logo file information
+     *
+     * @param mixed $azienda
+     * @return array|null
+     */
+    public function getLogoInfo($azienda): ?array
+    {
+        if (!$azienda->logo || !$this->disk->exists($azienda->logo)) {
+            return null;
+        }
+
+        try {
+            return [
+                'path' => $azienda->logo,
+                'size' => $this->disk->size($azienda->logo),
+                'last_modified' => $this->disk->lastModified($azienda->logo),
+                'url' => $this->getLogoUrl($azienda),
+                'exists' => true
+            ];
+
+        } catch (Exception $e) {
+            Log::error("Error getting logo info", [
+                'path' => $azienda->logo,
+                'error' => $e->getMessage()
+            ]);
+            return null;
+        }
+    }
+
+    /**
+     * List all logos for a client
+     *
+     * @param string|null $clientName
+     * @return array
+     */
+    public function listClientLogos(?string $clientName = null): array
+    {
+        $clientName = $clientName ?? session('currentClient');
+
+        if (!$clientName) {
+            return [];
+        }
+
+        try {
+            $files = $this->disk->files($clientName);
+
+            return array_filter($files, function($file) {
+                $filename = basename($file);
+                return strpos($filename, 'logo.') === 0;
+            });
+
+        } catch (Exception $e) {
+            Log::error("Error listing client logos", [
+                'client' => $clientName,
+                'error' => $e->getMessage()
+            ]);
+            return [];
+        }
+    }
+}

+ 1 - 0
composer.json

@@ -11,6 +11,7 @@
         "laravel/framework": "^9.19",
         "laravel/sanctum": "^3.0",
         "laravel/tinker": "^2.7",
+        "league/flysystem-aws-s3-v3": "^3.0",
         "livewire/livewire": "^2.12",
         "phpoffice/phpspreadsheet": "^2.0"
     },

+ 285 - 4
composer.lock

@@ -4,8 +4,157 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "929f88783ff020750e47e3ae1b2963b4",
+    "content-hash": "8aacf85495c8b6cb33caeb8ac21b78c3",
     "packages": [
+        {
+            "name": "aws/aws-crt-php",
+            "version": "v1.2.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/awslabs/aws-crt-php.git",
+                "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e",
+                "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
+                "yoast/phpunit-polyfills": "^1.0"
+            },
+            "suggest": {
+                "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "AWS SDK Common Runtime Team",
+                    "email": "aws-sdk-common-runtime@amazon.com"
+                }
+            ],
+            "description": "AWS Common Runtime for PHP",
+            "homepage": "https://github.com/awslabs/aws-crt-php",
+            "keywords": [
+                "amazon",
+                "aws",
+                "crt",
+                "sdk"
+            ],
+            "support": {
+                "issues": "https://github.com/awslabs/aws-crt-php/issues",
+                "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
+            },
+            "time": "2024-10-18T22:15:13+00:00"
+        },
+        {
+            "name": "aws/aws-sdk-php",
+            "version": "3.263.13",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/aws/aws-sdk-php.git",
+                "reference": "939120791996563677afe75a97ff18f514b7418f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/939120791996563677afe75a97ff18f514b7418f",
+                "reference": "939120791996563677afe75a97ff18f514b7418f",
+                "shasum": ""
+            },
+            "require": {
+                "aws/aws-crt-php": "^1.0.4",
+                "ext-json": "*",
+                "ext-pcre": "*",
+                "ext-simplexml": "*",
+                "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
+                "guzzlehttp/promises": "^1.4.0",
+                "guzzlehttp/psr7": "^1.8.5 || ^2.3",
+                "mtdowling/jmespath.php": "^2.6",
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "andrewsville/php-token-reflection": "^1.4",
+                "aws/aws-php-sns-message-validator": "~1.0",
+                "behat/behat": "~3.0",
+                "composer/composer": "^1.10.22",
+                "dms/phpunit-arraysubset-asserts": "^0.4.0",
+                "doctrine/cache": "~1.4",
+                "ext-dom": "*",
+                "ext-openssl": "*",
+                "ext-pcntl": "*",
+                "ext-sockets": "*",
+                "nette/neon": "^2.3",
+                "paragonie/random_compat": ">= 2",
+                "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5",
+                "psr/cache": "^1.0",
+                "psr/http-message": "<1.1",
+                "psr/simple-cache": "^1.0",
+                "sebastian/comparator": "^1.2.3 || ^4.0",
+                "yoast/phpunit-polyfills": "^1.0"
+            },
+            "suggest": {
+                "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
+                "doctrine/cache": "To use the DoctrineCacheAdapter",
+                "ext-curl": "To send requests using cURL",
+                "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
+                "ext-sockets": "To use client-side monitoring"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions.php"
+                ],
+                "psr-4": {
+                    "Aws\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Amazon Web Services",
+                    "homepage": "http://aws.amazon.com"
+                }
+            ],
+            "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
+            "homepage": "http://aws.amazon.com/sdkforphp",
+            "keywords": [
+                "amazon",
+                "aws",
+                "cloud",
+                "dynamodb",
+                "ec2",
+                "glacier",
+                "s3",
+                "sdk"
+            ],
+            "support": {
+                "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
+                "issues": "https://github.com/aws/aws-sdk-php/issues",
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.263.13"
+            },
+            "time": "2023-04-19T18:23:42+00:00"
+        },
         {
             "name": "barryvdh/laravel-dompdf",
             "version": "v2.1.1",
@@ -1834,6 +1983,72 @@
             ],
             "time": "2023-02-18T15:32:41+00:00"
         },
+        {
+            "name": "league/flysystem-aws-s3-v3",
+            "version": "3.23.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
+                "reference": "97728e7a0d40ec9c6147eb0f4ee4cdc6ff0a8240"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/97728e7a0d40ec9c6147eb0f4ee4cdc6ff0a8240",
+                "reference": "97728e7a0d40ec9c6147eb0f4ee4cdc6ff0a8240",
+                "shasum": ""
+            },
+            "require": {
+                "aws/aws-sdk-php": "^3.220.0",
+                "league/flysystem": "^3.10.0",
+                "league/mime-type-detection": "^1.0.0",
+                "php": "^8.0.2"
+            },
+            "conflict": {
+                "guzzlehttp/guzzle": "<7.0",
+                "guzzlehttp/ringphp": "<1.1.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "League\\Flysystem\\AwsS3V3\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Frank de Jonge",
+                    "email": "info@frankdejonge.nl"
+                }
+            ],
+            "description": "AWS S3 filesystem adapter for Flysystem.",
+            "keywords": [
+                "Flysystem",
+                "aws",
+                "file",
+                "files",
+                "filesystem",
+                "s3",
+                "storage"
+            ],
+            "support": {
+                "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
+                "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.23.1"
+            },
+            "funding": [
+                {
+                    "url": "https://ecologi.com/frankdejonge",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/frankdejonge",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-01-26T18:25:23+00:00"
+        },
         {
             "name": "league/mime-type-detection",
             "version": "1.11.0",
@@ -2320,6 +2535,72 @@
             ],
             "time": "2023-02-06T13:44:46+00:00"
         },
+        {
+            "name": "mtdowling/jmespath.php",
+            "version": "2.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/jmespath/jmespath.php.git",
+                "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
+                "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0",
+                "symfony/polyfill-mbstring": "^1.17"
+            },
+            "require-dev": {
+                "composer/xdebug-handler": "^3.0.3",
+                "phpunit/phpunit": "^8.5.33"
+            },
+            "bin": [
+                "bin/jp.php"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.8-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/JmesPath.php"
+                ],
+                "psr-4": {
+                    "JmesPath\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Declaratively specify how to extract elements from a JSON document",
+            "keywords": [
+                "json",
+                "jsonpath"
+            ],
+            "support": {
+                "issues": "https://github.com/jmespath/jmespath.php/issues",
+                "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0"
+            },
+            "time": "2024-09-04T18:46:31+00:00"
+        },
         {
             "name": "nesbot/carbon",
             "version": "2.66.0",
@@ -9027,12 +9308,12 @@
     ],
     "aliases": [],
     "minimum-stability": "stable",
-    "stability-flags": [],
+    "stability-flags": {},
     "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
         "php": "^8.0.2"
     },
-    "platform-dev": [],
-    "plugin-api-version": "2.3.0"
+    "platform-dev": {},
+    "plugin-api-version": "2.6.0"
 }

+ 6 - 7
config/filesystems.php

@@ -46,13 +46,12 @@ return [
 
         's3' => [
             'driver' => 's3',
-            'key' => env('AWS_ACCESS_KEY_ID'),
-            'secret' => env('AWS_SECRET_ACCESS_KEY'),
-            'region' => env('AWS_DEFAULT_REGION'),
-            'bucket' => env('AWS_BUCKET'),
-            'url' => env('AWS_URL'),
-            'endpoint' => env('AWS_ENDPOINT'),
-            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
+            'key' => env('S3_ACCESS_KEY_ID'),
+            'secret' => env('S3_SECRET_ACCESS_KEY'),
+            'region' => env('S3_DEFAULT_REGION', 'eu-south-1'),
+            'bucket' => env('S3_BUCKET'),
+            'endpoint' => env('S3_ENDPOINT', 'https://s3.wasabisys.com'),
+            'use_path_style_endpoint' => true,
             'throw' => false,
         ],
 

+ 202 - 163
resources/views/livewire/azienda.blade.php

@@ -1,217 +1,257 @@
 <div>
 
-        <div class="col-md-12">
-            <div class="card">
-                <div class="card-body">
-                    @if (session()->has('message'))
-                        <div class="alert alert-success" role="alert">
-                            {{ session()->get('message') }}
-                        </div>
-                    @endif
+    <div class="col-md-12">
+        <div class="card">
+            <div class="card-body">
+                @if (session()->has('message'))
+                    <div class="alert alert-success" role="alert">
+                        {{ session()->get('message') }}
+                    </div>
+                @endif
 
-                    @if (session()->has('error'))
-                        <div class="alert alert-danger" role="alert">
-                            {{ session()->get('error') }}
-                        </div>
-                    @endif
-
-                    <div class="tab-content">
-                        <form wire:submit.prevent="save">
-                            <div class="row mb-4">
-                                <div class="col-md-4">
-                                    <div class="logo-container mb-3 d-flex align-items-center">
-                                        @if($azienda && $azienda->logo)
-                                            <div class="me-3">
-                                                <img src="{{ asset('storage/' . $azienda->logo) }}" alt="Logo" style="height: 200px; width: 200px;border-radius: 10px;">
+                @if (session()->has('error'))
+                    <div class="alert alert-danger" role="alert">
+                        {{ session()->get('error') }}
+                    </div>
+                @endif
+
+                <div class="tab-content">
+                    <form wire:submit.prevent="save">
+                        <div class="row mb-4">
+                            <div class="col-md-4">
+                                <div class="logo-container mb-3 d-flex align-items-center">
+                                    {{-- Display current logo from Wasabi --}}
+                                    @if($this->hasLogo && $this->logoUrl)
+                                        <div class="me-3 position-relative">
+                                            <img src="{{ $this->logoUrl }}" alt="Logo"
+                                                style="height: 200px; width: 200px; border-radius: 10px; object-fit: cover;">
+                                            {{-- Remove logo button --}}
+                                            <button type="button"
+                                                class="btn btn-sm btn-danger position-absolute top-0 end-0 m-1"
+                                                wire:click="removeLogo"
+                                                style="border-radius: 50%; width: 30px; height: 30px; padding: 0;"
+                                                title="Rimuovi logo">
+                                                <i class="fas fa-times"></i>
+                                            </button>
+                                        </div>
+                                    @endif
+
+                                    <div class="me-3 flex-grow-1 d-flex flex-column justify-content-center">
+                                        <div class="mt-2">
+                                            <label class="form-label">Logo</label>
+                                        </div>
+                                        <input type="file"
+                                            class="form-control form-control-lg @error('temp_logo') is-invalid @enderror"
+                                            wire:model="temp_logo" style="width: 300px;"
+                                            accept="image/jpeg,image/png,image/jpg,image/gif,image/webp">
+
+                                        @error('temp_logo')
+                                            <span class="text-danger">{{ $message }}</span>
+                                        @enderror
+
+                                        {{-- Preview new logo before upload --}}
+                                        @if ($temp_logo)
+                                            <div class="mt-2">
+                                                <p class="text-muted small">Anteprima nuovo logo:</p>
+                                                <img src="{{ $temp_logo->temporaryUrl() }}" class="mt-2"
+                                                    style="max-width: 200px; max-height: 200px; border-radius: 10px; object-fit: cover;">
                                             </div>
                                         @endif
-                                        <div class="me-3 flex-grow-1 d-flex flex-column justify-content-center">
-                                            <div class="mt-2">
-                                                <label class="form-label">Logo</label>
+
+                                        {{-- Loading indicator --}}
+                                        <div wire:loading wire:target="temp_logo" class="mt-2">
+                                            <div class="spinner-border spinner-border-sm text-primary" role="status">
+                                                <span class="visually-hidden">Caricamento...</span>
                                             </div>
-                                            <input type="file" class="form-control form-control-lg" wire:model="temp_logo" style="width: 300px;">
-                                            @if ($temp_logo)
-                                                <img src="{{ $temp_logo->temporaryUrl() }}" class="mt-2" style="max-width: 200px; max-height: 200px;">
-                                            @endif
+                                            <span class="text-primary ms-2">Caricamento logo...</span>
                                         </div>
                                     </div>
                                 </div>
                             </div>
+                        </div>
 
 
-                            <div class="mb-4">
+                        <div class="mb-4">
                             <h5 class="mb-3 blu-text">Anagrafica società</h5>
-                                <div class="row">
-                                    <div class="col-md-6 mb-3">
-                                        <label class="form-label">Ragione sociale*</label>
-                                        <input type="text" class="form-control form-control-lg @error('ragione_sociale') is-invalid @enderror"
-                                                wire:model="ragione_sociale">
-                                        @error('ragione_sociale') <span class="text-danger">{{ $message }}</span> @enderror
-                                    </div>
-                                    <div class="col-md-6 mb-3">
-                                        <label class="form-label">Nome associazione/società</label>
-                                        <input type="text" class="form-control form-control-lg" wire:model="nome_associazione">
-                                    </div>
+                            <div class="row">
+                                <div class="col-md-6 mb-3">
+                                    <label class="form-label">Ragione sociale*</label>
+                                    <input type="text"
+                                        class="form-control form-control-lg @error('ragione_sociale') is-invalid @enderror"
+                                        wire:model="ragione_sociale">
+                                    @error('ragione_sociale') <span class="text-danger">{{ $message }}</span> @enderror
                                 </div>
-                                <div class="row">
-                                    <div class="col-md-6 mb-3">
-                                        <label class="form-label">Tipologia* (ASD/SSD/Polisportiva ecc.)</label>
-                                        <input type="text" class="form-control form-control-lg" wire:model="tipologia">
-                                    </div>
-                                    <div class="col-md-6  mb-3">
-                                        <label class="form-label">Discipline</label>
-                                        <div wire:ignore>
-                                            <select class="form-select discipline-select" style="border-radius: 20px;" multiple wire:model="selectedDisciplines">
-                                                @forelse($discipline as $disciplineScelte)
-                                                    <option value="{{ $disciplineScelte->id }}">
-                                                        {{ $disciplineScelte->name }}
-                                                    </option>
-                                                @empty
-                                                    <option disabled>Nessuna disciplina trovata</option>
-                                                @endforelse
-                                            </select>
-                                        </div>
+                                <div class="col-md-6 mb-3">
+                                    <label class="form-label">Nome associazione/società</label>
+                                    <input type="text" class="form-control form-control-lg"
+                                        wire:model="nome_associazione">
+                                </div>
+                            </div>
+                            <div class="row">
+                                <div class="col-md-6 mb-3">
+                                    <label class="form-label">Tipologia* (ASD/SSD/Polisportiva ecc.)</label>
+                                    <input type="text" class="form-control form-control-lg" wire:model="tipologia">
+                                </div>
+                                <div class="col-md-6  mb-3">
+                                    <label class="form-label">Discipline</label>
+                                    <div wire:ignore>
+                                        <select class="form-select discipline-select" style="border-radius: 20px;"
+                                            multiple wire:model="selectedDisciplines">
+                                            @forelse($discipline as $disciplineScelte)
+                                                <option value="{{ $disciplineScelte->id }}">
+                                                    {{ $disciplineScelte->name }}
+                                                </option>
+                                            @empty
+                                                <option disabled>Nessuna disciplina trovata</option>
+                                            @endforelse
+                                        </select>
                                     </div>
                                 </div>
                             </div>
-                            <h5 class="mb-3 blu-text">Sede legale</h5>
+                        </div>
+                        <h5 class="mb-3 blu-text">Sede legale</h5>
+                        <div class="row">
+                            <div class="col-md-3 mb-3">
+                                <label class="form-label">Nazione</label>
+                                <input type="text" class="form-control" wire:model="sede_legale_nazione">
+                            </div>
+                            <div class="col-md-3 mb-3">
+                                <label class="form-label">Provincia</label>
+                                <input type="text" class="form-control" wire:model="sede_legale_provincia">
+                            </div>
+                            <div class="col-md-6 mb-3">
+                                <label class="form-label">Comune</label>
+                                <input type="text" class="form-control" wire:model="sede_legale_comune">
+                            </div>
+                        </div>
+                        <div class="row">
+                            <div class="col-md-6 mb-3">
+                                <label class="form-label">Indirizzo</label>
+                                <input type="text" class="form-control" wire:model="sede_legale_indirizzo">
+                            </div>
+                            <div class="col-md-3 mb-3">
+                                <label class="form-label">CAP</label>
+                                <input type="text" class="form-control" wire:model="sede_legale_cap">
+                            </div>
+                        </div>
+
+                        <div class="form-check mb-3">
+                            <input class="form-check-input mt-1" type="checkbox" wire:model="same_address"
+                                id="sameAddress">
+                            <label class="form-check-label" style="font-size: medium" for="sameAddress">
+                                Sede operativa uguale a sede legale
+                            </label>
+                        </div>
+
+                        @if(!$same_address)
+                            <h5 class="mb-3 blu-text">Sede operativa</h5>
                             <div class="row">
                                 <div class="col-md-3 mb-3">
                                     <label class="form-label">Nazione</label>
-                                    <input type="text" class="form-control" wire:model="sede_legale_nazione">
+                                    <input type="text" class="form-control" wire:model="sede_operativa_nazione">
                                 </div>
                                 <div class="col-md-3 mb-3">
                                     <label class="form-label">Provincia</label>
-                                    <input type="text" class="form-control" wire:model="sede_legale_provincia">
+                                    <input type="text" class="form-control" wire:model="sede_operativa_provincia">
                                 </div>
                                 <div class="col-md-6 mb-3">
                                     <label class="form-label">Comune</label>
-                                    <input type="text" class="form-control" wire:model="sede_legale_comune">
+                                    <input type="text" class="form-control" wire:model="sede_operativa_comune">
                                 </div>
                             </div>
                             <div class="row">
                                 <div class="col-md-6 mb-3">
                                     <label class="form-label">Indirizzo</label>
-                                    <input type="text" class="form-control" wire:model="sede_legale_indirizzo">
+                                    <input type="text" class="form-control" wire:model="sede_operativa_indirizzo">
                                 </div>
                                 <div class="col-md-3 mb-3">
                                     <label class="form-label">CAP</label>
-                                    <input type="text" class="form-control" wire:model="sede_legale_cap">
+                                    <input type="text" class="form-control" wire:model="sede_operativa_cap">
                                 </div>
                             </div>
+                        @endif
 
-                            <div class="form-check mb-3">
-                                <input class="form-check-input mt-1" type="checkbox" wire:model="same_address" id="sameAddress">
-                                <label class="form-check-label" style="font-size: medium" for="sameAddress">
-                                    Sede operativa uguale a sede legale
-                                </label>
+                        <h5 class="mb-3 blu-text">Contatti</h5>
+                        <div class="row">
+                            <div class="col-md-6 mb-3">
+                                <label class="form-label">Email*</label>
+                                <input type="email" class="form-control @error('email') is-invalid @enderror"
+                                    wire:model="email">
+                                @error('email') <span class="text-danger">{{ $message }}</span> @enderror
                             </div>
-
-                            @if(!$same_address)
-                                <h5 class="mb-3 blu-text">Sede operativa</h5>
-                                <div class="row">
-                                    <div class="col-md-3 mb-3">
-                                        <label class="form-label">Nazione</label>
-                                        <input type="text" class="form-control" wire:model="sede_operativa_nazione">
-                                    </div>
-                                    <div class="col-md-3 mb-3">
-                                        <label class="form-label">Provincia</label>
-                                        <input type="text" class="form-control" wire:model="sede_operativa_provincia">
-                                    </div>
-                                    <div class="col-md-6 mb-3">
-                                        <label class="form-label">Comune</label>
-                                        <input type="text" class="form-control" wire:model="sede_operativa_comune">
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-6 mb-3">
-                                        <label class="form-label">Indirizzo</label>
-                                        <input type="text" class="form-control" wire:model="sede_operativa_indirizzo">
-                                    </div>
-                                    <div class="col-md-3 mb-3">
-                                        <label class="form-label">CAP</label>
-                                        <input type="text" class="form-control" wire:model="sede_operativa_cap">
-                                    </div>
-                                </div>
-                            @endif
-
-                            <h5 class="mb-3 blu-text">Contatti</h5>
-                            <div class="row">
-                                <div class="col-md-6 mb-3">
-                                    <label class="form-label">Email*</label>
-                                    <input type="email" class="form-control @error('email') is-invalid @enderror"
-                                            wire:model="email">
-                                    @error('email') <span class="text-danger">{{ $message }}</span> @enderror
-                                </div>
-                                <div class="col-md-6 mb-3">
-                                    <label class="form-label">Pec*</label>
-                                    <input type="email" class="form-control @error('pec') is-invalid @enderror"
-                                            wire:model="pec">
-                                    @error('pec') <span class="text-danger">{{ $message }}</span> @enderror
-                                </div>
+                            <div class="col-md-6 mb-3">
+                                <label class="form-label">Pec*</label>
+                                <input type="email" class="form-control @error('pec') is-invalid @enderror"
+                                    wire:model="pec">
+                                @error('pec') <span class="text-danger">{{ $message }}</span> @enderror
                             </div>
-                            <div class="row">
-                                <div class="col-md-6 mb-3">
-                                    <label class="form-label">Telefono</label>
-                                    <input type="text" class="form-control" wire:model="telefono">
-                                </div>
-                                <div class="col-md-6 mb-3">
-                                    <label class="form-label">Cellulare*</label>
-                                    <input type="text" class="form-control @error('cellulare') is-invalid @enderror"
-                                            wire:model="cellulare">
-                                    @error('cellulare') <span class="text-danger">{{ $message }}</span> @enderror
-                                </div>
+                        </div>
+                        <div class="row">
+                            <div class="col-md-6 mb-3">
+                                <label class="form-label">Telefono</label>
+                                <input type="text" class="form-control" wire:model="telefono">
                             </div>
-
-                            <h5 class="mb-3  blu-text">Dati fiscali</h5>
-                            <div class="row">
-                                <div class="col-md-3 mb-3">
-                                    <label class="form-label">Partita IVA</label>
-                                    <input type="text" class="form-control " wire:model="partita_iva">
-                                </div>
-                                <div class="col-md-3 mb-3">
-                                    <label class="form-label">Codice fiscale</label>
-                                    <input type="text" class="form-control " wire:model="codice_fiscale">
-                                </div>
-                                <div class="col-md-3 mb-3">
-                                    <label class="form-label">Codice SDI</label>
-                                    <input type="text" class="form-control " wire:model="codice_sdi">
-                                </div>
+                            <div class="col-md-6 mb-3">
+                                <label class="form-label">Cellulare*</label>
+                                <input type="text" class="form-control @error('cellulare') is-invalid @enderror"
+                                    wire:model="cellulare">
+                                @error('cellulare') <span class="text-danger">{{ $message }}</span> @enderror
                             </div>
+                        </div>
 
-                            <div class="mt-4 d-flex justify-content-start">
-                                <button type="button" class="btn--ui lightGrey" onclick="annulla()">Annulla</button>
-                                <button type="submit" class="btn text-light"style="background-color:#0C6197;margin-left:15px; ">Salva</button>
+                        <h5 class="mb-3  blu-text">Dati fiscali</h5>
+                        <div class="row">
+                            <div class="col-md-3 mb-3">
+                                <label class="form-label">Partita IVA</label>
+                                <input type="text" class="form-control " wire:model="partita_iva">
                             </div>
-                        </form>
-                    </div>
+                            <div class="col-md-3 mb-3">
+                                <label class="form-label">Codice fiscale</label>
+                                <input type="text" class="form-control " wire:model="codice_fiscale">
+                            </div>
+                            <div class="col-md-3 mb-3">
+                                <label class="form-label">Codice SDI</label>
+                                <input type="text" class="form-control " wire:model="codice_sdi">
+                            </div>
+                        </div>
+
+                        <div class="mt-4 d-flex justify-content-start">
+                            <button type="button" class="btn--ui lightGrey" onclick="annulla()">Annulla</button>
+                            <button type="submit" class="btn text-light"
+                                style="background-color:#0C6197;margin-left:15px; ">Salva</button>
+                        </div>
+                    </form>
                 </div>
             </div>
         </div>
     </div>
-    <style>
-        /* Custom CSS for rounded multi-select */
+</div>
+<style>
+    /* Custom CSS for rounded multi-select */
 
-        /* If using Choices.js */
-        .discipline-select + .choices {
-            border-radius: 20px !important;
-        }
-        .discipline-select + .choices .choices__inner {
-            border-radius: 20px !important;
-        }
-        .discipline-select + .choices .choices__list--dropdown {
-            border-radius: 15px !important;
-        }
-        .blu-text {
-            color: #0C6197 !important;
-        }
-        </style>
+    /* If using Choices.js */
+    .discipline-select+.choices {
+        border-radius: 20px !important;
+    }
+
+    .discipline-select+.choices .choices__inner {
+        border-radius: 20px !important;
+    }
+
+    .discipline-select+.choices .choices__list--dropdown {
+        border-radius: 15px !important;
+    }
+
+    .blu-text {
+        color: #0C6197 !important;
+    }
+</style>
 @push('scripts')
     <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
     <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
     <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
     <script>
-        $(document).ready(function() {
+        $(document).ready(function () {
             $('.discipline-select').select2({
                 placeholder: 'Seleziona discipline',
                 allowClear: true
@@ -223,8 +263,7 @@
             });
         });
 
-        function annulla()
-        {
+        function annulla() {
             window.onbeforeunload = null;
             document.location.href = '/dashboard';
         }

+ 118 - 46
resources/views/receipt.blade.php

@@ -1,34 +1,93 @@
 <!DOCTYPE html>
 <html>
 <head>
-  <title></title>
-  <style>
-    body {
-        font-family: Helvetica, Arial, sans-serif;
-        font-size:14px;
-    }
-    footer {
-        position: fixed;
-        bottom: -20px;
-        left: 0px;
-        right: 0px;
-        height: 50px;
-
-    }
+    <title></title>
+    <style>
+        body {
+            font-family: Helvetica, Arial, sans-serif;
+            font-size:14px;
+        }
+        footer {
+            position: fixed;
+            bottom: -20px;
+            left: 0px;
+            right: 0px;
+            height: 50px;
+        }
+        .logo {
+            max-width: 200px;
+            max-height: 100px;
+            object-fit: contain;
+        }
     </style>
 </head>
 <body>
-    <img src="{{public_path() . env('LOGO')}}" width="200">
+    @php
+        $azienda = App\Models\Azienda::first();
+        $logoService = app(App\Services\LogoUploadServices::class);
+        $logoUrl = $azienda ? $logoService->getLogoUrl($azienda) : null;
+    @endphp
+
+    {{-- Display logo from database/Wasabi instead of env --}}
+    @if($logoUrl)
+        <img src="{{ $logoUrl }}" class="logo" alt="Logo">
+    @elseif($azienda && $azienda->logo)
+        {{-- Fallback if URL generation fails --}}
+        <div style="width: 200px; height: 100px; border: 1px dashed #ccc; display: flex; align-items: center; justify-content: center;">
+            <small>Logo non disponibile</small>
+        </div>
+    @endif
+
     <br><br><br>
     <div align="right"><b>RICEVUTA DI PAGAMENTO N. {{$receipt->number . "/" . $receipt->year}} del {{date("d/m/Y", strtotime($receipt->created_at))}}</b></div><br><br>
+
     @if($receipt->status == 99)
         <div align="right"><b style="color:red">ANNULLATA</b></div><br><br>
     @endif
+
     <br>
-    {{env('RAGIONE_SOCIALE', '')}}<br><br>
-    <b>Indirizzo</b>: {{env('INDIRIZZO', '')}} {{env('LOCALITA', '')}} ({{env('PROVINCIA', '')}})<br><br>
-    <b>C.F.</b>: {{env('CODICE_FISCALE', '')}}<br><br>
-    <b>P.IVA</b>: {{env('PARTITA_IVA', '')}}<br><br>
+
+    @if($azienda)
+        @if($azienda->ragione_sociale)
+            {{$azienda->ragione_sociale}}
+        @elseif($azienda->nome_associazione)
+            {{$azienda->nome_associazione}}
+        @endif
+        <br><br>
+
+        <b>Indirizzo</b>:
+        @php
+            $addressParts = array_filter([
+                $azienda->sede_legale_indirizzo,
+                $azienda->sede_legale_cap,
+                $azienda->sede_legale_comune,
+                $azienda->sede_legale_provincia ? '(' . $azienda->sede_legale_provincia . ')' : null
+            ]);
+        @endphp
+        {{ implode(' ', $addressParts) }}
+        <br><br>
+
+        @if($azienda->codice_fiscale)
+            <b>C.F.</b>: {{$azienda->codice_fiscale}}<br><br>
+        @endif
+
+        @if($azienda->partita_iva)
+            <b>P.IVA</b>: {{$azienda->partita_iva}}<br><br>
+        @endif
+
+        @if($azienda->email)
+            <b>Email</b>: {{$azienda->email}}<br><br>
+        @endif
+
+        @if($azienda->pec)
+            <b>PEC</b>: {{$azienda->pec}}<br><br>
+        @endif
+    @else
+        <div style="color: red; font-weight: bold;">
+            ATTENZIONE: Configurare i dati aziendali nel sistema
+        </div><br><br>
+    @endif
+
     <hr><br>
 
     <b>Intestata a</b><br><br>
@@ -72,44 +131,57 @@
     @endif
     <hr><br>
 
-   @php
+    @php
     $total = 0;
     $totalSconto = 0;
     $totalPrediscount = 0;
     $hasDiscount = false;
-@endphp
+    @endphp
 
-@foreach($receipt->rows as $row)
-    <b>Causale</b>: {{@$row->causal->getTree()}}<br><br>
-    <b>Dettaglio causale</b>: {{$row->note != '' ? $row->note : ''}}<br><br>
+    @foreach($receipt->rows as $row)
+        <b>Causale</b>: {{@$row->causal->getTree()}}<br><br>
+        <b>Dettaglio causale</b>: {{$row->note != '' ? $row->note : ''}}<br><br>
 
-    @if($row->sconto > 0)
-        @php $hasDiscount = true; @endphp
-        <b>Importo</b>: {{formatPrice($row->prediscount_amount)}}<br><br>
-        <b>Sconto</b>: {{formatPrice($row->sconto)}}<br><br>
-    @endif
+        @if($row->sconto > 0)
+            @php $hasDiscount = true; @endphp
+            <b>Importo</b>: {{formatPrice($row->prediscount_amount)}}<br><br>
+            <b>Sconto</b>: {{formatPrice($row->sconto)}}<br><br>
+        @endif
 
-    <b>{{$row->sconto > 0 ? 'Importo finale' : 'Importo'}}</b>: {{formatPrice($row->amount)}}<br><br>
-    <hr><br>
-    @php
-        $totalSconto += $row->sconto;
-        $totalPrediscount += $row->prediscount_amount;
-        $total += $row->amount;
-    @endphp
-@endforeach
+        <b>{{$row->sconto > 0 ? 'Importo finale' : 'Importo'}}</b>: {{formatPrice($row->amount)}}<br><br>
+        <hr><br>
+        @php
+            $totalSconto += $row->sconto;
+            $totalPrediscount += $row->prediscount_amount;
+            $total += $row->amount;
+        @endphp
+    @endforeach
 
-<br><br>
+    <br><br>
 
-@if($hasDiscount)
-    <b>Totale prescontato</b>: {{formatPrice($totalPrediscount)}}<br><br>
-    <b>Sconto totale</b>: {{formatPrice($totalSconto)}}<br><br>
-@endif
+    @if($hasDiscount)
+        <b>Totale prescontato</b>: {{formatPrice($totalPrediscount)}}<br><br>
+        <b>Sconto totale</b>: {{formatPrice($totalSconto)}}<br><br>
+    @endif
 
-<b>Totale</b>: {{formatPrice($total)}}<br><br>
+    <b>Totale</b>: {{formatPrice($total)}}<br><br>
 
-<footer>
-    <small>{{env('LOCALITA', '')}} ({{env('PROVINCIA', '')}}) li {{date("d/m/Y", strtotime($receipt->created_at))}}</small>
-</footer>
+    <footer>
+        <small>
+            @if($azienda)
+                @php
+                    $locationParts = array_filter([
+                        $azienda->sede_legale_comune,
+                        $azienda->sede_legale_provincia ? '(' . $azienda->sede_legale_provincia . ')' : null
+                    ]);
+                    $location = implode(' ', $locationParts);
+                @endphp
+                {{ $location ?: 'Sede legale' }} li {{date("d/m/Y", strtotime($receipt->created_at))}}
+            @else
+                {{date("d/m/Y", strtotime($receipt->created_at))}}
+            @endif
+        </small>
+    </footer>
 
 </body>
 </html>