Offene Vorschauen sichtbar machen und doppelt aufnehmen verhindern
Der Abgleich bucht bewusst nichts, sondern legt eine Vorschau zur Bestaetigung an. Nur fuehrte von dort kein Weg weiter: die Import-Seite zeigte allein den Datei-Upload, und die Rueckmeldung sagte lediglich "abgeglichen". Die gefundenen Pakete lagen damit unsichtbar herum und der Knopf wirkte, als tue er nichts. Die Import-Seite listet jetzt alle Importe, die noch auf Bestaetigung warten, und oeffnet sie in derselben Vorschau wie einen frischen Upload. Die Meldung nach dem Abgleich nennt die Zahl der Funde und den naechsten Schritt. Ausserdem beruecksichtigt der Abgleich jetzt offene Vorschauen: bisher pruefte er nur gegen bereits gebuchte Pakete. Ein zweiter Lauf legte dieselben Zeilen deshalb ein weiteres Mal an, und beim Buchen kollidierten beide Importe an der eindeutigen Bestell-ID - der zweite schlug komplett fehl.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import Notice from '../components/Notice';
|
||||
import { ApiError, api, uploadFile } from '../lib/api';
|
||||
import { PLAN_LABEL, formatDate } from '../lib/format';
|
||||
@@ -62,12 +62,53 @@ export default function Import() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [flash, setFlash] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [open, setOpen] = useState<ImportRecord[]>([]);
|
||||
|
||||
const loadOpen = useCallback(async () => {
|
||||
try {
|
||||
const result = await api<{ items: ImportRecord[] }>('/api/imports');
|
||||
setOpen(result.items.filter((entry) => entry.status === 'PENDING'));
|
||||
} catch {
|
||||
// Die Liste ist Beiwerk; ein Fehler hier darf den Upload nicht blockieren.
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadOpen();
|
||||
}, [loadOpen]);
|
||||
|
||||
const reset = () => {
|
||||
setUpload(null);
|
||||
setPreview(null);
|
||||
setMapping(EMPTY_MAPPING);
|
||||
setError(null);
|
||||
void loadOpen();
|
||||
};
|
||||
|
||||
/** Einen bereits angelegten Import (etwa aus dem GExtension-Abgleich) oeffnen. */
|
||||
const openExisting = async (record: ImportRecord) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const full = await api<ImportRecord>(`/api/imports/${record.id}`);
|
||||
const rows = full.rows ?? [];
|
||||
const summary = {
|
||||
BOOK: 0,
|
||||
CREATE_PLAYER_AND_BOOK: 0,
|
||||
SKIP_DUPLICATE: 0,
|
||||
SKIP_IGNORED: 0,
|
||||
SKIP_UNMAPPED: 0,
|
||||
SKIP_NO_PLAYER: 0,
|
||||
} as Record<ImportPlan, number>;
|
||||
for (const row of rows) summary[row.plan] += 1;
|
||||
|
||||
setUpload({ importId: full.id, columns: [], rowCount: full.rowCount, sample: [] });
|
||||
setPreview({ summary, import: full });
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Import konnte nicht geoeffnet werden');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
@@ -154,6 +195,41 @@ export default function Import() {
|
||||
{error && <Notice kind="error" onDismiss={() => setError(null)}>{error}</Notice>}
|
||||
{flash && <Notice kind="success" onDismiss={() => setFlash(null)}>{flash}</Notice>}
|
||||
|
||||
{!upload && open.length > 0 && (
|
||||
<section className="block block-flag">
|
||||
<h2>Wartet auf Bestätigung ({open.length})</h2>
|
||||
<Notice kind="warn">
|
||||
Diese Importe sind angelegt, aber noch nicht gebucht. Der GExtension-Abgleich landet
|
||||
ebenfalls hier — erst nach dem Bestätigen entstehen Guthaben.
|
||||
</Notice>
|
||||
|
||||
<ul className="cc-list">
|
||||
{open.map((record) => (
|
||||
<li key={record.id} className="cc-item">
|
||||
<div>
|
||||
<strong>{record.filename}</strong>
|
||||
<p className="muted small">
|
||||
{record.rowCount} {record.rowCount === 1 ? 'Zeile' : 'Zeilen'} · angelegt{' '}
|
||||
{formatDate(record.createdAt)}
|
||||
{record.importedBy && ` von ${record.importedBy.username}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="cc-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-small btn-primary"
|
||||
disabled={busy}
|
||||
onClick={() => void openExisting(record)}
|
||||
>
|
||||
Öffnen
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!upload && (
|
||||
<section className="block">
|
||||
<Notice kind="info">
|
||||
|
||||
Reference in New Issue
Block a user