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\ImportTemplate;
use App\Services\CsvImportService;
use App\Services\Import\ImportServiceV2;
use App\Services\Import\ImportSimulationServiceV2;
use App\Services\ImportProcessor;
use Illuminate\Http\Request;
@ -190,6 +189,7 @@ public function process(Import $import, Request $request, ImportProcessor $proce
try {
$result = $processor->process($import, user: $request->user());
return response()->json($result);
} catch (\Throwable $e) {
\Log::error('Import processing failed', [
@ -202,7 +202,7 @@ public function process(Import $import, Request $request, ImportProcessor $proce
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);
}
}

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
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"

View File

@ -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",
]);
</script>
<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
variant="secondary"
@click.prevent="$emit('preview')"
@click.prevent="$emit('download')"
:disabled="!importId"
title="Preznesi originalno uvozno datoteko"
>
<EyeIcon class="h-4 w-4 mr-2" />
Predogled vrstic
</Button>
<Button
variant="default"
class="bg-orange-600 hover:bg-orange-700"
@click.prevent="$emit('save-mappings')"
:disabled="!importId || processing || savingMappings || isCompleted"
title="Shrani preslikave za ta uvoz"
>
<span
v-if="savingMappings"
class="inline-block h-4 w-4 mr-2 border-2 border-white/70 border-t-transparent rounded-full animate-spin"
></span>
<ArrowPathIcon v-else class="h-4 w-4 mr-2" />
<span>Shrani preslikave</span>
<Badge
v-if="selectedMappingsCount"
variant="secondary"
class="ml-2 text-xs"
>{{ selectedMappingsCount }}</Badge>
</Button>
<Button
variant="default"
class="bg-purple-600 hover:bg-purple-700"
@click.prevent="$emit('process-import')"
:disabled="!canProcess"
>
<BeakerIcon class="h-4 w-4 mr-2" />
{{ processing ? "Obdelava…" : "Obdelaj uvoz" }}
</Button>
<Button
variant="default"
class="bg-blue-600 hover:bg-blue-700"
@click.prevent="$emit('simulate')"
:disabled="!importId || processing"
>
<ArrowDownOnSquareIcon class="h-4 w-4 mr-2" />
Simulacija vnosa
<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" />
Predogled vrstic
</Button>
<Button
variant="default"
class="bg-orange-600 hover:bg-orange-700"
@click.prevent="$emit('save-mappings')"
:disabled="!importId || processing || savingMappings || isCompleted"
title="Shrani preslikave za ta uvoz"
>
<span
v-if="savingMappings"
class="inline-block h-4 w-4 mr-2 border-2 border-white/70 border-t-transparent rounded-full animate-spin"
></span>
<ArrowPathIcon v-else class="h-4 w-4 mr-2" />
<span>Shrani preslikave</span>
<Badge v-if="selectedMappingsCount" variant="secondary" class="ml-2 text-xs">{{
selectedMappingsCount
}}</Badge>
</Button>
<Button
variant="default"
class="bg-purple-600 hover:bg-purple-700"
@click.prevent="$emit('process-import')"
:disabled="!canProcess"
>
<BeakerIcon class="h-4 w-4 mr-2" />
{{ processing ? "Obdelava…" : "Obdelaj uvoz" }}
</Button>
<Button
variant="default"
class="bg-blue-600 hover:bg-blue-700"
@click.prevent="$emit('simulate')"
:disabled="!importId || processing"
>
<ArrowDownOnSquareIcon class="h-4 w-4 mr-2" />
Simulacija vnosa
</Button>
</div>
</div>
</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-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

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();
});