Initial commit with Email Import Wizard and Task Processor updates

This commit is contained in:
2026-05-04 08:02:11 +02:00
commit effdc5d59f
170 changed files with 67739 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
import api from './client';
export interface ApiKey {
id: string;
name: string;
keyPrefix: string;
createdAt: string;
lastUsedAt: string | null;
expiresAt: string | null;
}
export interface CreatedApiKey {
plainKey: string;
entity: ApiKey;
}
export const apiKeysApi = {
getApiKeys: () => api.get<ApiKey[]>('/api/api-keys').then(r => r.data),
createApiKey: (name: string, expiresDays?: number) =>
api.post<CreatedApiKey>('/api/api-keys', { name, expiresDays }).then(r => r.data),
deleteApiKey: (id: string) => api.delete(`/api/api-keys/${id}`).then(r => r.data),
};
@@ -0,0 +1,45 @@
import api from './client';
export type BarcodeActionType = 'SEND_TO_PAPERLESS' | 'SEND_BY_EMAIL';
export interface BarcodeTemplate {
Id: number;
Name: string;
Regex: string;
SplitBefore: boolean;
DateinameTemplate: string | null;
Actions: BarcodeActionType[];
CreatedAt: string;
UpdatedAt: string;
}
export interface BarcodeTemplateInput {
Name: string;
Regex: string;
SplitBefore: boolean;
DateinameTemplate?: string | null;
Actions: BarcodeActionType[];
}
export const BARCODE_ACTION_LABELS: Record<BarcodeActionType, string> = {
SEND_TO_PAPERLESS: 'Datei an Paperless senden',
SEND_BY_EMAIL: 'Datei per E-Mail senden',
};
export const barcodeTemplatesApi = {
list: () =>
api.get<BarcodeTemplate[]>('/api/barcode-templates').then((r) => r.data),
create: (input: BarcodeTemplateInput) =>
api
.post<BarcodeTemplate>('/api/barcode-templates', input)
.then((r) => r.data),
update: (id: number, input: Partial<BarcodeTemplateInput>) =>
api
.put<BarcodeTemplate>(`/api/barcode-templates/${id}`, input)
.then((r) => r.data),
remove: (id: number) =>
api.delete(`/api/barcode-templates/${id}`).then((r) => r.data),
};
+29
View File
@@ -0,0 +1,29 @@
import axios from 'axios';
import { getAccessToken } from '../auth/oidc';
import { triggerLoginRedirect } from '../auth/sessionRedirect';
import { getEnv } from '../utils/env';
const api = axios.create({
baseURL: getEnv('VITE_API_URL') || '',
timeout: 30000,
});
api.interceptors.request.use(async (config) => {
const token = await getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
await triggerLoginRedirect();
}
return Promise.reject(error);
},
);
export default api;
@@ -0,0 +1,68 @@
import api from './client';
export interface CorrespondentMapping {
Id: number;
EmailAddress: string;
PaperlessCorrespondentId: number;
}
export interface AttachmentImportData {
attachmentId: number;
virtualId: string;
fileName: string;
pages?: { start: number; end: number };
type: 'MAIN' | 'ATTACHMENT' | 'IGNORE';
paperlessCorrespondentId?: number | null;
parentDocumentId?: number | null;
parentVirtualId?: string | null;
splitRanges?: { start: number; end: number }[];
barcode?: { x: number; y: number; nummer: string; datum: string; jahr: string };
belegnummer?: string;
isDuplicate?: boolean;
}
export const emailImportApi = {
getMappings: async (): Promise<CorrespondentMapping[]> => {
const res = await api.get('/api/email-import/mappings');
return res.data;
},
addMapping: async (emailAddress: string, paperlessCorrespondentId: number): Promise<CorrespondentMapping> => {
const res = await api.post('/api/email-import/mappings', { emailAddress, paperlessCorrespondentId });
return res.data;
},
deleteMapping: async (id: number): Promise<void> => {
await api.delete(`/api/email-import/mappings/${id}`);
},
getCorrespondentByEmail: async (emailAddress: string): Promise<{ paperlessCorrespondentId: number | null }> => {
const res = await api.get('/api/email-import/correspondent', { params: { email: emailAddress } });
return res.data;
},
getBelegnummer: async (dateStr: string): Promise<{ nummer: string }> => {
const res = await api.get('/api/email-import/belegnummer', { params: { date: dateStr } });
return res.data;
},
releaseBelegnummer: async (dateStr: string, number: string): Promise<void> => {
await api.post('/api/email-import/belegnummer/release', { date: dateStr, number });
},
printPreview: async (attachmentId: number, barcodeData: any): Promise<Blob> => {
const res = await api.post(`/api/email-import/attachments/${attachmentId}/print-preview`, barcodeData, {
responseType: 'blob',
});
return res.data;
},
executeImport: async (emailDate: string, attachments: AttachmentImportData[]): Promise<{ success: boolean; results: any[] }> => {
const res = await api.post('/api/email-import/execute', { emailDate, attachments });
return res.data;
},
ensurePreviews: async (emailId: number): Promise<void> => {
await api.post(`/api/email-import/emails/${emailId}/ensure-previews`);
},
};
+51
View File
@@ -0,0 +1,51 @@
import api from './client';
export interface EmailAttachment {
Id: number;
FileName: string;
ContentType: string;
Erechnung: boolean;
Checksum?: string;
ContentId?: string | null;
IsEmbedded?: boolean;
ParentId?: number | null;
PageCount?: number;
PaperlessDocumentIds?: Record<string, number> | null;
}
export interface EmailItem {
Id: number;
MessageId: string;
SenderAddress: string;
RecipientAddress: string;
Subject: string;
Date: string;
Body: string;
Status: number;
Attachments?: EmailAttachment[];
}
export const emailsApi = {
list: (params?: { status?: number; limit?: number }) =>
api.get<EmailItem[]>('/api/emails', { params }).then((r) => r.data),
get: (id: number) =>
api.get<EmailItem>(`/api/emails/${id}`).then((r) => r.data),
listAttachments: (emailId: number) =>
api.get<EmailAttachment[]>(`/api/emails/${emailId}/attachments`).then((r) => r.data),
getAttachmentContent: (attachmentId: number) =>
api.get<Blob>(`/api/emails/attachments/${attachmentId}/content`, {
responseType: 'blob',
}).then((r) => r.data),
triggerFetch: () =>
api.post<{ message: string }>('/api/emails/fetch').then((r) => r.data),
checkAttachments: () =>
api.post<{ updatedCount: number }>('/api/emails/check-attachments').then((r) => r.data),
updateStatus: (id: number, status: number) =>
api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data),
};
+106
View File
@@ -0,0 +1,106 @@
import api from './client';
import type { BarcodeActionType } from './barcode-templates';
export type InboxSource = 'all' | 'user';
export interface InboxBarcode {
page: number;
value: string;
templateId: number | null;
templateName: string | null;
splitBefore: boolean;
actions: BarcodeActionType[];
}
export interface InboxFile {
id: string;
name: string;
source: InboxSource;
pageCount: number;
deletedPages: number[];
rotations: Record<string, number>;
barcodes: InboxBarcode[];
createdAt: string;
}
export interface Client {
Id: number;
Name: string;
PaperlessUserId: number;
}
export const inboxApi = {
list: () => api.get<InboxFile[]>('/api/inbox').then((r) => r.data),
rescan: () =>
api
.post<{ scanned: number; failed: number }>('/api/inbox/rescan', {}, { timeout: 600000 })
.then((r) => r.data),
previewBlob: (id: string) =>
api
.get<Blob>(`/api/inbox/${encodeURIComponent(id)}/preview`, { responseType: 'blob' })
.then((r) => r.data),
thumbnailBlob: (id: string, page: number) =>
api
.get<Blob>(
`/api/inbox/${encodeURIComponent(id)}/pages/${page}/thumbnail`,
{ responseType: 'blob' },
)
.then((r) => r.data),
pagePreviewBlob: (id: string, page: number) =>
api
.get<Blob>(
`/api/inbox/${encodeURIComponent(id)}/pages/${page}/preview`,
{ responseType: 'blob' },
)
.then((r) => r.data),
remove: (id: string) =>
api.delete(`/api/inbox/${encodeURIComponent(id)}`).then((r) => r.data),
removePage: (id: string, page: number) =>
api
.delete(`/api/inbox/${encodeURIComponent(id)}/pages/${page}`)
.then((r) => r.data),
resetEdits: (id: string) =>
api
.post(`/api/inbox/${encodeURIComponent(id)}/reset-edits`)
.then((r) => r.data),
setPageRotation: (id: string, page: number, rotation: number) =>
api
.put(
`/api/inbox/${encodeURIComponent(id)}/pages/${page}/rotation`,
{ rotation },
)
.then((r) => r.data),
postprocess: (
id: string,
opts?: { sectionOffset?: number; processOnlyOne?: boolean; replaceDuplicate?: boolean },
) =>
api
.post<{ results: PostprocessActionResult[]; totalSections: number }>(
`/api/inbox/${encodeURIComponent(id)}/postprocess`,
opts ?? {},
)
.then((r) => r.data),
};
export interface PostprocessActionResult {
sectionIndex: number;
actionId: number;
actionType: string;
ok: boolean;
skipped?: boolean;
message?: string;
duplicateOfDocumentId?: number;
}
export const clientsApi = {
getMyClients: () => api.get<Client[]>('/api/clients').then((r) => r.data),
};
+64
View File
@@ -0,0 +1,64 @@
import api from './client';
export interface PaperlessTag {
id: number;
slug: string;
name: string;
color: string;
text_color: string;
match: string;
matching_algorithm: number;
is_insensitive: boolean;
is_inbox_tag: boolean;
document_count: number;
}
export interface PaperlessDocType {
id: number;
slug: string;
name: string;
match: string;
matching_algorithm: number;
is_insensitive: boolean;
document_count: number;
}
export interface PaperlessCustomField {
id: number;
name: string;
data_type: string;
extra_data?: any;
}
export interface PaperlessCorrespondent {
id: number;
slug: string;
name: string;
match: string;
matching_algorithm: number;
is_insensitive: boolean;
document_count: number;
}
export interface PaperlessUser {
id: number;
username: string;
first_name?: string;
last_name?: string;
}
export const paperlessApi = {
getTags: () => api.get<PaperlessTag[]>('/api/paperless/tags').then(r => r.data),
getDocumentTypes: () => api.get<PaperlessDocType[]>('/api/paperless/document-types').then(r => r.data),
getCustomFields: () => api.get<PaperlessCustomField[]>('/api/paperless/custom-fields').then(r => r.data),
getCorrespondents: (search?: string) => api.get<PaperlessCorrespondent[]>('/api/paperless/correspondents', { params: { search } }).then(r => r.data),
getCorrespondent: (id: number) => api.get<PaperlessCorrespondent>(`/api/paperless/correspondents/${id}`).then(r => r.data),
getUsers: () => api.get<PaperlessUser[]>('/api/paperless/users').then(r => r.data),
searchDocuments: (params: { search?: string, page?: number, pageSize?: number }) =>
api.get<{ results: any[], count: number }>('/api/paperless/documents', { params }).then(r => r.data),
getDocument: (id: number) => api.get<any>(`/api/paperless/documents/${id}`).then(r => r.data),
checksumExists: (checksum: string) =>
api.post<{ exists: boolean }>('/api/paperless/checksum', { checksum }).then(r => r.data.exists),
getDocumentPdfBlob: (id: number) =>
api.get<Blob>(`/api/paperless/inbox/pdf/${id}`, { responseType: 'blob' }).then(r => r.data),
};
+57
View File
@@ -0,0 +1,57 @@
import api from './client';
export interface PosteingangDocument {
id: number;
title: string;
asn?: number;
documentType?: number;
correspondent?: number;
created?: string;
added: string;
tags: number[];
customFields: { field: number; value: any }[];
owner?: number;
}
export interface DocumentRequirement {
id: number;
feldId: string;
feldName: string;
feldTyp: string;
hinweis: string;
required: boolean;
isCustomField?: boolean;
customFieldIndex?: number;
fieldOptions?: { id: string | number; label: string }[];
}
export interface Kontonummer {
KontonummerId?: number;
CorrespondentId: number;
Nummer: string;
}
export const posteingangApi = {
getList: async (): Promise<PosteingangDocument[]> => {
const res = await api.get('/api/paperless/inbox/list');
return res.data;
},
getManuellList: async (): Promise<PosteingangDocument[]> => {
const res = await api.get('/api/paperless/manuell/list');
return res.data;
},
getRequirements: (docTypeId: number, isPosteingang: boolean = true) =>
api.get<DocumentRequirement[]>(`/api/paperless/requirements/${docTypeId}?Posteingang=${isPosteingang ? '1' : '0'}`)
.then((r) => r.data),
updateDocument: (id: number, data: any) =>
api.put(`/api/paperless/inbox/${id}`, data).then((r) => r.data),
getKontonummern: (correspondentId: number) =>
api.get<Kontonummer[]>(`/api/kontonummern/FromCorrespondent/${correspondentId}`).then(r => r.data),
createKontonummer: (data: { correspondentId: number; nummer: string }) =>
api.post<Kontonummer>('/api/kontonummern', data).then(r => r.data),
};
+178
View File
@@ -0,0 +1,178 @@
import api from './client';
export interface SettingDocType {
Id: number;
DocumentTypeId: number;
TitelTemplate: string;
TagNotReady: number | null;
TagReady: number | null;
}
export interface SettingDocField {
Id: number;
DocumentType: number;
Type: number;
TypeIndex: number | null;
IsRequired: boolean;
IsRequiredPosteingang: boolean;
Hinweis: string | null;
VisiblePosteingang: boolean;
}
// Filter types
export interface FilterCondition {
field: string;
operator: string;
value: any;
}
export interface FilterGroup {
combinator: 'AND' | 'OR';
rules: (FilterCondition | FilterGroup)[];
}
export interface SettingPostprocessing {
Id: number;
Name: string;
FilterJson: FilterGroup;
Order: number;
IsActive: boolean;
NoFurther: boolean;
}
export interface SettingPostprocessingAction {
Id: number;
PostprocessingId: number;
ActionType: number; // 1=Export, 2=Mail, 3=Tags, 4=CustomField, 5=Webhook
Content: Record<string, any>;
Order: number;
IsActive: boolean;
}
export interface SettingExportTarget {
Id: number;
Name: string;
Protocol: string; // 'ftp' | 'webdav'
Host: string;
Port: number | null;
Username: string | null;
Password: string | null;
RemotePath: string | null;
IsActive: boolean;
}
export interface SettingPostprocessingLog {
Id: number;
PostprocessingId: number;
ActionId: number | null;
DocumentId: number;
Status: string;
Message: string | null;
CreatedAt: string;
}
export interface SettingUserClient {
Id: number;
UserId: string;
ClientId: number;
Role: 'viewer' | 'editor' | 'admin';
}
export const settingsApi = {
// Dokumenttypen
getDocTypes: () => api.get<SettingDocType[]>('/api/settings/document-types').then(r => r.data),
updateDocType: (id: number, data: Partial<SettingDocType>) =>
api.put<SettingDocType>(`/api/settings/document-types/${id}`, data).then(r => r.data),
// Document Fields
getDocFields: (docTypeId: number) =>
api.get<SettingDocField[]>(`/api/settings/document-types/${docTypeId}/fields`).then(r => r.data),
createDocField: (docTypeId: number, data: Partial<SettingDocField>) =>
api.post<SettingDocField>(`/api/settings/document-types/${docTypeId}/fields`, data).then(r => r.data),
updateDocField: (id: number, data: Partial<SettingDocField>) =>
api.put<SettingDocField>(`/api/settings/document-fields/${id}`, data).then(r => r.data),
deleteDocField: (id: number) =>
api.delete(`/api/settings/document-fields/${id}`).then(r => r.data),
// Postprocessing
getPostprocessing: () => api.get<SettingPostprocessing[]>('/api/settings/postprocessing').then(r => r.data),
createPostprocessing: (data: Partial<SettingPostprocessing>) =>
api.post<SettingPostprocessing>('/api/settings/postprocessing', data).then(r => r.data),
updatePostprocessing: (id: number, data: Partial<SettingPostprocessing>) =>
api.put<SettingPostprocessing>(`/api/settings/postprocessing/${id}`, data).then(r => r.data),
deletePostprocessing: (id: number) =>
api.delete(`/api/settings/postprocessing/${id}`).then(r => r.data),
duplicatePostprocessing: (id: number) =>
api.post<SettingPostprocessing>(`/api/settings/postprocessing/${id}/duplicate`).then(r => r.data),
// Postprocessing Actions
getActions: (ppId: number) =>
api.get<SettingPostprocessingAction[]>(`/api/settings/postprocessing/${ppId}/actions`).then(r => r.data),
createAction: (ppId: number, data: Partial<SettingPostprocessingAction>) =>
api.post<SettingPostprocessingAction>(`/api/settings/postprocessing/${ppId}/actions`, data).then(r => r.data),
updateAction: (actionId: number, data: Partial<SettingPostprocessingAction>) =>
api.put<SettingPostprocessingAction>(`/api/settings/postprocessing-actions/${actionId}`, data).then(r => r.data),
deleteAction: (actionId: number) =>
api.delete(`/api/settings/postprocessing-actions/${actionId}`).then(r => r.data),
// Export-Ziele
getExportTargets: () => api.get<SettingExportTarget[]>('/api/settings/export-targets').then(r => r.data),
createExportTarget: (data: Partial<SettingExportTarget>) =>
api.post<SettingExportTarget>('/api/settings/export-targets', data).then(r => r.data),
updateExportTarget: (id: number, data: Partial<SettingExportTarget>) =>
api.put<SettingExportTarget>(`/api/settings/export-targets/${id}`, data).then(r => r.data),
deleteExportTarget: (id: number) =>
api.delete(`/api/settings/export-targets/${id}`).then(r => r.data),
testExportTarget: (id: number) =>
api.post<{ success: boolean; message: string }>(`/api/settings/export-targets/${id}/test`).then(r => r.data),
// Postprocessing Logs
getPostprocessingLogs: (limit = 50, offset = 0) =>
api.get<{ data: SettingPostprocessingLog[]; total: number }>('/api/settings/postprocessing-logs', { params: { limit, offset } }).then(r => r.data),
// Benutzer-Betrieb
getUserClients: () => api.get<SettingUserClient[]>('/api/settings/user-clients').then(r => r.data),
createUserClient: (data: Partial<SettingUserClient>) =>
api.post<SettingUserClient>('/api/settings/user-clients', data).then(r => r.data),
deleteUserClient: (id: number) =>
api.delete(`/api/settings/user-clients/${id}`).then(r => r.data),
// Korrespondenten
getCorrespondents: (page = 1, pageSize = 50, search?: string) =>
api.get<{ data: any[], total: number }>('/api/settings/correspondents', { params: { page, pageSize, search } }).then(r => r.data),
createCorrespondent: (data: { name: string }) => api.post<any>('/api/settings/correspondents', data).then(r => r.data),
updateCorrespondentSetting: (id: number, agrarmonitorId: number | null) =>
api.put<any>(`/api/settings/correspondents/${id}`, { agrarmonitorId }).then(r => r.data),
// Inbox-Postprozessor (global, deprecated)
listInboxActions: () =>
api.get<InboxAction[]>('/api/settings/inbox-actions').then((r) => r.data),
createInboxAction: (data: Partial<InboxAction>) =>
api.post<InboxAction>('/api/settings/inbox-actions', data).then((r) => r.data),
updateInboxAction: (id: number, data: Partial<InboxAction>) =>
api.put<InboxAction>(`/api/settings/inbox-actions/${id}`, data).then((r) => r.data),
deleteInboxAction: (id: number) =>
api.delete(`/api/settings/inbox-actions/${id}`).then((r) => r.data),
// Inbox-Aktionen pro Barcode-Vorlage
listInboxActionsForTemplate: (templateId: number) =>
api.get<InboxAction[]>(`/api/settings/barcode-templates/${templateId}/inbox-actions`).then((r) => r.data),
createInboxActionForTemplate: (templateId: number, data: Partial<InboxAction>) =>
api.post<InboxAction>(`/api/settings/barcode-templates/${templateId}/inbox-actions`, data).then((r) => r.data),
};
export type InboxActionType = 'MAIL' | 'EXPORT' | 'PAPERLESS';
export interface InboxAction {
Id: number;
ActionType: InboxActionType;
Content: Record<string, any>;
Order: number;
IsActive: boolean;
}
export const INBOX_ACTION_LABELS: Record<InboxActionType, string> = {
MAIL: 'Per E-Mail senden',
EXPORT: 'Export (FTP/WebDAV)',
PAPERLESS: 'In Paperless importieren',
};
+13
View File
@@ -0,0 +1,13 @@
import api from './client';
export interface StatsCounts {
inbox: number;
posteingang: number;
manuell: number;
mailpostfach: number;
}
export const statsApi = {
getCounts: () =>
api.get<StatsCounts>('/api/stats/counts').then((r) => r.data),
};
+29
View File
@@ -0,0 +1,29 @@
import api from './client';
export interface Task {
TaskId: string;
InterneBelegnummer: string;
DocumentType: number | null;
Eingangsdatum: string | null;
Fertig: number | null;
Lieferant: string | null;
externeBelegnummer: string | null;
Belegdatum: string | null;
PaperlessDocumentID: number | null;
TaskReferenceID: string | null;
}
export const tasksApi = {
getAll: async (): Promise<Task[]> => {
const res = await api.get('/api/paperless/tasks');
return res.data;
},
deleteFertige: async (): Promise<{ deleted: number }> => {
const res = await api.delete('/api/paperless/tasks/fertig');
return res.data;
},
deleteOne: async (taskId: string): Promise<{ deleted: number }> => {
const res = await api.delete(`/api/paperless/tasks/${encodeURIComponent(taskId)}`);
return res.data;
},
};