mandant-crm/frontend/pages/settings/import.vue
Tim Stollberg 570f17e8b8 feat(frontend): commit uncommitted settings, custom fields, and import work
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.
2026-07-18 16:27:19 +07:00

131 lines
4.7 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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