Teren-app/resources/js/Pages/Imports/Partials/CsvPreviewModal.vue

205 lines
7.5 KiB
Vue

<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Badge } from "@/Components/ui/badge";
import { Label } from "@/Components/ui/label";
import { Checkbox } from "@/Components/ui/checkbox";
import { ChevronRightIcon } from "@heroicons/vue/24/outline";
import { computed, ref } from "vue";
const props = defineProps({
show: Boolean,
limit: Number,
rows: Array,
columns: Array,
loading: Boolean,
truncated: Boolean,
hasHeader: Boolean,
})
const emits = defineEmits(['close','change-limit','refresh'])
// State
const selectedRow = ref(null);
const hideEmptyRows = ref(true);
// Filter out columns with empty headers
const visibleColumns = computed(() => {
if (!props.columns) return [];
return props.columns.filter(col => col && String(col).trim() !== '');
});
// Check if row is empty (first 2 columns are empty)
function isRowEmpty(row) {
if (!visibleColumns.value || visibleColumns.value.length === 0) return false;
const firstCols = visibleColumns.value.slice(0, 2);
return firstCols.every(col => !row[col] || String(row[col]).trim() === '');
}
// Filtered rows
const visibleRows = computed(() => {
if (!props.rows) return [];
let filtered = props.rows;
if (hideEmptyRows.value) {
filtered = filtered.filter(r => !isRowEmpty(r));
}
return filtered.map((r, idx) => ({ ...r, index: idx + 1 }));
});
// Select row
function selectRow(row) {
selectedRow.value = row;
}
function onLimit(val) {
emits('change-limit', Number(val));
emits('refresh');
}
</script>
<template>
<Dialog :open="show" @update:open="(val) => !val && $emit('close')">
<DialogContent class="max-w-7xl max-h-[90vh] overflow-hidden flex flex-col p-0">
<!-- Header -->
<div class="px-6 py-4 border-b bg-linear-to-r from-gray-50 to-white">
<div class="flex items-center justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-900">CSV Preview</h2>
<p class="text-sm text-gray-500 mt-1">
Showing {{ visibleRows.length }} of {{ rows.length }} rows
</p>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Label for="limit-select" class="text-sm text-gray-600">Limit:</Label>
<Select :model-value="String(limit)" @update:model-value="onLimit">
<SelectTrigger id="limit-select" class="w-24 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="200">200</SelectItem>
<SelectItem value="300">300</SelectItem>
<SelectItem value="500">500</SelectItem>
</SelectContent>
</Select>
</div>
<Button @click="$emit('refresh')" variant="outline" size="sm" :disabled="loading">
{{ loading ? 'Loading…' : 'Refresh' }}
</Button>
<div class="flex items-center gap-2">
<Checkbox
id="hide-empty-rows"
:checked="hideEmptyRows"
@update:checked="(val) => hideEmptyRows = val"
/>
<Label for="hide-empty-rows" class="text-xs cursor-pointer">
Hide empty rows
</Label>
</div>
<Badge v-if="truncated" variant="outline" class="bg-amber-50 text-amber-700 border-amber-200">
Truncated at limit
</Badge>
</div>
</div>
</div>
<!-- Split View -->
<div class="flex-1 flex overflow-hidden">
<!-- Left Panel - Row List -->
<div class="w-96 border-r bg-gray-50 overflow-y-auto">
<div v-if="loading" class="p-8 text-center text-gray-500">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
Loading...
</div>
<div v-else-if="!visibleRows.length" class="p-8 text-center text-gray-500">
No rows to display
</div>
<div v-else class="divide-y">
<button
v-for="row in visibleRows"
:key="row.index"
@click="selectRow(row)"
class="w-full px-4 py-3 text-left hover:bg-white transition-colors"
:class="{
'bg-white shadow-sm': selectedRow?.index === row.index,
}"
>
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
<!-- Row Number -->
<div class="flex-shrink-0">
<div class="w-8 h-8 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center text-xs font-semibold">
{{ row.index }}
</div>
</div>
<!-- Row Preview -->
<div class="flex-1 min-w-0">
<div class="text-xs font-semibold text-gray-900 mb-1">
Row #{{ row.index }}
</div>
<div class="text-xs text-gray-600 truncate">
{{
visibleColumns.slice(0, 2).map(col => row[col]).filter(Boolean).join(' • ') || 'Empty row'
}}
</div>
</div>
</div>
<!-- Arrow -->
<ChevronRightIcon class="h-4 w-4 text-gray-400 flex-shrink-0" />
</div>
</button>
</div>
</div>
<!-- Right Panel - Row Details -->
<div v-if="selectedRow" class="flex-1 overflow-y-auto p-6">
<!-- Row Header -->
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-900">
Row #{{ selectedRow.index }}
</h3>
<p class="text-sm text-gray-500">Full row details</p>
</div>
<!-- Row Data -->
<div class="bg-gray-50 rounded-lg p-4">
<dl class="grid grid-cols-1 gap-3">
<div
v-for="col in visibleColumns"
:key="col"
class="flex items-start gap-3 py-2 border-b border-gray-200 last:border-0"
>
<dt class="text-sm font-medium text-gray-600 w-48 flex-shrink-0">
{{ col }}
</dt>
<dd class="text-sm text-gray-900 flex-1 font-medium whitespace-pre-wrap break-words">
{{ selectedRow[col] || '—' }}
</dd>
</div>
</dl>
</div>
</div>
<!-- Empty State for Right Panel -->
<div v-else class="flex-1 flex items-center justify-center text-gray-400">
<div class="text-center">
<div class="text-5xl mb-3">📄</div>
<p class="text-sm">Select a row to view details</p>
</div>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-3 border-t bg-gray-50 text-xs text-gray-500">
Header detection: <span class="font-medium">{{ hasHeader ? 'header present' : 'no header' }}</span>
Click a row to view full details
</div>
</DialogContent>
</Dialog>
</template>