diff --git a/docs/api/README.md b/docs/api/README.md index 561005f..76cac1b 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -134,6 +134,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/delivery/delete.md`](routes/delivery/delete.md) | `DELETE /delivery/{id}` | | [`routes/delivery/get.md`](routes/delivery/get.md) | `GET /delivery/{id}` | | [`routes/delivery/update.md`](routes/delivery/update.md) | `PATCH /delivery/{id}` | +| [`routes/delivery/_id/file/_fileId.delete.md`](routes/delivery/_id/file/_fileId.delete.md) | `DELETE /delivery/{id}/file/{fileId}` | | [`routes/delivery/file.md`](routes/delivery/file.md) | `GET /delivery/{id}/file/{fileId}` | | [`routes/delivery/_id/pdf.md`](routes/delivery/_id/pdf.md) | `POST /delivery/{id}/pdf` | | [`routes/entity/list.md`](routes/entity/list.md) | `GET /entity` | @@ -188,7 +189,8 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/order/delete.md`](routes/order/delete.md) | `DELETE /order/{id}` | | [`routes/order/get.md`](routes/order/get.md) | `GET /order/{id}` | | [`routes/order/update.md`](routes/order/update.md) | `PATCH /order/{id}` | -| [`routes/order/_id/file/_fileId.md`](routes/order/_id/file/_fileId.md) | `GET /order/{id}/file/{fileId}` | +| [`routes/order/_id/file/_fileId.delete.md`](routes/order/_id/file/_fileId.delete.md) | `DELETE /order/{id}/file/{fileId}` | +| [`routes/order/_id/file/_fileId.get.md`](routes/order/_id/file/_fileId.get.md) | `GET /order/{id}/file/{fileId}` | | [`routes/order/_id/payment.get.md`](routes/order/_id/payment.get.md) | `GET /order/{id}/payment` | | [`routes/order/_id/payment.post.md`](routes/order/_id/payment.post.md) | `POST /order/{id}/payment` | | [`routes/order/_id/pdf.md`](routes/order/_id/pdf.md) | `POST /order/{id}/pdf` | @@ -227,13 +229,15 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/quote/delete.md`](routes/quote/delete.md) | `DELETE /quote/{id}` | | [`routes/quote/get.md`](routes/quote/get.md) | `GET /quote/{id}` | | [`routes/quote/update.md`](routes/quote/update.md) | `PATCH /quote/{id}` | -| [`routes/quote/_id/file/_fileId.md`](routes/quote/_id/file/_fileId.md) | `GET /quote/{id}/file/{fileId}` | +| [`routes/quote/_id/file/_fileId.delete.md`](routes/quote/_id/file/_fileId.delete.md) | `DELETE /quote/{id}/file/{fileId}` | +| [`routes/quote/_id/file/_fileId.get.md`](routes/quote/_id/file/_fileId.get.md) | `GET /quote/{id}/file/{fileId}` | | [`routes/quote/_id/pdf.md`](routes/quote/_id/pdf.md) | `POST /quote/{id}/pdf` | | [`routes/serper/image.md`](routes/serper/image.md) | `POST /serper/image` | | [`routes/serper/images.md`](routes/serper/images.md) | `POST /serper/images` | | [`routes/shipment/_id.delete.md`](routes/shipment/_id.delete.md) | `DELETE /shipment/{id}` | | [`routes/shipment/_id.get.md`](routes/shipment/_id.get.md) | `GET /shipment/{id}` | -| [`routes/shipment/_id/file/_fileId.md`](routes/shipment/_id/file/_fileId.md) | `GET /shipment/{id}/file/{fileId}` | +| [`routes/shipment/_id/file/_fileId.delete.md`](routes/shipment/_id/file/_fileId.delete.md) | `DELETE /shipment/{id}/file/{fileId}` | +| [`routes/shipment/_id/file/_fileId.get.md`](routes/shipment/_id/file/_fileId.get.md) | `GET /shipment/{id}/file/{fileId}` | | [`routes/shipment/_id/pdf.md`](routes/shipment/_id/pdf.md) | `POST /shipment/{id}/pdf` | | [`routes/shipment/_id/shipping.get.md`](routes/shipment/_id/shipping.get.md) | `GET /shipment/{id}/shipping` | | [`routes/shipment/_id/shipping.post.md`](routes/shipment/_id/shipping.post.md) | `POST /shipment/{id}/shipping` | @@ -288,16 +292,16 @@ Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). A | `attributename` | `POST /attributename`, `PUT /attributename/order`, `DELETE /attributename/{id}`, `PATCH /attributename/{id}`, `DELETE /attributevalue/{id}/image`, `POST /attributevalue/{id}/image` | | `brand` | `POST /brand`, `DELETE /brand/{id}`, `PATCH /brand/{id}`, `DELETE /brand/{id}/image`, `POST /brand/{id}/image` | | `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image` | -| `delivery` | `POST /delivery/{id}/pdf` | +| `delivery` | `DELETE /delivery/{id}/file/{fileId}`, `POST /delivery/{id}/pdf` | | `labeltemplate` | `POST /labeltemplate`, `DELETE /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `PUT /labeltemplate/{id}` | | `ledger` | `POST /stock/adjust` | | `modelconversation` | `POST /modelconversation`, `DELETE /modelconversation/{id}` | | `modelconversation-` | `POST /modelconversation/{id}/message` | -| `order` | `POST /order/{id}/pdf` | +| `order` | `DELETE /order/{id}/file/{fileId}`, `POST /order/{id}/pdf`, `DELETE /shipment/{id}/file/{fileId}` | | `printer` | `POST /printer`, `PUT /printer/order`, `DELETE /printer/{id}`, `PATCH /printer/{id}`, `PUT /printer/{id}` | | `propertyname` | `POST /propertyname`, `PUT /propertyname/order`, `DELETE /propertyname/{id}`, `PATCH /propertyname/{id}` | -| `quote` | `POST /quote/{id}/pdf` | -| `shipment` | `POST /shipment/{id}/pdf` | +| `quote` | `DELETE /quote/{id}/file/{fileId}`, `POST /quote/{id}/pdf` | +| `shipment` | `DELETE /shipment/{id}/file/{fileId}`, `POST /shipment/{id}/pdf` | | `stock` | `POST /stock/adjust` | | `vfs` | `POST /vfs/mkdir`, `POST /vfs/rename`, `POST /vfs/upload`, `DELETE /vfs/{*path}`, `POST /vfs/{*path}`, `PUT /vfs/{*path}` | | `warehouse` | `POST /warehouse`, `PUT /warehouse/order`, `DELETE /warehouse/{id}`, `PUT /warehouse/{id}` | diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index 22a81b9..82bd381 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -1,6 +1,6 @@ # Dash API Routes Overview (All) -Overview of all 242 API endpoints across all modules. +Overview of all 246 API endpoints across all modules. ### Notation @@ -419,6 +419,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` @@ -632,6 +635,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` @@ -770,6 +776,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` @@ -800,6 +809,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/docs/api/routes.md b/docs/api/routes.md index 683db32..0cf7ed3 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -1,6 +1,6 @@ # Dash API Routes Overview -Overview of all 167 API endpoints across all modules. +Overview of all 171 API endpoints across all modules. ### Notation @@ -325,6 +325,9 @@ _none_ #### Update delivery — `PATCH /delivery/{id}` - Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T28[], 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` #### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf` - Params: `id: int` - Returns: `201 T11` @@ -436,6 +439,9 @@ _none_ #### Update order — `PATCH /order/{id}` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T28[] }` - Returns: `200 T1` +#### Delete order file attachment — `DELETE /order/{id}/file/{fileId}` +- Params: `id: int, fileId: int` +- Returns: `204 void` #### List payments of an order — `GET /order/{id}/payment` - Params: `id: int` - Returns: `200 { payments: T37[] }` @@ -529,6 +535,9 @@ _none_ #### Update quote — `PATCH /quote/{id}` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T28[] }` - Returns: `200 T8` +#### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}` +- Params: `id: int, fileId: int` +- Returns: `204 void` #### Generate and attach quote PDF — `POST /quote/{id}/pdf` - Params: `id: int` - Returns: `201 T11` @@ -541,6 +550,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` #### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf` - Params: `id: int` - Returns: `201 T11` diff --git a/docs/api/routes/delivery/_id/file/_fileId.delete.md b/docs/api/routes/delivery/_id/file/_fileId.delete.md new file mode 100644 index 0000000..f5e4b1d --- /dev/null +++ b/docs/api/routes/delivery/_id/file/_fileId.delete.md @@ -0,0 +1,38 @@ +# `DELETE /delivery/{id}/file/{fileId}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete delivery file attachment + +Removes a file attachment from the delivery and deletes the underlying file. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Delivery ID | +| `fileId` | yes | integer | File ID or attachment ID | + +## Signals + +On success, emits SSE signal: `delivery` (see [`GET /events`](../events.md)). + +## Response `204` + +Attachment deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid parameters or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Delivery or attachment not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/order/_id/file/_fileId.delete.md b/docs/api/routes/order/_id/file/_fileId.delete.md new file mode 100644 index 0000000..ccc1b12 --- /dev/null +++ b/docs/api/routes/order/_id/file/_fileId.delete.md @@ -0,0 +1,38 @@ +# `DELETE /order/{id}/file/{fileId}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete order file attachment + +Removes a file attachment from the order and deletes the underlying file. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Order ID | +| `fileId` | yes | integer | File ID or attachment ID | + +## Signals + +On success, emits SSE signal: `order` (see [`GET /events`](../events.md)). + +## Response `204` + +Attachment deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid parameters or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Order or attachment not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/order/_id/file/_fileId.md b/docs/api/routes/order/_id/file/_fileId.get.md similarity index 100% rename from docs/api/routes/order/_id/file/_fileId.md rename to docs/api/routes/order/_id/file/_fileId.get.md diff --git a/docs/api/routes/quote/_id/file/_fileId.delete.md b/docs/api/routes/quote/_id/file/_fileId.delete.md new file mode 100644 index 0000000..6ac1ef2 --- /dev/null +++ b/docs/api/routes/quote/_id/file/_fileId.delete.md @@ -0,0 +1,38 @@ +# `DELETE /quote/{id}/file/{fileId}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete quote file attachment + +Removes a file attachment from the quote and deletes the underlying file. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Quote ID | +| `fileId` | yes | integer | File ID or attachment ID | + +## Signals + +On success, emits SSE signal: `quote` (see [`GET /events`](../events.md)). + +## Response `204` + +Attachment deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid parameters or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Quote or attachment not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/quote/_id/file/_fileId.md b/docs/api/routes/quote/_id/file/_fileId.get.md similarity index 100% rename from docs/api/routes/quote/_id/file/_fileId.md rename to docs/api/routes/quote/_id/file/_fileId.get.md diff --git a/docs/api/routes/shipment/_id/file/_fileId.delete.md b/docs/api/routes/shipment/_id/file/_fileId.delete.md new file mode 100644 index 0000000..9934e9b --- /dev/null +++ b/docs/api/routes/shipment/_id/file/_fileId.delete.md @@ -0,0 +1,38 @@ +# `DELETE /shipment/{id}/file/{fileId}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete shipment file attachment + +Removes a file attachment from the shipment and deletes the underlying file. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Shipment ID | +| `fileId` | yes | integer | File ID or attachment ID | + +## Signals + +On success, emits SSE signals: `shipment`, `order` (see [`GET /events`](../events.md)). + +## Response `204` + +Attachment deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid parameters or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Shipment or attachment not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/shipment/_id/file/_fileId.md b/docs/api/routes/shipment/_id/file/_fileId.get.md similarity index 100% rename from docs/api/routes/shipment/_id/file/_fileId.md rename to docs/api/routes/shipment/_id/file/_fileId.get.md diff --git a/src/api/routes/delivery.js b/src/api/routes/delivery.js index efe92ec..80c1cbe 100644 --- a/src/api/routes/delivery.js +++ b/src/api/routes/delivery.js @@ -2,6 +2,7 @@ import multer from 'multer'; import * as db from '../../db/delivery.js'; import { createAndAttachManifestPdf } from '../../services/manifestPdf.js'; import { writeErrorLog } from '../../util/errorLog.js'; +import { emit } from '../sse.js'; // #region Helpers @@ -995,5 +996,89 @@ export function register(app, basePath = '') { }); // #endregion + + /** + * @openapi + * /delivery/{id}/file/{fileId}: + * delete: + * tags: [Delivery] + * summary: Delete delivery file attachment + * description: | + * Removes a file attachment from the delivery and deletes the underlying file. + * security: + * - cookieAuth: [] + * x-signals: [delivery] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Delivery ID + * - in: path + * name: fileId + * required: true + * schema: + * type: integer + * description: File ID or attachment ID + * responses: + * 204: + * description: Attachment deleted + * 400: + * description: Invalid parameters or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Delivery or attachment not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + // #region DELETE /delivery/:id/file/:fileId + + app.delete(`${basePath}/delivery/:id/file/:fileId`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + const fileId = Number(req.params.fileId); + if (!Number.isInteger(fileId) || fileId <= 0) { + return res.status(400).json({ error: 'fileId is required' }); + } + + try { + await db.removeAttachment(req.user.id, id, fileId); + emit('delivery', { companyId: selected.companyId }); + res.sendStatus(204); + } catch (error) { + const status = deliveryErrorStatus(error); + if (status === 500) console.error('Delete delivery attachment failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + // #endregion } + diff --git a/src/api/routes/order.js b/src/api/routes/order.js index 64b8796..d5c61c5 100644 --- a/src/api/routes/order.js +++ b/src/api/routes/order.js @@ -1,5 +1,6 @@ import * as db from '../../db/order.js'; import { createAndAttachManifestPdf } from '../../services/manifestPdf.js'; +import { emit } from '../sse.js'; // #region Helpers @@ -779,4 +780,88 @@ export function register(app, basePath = '') { }); // #endregion + + /** + * @openapi + * /order/{id}/file/{fileId}: + * delete: + * tags: [Order] + * summary: Delete order file attachment + * description: | + * Removes a file attachment from the order and deletes the underlying file. + * security: + * - cookieAuth: [] + * x-signals: [order] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order ID + * - in: path + * name: fileId + * required: true + * schema: + * type: integer + * description: File ID or attachment ID + * responses: + * 204: + * description: Attachment deleted + * 400: + * description: Invalid parameters or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Order or attachment not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + // #region DELETE /order/:id/file/:fileId + + app.delete(`${basePath}/order/:id/file/:fileId`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + const fileId = Number(req.params.fileId); + if (!Number.isInteger(fileId) || fileId <= 0) { + return res.status(400).json({ error: 'fileId is required' }); + } + + try { + await db.removeAttachment(req.user.id, id, fileId); + emit('order', { companyId: selected.companyId }); + res.sendStatus(204); + } catch (error) { + const status = orderErrorStatus(error); + if (status === 500) console.error('Delete order attachment failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + // #endregion } + diff --git a/src/api/routes/quote.js b/src/api/routes/quote.js index 53e9a64..ad9036c 100644 --- a/src/api/routes/quote.js +++ b/src/api/routes/quote.js @@ -1,5 +1,6 @@ import * as db from '../../db/quote.js'; import { createAndAttachManifestPdf } from '../../services/manifestPdf.js'; +import { emit } from '../sse.js'; // #region Helpers @@ -758,4 +759,88 @@ export function register(app, basePath = '') { }); // #endregion + + /** + * @openapi + * /quote/{id}/file/{fileId}: + * delete: + * tags: [Quote] + * summary: Delete quote file attachment + * description: | + * Removes a file attachment from the quote and deletes the underlying file. + * security: + * - cookieAuth: [] + * x-signals: [quote] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Quote ID + * - in: path + * name: fileId + * required: true + * schema: + * type: integer + * description: File ID or attachment ID + * responses: + * 204: + * description: Attachment deleted + * 400: + * description: Invalid parameters or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Quote or attachment not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + // #region DELETE /quote/:id/file/:fileId + + app.delete(`${basePath}/quote/:id/file/:fileId`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + const fileId = Number(req.params.fileId); + if (!Number.isInteger(fileId) || fileId <= 0) { + return res.status(400).json({ error: 'fileId is required' }); + } + + try { + await db.removeAttachment(req.user.id, id, fileId); + emit('quote', { companyId: selected.companyId }); + res.sendStatus(204); + } catch (error) { + const status = quoteErrorStatus(error); + if (status === 500) console.error('Delete quote attachment failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + // #endregion } + diff --git a/src/api/routes/shipment.js b/src/api/routes/shipment.js index fff0498..dd4add3 100644 --- a/src/api/routes/shipment.js +++ b/src/api/routes/shipment.js @@ -591,5 +591,89 @@ export function register(app, basePath = '') { }); // #endregion + + /** + * @openapi + * /shipment/{id}/file/{fileId}: + * delete: + * tags: [Shipment] + * summary: Delete shipment file attachment + * description: | + * Removes a file attachment from the shipment and deletes the underlying file. + * security: + * - cookieAuth: [] + * x-signals: [shipment, order] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Shipment ID + * - in: path + * name: fileId + * required: true + * schema: + * type: integer + * description: File ID or attachment ID + * responses: + * 204: + * description: Attachment deleted + * 400: + * description: Invalid parameters or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Shipment or attachment not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + // #region DELETE /shipment/:id/file/:fileId + + app.delete(`${basePath}/shipment/:id/file/:fileId`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + const fileId = Number(req.params.fileId); + if (!Number.isInteger(fileId) || fileId <= 0) { + return res.status(400).json({ error: 'fileId is required' }); + } + + try { + await db.removeAttachment(req.user.id, id, fileId); + emitShipmentEvents(selected.companyId); + res.sendStatus(204); + } catch (error) { + const status = shipmentErrorStatus(error); + if (status === 500) console.error('Delete shipment attachment failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + // #endregion } + diff --git a/src/db/delivery.js b/src/db/delivery.js index 00c8db7..dd1c0e2 100644 --- a/src/db/delivery.js +++ b/src/db/delivery.js @@ -40,3 +40,9 @@ export async function listAttachments(googleAuthId, deliveryId) { return store.manifestattachment.listByManifestId(googleAuthId, deliveryId); } +export async function removeAttachment(googleAuthId, deliveryId, fileId) { + const store = await getStore(); + return store.manifestattachment.remove(googleAuthId, deliveryId, fileId); +} + + diff --git a/src/db/order.js b/src/db/order.js index 1dd1549..5393d17 100644 --- a/src/db/order.js +++ b/src/db/order.js @@ -173,4 +173,10 @@ export async function listAttachments(googleAuthId, orderId) { return store.manifestattachment.listByManifestId(googleAuthId, orderId); } +export async function removeAttachment(googleAuthId, orderId, fileId) { + const store = await getStore(); + return store.manifestattachment.remove(googleAuthId, orderId, fileId); +} + + diff --git a/src/db/quote.js b/src/db/quote.js index 5b6461a..d7b3e6a 100644 --- a/src/db/quote.js +++ b/src/db/quote.js @@ -103,3 +103,9 @@ export async function listAttachments(googleAuthId, quoteId) { return store.manifestattachment.listByManifestId(googleAuthId, quoteId); } +export async function removeAttachment(googleAuthId, quoteId, fileId) { + const store = await getStore(); + return store.manifestattachment.remove(googleAuthId, quoteId, fileId); +} + + diff --git a/src/db/shipment.js b/src/db/shipment.js index fb125b5..6cdba43 100644 --- a/src/db/shipment.js +++ b/src/db/shipment.js @@ -43,3 +43,9 @@ export async function listAttachments(googleAuthId, shipmentId) { return store.manifestattachment.listByManifestId(googleAuthId, shipmentId); } +export async function removeAttachment(googleAuthId, shipmentId, fileId) { + const store = await getStore(); + return store.manifestattachment.remove(googleAuthId, shipmentId, fileId); +} + +