This commit is contained in:
seb
2026-10-04 11:25:17 +02:00
parent e5ff4ef307
commit 43fa8ea105
24 changed files with 1016 additions and 214 deletions

View File

@@ -217,6 +217,20 @@
* format: date-time
* text:
* type: string
* CompanyHistoryEntry:
* type: object
* required: [id, date, text]
* properties:
* id:
* type: integer
* companyuserId:
* type: integer
* nullable: true
* date:
* type: string
* format: date-time
* text:
* type: string
* ArticleSupplierPrice:
* type: object
* required: [net, taxId]

View File

@@ -84,26 +84,11 @@ export function register(app, basePath = '') {
* content:
* application/json:
* schema:
* type: object
* required: [attributevalues]
* properties:
* attributevalues:
* type: array
* description: Array of attribute value IDs (numbers) or objects with id or attributeValueId
* items:
* oneOf:
* - type: integer
* description: Attribute value ID
* - type: object
* properties:
* id:
* type: integer
* description: Attribute value ID
* attributeValueId:
* type: integer
* description: Attribute value ID
* sort:
* type: integer
* type: array
* description: Array of attribute value IDs. Array order defines sort.
* items:
* type: integer
* example: [166]
* responses:
* 200:
* description: Updated article attribute values
@@ -147,13 +132,10 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: skuParsed.error });
}
const valuesInput = req.body?.attributevalues ?? req.body?.attributeValues ?? req.body?.attribute_values ?? req.body?.values ?? req.body;
if (!Array.isArray(valuesInput)) {
return res.status(400).json({ error: 'attributevalues array required' });
}
if (!Array.isArray(req.body)) return res.status(400).json({ error: 'attributevalues array required' });
try {
const attributevalues = await db.setByArticleSku(req.user.id, skuParsed.sku, valuesInput);
const attributevalues = await db.setByArticleSku(req.user.id, skuParsed.sku, req.body);
emitArticleSignal(req, skuParsed.sku);
res.json({ attributevalues });
} catch (error) {

View File

@@ -17,6 +17,7 @@ function attributeErrorStatus(error) {
if (error.message === 'Name required') return 400;
if (error.message === 'Invalid attribute name order') return 400;
if (error.message === 'Invalid id') return 400;
if (error.message === 'Attribute name in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500;
}
@@ -200,6 +201,42 @@ export function register(app, basePath = '') {
* responses:
* 200:
* description: Deleted successfully
* 400:
* description: Bad request 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Attribute name not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Attribute name in use
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.patch(`${basePath}/attributename/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
@@ -213,7 +250,9 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId });
res.json({ attributename });
} catch (error) {
res.status(attributeErrorStatus(error)).json({ error: error.message });
const status = attributeErrorStatus(error);
if (status === 500) console.error('Update attribute name failed:', error);
res.status(status).json({ error: error.message });
}
});
@@ -229,7 +268,12 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId });
res.json({ ok: true });
} catch (error) {
res.status(attributeErrorStatus(error)).json({ error: error.message });
const status = attributeErrorStatus(error);
const message = (error.message === 'Attribute name in use' || error.message?.includes('FOREIGN KEY constraint'))
? 'Attribute name in use'
: error.message;
if (status === 500) console.error('Delete attribute name failed:', error);
res.status(status).json({ error: message });
}
});
}

View File

@@ -16,6 +16,7 @@ function attributeValueErrorStatus(error) {
if (error.message === 'Value required') return 400;
if (error.message === 'Invalid attribute value order') return 400;
if (error.message === 'Invalid id' || error.message === 'Invalid attributeId') return 400;
if (error.message === 'Attribute value in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500;
}
@@ -216,6 +217,42 @@ export function register(app, basePath = '') {
* responses:
* 200:
* description: Deleted successfully
* 400:
* description: Bad request 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Attribute value not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Attribute value in use
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.patch(`${basePath}/attributevalue/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
@@ -229,7 +266,9 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId });
res.json({ attributevalue });
} catch (error) {
res.status(attributeValueErrorStatus(error)).json({ error: error.message });
const status = attributeValueErrorStatus(error);
if (status === 500) console.error('Update attribute value failed:', error);
res.status(status).json({ error: error.message });
}
});
@@ -245,7 +284,12 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId });
res.json({ ok: true });
} catch (error) {
res.status(attributeValueErrorStatus(error)).json({ error: error.message });
const status = attributeValueErrorStatus(error);
const message = (error.message === 'Attribute value in use' || error.message?.includes('FOREIGN KEY constraint'))
? 'Attribute value in use'
: error.message;
if (status === 500) console.error('Delete attribute value failed:', error);
res.status(status).json({ error: message });
}
});
}

View File

@@ -400,6 +400,7 @@ export function register(app, basePath = '') {
.map((a) => ({
...a,
childCount: Number(a.childCount ?? 0),
isFather: Boolean(a.isFather ?? a.is_father),
}));
res.status(200).json({ articles });
} catch (error) {

View File

@@ -85,26 +85,11 @@ export function register(app, basePath = '') {
* content:
* application/json:
* schema:
* type: object
* required: [attributevalues]
* properties:
* attributevalues:
* type: array
* description: Array of attribute value IDs (numbers) or objects with id or attributeValueId
* items:
* oneOf:
* - type: integer
* description: Attribute value ID
* - type: object
* properties:
* id:
* type: integer
* description: Attribute value ID
* attributeValueId:
* type: integer
* description: Attribute value ID
* sort:
* type: integer
* type: array
* description: Array of attribute value IDs. Array order defines sort.
* items:
* type: integer
* example: [166]
* responses:
* 200:
* description: Updated category attribute values
@@ -148,13 +133,10 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error });
}
const valuesInput = req.body?.attributevalues ?? req.body?.attributeValues ?? req.body?.attribute_values ?? req.body?.values ?? req.body;
if (!Array.isArray(valuesInput)) {
return res.status(400).json({ error: 'attributevalues array required' });
}
if (!Array.isArray(req.body)) return res.status(400).json({ error: 'attributevalues array required' });
try {
const attributevalues = await db.setByCategoryId(req.user.id, idParsed.id, valuesInput);
const attributevalues = await db.setByCategoryId(req.user.id, idParsed.id, req.body);
emitCategorySignal(req);
res.json({ attributevalues });
} catch (error) {

View File

@@ -32,10 +32,21 @@ function parseCompanyId(id) {
return { companyId };
}
function requireSelectedCompany(req) {
const id = req.user?.selectedCompanyId;
if (id == null) {
const err = new Error('No company selected');
err.statusCode = 400;
throw err;
}
return id;
}
function companyErrorStatus(error) {
if (error.message === 'Company not found') return 404;
if (error.message === 'Company name not available') return 409;
if (error.message === 'Invalid tax rate') return 400;
if (error.message === 'Invalid tax rate' || error.message === 'Text required' || error.message === 'No company selected') return 400;
if (error.message === 'Not authorized') return 403;
return 500;
}
@@ -167,6 +178,170 @@ export function register(app, basePath = '') {
}
});
/**
* @openapi
* /company/history:
* get:
* tags: [Company]
* summary: Get company history
* description: |
* Returns `companyhistory` rows for the selected company, newest first (`date` DESC, `id` DESC).
* security:
* - cookieAuth: []
* x-markdown: company/history.md
* parameters:
* - in: query
* name: limit
* required: false
* schema:
* type: integer
* minimum: 1
* maximum: 1000
* description: Maximum number of entries to return (newest first). Omit for all entries.
* responses:
* 200:
* description: Company history (newest first)
* content:
* application/json:
* schema:
* type: object
* required: [history, truncated]
* properties:
* history:
* type: array
* items:
* $ref: '#/components/schemas/CompanyHistoryEntry'
* truncated:
* type: boolean
* description: True when a limit was applied and the result set was exactly that size (more entries may exist).
* 400:
* description: Invalid limit 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* post:
* tags: [Company]
* summary: Add company history entry
* description: |
* Appends an entry to `companyhistory` for the selected company.
* security:
* - cookieAuth: []
* x-markdown: company/addHistory.md
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [text]
* properties:
* text:
* type: string
* description: History entry text or note
* responses:
* 201:
* description: History entry created
* content:
* application/json:
* schema:
* type: object
* required: [history]
* properties:
* history:
* $ref: '#/components/schemas/CompanyHistoryEntry'
* 400:
* description: Text required 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.get(`${basePath}/company/history`, async (req, res) => {
try {
requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const rawLimit = req.query.limit;
let limit = null;
if (rawLimit !== undefined) {
limit = parseInt(rawLimit, 10);
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
return res.status(400).json({ error: 'limit must be an integer between 1 and 1000' });
}
}
try {
const history = await db.getCompanyHistory(req.user.id, limit != null ? { limit } : undefined);
const truncated = limit != null && history.length === limit;
res.status(200).json({ history, truncated });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Get company history failed:', error);
res.status(status).json({ error: error.message });
}
});
app.post(`${basePath}/company/history`, async (req, res) => {
try {
requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const text = typeof req.body?.text === 'string' ? req.body.text.trim() : '';
if (!text) {
return res.status(400).json({ error: 'text is required' });
}
try {
const entry = await db.addCompanyHistory(req.user.id, { text });
res.status(201).json({ history: entry });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Add company history failed:', error);
res.status(status).json({ error: error.message });
}
});
/**
* @openapi
* /company/{id}:

View File

@@ -17,6 +17,7 @@ function propertyErrorStatus(error) {
if (error.message === 'Name required') return 400;
if (error.message === 'Invalid property name order') return 400;
if (error.message === 'Invalid id') return 400;
if (error.message === 'Property name in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500;
}
@@ -214,6 +215,42 @@ export function register(app, basePath = '') {
* responses:
* 200:
* description: Deleted successfully
* 400:
* description: Bad request 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Property name not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Property name in use
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.patch(`${basePath}/propertyname/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
@@ -227,7 +264,9 @@ export function register(app, basePath = '') {
emit('propertyname', { companyId: selected.companyId });
res.json({ propertyname });
} catch (error) {
res.status(propertyErrorStatus(error)).json({ error: error.message });
const status = propertyErrorStatus(error);
if (status === 500) console.error('Update property name failed:', error);
res.status(status).json({ error: error.message });
}
});
@@ -243,7 +282,12 @@ export function register(app, basePath = '') {
emit('propertyname', { companyId: selected.companyId });
res.json({ ok: true });
} catch (error) {
res.status(propertyErrorStatus(error)).json({ error: error.message });
const status = propertyErrorStatus(error);
const message = (error.message === 'Property name in use' || error.message?.includes('FOREIGN KEY constraint'))
? 'Property name in use'
: error.message;
if (status === 500) console.error('Delete property name failed:', error);
res.status(status).json({ error: message });
}
});
}