Add customergroup sort with bulk order and locked implicit names.

Reorder via customergroup.order (exact id list → sort 0…n-1); block renaming POS/Lieferanten while still allowing create/update of other groups by name.
This commit is contained in:
seb
2026-08-07 03:38:01 +02:00
parent b095ab6199
commit c290fe8f4b
13 changed files with 229 additions and 59 deletions

View File

@@ -1,3 +1,4 @@
import { POS_GROUP_NAME } from '../../sql/entity.js';
import { CREATE } from '../../sql/company.js';
import { CREATE as CREATE_CATEGORY } from '../../sql/category.js';
import {
@@ -10,8 +11,6 @@ import langEnsure from '../lang/ensure.js';
import taxEnsure from '../tax/ensure.js';
import request from '../request.js';
const POS_CUSTOMERGROUP_NAME = 'POS';
export default function create(pool) {
const nameTaken = getByName(pool);
const ensureLang = langEnsure(pool);
@@ -42,7 +41,8 @@ export default function create(pool) {
// Circular FK: create POS group first (company_id null), company with
// poscustomergroup_id NOT NULL, then link the group back to the company.
const posGroup = await request(pool, tx)
.input('name', POS_CUSTOMERGROUP_NAME)
.input('name', POS_GROUP_NAME)
.input('sort', 0)
.query(CREATE_CUSTOMERGROUP);
const posCustomerGroupId = posGroup.recordset[0].id;

View File

@@ -1,6 +1,13 @@
import { CREATE } from '../../sql/customergroup.js';
import { CREATE, GET_NEXT_SORT } from '../../sql/customergroup.js';
import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../sql/entity.js';
import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js';
const RESERVED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]);
function shape(row) {
return { id: row.id, name: row.name, sort: row.sort };
}
export default function create(pool) {
const access = resolveCompanyAccess(pool);
@@ -9,16 +16,25 @@ export default function create(pool) {
if (!cleaned) {
throw new Error('Name required');
}
if (RESERVED_NAMES.has(cleaned)) {
throw new Error('Name not allowed');
}
await access(googleAuthId, companyId);
const next = await pool
.request()
.input('companyId', companyId)
.query(GET_NEXT_SORT);
const sort = next.recordset[0].sort;
const result = await pool
.request()
.input('companyId', companyId)
.input('name', cleaned)
.input('sort', sort)
.query(CREATE);
const row = result.recordset[0];
return { id: row.id, name: row.name };
return shape(result.recordset[0]);
};
}

View File

@@ -15,6 +15,7 @@ export default function list(pool) {
return result.recordset.map((row) => ({
id: row.id,
name: row.name,
sort: row.sort,
}));
};
}

View File

@@ -0,0 +1,65 @@
import {
LIST_IDS_BY_COMPANY,
UPDATE_SORT,
} from '../../sql/customergroup.js';
import request from '../request.js';
import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js';
export default function order(pool) {
const access = resolveCompanyAccess(pool);
return async (googleAuthId, companyId, { customergroupIds } = {}) => {
if (!Array.isArray(customergroupIds)) {
throw new Error('Invalid customer group order');
}
await access(googleAuthId, companyId);
const groups = await pool
.request()
.input('companyId', companyId)
.query(LIST_IDS_BY_COMPANY);
const existing = new Set(groups.recordset.map((r) => r.id));
if (customergroupIds.length !== existing.size) {
throw new Error('Invalid customer group order');
}
const seen = new Set();
for (const customergroupId of customergroupIds) {
if (
!Number.isFinite(customergroupId) ||
!existing.has(customergroupId) ||
seen.has(customergroupId)
) {
throw new Error('Invalid customer group order');
}
seen.add(customergroupId);
}
const tx = pool.transaction();
await tx.begin();
try {
for (let sort = 0; sort < customergroupIds.length; sort++) {
await request(pool, tx)
.input('id', customergroupIds[sort])
.input('companyId', companyId)
.input('sort', sort)
.query(UPDATE_SORT);
}
await tx.commit();
return { customergroupIds };
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
}
throw err;
}
};
}

View File

@@ -1,6 +1,13 @@
import { UPDATE_NAME } from '../../sql/customergroup.js';
import { GET_BY_COMPANY_AND_ID, UPDATE } from '../../sql/customergroup.js';
import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../sql/entity.js';
import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js';
const LOCKED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]);
function shape(row) {
return { id: row.id, name: row.name, sort: row.sort };
}
export default function update(pool) {
const access = resolveCompanyAccess(pool);
@@ -9,24 +16,39 @@ export default function update(pool) {
throw new Error('Customer group not found');
}
const cleaned = name?.trim();
const cleaned = name?.trim?.() ? name.trim() : null;
if (!cleaned) {
throw new Error('Name required');
}
await access(googleAuthId, companyId);
const existing = await pool
.request()
.input('id', id)
.input('companyId', companyId)
.query(GET_BY_COMPANY_AND_ID);
const current = existing.recordset[0];
if (!current) {
throw new Error('Customer group not found');
}
if (LOCKED_NAMES.has(current.name) && cleaned !== current.name) {
throw new Error('Name cannot be changed');
}
const result = await pool
.request()
.input('id', id)
.input('companyId', companyId)
.input('name', cleaned)
.query(UPDATE_NAME);
.query(UPDATE);
const row = result.recordset[0];
if (!row) {
throw new Error('Customer group not found');
}
return { id: row.id, name: row.name };
return shape(row);
};
}

View File

@@ -1,6 +1,7 @@
import {
CREATE,
GET_BY_COMPANY_AND_NAME,
GET_NEXT_SORT,
} from '../../sql/customergroup.js';
import { SUPPLIER_GROUP_NAME } from '../../sql/entity.js';
@@ -17,9 +18,15 @@ export default async function ensureLieferantenGroup(pool, companyId, tx) {
return existing.recordset[0].id;
}
const next = await req()
.input('companyId', companyId)
.query(GET_NEXT_SORT);
const sort = next.recordset[0].sort;
const created = await req()
.input('companyId', companyId)
.input('name', SUPPLIER_GROUP_NAME)
.input('sort', sort)
.query(CREATE);
return created.recordset[0].id;