From 9cc1b7072cf48efb42ba1b91a8f9313ef61dff3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Sun, 1 Feb 2026 09:22:34 +0100 Subject: [PATCH] added download button for orignal import csv file --- app/Http/Controllers/ImportController.php | 27 +++-- resources/js/Pages/Imports/Import.vue | 11 ++ .../js/Pages/Imports/Partials/ActionsBar.vue | 103 ++++++++++-------- routes/web.php | 1 + tests/Feature/ImportDownloadTest.php | 72 ++++++++++++ 5 files changed, 163 insertions(+), 51 deletions(-) create mode 100644 tests/Feature/ImportDownloadTest.php diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 689a872..b97d655 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -9,7 +9,6 @@ use App\Models\ImportEvent; use App\Models\ImportTemplate; use App\Services\CsvImportService; -use App\Services\Import\ImportServiceV2; use App\Services\Import\ImportSimulationServiceV2; use App\Services\ImportProcessor; use Illuminate\Http\Request; @@ -187,9 +186,10 @@ public function store(Request $request) public function process(Import $import, Request $request, ImportProcessor $processor) { $import->update(['status' => 'validating', 'started_at' => now()]); - + try { $result = $processor->process($import, user: $request->user()); + return response()->json($result); } catch (\Throwable $e) { \Log::error('Import processing failed', [ @@ -197,12 +197,12 @@ public function process(Import $import, Request $request, ImportProcessor $proce 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); - + $import->update(['status' => 'failed']); - + return response()->json([ 'success' => false, - 'message' => 'Import processing failed: ' . $e->getMessage(), + 'message' => 'Import processing failed: '.$e->getMessage(), ], 500); } } @@ -712,8 +712,6 @@ public function simulatePayments(Import $import, Request $request) * templates. For payments templates, payment-specific summaries/entities will be included * automatically by the simulation service when mappings contain the payment root. * - * @param Import $import - * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function simulate(Import $import, Request $request) @@ -829,4 +827,19 @@ public function destroy(Request $request, Import $import) return back()->with('success', 'Import deleted successfully'); } + + // Download the original import file + public function download(Import $import) + { + // Verify file exists + if (! $import->disk || ! $import->path || ! Storage::disk($import->disk)->exists($import->path)) { + return response()->json([ + 'error' => 'File not found', + ], 404); + } + + $fileName = $import->original_name ?? 'import_'.$import->uuid; + + return Storage::disk($import->disk)->download($import->path, $fileName); + } } diff --git a/resources/js/Pages/Imports/Import.vue b/resources/js/Pages/Imports/Import.vue index 83e18c1..5bb4f6b 100644 --- a/resources/js/Pages/Imports/Import.vue +++ b/resources/js/Pages/Imports/Import.vue @@ -1094,6 +1094,16 @@ async function fetchEvents() { } } +async function downloadImport() { + if (!importId.value) return; + try { + const url = route("imports.download", { import: importId.value }); + window.location.href = url; + } catch (e) { + console.error("Download failed", e); + } +} + // Simulation (generic or payments) state const showPaymentSim = ref(false); const paymentSimLoading = ref(false); @@ -1339,6 +1349,7 @@ async function fetchSimulation() { :can-process="canProcess" :selected-mappings-count="selectedMappingsCount" @preview="openPreview" + @download="downloadImport" @save-mappings="saveMappings" @process-import="processImport" @simulate="openSimulation" diff --git a/resources/js/Pages/Imports/Partials/ActionsBar.vue b/resources/js/Pages/Imports/Partials/ActionsBar.vue index 47526ed..a6903df 100644 --- a/resources/js/Pages/Imports/Partials/ActionsBar.vue +++ b/resources/js/Pages/Imports/Partials/ActionsBar.vue @@ -4,9 +4,10 @@ import { ArrowPathIcon, BeakerIcon, ArrowDownOnSquareIcon, + ArrowDownTrayIcon, } from "@heroicons/vue/24/outline"; -import { Button } from '@/Components/ui/button'; -import { Badge } from '@/Components/ui/badge'; +import { Button } from "@/Components/ui/button"; +import { Badge } from "@/Components/ui/badge"; const props = defineProps({ importId: [Number, String], @@ -16,54 +17,68 @@ const props = defineProps({ canProcess: Boolean, selectedMappingsCount: Number, }); -const emits = defineEmits(["preview", "save-mappings", "process-import", "simulate"]); +const emits = defineEmits([ + "preview", + "save-mappings", + "process-import", + "simulate", + "download", +]); diff --git a/routes/web.php b/routes/web.php index 3580a5c..c2c269e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -463,6 +463,7 @@ Route::get('imports/{import}/missing-keyref-rows', [ImportController::class, 'missingKeyrefRows'])->name('imports.missing-keyref-rows'); Route::get('imports/{import}/missing-keyref-csv', [ImportController::class, 'exportMissingKeyrefCsv'])->name('imports.missing-keyref-csv'); Route::get('imports/{import}/preview', [ImportController::class, 'preview'])->name('imports.preview'); + Route::get('imports/{import}/download', [ImportController::class, 'download'])->name('imports.download'); Route::get('imports/{import}/missing-contracts', [ImportController::class, 'missingContracts'])->name('imports.missing-contracts'); Route::post('imports/{import}/options', [ImportController::class, 'updateOptions'])->name('imports.options'); // Generic simulation endpoint (new) – provides projected effects for first N rows regardless of payments template diff --git a/tests/Feature/ImportDownloadTest.php b/tests/Feature/ImportDownloadTest.php new file mode 100644 index 0000000..37f5059 --- /dev/null +++ b/tests/Feature/ImportDownloadTest.php @@ -0,0 +1,72 @@ +put($path, $csv); + + // Authenticate a user + $user = User::factory()->create(); + Auth::login($user); + + // Create import record + $import = Import::create([ + 'uuid' => $uuid, + 'user_id' => $user->id, + 'import_template_id' => null, + 'client_id' => null, + 'source_type' => 'csv', + 'file_name' => basename($path), + 'original_name' => 'test-import.csv', + 'disk' => $disk, + 'path' => $path, + 'size' => strlen($csv), + 'status' => 'uploaded', + 'meta' => ['has_header' => true], + ]); + + // Test download endpoint + $response = test()->get(route('imports.download', ['import' => $import->id])); + + $response->assertSuccessful(); + expect($response->headers->get('Content-Disposition'))->toContain('test-import.csv'); + + // Clean up + Storage::disk($disk)->delete($path); +}); + +it('returns 404 when file does not exist', function () { + // Authenticate a user + $user = User::factory()->create(); + Auth::login($user); + + // Create import record with non-existent file + $import = Import::create([ + 'uuid' => (string) Str::uuid(), + 'user_id' => $user->id, + 'import_template_id' => null, + 'client_id' => null, + 'source_type' => 'csv', + 'file_name' => 'missing.csv', + 'original_name' => 'missing.csv', + 'disk' => 'local', + 'path' => 'imports/nonexistent.csv', + 'size' => 0, + 'status' => 'uploaded', + 'meta' => ['has_header' => true], + ]); + + // Test download endpoint + $response = test()->get(route('imports.download', ['import' => $import->id])); + + $response->assertNotFound(); +});