added download button for orignal import csv file

This commit is contained in:
Simon Pocrnjič 2026-02-01 09:22:34 +01:00
parent 2968bcf3f8
commit 9cc1b7072c
5 changed files with 163 additions and 51 deletions

View File

@ -9,7 +9,6 @@
use App\Models\ImportEvent; use App\Models\ImportEvent;
use App\Models\ImportTemplate; use App\Models\ImportTemplate;
use App\Services\CsvImportService; use App\Services\CsvImportService;
use App\Services\Import\ImportServiceV2;
use App\Services\Import\ImportSimulationServiceV2; use App\Services\Import\ImportSimulationServiceV2;
use App\Services\ImportProcessor; use App\Services\ImportProcessor;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -190,6 +189,7 @@ public function process(Import $import, Request $request, ImportProcessor $proce
try { try {
$result = $processor->process($import, user: $request->user()); $result = $processor->process($import, user: $request->user());
return response()->json($result); return response()->json($result);
} catch (\Throwable $e) { } catch (\Throwable $e) {
\Log::error('Import processing failed', [ \Log::error('Import processing failed', [
@ -202,7 +202,7 @@ public function process(Import $import, Request $request, ImportProcessor $proce
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Import processing failed: ' . $e->getMessage(), 'message' => 'Import processing failed: '.$e->getMessage(),
], 500); ], 500);
} }
} }
@ -712,8 +712,6 @@ public function simulatePayments(Import $import, Request $request)
* templates. For payments templates, payment-specific summaries/entities will be included * templates. For payments templates, payment-specific summaries/entities will be included
* automatically by the simulation service when mappings contain the payment root. * automatically by the simulation service when mappings contain the payment root.
* *
* @param Import $import
* @param Request $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function simulate(Import $import, Request $request) 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'); 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);
}
} }

View File

@ -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 // Simulation (generic or payments) state
const showPaymentSim = ref(false); const showPaymentSim = ref(false);
const paymentSimLoading = ref(false); const paymentSimLoading = ref(false);
@ -1339,6 +1349,7 @@ async function fetchSimulation() {
:can-process="canProcess" :can-process="canProcess"
:selected-mappings-count="selectedMappingsCount" :selected-mappings-count="selectedMappingsCount"
@preview="openPreview" @preview="openPreview"
@download="downloadImport"
@save-mappings="saveMappings" @save-mappings="saveMappings"
@process-import="processImport" @process-import="processImport"
@simulate="openSimulation" @simulate="openSimulation"

View File

@ -4,9 +4,10 @@ import {
ArrowPathIcon, ArrowPathIcon,
BeakerIcon, BeakerIcon,
ArrowDownOnSquareIcon, ArrowDownOnSquareIcon,
ArrowDownTrayIcon,
} from "@heroicons/vue/24/outline"; } from "@heroicons/vue/24/outline";
import { Button } from '@/Components/ui/button'; import { Button } from "@/Components/ui/button";
import { Badge } from '@/Components/ui/badge'; import { Badge } from "@/Components/ui/badge";
const props = defineProps({ const props = defineProps({
importId: [Number, String], importId: [Number, String],
@ -16,15 +17,30 @@ const props = defineProps({
canProcess: Boolean, canProcess: Boolean,
selectedMappingsCount: Number, selectedMappingsCount: Number,
}); });
const emits = defineEmits(["preview", "save-mappings", "process-import", "simulate"]); const emits = defineEmits([
"preview",
"save-mappings",
"process-import",
"simulate",
"download",
]);
</script> </script>
<template> <template>
<div class="flex flex-wrap gap-2 items-center" v-if="!isCompleted"> <div class="flex flex-wrap gap-2 items-center">
<!-- Download button - always visible -->
<Button <Button
variant="secondary" variant="secondary"
@click.prevent="$emit('preview')" @click.prevent="$emit('download')"
:disabled="!importId" :disabled="!importId"
title="Preznesi originalno uvozno datoteko"
> >
<ArrowDownTrayIcon class="h-4 w-4 mr-2" />
Presnemi datoteko
</Button>
<!-- Other action buttons - only when not completed -->
<div class="flex flex-wrap gap-2 items-center" v-if="!isCompleted">
<Button variant="secondary" @click.prevent="$emit('preview')" :disabled="!importId">
<EyeIcon class="h-4 w-4 mr-2" /> <EyeIcon class="h-4 w-4 mr-2" />
Predogled vrstic Predogled vrstic
</Button> </Button>
@ -41,11 +57,9 @@ const emits = defineEmits(["preview", "save-mappings", "process-import", "simula
></span> ></span>
<ArrowPathIcon v-else class="h-4 w-4 mr-2" /> <ArrowPathIcon v-else class="h-4 w-4 mr-2" />
<span>Shrani preslikave</span> <span>Shrani preslikave</span>
<Badge <Badge v-if="selectedMappingsCount" variant="secondary" class="ml-2 text-xs">{{
v-if="selectedMappingsCount" selectedMappingsCount
variant="secondary" }}</Badge>
class="ml-2 text-xs"
>{{ selectedMappingsCount }}</Badge>
</Button> </Button>
<Button <Button
variant="default" variant="default"
@ -66,4 +80,5 @@ const emits = defineEmits(["preview", "save-mappings", "process-import", "simula
Simulacija vnosa Simulacija vnosa
</Button> </Button>
</div> </div>
</div>
</template> </template>

View File

@ -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-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}/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}/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::get('imports/{import}/missing-contracts', [ImportController::class, 'missingContracts'])->name('imports.missing-contracts');
Route::post('imports/{import}/options', [ImportController::class, 'updateOptions'])->name('imports.options'); 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 // Generic simulation endpoint (new) provides projected effects for first N rows regardless of payments template

View File

@ -0,0 +1,72 @@
<?php
use App\Models\Import;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
it('downloads the original import file', function () {
// Create a test file
$uuid = (string) Str::uuid();
$disk = 'local';
$path = "imports/{$uuid}.csv";
$csv = "email,reference\nalpha@example.com,REF-1\n";
Storage::disk($disk)->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();
});