auto mail generation
This commit is contained in:
@@ -163,7 +163,17 @@ interface RequestOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
/**
|
||||
* One request, answered with the WHOLE envelope.
|
||||
*
|
||||
* `request` below is this plus "take the payload out", which is what nearly
|
||||
* every caller wants. A handful need a field that sits BESIDE the payload —
|
||||
* `invited` on onboarding is the one this was extracted for — and the only way
|
||||
* to read one used to be `requestEnvelope`, which does none of the checking: no
|
||||
* 401 handling, no `status: false`, no thrown `FiestaError`. So a caller that
|
||||
* wanted one extra field had to give up all the error handling to get it.
|
||||
*/
|
||||
async function send<T>(path: string, options: RequestOptions = {}): Promise<FiestaEnvelope<T>> {
|
||||
const { method = 'GET', params, body, signal } = options;
|
||||
|
||||
// There is exactly one path out of this function and it goes to `fetch`.
|
||||
@@ -223,10 +233,19 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
||||
);
|
||||
}
|
||||
|
||||
// Most handlers put the payload in `details`, but a handful answer with
|
||||
// `data` instead — `products/getallproducts` and `products/create` among the
|
||||
// ones the console calls (`productController.go:400,206`). Reading only
|
||||
// `details` handed those two callers `undefined` with no error anywhere.
|
||||
return envelope;
|
||||
}
|
||||
|
||||
/**
|
||||
* The payload, unwrapped.
|
||||
*
|
||||
* Most handlers put it in `details`, but a handful answer with `data` instead —
|
||||
* `products/getallproducts` and `products/create` among the ones the console
|
||||
* calls (`productController.go:400,206`). Reading only `details` handed those
|
||||
* two callers `undefined` with no error anywhere.
|
||||
*/
|
||||
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const envelope = await send<T>(path, options);
|
||||
return (envelope.details ?? envelope.data) as T;
|
||||
}
|
||||
|
||||
@@ -288,6 +307,16 @@ export const api = {
|
||||
post: <T>(path: string, body?: unknown, params?: Record<string, QueryValue>) =>
|
||||
request<T>(path, { method: 'POST', body, params }),
|
||||
|
||||
/**
|
||||
* A POST whose answer carries something beside the payload.
|
||||
*
|
||||
* Same checking as `post` — a failure still throws a `FiestaError` — so a
|
||||
* caller reading one extra envelope field does not give up the error handling
|
||||
* to get at it. `tenants/createtenantuser` needs `invited`.
|
||||
*/
|
||||
postEnvelope: <T>(path: string, body?: unknown, params?: Record<string, QueryValue>) =>
|
||||
send<T>(path, { method: 'POST', body, params }),
|
||||
|
||||
put: <T>(path: string, body?: unknown, params?: Record<string, QueryValue>) =>
|
||||
request<T>(path, { method: 'PUT', body, params }),
|
||||
|
||||
|
||||
@@ -37,6 +37,24 @@ export interface CreateStaffRequest {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* What happened to somebody's first-password invitation.
|
||||
*
|
||||
* Reported beside the person rather than folded into success or failure: they are
|
||||
* hired either way, and an unsent invitation is a task — resend, or correct the
|
||||
* address — not a hire to retry.
|
||||
*/
|
||||
export interface InviteOutcome {
|
||||
sent: boolean;
|
||||
/** Why not — an unconfigured mail host, a rejecting relay. Absent when it sent. */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface CreateStaffResult {
|
||||
person: StaffInfo;
|
||||
invite: InviteOutcome;
|
||||
}
|
||||
|
||||
export interface UpdateStaffRequest {
|
||||
userid: number;
|
||||
firstname?: string;
|
||||
@@ -85,7 +103,42 @@ export const staffApi = {
|
||||
unassign: (body: { tenantid: number; userid: number }) =>
|
||||
api.put<unknown>(`${WEB}/tenants/assignstaff`, { ...body, unassign: true }),
|
||||
|
||||
create: (body: CreateStaffRequest) => api.post<StaffInfo>(`${WEB}/users/create`, body),
|
||||
/**
|
||||
* Hires somebody, and emails them their way in.
|
||||
*
|
||||
* The account is created with NO password — nothing on this path sets one —
|
||||
* and since the sign-in screen stopped offering to set a first password, the
|
||||
* emailed link is the only way in. So the outcome of that email comes back
|
||||
* beside the person, and the screen shows it: an invitation that did not send
|
||||
* means somebody has been added to the directory who cannot sign in, and
|
||||
* nothing else in the product would ever mention it.
|
||||
*/
|
||||
create: (body: CreateStaffRequest): Promise<CreateStaffResult> =>
|
||||
api.postEnvelope<StaffInfo>(`${WEB}/users/create`, body).then((envelope) => ({
|
||||
person: (envelope.details ?? {}) as StaffInfo,
|
||||
invite: {
|
||||
// Absent means a backend that predates the invitation — read as "not
|
||||
// sent", so a half-deployed pair cannot claim an email that never left.
|
||||
sent: envelope.invited === true,
|
||||
...(envelope.invitereason ? { reason: envelope.invitereason } : {}),
|
||||
},
|
||||
})),
|
||||
|
||||
/**
|
||||
* Sends somebody's first-password link again.
|
||||
*
|
||||
* By `userid`, not by tenant: the owner's invitation is reachable by tenantid
|
||||
* because a business has one owner, but staff and branch logins are many, so
|
||||
* an operator chasing one names the person.
|
||||
*
|
||||
* The backend refuses anybody who already has a password and says to send them
|
||||
* to the sign-in page instead. That refusal is the point — an endpoint that
|
||||
* re-issues a working password link for any account on request is a password
|
||||
* reset, and nothing here verifies identity well enough to have one. It arrives
|
||||
* as a `FiestaError` and is shown as written.
|
||||
*/
|
||||
resendInvite: (userid: number) =>
|
||||
api.post<unknown>(`${WEB}/tenants/resendinvite`, { userid }),
|
||||
|
||||
/**
|
||||
* Update a person.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { api, WEB } from './client';
|
||||
import type { AppLocation } from './deliveries';
|
||||
import type { InviteOutcome } from './people';
|
||||
import type { TenantInfo, TenantLocation } from './types';
|
||||
|
||||
/** Everything the tenant-onboarding form collects. */
|
||||
@@ -101,6 +102,20 @@ export interface CreateBranchRequest {
|
||||
operatorid?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A commissioned branch, and what happened to its operator's invitation.
|
||||
*
|
||||
* The branch exists either way. A login that was not emailed is a task for
|
||||
* whoever commissioned it — resend, or fix the address — and not a branch to
|
||||
* create again.
|
||||
*/
|
||||
export interface CreateBranchResult {
|
||||
branch: TenantLocation;
|
||||
invite: InviteOutcome;
|
||||
/** The login the branch spawned, for a resend. 0 when a person was placed. */
|
||||
operatorUserid: number;
|
||||
}
|
||||
|
||||
export interface TenantListQuery {
|
||||
pageno?: number;
|
||||
pagesize?: number;
|
||||
@@ -176,9 +191,24 @@ export const tenantsApi = {
|
||||
* created row, and the new `locationid` is what a QR code and every
|
||||
* follow-up write need. `createlocation` answers 201 with a message and no
|
||||
* `details` at all.
|
||||
*
|
||||
* `invite` reports the spawned login's first-password email. It is `sent:
|
||||
* false` with NO reason when an `operatorid` was named — nothing was created,
|
||||
* so there was nothing to send, and that is not a failure to report.
|
||||
*/
|
||||
createBranch: (body: CreateBranchRequest) =>
|
||||
api.post<TenantLocation>(`${WEB}/tenants/createtenantlocation`, body),
|
||||
createBranch: (body: CreateBranchRequest): Promise<CreateBranchResult> =>
|
||||
api
|
||||
.postEnvelope<TenantLocation>(`${WEB}/tenants/createtenantlocation`, body)
|
||||
.then((envelope) => ({
|
||||
branch: (envelope.details ?? {}) as TenantLocation,
|
||||
invite: {
|
||||
sent: envelope.invited === true,
|
||||
...(envelope.invitereason ? { reason: envelope.invitereason } : {}),
|
||||
},
|
||||
// The spawned login, so a failed invitation can be resent without
|
||||
// hunting for the row by eye. 0 when an existing person was placed.
|
||||
operatorUserid: envelope.inviteuserid ?? 0,
|
||||
})),
|
||||
|
||||
updateBranch: (body: Partial<TenantLocation> & { locationid: number }) =>
|
||||
api.put<TenantLocation>(`${WEB}/tenants/updatelocation`, body),
|
||||
|
||||
@@ -54,6 +54,26 @@ export interface FiestaEnvelope<T> {
|
||||
* a number Fiesta has already worked out.
|
||||
*/
|
||||
pricedetails?: { orderamount?: number; totaltaxamount?: number };
|
||||
/**
|
||||
* Whether a newly created account was emailed its first-password link.
|
||||
* On the three endpoints that create one: `users/create`,
|
||||
* `tenants/createstaff` and `tenants/createtenantlocation`.
|
||||
*
|
||||
* Beside `details` rather than inside it because it is not a fact about the
|
||||
* person or the branch — those exist either way. It is what happened to a
|
||||
* separate side effect, and the only one an operator can act on.
|
||||
*/
|
||||
invited?: boolean;
|
||||
/** Why it did not send. Absent when it did. */
|
||||
invitereason?: string;
|
||||
/**
|
||||
* Who to resend to, from `tenants/createtenantlocation` only.
|
||||
*
|
||||
* `details` there is the branch row, and the login the branch spawned lives in
|
||||
* `app_users` — so this is the only way to name it. 0 when an existing person
|
||||
* was placed and no account was created.
|
||||
*/
|
||||
inviteuserid?: number;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -947,6 +967,19 @@ export interface StaffInfo {
|
||||
locationid?: number;
|
||||
locationname?: string;
|
||||
status?: string;
|
||||
/**
|
||||
* Whether they have ever chosen a password.
|
||||
*
|
||||
* Every person here is created with an empty one and emailed a link to set it.
|
||||
* Until that link is used they are in this list, in every branch picker, and
|
||||
* cannot sign in — and `status` does not say so, because an Active account with
|
||||
* no password is refused at the login screen like any other.
|
||||
*
|
||||
* So `false` is the row that needs an action: a lost invitation, waiting to be
|
||||
* sent again. Optional because a backend that predates the column sends
|
||||
* nothing, and the directory then simply does not make the claim.
|
||||
*/
|
||||
issetup?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user