feat: add functionality to manually split documents at specific pages via the UI and API
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s
This commit is contained in:
@@ -59,6 +59,16 @@ export class InboxDocument {
|
|||||||
})
|
})
|
||||||
Rotations!: Record<string, number>;
|
Rotations!: Record<string, number>;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'json',
|
||||||
|
nullable: true,
|
||||||
|
transformer: {
|
||||||
|
to: (v: number[] | null | undefined) => (v && v.length ? v : null),
|
||||||
|
from: (v: number[] | null) => v ?? [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ManualSplitPages!: number[];
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
CreatedAt!: Date;
|
CreatedAt!: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,17 @@ export class InboxController {
|
|||||||
await this.inboxService.deletePage(id, page, preferredUsername);
|
await this.inboxService.deletePage(id, page, preferredUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post(':id/pages/:page/split')
|
||||||
|
@HttpCode(204)
|
||||||
|
async toggleManualSplit(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('page', ParseIntPipe) page: number,
|
||||||
|
@Request() req: any,
|
||||||
|
): Promise<void> {
|
||||||
|
const preferredUsername: string | null = req.user?.preferredUsername ?? null;
|
||||||
|
await this.inboxService.toggleManualSplit(id, page, preferredUsername);
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':id/reset-edits')
|
@Post(':id/reset-edits')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
async resetEdits(@Param('id') id: string, @Request() req: any): Promise<void> {
|
async resetEdits(@Param('id') id: string, @Request() req: any): Promise<void> {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface InboxFile {
|
|||||||
source: InboxSource;
|
source: InboxSource;
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
deletedPages: number[];
|
deletedPages: number[];
|
||||||
|
manualSplitPages: number[];
|
||||||
rotations: Record<string, number>;
|
rotations: Record<string, number>;
|
||||||
barcodes: MatchedBarcode[];
|
barcodes: MatchedBarcode[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -60,6 +61,7 @@ export class InboxService {
|
|||||||
source: doc.Source,
|
source: doc.Source,
|
||||||
pageCount: doc.PageCount,
|
pageCount: doc.PageCount,
|
||||||
deletedPages: [...(doc.DeletedPages ?? [])].sort((a, b) => a - b),
|
deletedPages: [...(doc.DeletedPages ?? [])].sort((a, b) => a - b),
|
||||||
|
manualSplitPages: [...(doc.ManualSplitPages ?? [])].sort((a, b) => a - b),
|
||||||
rotations: { ...(doc.Rotations ?? {}) },
|
rotations: { ...(doc.Rotations ?? {}) },
|
||||||
barcodes: await this.barcodeScanner.getMatched(doc),
|
barcodes: await this.barcodeScanner.getMatched(doc),
|
||||||
createdAt: doc.CreatedAt.toISOString(),
|
createdAt: doc.CreatedAt.toISOString(),
|
||||||
@@ -142,7 +144,7 @@ export class InboxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setzt alle markierten Bearbeitungen (DeletedPages, Rotations) zurück.
|
* Setzt alle markierten Bearbeitungen (DeletedPages, Rotations, ManualSplitPages) zurück.
|
||||||
*/
|
*/
|
||||||
async resetEdits(id: string, preferredUsername: string | null): Promise<void> {
|
async resetEdits(id: string, preferredUsername: string | null): Promise<void> {
|
||||||
const { doc } = await this.resolveDocument(id, preferredUsername);
|
const { doc } = await this.resolveDocument(id, preferredUsername);
|
||||||
@@ -155,9 +157,31 @@ export class InboxService {
|
|||||||
doc.Rotations = {};
|
doc.Rotations = {};
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
if (doc.ManualSplitPages && doc.ManualSplitPages.length > 0) {
|
||||||
|
doc.ManualSplitPages = [];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
if (changed) await this.documentRepo.save(doc);
|
if (changed) await this.documentRepo.save(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt oder entfernt einen manuellen Trennpunkt vor der angegebenen Seite.
|
||||||
|
*/
|
||||||
|
async toggleManualSplit(id: string, page: number, preferredUsername: string | null): Promise<void> {
|
||||||
|
const { doc } = await this.resolveDocument(id, preferredUsername);
|
||||||
|
if (!Number.isInteger(page) || page < 2 || page > doc.PageCount) {
|
||||||
|
throw new BadRequestException('Ungültige Seitennummer für Trennung');
|
||||||
|
}
|
||||||
|
const splits = new Set<number>(doc.ManualSplitPages ?? []);
|
||||||
|
if (splits.has(page)) {
|
||||||
|
splits.delete(page);
|
||||||
|
} else {
|
||||||
|
splits.add(page);
|
||||||
|
}
|
||||||
|
doc.ManualSplitPages = Array.from(splits).sort((a, b) => a - b);
|
||||||
|
await this.documentRepo.save(doc);
|
||||||
|
}
|
||||||
|
|
||||||
async deleteDocument(id: string, preferredUsername: string | null): Promise<void> {
|
async deleteDocument(id: string, preferredUsername: string | null): Promise<void> {
|
||||||
const { doc } = await this.resolveDocument(id, preferredUsername);
|
const { doc } = await this.resolveDocument(id, preferredUsername);
|
||||||
const dir = this.pageCache.documentDir(doc.Id);
|
const dir = this.pageCache.documentDir(doc.Id);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface InboxFile {
|
|||||||
source: InboxSource;
|
source: InboxSource;
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
deletedPages: number[];
|
deletedPages: number[];
|
||||||
|
manualSplitPages: number[];
|
||||||
rotations: Record<string, number>;
|
rotations: Record<string, number>;
|
||||||
barcodes: InboxBarcode[];
|
barcodes: InboxBarcode[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -62,6 +63,9 @@ export const inboxApi = {
|
|||||||
remove: (id: string) =>
|
remove: (id: string) =>
|
||||||
api.delete(`/api/inbox/${encodeURIComponent(id)}`).then((r) => r.data),
|
api.delete(`/api/inbox/${encodeURIComponent(id)}`).then((r) => r.data),
|
||||||
|
|
||||||
|
toggleSplit: (id: string, page: number) =>
|
||||||
|
api.post(`/api/inbox/${encodeURIComponent(id)}/pages/${page}/split`).then(() => {}),
|
||||||
|
|
||||||
removePage: (id: string, page: number) =>
|
removePage: (id: string, page: number) =>
|
||||||
api
|
api
|
||||||
.delete(`/api/inbox/${encodeURIComponent(id)}/pages/${page}`)
|
.delete(`/api/inbox/${encodeURIComponent(id)}/pages/${page}`)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
QrcodeOutlined,
|
QrcodeOutlined,
|
||||||
RedoOutlined,
|
RedoOutlined,
|
||||||
RightOutlined,
|
RightOutlined,
|
||||||
|
ScissorOutlined,
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
UndoOutlined,
|
UndoOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -35,11 +36,12 @@ function buildDocuments(
|
|||||||
pageCount: number,
|
pageCount: number,
|
||||||
splitPages: number[],
|
splitPages: number[],
|
||||||
deletedPages: number[],
|
deletedPages: number[],
|
||||||
|
manualSplitPages: number[],
|
||||||
barcodes: InboxBarcode[],
|
barcodes: InboxBarcode[],
|
||||||
): DocumentSegment[] {
|
): DocumentSegment[] {
|
||||||
if (pageCount === 0) return [];
|
if (pageCount === 0) return [];
|
||||||
const deleted = new Set(deletedPages);
|
const deleted = new Set(deletedPages);
|
||||||
const splits = new Set(splitPages.filter((p) => !deleted.has(p)));
|
const splits = new Set([...splitPages, ...manualSplitPages].filter((p) => !deleted.has(p)));
|
||||||
|
|
||||||
const docs: DocumentSegment[] = [];
|
const docs: DocumentSegment[] = [];
|
||||||
let current: number[] = [];
|
let current: number[] = [];
|
||||||
@@ -570,7 +572,7 @@ export default function InboxDetailPage() {
|
|||||||
const splitPages = file.barcodes
|
const splitPages = file.barcodes
|
||||||
.filter((b) => b.splitBefore)
|
.filter((b) => b.splitBefore)
|
||||||
.map((b) => b.page);
|
.map((b) => b.page);
|
||||||
return buildDocuments(file.pageCount, splitPages, file.deletedPages, file.barcodes);
|
return buildDocuments(file.pageCount, splitPages, file.deletedPages, file.manualSplitPages, file.barcodes);
|
||||||
}, [file]);
|
}, [file]);
|
||||||
|
|
||||||
const effectivePages = useMemo<number[]>(() => {
|
const effectivePages = useMemo<number[]>(() => {
|
||||||
@@ -700,6 +702,18 @@ export default function InboxDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleToggleSplit = async () => {
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
await inboxApi.toggleSplit(file.id, selectedPage);
|
||||||
|
const list = await inboxApi.list();
|
||||||
|
const refreshed = list.find((f) => f.id === file.id) ?? null;
|
||||||
|
if (refreshed) setFile(refreshed);
|
||||||
|
} catch {
|
||||||
|
message.error('Trennung konnte nicht gespeichert werden');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleResetEdits = async () => {
|
const handleResetEdits = async () => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
try {
|
try {
|
||||||
@@ -846,16 +860,16 @@ export default function InboxDetailPage() {
|
|||||||
const canPrev = effectiveIndex > 0;
|
const canPrev = effectiveIndex > 0;
|
||||||
const canNext = effectiveIndex >= 0 && effectiveIndex < effectivePages.length - 1;
|
const canNext = effectiveIndex >= 0 && effectiveIndex < effectivePages.length - 1;
|
||||||
const canDelete = effectivePages.length > 1;
|
const canDelete = effectivePages.length > 1;
|
||||||
|
const isSplitPage = file.manualSplitPages.includes(selectedPage);
|
||||||
|
const canSplit = selectedPage !== currentDoc?.pages[0];
|
||||||
const rotationCount = Object.keys(file.rotations ?? {}).length;
|
const rotationCount = Object.keys(file.rotations ?? {}).length;
|
||||||
const pendingEdits = file.deletedPages.length + rotationCount;
|
const manualSplitCount = file.manualSplitPages.length;
|
||||||
|
const pendingEdits = file.deletedPages.length + rotationCount + manualSplitCount;
|
||||||
const editsLabel = (() => {
|
const editsLabel = (() => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (file.deletedPages.length > 0) {
|
if (file.deletedPages.length > 0) parts.push(`${file.deletedPages.length} zur Löschung markiert`);
|
||||||
parts.push(`${file.deletedPages.length} zur Löschung markiert`);
|
if (rotationCount > 0) parts.push(`${rotationCount} gedreht`);
|
||||||
}
|
if (manualSplitCount > 0) parts.push(`${manualSplitCount} manuell getrennt`);
|
||||||
if (rotationCount > 0) {
|
|
||||||
parts.push(`${rotationCount} gedreht`);
|
|
||||||
}
|
|
||||||
return parts.join(' · ');
|
return parts.join(' · ');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -1211,6 +1225,18 @@ export default function InboxDetailPage() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<span style={{ width: 1, height: 20, background: 'rgba(255,255,255,0.2)' }} />
|
<span style={{ width: 1, height: 20, background: 'rgba(255,255,255,0.2)' }} />
|
||||||
|
{canSplit && (
|
||||||
|
<Tooltip title={isSplitPage ? 'Manuelle Trennung aufheben' : 'Vor dieser Seite trennen'}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
shape="circle"
|
||||||
|
icon={<ScissorOutlined style={{ fontSize: 16 }} />}
|
||||||
|
onClick={handleToggleSplit}
|
||||||
|
style={{ color: isSplitPage ? '#ffd666' : '#fff' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<span style={{ width: 1, height: 20, background: 'rgba(255,255,255,0.2)' }} />
|
||||||
<Tooltip title={scanMode ? 'Bereich-Scan abbrechen' : 'QR-Code in Bereich scannen'}>
|
<Tooltip title={scanMode ? 'Bereich-Scan abbrechen' : 'QR-Code in Bereich scannen'}>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
Reference in New Issue
Block a user