feat(agrarmonitor): fehlende Lieferantennummern vergeben
Build and Push Multi-Platform Images / build-and-push (push) Successful in 1m1s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 1m1s
Neuer Bereich „Lieferantennummern überprüfen“ in den Agrarmonitor- Einstellungen. Er vergibt allen aktiven Lieferanten ohne Lieferantennummer eine fortlaufende Nummer im Schema 1555xxxx, ab der höchsten vergebenen Nummer (Inaktive zählen mit, Altlasten werden ignoriert). - Vorschau „Lieferant → neue Nummer“, geschrieben wird erst nach Bestätigung; Einzelmodus zum Testen an einem Lieferanten - formData wird wie in der Weboberfläche aus #form_detail aufgebaut, nur die Lieferantennummer ändert sich - Dublettencheck über vorhanden.php, Erfolgskontrolle durch Neuladen des Details, Abbruch beim ersten Fehler - Lauf im Hintergrund mit Status-Abfrage (wegen Proxy-Timeouts), Listenfilter werden danach zurückgesetzt - HTML-Parsing mit cheerio, Unit-Tests für Parser und Vergabe-Ablauf Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
029009182a
commit
3d4125b29a
11 files changed
+1507
-3
No files matched your search
@@ -258,6 +258,46 @@ export interface SyncCorrespondentsResult {
|
||||
conflicts: SyncConflict[];
|
||||
}
|
||||
|
||||
export interface LieferantennummerKandidat {
|
||||
kundeId: number;
|
||||
name: string;
|
||||
neueNummer: string;
|
||||
}
|
||||
|
||||
export interface LieferantennummerVorschau {
|
||||
maximum: number;
|
||||
naechsteNummer: number;
|
||||
kandidaten: LieferantennummerKandidat[];
|
||||
}
|
||||
|
||||
export type LieferantennummerEintragStatus = 'vergeben' | 'uebersprungen' | 'pruefen' | 'fehler';
|
||||
|
||||
export interface LieferantennummerEintrag {
|
||||
kundeId: number;
|
||||
name: string;
|
||||
status: LieferantennummerEintragStatus;
|
||||
nummer?: string;
|
||||
hinweis?: string;
|
||||
}
|
||||
|
||||
export interface LieferantennummerLaufErgebnis {
|
||||
modus: 'alle' | 'einzeln';
|
||||
gestartetAm: string;
|
||||
beendetAm: string | null;
|
||||
gesamt: number;
|
||||
vergeben: number;
|
||||
uebersprungen: number;
|
||||
zuPruefen: number;
|
||||
fehler: number;
|
||||
abgebrochen: boolean;
|
||||
eintraege: LieferantennummerEintrag[];
|
||||
}
|
||||
|
||||
export interface LieferantennummerStatus {
|
||||
aktiv: boolean;
|
||||
lauf: LieferantennummerLaufErgebnis | null;
|
||||
}
|
||||
|
||||
export const agrarmonitorApi = {
|
||||
getStatus: () =>
|
||||
api.get<AgrarmonitorStatusData>('/api/agrarmonitor/status').then((r) => r.data),
|
||||
@@ -277,4 +317,18 @@ export const agrarmonitorApi = {
|
||||
api.post<SyncCorrespondentsResult>('/api/agrarmonitor/sync-correspondents').then((r) => r.data),
|
||||
mergeCorrespondents: (keepId: number, deleteId: number) =>
|
||||
api.post<{ mergedDocuments: number }>('/api/agrarmonitor/merge-correspondents', { keepId, deleteId }).then((r) => r.data),
|
||||
getLieferantennummernStatus: () =>
|
||||
api.get<LieferantennummerStatus>('/api/agrarmonitor/lieferantennummern/status').then((r) => r.data),
|
||||
lieferantennummernVorschau: () =>
|
||||
api
|
||||
.post<LieferantennummerVorschau>('/api/agrarmonitor/lieferantennummern/vorschau', {}, { timeout: 120_000 })
|
||||
.then((r) => r.data),
|
||||
lieferantennummernVergeben: (startNummer: number, nurKundeId?: number) =>
|
||||
api
|
||||
.post<LieferantennummerStatus>(
|
||||
'/api/agrarmonitor/lieferantennummern/vergeben',
|
||||
{ startNummer, nurKundeId },
|
||||
{ timeout: 120_000 },
|
||||
)
|
||||
.then((r) => r.data),
|
||||
};
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
agrarmonitorApi, type AgrarmonitorStatusData,
|
||||
type SettingClient, type AgrarmonitorPollingConfig, type AgrarmonitorPollingResult,
|
||||
type SyncConflict,
|
||||
type LieferantennummerVorschau, type LieferantennummerStatus, type LieferantennummerEintrag,
|
||||
type LieferantennummerEintragStatus,
|
||||
type SettingClientIdentifier, type ClientIdentifierTyp, CLIENT_IDENTIFIER_LABELS,
|
||||
} from '../api/settings';
|
||||
import { clientsApi, type Client } from '../api/inbox';
|
||||
@@ -2778,6 +2780,8 @@ function AgrarmonitorTab() {
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<LieferantennummernCard />
|
||||
|
||||
<Card size="small" title="Dokumenten-Verarbeitung">
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
|
||||
Prüft Belege mit Tag "Hochgeladen in Agrarmonitor" und setzt den Tag auf "AMfertig",
|
||||
@@ -2812,6 +2816,214 @@ function AgrarmonitorTab() {
|
||||
}
|
||||
|
||||
|
||||
const LIEFERANTENNUMMER_STATUS: Record<LieferantennummerEintragStatus, { label: string; color: string }> = {
|
||||
vergeben: { label: 'vergeben', color: 'success' },
|
||||
uebersprungen: { label: 'übersprungen', color: 'default' },
|
||||
pruefen: { label: 'zu prüfen', color: 'warning' },
|
||||
fehler: { label: 'Fehler', color: 'error' },
|
||||
};
|
||||
|
||||
function fehlermeldung(err: unknown, fallback: string): string {
|
||||
const msg = (err as { response?: { data?: { message?: unknown } } })?.response?.data?.message;
|
||||
return typeof msg === 'string' && msg ? msg : fallback;
|
||||
}
|
||||
|
||||
function LieferantennummernCard() {
|
||||
const [status, setStatus] = useState<LieferantennummerStatus | null>(null);
|
||||
const [vorschau, setVorschau] = useState<LieferantennummerVorschau | null>(null);
|
||||
const [vorschauLaedt, setVorschauLaedt] = useState(false);
|
||||
const [startet, setStartet] = useState(false);
|
||||
const [einzelKundeId, setEinzelKundeId] = useState<number | undefined>(undefined);
|
||||
|
||||
const ladeStatus = useCallback(async () => {
|
||||
try {
|
||||
setStatus(await agrarmonitorApi.getLieferantennummernStatus());
|
||||
} catch {
|
||||
// Status ist nur Anzeige – beim nächsten Intervall erneut versuchen
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
ladeStatus();
|
||||
}, [ladeStatus]);
|
||||
|
||||
// Solange ein Lauf aktiv ist, den Fortschritt abfragen
|
||||
useEffect(() => {
|
||||
if (!status?.aktiv) return;
|
||||
const timer = setInterval(ladeStatus, 2000);
|
||||
return () => clearInterval(timer);
|
||||
}, [status?.aktiv, ladeStatus]);
|
||||
|
||||
const laufAktiv = !!status?.aktiv || startet;
|
||||
|
||||
const handleVorschau = async () => {
|
||||
setVorschauLaedt(true);
|
||||
try {
|
||||
const data = await agrarmonitorApi.lieferantennummernVorschau();
|
||||
setEinzelKundeId(undefined);
|
||||
setVorschau(data);
|
||||
} catch (err) {
|
||||
message.error(fehlermeldung(err, 'Lieferantenliste konnte nicht geladen werden'));
|
||||
} finally {
|
||||
setVorschauLaedt(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVergeben = async (nurKundeId?: number) => {
|
||||
if (!vorschau) return;
|
||||
setStartet(true);
|
||||
try {
|
||||
setStatus(await agrarmonitorApi.lieferantennummernVergeben(vorschau.naechsteNummer, nurKundeId));
|
||||
setVorschau(null);
|
||||
} catch (err) {
|
||||
message.error(fehlermeldung(err, 'Vergabe konnte nicht gestartet werden'));
|
||||
} finally {
|
||||
setStartet(false);
|
||||
}
|
||||
};
|
||||
|
||||
const lauf = status?.lauf;
|
||||
const einzelKandidat = vorschau?.kandidaten.find(k => k.kundeId === einzelKundeId);
|
||||
|
||||
return (
|
||||
<Card size="small" title="Lieferantennummern überprüfen">
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
|
||||
Vergibt allen aktiven Lieferanten in Agrarmonitor ohne Lieferantennummer eine fortlaufende
|
||||
Nummer (Schema 1555xxxx, ab der höchsten vergebenen Nummer). Vor dem Schreiben wird eine
|
||||
Vorschau angezeigt.
|
||||
</Typography.Text>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Button loading={vorschauLaedt} disabled={laufAktiv} onClick={handleVorschau}>
|
||||
Fehlende Lieferantennummern vergeben
|
||||
</Button>
|
||||
|
||||
{lauf && (
|
||||
<div>
|
||||
{status?.aktiv && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 8 }}
|
||||
message={`Vergabe läuft … ${lauf.eintraege.length} von ${lauf.gesamt} bearbeitet`}
|
||||
/>
|
||||
)}
|
||||
{!status?.aktiv && lauf.abgebrochen && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 8 }}
|
||||
message="Lauf wegen eines Fehlers abgebrochen"
|
||||
/>
|
||||
)}
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Tag color="success">{lauf.vergeben} vergeben</Tag>
|
||||
<Tag>{lauf.uebersprungen} übersprungen</Tag>
|
||||
<Tag color="warning">{lauf.zuPruefen} zu prüfen</Tag>
|
||||
{lauf.fehler > 0 && <Tag color="error">{lauf.fehler} Fehler</Tag>}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{lauf.modus === 'einzeln' ? 'Einzeltest' : 'Alle'} · {dayjs(lauf.gestartetAm).format('DD.MM.YYYY HH:mm')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{lauf.eintraege.length > 0 && (
|
||||
<Table<LieferantennummerEintrag>
|
||||
size="small"
|
||||
rowKey="kundeId"
|
||||
dataSource={lauf.eintraege}
|
||||
pagination={lauf.eintraege.length > 10 ? { pageSize: 10, size: 'small' } : false}
|
||||
columns={[
|
||||
{ title: 'Lieferant', dataIndex: 'name' },
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: (st: LieferantennummerEintragStatus) => (
|
||||
<Tag color={LIEFERANTENNUMMER_STATUS[st].color}>{LIEFERANTENNUMMER_STATUS[st].label}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: 'Nummer', dataIndex: 'nummer', width: 100 },
|
||||
{ title: 'Hinweis', dataIndex: 'hinweis' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
<Modal
|
||||
open={!!vorschau}
|
||||
title="Lieferantennummern vergeben"
|
||||
width={720}
|
||||
onCancel={() => setVorschau(null)}
|
||||
footer={[
|
||||
<Button key="abbrechen" onClick={() => setVorschau(null)}>
|
||||
Abbrechen
|
||||
</Button>,
|
||||
<Button
|
||||
key="einzeln"
|
||||
disabled={!einzelKandidat}
|
||||
loading={startet}
|
||||
onClick={() => handleVergeben(einzelKundeId)}
|
||||
>
|
||||
Nur diesen vergeben
|
||||
</Button>,
|
||||
<Button
|
||||
key="alle"
|
||||
type="primary"
|
||||
disabled={!vorschau?.kandidaten.length}
|
||||
loading={startet}
|
||||
onClick={() => handleVergeben()}
|
||||
>
|
||||
Jetzt vergeben
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{vorschau && (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Typography.Text>
|
||||
Höchste vergebene Nummer: <strong>{vorschau.maximum}</strong> · nächste freie
|
||||
Nummer: <strong>{vorschau.naechsteNummer}</strong> · <strong>{vorschau.kandidaten.length}</strong>{' '}
|
||||
aktive Lieferanten ohne Nummer
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Werden Lieferanten übersprungen oder als „zu prüfen“ gemeldet, rücken die folgenden Nummern
|
||||
nach, damit keine Lücken entstehen.
|
||||
</Typography.Text>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="kundeId"
|
||||
dataSource={vorschau.kandidaten}
|
||||
pagination={vorschau.kandidaten.length > 10 ? { pageSize: 10, size: 'small' } : false}
|
||||
columns={[
|
||||
{ title: 'Lieferant', dataIndex: 'name' },
|
||||
{ title: 'Kunden-ID', dataIndex: 'kundeId', width: 100 },
|
||||
{ title: 'Neue Nummer', dataIndex: 'neueNummer', width: 120 },
|
||||
]}
|
||||
/>
|
||||
<Divider style={{ margin: '4px 0' }} />
|
||||
<Typography.Text strong>Test: nur einen Lieferanten</Typography.Text>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="Lieferant auswählen"
|
||||
style={{ width: '100%' }}
|
||||
value={einzelKundeId}
|
||||
onChange={setEinzelKundeId}
|
||||
optionFilterProp="label"
|
||||
options={vorschau.kandidaten.map(k => ({ value: k.kundeId, label: `${k.name} (${k.kundeId})` }))}
|
||||
/>
|
||||
{einzelKandidat && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{einzelKandidat.name} erhält die Nummer {vorschau.naechsteNummer}.
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Steuertags Tab
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in new issue
Block a user