FabioFratini 7 月之前
父節點
當前提交
918f3031ae

+ 281 - 92
app/Http/Livewire/RecordOUT.php

@@ -10,6 +10,7 @@ use Livewire\WithFileUploads;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Schema;
 use App\Services\RecordFileService;
+use App\Jobs\ProcessRecordAttachment;
 
 class RecordOUT extends Component
 {
@@ -90,6 +91,9 @@ class RecordOUT extends Component
     public $vats = array();
 
     public $numero_fattura;
+    public $attachmentUploadStatus = 'none';
+    public $uploadProgress = 0;
+    public $uploadStartTime = null;
 
 
     protected $rules = [
@@ -375,9 +379,7 @@ class RecordOUT extends Component
 
     public function store()
     {
-        // Start loading state
-        $this->emit('start-loading', 'Salvataggio in corso...');
-
+        $this->emit('start-loading', 'Validazione dati...');
         $this->emit('refresh');
 
         if ($this->numero_fattura == null || $this->numero_fattura == '') {
@@ -391,37 +393,74 @@ class RecordOUT extends Component
             $is_paid = true;
         }
 
+        if ($this->attachment) {
+            $fileSize = $this->attachment->getSize();
+            $fileName = $this->attachment->getClientOriginalName();
+            $mimeType = $this->attachment->getMimeType();
+            $maxSize = 50 * 1024 * 1024;
+
+            Log::info("=== FILE VALIDATION ===");
+            Log::info("File: {$fileName}");
+            Log::info("Size: " . round($fileSize / 1024 / 1024, 2) . " MB");
+            Log::info("MIME: {$mimeType}");
+
+            if ($fileSize > $maxSize) {
+                $this->emit('stop-loading');
+                $this->emit('flash-error', 'File troppo grande. Dimensione massima: 50MB');
+                return;
+            }
+
+            $allowedMimes = [
+                'image/jpeg',
+                'image/jpg',
+                'image/png',
+                'image/gif',
+                'image/webp',
+                'application/pdf',
+                'application/msword',
+                'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+                'text/xml',
+                'application/xml',
+                'text/plain',
+                'text/csv',
+                'application/vnd.ms-excel',
+                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+            ];
+
+            if (!in_array($mimeType, $allowedMimes)) {
+                $this->emit('stop-loading');
+                $this->emit('flash-error', 'Tipo di file non supportato');
+                return;
+            }
+
+            Log::info("File validation passed");
+        }
+
         $this->validate();
 
         try {
-            // Optimize: Use DB transaction for better performance
-            \DB::beginTransaction();
+            $this->emit('update-loading', 'Salvataggio record...');
+
+            DB::beginTransaction();
 
-            // Create record first
             $record = \App\Models\Record::create([
                 'member_id' => $this->member_id,
                 'supplier_id' => $this->supplier_id,
                 'payment_method_id' => $this->payment_method_id,
                 'date' => $this->date,
                 'data_pagamento' => $this->data_pagamento,
-                'attachment' => '', // Will be updated later if needed
+                'attachment' => '',
                 'type' => $this->type,
-                'amount' => 0, // Will be calculated after rows
+                'amount' => 0,
                 'commercial' => $this->commercial,
                 'numero_fattura' => $this->numero_fattura,
                 'is_paid' => $is_paid,
+                'attachment_status' => $this->attachment ? 'pending' : 'none',
             ]);
 
             $this->dataId = $record->id;
+            Log::info("Record created with ID: {$this->dataId}");
 
-            // Update loading message
-            $this->emit('update-loading', 'Creazione cartelle...');
-
-            // Create folders (this is usually fast)
-            $this->recordFileService->createRecordFolders($record->id, 'OUT');
-
-            // Create record rows first (faster than file operations)
-            $this->emit('update-loading', 'Salvataggio dati...');
             $tot = 0;
             $rowsData = [];
 
@@ -449,48 +488,68 @@ class RecordOUT extends Component
                 $tot += $amount;
             }
 
-            // Bulk insert for better performance
             \App\Models\RecordRow::insert($rowsData);
-
-            // Update total amount
             $record->amount = $tot;
             $record->save();
 
-            // Handle attachment upload last (slowest operation)
+            DB::commit();
+            Log::info("✅ Database transaction committed");
+
             if ($this->attachment) {
-                $this->emit('update-loading', 'Caricamento allegato...');
+                $this->emit('update-loading', 'Preparazione file per elaborazione...');
 
                 try {
-                    // Use a more optimized approach for file upload
-                    $attachmentPath = $this->recordFileService->uploadAttachment($this->attachment, $record->id, 'OUT');
+                    Log::info("=== STARTING FILE PROCESSING ===");
 
-                    // Update only the attachment field to avoid unnecessary queries
-                    \DB::table('records')
-                        ->where('id', $record->id)
-                        ->update(['attachment' => $attachmentPath]);
+                    $tempPath = $this->recordFileService->storeTemporarily($this->attachment);
+                    $originalFileName = $this->attachment->getClientOriginalName();
+
+                    Log::info(" File stored temporarily at: {$tempPath}");
+
+                    $clientName = session('clientName', 'default');
+
+                    ProcessRecordAttachment::dispatch(
+                        $record->id,
+                        $tempPath,
+                        $originalFileName,
+                        'out',
+                        $clientName
+                    )->onQueue('attachments');
+
+                    Log::info("File processing job dispatched for record {$record->id}");
                 } catch (\Exception $ex) {
-                    // Don't fail the entire operation for attachment issues
-                    Log::error("Attachment upload failed for record {$record->id}: " . $ex->getMessage());
-                    session()->flash('warning', 'Record creato ma caricamento allegato fallito: ' . $ex->getMessage());
+                    Log::error("Failed to process file: " . $ex->getMessage());
+                    Log::error("Stack trace: " . $ex->getTraceAsString());
+
+                    DB::table('records')
+                        ->where('id', $record->id)
+                        ->update(['attachment_status' => 'failed']);
+
+                    session()->flash('warning', 'Record creato ma elaborazione allegato fallita. Prova a modificare il record e caricare nuovamente il file.');
                 }
             }
 
-            \DB::commit();
-
             $this->emit('stop-loading');
-            session()->flash('success', 'Movimento creato con successo');
+
+            if ($this->attachment) {
+                session()->flash('success', 'Movimento creato con successo. L\'allegato verrà elaborato in background. Aggiorna la pagina tra qualche secondo per vedere lo stato.');
+            } else {
+                session()->flash('success', 'Movimento creato con successo.');
+            }
 
             $this->resetFields();
             $this->add = false;
             $this->emit('setEdit', false);
         } catch (\Exception $ex) {
-            \DB::rollback();
+            DB::rollback();
             $this->emit('stop-loading');
 
             Log::error("Store error: " . $ex->getMessage());
+            Log::error("Stack trace: " . $ex->getTraceAsString());
             $this->emit('flash-error', 'Errore durante il salvataggio: ' . $ex->getMessage());
         }
     }
+
     public function setDataPagamentoAttribute($value)
     {
         if (empty($value) || $value == '1970-01-01' || $value == '0000-00-00') {
@@ -508,6 +567,31 @@ class RecordOUT extends Component
         return $value;
     }
 
+    public function removeAttachment()
+    {
+        if ($this->attachment_old) {
+            try {
+                $this->recordFileService->deleteAttachment($this->attachment_old);
+
+                if ($this->dataId) {
+                    DB::table('records')
+                        ->where('id', $this->dataId)
+                        ->update([
+                            'attachment' => '',
+                        ]);
+                }
+
+                $this->attachment_old = '';
+                $this->attachmentUploadStatus = 'none';
+
+                session()->flash('success', 'Allegato rimosso con successo');
+            } catch (\Exception $e) {
+                Log::error("Error removing attachment: " . $e->getMessage());
+                $this->emit('flash-error', 'Errore durante la rimozione dell\'allegato');
+            }
+        }
+    }
+
     public function edit($id)
     {
         if (!isset($_GET["from"]) && $this->fromPage == '')
@@ -605,46 +689,73 @@ class RecordOUT extends Component
 
     public function update()
     {
-        // Start loading state
-        $this->emit('start-loading', 'Aggiornamento in corso...');
-
+        $this->emit('start-loading', 'Validazione dati...');
         $this->emit('refresh');
-        $this->validate();
 
-        if (empty($this->data_pagamento) || $this->data_pagamento == '') {
+        if ($this->numero_fattura == null || $this->numero_fattura == '') {
+            $this->numero_fattura = 'USC-' . date('Ymd');
+        }
+
+        if (empty($this->data_pagamento) || $this->data_pagamento == '1970-01-01') {
             $this->data_pagamento = null;
             $is_paid = false;
         } else {
             $is_paid = true;
         }
 
-        try {
-            \DB::beginTransaction();
+        if ($this->attachment) {
+            $fileSize = $this->attachment->getSize();
+            $fileName = $this->attachment->getClientOriginalName();
+            $mimeType = $this->attachment->getMimeType();
+            $maxSize = 50 * 1024 * 1024;
 
-            $attachmentPath = $this->attachment_old;
-
-            // Handle attachment first if needed
-            if ($this->attachment) {
-                $this->emit('update-loading', 'Aggiornamento allegato...');
+            Log::info("=== FILE VALIDATION (UPDATE) ===");
+            Log::info("File: {$fileName}");
+            Log::info("Size: " . round($fileSize / 1024 / 1024, 2) . " MB");
+            Log::info("MIME: {$mimeType}");
 
-                try {
-                    // Delete old attachment if exists
-                    if ($this->attachment_old) {
-                        $this->recordFileService->deleteAttachment($this->attachment_old);
-                    }
+            if ($fileSize > $maxSize) {
+                $this->emit('stop-loading');
+                $this->emit('flash-error', 'File troppo grande. Dimensione massima: 50MB');
+                return;
+            }
 
-                    $attachmentPath = $this->recordFileService->uploadAttachment($this->attachment, $this->dataId, 'OUT');
-                } catch (\Exception $ex) {
-                    $this->emit('stop-loading');
-                    session()->flash('error', 'Errore caricamento allegato: ' . $ex->getMessage());
-                    return;
-                }
+            $allowedMimes = [
+                'image/jpeg',
+                'image/jpg',
+                'image/png',
+                'image/gif',
+                'image/webp',
+                'application/pdf',
+                'application/msword',
+                'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+                'text/xml',
+                'application/xml',
+                'text/plain',
+                'text/csv',
+                'application/vnd.ms-excel',
+                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+            ];
+
+            if (!in_array($mimeType, $allowedMimes)) {
+                $this->emit('stop-loading');
+                $this->emit('flash-error', 'Tipo di file non supportato');
+                return;
             }
 
+            Log::info("File validation passed");
+        }
+
+        $this->validate();
+
+        try {
             $this->emit('update-loading', 'Aggiornamento record...');
 
-            // Update main record
-            \App\Models\Record::whereId($this->dataId)->update([
+            DB::beginTransaction();
+
+            $record = \App\Models\Record::findOrFail($this->dataId);
+
+            $record->update([
                 'member_id' => $this->member_id,
                 'supplier_id' => $this->supplier_id,
                 'payment_method_id' => $this->payment_method_id,
@@ -652,12 +763,12 @@ class RecordOUT extends Component
                 'data_pagamento' => $this->data_pagamento,
                 'type' => $this->type,
                 'commercial' => $this->commercial,
-                'is_paid' => $is_paid,
                 'numero_fattura' => $this->numero_fattura,
-                'attachment' => $attachmentPath,
+                'is_paid' => $is_paid,
+                'attachment_status' => $this->attachment ? 'pending' : ($this->attachment_old ? 'completed' : 'none'),
             ]);
 
-            $this->emit('update-loading', 'Aggiornamento righe...');
+            Log::info("Record updated with ID: {$this->dataId}");
 
             $existingRows = \App\Models\RecordRow::where('record_id', $this->dataId)
                 ->select('id', 'quantita', 'numero_linea')
@@ -675,16 +786,8 @@ class RecordOUT extends Component
                     $row["when"][$x]['period'] = $row["when"][$x]['month'] . "-" . $row["when"][$x]['year'];
                 }
 
-                $imponibile = null;
-                if (isset($row["imponibile"]) && $row["imponibile"] !== null && $row["imponibile"] !== '') {
-                    $imponibile = $this->currencyToDouble($row["imponibile"]);
-                }
-
-                $aliquota_iva = null;
-                if (isset($row["aliquota_iva"]) && $row["aliquota_iva"] !== null && $row["aliquota_iva"] !== '') {
-                    $aliquota_iva = floatval(str_replace('%', '', $row["aliquota_iva"]));
-                }
-
+                $imponibile = isset($row["imponibile"]) ? $this->currencyToDouble($row["imponibile"]) : null;
+                $aliquota_iva = isset($row["aliquota_iva"]) ? floatval(str_replace('%', '', $row["aliquota_iva"])) : null;
                 $amount = $this->currencyToDouble($row["amount"]);
                 $imposta = $imponibile !== null ? $amount - $imponibile : null;
 
@@ -693,17 +796,16 @@ class RecordOUT extends Component
                     'causal_id' => $row["causal_id"],
                     'note' => $row["note"],
                     'amount' => $amount,
-                    'commercial' => $row["commercial"],
-                    'when' => json_encode($row["when"]),
                     'imponibile' => $imponibile,
                     'aliquota_iva' => $aliquota_iva,
                     'imposta' => $imposta,
+                    'commercial' => $row["commercial"],
+                    'when' => json_encode($row["when"]),
                     'divisa' => 'EUR',
                     'created_at' => now(),
                     'updated_at' => now()
                 ];
 
-                // Preserve existing metadata if available
                 if (isset($row["id"]) && isset($existingRows[$row["id"]])) {
                     $existingRow = $existingRows[$row["id"]];
                     $recordRowData['quantita'] = $existingRow['quantita'];
@@ -715,24 +817,68 @@ class RecordOUT extends Component
             }
 
             \App\Models\RecordRow::insert($newRowsData);
+            $record->amount = $tot;
+            $record->save();
+
+            DB::commit();
+            Log::info("Database transaction committed");
+
+            if ($this->attachment) {
+                $this->emit('update-loading', 'Preparazione file per elaborazione...');
+
+                try {
+                    Log::info("=== STARTING FILE PROCESSING (UPDATE) ===");
+
+                    if ($this->attachment_old) {
+                        $this->recordFileService->deleteAttachment($this->attachment_old);
+                        Log::info("Old attachment deleted: {$this->attachment_old}");
+                    }
 
-            \DB::table('records')
-                ->where('id', $this->dataId)
-                ->update(['amount' => $tot]);
+                    $tempPath = $this->recordFileService->storeTemporarily($this->attachment);
+                    $originalFileName = $this->attachment->getClientOriginalName();
 
-            \DB::commit();
+                    Log::info("File stored temporarily at: {$tempPath}");
+
+                    $clientName = session('clientName', 'default');
+
+                    ProcessRecordAttachment::dispatch(
+                        $record->id,
+                        $tempPath,
+                        $originalFileName,
+                        'out',
+                        $clientName
+                    )->onQueue('attachments');
+
+                    Log::info("File processing job dispatched for record {$record->id}");
+                } catch (\Exception $ex) {
+                    Log::error("Failed to process file: " . $ex->getMessage());
+                    Log::error("Stack trace: " . $ex->getTraceAsString());
+
+                    DB::table('records')
+                        ->where('id', $record->id)
+                        ->update(['attachment_status' => 'failed']);
+
+                    session()->flash('warning', 'Record aggiornato ma elaborazione allegato fallita. Prova a modificare il record e caricare nuovamente il file.');
+                }
+            }
 
             $this->emit('stop-loading');
-            session()->flash('success', 'Movimento aggiornato con successo');
+
+            if ($this->attachment) {
+                session()->flash('success', 'Movimento aggiornato con successo. L\'allegato verrà elaborato in background. Aggiorna la pagina tra qualche secondo per vedere lo stato.');
+            } else {
+                session()->flash('success', 'Movimento aggiornato con successo.');
+            }
 
             $this->resetFields();
             $this->update = false;
             $this->emit('setEdit', false);
         } catch (\Exception $ex) {
-            \DB::rollback();
+            DB::rollback();
             $this->emit('stop-loading');
 
             Log::error("Update error: " . $ex->getMessage());
+            Log::error("Stack trace: " . $ex->getTraceAsString());
             $this->emit('flash-error', 'Errore durante l\'aggiornamento: ' . $ex->getMessage());
         }
     }
@@ -759,7 +905,6 @@ class RecordOUT extends Component
             $this->emit('flash-error', 'Errore (' . $e->getMessage() . ')');
         }
     }
-
     public function multipleDelete()
     {
         try {
@@ -787,16 +932,6 @@ class RecordOUT extends Component
         return $this->recordFileService->getAttachmentUrl($filePath);
     }
 
-    /**
-     * Remove attachment file
-     */
-    public function removeAttachment()
-    {
-        if ($this->attachment_old) {
-            $this->recordFileService->deleteAttachment($this->attachment_old);
-        }
-        $this->attachment_old = '';
-    }
     function currencyToDouble($val)
     {
         $x = str_replace("€", "", $val);
@@ -1701,4 +1836,58 @@ class RecordOUT extends Component
             $this->emit('flash-error', 'Errore nel caricamento dei dettagli: ' . $e->getMessage());
         }
     }
+
+    public function getUploadProgressProperty()
+    {
+        return $this->uploadProgress;
+    }
+
+    /**
+     * Get upload status
+     */
+    public function getUploadStatusProperty()
+    {
+        return $this->attachmentUploadStatus;
+    }
+
+    /**
+     * Check if file upload is in progress
+     */
+    public function getIsUploadingProperty()
+    {
+        return $this->attachmentUploadStatus === 'pending';
+    }
+
+    /**
+     * Get estimated upload time remaining (in seconds)
+     */
+    public function getEstimatedTimeRemainingProperty()
+    {
+        if (!$this->uploadStartTime || $this->uploadProgress <= 0) {
+            return null;
+        }
+
+        $elapsed = microtime(true) - $this->uploadStartTime;
+        $rate = $this->uploadProgress / $elapsed;
+        $remaining = (100 - $this->uploadProgress) / $rate;
+
+        return max(0, round($remaining));
+    }
+
+    /**
+     * Get file upload speed (KB/s)
+     */
+    public function getUploadSpeedProperty()
+    {
+        if (!$this->uploadStartTime || !$this->attachment) {
+            return null;
+        }
+
+        $elapsed = microtime(true) - $this->uploadStartTime;
+        $fileSize = $this->attachment->getSize();
+        $uploadedBytes = ($this->uploadProgress / 100) * $fileSize;
+        $speed = $uploadedBytes / $elapsed; // bytes per second
+
+        return round($speed / 1024, 1); // KB per second
+    }
 }

+ 249 - 0
app/Jobs/ProcessRecordAttachment.php

@@ -0,0 +1,249 @@
+<?php
+
+namespace App\Jobs;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use App\Services\RecordFileService;
+
+class ProcessRecordAttachment implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $recordId;
+    protected $tempFilePath;
+    protected $originalFileName;
+    protected $type;
+    protected $clientName;
+
+    public $timeout = 300;
+    public $tries = 3;
+    public $backoff = [10, 30, 60];
+
+    public function __construct($recordId, $tempFilePath, $originalFileName, $type = 'OUT', $clientName = null)
+    {
+        $this->recordId = $recordId;
+        $this->tempFilePath = $tempFilePath;
+        $this->originalFileName = $originalFileName;
+        $this->type = strtolower($type);
+        $this->clientName = $clientName ?: session('clientName', 'default');
+
+        $this->clientName = Str::slug($this->clientName, '_');
+    }
+
+    public function handle(RecordFileService $recordFileService)
+    {
+        try {
+            Log::info("=== PROCESSING ATTACHMENT JOB START ===");
+            Log::info("Client: {$this->clientName}");
+            Log::info("Record ID: {$this->recordId}");
+            Log::info("Temp file: {$this->tempFilePath}");
+            Log::info("Original name: {$this->originalFileName}");
+            Log::info("Type: {$this->type}");
+
+            DB::table('records')
+                ->where('id', $this->recordId)
+                ->update([
+                    'attachment_status' => 'processing',
+                    'updated_at' => now()
+                ]);
+
+            if (!Storage::disk('s3')->exists($this->tempFilePath)) {
+                Log::error("Temp file not found on S3: {$this->tempFilePath}");
+
+                try {
+                    $tempFiles = Storage::disk('s3')->files("{$this->clientName}/temp/uploads");
+                    Log::info("Available temp files for client '{$this->clientName}' on S3: " . json_encode($tempFiles));
+                } catch (\Exception $e) {
+                    Log::error("Could not list temp files for client '{$this->clientName}': " . $e->getMessage());
+                }
+
+                throw new \Exception("Temp file not found on S3: {$this->tempFilePath}");
+            }
+
+            $tempFileSize = Storage::disk('s3')->size($this->tempFilePath);
+            Log::info("Temp file size: {$tempFileSize} bytes");
+
+            $extension = pathinfo($this->originalFileName, PATHINFO_EXTENSION);
+            $fileName = time() . '_' . Str::random(10) . '.' . $extension;
+            $finalPath = "{$this->clientName}/records/{$this->type}/{$this->recordId}/attachments/{$fileName}";
+
+            Log::info("Final path: {$finalPath}");
+
+            $copySuccess = $this->copyFileOnS3($this->tempFilePath, $finalPath);
+
+            if (!$copySuccess) {
+                throw new \Exception("Failed to copy file from {$this->tempFilePath} to {$finalPath}");
+            }
+
+            if (!Storage::disk('s3')->exists($finalPath)) {
+                throw new \Exception("Final file not found after copy: {$finalPath}");
+            }
+
+            $finalFileSize = Storage::disk('s3')->size($finalPath);
+            Log::info("Final file size: {$finalFileSize} bytes");
+
+            if ($finalFileSize !== $tempFileSize) {
+                Log::warning("File size mismatch! Temp: {$tempFileSize}, Final: {$finalFileSize}");
+            } else {
+                Log::info("File sizes match - copy successful");
+            }
+
+            DB::table('records')
+                ->where('id', $this->recordId)
+                ->update([
+                    'attachment' => $finalPath,
+                    'attachment_status' => 'completed',
+                    'updated_at' => now()
+                ]);
+
+            $this->cleanupTempFile($this->tempFilePath);
+
+            Log::info("Attachment processing completed successfully for record {$this->recordId}: {$finalPath}");
+            Log::info("=== PROCESSING ATTACHMENT JOB END ===");
+
+        } catch (\Exception $e) {
+            Log::error("Failed to process attachment for record {$this->recordId}: " . $e->getMessage());
+            Log::error("Stack trace: " . $e->getTraceAsString());
+
+            DB::table('records')
+                ->where('id', $this->recordId)
+                ->update([
+                    'attachment_status' => 'failed',
+                    'updated_at' => now()
+                ]);
+
+            $this->cleanupTempFile($this->tempFilePath);
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Enhanced S3 copy with multiple fallback approaches
+     */
+    private function copyFileOnS3($sourcePath, $destinationPath)
+    {
+        Log::info("Attempting S3 copy from {$sourcePath} to {$destinationPath}");
+
+        try {
+            Log::info("Trying Method 1: Standard S3 copy");
+            $copyResult = Storage::disk('s3')->copy($sourcePath, $destinationPath);
+
+            if ($copyResult && Storage::disk('s3')->exists($destinationPath)) {
+                Log::info("Method 1 successful: Standard S3 copy");
+                return true;
+            } else {
+                Log::warning("Method 1 failed: Standard S3 copy returned " . ($copyResult ? 'true' : 'false'));
+            }
+        } catch (\Exception $e) {
+            Log::warning("Method 1 exception: " . $e->getMessage());
+        }
+
+        try {
+            Log::info("Trying Method 2: Read and write");
+
+            $fileContent = Storage::disk('s3')->get($sourcePath);
+            if (!$fileContent) {
+                throw new \Exception("Could not read source file content");
+            }
+
+            $writeResult = Storage::disk('s3')->put($destinationPath, $fileContent);
+
+            if ($writeResult && Storage::disk('s3')->exists($destinationPath)) {
+                Log::info("Method 2 successful: Read and write");
+                return true;
+            } else {
+                Log::warning("Method 2 failed: Write returned " . ($writeResult ? 'true' : 'false'));
+            }
+        } catch (\Exception $e) {
+            Log::warning("Method 2 exception: " . $e->getMessage());
+        }
+
+        try {
+            Log::info("Trying Method 3: Stream copy");
+
+            $sourceStream = Storage::disk('s3')->readStream($sourcePath);
+            if (!$sourceStream) {
+                throw new \Exception("Could not open source stream");
+            }
+
+            $writeResult = Storage::disk('s3')->writeStream($destinationPath, $sourceStream);
+
+            if (is_resource($sourceStream)) {
+                fclose($sourceStream);
+            }
+
+            if ($writeResult && Storage::disk('s3')->exists($destinationPath)) {
+                Log::info(" Method 3 successful: Stream copy");
+                return true;
+            } else {
+                Log::warning("Method 3 failed: Stream write returned " . ($writeResult ? 'true' : 'false'));
+            }
+        } catch (\Exception $e) {
+            Log::warning("Method 3 exception: " . $e->getMessage());
+        }
+
+        Log::error("All S3 copy methods failed");
+        return false;
+    }
+
+    /**
+     * Clean up temp file with error handling
+     */
+    private function cleanupTempFile($tempPath)
+    {
+        try {
+            if (Storage::disk('s3')->exists($tempPath)) {
+                $deleted = Storage::disk('s3')->delete($tempPath);
+                if ($deleted) {
+                    Log::info("Temp file deleted: {$tempPath}");
+                } else {
+                    Log::warning("Failed to delete temp file: {$tempPath}");
+                }
+            } else {
+                Log::info("Temp file already gone: {$tempPath}");
+            }
+        } catch (\Exception $e) {
+            Log::error("Error deleting temp file {$tempPath}: " . $e->getMessage());
+        }
+    }
+
+    public function failed(\Exception $exception)
+    {
+        Log::error("=== JOB PERMANENTLY FAILED ===");
+        Log::error("Client: {$this->clientName}");
+        Log::error("Record ID: {$this->recordId}");
+        Log::error("Exception: " . $exception->getMessage());
+
+        DB::table('records')
+            ->where('id', $this->recordId)
+            ->update([
+                'attachment_status' => 'failed',
+                'updated_at' => now()
+            ]);
+
+        $this->cleanupTempFile($this->tempFilePath);
+    }
+
+    /**
+     * Get job tags for monitoring
+     */
+    public function tags()
+    {
+        return [
+            'attachment',
+            'client:' . $this->clientName,
+            'record:' . $this->recordId,
+            'type:' . $this->type,
+            'file:' . basename($this->tempFilePath)
+        ];
+    }
+}

+ 1 - 1
app/Services/LogoUploadServices.php

@@ -43,7 +43,7 @@ class LogoUploadServices
     public function uploadLogo(UploadedFile $logoFile, $azienda): string
     {
         try {
-            $currentClient = 'iao';
+            $currentClient = session('currentClient', 'default');
 
             if (!$currentClient) {
                 throw new Exception('No current client found in session');

+ 292 - 350
app/Services/RecordFileService.php

@@ -4,471 +4,413 @@ namespace App\Services;
 
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
 use Illuminate\Http\UploadedFile;
-use Exception;
 
 class RecordFileService
 {
     /**
-     * The storage disk to use for file operations
+     * Get client name from session, fallback to 'default'
      */
-    private $disk;
+    private function getClientName()
+    {
+        $clientName = session('clientName', 'default');
 
-    /**
-     * Allowed file extensions for record attachments
-     */
-    private const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xml', 'txt', 'csv', 'xls', 'xlsx'];
+        $clientName = Str::slug($clientName, '_');
 
-    /**
-     * Maximum file size in KB
-     */
-    private const MAX_FILE_SIZE = 10240; // 10MB
+        Log::info("Using client name for folders: {$clientName}");
+        return $clientName;
+    }
 
     /**
-     * Constructor
+     * Create record folders with client structure
      */
-    public function __construct()
+    public function createRecordFolders($recordId, $type)
     {
-        $this->disk = Storage::disk('s3');
+        $clientName = $this->getClientName();
+        $type = strtolower($type);
+
+        Log::info("Preparing S3 structure for client: {$clientName}, record {$recordId}, type: {$type}");
+        $folderPath = "{$clientName}/records/{$type}/{$recordId}/attachments";
+        Log::info("S3 folder structure: {$folderPath}");
+
+        return true;
     }
 
     /**
-     * Upload record attachment file
-     *
-     * @param UploadedFile $attachmentFile
-     * @param int $recordId
-     * @param string $recordType ('IN' or 'OUT')
-     * @return string
-     * @throws Exception
+     * Store file temporarily with client structure
      */
-    public function uploadAttachment(UploadedFile $attachmentFile, int $recordId, string $recordType = 'OUT'): string
+    public function storeTemporarily($uploadedFile)
     {
         try {
-            $currentClient = session('currentClient', 'iao');
+            $clientName = $this->getClientName();
+            $extension = $uploadedFile->getClientOriginalExtension();
+            $fileName = time() . '_' . Str::random(10) . '.' . $extension;
+            $tempPath = "{$clientName}/temp/uploads/{$fileName}";
+
+            Log::info("=== STORING FILE TEMPORARILY ===");
+            Log::info("Client: {$clientName}");
+            Log::info("Original filename: " . $uploadedFile->getClientOriginalName());
+            Log::info("File size: " . $uploadedFile->getSize() . " bytes");
+            Log::info("Temp path: {$tempPath}");
 
-            // Validate file
-            $this->validateAttachmentFile($attachmentFile);
+            try {
+                $storedPath = Storage::disk('s3')->putFileAs("{$clientName}/temp/uploads", $uploadedFile, $fileName);
+                Log::info("Method 1 success - putFileAs returned: {$storedPath}");
+
+                if (Storage::disk('s3')->exists($tempPath)) {
+                    $storedSize = Storage::disk('s3')->size($tempPath);
+                    Log::info("File verification successful - size: {$storedSize} bytes");
+
+                    if ($storedSize === $uploadedFile->getSize()) {
+                        Log::info("File sizes match perfectly");
+                        return $tempPath;
+                    } else {
+                        Log::warning("⚠ File size mismatch - Original: {$uploadedFile->getSize()}, Stored: {$storedSize}");
+                        return $tempPath;
+                    }
+                } else {
+                    throw new \Exception("File not found after putFileAs");
+                }
+            } catch (\Exception $e) {
+                Log::warning("Method 1 failed: " . $e->getMessage());
+            }
 
-            // Create filename
-            $originalName = pathinfo($attachmentFile->getClientOriginalName(), PATHINFO_FILENAME);
-            $extension = strtolower($attachmentFile->getClientOriginalExtension());
-            $timestamp = time();
-            $filename = 'attachment_' . $recordId . '_' . $timestamp . '_' . substr(md5($originalName), 0, 8) . '.' . $extension;
+            try {
+                Log::info("Trying Method 2: put with file contents");
+                $fileContent = file_get_contents($uploadedFile->getRealPath());
 
-            // Upload to S3
-            $s3Path = $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/attachments/' . $filename;
+                if (!$fileContent) {
+                    throw new \Exception("Could not read file contents");
+                }
 
-            $uploaded = $this->disk->putFileAs(
-                $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/attachments',
-                $attachmentFile,
-                $filename,
-                'private'
-            );
+                $stored = Storage::disk('s3')->put($tempPath, $fileContent);
 
-            if (!$uploaded) {
-                throw new Exception('Failed to upload attachment to S3: ' . $originalName);
+                if ($stored && Storage::disk('s3')->exists($tempPath)) {
+                    Log::info("Method 2 success - put with contents");
+                    return $tempPath;
+                } else {
+                    throw new \Exception("Put method failed");
+                }
+            } catch (\Exception $e) {
+                Log::warning("Method 2 failed: " . $e->getMessage());
             }
 
-            Log::info("Record attachment uploaded", [
-                'record_id' => $recordId,
-                'record_type' => $recordType,
-                'path' => $s3Path,
-                'original_name' => $attachmentFile->getClientOriginalName(),
-                'size' => $attachmentFile->getSize()
-            ]);
-
-            return $s3Path;
-
-        } catch (Exception $e) {
-            Log::error("Error uploading record attachment", [
-                'record_id' => $recordId,
-                'record_type' => $recordType,
-                'error' => $e->getMessage()
-            ]);
+            throw new \Exception("All temp storage methods failed");
+
+        } catch (\Exception $e) {
+            Log::error("Error storing file temporarily: " . $e->getMessage());
+            Log::error("Stack trace: " . $e->getTraceAsString());
             throw $e;
         }
     }
 
     /**
-     * Upload XML receipt file (for electronic invoice imports)
-     *
-     * @param UploadedFile $xmlFile
-     * @param int $recordId
-     * @param string $recordType
-     * @return string
-     * @throws Exception
+     * Upload attachment directly to final S3 location with client structure
      */
-    public function uploadXmlReceipt(UploadedFile $xmlFile, int $recordId, string $recordType = 'OUT'): string
+    public function uploadAttachment($file, $recordId, $type)
     {
         try {
-            $currentClient = session('currentClient', 'iao');
+            $clientName = $this->getClientName();
+            $type = strtolower($type);
+            $extension = $file->getClientOriginalExtension();
+            $fileName = time() . '_' . Str::random(10) . '.' . $extension;
+            $finalPath = "{$clientName}/records/{$type}/{$recordId}/attachments/{$fileName}";
+
+            Log::info("Uploading attachment to S3:");
+            Log::info("- Client: {$clientName}");
+            Log::info("- Record ID: {$recordId}");
+            Log::info("- Type: {$type}");
+            Log::info("- File path: {$finalPath}");
+            Log::info("- File size: " . $file->getSize() . " bytes");
+
+            $storedPath = Storage::disk('s3')->putFileAs(
+                "{$clientName}/records/{$type}/{$recordId}/attachments",
+                $file,
+                $fileName
+            );
 
-            // Validate XML file
-            $this->validateXmlFile($xmlFile);
+            Log::info("File uploaded successfully to S3: {$storedPath}");
 
-            // Create filename
-            $originalName = pathinfo($xmlFile->getClientOriginalName(), PATHINFO_FILENAME);
-            $extension = strtolower($xmlFile->getClientOriginalExtension());
-            $timestamp = time();
-            $filename = 'xml_receipt_' . $recordId . '_' . $timestamp . '_' . substr(md5($originalName), 0, 8) . '.' . $extension;
+            if (Storage::disk('s3')->exists($finalPath)) {
+                Log::info("S3 upload verified successfully");
+                return $finalPath;
+            } else {
+                throw new \Exception("File verification failed - not found on S3");
+            }
 
-            // Upload to S3
-            $s3Path = $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/xml/' . $filename;
+        } catch (\Exception $e) {
+            Log::error("Error uploading attachment to S3: " . $e->getMessage());
+            throw $e;
+        }
+    }
 
-            $uploaded = $this->disk->putFileAs(
-                $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/xml',
-                $xmlFile,
-                $filename,
-                'private'
+    /**
+     * Upload XML receipt for import functionality with client structure
+     */
+    public function uploadXmlReceipt($file, $recordId, $type)
+    {
+        try {
+            $clientName = $this->getClientName();
+            $type = strtolower($type);
+            $extension = $file->getClientOriginalExtension() ?: 'xml';
+            $fileName = 'receipt_' . time() . '_' . Str::random(8) . '.' . $extension;
+            $finalPath = "{$clientName}/records/{$type}/{$recordId}/attachments/{$fileName}";
+
+            Log::info("Uploading XML receipt to S3:");
+            Log::info("- Client: {$clientName}");
+            Log::info("- Path: {$finalPath}");
+
+            $storedPath = Storage::disk('s3')->putFileAs(
+                "{$clientName}/records/{$type}/{$recordId}/attachments",
+                $file,
+                $fileName
             );
 
-            if (!$uploaded) {
-                throw new Exception('Failed to upload XML receipt to S3: ' . $originalName);
-            }
+            Log::info("XML receipt uploaded to S3: {$storedPath}");
+            return $finalPath;
 
-            Log::info("XML receipt uploaded", [
-                'record_id' => $recordId,
-                'record_type' => $recordType,
-                'path' => $s3Path,
-                'original_name' => $xmlFile->getClientOriginalName(),
-                'size' => $xmlFile->getSize()
-            ]);
-
-            return $s3Path;
-
-        } catch (Exception $e) {
-            Log::error("Error uploading XML receipt", [
-                'record_id' => $recordId,
-                'record_type' => $recordType,
-                'error' => $e->getMessage()
-            ]);
+        } catch (\Exception $e) {
+            Log::error("Error uploading XML receipt to S3: " . $e->getMessage());
             throw $e;
         }
     }
 
     /**
-     * Get attachment file URL for display/download
-     *
-     * @param string $filePath
-     * @param string $expiresIn
-     * @return string|null
+     * Get S3 attachment URL
      */
-    public function getAttachmentUrl(string $filePath, string $expiresIn = '+1 hour'): ?string
+    public function getAttachmentUrl($filePath)
     {
-        if (!$filePath) {
-            return null;
-        }
+        try {
+            if (!$filePath) {
+                return null;
+            }
 
-        // Handle legacy local paths - return asset URL
-        if (!$this->isS3Path($filePath)) {
-            return asset('storage/' . $filePath);
-        }
+            Log::info("Getting S3 attachment URL for: {$filePath}");
+
+            if (!Storage::disk('s3')->exists($filePath)) {
+                Log::warning("S3 attachment file not found: {$filePath}");
+
+                $directory = dirname($filePath);
+                try {
+                    $files = Storage::disk('s3')->files($directory);
+                    Log::info("Files in S3 directory {$directory}: " . json_encode($files));
+                } catch (\Exception $e) {
+                    Log::warning("Could not list S3 directory {$directory}: " . $e->getMessage());
+                }
 
-        try {
-            if (!$this->disk->exists($filePath)) {
-                Log::warning("Attachment file not found", ['path' => $filePath]);
                 return null;
             }
 
-            return $this->disk->temporaryUrl($filePath, now()->add($expiresIn));
+            $url = Storage::disk('s3')->temporaryUrl($filePath, now()->addHours(1));
+            Log::info("Generated S3 temporary URL for: {$filePath}");
+            return $url;
 
-        } catch (Exception $e) {
-            Log::error("Error generating attachment URL", [
-                'path' => $filePath,
-                'error' => $e->getMessage()
-            ]);
+        } catch (\Exception $e) {
+            Log::error("Error getting S3 attachment URL for {$filePath}: " . $e->getMessage());
             return null;
         }
     }
 
     /**
-     * Delete attachment file from S3
-     *
-     * @param string $filePath
-     * @return bool
+     * Delete attachment from S3
      */
-    public function deleteAttachment(string $filePath): bool
+    public function deleteAttachment($filePath)
     {
-        // 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("Attachment file deleted", ['path' => $filePath]);
-                return true;
+            if (!$filePath) {
+                return false;
             }
-            return false;
 
-        } catch (Exception $e) {
-            Log::error("Error deleting attachment file", [
-                'path' => $filePath,
-                'error' => $e->getMessage()
-            ]);
-            return false;
+            Log::info("Deleting S3 attachment: {$filePath}");
+
+            if (Storage::disk('s3')->exists($filePath)) {
+                $deleted = Storage::disk('s3')->delete($filePath);
+                if ($deleted) {
+                    Log::info("S3 attachment deleted successfully: {$filePath}");
+                    return true;
+                } else {
+                    Log::error("Failed to delete S3 attachment: {$filePath}");
+                    return false;
+                }
+            } else {
+                Log::warning("S3 attachment not found for deletion: {$filePath}");
+                return false;
+            }
+        } catch (\Exception $e) {
+            Log::error("Error deleting S3 attachment: " . $e->getMessage());
+            throw $e;
         }
     }
 
     /**
-     * Check if path is S3 path
+     * Debug S3 configuration and connectivity
      */
-    private function isS3Path(string $path): bool
+    public function debugFileSystem()
     {
-        return strpos($path, '/records/') !== false ||
-               strpos($path, session('currentClient', 'iao')) === 0;
-    }
+        $clientName = $this->getClientName();
 
-    /**
-     * Validate the uploaded attachment file
-     *
-     * @param UploadedFile $attachmentFile
-     * @throws Exception
-     */
-    private function validateAttachmentFile(UploadedFile $attachmentFile): void
-    {
-        // Check if file is valid
-        if (!$attachmentFile->isValid()) {
-            throw new Exception('Invalid file upload');
-        }
+        Log::info("=== S3 DEBUG ===");
+        Log::info("Client Name: {$clientName}");
+        Log::info("S3 Configuration:");
+        Log::info("- Bucket: " . config('filesystems.disks.s3.bucket'));
+        Log::info("- Region: " . config('filesystems.disks.s3.region'));
+        Log::info("- URL: " . config('filesystems.disks.s3.url'));
+        Log::info("- Key: " . (config('filesystems.disks.s3.key') ? 'Set' : 'Not set'));
+        Log::info("- Secret: " . (config('filesystems.disks.s3.secret') ? 'Set' : 'Not set'));
 
-        // Check file size
-        if ($attachmentFile->getSize() > (self::MAX_FILE_SIZE * 1024)) {
-            throw new Exception('File size exceeds maximum allowed size of ' . self::MAX_FILE_SIZE . 'KB');
-        }
+        try {
+            $testFile = "{$clientName}/test_connection_" . time() . '.txt';
+            $testContent = 'S3 connection test: ' . now();
 
-        // Check file extension
-        $extension = strtolower($attachmentFile->getClientOriginalExtension());
-        if (!in_array($extension, self::ALLOWED_EXTENSIONS)) {
-            throw new Exception('File type not allowed. Allowed types: ' . implode(', ', self::ALLOWED_EXTENSIONS));
-        }
+            Log::info("Testing S3 connection with client structure...");
+            Storage::disk('s3')->put($testFile, $testContent);
 
-        // Check mime type for additional security
-        $mimeType = $attachmentFile->getMimeType();
-        $allowedMimeTypes = [
-            'image/jpeg', 'image/png',
-            'application/pdf',
-            'application/msword',
-            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-            'application/xml', 'text/xml',
-            'text/plain',
-            'text/csv',
-            'application/vnd.ms-excel',
-            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
-        ];
-
-        if (!in_array($mimeType, $allowedMimeTypes)) {
-            throw new Exception('Invalid file type');
+            if (Storage::disk('s3')->exists($testFile)) {
+                Log::info("S3 connection test: SUCCESS");
+                Storage::disk('s3')->delete($testFile);
+            } else {
+                Log::error("S3 connection test: FAILED - file not found after upload");
+            }
+        } catch (\Exception $e) {
+            Log::error("S3 connection test: FAILED - " . $e->getMessage());
         }
+
+        Log::info("=== END S3 DEBUG ===");
     }
 
     /**
-     * Validate XML file specifically
-     *
-     * @param UploadedFile $xmlFile
-     * @throws Exception
+     * Clean up old temp files from S3 for specific client or all clients
      */
-    private function validateXmlFile(UploadedFile $xmlFile): void
+    public function cleanupTempFiles($olderThanHours = 24, $specificClient = null)
     {
-        // Check if file is valid
-        if (!$xmlFile->isValid()) {
-            throw new Exception('Invalid XML file upload');
-        }
-
-        // Check file size
-        if ($xmlFile->getSize() > (self::MAX_FILE_SIZE * 1024)) {
-            throw new Exception('XML file size exceeds maximum allowed size of ' . self::MAX_FILE_SIZE . 'KB');
-        }
-
-        // Check file extension
-        $extension = strtolower($xmlFile->getClientOriginalExtension());
-        if ($extension !== 'xml') {
-            throw new Exception('Only XML files are allowed for receipt uploads');
-        }
+        try {
+            $clientName = $specificClient ?: $this->getClientName();
+            $tempPath = $specificClient ? "{$specificClient}/temp/uploads" : "{$clientName}/temp/uploads";
+
+            $tempFiles = Storage::disk('s3')->files($tempPath);
+            $cutoffTime = now()->subHours($olderThanHours);
+            $deletedCount = 0;
+
+            Log::info("Cleaning up S3 temp files for client '{$clientName}' older than {$olderThanHours} hours");
+
+            foreach ($tempFiles as $file) {
+                $fileTime = Storage::disk('s3')->lastModified($file);
+                if ($fileTime < $cutoffTime->timestamp) {
+                    if (Storage::disk('s3')->delete($file)) {
+                        $deletedCount++;
+                        Log::info("Cleaned up old S3 temp file: {$file}");
+                    }
+                }
+            }
 
-        // Check mime type
-        $mimeType = $xmlFile->getMimeType();
-        $allowedMimeTypes = ['application/xml', 'text/xml'];
+            Log::info("S3 cleanup completed for client '{$clientName}'. Deleted {$deletedCount} temp files.");
 
-        if (!in_array($mimeType, $allowedMimeTypes)) {
-            throw new Exception('Invalid XML file type');
+        } catch (\Exception $e) {
+            Log::error("Error cleaning up S3 temp files: " . $e->getMessage());
         }
     }
 
     /**
-     * Create record folder structure in S3
-     *
-     * @param int $recordId
-     * @param string $recordType
+     * Clean up temp files for all clients
      */
-    public function createRecordFolders(int $recordId, string $recordType = 'OUT'): void
+    public function cleanupAllClientTempFiles($olderThanHours = 24)
     {
         try {
-            $currentClient = session('currentClient', 'iao');
-
-            $folders = [
-                $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/attachments/.gitkeep',
-                $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/xml/.gitkeep'
-            ];
-
-            foreach ($folders as $folder) {
-                if (!$this->disk->exists($folder)) {
-                    $this->disk->put($folder, '');
+            $allDirectories = Storage::disk('s3')->directories('');
+            $deletedCount = 0;
+
+            Log::info("Cleaning up temp files for all clients older than {$olderThanHours} hours");
+
+            foreach ($allDirectories as $clientDir) {
+                $tempPath = "{$clientDir}/temp/uploads";
+
+                if (Storage::disk('s3')->exists($tempPath)) {
+                    $tempFiles = Storage::disk('s3')->files($tempPath);
+                    $cutoffTime = now()->subHours($olderThanHours);
+
+                    foreach ($tempFiles as $file) {
+                        $fileTime = Storage::disk('s3')->lastModified($file);
+                        if ($fileTime < $cutoffTime->timestamp) {
+                            if (Storage::disk('s3')->delete($file)) {
+                                $deletedCount++;
+                                Log::info("Cleaned up old S3 temp file: {$file}");
+                            }
+                        }
+                    }
                 }
             }
 
-            Log::info("Created record folder structure", [
-                'record_id' => $recordId,
-                'record_type' => $recordType
-            ]);
-
-        } catch (Exception $e) {
-            Log::error("Error creating record folders", [
-                'record_id' => $recordId,
-                'record_type' => $recordType,
-                'error' => $e->getMessage()
-            ]);
+            Log::info("S3 cleanup completed for all clients. Deleted {$deletedCount} temp files.");
+
+        } catch (\Exception $e) {
+            Log::error("Error cleaning up all client temp files: " . $e->getMessage());
         }
     }
 
     /**
-     * Get attachment file info
-     *
-     * @param string $filePath
-     * @return array|null
+     * Check if a file exists on S3
      */
-    public function getAttachmentInfo(string $filePath): ?array
+    public function fileExists($filePath)
     {
-        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->getAttachmentUrl($filePath),
-                    'exists' => true,
-                    'storage_type' => 'local'
-                ];
-            }
-            return null;
-        }
-
-        if (!$this->disk->exists($filePath)) {
-            return null;
+        try {
+            return Storage::disk('s3')->exists($filePath);
+        } catch (\Exception $e) {
+            Log::error("Error checking if S3 file exists: " . $e->getMessage());
+            return false;
         }
+    }
 
+    /**
+     * Get file size from S3
+     */
+    public function getFileSize($filePath)
+    {
         try {
-            return [
-                'path' => $filePath,
-                'name' => basename($filePath),
-                'size' => $this->disk->size($filePath),
-                'last_modified' => $this->disk->lastModified($filePath),
-                'url' => $this->getAttachmentUrl($filePath),
-                'exists' => true,
-                'storage_type' => 's3'
-            ];
-
-        } catch (Exception $e) {
-            Log::error("Error getting attachment info", [
-                'path' => $filePath,
-                'error' => $e->getMessage()
-            ]);
-            return null;
+            if (Storage::disk('s3')->exists($filePath)) {
+                return Storage::disk('s3')->size($filePath);
+            }
+            return 0;
+        } catch (\Exception $e) {
+            Log::error("Error getting S3 file size: " . $e->getMessage());
+            return 0;
         }
     }
 
     /**
-     * List all attachments for a record
-     *
-     * @param int $recordId
-     * @param string $recordType
-     * @return array
+     * Get file last modified time from S3
      */
-    public function listRecordAttachments(int $recordId, string $recordType = 'OUT'): array
+    public function getFileLastModified($filePath)
     {
         try {
-            $currentClient = session('currentClient', 'iao');
-            $attachmentPath = $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/attachments';
-
-            $files = $this->disk->files($attachmentPath);
-
-            return array_filter($files, function($file) {
-                $filename = basename($file);
-                return strpos($filename, 'attachment_') === 0;
-            });
-
-        } catch (Exception $e) {
-            Log::error("Error listing record attachments", [
-                'record_id' => $recordId,
-                'record_type' => $recordType,
-                'error' => $e->getMessage()
-            ]);
-            return [];
+            if (Storage::disk('s3')->exists($filePath)) {
+                return Storage::disk('s3')->lastModified($filePath);
+            }
+            return null;
+        } catch (\Exception $e) {
+            Log::error("Error getting S3 file last modified: " . $e->getMessage());
+            return null;
         }
     }
 
     /**
-     * Store XML receipt files for batch import
-     *
-     * @param array $xmlFiles
-     * @param string $batchId
-     * @return array
+     * Get all files for a specific client and type
      */
-    public function storeXmlBatch(array $xmlFiles, string $batchId): array
+    public function getClientFiles($type = null, $clientName = null)
     {
-        $storedFiles = [];
-        $currentClient = session('currentClient', 'iao');
+        try {
+            $clientName = $clientName ?: $this->getClientName();
+            $type = $type ? strtolower($type) : '*';
 
-        foreach ($xmlFiles as $index => $xmlFile) {
-            try {
-                $this->validateXmlFile($xmlFile);
-
-                $originalName = pathinfo($xmlFile->getClientOriginalName(), PATHINFO_FILENAME);
-                $extension = strtolower($xmlFile->getClientOriginalExtension());
-                $timestamp = time();
-                $filename = 'batch_' . $batchId . '_' . $index . '_' . $timestamp . '_' . substr(md5($originalName), 0, 8) . '.' . $extension;
-
-                $s3Path = $currentClient . '/imports/xml_batch/' . $batchId . '/' . $filename;
-
-                $uploaded = $this->disk->putFileAs(
-                    $currentClient . '/imports/xml_batch/' . $batchId,
-                    $xmlFile,
-                    $filename,
-                    'private'
-                );
-
-                if ($uploaded) {
-                    $storedFiles[] = [
-                        'original_name' => $xmlFile->getClientOriginalName(),
-                        's3_path' => $s3Path,
-                        'local_path' => $xmlFile->getRealPath(),
-                        'index' => $index
-                    ];
-
-                    Log::info("XML file stored for batch processing", [
-                        'batch_id' => $batchId,
-                        'original_name' => $xmlFile->getClientOriginalName(),
-                        's3_path' => $s3Path
-                    ]);
-                }
+            $basePath = $type === '*' ? "{$clientName}/records" : "{$clientName}/records/{$type}";
 
-            } catch (Exception $e) {
-                Log::error("Error storing XML file for batch", [
-                    'batch_id' => $batchId,
-                    'file_name' => $xmlFile->getClientOriginalName(),
-                    'error' => $e->getMessage()
-                ]);
-            }
-        }
+            $files = Storage::disk('s3')->allFiles($basePath);
+            Log::info("Found " . count($files) . " files for client '{$clientName}' and type '{$type}'");
 
-        return $storedFiles;
+            return $files;
+        } catch (\Exception $e) {
+            Log::error("Error getting client files: " . $e->getMessage());
+            return [];
+        }
     }
 }

+ 33 - 0
database/migrations/2025_06_12_130008_add_attachment_status_to_records_table.php

@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('records', function (Blueprint $table) {
+            $table->enum('attachment_status', ['none', 'pending', 'processing', 'completed', 'failed'])
+                ->default('none')
+                ->after('attachment');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('records', function (Blueprint $table) {
+            $table->dropColumn('attachment_status');});
+    }
+};

File diff suppressed because it is too large
+ 794 - 167
resources/views/livewire/records_out.blade.php


Some files were not shown because too many files changed in this diff