@withpotter/pages-admin 0.13.0-next.102 → 0.13.0-next.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/_pos/olive-balance.js +40 -0
- package/dist/admin/_pos/olive-balance.js.map +1 -0
- package/dist/admin/_pos/olive-state.js +23 -0
- package/dist/admin/_pos/olive-state.js.map +1 -0
- package/dist/admin/_pos/pos-rules.js +2 -0
- package/dist/admin/_pos/pos-rules.js.map +1 -1
- package/dist/admin/_pos/tenant-operators.js +63 -0
- package/dist/admin/_pos/tenant-operators.js.map +1 -0
- package/dist/admin/_pos/tenant-outlets.js +77 -0
- package/dist/admin/_pos/tenant-outlets.js.map +1 -0
- package/dist/admin/_pos/tenant-pos-figures.js +82 -0
- package/dist/admin/_pos/tenant-pos-figures.js.map +1 -0
- package/dist/admin/_pos/tenant-requests.js +64 -0
- package/dist/admin/_pos/tenant-requests.js.map +1 -0
- package/dist/admin/_pos/tenant-transactions.js +145 -0
- package/dist/admin/_pos/tenant-transactions.js.map +1 -0
- package/dist/admin/_pos/use-pos.js +62 -1
- package/dist/admin/_pos/use-pos.js.map +1 -1
- package/dist/admin/pos/shifts-tab.js +15 -8
- package/dist/admin/pos/shifts-tab.js.map +1 -1
- package/dist/admin/tenants/[id]/tenant-pos.js +64 -16
- package/dist/admin/tenants/[id]/tenant-pos.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@withpotter/ui-kit";
|
|
5
|
+
import { oliveBalance } from "@withpotter/lib/admin-pos-olive";
|
|
6
|
+
import { Panel } from "../_ui";
|
|
7
|
+
import { OLIVE_NOTE, OliveProblem, naira } from "./olive-state";
|
|
8
|
+
import { useOliveAccount } from "./use-pos";
|
|
9
|
+
import pos from "./pos.module.css";
|
|
10
|
+
function OliveBalance({ tenantId }) {
|
|
11
|
+
const [asked, setAsked] = useState(false);
|
|
12
|
+
const q = useOliveAccount(tenantId, asked);
|
|
13
|
+
const account = oliveBalance(q.data);
|
|
14
|
+
return /* @__PURE__ */ jsx(Panel, { title: "POS account balance", height: "auto", children: /* @__PURE__ */ jsx("div", { className: pos.stack, children: !asked ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
15
|
+
/* @__PURE__ */ jsx("span", { className: "adm-caption", children: OLIVE_NOTE }),
|
|
16
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
|
|
17
|
+
Button,
|
|
18
|
+
{
|
|
19
|
+
size: "sm",
|
|
20
|
+
variant: "secondary",
|
|
21
|
+
onClick: () => setAsked(true),
|
|
22
|
+
children: "Show balance"
|
|
23
|
+
}
|
|
24
|
+
) })
|
|
25
|
+
] }) : q.isLoading ? /* @__PURE__ */ jsx("span", { className: "adm-caption", children: "Asking Olive\u2026" }) : q.isError ? /* @__PURE__ */ jsx(
|
|
26
|
+
OliveProblem,
|
|
27
|
+
{
|
|
28
|
+
error: q.error,
|
|
29
|
+
what: "balance",
|
|
30
|
+
onRetry: () => void q.refetch()
|
|
31
|
+
}
|
|
32
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
33
|
+
/* @__PURE__ */ jsx("strong", { children: naira(account.balanceMinor) }),
|
|
34
|
+
/* @__PURE__ */ jsx("span", { className: "adm-caption", children: [account.accountName, account.accountNumber].filter(Boolean).join(" \xB7 ") || "No account details" })
|
|
35
|
+
] }) }) });
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
OliveBalance
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=olive-balance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/olive-balance.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Button } from '@withpotter/ui-kit';\nimport { oliveBalance } from '@withpotter/lib/admin-pos-olive';\nimport { Panel } from '../_ui';\nimport { OLIVE_NOTE, OliveProblem, naira } from './olive-state';\nimport { useOliveAccount } from './use-pos';\nimport pos from './pos.module.css';\n\n/**\n * The business's POS account balance, from Olive. Not fetched until asked for: every look is a call to\n * Olive under the merchant's login and a line in the audit log, so opening the tab alone does neither.\n */\nexport function OliveBalance({ tenantId }: { tenantId: string }) {\n const [asked, setAsked] = useState(false);\n const q = useOliveAccount(tenantId, asked);\n const account = oliveBalance(q.data);\n\n return (\n <Panel title=\"POS account balance\" height=\"auto\">\n <div className={pos.stack}>\n {!asked ? (\n <>\n <span className=\"adm-caption\">{OLIVE_NOTE}</span>\n <div>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => setAsked(true)}\n >\n Show balance\n </Button>\n </div>\n </>\n ) : q.isLoading ? (\n <span className=\"adm-caption\">Asking Olive…</span>\n ) : q.isError ? (\n <OliveProblem\n error={q.error}\n what=\"balance\"\n onRetry={() => void q.refetch()}\n />\n ) : (\n <>\n <strong>{naira(account.balanceMinor)}</strong>\n <span className=\"adm-caption\">\n {[account.accountName, account.accountNumber]\n .filter(Boolean)\n .join(' · ') || 'No account details'}\n </span>\n </>\n )}\n </div>\n </Panel>\n );\n}\n"],"mappings":";AAuBU,mBACE,KADF;AArBV,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AACtB,SAAS,YAAY,cAAc,aAAa;AAChD,SAAS,uBAAuB;AAChC,OAAO,SAAS;AAMT,SAAS,aAAa,EAAE,SAAS,GAAyB;AAC/D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,IAAI,gBAAgB,UAAU,KAAK;AACzC,QAAM,UAAU,aAAa,EAAE,IAAI;AAEnC,SACE,oBAAC,SAAM,OAAM,uBAAsB,QAAO,QACxC,8BAAC,SAAI,WAAW,IAAI,OACjB,WAAC,QACA,iCACE;AAAA,wBAAC,UAAK,WAAU,eAAe,sBAAW;AAAA,IAC1C,oBAAC,SACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,SAAS,MAAM,SAAS,IAAI;AAAA,QAC7B;AAAA;AAAA,IAED,GACF;AAAA,KACF,IACE,EAAE,YACJ,oBAAC,UAAK,WAAU,eAAc,gCAAa,IACzC,EAAE,UACJ;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,EAAE;AAAA,MACT,MAAK;AAAA,MACL,SAAS,MAAM,KAAK,EAAE,QAAQ;AAAA;AAAA,EAChC,IAEA,iCACE;AAAA,wBAAC,YAAQ,gBAAM,QAAQ,YAAY,GAAE;AAAA,IACrC,oBAAC,UAAK,WAAU,eACb,WAAC,QAAQ,aAAa,QAAQ,aAAa,EACzC,OAAO,OAAO,EACd,KAAK,QAAK,KAAK,sBACpB;AAAA,KACF,GAEJ,GACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { EmptyNote } from "@withpotter/ui-kit";
|
|
4
|
+
import { formatMinor } from "@withpotter/lib/money-minor";
|
|
5
|
+
import { LoadError } from "./pos-parts";
|
|
6
|
+
import { isNotLinked } from "./pos-rules";
|
|
7
|
+
const naira = (minor) => minor === null ? "\u2013" : formatMinor(String(Math.trunc(minor)), "NGN");
|
|
8
|
+
function OliveProblem({
|
|
9
|
+
error,
|
|
10
|
+
what,
|
|
11
|
+
onRetry
|
|
12
|
+
}) {
|
|
13
|
+
if (isNotLinked(error))
|
|
14
|
+
return /* @__PURE__ */ jsx(EmptyNote, { well: true, children: `This business has not linked its Olive account, so its ${what} cannot be read yet.` });
|
|
15
|
+
return /* @__PURE__ */ jsx(LoadError, { error, what: `its ${what}`, onRetry });
|
|
16
|
+
}
|
|
17
|
+
const OLIVE_NOTE = "Read live from Olive under the business\u2019s own login. Each view is recorded in the audit log.";
|
|
18
|
+
export {
|
|
19
|
+
OLIVE_NOTE,
|
|
20
|
+
OliveProblem,
|
|
21
|
+
naira
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=olive-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/olive-state.tsx"],"sourcesContent":["'use client';\n\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport { formatMinor } from '@withpotter/lib/money-minor';\nimport { LoadError } from './pos-parts';\nimport { isNotLinked } from './pos-rules';\n\n/** Olive's amounts are kobo; the merchant's own POS page shows them as naira. */\nexport const naira = (minor: number | null) =>\n minor === null ? '–' : formatMinor(String(Math.trunc(minor)), 'NGN');\n\n/**\n * Why Olive's data is not on screen. A business that has not linked Olive is a state, not a fault, and\n * says so; anything else is a retry.\n */\nexport function OliveProblem({\n error,\n what,\n onRetry,\n}: {\n error: unknown;\n what: string;\n onRetry: () => void;\n}) {\n if (isNotLinked(error))\n return (\n <EmptyNote well>\n {`This business has not linked its Olive account, so its ${what} cannot be read yet.`}\n </EmptyNote>\n );\n\n return <LoadError error={error} what={`its ${what}`} onRetry={onRetry} />;\n}\n\nexport const OLIVE_NOTE =\n 'Read live from Olive under the business’s own login. Each view is recorded in the audit log.';\n"],"mappings":";AA0BM;AAxBN,SAAS,iBAAiB;AAC1B,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB;AAC1B,SAAS,mBAAmB;AAGrB,MAAM,QAAQ,CAAC,UACpB,UAAU,OAAO,WAAM,YAAY,OAAO,KAAK,MAAM,KAAK,CAAC,GAAG,KAAK;AAM9D,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,MAAI,YAAY,KAAK;AACnB,WACE,oBAAC,aAAU,MAAI,MACZ,oEAA0D,IAAI,wBACjE;AAGJ,SAAO,oBAAC,aAAU,OAAc,MAAM,OAAO,IAAI,IAAI,SAAkB;AACzE;AAEO,MAAM,aACX;","names":[]}
|
|
@@ -17,6 +17,7 @@ const statusOf = (e) => e?.status;
|
|
|
17
17
|
const isStale = (e) => statusOf(e) === 409;
|
|
18
18
|
const isForbidden = (e) => statusOf(e) === 403;
|
|
19
19
|
const isNotFound = (e) => statusOf(e) === 404;
|
|
20
|
+
const isNotLinked = (e) => statusOf(e) === 400;
|
|
20
21
|
const STALE_TEXT = "This changed since you opened it. Refresh to see where it stands now.";
|
|
21
22
|
function posErrorText(error, fallback) {
|
|
22
23
|
const status = statusOf(error);
|
|
@@ -35,6 +36,7 @@ export {
|
|
|
35
36
|
isForbidden,
|
|
36
37
|
isLongOpen,
|
|
37
38
|
isNotFound,
|
|
39
|
+
isNotLinked,
|
|
38
40
|
isStale,
|
|
39
41
|
posErrorText,
|
|
40
42
|
reasonError,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_pos/pos-rules.ts"],"sourcesContent":["import type {\n PosShift,\n PosTerminalStatus,\n} from '@withpotter/lib/admin-pos-types';\n\n/**\n * What an admin may do to a terminal or a shift, the forms' checks and what\n * an error says. Pure, so the rules the API enforces are tested here rather\n * than met as a 409.\n */\n\nexport const REASON_MAX = 300;\n\n/** A shift open this long is flagged; the API's own threshold. */\nexport const LONG_OPEN_HOURS = 12;\n\nexport type TerminalAction = 'suspend' | 'resume' | 'revoke';\n\n/**\n * Suspend needs active, resume needs suspended, revoke works from either\n * and is one-way. A revoked terminal has nothing left to do.\n */\nexport function terminalActions(status: PosTerminalStatus): TerminalAction[] {\n if (status === 'revoked') return [];\n\n return [status === 'active' ? 'suspend' : 'resume', 'revoke'];\n}\n\nexport const canCloseShift = (s: Pick<PosShift, 'status'>) =>\n s.status === 'open';\n\nexport const isLongOpen = (s: Pick<PosShift, 'status' | 'openHours'>) =>\n s.status === 'open' && (s.openHours ?? 0) >= LONG_OPEN_HOURS;\n\n/** An error for a reason, or undefined when it can be sent. */\nexport function reasonError(reason: string): string | undefined {\n const clean = reason.trim();\n\n if (!clean) return 'Say why, for the audit log.';\n\n if (clean.length > REASON_MAX)\n return `Keep the reason to ${REASON_MAX} characters.`;\n\n return undefined;\n}\n\nconst statusOf = (e: unknown) => (e as { status?: unknown } | null)?.status;\n\n/** The record changed under the admin: someone else acted first. */\nexport const isStale = (e: unknown) => statusOf(e) === 409;\n\nexport const isForbidden = (e: unknown) => statusOf(e) === 403;\n\nexport const isNotFound = (e: unknown) => statusOf(e) === 404;\n\nexport const STALE_TEXT =\n 'This changed since you opened it. Refresh to see where it stands now.';\n\n/**\n * An error in plain words. A 409 means the terminal or shift moved on, so\n * it says so and offers a refresh. A 400 keeps the server's line.\n */\nexport function posErrorText(error: unknown, fallback: string): string {\n const status = statusOf(error);\n const message = error instanceof Error ? error.message.trim() : '';\n\n if (status === 409) return STALE_TEXT;\n\n if (status === 403) return \"You don't have permission to do this.\";\n\n if (status === 404) return 'This no longer exists. Refresh the list.';\n\n if (status === 400 && message) return message;\n\n return fallback;\n}\n"],"mappings":"AAWO,MAAM,aAAa;AAGnB,MAAM,kBAAkB;AAQxB,SAAS,gBAAgB,QAA6C;AAC3E,MAAI,WAAW,UAAW,QAAO,CAAC;AAElC,SAAO,CAAC,WAAW,WAAW,YAAY,UAAU,QAAQ;AAC9D;AAEO,MAAM,gBAAgB,CAAC,MAC5B,EAAE,WAAW;AAER,MAAM,aAAa,CAAC,MACzB,EAAE,WAAW,WAAW,EAAE,aAAa,MAAM;AAGxC,SAAS,YAAY,QAAoC;AAC9D,QAAM,QAAQ,OAAO,KAAK;AAE1B,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI,MAAM,SAAS;AACjB,WAAO,sBAAsB,UAAU;AAEzC,SAAO;AACT;AAEA,MAAM,WAAW,CAAC,MAAgB,GAAmC;AAG9D,MAAM,UAAU,CAAC,MAAe,SAAS,CAAC,MAAM;AAEhD,MAAM,cAAc,CAAC,MAAe,SAAS,CAAC,MAAM;AAEpD,MAAM,aAAa,CAAC,MAAe,SAAS,CAAC,MAAM;
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/pos-rules.ts"],"sourcesContent":["import type {\n PosShift,\n PosTerminalStatus,\n} from '@withpotter/lib/admin-pos-types';\n\n/**\n * What an admin may do to a terminal or a shift, the forms' checks and what\n * an error says. Pure, so the rules the API enforces are tested here rather\n * than met as a 409.\n */\n\nexport const REASON_MAX = 300;\n\n/** A shift open this long is flagged; the API's own threshold. */\nexport const LONG_OPEN_HOURS = 12;\n\nexport type TerminalAction = 'suspend' | 'resume' | 'revoke';\n\n/**\n * Suspend needs active, resume needs suspended, revoke works from either\n * and is one-way. A revoked terminal has nothing left to do.\n */\nexport function terminalActions(status: PosTerminalStatus): TerminalAction[] {\n if (status === 'revoked') return [];\n\n return [status === 'active' ? 'suspend' : 'resume', 'revoke'];\n}\n\nexport const canCloseShift = (s: Pick<PosShift, 'status'>) =>\n s.status === 'open';\n\nexport const isLongOpen = (s: Pick<PosShift, 'status' | 'openHours'>) =>\n s.status === 'open' && (s.openHours ?? 0) >= LONG_OPEN_HOURS;\n\n/** An error for a reason, or undefined when it can be sent. */\nexport function reasonError(reason: string): string | undefined {\n const clean = reason.trim();\n\n if (!clean) return 'Say why, for the audit log.';\n\n if (clean.length > REASON_MAX)\n return `Keep the reason to ${REASON_MAX} characters.`;\n\n return undefined;\n}\n\nconst statusOf = (e: unknown) => (e as { status?: unknown } | null)?.status;\n\n/** The record changed under the admin: someone else acted first. */\nexport const isStale = (e: unknown) => statusOf(e) === 409;\n\nexport const isForbidden = (e: unknown) => statusOf(e) === 403;\n\nexport const isNotFound = (e: unknown) => statusOf(e) === 404;\n\n/** The business has not linked Olive (or must reconnect it): Olive's data cannot be read yet. */\nexport const isNotLinked = (e: unknown) => statusOf(e) === 400;\n\nexport const STALE_TEXT =\n 'This changed since you opened it. Refresh to see where it stands now.';\n\n/**\n * An error in plain words. A 409 means the terminal or shift moved on, so\n * it says so and offers a refresh. A 400 keeps the server's line.\n */\nexport function posErrorText(error: unknown, fallback: string): string {\n const status = statusOf(error);\n const message = error instanceof Error ? error.message.trim() : '';\n\n if (status === 409) return STALE_TEXT;\n\n if (status === 403) return \"You don't have permission to do this.\";\n\n if (status === 404) return 'This no longer exists. Refresh the list.';\n\n if (status === 400 && message) return message;\n\n return fallback;\n}\n"],"mappings":"AAWO,MAAM,aAAa;AAGnB,MAAM,kBAAkB;AAQxB,SAAS,gBAAgB,QAA6C;AAC3E,MAAI,WAAW,UAAW,QAAO,CAAC;AAElC,SAAO,CAAC,WAAW,WAAW,YAAY,UAAU,QAAQ;AAC9D;AAEO,MAAM,gBAAgB,CAAC,MAC5B,EAAE,WAAW;AAER,MAAM,aAAa,CAAC,MACzB,EAAE,WAAW,WAAW,EAAE,aAAa,MAAM;AAGxC,SAAS,YAAY,QAAoC;AAC9D,QAAM,QAAQ,OAAO,KAAK;AAE1B,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI,MAAM,SAAS;AACjB,WAAO,sBAAsB,UAAU;AAEzC,SAAO;AACT;AAEA,MAAM,WAAW,CAAC,MAAgB,GAAmC;AAG9D,MAAM,UAAU,CAAC,MAAe,SAAS,CAAC,MAAM;AAEhD,MAAM,cAAc,CAAC,MAAe,SAAS,CAAC,MAAM;AAEpD,MAAM,aAAa,CAAC,MAAe,SAAS,CAAC,MAAM;AAGnD,MAAM,cAAc,CAAC,MAAe,SAAS,CAAC,MAAM;AAEpD,MAAM,aACX;AAMK,SAAS,aAAa,OAAgB,UAA0B;AACrE,QAAM,SAAS,SAAS,KAAK;AAC7B,QAAM,UAAU,iBAAiB,QAAQ,MAAM,QAAQ,KAAK,IAAI;AAEhE,MAAI,WAAW,IAAK,QAAO;AAE3B,MAAI,WAAW,IAAK,QAAO;AAE3B,MAAI,WAAW,IAAK,QAAO;AAE3B,MAAI,WAAW,OAAO,QAAS,QAAO;AAEtC,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { TableRow, Th } from "@withpotter/ui-kit";
|
|
4
|
+
import { ListCard } from "../_ui/list-card";
|
|
5
|
+
import list from "../_ui/list-card.module.css";
|
|
6
|
+
import { LoadError } from "./pos-parts";
|
|
7
|
+
import { isForbidden, posErrorText } from "./pos-rules";
|
|
8
|
+
import { atTime } from "./pos-words";
|
|
9
|
+
import { useTenantRoster } from "./use-pos";
|
|
10
|
+
import pos from "./pos.module.css";
|
|
11
|
+
const sentence = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, " ") : s;
|
|
12
|
+
function shiftLimit(minutes) {
|
|
13
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return "\u2013";
|
|
14
|
+
const h = Math.floor(minutes / 60);
|
|
15
|
+
const m = minutes % 60;
|
|
16
|
+
if (!h) return `${m} min`;
|
|
17
|
+
return m ? `${h} h ${m} min` : `${h} h`;
|
|
18
|
+
}
|
|
19
|
+
function TenantOperators({ tenantId }) {
|
|
20
|
+
const { data, isLoading, isError, error, refetch } = useTenantRoster(tenantId);
|
|
21
|
+
if (isError && !data)
|
|
22
|
+
return /* @__PURE__ */ jsx(
|
|
23
|
+
LoadError,
|
|
24
|
+
{
|
|
25
|
+
error,
|
|
26
|
+
what: "this business's operators",
|
|
27
|
+
onRetry: () => void refetch()
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
return /* @__PURE__ */ jsx(
|
|
31
|
+
ListCard,
|
|
32
|
+
{
|
|
33
|
+
title: "Operators",
|
|
34
|
+
count: data?.length,
|
|
35
|
+
columns: 4,
|
|
36
|
+
minWidth: 640,
|
|
37
|
+
isLoading,
|
|
38
|
+
errorTitle: isForbidden(error) ? "" : posErrorText(error, "Operators did not load."),
|
|
39
|
+
isEmpty: data?.length === 0,
|
|
40
|
+
emptyTitle: "Nobody can sign in at this business's terminals yet.",
|
|
41
|
+
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
42
|
+
/* @__PURE__ */ jsx(Th, { children: "Operator" }),
|
|
43
|
+
/* @__PURE__ */ jsx(Th, { children: "Role" }),
|
|
44
|
+
/* @__PURE__ */ jsx(Th, { children: "Shift limit" }),
|
|
45
|
+
/* @__PURE__ */ jsx(Th, { children: "Updated" })
|
|
46
|
+
] }),
|
|
47
|
+
children: data?.map((o) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
48
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { className: pos.cellStack, children: [
|
|
49
|
+
/* @__PURE__ */ jsx("span", { className: pos.clip, children: o.name }),
|
|
50
|
+
/* @__PURE__ */ jsx("span", { className: pos.subtle, children: o.active ? o.signIn !== o.name ? `Signs in as ${o.signIn}` : "" : "Removed" })
|
|
51
|
+
] }) }),
|
|
52
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: sentence(o.role) }),
|
|
53
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: shiftLimit(o.shiftLimitMinutes) }),
|
|
54
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: atTime(o.updatedAt) })
|
|
55
|
+
] }, o.id))
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
TenantOperators,
|
|
61
|
+
shiftLimit
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=tenant-operators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/tenant-operators.tsx"],"sourcesContent":["'use client';\n\nimport { TableRow, Th } from '@withpotter/ui-kit';\nimport { ListCard } from '../_ui/list-card';\nimport list from '../_ui/list-card.module.css';\nimport { LoadError } from './pos-parts';\nimport { isForbidden, posErrorText } from './pos-rules';\nimport { atTime } from './pos-words';\nimport { useTenantRoster } from './use-pos';\nimport pos from './pos.module.css';\n\nconst sentence = (s: string) =>\n s ? s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, ' ') : s;\n\n/** \"168 h\", \"30 min\", \"1 h 30 min\". */\nexport function shiftLimit(minutes: number): string {\n if (!Number.isFinite(minutes) || minutes <= 0) return '–';\n\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n\n if (!h) return `${m} min`;\n\n return m ? `${h} h ${m} min` : `${h} h`;\n}\n\n/** The business's whole till roster: who can sign in, as what, and for how long a shift may run. */\nexport function TenantOperators({ tenantId }: { tenantId: string }) {\n const { data, isLoading, isError, error, refetch } =\n useTenantRoster(tenantId);\n\n if (isError && !data)\n return (\n <LoadError\n error={error}\n what=\"this business's operators\"\n onRetry={() => void refetch()}\n />\n );\n\n return (\n <ListCard\n title=\"Operators\"\n count={data?.length}\n columns={4}\n minWidth={640}\n isLoading={isLoading}\n errorTitle={\n isForbidden(error) ? '' : posErrorText(error, 'Operators did not load.')\n }\n isEmpty={data?.length === 0}\n emptyTitle=\"Nobody can sign in at this business's terminals yet.\"\n head={\n <tr>\n <Th>Operator</Th>\n <Th>Role</Th>\n <Th>Shift limit</Th>\n <Th>Updated</Th>\n </tr>\n }\n >\n {data?.map((o) => (\n <TableRow key={o.id}>\n <td>\n <span className={pos.cellStack}>\n <span className={pos.clip}>{o.name}</span>\n <span className={pos.subtle}>\n {o.active\n ? o.signIn !== o.name\n ? `Signs in as ${o.signIn}`\n : ''\n : 'Removed'}\n </span>\n </span>\n </td>\n <td className={`${list.nowrap} ${list.muted}`}>{sentence(o.role)}</td>\n <td className={`${list.nowrap} ${list.muted}`}>\n {shiftLimit(o.shiftLimitMinutes)}\n </td>\n <td className={`${list.nowrap} ${list.muted}`}>\n {atTime(o.updatedAt)}\n </td>\n </TableRow>\n ))}\n </ListCard>\n );\n}\n"],"mappings":";AAiCM,cAoBE,YApBF;AA/BN,SAAS,UAAU,UAAU;AAC7B,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,aAAa,oBAAoB;AAC1C,SAAS,cAAc;AACvB,SAAS,uBAAuB;AAChC,OAAO,SAAS;AAEhB,MAAM,WAAW,CAAC,MAChB,IAAI,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,EAAE,QAAQ,MAAM,GAAG,IAAI;AAG3D,SAAS,WAAW,SAAyB;AAClD,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,EAAG,QAAO;AAEtD,QAAM,IAAI,KAAK,MAAM,UAAU,EAAE;AACjC,QAAM,IAAI,UAAU;AAEpB,MAAI,CAAC,EAAG,QAAO,GAAG,CAAC;AAEnB,SAAO,IAAI,GAAG,CAAC,MAAM,CAAC,SAAS,GAAG,CAAC;AACrC;AAGO,SAAS,gBAAgB,EAAE,SAAS,GAAyB;AAClE,QAAM,EAAE,MAAM,WAAW,SAAS,OAAO,QAAQ,IAC/C,gBAAgB,QAAQ;AAE1B,MAAI,WAAW,CAAC;AACd,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,SAAS,MAAM,KAAK,QAAQ;AAAA;AAAA,IAC9B;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,MAAM;AAAA,MACb,SAAS;AAAA,MACT,UAAU;AAAA,MACV;AAAA,MACA,YACE,YAAY,KAAK,IAAI,KAAK,aAAa,OAAO,yBAAyB;AAAA,MAEzE,SAAS,MAAM,WAAW;AAAA,MAC1B,YAAW;AAAA,MACX,MACE,qBAAC,QACC;AAAA,4BAAC,MAAG,sBAAQ;AAAA,QACZ,oBAAC,MAAG,kBAAI;AAAA,QACR,oBAAC,MAAG,yBAAW;AAAA,QACf,oBAAC,MAAG,qBAAO;AAAA,SACb;AAAA,MAGD,gBAAM,IAAI,CAAC,MACV,qBAAC,YACC;AAAA,4BAAC,QACC,+BAAC,UAAK,WAAW,IAAI,WACnB;AAAA,8BAAC,UAAK,WAAW,IAAI,MAAO,YAAE,MAAK;AAAA,UACnC,oBAAC,UAAK,WAAW,IAAI,QAClB,YAAE,SACC,EAAE,WAAW,EAAE,OACb,eAAe,EAAE,MAAM,KACvB,KACF,WACN;AAAA,WACF,GACF;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IAAK,mBAAS,EAAE,IAAI,GAAE;AAAA,QACjE,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IACxC,qBAAW,EAAE,iBAAiB,GACjC;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IACxC,iBAAO,EAAE,SAAS,GACrB;AAAA,WAnBa,EAAE,EAoBjB,CACD;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { Badge, TableRow, Th } from "@withpotter/ui-kit";
|
|
5
|
+
import { oliveOutlets } from "@withpotter/lib/admin-pos-olive";
|
|
6
|
+
import { FilterSearch, Filters } from "../_ui";
|
|
7
|
+
import { ListCard } from "../_ui/list-card";
|
|
8
|
+
import list from "../_ui/list-card.module.css";
|
|
9
|
+
import { OLIVE_NOTE, OliveProblem } from "./olive-state";
|
|
10
|
+
import { atTime } from "./pos-words";
|
|
11
|
+
import { useOliveOutlets } from "./use-pos";
|
|
12
|
+
function TenantOutlets({ tenantId }) {
|
|
13
|
+
const { data, isLoading, isError, error, refetch } = useOliveOutlets(tenantId);
|
|
14
|
+
const [search, setSearch] = useState("");
|
|
15
|
+
const outlets = useMemo(() => oliveOutlets(data), [data]);
|
|
16
|
+
const shown = useMemo(() => {
|
|
17
|
+
const q = search.trim().toLowerCase();
|
|
18
|
+
if (!q) return outlets;
|
|
19
|
+
return outlets.filter(
|
|
20
|
+
(o) => [o.name, o.address, o.contact].some((v) => v?.toLowerCase().includes(q))
|
|
21
|
+
);
|
|
22
|
+
}, [outlets, search]);
|
|
23
|
+
if (isError)
|
|
24
|
+
return /* @__PURE__ */ jsx(
|
|
25
|
+
OliveProblem,
|
|
26
|
+
{
|
|
27
|
+
error,
|
|
28
|
+
what: "outlets",
|
|
29
|
+
onRetry: () => void refetch()
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
return /* @__PURE__ */ jsx(
|
|
33
|
+
ListCard,
|
|
34
|
+
{
|
|
35
|
+
title: "Outlets",
|
|
36
|
+
count: outlets.length,
|
|
37
|
+
columns: 6,
|
|
38
|
+
minWidth: 860,
|
|
39
|
+
isLoading,
|
|
40
|
+
errorTitle: "",
|
|
41
|
+
isEmpty: shown.length === 0,
|
|
42
|
+
emptyTitle: search ? "No outlets match." : "No outlets yet.",
|
|
43
|
+
controls: /* @__PURE__ */ jsxs(Filters, { label: "Filter outlets", children: [
|
|
44
|
+
/* @__PURE__ */ jsx(
|
|
45
|
+
FilterSearch,
|
|
46
|
+
{
|
|
47
|
+
value: search,
|
|
48
|
+
onChange: setSearch,
|
|
49
|
+
placeholder: "Search outlets\u2026",
|
|
50
|
+
label: "Search outlets"
|
|
51
|
+
}
|
|
52
|
+
),
|
|
53
|
+
/* @__PURE__ */ jsx("span", { className: "adm-caption", children: OLIVE_NOTE })
|
|
54
|
+
] }),
|
|
55
|
+
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
56
|
+
/* @__PURE__ */ jsx(Th, { children: "Outlet" }),
|
|
57
|
+
/* @__PURE__ */ jsx(Th, { children: "Address" }),
|
|
58
|
+
/* @__PURE__ */ jsx(Th, { children: "Contact" }),
|
|
59
|
+
/* @__PURE__ */ jsx(Th, { children: "Terminals" }),
|
|
60
|
+
/* @__PURE__ */ jsx(Th, { children: "Status" }),
|
|
61
|
+
/* @__PURE__ */ jsx(Th, { children: "Created" })
|
|
62
|
+
] }),
|
|
63
|
+
children: shown.map((o) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
64
|
+
/* @__PURE__ */ jsx("td", { children: o.name }),
|
|
65
|
+
/* @__PURE__ */ jsx("td", { className: list.muted, children: o.address ?? "\u2013" }),
|
|
66
|
+
/* @__PURE__ */ jsx("td", { className: list.muted, children: o.contact ?? "\u2013" }),
|
|
67
|
+
/* @__PURE__ */ jsx("td", { className: list.nowrap, children: o.terminalCount ?? "\u2013" }),
|
|
68
|
+
/* @__PURE__ */ jsx("td", { className: list.nowrap, children: /* @__PURE__ */ jsx(Badge, { variant: o.active ? "success" : "secondary", children: o.active ? "Active" : "Inactive" }) }),
|
|
69
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: o.createdAt ? atTime(o.createdAt) : "\u2013" })
|
|
70
|
+
] }, o.id))
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
TenantOutlets
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=tenant-outlets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/tenant-outlets.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo, useState } from 'react';\nimport { Badge, TableRow, Th } from '@withpotter/ui-kit';\nimport { oliveOutlets } from '@withpotter/lib/admin-pos-olive';\nimport { FilterSearch, Filters } from '../_ui';\nimport { ListCard } from '../_ui/list-card';\nimport list from '../_ui/list-card.module.css';\nimport { OLIVE_NOTE, OliveProblem } from './olive-state';\nimport { atTime } from './pos-words';\nimport { useOliveOutlets } from './use-pos';\n\n/** The business's outlets as Olive keeps them: where it sells, who to call, how many terminals. */\nexport function TenantOutlets({ tenantId }: { tenantId: string }) {\n const { data, isLoading, isError, error, refetch } =\n useOliveOutlets(tenantId);\n const [search, setSearch] = useState('');\n const outlets = useMemo(() => oliveOutlets(data), [data]);\n const shown = useMemo(() => {\n const q = search.trim().toLowerCase();\n\n if (!q) return outlets;\n\n return outlets.filter((o) =>\n [o.name, o.address, o.contact].some((v) => v?.toLowerCase().includes(q)),\n );\n }, [outlets, search]);\n\n if (isError)\n return (\n <OliveProblem\n error={error}\n what=\"outlets\"\n onRetry={() => void refetch()}\n />\n );\n\n return (\n <ListCard\n title=\"Outlets\"\n count={outlets.length}\n columns={6}\n minWidth={860}\n isLoading={isLoading}\n errorTitle=\"\"\n isEmpty={shown.length === 0}\n emptyTitle={search ? 'No outlets match.' : 'No outlets yet.'}\n controls={\n <Filters label=\"Filter outlets\">\n <FilterSearch\n value={search}\n onChange={setSearch}\n placeholder=\"Search outlets…\"\n label=\"Search outlets\"\n />\n <span className=\"adm-caption\">{OLIVE_NOTE}</span>\n </Filters>\n }\n head={\n <tr>\n <Th>Outlet</Th>\n <Th>Address</Th>\n <Th>Contact</Th>\n <Th>Terminals</Th>\n <Th>Status</Th>\n <Th>Created</Th>\n </tr>\n }\n >\n {shown.map((o) => (\n <TableRow key={o.id}>\n <td>{o.name}</td>\n <td className={list.muted}>{o.address ?? '–'}</td>\n <td className={list.muted}>{o.contact ?? '–'}</td>\n <td className={list.nowrap}>{o.terminalCount ?? '–'}</td>\n <td className={list.nowrap}>\n <Badge variant={o.active ? 'success' : 'secondary'}>\n {o.active ? 'Active' : 'Inactive'}\n </Badge>\n </td>\n <td className={`${list.nowrap} ${list.muted}`}>\n {o.createdAt ? atTime(o.createdAt) : '–'}\n </td>\n </TableRow>\n ))}\n </ListCard>\n );\n}\n"],"mappings":";AA8BM,cAkBE,YAlBF;AA5BN,SAAS,SAAS,gBAAgB;AAClC,SAAS,OAAO,UAAU,UAAU;AACpC,SAAS,oBAAoB;AAC7B,SAAS,cAAc,eAAe;AACtC,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,SAAS,YAAY,oBAAoB;AACzC,SAAS,cAAc;AACvB,SAAS,uBAAuB;AAGzB,SAAS,cAAc,EAAE,SAAS,GAAyB;AAChE,QAAM,EAAE,MAAM,WAAW,SAAS,OAAO,QAAQ,IAC/C,gBAAgB,QAAQ;AAC1B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,UAAU,QAAQ,MAAM,aAAa,IAAI,GAAG,CAAC,IAAI,CAAC;AACxD,QAAM,QAAQ,QAAQ,MAAM;AAC1B,UAAM,IAAI,OAAO,KAAK,EAAE,YAAY;AAEpC,QAAI,CAAC,EAAG,QAAO;AAEf,WAAO,QAAQ;AAAA,MAAO,CAAC,MACrB,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,GAAG,YAAY,EAAE,SAAS,CAAC,CAAC;AAAA,IACzE;AAAA,EACF,GAAG,CAAC,SAAS,MAAM,CAAC;AAEpB,MAAI;AACF,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,SAAS,MAAM,KAAK,QAAQ;AAAA;AAAA,IAC9B;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,QAAQ;AAAA,MACf,SAAS;AAAA,MACT,UAAU;AAAA,MACV;AAAA,MACA,YAAW;AAAA,MACX,SAAS,MAAM,WAAW;AAAA,MAC1B,YAAY,SAAS,sBAAsB;AAAA,MAC3C,UACE,qBAAC,WAAQ,OAAM,kBACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,UAAU;AAAA,YACV,aAAY;AAAA,YACZ,OAAM;AAAA;AAAA,QACR;AAAA,QACA,oBAAC,UAAK,WAAU,eAAe,sBAAW;AAAA,SAC5C;AAAA,MAEF,MACE,qBAAC,QACC;AAAA,4BAAC,MAAG,oBAAM;AAAA,QACV,oBAAC,MAAG,qBAAO;AAAA,QACX,oBAAC,MAAG,qBAAO;AAAA,QACX,oBAAC,MAAG,uBAAS;AAAA,QACb,oBAAC,MAAG,oBAAM;AAAA,QACV,oBAAC,MAAG,qBAAO;AAAA,SACb;AAAA,MAGD,gBAAM,IAAI,CAAC,MACV,qBAAC,YACC;AAAA,4BAAC,QAAI,YAAE,MAAK;AAAA,QACZ,oBAAC,QAAG,WAAW,KAAK,OAAQ,YAAE,WAAW,UAAI;AAAA,QAC7C,oBAAC,QAAG,WAAW,KAAK,OAAQ,YAAE,WAAW,UAAI;AAAA,QAC7C,oBAAC,QAAG,WAAW,KAAK,QAAS,YAAE,iBAAiB,UAAI;AAAA,QACpD,oBAAC,QAAG,WAAW,KAAK,QAClB,8BAAC,SAAM,SAAS,EAAE,SAAS,YAAY,aACpC,YAAE,SAAS,WAAW,YACzB,GACF;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IACxC,YAAE,YAAY,OAAO,EAAE,SAAS,IAAI,UACvC;AAAA,WAZa,EAAE,EAajB,CACD;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { StatCard } from "../_ui";
|
|
4
|
+
import { amountLines } from "./pos-money";
|
|
5
|
+
import { isLongOpen } from "./pos-rules";
|
|
6
|
+
import { inPeriod, onlineTone } from "./pos-words";
|
|
7
|
+
import styles from "./pos.module.css";
|
|
8
|
+
function TenantPosFigures({
|
|
9
|
+
data,
|
|
10
|
+
phrase,
|
|
11
|
+
currency = ""
|
|
12
|
+
}) {
|
|
13
|
+
const live = data.terminals.filter((t) => t.status !== "revoked");
|
|
14
|
+
const online = live.filter((t) => t.online).length;
|
|
15
|
+
const offlineOverDay = live.filter((t) => {
|
|
16
|
+
const tone = onlineTone(t);
|
|
17
|
+
return tone === "stale" || tone === "never";
|
|
18
|
+
}).length;
|
|
19
|
+
const suspended = live.filter((t) => t.status === "suspended").length;
|
|
20
|
+
const longOpen = data.openShifts.filter(isLongOpen).length;
|
|
21
|
+
const money = amountLines(data.takings.amounts, currency);
|
|
22
|
+
return /* @__PURE__ */ jsxs(
|
|
23
|
+
"section",
|
|
24
|
+
{
|
|
25
|
+
className: styles.figures,
|
|
26
|
+
"aria-label": "This business's POS at a glance",
|
|
27
|
+
children: [
|
|
28
|
+
/* @__PURE__ */ jsx(
|
|
29
|
+
StatCard,
|
|
30
|
+
{
|
|
31
|
+
compact: true,
|
|
32
|
+
label: "Terminals",
|
|
33
|
+
value: String(live.length),
|
|
34
|
+
sub: `${data.terminals.length - live.length} revoked`
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
/* @__PURE__ */ jsx(
|
|
38
|
+
StatCard,
|
|
39
|
+
{
|
|
40
|
+
compact: true,
|
|
41
|
+
label: "Online now",
|
|
42
|
+
value: String(online),
|
|
43
|
+
sub: `Of ${live.length}`
|
|
44
|
+
}
|
|
45
|
+
),
|
|
46
|
+
/* @__PURE__ */ jsx(
|
|
47
|
+
StatCard,
|
|
48
|
+
{
|
|
49
|
+
compact: true,
|
|
50
|
+
label: "Offline over a day",
|
|
51
|
+
value: String(offlineOverDay),
|
|
52
|
+
alert: offlineOverDay > 0
|
|
53
|
+
}
|
|
54
|
+
),
|
|
55
|
+
/* @__PURE__ */ jsx(StatCard, { compact: true, label: "Suspended", value: String(suspended) }),
|
|
56
|
+
/* @__PURE__ */ jsx(
|
|
57
|
+
StatCard,
|
|
58
|
+
{
|
|
59
|
+
compact: true,
|
|
60
|
+
label: "Open shifts",
|
|
61
|
+
value: String(data.openShifts.length),
|
|
62
|
+
sub: longOpen ? `${longOpen} open over 12 h` : void 0,
|
|
63
|
+
alert: longOpen > 0
|
|
64
|
+
}
|
|
65
|
+
),
|
|
66
|
+
/* @__PURE__ */ jsx(
|
|
67
|
+
StatCard,
|
|
68
|
+
{
|
|
69
|
+
compact: true,
|
|
70
|
+
label: "Card payments",
|
|
71
|
+
value: data.takings.count.toLocaleString("en-US"),
|
|
72
|
+
sub: money.length ? `${money.join(" \xB7 ")}, ${phrase}` : `None ${inPeriod(phrase)}`
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
TenantPosFigures
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=tenant-pos-figures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/tenant-pos-figures.tsx"],"sourcesContent":["'use client';\n\nimport type { TenantPos } from '@withpotter/lib/admin-pos-types';\nimport { StatCard } from '../_ui';\nimport { amountLines } from './pos-money';\nimport { isLongOpen } from './pos-rules';\nimport { inPeriod, onlineTone } from './pos-words';\nimport styles from './pos.module.css';\n\n/**\n * One business's POS at a glance, worked out from its own terminals and\n * shifts, so it matches what the merchant's POS page counts.\n */\nexport function TenantPosFigures({\n data,\n phrase,\n currency = '',\n}: {\n data: TenantPos;\n phrase: string;\n currency?: string;\n}) {\n const live = data.terminals.filter((t) => t.status !== 'revoked');\n const online = live.filter((t) => t.online).length;\n const offlineOverDay = live.filter((t) => {\n const tone = onlineTone(t);\n\n return tone === 'stale' || tone === 'never';\n }).length;\n const suspended = live.filter((t) => t.status === 'suspended').length;\n const longOpen = data.openShifts.filter(isLongOpen).length;\n const money = amountLines(data.takings.amounts, currency);\n\n return (\n <section\n className={styles.figures}\n aria-label=\"This business's POS at a glance\"\n >\n <StatCard\n compact\n label=\"Terminals\"\n value={String(live.length)}\n sub={`${data.terminals.length - live.length} revoked`}\n />\n <StatCard\n compact\n label=\"Online now\"\n value={String(online)}\n sub={`Of ${live.length}`}\n />\n <StatCard\n compact\n label=\"Offline over a day\"\n value={String(offlineOverDay)}\n alert={offlineOverDay > 0}\n />\n <StatCard compact label=\"Suspended\" value={String(suspended)} />\n <StatCard\n compact\n label=\"Open shifts\"\n value={String(data.openShifts.length)}\n sub={longOpen ? `${longOpen} open over 12 h` : undefined}\n alert={longOpen > 0}\n />\n <StatCard\n compact\n label=\"Card payments\"\n value={data.takings.count.toLocaleString('en-US')}\n sub={\n money.length\n ? `${money.join(' · ')}, ${phrase}`\n : `None ${inPeriod(phrase)}`\n }\n />\n </section>\n );\n}\n"],"mappings":";AAkCI,SAIE,KAJF;AA/BJ,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,kBAAkB;AAC3B,SAAS,UAAU,kBAAkB;AACrC,OAAO,YAAY;AAMZ,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAIG;AACD,QAAM,OAAO,KAAK,UAAU,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS;AAChE,QAAM,SAAS,KAAK,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE;AAC5C,QAAM,iBAAiB,KAAK,OAAO,CAAC,MAAM;AACxC,UAAM,OAAO,WAAW,CAAC;AAEzB,WAAO,SAAS,WAAW,SAAS;AAAA,EACtC,CAAC,EAAE;AACH,QAAM,YAAY,KAAK,OAAO,CAAC,MAAM,EAAE,WAAW,WAAW,EAAE;AAC/D,QAAM,WAAW,KAAK,WAAW,OAAO,UAAU,EAAE;AACpD,QAAM,QAAQ,YAAY,KAAK,QAAQ,SAAS,QAAQ;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,OAAO;AAAA,MAClB,cAAW;AAAA,MAEX;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,SAAO;AAAA,YACP,OAAM;AAAA,YACN,OAAO,OAAO,KAAK,MAAM;AAAA,YACzB,KAAK,GAAG,KAAK,UAAU,SAAS,KAAK,MAAM;AAAA;AAAA,QAC7C;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAO;AAAA,YACP,OAAM;AAAA,YACN,OAAO,OAAO,MAAM;AAAA,YACpB,KAAK,MAAM,KAAK,MAAM;AAAA;AAAA,QACxB;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAO;AAAA,YACP,OAAM;AAAA,YACN,OAAO,OAAO,cAAc;AAAA,YAC5B,OAAO,iBAAiB;AAAA;AAAA,QAC1B;AAAA,QACA,oBAAC,YAAS,SAAO,MAAC,OAAM,aAAY,OAAO,OAAO,SAAS,GAAG;AAAA,QAC9D;AAAA,UAAC;AAAA;AAAA,YACC,SAAO;AAAA,YACP,OAAM;AAAA,YACN,OAAO,OAAO,KAAK,WAAW,MAAM;AAAA,YACpC,KAAK,WAAW,GAAG,QAAQ,oBAAoB;AAAA,YAC/C,OAAO,WAAW;AAAA;AAAA,QACpB;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAO;AAAA,YACP,OAAM;AAAA,YACN,OAAO,KAAK,QAAQ,MAAM,eAAe,OAAO;AAAA,YAChD,KACE,MAAM,SACF,GAAG,MAAM,KAAK,QAAK,CAAC,KAAK,MAAM,KAC/B,QAAQ,SAAS,MAAM,CAAC;AAAA;AAAA,QAEhC;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge, TableRow, Th } from "@withpotter/ui-kit";
|
|
4
|
+
import { ListCard } from "../_ui/list-card";
|
|
5
|
+
import list from "../_ui/list-card.module.css";
|
|
6
|
+
import { LoadError } from "./pos-parts";
|
|
7
|
+
import { isForbidden, posErrorText } from "./pos-rules";
|
|
8
|
+
import { atTime, count } from "./pos-words";
|
|
9
|
+
import { useTenantTillRequests } from "./use-pos";
|
|
10
|
+
import pos from "./pos.module.css";
|
|
11
|
+
const LABEL = {
|
|
12
|
+
pending: "Pending",
|
|
13
|
+
approved: "Approved",
|
|
14
|
+
rejected: "Rejected",
|
|
15
|
+
fulfilled: "Provisioned"
|
|
16
|
+
};
|
|
17
|
+
const TONE = {
|
|
18
|
+
pending: "warning",
|
|
19
|
+
approved: "success",
|
|
20
|
+
rejected: "secondary",
|
|
21
|
+
fulfilled: "success"
|
|
22
|
+
};
|
|
23
|
+
function TenantRequests({ tenantId }) {
|
|
24
|
+
const { data, isLoading, isError, error, refetch } = useTenantTillRequests(tenantId);
|
|
25
|
+
if (isError && !data)
|
|
26
|
+
return /* @__PURE__ */ jsx(
|
|
27
|
+
LoadError,
|
|
28
|
+
{
|
|
29
|
+
error,
|
|
30
|
+
what: "this business's terminal requests",
|
|
31
|
+
onRetry: () => void refetch()
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
return /* @__PURE__ */ jsx(
|
|
35
|
+
ListCard,
|
|
36
|
+
{
|
|
37
|
+
title: "Terminal requests",
|
|
38
|
+
count: data?.length,
|
|
39
|
+
columns: 3,
|
|
40
|
+
minWidth: 560,
|
|
41
|
+
isLoading,
|
|
42
|
+
errorTitle: isForbidden(error) ? "" : posErrorText(error, "Requests did not load."),
|
|
43
|
+
isEmpty: data?.length === 0,
|
|
44
|
+
emptyTitle: "This business has not asked for any terminals.",
|
|
45
|
+
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
46
|
+
/* @__PURE__ */ jsx(Th, { children: "Request" }),
|
|
47
|
+
/* @__PURE__ */ jsx(Th, { children: "Status" }),
|
|
48
|
+
/* @__PURE__ */ jsx(Th, { children: "Requested" })
|
|
49
|
+
] }),
|
|
50
|
+
children: data?.map((r) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
51
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { className: pos.cellStack, children: [
|
|
52
|
+
/* @__PURE__ */ jsx("span", { className: pos.clip, children: `${count(r.quantity, "terminal")} requested` }),
|
|
53
|
+
(r.decisionNote || r.note) && /* @__PURE__ */ jsx("span", { className: pos.subtle, children: r.decisionNote ?? r.note })
|
|
54
|
+
] }) }),
|
|
55
|
+
/* @__PURE__ */ jsx("td", { className: list.nowrap, children: /* @__PURE__ */ jsx(Badge, { variant: TONE[r.status] ?? "secondary", children: LABEL[r.status] ?? r.status }) }),
|
|
56
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: atTime(r.createdAt) })
|
|
57
|
+
] }, r.id))
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
TenantRequests
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=tenant-requests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/tenant-requests.tsx"],"sourcesContent":["'use client';\n\nimport { Badge, TableRow, Th } from '@withpotter/ui-kit';\nimport type { TillRequestStatus } from '@withpotter/lib/admin-pos-types';\nimport { ListCard } from '../_ui/list-card';\nimport list from '../_ui/list-card.module.css';\nimport { LoadError } from './pos-parts';\nimport { isForbidden, posErrorText } from './pos-rules';\nimport { atTime, count } from './pos-words';\nimport { useTenantTillRequests } from './use-pos';\nimport pos from './pos.module.css';\n\nconst LABEL: Record<TillRequestStatus, string> = {\n pending: 'Pending',\n approved: 'Approved',\n rejected: 'Rejected',\n fulfilled: 'Provisioned',\n};\n\nconst TONE = {\n pending: 'warning',\n approved: 'success',\n rejected: 'secondary',\n fulfilled: 'success',\n} as const;\n\n/** The terminals the business has asked for, newest first, and where each request stands. */\nexport function TenantRequests({ tenantId }: { tenantId: string }) {\n const { data, isLoading, isError, error, refetch } =\n useTenantTillRequests(tenantId);\n\n if (isError && !data)\n return (\n <LoadError\n error={error}\n what=\"this business's terminal requests\"\n onRetry={() => void refetch()}\n />\n );\n\n return (\n <ListCard\n title=\"Terminal requests\"\n count={data?.length}\n columns={3}\n minWidth={560}\n isLoading={isLoading}\n errorTitle={\n isForbidden(error) ? '' : posErrorText(error, 'Requests did not load.')\n }\n isEmpty={data?.length === 0}\n emptyTitle=\"This business has not asked for any terminals.\"\n head={\n <tr>\n <Th>Request</Th>\n <Th>Status</Th>\n <Th>Requested</Th>\n </tr>\n }\n >\n {data?.map((r) => (\n <TableRow key={r.id}>\n <td>\n <span className={pos.cellStack}>\n <span className={pos.clip}>\n {`${count(r.quantity, 'terminal')} requested`}\n </span>\n {(r.decisionNote || r.note) && (\n <span className={pos.subtle}>{r.decisionNote ?? r.note}</span>\n )}\n </span>\n </td>\n <td className={list.nowrap}>\n <Badge variant={TONE[r.status] ?? 'secondary'}>\n {LABEL[r.status] ?? r.status}\n </Badge>\n </td>\n <td className={`${list.nowrap} ${list.muted}`}>\n {atTime(r.createdAt)}\n </td>\n </TableRow>\n ))}\n </ListCard>\n );\n}\n"],"mappings":";AAiCM,cAoBE,YApBF;AA/BN,SAAS,OAAO,UAAU,UAAU;AAEpC,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,aAAa,oBAAoB;AAC1C,SAAS,QAAQ,aAAa;AAC9B,SAAS,6BAA6B;AACtC,OAAO,SAAS;AAEhB,MAAM,QAA2C;AAAA,EAC/C,SAAS;AAAA,EACT,UAAU;AAAA,EACV,UAAU;AAAA,EACV,WAAW;AACb;AAEA,MAAM,OAAO;AAAA,EACX,SAAS;AAAA,EACT,UAAU;AAAA,EACV,UAAU;AAAA,EACV,WAAW;AACb;AAGO,SAAS,eAAe,EAAE,SAAS,GAAyB;AACjE,QAAM,EAAE,MAAM,WAAW,SAAS,OAAO,QAAQ,IAC/C,sBAAsB,QAAQ;AAEhC,MAAI,WAAW,CAAC;AACd,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,SAAS,MAAM,KAAK,QAAQ;AAAA;AAAA,IAC9B;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,MAAM;AAAA,MACb,SAAS;AAAA,MACT,UAAU;AAAA,MACV;AAAA,MACA,YACE,YAAY,KAAK,IAAI,KAAK,aAAa,OAAO,wBAAwB;AAAA,MAExE,SAAS,MAAM,WAAW;AAAA,MAC1B,YAAW;AAAA,MACX,MACE,qBAAC,QACC;AAAA,4BAAC,MAAG,qBAAO;AAAA,QACX,oBAAC,MAAG,oBAAM;AAAA,QACV,oBAAC,MAAG,uBAAS;AAAA,SACf;AAAA,MAGD,gBAAM,IAAI,CAAC,MACV,qBAAC,YACC;AAAA,4BAAC,QACC,+BAAC,UAAK,WAAW,IAAI,WACnB;AAAA,8BAAC,UAAK,WAAW,IAAI,MAClB,aAAG,MAAM,EAAE,UAAU,UAAU,CAAC,cACnC;AAAA,WACE,EAAE,gBAAgB,EAAE,SACpB,oBAAC,UAAK,WAAW,IAAI,QAAS,YAAE,gBAAgB,EAAE,MAAK;AAAA,WAE3D,GACF;AAAA,QACA,oBAAC,QAAG,WAAW,KAAK,QAClB,8BAAC,SAAM,SAAS,KAAK,EAAE,MAAM,KAAK,aAC/B,gBAAM,EAAE,MAAM,KAAK,EAAE,QACxB,GACF;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IACxC,iBAAO,EAAE,SAAS,GACrB;AAAA,WAlBa,EAAE,EAmBjB,CACD;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { Badge, Button, TableRow, Th } from "@withpotter/ui-kit";
|
|
5
|
+
import { oliveTransactions } from "@withpotter/lib/admin-pos-olive";
|
|
6
|
+
import { formatMinor } from "@withpotter/lib/money-minor";
|
|
7
|
+
import { FilterPress, Filters } from "../_ui";
|
|
8
|
+
import { ListCard } from "../_ui/list-card";
|
|
9
|
+
import list from "../_ui/list-card.module.css";
|
|
10
|
+
import { OLIVE_NOTE, OliveProblem, naira } from "./olive-state";
|
|
11
|
+
import { LoadError } from "./pos-parts";
|
|
12
|
+
import { isForbidden, posErrorText } from "./pos-rules";
|
|
13
|
+
import { atTime } from "./pos-words";
|
|
14
|
+
import { useOliveAccountTransactions, useTenantPayments } from "./use-pos";
|
|
15
|
+
import pos from "./pos.module.css";
|
|
16
|
+
function TenantTransactions({ tenantId }) {
|
|
17
|
+
const [source, setSource] = useState("card");
|
|
18
|
+
const switcher = /* @__PURE__ */ jsxs(Filters, { label: "Transactions source", children: [
|
|
19
|
+
/* @__PURE__ */ jsx(FilterPress, { on: source === "card", onToggle: () => setSource("card"), children: "Card payments" }),
|
|
20
|
+
/* @__PURE__ */ jsx(
|
|
21
|
+
FilterPress,
|
|
22
|
+
{
|
|
23
|
+
on: source === "account",
|
|
24
|
+
onToggle: () => setSource("account"),
|
|
25
|
+
children: "POS account"
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
] });
|
|
29
|
+
return source === "card" ? /* @__PURE__ */ jsx(CardPayments, { tenantId, controls: switcher }) : /* @__PURE__ */ jsx(AccountLines, { tenantId, controls: switcher });
|
|
30
|
+
}
|
|
31
|
+
function CardPayments({
|
|
32
|
+
tenantId,
|
|
33
|
+
controls
|
|
34
|
+
}) {
|
|
35
|
+
const q = useTenantPayments(tenantId);
|
|
36
|
+
const rows = useMemo(
|
|
37
|
+
() => q.data?.pages.flatMap((p) => p.rows) ?? [],
|
|
38
|
+
[q.data]
|
|
39
|
+
);
|
|
40
|
+
if (q.isError && !q.data)
|
|
41
|
+
return /* @__PURE__ */ jsx(
|
|
42
|
+
LoadError,
|
|
43
|
+
{
|
|
44
|
+
error: q.error,
|
|
45
|
+
what: "this business's card payments",
|
|
46
|
+
onRetry: () => void q.refetch()
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
return /* @__PURE__ */ jsx(
|
|
50
|
+
ListCard,
|
|
51
|
+
{
|
|
52
|
+
title: "Card payments",
|
|
53
|
+
count: q.hasNextPage ? void 0 : rows.length,
|
|
54
|
+
columns: 5,
|
|
55
|
+
minWidth: 760,
|
|
56
|
+
isLoading: q.isLoading,
|
|
57
|
+
errorTitle: isForbidden(q.error) ? "" : posErrorText(q.error, "Payments did not load."),
|
|
58
|
+
isEmpty: rows.length === 0,
|
|
59
|
+
emptyTitle: "No card payments yet.",
|
|
60
|
+
controls,
|
|
61
|
+
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
62
|
+
/* @__PURE__ */ jsx(Th, { children: "Terminal" }),
|
|
63
|
+
/* @__PURE__ */ jsx(Th, { children: "Reference" }),
|
|
64
|
+
/* @__PURE__ */ jsx(Th, { children: "Status" }),
|
|
65
|
+
/* @__PURE__ */ jsx(Th, { children: "Amount" }),
|
|
66
|
+
/* @__PURE__ */ jsx(Th, { children: "When" })
|
|
67
|
+
] }),
|
|
68
|
+
footer: q.hasNextPage ? /* @__PURE__ */ jsx("div", { className: pos.stack, children: /* @__PURE__ */ jsx(
|
|
69
|
+
Button,
|
|
70
|
+
{
|
|
71
|
+
size: "sm",
|
|
72
|
+
variant: "secondary",
|
|
73
|
+
disabled: q.isFetchingNextPage,
|
|
74
|
+
onClick: () => void q.fetchNextPage(),
|
|
75
|
+
children: q.isFetchingNextPage ? "Loading\u2026" : "Show more"
|
|
76
|
+
}
|
|
77
|
+
) }) : void 0,
|
|
78
|
+
children: rows.map((p) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
79
|
+
/* @__PURE__ */ jsx("td", { children: p.deviceLabel ?? "\u2013" }),
|
|
80
|
+
/* @__PURE__ */ jsx("td", { className: list.muted, children: p.reference ?? "\u2013" }),
|
|
81
|
+
/* @__PURE__ */ jsx("td", { className: list.nowrap, children: /* @__PURE__ */ jsx(Badge, { variant: p.status === "SUCCEEDED" ? "success" : "secondary", children: p.status.charAt(0) + p.status.slice(1).toLowerCase() }) }),
|
|
82
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${pos.num}`, children: `${p.direction === "out" ? "-" : ""}${formatMinor(
|
|
83
|
+
p.amountMinor,
|
|
84
|
+
p.currency
|
|
85
|
+
)}` }),
|
|
86
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: atTime(p.at) })
|
|
87
|
+
] }, p.id))
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function AccountLines({
|
|
92
|
+
tenantId,
|
|
93
|
+
controls
|
|
94
|
+
}) {
|
|
95
|
+
const q = useOliveAccountTransactions(tenantId);
|
|
96
|
+
const rows = useMemo(() => oliveTransactions(q.data), [q.data]);
|
|
97
|
+
if (q.isError)
|
|
98
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
99
|
+
controls,
|
|
100
|
+
/* @__PURE__ */ jsx(
|
|
101
|
+
OliveProblem,
|
|
102
|
+
{
|
|
103
|
+
error: q.error,
|
|
104
|
+
what: "account transactions",
|
|
105
|
+
onRetry: () => void q.refetch()
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
] });
|
|
109
|
+
return /* @__PURE__ */ jsx(
|
|
110
|
+
ListCard,
|
|
111
|
+
{
|
|
112
|
+
title: "POS account",
|
|
113
|
+
count: rows.length,
|
|
114
|
+
columns: 4,
|
|
115
|
+
minWidth: 680,
|
|
116
|
+
isLoading: q.isLoading,
|
|
117
|
+
errorTitle: "",
|
|
118
|
+
isEmpty: rows.length === 0,
|
|
119
|
+
emptyTitle: "No account transactions yet.",
|
|
120
|
+
controls: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
121
|
+
controls,
|
|
122
|
+
/* @__PURE__ */ jsx("span", { className: "adm-caption", children: OLIVE_NOTE })
|
|
123
|
+
] }),
|
|
124
|
+
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
125
|
+
/* @__PURE__ */ jsx(Th, { children: "Transaction" }),
|
|
126
|
+
/* @__PURE__ */ jsx(Th, { children: "Status" }),
|
|
127
|
+
/* @__PURE__ */ jsx(Th, { children: "Amount" }),
|
|
128
|
+
/* @__PURE__ */ jsx(Th, { children: "When" })
|
|
129
|
+
] }),
|
|
130
|
+
children: rows.map((t) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
131
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { className: pos.cellStack, children: [
|
|
132
|
+
/* @__PURE__ */ jsx("span", { className: pos.clip, children: t.summary }),
|
|
133
|
+
t.reference && /* @__PURE__ */ jsx("span", { className: pos.subtle, children: t.reference })
|
|
134
|
+
] }) }),
|
|
135
|
+
/* @__PURE__ */ jsx("td", { className: list.nowrap, children: t.status ?? "\u2013" }),
|
|
136
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${pos.num}`, children: `${t.direction === "in" ? "+" : t.direction === "out" ? "-" : ""}${naira(t.amountMinor)}` }),
|
|
137
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: t.at ? atTime(t.at) : "\u2013" })
|
|
138
|
+
] }, t.id))
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
TenantTransactions
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=tenant-transactions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/tenant-transactions.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo, useState } from 'react';\nimport { Badge, Button, TableRow, Th } from '@withpotter/ui-kit';\nimport { oliveTransactions } from '@withpotter/lib/admin-pos-olive';\nimport { formatMinor } from '@withpotter/lib/money-minor';\nimport { FilterPress, Filters } from '../_ui';\nimport { ListCard } from '../_ui/list-card';\nimport list from '../_ui/list-card.module.css';\nimport { OLIVE_NOTE, OliveProblem, naira } from './olive-state';\nimport { LoadError } from './pos-parts';\nimport { isForbidden, posErrorText } from './pos-rules';\nimport { atTime } from './pos-words';\nimport { useOliveAccountTransactions, useTenantPayments } from './use-pos';\nimport pos from './pos.module.css';\n\ntype Source = 'card' | 'account';\n\n/**\n * What moved: the card payments its terminals took (Box's own record), or the lines on its POS account\n * (Olive's). One switch; the other source is not fetched until it is chosen.\n */\nexport function TenantTransactions({ tenantId }: { tenantId: string }) {\n const [source, setSource] = useState<Source>('card');\n\n const switcher = (\n <Filters label=\"Transactions source\">\n <FilterPress on={source === 'card'} onToggle={() => setSource('card')}>\n Card payments\n </FilterPress>\n <FilterPress\n on={source === 'account'}\n onToggle={() => setSource('account')}\n >\n POS account\n </FilterPress>\n </Filters>\n );\n\n return source === 'card' ? (\n <CardPayments tenantId={tenantId} controls={switcher} />\n ) : (\n <AccountLines tenantId={tenantId} controls={switcher} />\n );\n}\n\nfunction CardPayments({\n tenantId,\n controls,\n}: {\n tenantId: string;\n controls: React.ReactNode;\n}) {\n const q = useTenantPayments(tenantId);\n const rows = useMemo(\n () => q.data?.pages.flatMap((p) => p.rows) ?? [],\n [q.data],\n );\n\n if (q.isError && !q.data)\n return (\n <LoadError\n error={q.error}\n what=\"this business's card payments\"\n onRetry={() => void q.refetch()}\n />\n );\n\n return (\n <ListCard\n title=\"Card payments\"\n count={q.hasNextPage ? undefined : rows.length}\n columns={5}\n minWidth={760}\n isLoading={q.isLoading}\n errorTitle={\n isForbidden(q.error)\n ? ''\n : posErrorText(q.error, 'Payments did not load.')\n }\n isEmpty={rows.length === 0}\n emptyTitle=\"No card payments yet.\"\n controls={controls}\n head={\n <tr>\n <Th>Terminal</Th>\n <Th>Reference</Th>\n <Th>Status</Th>\n <Th>Amount</Th>\n <Th>When</Th>\n </tr>\n }\n footer={\n q.hasNextPage ? (\n <div className={pos.stack}>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n disabled={q.isFetchingNextPage}\n onClick={() => void q.fetchNextPage()}\n >\n {q.isFetchingNextPage ? 'Loading…' : 'Show more'}\n </Button>\n </div>\n ) : undefined\n }\n >\n {rows.map((p) => (\n <TableRow key={p.id}>\n <td>{p.deviceLabel ?? '–'}</td>\n <td className={list.muted}>{p.reference ?? '–'}</td>\n <td className={list.nowrap}>\n <Badge variant={p.status === 'SUCCEEDED' ? 'success' : 'secondary'}>\n {p.status.charAt(0) + p.status.slice(1).toLowerCase()}\n </Badge>\n </td>\n <td className={`${list.nowrap} ${pos.num}`}>\n {`${p.direction === 'out' ? '-' : ''}${formatMinor(\n p.amountMinor,\n p.currency,\n )}`}\n </td>\n <td className={`${list.nowrap} ${list.muted}`}>{atTime(p.at)}</td>\n </TableRow>\n ))}\n </ListCard>\n );\n}\n\nfunction AccountLines({\n tenantId,\n controls,\n}: {\n tenantId: string;\n controls: React.ReactNode;\n}) {\n const q = useOliveAccountTransactions(tenantId);\n const rows = useMemo(() => oliveTransactions(q.data), [q.data]);\n\n if (q.isError)\n return (\n <>\n {controls}\n <OliveProblem\n error={q.error}\n what=\"account transactions\"\n onRetry={() => void q.refetch()}\n />\n </>\n );\n\n return (\n <ListCard\n title=\"POS account\"\n count={rows.length}\n columns={4}\n minWidth={680}\n isLoading={q.isLoading}\n errorTitle=\"\"\n isEmpty={rows.length === 0}\n emptyTitle=\"No account transactions yet.\"\n controls={\n <>\n {controls}\n <span className=\"adm-caption\">{OLIVE_NOTE}</span>\n </>\n }\n head={\n <tr>\n <Th>Transaction</Th>\n <Th>Status</Th>\n <Th>Amount</Th>\n <Th>When</Th>\n </tr>\n }\n >\n {rows.map((t) => (\n <TableRow key={t.id}>\n <td>\n <span className={pos.cellStack}>\n <span className={pos.clip}>{t.summary}</span>\n {t.reference && <span className={pos.subtle}>{t.reference}</span>}\n </span>\n </td>\n <td className={list.nowrap}>{t.status ?? '–'}</td>\n <td className={`${list.nowrap} ${pos.num}`}>\n {`${\n t.direction === 'in' ? '+' : t.direction === 'out' ? '-' : ''\n }${naira(t.amountMinor)}`}\n </td>\n <td className={`${list.nowrap} ${list.muted}`}>\n {t.at ? atTime(t.at) : '–'}\n </td>\n </TableRow>\n ))}\n </ListCard>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
keepPreviousData,
|
|
4
4
|
useMutation,
|
|
5
|
+
useInfiniteQuery,
|
|
5
6
|
useQuery,
|
|
6
7
|
useQueryClient
|
|
7
8
|
} from "@tanstack/react-query";
|
|
8
9
|
import { adminPosApi } from "@withpotter/lib/admin-pos-api";
|
|
9
|
-
import { isForbidden, isNotFound } from "./pos-rules";
|
|
10
|
+
import { isForbidden, isNotFound, isNotLinked } from "./pos-rules";
|
|
10
11
|
const POS_KEY = ["platform-admin", "pos"];
|
|
11
12
|
const key = (...parts) => [...POS_KEY, ...parts];
|
|
12
13
|
const retry = (count, error) => !isForbidden(error) && !isNotFound(error) && count < 1;
|
|
@@ -52,6 +53,60 @@ function usePosShifts(filters, enabled = true) {
|
|
|
52
53
|
retry
|
|
53
54
|
});
|
|
54
55
|
}
|
|
56
|
+
function useTenantRoster(tenantId, enabled = true) {
|
|
57
|
+
return useQuery({
|
|
58
|
+
queryKey: key("roster", tenantId),
|
|
59
|
+
queryFn: () => adminPosApi.roster(tenantId),
|
|
60
|
+
enabled,
|
|
61
|
+
retry
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function useTenantTillRequests(tenantId, enabled = true) {
|
|
65
|
+
return useQuery({
|
|
66
|
+
queryKey: key("till-requests", tenantId),
|
|
67
|
+
queryFn: () => adminPosApi.tillRequests(tenantId),
|
|
68
|
+
enabled,
|
|
69
|
+
retry
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function useTenantPayments(tenantId, enabled = true) {
|
|
73
|
+
return useInfiniteQuery({
|
|
74
|
+
queryKey: key("payments", tenantId),
|
|
75
|
+
queryFn: ({ pageParam }) => adminPosApi.payments({ tenantId, cursor: pageParam, limit: 25 }),
|
|
76
|
+
initialPageParam: void 0,
|
|
77
|
+
getNextPageParam: (last) => last.nextCursor ?? void 0,
|
|
78
|
+
enabled,
|
|
79
|
+
retry
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const oliveRetry = (count, error) => !isForbidden(error) && !isNotFound(error) && !isNotLinked(error) && count < 1;
|
|
83
|
+
function useOliveAccount(tenantId, enabled = true) {
|
|
84
|
+
return useQuery({
|
|
85
|
+
queryKey: key("olive", tenantId, "account"),
|
|
86
|
+
queryFn: () => adminPosApi.olive.account(tenantId),
|
|
87
|
+
enabled,
|
|
88
|
+
retry: oliveRetry,
|
|
89
|
+
staleTime: 6e4
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function useOliveAccountTransactions(tenantId, enabled = true) {
|
|
93
|
+
return useQuery({
|
|
94
|
+
queryKey: key("olive", tenantId, "account-transactions"),
|
|
95
|
+
queryFn: () => adminPosApi.olive.accountTransactions(tenantId),
|
|
96
|
+
enabled,
|
|
97
|
+
retry: oliveRetry,
|
|
98
|
+
staleTime: 6e4
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function useOliveOutlets(tenantId, enabled = true) {
|
|
102
|
+
return useQuery({
|
|
103
|
+
queryKey: key("olive", tenantId, "outlets"),
|
|
104
|
+
queryFn: () => adminPosApi.olive.outlets(tenantId),
|
|
105
|
+
enabled,
|
|
106
|
+
retry: oliveRetry,
|
|
107
|
+
staleTime: 6e4
|
|
108
|
+
});
|
|
109
|
+
}
|
|
55
110
|
function useTenantPos(tenantId, w, enabled = true) {
|
|
56
111
|
return useQuery({
|
|
57
112
|
queryKey: key("tenant", tenantId, w),
|
|
@@ -78,12 +133,18 @@ const useCloseShift = () => usePosWrite(
|
|
|
78
133
|
export {
|
|
79
134
|
POS_KEY,
|
|
80
135
|
useCloseShift,
|
|
136
|
+
useOliveAccount,
|
|
137
|
+
useOliveAccountTransactions,
|
|
138
|
+
useOliveOutlets,
|
|
81
139
|
usePosHistory,
|
|
82
140
|
usePosShifts,
|
|
83
141
|
usePosSummary,
|
|
84
142
|
usePosTerminal,
|
|
85
143
|
usePosTerminals,
|
|
144
|
+
useTenantPayments,
|
|
86
145
|
useTenantPos,
|
|
146
|
+
useTenantRoster,
|
|
147
|
+
useTenantTillRequests,
|
|
87
148
|
useTerminalStatus
|
|
88
149
|
};
|
|
89
150
|
//# sourceMappingURL=use-pos.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_pos/use-pos.ts"],"sourcesContent":["'use client';\n\nimport {\n keepPreviousData,\n useMutation,\n useQuery,\n useQueryClient,\n} from '@tanstack/react-query';\nimport { adminPosApi, type TerminalAct } from '@withpotter/lib/admin-pos-api';\nimport type {\n ShiftFilters,\n TerminalFilters,\n} from '@withpotter/lib/admin-pos-query';\nimport type { PosWindow } from '@withpotter/lib/admin-pos-types';\nimport { isForbidden, isNotFound } from './pos-rules';\n\n/** Every POS query sits under this, so one action refreshes them all. */\nexport const POS_KEY = ['platform-admin', 'pos'] as const;\n\nconst key = (...parts: unknown[]) => [...POS_KEY, ...parts];\n\n/** A 403 or 404 will not change on a retry; anything else gets one more go. */\nconst retry = (count: number, error: unknown) =>\n !isForbidden(error) && !isNotFound(error) && count < 1;\n\nexport function usePosSummary(w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('summary', w),\n queryFn: () => adminPosApi.summary(w),\n enabled,\n retry,\n });\n}\n\nexport function usePosTerminals(filters: TerminalFilters, enabled = true) {\n return useQuery({\n queryKey: key('terminals', filters),\n queryFn: () => adminPosApi.terminals(filters),\n placeholderData: keepPreviousData,\n enabled,\n retry,\n });\n}\n\nexport function usePosTerminal(id: string, w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('terminal', id, w),\n queryFn: () => adminPosApi.terminal(id, w),\n enabled,\n retry,\n });\n}\n\nexport function usePosHistory(id: string, enabled = true) {\n return useQuery({\n queryKey: key('history', id),\n queryFn: () => adminPosApi.history(id),\n enabled,\n retry,\n });\n}\n\nexport function usePosShifts(filters: ShiftFilters, enabled = true) {\n return useQuery({\n queryKey: key('shifts', filters),\n queryFn: () => adminPosApi.shifts(filters),\n placeholderData: keepPreviousData,\n enabled,\n retry,\n });\n}\n\nexport function useTenantPos(tenantId: string, w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('tenant', tenantId, w),\n queryFn: () => adminPosApi.tenant(tenantId, w),\n enabled,\n retry,\n });\n}\n\n/**\n * Every action can move the figures, the lists and a business's tab, so\n * all of POS is refetched after it, a 409 included: that means someone\n * else acted first. The refetch is not awaited, so the caller's own\n * `onSuccess` (the toast, closing the form) is not held back by it.\n */\nfunction usePosWrite<V, R>(fn: (v: V) => Promise<R>) {\n const client = useQueryClient();\n\n return useMutation({\n mutationFn: fn,\n onSettled: () => {\n void client.invalidateQueries({ queryKey: POS_KEY });\n },\n });\n}\n\nexport const useTerminalStatus = () =>\n usePosWrite((v: { id: string; act: TerminalAct; reason: string }) =>\n adminPosApi.setTerminalStatus(v.id, v.act, v.reason),\n );\n\nexport const useCloseShift = () =>\n usePosWrite((v: { id: string; reason: string }) =>\n adminPosApi.closeShift(v.id, v.reason),\n );\n"],"mappings":";AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,mBAAqC;AAM9C,SAAS,aAAa,
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/use-pos.ts"],"sourcesContent":["'use client';\n\nimport {\n keepPreviousData,\n useMutation,\n useInfiniteQuery,\n useQuery,\n useQueryClient,\n} from '@tanstack/react-query';\nimport { adminPosApi, type TerminalAct } from '@withpotter/lib/admin-pos-api';\nimport type {\n ShiftFilters,\n TerminalFilters,\n} from '@withpotter/lib/admin-pos-query';\nimport type { PosWindow } from '@withpotter/lib/admin-pos-types';\nimport { isForbidden, isNotFound, isNotLinked } from './pos-rules';\n\n/** Every POS query sits under this, so one action refreshes them all. */\nexport const POS_KEY = ['platform-admin', 'pos'] as const;\n\nconst key = (...parts: unknown[]) => [...POS_KEY, ...parts];\n\n/** A 403 or 404 will not change on a retry; anything else gets one more go. */\nconst retry = (count: number, error: unknown) =>\n !isForbidden(error) && !isNotFound(error) && count < 1;\n\nexport function usePosSummary(w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('summary', w),\n queryFn: () => adminPosApi.summary(w),\n enabled,\n retry,\n });\n}\n\nexport function usePosTerminals(filters: TerminalFilters, enabled = true) {\n return useQuery({\n queryKey: key('terminals', filters),\n queryFn: () => adminPosApi.terminals(filters),\n placeholderData: keepPreviousData,\n enabled,\n retry,\n });\n}\n\nexport function usePosTerminal(id: string, w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('terminal', id, w),\n queryFn: () => adminPosApi.terminal(id, w),\n enabled,\n retry,\n });\n}\n\nexport function usePosHistory(id: string, enabled = true) {\n return useQuery({\n queryKey: key('history', id),\n queryFn: () => adminPosApi.history(id),\n enabled,\n retry,\n });\n}\n\nexport function usePosShifts(filters: ShiftFilters, enabled = true) {\n return useQuery({\n queryKey: key('shifts', filters),\n queryFn: () => adminPosApi.shifts(filters),\n placeholderData: keepPreviousData,\n enabled,\n retry,\n });\n}\n\nexport function useTenantRoster(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('roster', tenantId),\n queryFn: () => adminPosApi.roster(tenantId),\n enabled,\n retry,\n });\n}\n\nexport function useTenantTillRequests(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('till-requests', tenantId),\n queryFn: () => adminPosApi.tillRequests(tenantId),\n enabled,\n retry,\n });\n}\n\n/** Card payments, newest first, a page at a time. */\nexport function useTenantPayments(tenantId: string, enabled = true) {\n return useInfiniteQuery({\n queryKey: key('payments', tenantId),\n queryFn: ({ pageParam }) =>\n adminPosApi.payments({ tenantId, cursor: pageParam, limit: 25 }),\n initialPageParam: undefined as string | undefined,\n getNextPageParam: (last) => last.nextCursor ?? undefined,\n enabled,\n retry,\n });\n}\n\n/**\n * What the business sees from Olive. Read live and audited on the server, so these are fetched only\n * when their tab or card is on screen, never in the background, and a 400 (Olive not linked) is not retried.\n */\nconst oliveRetry = (count: number, error: unknown) =>\n !isForbidden(error) && !isNotFound(error) && !isNotLinked(error) && count < 1;\n\nexport function useOliveAccount(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('olive', tenantId, 'account'),\n queryFn: () => adminPosApi.olive.account(tenantId),\n enabled,\n retry: oliveRetry,\n staleTime: 60_000,\n });\n}\n\nexport function useOliveAccountTransactions(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('olive', tenantId, 'account-transactions'),\n queryFn: () => adminPosApi.olive.accountTransactions(tenantId),\n enabled,\n retry: oliveRetry,\n staleTime: 60_000,\n });\n}\n\nexport function useOliveOutlets(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('olive', tenantId, 'outlets'),\n queryFn: () => adminPosApi.olive.outlets(tenantId),\n enabled,\n retry: oliveRetry,\n staleTime: 60_000,\n });\n}\n\nexport function useTenantPos(tenantId: string, w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('tenant', tenantId, w),\n queryFn: () => adminPosApi.tenant(tenantId, w),\n enabled,\n retry,\n });\n}\n\n/**\n * Every action can move the figures, the lists and a business's tab, so\n * all of POS is refetched after it, a 409 included: that means someone\n * else acted first. The refetch is not awaited, so the caller's own\n * `onSuccess` (the toast, closing the form) is not held back by it.\n */\nfunction usePosWrite<V, R>(fn: (v: V) => Promise<R>) {\n const client = useQueryClient();\n\n return useMutation({\n mutationFn: fn,\n onSettled: () => {\n void client.invalidateQueries({ queryKey: POS_KEY });\n },\n });\n}\n\nexport const useTerminalStatus = () =>\n usePosWrite((v: { id: string; act: TerminalAct; reason: string }) =>\n adminPosApi.setTerminalStatus(v.id, v.act, v.reason),\n );\n\nexport const useCloseShift = () =>\n usePosWrite((v: { id: string; reason: string }) =>\n adminPosApi.closeShift(v.id, v.reason),\n );\n"],"mappings":"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","names":[]}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Button, RemovableChip } from "@withpotter/ui-kit";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
POS_PAGE_SIZE
|
|
7
|
+
} from "@withpotter/lib/admin-pos-query";
|
|
6
8
|
import { FilterPress, FilterSelect, Filters } from "../_ui";
|
|
7
9
|
import { ListPager } from "../_ui/list-card";
|
|
8
10
|
import { LONG_OPEN_HOURS } from "../_pos/pos-rules";
|
|
@@ -17,8 +19,13 @@ const STATUS_OPTIONS = [
|
|
|
17
19
|
label
|
|
18
20
|
}))
|
|
19
21
|
];
|
|
20
|
-
function ShiftsTab({
|
|
21
|
-
|
|
22
|
+
function ShiftsTab({
|
|
23
|
+
currency = "",
|
|
24
|
+
tenantId: fixedTenantId,
|
|
25
|
+
title = "Shifts"
|
|
26
|
+
}) {
|
|
27
|
+
const [paramTenantId, setTenantId] = useTenantParam();
|
|
28
|
+
const tenantId = fixedTenantId ?? paramTenantId;
|
|
22
29
|
const [status, setStatus] = useState("");
|
|
23
30
|
const [longOpen, setLongOpen] = useState(false);
|
|
24
31
|
const [page, setPage] = useState(1);
|
|
@@ -30,7 +37,7 @@ function ShiftsTab({ currency = "" }) {
|
|
|
30
37
|
limit: POS_PAGE_SIZE
|
|
31
38
|
};
|
|
32
39
|
const { data, isLoading, isError, error, refetch } = usePosShifts(filters);
|
|
33
|
-
const narrowed = !!(status || longOpen || tenantId);
|
|
40
|
+
const narrowed = !!(status || longOpen || tenantId && !fixedTenantId);
|
|
34
41
|
const reset = (fn) => {
|
|
35
42
|
fn();
|
|
36
43
|
setPage(1);
|
|
@@ -38,10 +45,10 @@ function ShiftsTab({ currency = "" }) {
|
|
|
38
45
|
return /* @__PURE__ */ jsx(
|
|
39
46
|
ShiftsTable,
|
|
40
47
|
{
|
|
41
|
-
title
|
|
48
|
+
title,
|
|
42
49
|
rows: data?.data,
|
|
43
50
|
total: data?.total,
|
|
44
|
-
withBusiness:
|
|
51
|
+
withBusiness: !fixedTenantId,
|
|
45
52
|
currency,
|
|
46
53
|
isLoading,
|
|
47
54
|
isError,
|
|
@@ -49,7 +56,7 @@ function ShiftsTab({ currency = "" }) {
|
|
|
49
56
|
onRetry: () => void refetch(),
|
|
50
57
|
emptyTitle: narrowed ? "No shifts match." : "No shifts yet.",
|
|
51
58
|
controls: /* @__PURE__ */ jsxs(Filters, { label: "Filter shifts", children: [
|
|
52
|
-
tenantId && /* @__PURE__ */ jsx(
|
|
59
|
+
tenantId && !fixedTenantId && /* @__PURE__ */ jsx(
|
|
53
60
|
RemovableChip,
|
|
54
61
|
{
|
|
55
62
|
onRemove: () => reset(() => setTenantId("")),
|
|
@@ -86,7 +93,7 @@ function ShiftsTab({ currency = "" }) {
|
|
|
86
93
|
onClick: () => reset(() => {
|
|
87
94
|
setStatus("");
|
|
88
95
|
setLongOpen(false);
|
|
89
|
-
setTenantId("");
|
|
96
|
+
if (!fixedTenantId) setTenantId("");
|
|
90
97
|
}),
|
|
91
98
|
children: "Clear filters"
|
|
92
99
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/pos/shifts-tab.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Button, RemovableChip } from '@withpotter/ui-kit';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/pos/shifts-tab.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Button, RemovableChip } from '@withpotter/ui-kit';\nimport {\n POS_PAGE_SIZE,\n type ShiftFilters,\n} from '@withpotter/lib/admin-pos-query';\nimport type { PosShiftStatus } from '@withpotter/lib/admin-pos-types';\nimport { FilterPress, FilterSelect, Filters } from '../_ui';\nimport { ListPager } from '../_ui/list-card';\nimport { LONG_OPEN_HOURS } from '../_pos/pos-rules';\nimport { SHIFT_STATUS_LABEL } from '../_pos/pos-words';\nimport { ShiftsTable } from '../_pos/shifts-table';\nimport { usePosShifts } from '../_pos/use-pos';\nimport { useTenantParam } from '../_pos/use-tenant-param';\n\nconst STATUS_OPTIONS = [\n { value: '', label: 'Any status' },\n ...Object.entries(SHIFT_STATUS_LABEL).map(([value, label]) => ({\n value,\n label,\n })),\n];\n\n/** Open and recent shifts across every business, newest first. */\nexport function ShiftsTab({\n currency = '',\n tenantId: fixedTenantId,\n title = 'Shifts',\n}: {\n currency?: string;\n /** Fixes the list to one business (its own page); otherwise `?tenant=` narrows it. */\n tenantId?: string;\n title?: string;\n}) {\n const [paramTenantId, setTenantId] = useTenantParam();\n const tenantId = fixedTenantId ?? paramTenantId;\n const [status, setStatus] = useState<PosShiftStatus | ''>('');\n const [longOpen, setLongOpen] = useState(false);\n const [page, setPage] = useState(1);\n const filters: ShiftFilters = {\n status: longOpen ? 'open' : status,\n openOverHours: longOpen ? LONG_OPEN_HOURS : undefined,\n tenantId: tenantId || undefined,\n page,\n limit: POS_PAGE_SIZE,\n };\n const { data, isLoading, isError, error, refetch } = usePosShifts(filters);\n const narrowed = !!(status || longOpen || (tenantId && !fixedTenantId));\n const reset = (fn: () => void) => {\n fn();\n setPage(1);\n };\n\n return (\n <ShiftsTable\n title={title}\n rows={data?.data}\n total={data?.total}\n withBusiness={!fixedTenantId}\n currency={currency}\n isLoading={isLoading}\n isError={isError}\n error={error}\n onRetry={() => void refetch()}\n emptyTitle={narrowed ? 'No shifts match.' : 'No shifts yet.'}\n controls={\n <Filters label=\"Filter shifts\">\n {tenantId && !fixedTenantId && (\n <RemovableChip\n onRemove={() => reset(() => setTenantId(''))}\n removeLabel=\"Show every business\"\n >\n One business\n </RemovableChip>\n )}\n <FilterPress\n on={longOpen}\n onToggle={() => reset(() => setLongOpen((v) => !v))}\n >\n Open over {LONG_OPEN_HOURS} hours\n </FilterPress>\n {!longOpen && (\n <FilterSelect\n label=\"Status\"\n value={status}\n onChange={(v) => reset(() => setStatus(v as PosShiftStatus | ''))}\n options={STATUS_OPTIONS}\n />\n )}\n {narrowed && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() =>\n reset(() => {\n setStatus('');\n setLongOpen(false);\n if (!fixedTenantId) setTenantId('');\n })\n }\n >\n Clear filters\n </Button>\n )}\n </Filters>\n }\n footer={\n data && data.total > 0 ? (\n <ListPager\n page={page}\n totalPages={Math.max(1, data.totalPages)}\n total={data.total}\n noun={data.total === 1 ? 'shift' : 'shifts'}\n onPage={setPage}\n />\n ) : undefined\n }\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,7 +3,14 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
5
|
import { EmptyNote } from "@withpotter/ui-kit";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AdmLoading,
|
|
8
|
+
CurrencyFilter,
|
|
9
|
+
PageTabs,
|
|
10
|
+
currenciesIn,
|
|
11
|
+
useCurrencyParam,
|
|
12
|
+
useTabParam
|
|
13
|
+
} from "../../_ui";
|
|
7
14
|
import { ListCard } from "../../_ui/list-card";
|
|
8
15
|
import { LoadError, MoneyLines } from "../../_pos/pos-parts";
|
|
9
16
|
import { count } from "../../_pos/pos-words";
|
|
@@ -13,10 +20,32 @@ import { usePosAccess } from "../../_pos/use-pos-access";
|
|
|
13
20
|
import { usePosPeriod } from "../../_pos/use-pos-period";
|
|
14
21
|
import { useTenantPos } from "../../_pos/use-pos";
|
|
15
22
|
import pos from "../../_pos/pos.module.css";
|
|
16
|
-
import {
|
|
23
|
+
import { OliveBalance } from "../../_pos/olive-balance";
|
|
24
|
+
import { TenantOperators } from "../../_pos/tenant-operators";
|
|
25
|
+
import { TenantOutlets } from "../../_pos/tenant-outlets";
|
|
26
|
+
import { TenantRequests } from "../../_pos/tenant-requests";
|
|
27
|
+
import { TenantTransactions } from "../../_pos/tenant-transactions";
|
|
28
|
+
import { TenantPosFigures } from "../../_pos/tenant-pos-figures";
|
|
29
|
+
import { ShiftsTab } from "../../pos/shifts-tab";
|
|
30
|
+
import {
|
|
31
|
+
TerminalHead,
|
|
32
|
+
TerminalRow,
|
|
33
|
+
terminalColumns
|
|
34
|
+
} from "../../pos/terminal-row";
|
|
35
|
+
const VIEWS = [
|
|
36
|
+
{ id: "overview", label: "Overview" },
|
|
37
|
+
{ id: "terminals", label: "Terminals" },
|
|
38
|
+
{ id: "sessions", label: "Sessions" },
|
|
39
|
+
{ id: "operators", label: "Operators" },
|
|
40
|
+
{ id: "outlets", label: "Outlets" },
|
|
41
|
+
{ id: "transactions", label: "Transactions" },
|
|
42
|
+
{ id: "requests", label: "Requests" }
|
|
43
|
+
];
|
|
44
|
+
const VIEW_KEYS = VIEWS.map((v) => v.id);
|
|
17
45
|
function TenantPos({ tenantId }) {
|
|
18
46
|
const router = useRouter();
|
|
19
47
|
const { isLoading: accessLoading, canRead } = usePosAccess();
|
|
48
|
+
const [view, setView] = useTabParam(VIEW_KEYS, "view");
|
|
20
49
|
const { period, phrase, control } = usePosPeriod();
|
|
21
50
|
const [currency, setCurrency] = useCurrencyParam();
|
|
22
51
|
const query = useTenantPos(tenantId, period, canRead);
|
|
@@ -46,7 +75,14 @@ function TenantPos({ tenantId }) {
|
|
|
46
75
|
/* @__PURE__ */ jsx(MoneyLines, { amounts: data.takings.amounts, currency }),
|
|
47
76
|
` taken, ${phrase} \xB7 `
|
|
48
77
|
] }) : null,
|
|
49
|
-
/* @__PURE__ */ jsx(
|
|
78
|
+
/* @__PURE__ */ jsx(
|
|
79
|
+
Link,
|
|
80
|
+
{
|
|
81
|
+
href: `/admin/pos?tenant=${tenantId}`,
|
|
82
|
+
className: "text-brand-500 hover:underline",
|
|
83
|
+
children: "Open in POS"
|
|
84
|
+
}
|
|
85
|
+
)
|
|
50
86
|
] }),
|
|
51
87
|
/* @__PURE__ */ jsxs("div", { className: "adm-toolbar", children: [
|
|
52
88
|
/* @__PURE__ */ jsx(
|
|
@@ -54,13 +90,31 @@ function TenantPos({ tenantId }) {
|
|
|
54
90
|
{
|
|
55
91
|
value: currency,
|
|
56
92
|
onChange: setCurrency,
|
|
57
|
-
currencies: currenciesIn(
|
|
93
|
+
currencies: currenciesIn(
|
|
94
|
+
data.takings.amounts.map((a) => a.currency)
|
|
95
|
+
)
|
|
58
96
|
}
|
|
59
97
|
),
|
|
60
98
|
control
|
|
61
99
|
] })
|
|
62
100
|
] }),
|
|
63
|
-
/* @__PURE__ */ jsx(
|
|
101
|
+
/* @__PURE__ */ jsx(PageTabs, { tabs: VIEWS, active: view, onChange: setView }),
|
|
102
|
+
view === "overview" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
103
|
+
/* @__PURE__ */ jsx(TenantPosFigures, { data, phrase, currency }),
|
|
104
|
+
/* @__PURE__ */ jsx(OliveBalance, { tenantId }),
|
|
105
|
+
/* @__PURE__ */ jsx(
|
|
106
|
+
ShiftsTable,
|
|
107
|
+
{
|
|
108
|
+
title: "Open shifts",
|
|
109
|
+
rows: data.openShifts,
|
|
110
|
+
total: data.openShifts.length,
|
|
111
|
+
withBusiness: false,
|
|
112
|
+
currency,
|
|
113
|
+
emptyTitle: "No till is open right now."
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
] }),
|
|
117
|
+
view === "terminals" && /* @__PURE__ */ jsx(
|
|
64
118
|
ListCard,
|
|
65
119
|
{
|
|
66
120
|
title: "Terminals",
|
|
@@ -82,17 +136,11 @@ function TenantPos({ tenantId }) {
|
|
|
82
136
|
))
|
|
83
137
|
}
|
|
84
138
|
),
|
|
85
|
-
/* @__PURE__ */ jsx(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
total: data.openShifts.length,
|
|
91
|
-
withBusiness: false,
|
|
92
|
-
currency,
|
|
93
|
-
emptyTitle: "No till is open right now."
|
|
94
|
-
}
|
|
95
|
-
)
|
|
139
|
+
view === "sessions" && /* @__PURE__ */ jsx(ShiftsTab, { tenantId, currency, title: "Sessions" }),
|
|
140
|
+
view === "operators" && /* @__PURE__ */ jsx(TenantOperators, { tenantId }),
|
|
141
|
+
view === "outlets" && /* @__PURE__ */ jsx(TenantOutlets, { tenantId }),
|
|
142
|
+
view === "transactions" && /* @__PURE__ */ jsx(TenantTransactions, { tenantId }),
|
|
143
|
+
view === "requests" && /* @__PURE__ */ jsx(TenantRequests, { tenantId })
|
|
96
144
|
] });
|
|
97
145
|
}
|
|
98
146
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/admin/tenants/%5Bid%5D/tenant-pos.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { useRouter } from 'next/navigation';\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../../src/admin/tenants/%5Bid%5D/tenant-pos.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { useRouter } from 'next/navigation';\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageTabs,\n currenciesIn,\n useCurrencyParam,\n useTabParam,\n} from '../../_ui';\nimport { ListCard } from '../../_ui/list-card';\nimport { LoadError, MoneyLines } from '../../_pos/pos-parts';\nimport { count } from '../../_pos/pos-words';\nimport { terminalHref } from '../../_pos/shift-row';\nimport { ShiftsTable } from '../../_pos/shifts-table';\nimport { usePosAccess } from '../../_pos/use-pos-access';\nimport { usePosPeriod } from '../../_pos/use-pos-period';\nimport { useTenantPos } from '../../_pos/use-pos';\nimport pos from '../../_pos/pos.module.css';\nimport { OliveBalance } from '../../_pos/olive-balance';\nimport { TenantOperators } from '../../_pos/tenant-operators';\nimport { TenantOutlets } from '../../_pos/tenant-outlets';\nimport { TenantRequests } from '../../_pos/tenant-requests';\nimport { TenantTransactions } from '../../_pos/tenant-transactions';\nimport { TenantPosFigures } from '../../_pos/tenant-pos-figures';\nimport { ShiftsTab } from '../../pos/shifts-tab';\nimport {\n TerminalHead,\n TerminalRow,\n terminalColumns,\n} from '../../pos/terminal-row';\n\nconst VIEWS = [\n { id: 'overview', label: 'Overview' },\n { id: 'terminals', label: 'Terminals' },\n { id: 'sessions', label: 'Sessions' },\n { id: 'operators', label: 'Operators' },\n { id: 'outlets', label: 'Outlets' },\n { id: 'transactions', label: 'Transactions' },\n { id: 'requests', label: 'Requests' },\n] as const;\n\nconst VIEW_KEYS = VIEWS.map((v) => v.id);\n\n/**\n * Merchant 360 → POS: the business's terminals, the shifts open now, and\n * what its terminals took in the period, per currency.\n */\nexport function TenantPos({ tenantId }: { tenantId: string }) {\n const router = useRouter();\n const { isLoading: accessLoading, canRead } = usePosAccess();\n const [view, setView] = useTabParam(VIEW_KEYS, 'view');\n const { period, phrase, control } = usePosPeriod();\n const [currency, setCurrency] = useCurrencyParam();\n const query = useTenantPos(tenantId, period, canRead);\n const data = query.data;\n\n if (accessLoading || (canRead && query.isLoading))\n return <AdmLoading label=\"Loading POS…\" />;\n\n if (!canRead)\n return (\n <EmptyNote well>\n {\"You don't have permission to see this business's POS.\"}\n </EmptyNote>\n );\n\n if (query.isError || !data)\n return (\n <LoadError\n error={query.error}\n what=\"this business's POS\"\n onRetry={() => void query.refetch()}\n />\n );\n\n const live = data.terminals.filter((t) => t.status !== 'revoked').length;\n const revoked = data.terminals.length - live;\n\n return (\n <div className={pos.stack}>\n <div className={pos.toolbarRow}>\n <span className=\"adm-caption\">\n {`${count(live, 'terminal')} in use`}\n {revoked ? `, ${revoked} revoked` : ''}\n {' · '}\n {data.takings.count > 0 ? (\n <>\n <MoneyLines amounts={data.takings.amounts} currency={currency} />\n {` taken, ${phrase} · `}\n </>\n ) : null}\n <Link\n href={`/admin/pos?tenant=${tenantId}`}\n className=\"text-brand-500 hover:underline\"\n >\n Open in POS\n </Link>\n </span>\n <div className=\"adm-toolbar\">\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currenciesIn(\n data.takings.amounts.map((a) => a.currency),\n )}\n />\n {control}\n </div>\n </div>\n\n <PageTabs tabs={VIEWS} active={view} onChange={setView} />\n\n {view === 'overview' && (\n <>\n <TenantPosFigures data={data} phrase={phrase} currency={currency} />\n <OliveBalance tenantId={tenantId} />\n <ShiftsTable\n title=\"Open shifts\"\n rows={data.openShifts}\n total={data.openShifts.length}\n withBusiness={false}\n currency={currency}\n emptyTitle=\"No till is open right now.\"\n />\n </>\n )}\n\n {view === 'terminals' && (\n <ListCard\n title=\"Terminals\"\n count={data.terminals.length}\n columns={terminalColumns(false)}\n minWidth={860}\n errorTitle=\"\"\n isEmpty={data.terminals.length === 0}\n emptyTitle=\"This business has no terminals.\"\n head={<TerminalHead withBusiness={false} />}\n >\n {data.terminals.map((t) => (\n <TerminalRow\n key={t.id}\n t={t}\n currency={currency}\n onOpen={() => router.push(terminalHref(t.id))}\n />\n ))}\n </ListCard>\n )}\n\n {view === 'sessions' && (\n <ShiftsTab tenantId={tenantId} currency={currency} title=\"Sessions\" />\n )}\n\n {view === 'operators' && <TenantOperators tenantId={tenantId} />}\n\n {view === 'outlets' && <TenantOutlets tenantId={tenantId} />}\n\n {view === 'transactions' && <TenantTransactions tenantId={tenantId} />}\n\n {view === 'requests' && <TenantRequests tenantId={tenantId} />}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withpotter/pages-admin",
|
|
3
|
-
"version": "0.13.0-next.
|
|
3
|
+
"version": "0.13.0-next.104",
|
|
4
4
|
"description": "Box console platform-admin routes: overview, revenue, accounts, tenants, admins, roles, audit.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@radix-ui/react-popover": "^1.1.23",
|
|
33
33
|
"@tanstack/react-query": "^5.90.20",
|
|
34
|
-
"@withpotter/lib": "^0.13.0-next.
|
|
35
|
-
"@withpotter/platform-admin": "^0.13.0-next.
|
|
36
|
-
"@withpotter/ui-kit": "^0.13.0-next.
|
|
34
|
+
"@withpotter/lib": "^0.13.0-next.104",
|
|
35
|
+
"@withpotter/platform-admin": "^0.13.0-next.104",
|
|
36
|
+
"@withpotter/ui-kit": "^0.13.0-next.104",
|
|
37
37
|
"lucide-react": "^0.562.0",
|
|
38
38
|
"motion": "^12.42.2"
|
|
39
39
|
},
|