pos implemented
This commit is contained in:
@@ -1058,20 +1058,45 @@ export async function getProductSubcategories(opts: {
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Best-effort role label from the numeric roleid (roles aren't fully resolvable for every config). */
|
||||
/**
|
||||
* Role names exactly as `app_roles` holds them.
|
||||
*
|
||||
* These were wrong for five of six ids until 2026-08-07 — the old map said
|
||||
* 1 Owner / 2 Manager / 4 Staff / 5 Rider / 6 Cashier, none of which the
|
||||
* database agrees with. Anything deriving behaviour from a *name* was
|
||||
* therefore deriving it from fiction, which is how `isRiderRole` came to key
|
||||
* off roleid 5 ("Admin").
|
||||
*
|
||||
* Note 3 and 5 are both "Admin", and 4 and 6 are both "Manager" — they are the
|
||||
* same role under different configids (15 and 14). That is the schema's shape,
|
||||
* not a transcription error, so it is preserved here rather than tidied.
|
||||
*/
|
||||
const ROLE_NAMES: Record<number, string> = {
|
||||
[-1]: 'Unassigned',
|
||||
0: 'Unassigned',
|
||||
1: 'Super admin',
|
||||
2: 'Operations',
|
||||
3: 'Admin',
|
||||
4: 'Manager',
|
||||
5: 'Admin',
|
||||
6: 'Manager',
|
||||
// Till-only. These accounts have no Nearle Daily login at all — the backend
|
||||
// excludes them from every web login lookup — so they are never the role of
|
||||
// a signed-in console user. Listed because the POS staff screens render them.
|
||||
7: 'Supervisor',
|
||||
8: 'Cashier',
|
||||
};
|
||||
|
||||
export function roleName(roleid: number): string {
|
||||
const map: Record<number, string> = {
|
||||
[-1]: 'Unassigned',
|
||||
0: 'Unassigned',
|
||||
1: 'Owner',
|
||||
2: 'Manager',
|
||||
3: 'Admin',
|
||||
4: 'Staff',
|
||||
5: 'Rider',
|
||||
6: 'Cashier',
|
||||
};
|
||||
return map[roleid] || `Role ${roleid}`;
|
||||
return ROLE_NAMES[roleid] || `Role ${roleid}`;
|
||||
}
|
||||
|
||||
/** Till roles. Created through `tenants/createposuser`, never `users/create`. */
|
||||
export const POS_ROLE_SUPERVISOR = 7;
|
||||
export const POS_ROLE_CASHIER = 8;
|
||||
export const isPosRole = (roleid: number): boolean =>
|
||||
roleid === POS_ROLE_SUPERVISOR || roleid === POS_ROLE_CASHIER;
|
||||
|
||||
/** /users/getallusers?roleid=&tenantid=&pageno=&pagesize=&keyword= — staff/users under a tenant. */
|
||||
export async function getAllUsers(opts: {
|
||||
tenantid: number;
|
||||
@@ -1650,3 +1675,189 @@ export async function getGlobalProducts(opts: {
|
||||
export async function getGlobalProduct(opts: { brand: string; sku: string }): Promise<Row | null> {
|
||||
return firstRow(await fiestaGet('catalogue/getproduct', { brand: opts.brand, sku: opts.sku }));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// POS staff — supervisors and cashiers
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// A separate surface from `users/create` on purpose. A till account has no
|
||||
// Nearle Daily login (the backend excludes roleid 7 and 8 from every web login
|
||||
// lookup), and its username, password and PIN are all minted server-side. Sent
|
||||
// through `users/create` it would land as a row with none of those and be
|
||||
// unable to open a till.
|
||||
//
|
||||
// Consequently these people never appear in `getallusers` either — the two
|
||||
// lists are disjoint by design, not by omission.
|
||||
|
||||
/** One person who signs in at a till. */
|
||||
export interface PosStaffUser {
|
||||
user_id: number;
|
||||
full_name: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
authname?: string;
|
||||
contactno?: string;
|
||||
role_id: number;
|
||||
role: string;
|
||||
pin?: string;
|
||||
has_password: boolean;
|
||||
/** Present ONLY in the answer to a create or a reset. Never in a listing. */
|
||||
password?: string;
|
||||
location_id: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface PosRoleOption {
|
||||
role_id: number;
|
||||
/** The wire value `createposuser` expects — "supervisor" | "cashier". */
|
||||
role: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/** GET /tenants/posroles — the roles a till account may hold. Asked for rather
|
||||
* than hardcoded, so adding a till role later needs no frontend release. */
|
||||
export async function getPosRoles(): Promise<PosRoleOption[]> {
|
||||
return toRows<PosRoleOption>(await fiestaGet('tenants/posroles'));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /tenants/getposusers — till accounts at one outlet. Never returns a password.
|
||||
*
|
||||
* Note the envelope: `details` is an OBJECT — `{location_id, users: [...]}` —
|
||||
* not the bare array most Fiesta reads return. Verified against the live
|
||||
* response. Passing it through `toRows` yields one wrapper object rather than
|
||||
* the people, so the array is taken explicitly.
|
||||
*/
|
||||
export async function getPosStaff(opts: {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
}): Promise<PosStaffUser[]> {
|
||||
const json = await fiestaGet<{ details?: { users?: PosStaffUser[] } | PosStaffUser[] }>(
|
||||
'tenants/getposusers',
|
||||
{
|
||||
tenantid: opts.tenantid,
|
||||
locationid: opts.locationid,
|
||||
// Always asked for. The endpoint hides deactivated people by default,
|
||||
// which reads sensibly for a till but not for the screen that manages
|
||||
// them: a person deactivated by mistake would vanish with no way back,
|
||||
// and their PIN would silently stay reserved. The panel shows them
|
||||
// greyed with a Reactivate action instead.
|
||||
include_inactive: 'true',
|
||||
},
|
||||
);
|
||||
const d = json?.details;
|
||||
if (Array.isArray(d)) return d;
|
||||
return d?.users ?? [];
|
||||
}
|
||||
|
||||
export interface CreatePosStaffInput {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
full_name: string;
|
||||
/** "supervisor" | "cashier" — from getPosRoles, not a hardcoded literal. */
|
||||
role: string;
|
||||
pin: string;
|
||||
/** Optional. Omitted, the server generates one and walks past collisions. */
|
||||
authname?: string;
|
||||
contactno?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /tenants/createposuser
|
||||
*
|
||||
* The response is the ONLY time the password exists — it is not stored
|
||||
* anywhere the console can read back, and the listing endpoint omits it. Show
|
||||
* it once and mean it.
|
||||
*
|
||||
* `authname` may come back different from anything sent or expected: a shop
|
||||
* that already has `supervisor.1179@` gets `supervisor2.1179@` rather than an
|
||||
* error, so the returned value is the one to display.
|
||||
*/
|
||||
export async function createPosStaff(input: CreatePosStaffInput): Promise<PosStaffUser> {
|
||||
const res = await fiestaSend<{ details?: PosStaffUser }>(
|
||||
'tenants/createposuser',
|
||||
'POST',
|
||||
input,
|
||||
);
|
||||
const row = firstRow<PosStaffUser>(res);
|
||||
if (!row) throw new Error('The server accepted the request but returned no account.');
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /tenants/updateposuser
|
||||
*
|
||||
* Every field is optional and only non-empty ones are written, so this doubles
|
||||
* as the reactivate path (`status: 'Active'`) and the password reset.
|
||||
*
|
||||
* Unlike create, the password here is **caller-supplied** — the server writes
|
||||
* what it is given rather than minting one. A console offering a reset has to
|
||||
* generate it (see `generatePosPassword`) and show it once, because there is
|
||||
* still nowhere to read it back from.
|
||||
*
|
||||
* PIN uniqueness is enforced server-side under an advisory lock, so a clash
|
||||
* comes back as an error rather than two people sharing a PIN.
|
||||
*/
|
||||
export async function updatePosStaff(input: {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
user_id: number;
|
||||
full_name?: string;
|
||||
role?: string;
|
||||
pin?: string;
|
||||
authname?: string;
|
||||
contactno?: string;
|
||||
password?: string;
|
||||
status?: string;
|
||||
}): Promise<Row> {
|
||||
return fiestaSend<Row>('tenants/updateposuser', 'PUT', input);
|
||||
}
|
||||
|
||||
/**
|
||||
* A replacement password for a till account.
|
||||
*
|
||||
* Generated in the browser from `crypto.getRandomValues` — the update endpoint
|
||||
* writes whatever it is handed, so the strength of a reset password is entirely
|
||||
* this function's responsibility. Fourteen characters from a 62-symbol alphabet,
|
||||
* matching what the server mints on create.
|
||||
*
|
||||
* Ambiguous glyphs are kept rather than stripped: these are copied, not read
|
||||
* aloud, and shrinking the alphabet costs entropy for no real gain.
|
||||
*/
|
||||
export function generatePosPassword(length = 14): string {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const bytes = new Uint32Array(length);
|
||||
crypto.getRandomValues(bytes);
|
||||
let out = '';
|
||||
for (let i = 0; i < length; i++) out += alphabet[bytes[i] % alphabet.length];
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /tenants/deleteposuser
|
||||
*
|
||||
* Deactivates rather than removes — bills carry the cashier's name, and a
|
||||
* deleted row would orphan every one of them. The till refuses a deactivated
|
||||
* account immediately.
|
||||
*/
|
||||
export async function deactivatePosStaff(opts: {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
userid: number;
|
||||
}): Promise<Row> {
|
||||
const qs = new URLSearchParams({
|
||||
tenantid: String(opts.tenantid),
|
||||
locationid: String(opts.locationid),
|
||||
userid: String(opts.userid),
|
||||
});
|
||||
const res = await fetch(`${FIESTA_BASE}/tenants/deleteposuser?${qs}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const json = (await res.json().catch(() => null)) as { message?: string; status?: boolean } | null;
|
||||
if (!res.ok || (json && json.status === false)) {
|
||||
throw new Error(json?.message || `Could not deactivate the account (${res.status}).`);
|
||||
}
|
||||
return json as Row;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user