diff --git a/.agents/skills/backend/SKILL.md b/.agents/skills/backend/SKILL.md
index dbe9104..fc68e35 100644
--- a/.agents/skills/backend/SKILL.md
+++ b/.agents/skills/backend/SKILL.md
@@ -7,7 +7,7 @@ description: >-
# Dash API Routes Overview (All)
-Overview of all 242 API endpoints across all modules.
+Overview of all 246 API endpoints across all modules.
### Notation
@@ -426,6 +426,9 @@ _none_
#### Update delivery — `PATCH /delivery/{id}`
- Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T34[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `200 T6`
+#### Delete delivery file attachment — `DELETE /delivery/{id}/file/{fileId}`
+- Params: `id: int, fileId: int`
+- Returns: `204 void`
#### Get delivery attachment file content — `GET /delivery/{id}/file/{fileId}`
- Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str`
@@ -639,6 +642,9 @@ _none_
#### Update order — `PATCH /order/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T34[] }`
- Returns: `200 T1`
+#### Delete order file attachment — `DELETE /order/{id}/file/{fileId}`
+- Params: `id: int, fileId: int`
+- Returns: `204 void`
#### Get order attachment file content — `GET /order/{id}/file/{fileId}`
- Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str`
@@ -777,6 +783,9 @@ _none_
#### Update quote — `PATCH /quote/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T34[] }`
- Returns: `200 T8`
+#### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}`
+- Params: `id: int, fileId: int`
+- Returns: `204 void`
#### Get quote attachment file content — `GET /quote/{id}/file/{fileId}`
- Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str`
@@ -807,6 +816,9 @@ _none_
#### Get shipment by ID — `GET /shipment/{id}`
- Params: `id: int`
- Returns: `200 { shipment: T5 }`
+#### Delete shipment file attachment — `DELETE /shipment/{id}/file/{fileId}`
+- Params: `id: int, fileId: int`
+- Returns: `204 void`
#### Get shipment attachment file content — `GET /shipment/{id}/file/{fileId}`
- Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str`
diff --git a/src/api/quote.js b/src/api/quote.js
index 50ff05e..a3a6898 100644
--- a/src/api/quote.js
+++ b/src/api/quote.js
@@ -61,4 +61,30 @@ export const quote = {
return http(`/api/quote/${id}`, { method: 'DELETE' });
},
// #endregion
+
+ /**
+ * POST /api/quote/:id/pdf
+ * Generates and attaches quote PDF.
+ * @param {number|string} id Quote ID
+ * → 201 { attachment: object, fileUrl: string }
+ */
+ // #region POST /api/quote/:id/pdf — createPdf
+ createPdf(id) {
+ return http(`/api/quote/${id}/pdf`, { method: 'POST' });
+ },
+ // #endregion
+
+ /**
+ * DELETE /api/quote/:id/file/:fileId
+ * Deletes quote attachment file.
+ * @param {number|string} id Quote ID
+ * @param {number|string} fileId File ID
+ * → 204 void
+ */
+ // #region DELETE /api/quote/:id/file/:fileId — deleteFile
+ deleteFile(id, fileId) {
+ return http(`/api/quote/${id}/file/${fileId}`, { method: 'DELETE' });
+ },
+ // #endregion
};
+
diff --git a/src/components/AppShell.jsx b/src/components/AppShell.jsx
index 8dbb259..b3a5610 100644
--- a/src/components/AppShell.jsx
+++ b/src/components/AppShell.jsx
@@ -21,7 +21,7 @@ import {
} from '../colors';
import { ChatFab, ChatDrawer } from './chat';
-const BreakpointOverlay = 0 && import.meta.env.DEV
+const BreakpointOverlay = 1 && import.meta.env.DEV
? lazy(() => import('./BreakpointOverlay'))
: null;
diff --git a/src/components/manifest/Attachment.jsx b/src/components/manifest/Attachment.jsx
index 2706eb2..38ea0f8 100644
--- a/src/components/manifest/Attachment.jsx
+++ b/src/components/manifest/Attachment.jsx
@@ -89,11 +89,13 @@ export class Attachment extends PureComponent {
const url = URL.createObjectURL(attachment.file);
window.open(url, '_blank');
} else {
- const docId = attachment.manifestId;
+ const docId = this.props.docId ?? attachment.manifestId;
const fileId = attachment.fileId ?? attachment.id;
+ const kind = this.props.kind || 'delivery';
+ const prefix = kind === 'delivery' ? 'delivery' : kind === 'quote' ? 'quote' : 'order';
if (docId != null && fileId != null) {
window.open(
- `/api/delivery/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`,
+ `/api/${prefix}/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`,
'_blank',
);
}
@@ -109,7 +111,7 @@ export class Attachment extends PureComponent {
}
render() {
- const { attachment, disabled, sx, hasArticles } = this.props;
+ const { attachment, disabled, sx, hasArticles, kind, onRescan } = this.props;
const thumbnail = attachment?.thumbnail || this.state.generatedThumbnail;
const name = attachment?.name || 'Dokument';
const size = attachment?.size;
@@ -229,7 +231,7 @@ export class Attachment extends PureComponent {
}}
onClick={(e) => e.stopPropagation()}
>
- {isPdf || isText ? (
+ {kind === 'delivery' && Boolean(onRescan) && (isPdf || isText) ? (
) : null}
-
-
-
-
-
-
-
+
+
+
{/* Bottom file metadata overlay */}
diff --git a/src/components/manifest/AttachmentList.jsx b/src/components/manifest/AttachmentList.jsx
index b495f21..5cd5378 100644
--- a/src/components/manifest/AttachmentList.jsx
+++ b/src/components/manifest/AttachmentList.jsx
@@ -6,7 +6,7 @@ import Attachment from './Attachment';
export class AttachmentList extends PureComponent {
render() {
- const { attachments, onRescan, onDelete, onClick, disabled, hasArticles } = this.props;
+ const { attachments, onRescan, onDelete, onClick, disabled, hasArticles, kind, docId } = this.props;
if (!Array.isArray(attachments) || attachments.length === 0) {
return null;
}
@@ -26,6 +26,8 @@ export class AttachmentList extends PureComponent {
(a.id ?? a.fileId) !== targetId && a !== attachment,
+ );
+ this.setState({ attachments: nextAttachments });
+
+ if (this.editAutomat) {
+ this.editAutomat.actions.updateDraft({ attachments: nextAttachments });
+ }
+
+ if (docId != null && targetId != null && attachment.isPersisted) {
+ try {
+ const api = apiFor(this.cfg);
+ if (typeof api.deleteFile === 'function') {
+ await api.deleteFile(docId, targetId);
+ enqueueSnackbar('Anhang gelöscht.', { variant: 'success' });
+ }
+ } catch (err) {
+ notifyError(err);
+ }
+ }
+ }
+
async handleSave() {
const { mode, docId } = this.props;
const cfg = this.cfg;
@@ -965,12 +1045,17 @@ class ManifestForm extends PureComponent {
externalNumber={externalNumber}
saving={saving}
pdfUploading={pdfUploading}
+ pdfCreating={this.state.pdfCreating}
lines={lines}
+ attachments={this.state.attachments}
+ docId={this.props.docId}
isDirty={this.state.isDirty}
dirtyFields={this.state.dirtyFields}
dirtySummary={this.state.dirtySummary}
onRestore={this.props.mode === 'edit' && this.state.isDirty ? this.handleRestore : undefined}
onPdfUpload={kind === 'delivery' ? this.handlePdfUpload : undefined}
+ onPdfDelete={this.handleAttachmentDelete}
+ onPdfCreate={kind === 'quote' ? this.handleCreatePdf : undefined}
onSave={this.handleSave}
onPartyChange={this.handlePartyChange}
onDateChange={this.handleDateChange}
diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx
index 2717a39..c31acfb 100644
--- a/src/components/manifest/ManifestHeader.jsx
+++ b/src/components/manifest/ManifestHeader.jsx
@@ -7,6 +7,7 @@ import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import Chip from '@mui/material/Chip';
import UploadFileIcon from '@mui/icons-material/UploadFile';
+import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
@@ -225,6 +226,8 @@ class ManifestHeader extends PureComponent {
pdfUploading,
attachments,
onPdfUpload,
+ onPdfCreate,
+ pdfCreating,
onPdfRescan,
onPdfDelete,
onPdfClick,
@@ -428,6 +431,8 @@ class ManifestHeader extends PureComponent {
)}
)}
+ {kind === 'quote' && onPdfCreate && docId != null && Boolean(number) && (
+ : }
+ disabled={saving || pdfCreating}
+ onClick={onPdfCreate}
+ title="PDF erstellen"
+ sx={{
+ whiteSpace: 'nowrap',
+ height: 40,
+ width: '100%',
+ px: 1.5,
+ boxSizing: 'border-box',
+ fontWeight: 600,
+ fontSize: '0.875rem',
+ }}
+ >
+ {pdfCreating ? 'Wird erstellt…' : 'PDF erstellen'}
+
+ )}
{onPdfUpload ? (