This local working tree had accumulated a substantial amount of finished-but-never-committed frontend work: custom client fields (replacing the old static ClientFields with a dynamic ClientCustomFieldCard + useCustomFields), CSV import (useImport + settings/import page), history type management (useHistoryTypes + settings/history-types page), an admin-only route middleware, several new icon components, a shared useApiFetch composable, and a client detail page restructure ([id].vue -> [id]/index.vue). None of it had ever been pushed, so production was running a stale build that was already linking to routes/components that didn't exist server-side (missing IconSettings, 404s on /settings/import, etc). This commit brings the repo in line with local state.
131 lines
4.7 KiB
Vue
131 lines
4.7 KiB
Vue
<script setup lang="ts">
|
||
import type { ImportResult } from '~/types'
|
||
|
||
definePageMeta({ middleware: ['auth', 'admin'] })
|
||
|
||
const { importClients } = useImport()
|
||
|
||
const file = ref<File | null>(null)
|
||
const fileInput = ref<HTMLInputElement | null>(null)
|
||
const uploading = ref(false)
|
||
const error = ref('')
|
||
const result = ref<ImportResult | null>(null)
|
||
|
||
const onFile = (e: Event) => {
|
||
const target = e.target as HTMLInputElement
|
||
file.value = target.files?.[0] ?? null
|
||
error.value = ''
|
||
}
|
||
|
||
const submit = async () => {
|
||
if (!file.value) {
|
||
error.value = 'Bitte eine CSV-Datei auswählen.'
|
||
return
|
||
}
|
||
uploading.value = true
|
||
error.value = ''
|
||
result.value = null
|
||
try {
|
||
result.value = await importClients(file.value)
|
||
file.value = null
|
||
if (fileInput.value) fileInput.value.value = ''
|
||
} catch (e: any) {
|
||
error.value = e?.data?.message ?? 'Import fehlgeschlagen.'
|
||
} finally {
|
||
uploading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="min-h-screen bg-gray-50">
|
||
<header class="bg-white border-b border-gray-200 px-6 py-3 flex items-center gap-4">
|
||
<NuxtLink
|
||
to="/clients"
|
||
title="Alle Mandanten"
|
||
aria-label="Alle Mandanten"
|
||
class="text-gray-500 hover:text-gray-700"
|
||
>
|
||
<IconArrowLeft />
|
||
</NuxtLink>
|
||
<h1 class="text-base font-semibold text-gray-800">Mandanten-Import</h1>
|
||
</header>
|
||
|
||
<div class="max-w-3xl mx-auto py-6 px-6 space-y-6">
|
||
<!-- Upload -->
|
||
<section class="bg-white border border-gray-200 p-4 space-y-3">
|
||
<p class="text-sm text-gray-600">
|
||
CSV-Datei (Semikolon-getrennt) hochladen. Bekannte Mandantennummern werden
|
||
übersprungen – außer die Spalte „Ueberschreiben“ enthält „ja“, dann werden
|
||
die Stammdatenfelder aktualisiert.
|
||
</p>
|
||
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">CSV-Datei</label>
|
||
<input
|
||
ref="fileInput"
|
||
type="file"
|
||
accept=".csv,.txt"
|
||
@change="onFile"
|
||
class="block w-full text-sm text-gray-700 file:mr-3 file:border-0 file:bg-gray-100 file:px-3 file:py-1.5 file:text-sm file:text-gray-700 hover:file:bg-gray-200"
|
||
/>
|
||
</div>
|
||
|
||
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
|
||
|
||
<div>
|
||
<button
|
||
@click="submit"
|
||
:disabled="uploading || !file"
|
||
class="bg-blue-600 text-white px-4 py-1.5 text-sm hover:bg-blue-700 disabled:opacity-50"
|
||
>
|
||
{{ uploading ? 'Importiere…' : 'Import starten' }}
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Result -->
|
||
<section v-if="result" class="bg-white border border-gray-200 p-4 space-y-3">
|
||
<h2 class="text-sm font-semibold text-gray-700">Ergebnis</h2>
|
||
<div class="grid grid-cols-4 gap-3 text-center">
|
||
<div class="border border-gray-100 py-3">
|
||
<div class="text-lg font-semibold text-gray-800">{{ result.total }}</div>
|
||
<div class="text-xs text-gray-500">Zeilen</div>
|
||
</div>
|
||
<div class="border border-gray-100 py-3">
|
||
<div class="text-lg font-semibold text-green-700">{{ result.created }}</div>
|
||
<div class="text-xs text-gray-500">Neu angelegt</div>
|
||
</div>
|
||
<div class="border border-gray-100 py-3">
|
||
<div class="text-lg font-semibold text-blue-700">{{ result.updated }}</div>
|
||
<div class="text-xs text-gray-500">Aktualisiert</div>
|
||
</div>
|
||
<div class="border border-gray-100 py-3">
|
||
<div class="text-lg font-semibold text-gray-500">{{ result.skipped }}</div>
|
||
<div class="text-xs text-gray-500">Übersprungen</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="result.errors.length" class="space-y-2">
|
||
<h3 class="text-xs font-medium text-red-600">Fehler ({{ result.errors.length }})</h3>
|
||
<table class="w-full text-sm">
|
||
<thead>
|
||
<tr class="border-b border-gray-200 text-left text-xs text-gray-500">
|
||
<th class="px-3 py-1.5 font-medium">Zeile</th>
|
||
<th class="px-3 py-1.5 font-medium">Mandantennr.</th>
|
||
<th class="px-3 py-1.5 font-medium">Meldung</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(err, i) in result.errors" :key="i" class="border-b border-gray-100">
|
||
<td class="px-3 py-1.5 text-gray-600">{{ err.row }}</td>
|
||
<td class="px-3 py-1.5 text-gray-600">{{ err.client_number ?? '—' }}</td>
|
||
<td class="px-3 py-1.5 text-gray-700">{{ err.message }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</template>
|