templates2.Admin.Nuxt/app/components/ImportWizard.vue
w4c-templates 914988e187
Some checks failed
CI / quality (push) Has been cancelled
chore(template): publish Admin.Nuxt
2026-10-11 23:25:56 +03:00

140 lines
5.5 KiB
Vue

<script setup lang="ts">
defineProps<{ canImport: boolean }>();
type Row = { name: string; email: string; company: string; status: string };
type Preview = { total: number; valid: number; errors: { row: number; message: string }[]; duplicatesInFile: number[]; sample: Row[] };
type Committed = { imported: number; updated: number; errors: { row: number; message: string }[]; duplicatesInFile: number[] };
const SAMPLE = 'name,email,company,status\nJane Doe,jane@example.com,Acme,lead\nBad Row,not-an-email,Globex,active';
const csv = ref('');
const preview = ref<Preview | null>(null);
const committed = ref<Committed | null>(null);
const busy = ref(false);
const error = ref<string | null>(null);
async function post(commit: boolean) {
busy.value = true;
error.value = null;
try {
const data = await $fetch<Record<string, unknown>>('/api/admin/import/customers', {
method: 'POST',
body: { csv: csv.value, commit },
});
if (commit) {
preview.value = null;
committed.value = data as unknown as Committed;
} else {
committed.value = null;
preview.value = data as unknown as Preview;
}
} catch (e) {
const err = e as { data?: { statusMessage?: string }; statusMessage?: string };
error.value = err.data?.statusMessage || err.statusMessage || t('common.error');
} finally {
busy.value = false;
}
}
async function onFile(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) csv.value = await file.text();
}
</script>
<template>
<div class="space-y-6">
<h1 class="text-2xl font-bold">{{ t('admin.importTitle') }}</h1>
<p v-if="!canImport" class="muted text-sm">{{ t('admin.readOnly') }}</p>
<template v-else>
<header class="flex flex-wrap items-center justify-between gap-3">
<p class="muted text-sm">{{ t('admin.importHint') }}</p>
<a class="btn-secondary" href="/api/admin/export/customers" download>{{ t('admin.export') }}</a>
</header>
<div class="card space-y-3">
<label class="block text-sm">
{{ t('admin.chooseFile') }}
<input type="file" accept=".csv,text/csv" class="mt-1 block text-sm" @change="onFile" >
</label>
<label class="block text-sm">
CSV
<textarea v-model="csv" class="input mt-1 min-h-40 font-mono" :placeholder="SAMPLE"/>
</label>
<div class="flex flex-wrap gap-2">
<button type="button" class="btn-secondary" :disabled="busy || !csv.trim()" @click="post(false)">
{{ t('admin.preview') }}
</button>
<button type="button" class="btn-primary" :disabled="busy || !preview || preview.valid === 0" @click="post(true)">
{{ busy ? t('admin.importing') : t('admin.commit', { count: preview?.valid ?? 0 }) }}
</button>
</div>
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
</div>
<div v-if="committed" class="card">
<p class="text-sm font-medium">
{{ t('admin.imported', { imported: committed.imported, updated: committed.updated }) }}
</p>
</div>
<div v-if="preview" class="card space-y-4">
<p class="text-sm">
{{
t('admin.previewResult', {
total: preview.total,
valid: preview.valid,
errors: preview.errors.length,
duplicates: preview.duplicatesInFile.length,
})
}}
</p>
<div>
<h2 class="mb-2 text-sm font-semibold">{{ t('admin.sample') }}</h2>
<p v-if="preview.sample.length === 0" class="muted text-sm">{{ t('admin.empty') }}</p>
<div v-else class="overflow-x-auto rounded-xl border">
<table class="w-full text-sm">
<thead class="muted text-left text-xs uppercase">
<tr>
<th class="px-4 py-2">{{ t('admin.name') }}</th>
<th class="px-4 py-2">{{ t('admin.email') }}</th>
<th class="px-4 py-2">{{ t('admin.company') }}</th>
<th class="px-4 py-2">{{ t('admin.status') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="r in preview.sample" :key="r.email" class="border-t">
<td class="px-4 py-2">{{ r.name }}</td>
<td class="px-4 py-2">{{ r.email }}</td>
<td class="px-4 py-2">{{ r.company || t('admin.none') }}</td>
<td class="px-4 py-2">{{ t(`admin.${r.status}`) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h2 class="mb-2 text-sm font-semibold">{{ t('admin.perRowErrors') }}</h2>
<p v-if="preview.errors.length === 0" class="muted text-sm">{{ t('admin.noErrors') }}</p>
<div v-else class="overflow-x-auto rounded-xl border">
<table class="w-full text-sm">
<thead class="muted text-left text-xs uppercase">
<tr>
<th class="px-4 py-2">{{ t('admin.row') }}</th>
<th class="px-4 py-2">{{ t('admin.message') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="e in preview.errors" :key="`${e.row}-${e.message}`" class="border-t">
<td class="px-4 py-2">{{ e.row }}</td>
<td class="px-4 py-2 text-red-600">{{ e.message }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
</div>
</template>