@withpotter/pages-admin 0.13.0-next.113 → 0.13.0-next.114
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/payments-model.js +55 -0
- package/dist/admin/_pos/payments-model.js.map +1 -0
- package/dist/admin/_pos/use-pos.js +10 -4
- package/dist/admin/_pos/use-pos.js.map +1 -1
- package/dist/admin/pos/transactions-tab.js +168 -25
- package/dist/admin/pos/transactions-tab.js.map +1 -1
- package/dist/admin/revenue/merchant-payments-view.js +1 -22
- package/dist/admin/revenue/merchant-payments-view.js.map +1 -1
- package/dist/admin/revenue/print-summary-button.js +21 -28
- package/dist/admin/revenue/print-summary-button.js.map +1 -1
- package/dist/admin/revenue/revenue-overview.js +92 -76
- package/dist/admin/revenue/revenue-overview.js.map +1 -1
- package/dist/admin/revenue/revenue-workspace.js +0 -4
- package/dist/admin/revenue/revenue-workspace.js.map +1 -1
- package/dist/admin/revenue/subscriptions-panel.js +1 -1
- package/dist/admin/revenue/subscriptions-panel.js.map +1 -1
- package/dist/admin/revenue/summary-download.js +18 -14
- package/dist/admin/revenue/summary-download.js.map +1 -1
- package/package.json +4 -4
- package/dist/admin/revenue/merchants-view.js +0 -99
- package/dist/admin/revenue/merchants-view.js.map +0 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { formatMinor } from "@withpotter/lib/money-minor";
|
|
2
|
+
const PAYMENT_STATUS_OPTIONS = [
|
|
3
|
+
{ value: "", label: "Any result" },
|
|
4
|
+
{ value: "SUCCEEDED", label: "Succeeded" },
|
|
5
|
+
{ value: "FAILED", label: "Failed" },
|
|
6
|
+
{ value: "PROCESSING", label: "Processing" },
|
|
7
|
+
{ value: "CREATED", label: "Created" },
|
|
8
|
+
{ value: "CANCELLED", label: "Cancelled" }
|
|
9
|
+
];
|
|
10
|
+
const statusWord = (status) => status.charAt(0) + status.slice(1).toLowerCase();
|
|
11
|
+
function matchesSearch(payment, business, search) {
|
|
12
|
+
const words = search.toLowerCase().split(/\s+/).filter(Boolean);
|
|
13
|
+
if (words.length === 0) return true;
|
|
14
|
+
const haystack = [
|
|
15
|
+
business,
|
|
16
|
+
payment.deviceLabel,
|
|
17
|
+
payment.reference,
|
|
18
|
+
statusWord(payment.status),
|
|
19
|
+
payment.currency,
|
|
20
|
+
formatMinor(payment.amountMinor, payment.currency)
|
|
21
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
22
|
+
return words.every((w) => haystack.includes(w));
|
|
23
|
+
}
|
|
24
|
+
const PAYMENTS_CSV_HEAD = [
|
|
25
|
+
"When",
|
|
26
|
+
"Business",
|
|
27
|
+
"Terminal",
|
|
28
|
+
"Reference",
|
|
29
|
+
"Result",
|
|
30
|
+
"Direction",
|
|
31
|
+
"Currency",
|
|
32
|
+
"Amount",
|
|
33
|
+
"Amount (minor units)"
|
|
34
|
+
];
|
|
35
|
+
function paymentRow(p, business) {
|
|
36
|
+
return [
|
|
37
|
+
p.at,
|
|
38
|
+
business ?? "",
|
|
39
|
+
p.deviceLabel ?? "",
|
|
40
|
+
p.reference ?? "",
|
|
41
|
+
statusWord(p.status),
|
|
42
|
+
p.direction === "out" ? "Out" : "In",
|
|
43
|
+
p.currency,
|
|
44
|
+
formatMinor(p.amountMinor, p.currency),
|
|
45
|
+
p.amountMinor
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
PAYMENTS_CSV_HEAD,
|
|
50
|
+
PAYMENT_STATUS_OPTIONS,
|
|
51
|
+
matchesSearch,
|
|
52
|
+
paymentRow,
|
|
53
|
+
statusWord
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=payments-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/payments-model.ts"],"sourcesContent":["import { formatMinor } from '@withpotter/lib/money-minor';\nimport type { PosPayment } from '@withpotter/lib/admin-pos-types';\n\n/** The results the API can filter on, in the words the table shows. */\nexport const PAYMENT_STATUS_OPTIONS = [\n { value: '', label: 'Any result' },\n { value: 'SUCCEEDED', label: 'Succeeded' },\n { value: 'FAILED', label: 'Failed' },\n { value: 'PROCESSING', label: 'Processing' },\n { value: 'CREATED', label: 'Created' },\n { value: 'CANCELLED', label: 'Cancelled' },\n] as const;\n\n/** \"SUCCEEDED\" as \"Succeeded\". */\nexport const statusWord = (status: string) =>\n status.charAt(0) + status.slice(1).toLowerCase();\n\n/**\n * Whether a payment matches a search: every word typed must appear in its business, terminal, reference, result or\n * amount, in any order and any case. An empty search matches everything.\n */\nexport function matchesSearch(\n payment: PosPayment,\n business: string | undefined,\n search: string,\n): boolean {\n const words = search.toLowerCase().split(/\\s+/).filter(Boolean);\n\n if (words.length === 0) return true;\n\n const haystack = [\n business,\n payment.deviceLabel,\n payment.reference,\n statusWord(payment.status),\n payment.currency,\n formatMinor(payment.amountMinor, payment.currency),\n ]\n .filter(Boolean)\n .join(' ')\n .toLowerCase();\n\n return words.every((w) => haystack.includes(w));\n}\n\nexport const PAYMENTS_CSV_HEAD = [\n 'When',\n 'Business',\n 'Terminal',\n 'Reference',\n 'Result',\n 'Direction',\n 'Currency',\n 'Amount',\n 'Amount (minor units)',\n];\n\n/** One CSV row per payment: the time as ISO text, the amount as shown and in minor units. */\nexport function paymentRow(\n p: PosPayment,\n business: string | undefined,\n): (string | number)[] {\n return [\n p.at,\n business ?? '',\n p.deviceLabel ?? '',\n p.reference ?? '',\n statusWord(p.status),\n p.direction === 'out' ? 'Out' : 'In',\n p.currency,\n formatMinor(p.amountMinor, p.currency),\n p.amountMinor,\n ];\n}\n"],"mappings":"AAAA,SAAS,mBAAmB;AAIrB,MAAM,yBAAyB;AAAA,EACpC,EAAE,OAAO,IAAI,OAAO,aAAa;AAAA,EACjC,EAAE,OAAO,aAAa,OAAO,YAAY;AAAA,EACzC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,cAAc,OAAO,aAAa;AAAA,EAC3C,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,EACrC,EAAE,OAAO,aAAa,OAAO,YAAY;AAC3C;AAGO,MAAM,aAAa,CAAC,WACzB,OAAO,OAAO,CAAC,IAAI,OAAO,MAAM,CAAC,EAAE,YAAY;AAM1C,SAAS,cACd,SACA,UACA,QACS;AACT,QAAM,QAAQ,OAAO,YAAY,EAAE,MAAM,KAAK,EAAE,OAAO,OAAO;AAE9D,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,WAAW;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,WAAW,QAAQ,MAAM;AAAA,IACzB,QAAQ;AAAA,IACR,YAAY,QAAQ,aAAa,QAAQ,QAAQ;AAAA,EACnD,EACG,OAAO,OAAO,EACd,KAAK,GAAG,EACR,YAAY;AAEf,SAAO,MAAM,MAAM,CAAC,MAAM,SAAS,SAAS,CAAC,CAAC;AAChD;AAEO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,SAAS,WACd,GACA,UACqB;AACrB,SAAO;AAAA,IACL,EAAE;AAAA,IACF,YAAY;AAAA,IACZ,EAAE,eAAe;AAAA,IACjB,EAAE,aAAa;AAAA,IACf,WAAW,EAAE,MAAM;AAAA,IACnB,EAAE,cAAc,QAAQ,QAAQ;AAAA,IAChC,EAAE;AAAA,IACF,YAAY,EAAE,aAAa,EAAE,QAAQ;AAAA,IACrC,EAAE;AAAA,EACJ;AACF;","names":[]}
|
|
@@ -69,17 +69,23 @@ function useTenantTillRequests(tenantId, enabled = true) {
|
|
|
69
69
|
retry
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
-
function usePosPayments(
|
|
72
|
+
function usePosPayments(filters = {}, enabled = true) {
|
|
73
|
+
const { tenantId, status } = filters;
|
|
73
74
|
return useInfiniteQuery({
|
|
74
|
-
queryKey: key("payments", tenantId
|
|
75
|
-
queryFn: ({ pageParam }) => adminPosApi.payments({
|
|
75
|
+
queryKey: key("payments", tenantId || "all", status || "any"),
|
|
76
|
+
queryFn: ({ pageParam }) => adminPosApi.payments({
|
|
77
|
+
tenantId: tenantId || void 0,
|
|
78
|
+
status: status || void 0,
|
|
79
|
+
cursor: pageParam,
|
|
80
|
+
limit: 25
|
|
81
|
+
}),
|
|
76
82
|
initialPageParam: void 0,
|
|
77
83
|
getNextPageParam: (last) => last.nextCursor ?? void 0,
|
|
78
84
|
enabled,
|
|
79
85
|
retry
|
|
80
86
|
});
|
|
81
87
|
}
|
|
82
|
-
const useTenantPayments = (tenantId, enabled = true) => usePosPayments(tenantId, enabled);
|
|
88
|
+
const useTenantPayments = (tenantId, enabled = true) => usePosPayments({ tenantId }, enabled);
|
|
83
89
|
const oliveRetry = (count, error) => !isForbidden(error) && !isNotFound(error) && !isNotLinked(error) && count < 1;
|
|
84
90
|
function useOliveAccount(tenantId, enabled = true) {
|
|
85
91
|
return useQuery({
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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\nexport interface PaymentFilters {\n /** One business; every business when empty. */\n tenantId?: string;\n /** CREATED, PROCESSING, SUCCEEDED, FAILED, CANCELLED or UNKNOWN; any when empty. */\n status?: string;\n}\n\n/** Card payments, newest first, a page at a time, narrowed by business and result. */\nexport function usePosPayments(filters: PaymentFilters = {}, enabled = true) {\n const { tenantId, status } = filters;\n\n return useInfiniteQuery({\n queryKey: key('payments', tenantId || 'all', status || 'any'),\n queryFn: ({ pageParam }) =>\n adminPosApi.payments({\n tenantId: tenantId || undefined,\n status: status || undefined,\n cursor: pageParam,\n limit: 25,\n }),\n initialPageParam: undefined as string | undefined,\n getNextPageParam: (last) => last.nextCursor ?? undefined,\n enabled,\n retry,\n });\n}\n\nexport const useTenantPayments = (tenantId: string, enabled = true) =>\n usePosPayments({ tenantId }, enabled);\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":";AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,mBAAqC;AAM9C,SAAS,aAAa,YAAY,mBAAmB;AAG9C,MAAM,UAAU,CAAC,kBAAkB,KAAK;AAE/C,MAAM,MAAM,IAAI,UAAqB,CAAC,GAAG,SAAS,GAAG,KAAK;AAG1D,MAAM,QAAQ,CAAC,OAAe,UAC5B,CAAC,YAAY,KAAK,KAAK,CAAC,WAAW,KAAK,KAAK,QAAQ;AAEhD,SAAS,cAAc,GAAc,UAAU,MAAM;AAC1D,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,WAAW,CAAC;AAAA,IAC1B,SAAS,MAAM,YAAY,QAAQ,CAAC;AAAA,IACpC;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAEO,SAAS,gBAAgB,SAA0B,UAAU,MAAM;AACxE,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,aAAa,OAAO;AAAA,IAClC,SAAS,MAAM,YAAY,UAAU,OAAO;AAAA,IAC5C,iBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAEO,SAAS,eAAe,IAAY,GAAc,UAAU,MAAM;AACvE,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,YAAY,IAAI,CAAC;AAAA,IAC/B,SAAS,MAAM,YAAY,SAAS,IAAI,CAAC;AAAA,IACzC;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAEO,SAAS,cAAc,IAAY,UAAU,MAAM;AACxD,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,WAAW,EAAE;AAAA,IAC3B,SAAS,MAAM,YAAY,QAAQ,EAAE;AAAA,IACrC;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAEO,SAAS,aAAa,SAAuB,UAAU,MAAM;AAClE,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,UAAU,OAAO;AAAA,IAC/B,SAAS,MAAM,YAAY,OAAO,OAAO;AAAA,IACzC,iBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAEO,SAAS,gBAAgB,UAAkB,UAAU,MAAM;AAChE,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,UAAU,QAAQ;AAAA,IAChC,SAAS,MAAM,YAAY,OAAO,QAAQ;AAAA,IAC1C;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAEO,SAAS,sBAAsB,UAAkB,UAAU,MAAM;AACtE,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,iBAAiB,QAAQ;AAAA,IACvC,SAAS,MAAM,YAAY,aAAa,QAAQ;AAAA,IAChD;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAUO,SAAS,eAAe,UAA0B,CAAC,GAAG,UAAU,MAAM;AAC3E,QAAM,EAAE,UAAU,OAAO,IAAI;AAE7B,SAAO,iBAAiB;AAAA,IACtB,UAAU,IAAI,YAAY,YAAY,OAAO,UAAU,KAAK;AAAA,IAC5D,SAAS,CAAC,EAAE,UAAU,MACpB,YAAY,SAAS;AAAA,MACnB,UAAU,YAAY;AAAA,MACtB,QAAQ,UAAU;AAAA,MAClB,QAAQ;AAAA,MACR,OAAO;AAAA,IACT,CAAC;AAAA,IACH,kBAAkB;AAAA,IAClB,kBAAkB,CAAC,SAAS,KAAK,cAAc;AAAA,IAC/C;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAEO,MAAM,oBAAoB,CAAC,UAAkB,UAAU,SAC5D,eAAe,EAAE,SAAS,GAAG,OAAO;AAMtC,MAAM,aAAa,CAAC,OAAe,UACjC,CAAC,YAAY,KAAK,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC,YAAY,KAAK,KAAK,QAAQ;AAEvE,SAAS,gBAAgB,UAAkB,UAAU,MAAM;AAChE,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,SAAS,UAAU,SAAS;AAAA,IAC1C,SAAS,MAAM,YAAY,MAAM,QAAQ,QAAQ;AAAA,IACjD;AAAA,IACA,OAAO;AAAA,IACP,WAAW;AAAA,EACb,CAAC;AACH;AAEO,SAAS,4BAA4B,UAAkB,UAAU,MAAM;AAC5E,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,SAAS,UAAU,sBAAsB;AAAA,IACvD,SAAS,MAAM,YAAY,MAAM,oBAAoB,QAAQ;AAAA,IAC7D;AAAA,IACA,OAAO;AAAA,IACP,WAAW;AAAA,EACb,CAAC;AACH;AAEO,SAAS,gBAAgB,UAAkB,UAAU,MAAM;AAChE,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,SAAS,UAAU,SAAS;AAAA,IAC1C,SAAS,MAAM,YAAY,MAAM,QAAQ,QAAQ;AAAA,IACjD;AAAA,IACA,OAAO;AAAA,IACP,WAAW;AAAA,EACb,CAAC;AACH;AAEO,SAAS,aAAa,UAAkB,GAAc,UAAU,MAAM;AAC3E,SAAO,SAAS;AAAA,IACd,UAAU,IAAI,UAAU,UAAU,CAAC;AAAA,IACnC,SAAS,MAAM,YAAY,OAAO,UAAU,CAAC;AAAA,IAC7C;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAQA,SAAS,YAAkB,IAA0B;AACnD,QAAM,SAAS,eAAe;AAE9B,SAAO,YAAY;AAAA,IACjB,YAAY;AAAA,IACZ,WAAW,MAAM;AACf,WAAK,OAAO,kBAAkB,EAAE,UAAU,QAAQ,CAAC;AAAA,IACrD;AAAA,EACF,CAAC;AACH;AAEO,MAAM,oBAAoB,MAC/B;AAAA,EAAY,CAAC,MACX,YAAY,kBAAkB,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM;AACrD;AAEK,MAAM,gBAAgB,MAC3B;AAAA,EAAY,CAAC,MACX,YAAY,WAAW,EAAE,IAAI,EAAE,MAAM;AACvC;","names":[]}
|
|
@@ -1,30 +1,104 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useMemo } from "react";
|
|
4
|
-
import { Badge, Button, TableRow, Th } from "@withpotter/ui-kit";
|
|
3
|
+
import { useDeferredValue, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { Badge, Button, TableRow, Th, useToast } from "@withpotter/ui-kit";
|
|
5
|
+
import { toCsv } from "@withpotter/lib/csv";
|
|
6
|
+
import { adminPosApi } from "@withpotter/lib/admin-pos-api";
|
|
5
7
|
import { formatMinor } from "@withpotter/lib/money-minor";
|
|
8
|
+
import { FilterSearch, FilterSelect, Filters } from "../_ui";
|
|
6
9
|
import { ListCard } from "../_ui/list-card";
|
|
7
10
|
import list from "../_ui/list-card.module.css";
|
|
11
|
+
import { saveCsv } from "../revenue/download-button";
|
|
12
|
+
import {
|
|
13
|
+
PAYMENTS_CSV_HEAD,
|
|
14
|
+
PAYMENT_STATUS_OPTIONS,
|
|
15
|
+
matchesSearch,
|
|
16
|
+
paymentRow,
|
|
17
|
+
statusWord
|
|
18
|
+
} from "../_pos/payments-model";
|
|
8
19
|
import { isForbidden, posErrorText } from "../_pos/pos-rules";
|
|
9
20
|
import { atTime } from "../_pos/pos-words";
|
|
10
21
|
import { usePosPayments, usePosTerminals } from "../_pos/use-pos";
|
|
22
|
+
import { useTenantParam } from "../_pos/use-tenant-param";
|
|
11
23
|
import pos from "../_pos/pos.module.css";
|
|
24
|
+
const PAGE_SIZE = 25;
|
|
25
|
+
const EXPORT_MAX_PAGES = 20;
|
|
12
26
|
function TransactionsTab() {
|
|
13
|
-
const
|
|
27
|
+
const { toast } = useToast();
|
|
28
|
+
const [tenantId, setTenantId] = useTenantParam();
|
|
29
|
+
const [status, setStatus] = useState("");
|
|
30
|
+
const [search, setSearch] = useState("");
|
|
31
|
+
const [page, setPage] = useState(0);
|
|
32
|
+
const [exporting, setExporting] = useState(false);
|
|
33
|
+
const term = useDeferredValue(search);
|
|
34
|
+
const q = usePosPayments({ tenantId, status });
|
|
14
35
|
const terminals = usePosTerminals({ limit: 100 });
|
|
15
|
-
const rows = useMemo(
|
|
16
|
-
() => q.data?.pages.flatMap((p) => p.rows) ?? [],
|
|
17
|
-
[q.data]
|
|
18
|
-
);
|
|
19
36
|
const names = useMemo(
|
|
20
37
|
() => new Map(terminals.data?.data.map((t) => [t.tenant.id, t.tenant.name])),
|
|
21
38
|
[terminals.data]
|
|
22
39
|
);
|
|
40
|
+
const businessOptions = useMemo(
|
|
41
|
+
() => [
|
|
42
|
+
{ value: "", label: "Every business" },
|
|
43
|
+
...[...names].map(([value, label]) => ({ value, label }))
|
|
44
|
+
],
|
|
45
|
+
[names]
|
|
46
|
+
);
|
|
47
|
+
const loaded = useMemo(
|
|
48
|
+
() => q.data?.pages.flatMap((p) => p.rows) ?? [],
|
|
49
|
+
[q.data]
|
|
50
|
+
);
|
|
51
|
+
const matched = useMemo(
|
|
52
|
+
() => loaded.filter((p) => matchesSearch(p, names.get(p.tenantId), term)),
|
|
53
|
+
[loaded, names, term]
|
|
54
|
+
);
|
|
55
|
+
const pages = Math.max(1, Math.ceil(matched.length / PAGE_SIZE));
|
|
56
|
+
const last = page >= pages - 1;
|
|
57
|
+
const rows = matched.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
|
58
|
+
const narrowed = !!(tenantId || status || term.trim());
|
|
59
|
+
useEffect(() => setPage(0), [tenantId, status, term]);
|
|
60
|
+
const next = async () => {
|
|
61
|
+
if (last && q.hasNextPage) await q.fetchNextPage();
|
|
62
|
+
setPage((p) => p + 1);
|
|
63
|
+
};
|
|
64
|
+
const exportCsv = async () => {
|
|
65
|
+
setExporting(true);
|
|
66
|
+
try {
|
|
67
|
+
const all = [];
|
|
68
|
+
let cursor;
|
|
69
|
+
let capped = false;
|
|
70
|
+
for (let i = 0; i < EXPORT_MAX_PAGES; i += 1) {
|
|
71
|
+
const got = await adminPosApi.payments({
|
|
72
|
+
tenantId: tenantId || void 0,
|
|
73
|
+
status: status || void 0,
|
|
74
|
+
cursor,
|
|
75
|
+
limit: 100
|
|
76
|
+
});
|
|
77
|
+
all.push(...got.rows);
|
|
78
|
+
cursor = got.nextCursor ?? void 0;
|
|
79
|
+
if (!cursor) break;
|
|
80
|
+
capped = i === EXPORT_MAX_PAGES - 1;
|
|
81
|
+
}
|
|
82
|
+
const body = all.filter((p) => matchesSearch(p, names.get(p.tenantId), term)).map((p) => paymentRow(p, names.get(p.tenantId)));
|
|
83
|
+
saveCsv(
|
|
84
|
+
`pos-transactions-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`,
|
|
85
|
+
toCsv([PAYMENTS_CSV_HEAD, ...body])
|
|
86
|
+
);
|
|
87
|
+
if (capped)
|
|
88
|
+
toast({
|
|
89
|
+
title: `Exported the newest ${all.length} payments. Narrow the filters to reach older ones.`
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
toast({ title: "The export did not start. Please try again." });
|
|
93
|
+
} finally {
|
|
94
|
+
setExporting(false);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
23
97
|
return /* @__PURE__ */ jsx(
|
|
24
98
|
ListCard,
|
|
25
99
|
{
|
|
26
100
|
title: "Transactions",
|
|
27
|
-
count: q.hasNextPage ? void 0 :
|
|
101
|
+
count: q.hasNextPage ? void 0 : matched.length,
|
|
28
102
|
columns: 6,
|
|
29
103
|
minWidth: 900,
|
|
30
104
|
isLoading: q.isLoading,
|
|
@@ -32,34 +106,103 @@ function TransactionsTab() {
|
|
|
32
106
|
onRetry: () => void q.refetch(),
|
|
33
107
|
errorTitle: isForbidden(q.error) ? "You don't have permission to see transactions." : posErrorText(q.error, "Transactions did not load."),
|
|
34
108
|
isEmpty: rows.length === 0,
|
|
35
|
-
emptyTitle: "No card payments yet.",
|
|
109
|
+
emptyTitle: narrowed ? "No transactions match." : "No card payments yet.",
|
|
110
|
+
controls: /* @__PURE__ */ jsxs(Filters, { label: "Filter transactions", children: [
|
|
111
|
+
/* @__PURE__ */ jsx(
|
|
112
|
+
FilterSearch,
|
|
113
|
+
{
|
|
114
|
+
value: search,
|
|
115
|
+
onChange: setSearch,
|
|
116
|
+
placeholder: "Business, terminal, reference or amount",
|
|
117
|
+
label: "Search transactions"
|
|
118
|
+
}
|
|
119
|
+
),
|
|
120
|
+
/* @__PURE__ */ jsx(
|
|
121
|
+
FilterSelect,
|
|
122
|
+
{
|
|
123
|
+
label: "Business",
|
|
124
|
+
value: tenantId,
|
|
125
|
+
onChange: setTenantId,
|
|
126
|
+
options: businessOptions,
|
|
127
|
+
searchable: true
|
|
128
|
+
}
|
|
129
|
+
),
|
|
130
|
+
/* @__PURE__ */ jsx(
|
|
131
|
+
FilterSelect,
|
|
132
|
+
{
|
|
133
|
+
label: "Result",
|
|
134
|
+
value: status,
|
|
135
|
+
onChange: setStatus,
|
|
136
|
+
options: [...PAYMENT_STATUS_OPTIONS]
|
|
137
|
+
}
|
|
138
|
+
),
|
|
139
|
+
narrowed && /* @__PURE__ */ jsx(
|
|
140
|
+
Button,
|
|
141
|
+
{
|
|
142
|
+
size: "sm",
|
|
143
|
+
variant: "secondary",
|
|
144
|
+
onClick: () => {
|
|
145
|
+
setSearch("");
|
|
146
|
+
setStatus("");
|
|
147
|
+
setTenantId("");
|
|
148
|
+
},
|
|
149
|
+
children: "Clear filters"
|
|
150
|
+
}
|
|
151
|
+
),
|
|
152
|
+
/* @__PURE__ */ jsx(
|
|
153
|
+
Button,
|
|
154
|
+
{
|
|
155
|
+
size: "sm",
|
|
156
|
+
variant: "secondary",
|
|
157
|
+
loading: exporting,
|
|
158
|
+
onClick: () => void exportCsv(),
|
|
159
|
+
children: "Export CSV"
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
] }),
|
|
36
163
|
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
37
164
|
/* @__PURE__ */ jsx(Th, { children: "Business" }),
|
|
38
165
|
/* @__PURE__ */ jsx(Th, { children: "Terminal" }),
|
|
39
166
|
/* @__PURE__ */ jsx(Th, { children: "Reference" }),
|
|
40
|
-
/* @__PURE__ */ jsx(Th, { children: "
|
|
167
|
+
/* @__PURE__ */ jsx(Th, { children: "Result" }),
|
|
41
168
|
/* @__PURE__ */ jsx(Th, { children: "Amount" }),
|
|
42
169
|
/* @__PURE__ */ jsx(Th, { children: "When" })
|
|
43
170
|
] }),
|
|
44
|
-
footer: q.hasNextPage ? /* @__PURE__ */
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
171
|
+
footer: matched.length > 0 || q.hasNextPage ? /* @__PURE__ */ jsxs("div", { className: pos.toolbarRow, children: [
|
|
172
|
+
/* @__PURE__ */ jsxs("span", { className: "adm-caption", children: [
|
|
173
|
+
`Page ${page + 1}${last && !q.hasNextPage ? ` of ${pages}` : ""}`,
|
|
174
|
+
term.trim() && q.hasNextPage ? " \xB7 searching the payments loaded so far" : ""
|
|
175
|
+
] }),
|
|
176
|
+
/* @__PURE__ */ jsxs("div", { className: "adm-toolbar", children: [
|
|
177
|
+
/* @__PURE__ */ jsx(
|
|
178
|
+
Button,
|
|
179
|
+
{
|
|
180
|
+
size: "sm",
|
|
181
|
+
variant: "secondary",
|
|
182
|
+
disabled: page === 0,
|
|
183
|
+
onClick: () => setPage((p) => p - 1),
|
|
184
|
+
children: "Previous"
|
|
185
|
+
}
|
|
186
|
+
),
|
|
187
|
+
/* @__PURE__ */ jsx(
|
|
188
|
+
Button,
|
|
189
|
+
{
|
|
190
|
+
size: "sm",
|
|
191
|
+
variant: "secondary",
|
|
192
|
+
loading: q.isFetchingNextPage,
|
|
193
|
+
disabled: last && !q.hasNextPage,
|
|
194
|
+
onClick: () => void next(),
|
|
195
|
+
children: "Next"
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
] })
|
|
199
|
+
] }) : void 0,
|
|
54
200
|
children: rows.map((p) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
55
201
|
/* @__PURE__ */ jsx("td", { children: names.get(p.tenantId) ?? "\u2013" }),
|
|
56
202
|
/* @__PURE__ */ jsx("td", { children: p.deviceLabel ?? "\u2013" }),
|
|
57
203
|
/* @__PURE__ */ jsx("td", { className: list.muted, children: p.reference ?? "\u2013" }),
|
|
58
|
-
/* @__PURE__ */ jsx("td", { className: list.nowrap, children: /* @__PURE__ */ jsx(Badge, { variant: p.status === "SUCCEEDED" ? "success" : "secondary", children:
|
|
59
|
-
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${pos.num}`, children: `${p.direction === "out" ? "-" : ""}${formatMinor(
|
|
60
|
-
p.amountMinor,
|
|
61
|
-
p.currency
|
|
62
|
-
)}` }),
|
|
204
|
+
/* @__PURE__ */ jsx("td", { className: list.nowrap, children: /* @__PURE__ */ jsx(Badge, { variant: p.status === "SUCCEEDED" ? "success" : "secondary", children: statusWord(p.status) }) }),
|
|
205
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${pos.num}`, children: `${p.direction === "out" ? "-" : ""}${formatMinor(p.amountMinor, p.currency)}` }),
|
|
63
206
|
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: atTime(p.at) })
|
|
64
207
|
] }, p.id))
|
|
65
208
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/pos/transactions-tab.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { Badge, Button, TableRow, Th } from '@withpotter/ui-kit';\nimport { formatMinor } from '@withpotter/lib/money-minor';\nimport { ListCard } from '../_ui/list-card';\nimport list from '../_ui/list-card.module.css';\nimport { isForbidden, posErrorText } from '../_pos/pos-rules';\nimport { atTime } from '../_pos/pos-words';\nimport { usePosPayments, usePosTerminals } from '../_pos/use-pos';\nimport pos from '../_pos/pos.module.css';\n\n/**\n * Card payments taken on every business's terminals, newest first. The payments carry the business's\n * id, not its name, so the names come from the terminals (the API's page is as large as it allows).\n */\nexport function TransactionsTab() {\n const q = usePosPayments();\n const terminals = usePosTerminals({ limit: 100 });\n const rows = useMemo(\n () => q.data?.pages.flatMap((p) => p.rows) ?? [],\n [q.data],\n );\n const names = useMemo(\n () =>\n new Map(terminals.data?.data.map((t) => [t.tenant.id, t.tenant.name])),\n [terminals.data],\n );\n\n return (\n <ListCard\n title=\"Transactions\"\n count={q.hasNextPage ? undefined : rows.length}\n columns={6}\n minWidth={900}\n isLoading={q.isLoading}\n isError={q.isError && !q.data}\n onRetry={() => void q.refetch()}\n errorTitle={\n isForbidden(q.error)\n ? \"You don't have permission to see transactions.\"\n : posErrorText(q.error, 'Transactions did not load.')\n }\n isEmpty={rows.length === 0}\n emptyTitle=\"No card payments yet.\"\n head={\n <tr>\n <Th>Business</Th>\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>{names.get(p.tenantId) ?? '–'}</td>\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"],"mappings":";AA8CQ,SACE,KADF;AA5CR,SAAS,eAAe;AACxB,SAAS,OAAO,QAAQ,UAAU,UAAU;AAC5C,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,SAAS,aAAa,oBAAoB;AAC1C,SAAS,cAAc;AACvB,SAAS,gBAAgB,uBAAuB;AAChD,OAAO,SAAS;AAMT,SAAS,kBAAkB;AAChC,QAAM,IAAI,eAAe;AACzB,QAAM,YAAY,gBAAgB,EAAE,OAAO,IAAI,CAAC;AAChD,QAAM,OAAO;AAAA,IACX,MAAM,EAAE,MAAM,MAAM,QAAQ,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC;AAAA,IAC/C,CAAC,EAAE,IAAI;AAAA,EACT;AACA,QAAM,QAAQ;AAAA,IACZ,MACE,IAAI,IAAI,UAAU,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC;AAAA,IACvE,CAAC,UAAU,IAAI;AAAA,EACjB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,EAAE,cAAc,SAAY,KAAK;AAAA,MACxC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,WAAW,EAAE;AAAA,MACb,SAAS,EAAE,WAAW,CAAC,EAAE;AAAA,MACzB,SAAS,MAAM,KAAK,EAAE,QAAQ;AAAA,MAC9B,YACE,YAAY,EAAE,KAAK,IACf,mDACA,aAAa,EAAE,OAAO,4BAA4B;AAAA,MAExD,SAAS,KAAK,WAAW;AAAA,MACzB,YAAW;AAAA,MACX,MACE,qBAAC,QACC;AAAA,4BAAC,MAAG,sBAAQ;AAAA,QACZ,oBAAC,MAAG,sBAAQ;AAAA,QACZ,oBAAC,MAAG,uBAAS;AAAA,QACb,oBAAC,MAAG,oBAAM;AAAA,QACV,oBAAC,MAAG,oBAAM;AAAA,QACV,oBAAC,MAAG,kBAAI;AAAA,SACV;AAAA,MAEF,QACE,EAAE,cACA,oBAAC,SAAI,WAAW,IAAI,OAClB;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,UAAU,EAAE;AAAA,UACZ,SAAS,MAAM,KAAK,EAAE,cAAc;AAAA,UAEnC,YAAE,qBAAqB,kBAAa;AAAA;AAAA,MACvC,GACF,IACE;AAAA,MAGL,eAAK,IAAI,CAAC,MACT,qBAAC,YACC;AAAA,4BAAC,QAAI,gBAAM,IAAI,EAAE,QAAQ,KAAK,UAAI;AAAA,QAClC,oBAAC,QAAI,YAAE,eAAe,UAAI;AAAA,QAC1B,oBAAC,QAAG,WAAW,KAAK,OAAQ,YAAE,aAAa,UAAI;AAAA,QAC/C,oBAAC,QAAG,WAAW,KAAK,QAClB,8BAAC,SAAM,SAAS,EAAE,WAAW,cAAc,YAAY,aACpD,YAAE,OAAO,OAAO,CAAC,IAAI,EAAE,OAAO,MAAM,CAAC,EAAE,YAAY,GACtD,GACF;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,IAAI,GAAG,IACrC,aAAG,EAAE,cAAc,QAAQ,MAAM,EAAE,GAAG;AAAA,UACrC,EAAE;AAAA,UACF,EAAE;AAAA,QACJ,CAAC,IACH;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IAAK,iBAAO,EAAE,EAAE,GAAE;AAAA,WAfhD,EAAE,EAgBjB,CACD;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/pos/transactions-tab.tsx"],"sourcesContent":["'use client';\n\nimport { useDeferredValue, useEffect, useMemo, useState } from 'react';\nimport { Badge, Button, TableRow, Th, useToast } from '@withpotter/ui-kit';\nimport { toCsv } from '@withpotter/lib/csv';\nimport { adminPosApi } from '@withpotter/lib/admin-pos-api';\nimport { formatMinor } from '@withpotter/lib/money-minor';\nimport { FilterSearch, FilterSelect, Filters } from '../_ui';\nimport { ListCard } from '../_ui/list-card';\nimport list from '../_ui/list-card.module.css';\nimport { saveCsv } from '../revenue/download-button';\nimport {\n PAYMENTS_CSV_HEAD,\n PAYMENT_STATUS_OPTIONS,\n matchesSearch,\n paymentRow,\n statusWord,\n} from '../_pos/payments-model';\nimport { isForbidden, posErrorText } from '../_pos/pos-rules';\nimport { atTime } from '../_pos/pos-words';\nimport { usePosPayments, usePosTerminals } from '../_pos/use-pos';\nimport { useTenantParam } from '../_pos/use-tenant-param';\nimport pos from '../_pos/pos.module.css';\n\nconst PAGE_SIZE = 25;\n/** The export reads at most this many pages of 100, so one click cannot read the whole history. */\nconst EXPORT_MAX_PAGES = 20;\n\n/**\n * Card payments taken on every business's terminals, newest first, with search, filters, pages and a CSV.\n *\n * The API narrows by business and result and pages forward from the last row; it has no search. So the search looks\n * through the payments already loaded (more load as pages are turned), and the CSV reads every page that matches the\n * filters, then applies the search. Business names come from the terminals, which is as large a page as the API gives.\n */\nexport function TransactionsTab() {\n const { toast } = useToast();\n const [tenantId, setTenantId] = useTenantParam();\n const [status, setStatus] = useState('');\n const [search, setSearch] = useState('');\n const [page, setPage] = useState(0);\n const [exporting, setExporting] = useState(false);\n const term = useDeferredValue(search);\n\n const q = usePosPayments({ tenantId, status });\n const terminals = usePosTerminals({ limit: 100 });\n const names = useMemo(\n () =>\n new Map(terminals.data?.data.map((t) => [t.tenant.id, t.tenant.name])),\n [terminals.data],\n );\n const businessOptions = useMemo(\n () => [\n { value: '', label: 'Every business' },\n ...[...names].map(([value, label]) => ({ value, label })),\n ],\n [names],\n );\n\n const loaded = useMemo(\n () => q.data?.pages.flatMap((p) => p.rows) ?? [],\n [q.data],\n );\n const matched = useMemo(\n () => loaded.filter((p) => matchesSearch(p, names.get(p.tenantId), term)),\n [loaded, names, term],\n );\n const pages = Math.max(1, Math.ceil(matched.length / PAGE_SIZE));\n const last = page >= pages - 1;\n const rows = matched.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);\n const narrowed = !!(tenantId || status || term.trim());\n\n // A new search or filter starts at the first page.\n useEffect(() => setPage(0), [tenantId, status, term]);\n\n const next = async () => {\n // At the end of what is loaded, fetch the page after it first.\n if (last && q.hasNextPage) await q.fetchNextPage();\n\n setPage((p) => p + 1);\n };\n\n const exportCsv = async () => {\n setExporting(true);\n\n try {\n const all = [];\n let cursor: string | undefined;\n let capped = false;\n\n for (let i = 0; i < EXPORT_MAX_PAGES; i += 1) {\n const got = await adminPosApi.payments({\n tenantId: tenantId || undefined,\n status: status || undefined,\n cursor,\n limit: 100,\n });\n\n all.push(...got.rows);\n cursor = got.nextCursor ?? undefined;\n\n if (!cursor) break;\n\n capped = i === EXPORT_MAX_PAGES - 1;\n }\n\n const body = all\n .filter((p) => matchesSearch(p, names.get(p.tenantId), term))\n .map((p) => paymentRow(p, names.get(p.tenantId)));\n\n saveCsv(\n `pos-transactions-${new Date().toISOString().slice(0, 10)}.csv`,\n toCsv([PAYMENTS_CSV_HEAD, ...body]),\n );\n\n if (capped)\n toast({\n title: `Exported the newest ${all.length} payments. Narrow the filters to reach older ones.`,\n });\n } catch {\n toast({ title: 'The export did not start. Please try again.' });\n } finally {\n setExporting(false);\n }\n };\n\n return (\n <ListCard\n title=\"Transactions\"\n count={q.hasNextPage ? undefined : matched.length}\n columns={6}\n minWidth={900}\n isLoading={q.isLoading}\n isError={q.isError && !q.data}\n onRetry={() => void q.refetch()}\n errorTitle={\n isForbidden(q.error)\n ? \"You don't have permission to see transactions.\"\n : posErrorText(q.error, 'Transactions did not load.')\n }\n isEmpty={rows.length === 0}\n emptyTitle={\n narrowed ? 'No transactions match.' : 'No card payments yet.'\n }\n controls={\n <Filters label=\"Filter transactions\">\n <FilterSearch\n value={search}\n onChange={setSearch}\n placeholder=\"Business, terminal, reference or amount\"\n label=\"Search transactions\"\n />\n <FilterSelect\n label=\"Business\"\n value={tenantId}\n onChange={setTenantId}\n options={businessOptions}\n searchable\n />\n <FilterSelect\n label=\"Result\"\n value={status}\n onChange={setStatus}\n options={[...PAYMENT_STATUS_OPTIONS]}\n />\n {narrowed && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => {\n setSearch('');\n setStatus('');\n setTenantId('');\n }}\n >\n Clear filters\n </Button>\n )}\n <Button\n size=\"sm\"\n variant=\"secondary\"\n loading={exporting}\n onClick={() => void exportCsv()}\n >\n Export CSV\n </Button>\n </Filters>\n }\n head={\n <tr>\n <Th>Business</Th>\n <Th>Terminal</Th>\n <Th>Reference</Th>\n <Th>Result</Th>\n <Th>Amount</Th>\n <Th>When</Th>\n </tr>\n }\n footer={\n matched.length > 0 || q.hasNextPage ? (\n <div className={pos.toolbarRow}>\n <span className=\"adm-caption\">\n {`Page ${page + 1}${last && !q.hasNextPage ? ` of ${pages}` : ''}`}\n {term.trim() && q.hasNextPage\n ? ' · searching the payments loaded so far'\n : ''}\n </span>\n <div className=\"adm-toolbar\">\n <Button\n size=\"sm\"\n variant=\"secondary\"\n disabled={page === 0}\n onClick={() => setPage((p) => p - 1)}\n >\n Previous\n </Button>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n loading={q.isFetchingNextPage}\n disabled={last && !q.hasNextPage}\n onClick={() => void next()}\n >\n Next\n </Button>\n </div>\n </div>\n ) : undefined\n }\n >\n {rows.map((p) => (\n <TableRow key={p.id}>\n <td>{names.get(p.tenantId) ?? '–'}</td>\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 {statusWord(p.status)}\n </Badge>\n </td>\n <td className={`${list.nowrap} ${pos.num}`}>\n {`${p.direction === 'out' ? '-' : ''}${formatMinor(p.amountMinor, p.currency)}`}\n </td>\n <td className={`${list.nowrap} ${list.muted}`}>{atTime(p.at)}</td>\n </TableRow>\n ))}\n </ListCard>\n );\n}\n"],"mappings":";AAiJQ,SACE,KADF;AA/IR,SAAS,kBAAkB,WAAW,SAAS,gBAAgB;AAC/D,SAAS,OAAO,QAAQ,UAAU,IAAI,gBAAgB;AACtD,SAAS,aAAa;AACtB,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,cAAc,cAAc,eAAe;AACpD,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa,oBAAoB;AAC1C,SAAS,cAAc;AACvB,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,sBAAsB;AAC/B,OAAO,SAAS;AAEhB,MAAM,YAAY;AAElB,MAAM,mBAAmB;AASlB,SAAS,kBAAkB;AAChC,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,CAAC,UAAU,WAAW,IAAI,eAAe;AAC/C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAClC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,OAAO,iBAAiB,MAAM;AAEpC,QAAM,IAAI,eAAe,EAAE,UAAU,OAAO,CAAC;AAC7C,QAAM,YAAY,gBAAgB,EAAE,OAAO,IAAI,CAAC;AAChD,QAAM,QAAQ;AAAA,IACZ,MACE,IAAI,IAAI,UAAU,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC;AAAA,IACvE,CAAC,UAAU,IAAI;AAAA,EACjB;AACA,QAAM,kBAAkB;AAAA,IACtB,MAAM;AAAA,MACJ,EAAE,OAAO,IAAI,OAAO,iBAAiB;AAAA,MACrC,GAAG,CAAC,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,EAAE,OAAO,MAAM,EAAE;AAAA,IAC1D;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AAEA,QAAM,SAAS;AAAA,IACb,MAAM,EAAE,MAAM,MAAM,QAAQ,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC;AAAA,IAC/C,CAAC,EAAE,IAAI;AAAA,EACT;AACA,QAAM,UAAU;AAAA,IACd,MAAM,OAAO,OAAO,CAAC,MAAM,cAAc,GAAG,MAAM,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;AAAA,IACxE,CAAC,QAAQ,OAAO,IAAI;AAAA,EACtB;AACA,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,SAAS,SAAS,CAAC;AAC/D,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,OAAO,QAAQ,MAAM,OAAO,YAAY,OAAO,KAAK,SAAS;AACnE,QAAM,WAAW,CAAC,EAAE,YAAY,UAAU,KAAK,KAAK;AAGpD,YAAU,MAAM,QAAQ,CAAC,GAAG,CAAC,UAAU,QAAQ,IAAI,CAAC;AAEpD,QAAM,OAAO,YAAY;AAEvB,QAAI,QAAQ,EAAE,YAAa,OAAM,EAAE,cAAc;AAEjD,YAAQ,CAAC,MAAM,IAAI,CAAC;AAAA,EACtB;AAEA,QAAM,YAAY,YAAY;AAC5B,iBAAa,IAAI;AAEjB,QAAI;AACF,YAAM,MAAM,CAAC;AACb,UAAI;AACJ,UAAI,SAAS;AAEb,eAAS,IAAI,GAAG,IAAI,kBAAkB,KAAK,GAAG;AAC5C,cAAM,MAAM,MAAM,YAAY,SAAS;AAAA,UACrC,UAAU,YAAY;AAAA,UACtB,QAAQ,UAAU;AAAA,UAClB;AAAA,UACA,OAAO;AAAA,QACT,CAAC;AAED,YAAI,KAAK,GAAG,IAAI,IAAI;AACpB,iBAAS,IAAI,cAAc;AAE3B,YAAI,CAAC,OAAQ;AAEb,iBAAS,MAAM,mBAAmB;AAAA,MACpC;AAEA,YAAM,OAAO,IACV,OAAO,CAAC,MAAM,cAAc,GAAG,MAAM,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC,EAC3D,IAAI,CAAC,MAAM,WAAW,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC,CAAC;AAElD;AAAA,QACE,qBAAoB,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,QACzD,MAAM,CAAC,mBAAmB,GAAG,IAAI,CAAC;AAAA,MACpC;AAEA,UAAI;AACF,cAAM;AAAA,UACJ,OAAO,uBAAuB,IAAI,MAAM;AAAA,QAC1C,CAAC;AAAA,IACL,QAAQ;AACN,YAAM,EAAE,OAAO,8CAA8C,CAAC;AAAA,IAChE,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,EAAE,cAAc,SAAY,QAAQ;AAAA,MAC3C,SAAS;AAAA,MACT,UAAU;AAAA,MACV,WAAW,EAAE;AAAA,MACb,SAAS,EAAE,WAAW,CAAC,EAAE;AAAA,MACzB,SAAS,MAAM,KAAK,EAAE,QAAQ;AAAA,MAC9B,YACE,YAAY,EAAE,KAAK,IACf,mDACA,aAAa,EAAE,OAAO,4BAA4B;AAAA,MAExD,SAAS,KAAK,WAAW;AAAA,MACzB,YACE,WAAW,2BAA2B;AAAA,MAExC,UACE,qBAAC,WAAQ,OAAM,uBACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,UAAU;AAAA,YACV,aAAY;AAAA,YACZ,OAAM;AAAA;AAAA,QACR;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,OAAO;AAAA,YACP,UAAU;AAAA,YACV,SAAS;AAAA,YACT,YAAU;AAAA;AAAA,QACZ;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,OAAO;AAAA,YACP,UAAU;AAAA,YACV,SAAS,CAAC,GAAG,sBAAsB;AAAA;AAAA,QACrC;AAAA,QACC,YACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS,MAAM;AACb,wBAAU,EAAE;AACZ,wBAAU,EAAE;AACZ,0BAAY,EAAE;AAAA,YAChB;AAAA,YACD;AAAA;AAAA,QAED;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS;AAAA,YACT,SAAS,MAAM,KAAK,UAAU;AAAA,YAC/B;AAAA;AAAA,QAED;AAAA,SACF;AAAA,MAEF,MACE,qBAAC,QACC;AAAA,4BAAC,MAAG,sBAAQ;AAAA,QACZ,oBAAC,MAAG,sBAAQ;AAAA,QACZ,oBAAC,MAAG,uBAAS;AAAA,QACb,oBAAC,MAAG,oBAAM;AAAA,QACV,oBAAC,MAAG,oBAAM;AAAA,QACV,oBAAC,MAAG,kBAAI;AAAA,SACV;AAAA,MAEF,QACE,QAAQ,SAAS,KAAK,EAAE,cACtB,qBAAC,SAAI,WAAW,IAAI,YAClB;AAAA,6BAAC,UAAK,WAAU,eACb;AAAA,kBAAQ,OAAO,CAAC,GAAG,QAAQ,CAAC,EAAE,cAAc,OAAO,KAAK,KAAK,EAAE;AAAA,UAC/D,KAAK,KAAK,KAAK,EAAE,cACd,+CACA;AAAA,WACN;AAAA,QACA,qBAAC,SAAI,WAAU,eACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,UAAU,SAAS;AAAA,cACnB,SAAS,MAAM,QAAQ,CAAC,MAAM,IAAI,CAAC;AAAA,cACpC;AAAA;AAAA,UAED;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,SAAS,EAAE;AAAA,cACX,UAAU,QAAQ,CAAC,EAAE;AAAA,cACrB,SAAS,MAAM,KAAK,KAAK;AAAA,cAC1B;AAAA;AAAA,UAED;AAAA,WACF;AAAA,SACF,IACE;AAAA,MAGL,eAAK,IAAI,CAAC,MACT,qBAAC,YACC;AAAA,4BAAC,QAAI,gBAAM,IAAI,EAAE,QAAQ,KAAK,UAAI;AAAA,QAClC,oBAAC,QAAI,YAAE,eAAe,UAAI;AAAA,QAC1B,oBAAC,QAAG,WAAW,KAAK,OAAQ,YAAE,aAAa,UAAI;AAAA,QAC/C,oBAAC,QAAG,WAAW,KAAK,QAClB,8BAAC,SAAM,SAAS,EAAE,WAAW,cAAc,YAAY,aACpD,qBAAW,EAAE,MAAM,GACtB,GACF;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,IAAI,GAAG,IACrC,aAAG,EAAE,cAAc,QAAQ,MAAM,EAAE,GAAG,YAAY,EAAE,aAAa,EAAE,QAAQ,CAAC,IAC/E;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IAAK,iBAAO,EAAE,EAAE,GAAE;AAAA,WAZhD,EAAE,EAajB,CACD;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -102,7 +102,6 @@ function MerchantPaymentsView({
|
|
|
102
102
|
const focus = focusCurrency(currencies, currency) ?? "NGN";
|
|
103
103
|
const now = data.cards.current.find((c) => c.currency === focus);
|
|
104
104
|
const before = data.cards.previous?.find((c) => c.currency === focus) ?? null;
|
|
105
|
-
const strip = data.strip.find((s) => s.currency === focus);
|
|
106
105
|
const money = (minor) => minorToMoney(minor, focus);
|
|
107
106
|
const delta = (a, b, goodWhenUp = true) => before ? { pct: pctChange(a, b), positiveIsGood: goodWhenUp, caption: "vs the period before" } : void 0;
|
|
108
107
|
const totalPages = Math.max(Math.ceil(data.total / PAGE_SIZE), 1);
|
|
@@ -150,7 +149,7 @@ function MerchantPaymentsView({
|
|
|
150
149
|
/* @__PURE__ */ jsx("h2", { className: styles.sectionTitle, children: "How customers paid" }),
|
|
151
150
|
/* @__PURE__ */ jsx("p", { className: styles.sectionNote, children: "The ways of paying on the checkout, and how much came in through each." }),
|
|
152
151
|
/* @__PURE__ */ jsx(MethodCards, { now: data.byChannel.current, before: data.byChannel.previous, currency: focus }),
|
|
153
|
-
/* @__PURE__ */ jsx("h2", { className: styles.sectionTitle, children: "
|
|
152
|
+
/* @__PURE__ */ jsx("h2", { className: styles.sectionTitle, children: "Merchant revenue" }),
|
|
154
153
|
/* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
|
|
155
154
|
/* @__PURE__ */ jsxs(Filters, { label: "Filter payments", children: [
|
|
156
155
|
/* @__PURE__ */ jsx(
|
|
@@ -184,26 +183,6 @@ function MerchantPaymentsView({
|
|
|
184
183
|
}
|
|
185
184
|
)
|
|
186
185
|
] }),
|
|
187
|
-
strip ? /* @__PURE__ */ jsxs("p", { className: styles.strip, "aria-live": "polite", children: [
|
|
188
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
189
|
-
"Showing ",
|
|
190
|
-
/* @__PURE__ */ jsx("strong", { children: integer.format(data.total) }),
|
|
191
|
-
" ",
|
|
192
|
-
data.total === 1 ? "payment" : "payments"
|
|
193
|
-
] }),
|
|
194
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
195
|
-
/* @__PURE__ */ jsx("strong", { children: money(strip.amountMinor) }),
|
|
196
|
-
" received"
|
|
197
|
-
] }),
|
|
198
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
199
|
-
/* @__PURE__ */ jsx("strong", { children: money(strip.feeMinor) }),
|
|
200
|
-
" in fees"
|
|
201
|
-
] }),
|
|
202
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
203
|
-
/* @__PURE__ */ jsx("strong", { children: money(strip.netMinor) }),
|
|
204
|
-
" to merchants"
|
|
205
|
-
] })
|
|
206
|
-
] }) : null,
|
|
207
186
|
data.rows.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: table.empty, children: "No payments match. Try a wider date range or clear a filter." }) : /* @__PURE__ */ jsx(
|
|
208
187
|
TableShell,
|
|
209
188
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/merchant-payments-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport Link from 'next/link';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport { platformAdminApi, type MerchantPaymentRow } from '@withpotter/lib/admin-api';\nimport { useMerchantOptions, useMerchantPayments } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { CountUp, Reveal } from '../_fx';\nimport {\n AdmLoading,\n FilterSearch,\n FilterSelect,\n Filters,\n Pagination,\n StatCard,\n focusCurrency,\n} from '../_ui';\nimport { DownloadButton } from './download-button';\nimport {\n STATUS_WORDS,\n TYPE_WORDS,\n downloadName,\n minorToMoney,\n pctChange,\n word,\n} from './ledger-model';\nimport { MethodCards } from './method-cards';\nimport { PaymentDrawer } from './payment-drawer';\nimport { useUrlFilters } from './use-url-filters';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst FILTERS = { q: '', method: '', status: '', type: '', merchant: '' };\nconst integer = new Intl.NumberFormat('en-US');\n\nconst METHODS = [\n { value: '', label: 'Every way of paying' },\n { value: 'card', label: 'Card' },\n { value: 'ussd', label: 'USSD' },\n { value: 'transfer', label: 'Bank transfer' },\n { value: 'opay', label: 'Opay' },\n];\nconst STATUSES = [\n { value: '', label: 'Paid' },\n { value: 'pending', label: 'Waiting' },\n { value: 'failed', label: 'Failed' },\n { value: 'refunded', label: 'Refunded' },\n];\nconst TYPES = [\n { value: '', label: 'Every kind' },\n ...['order', 'invoice', 'booking', 'membership', 'subscription', 'deposit', 'tip', 'refund'].map((value) => ({\n value,\n label: word(TYPE_WORDS, value),\n })),\n];\n\nconst tone = (status: string) =>\n status === 'success' ? styles.statusGood : status === 'failed' ? styles.statusBad : status === 'pending' ? styles.statusWait : styles.statusMuted;\n\n/** The merchant payments view: who paid, how, and which merchant received it. */\nexport function MerchantPaymentsView({\n startDate,\n endDate,\n currency,\n}: {\n startDate?: string;\n endDate?: string;\n /** The currency filter's choice; empty follows the only one there is. */\n currency: string;\n}) {\n // The filters live in the page address, so this view can be bookmarked or sent to someone.\n const { values, page, set } = useUrlFilters('mp', FILTERS);\n const [search, setSearch] = useState(values.q);\n const [open, setOpen] = useState<MerchantPaymentRow | null>(null);\n\n // Typing waits a moment, so a search is one request and not one per key.\n useEffect(() => {\n const timer = setTimeout(() => {\n if (search.trim() !== values.q) set({ q: search.trim() });\n }, 300);\n\n return () => clearTimeout(timer);\n // Only a change the person typed should write to the address.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [search]);\n\n const filters = useMemo(\n () => ({\n startDate,\n endDate,\n method: values.method,\n status: values.status,\n type: values.type,\n q: values.q,\n merchantId: values.merchant,\n page,\n limit: PAGE_SIZE,\n }),\n [startDate, endDate, values, page],\n );\n const merchants = useMerchantOptions({ startDate, endDate });\n const merchantChoices = [\n { value: '', label: 'Every merchant' },\n ...(merchants.data?.merchants ?? []).map((one) => ({ value: one.id, label: one.name })),\n ];\n\n const { data, isLoading, isError, refetch } = useMerchantPayments(filters);\n\n if (isError) {\n return (\n <LoadFailed\n message=\"Merchant payments could not be loaded. This box may not have them yet.\"\n onRetry={() => void refetch()}\n />\n );\n }\n if (isLoading || !data) return <AdmLoading label=\"Loading merchant payments…\" />;\n\n const currencies = [...new Set(data.cards.current.map((c) => c.currency))];\n const focus = focusCurrency(currencies, currency) ?? 'NGN';\n const now = data.cards.current.find((c) => c.currency === focus);\n const before = data.cards.previous?.find((c) => c.currency === focus) ?? null;\n const strip = data.strip.find((s) => s.currency === focus);\n const money = (minor: string | number) => minorToMoney(minor, focus);\n const delta = (a?: string, b?: string, goodWhenUp = true) =>\n before ? { pct: pctChange(a, b), positiveIsGood: goodWhenUp, caption: 'vs the period before' } : undefined;\n const totalPages = Math.max(Math.ceil(data.total / PAGE_SIZE), 1);\n\n return (\n <div>\n <p className={styles.lede}>\n This is money that customers paid to merchants. Use the filters to narrow it down, and download exactly what you\n see.\n </p>\n\n <section aria-label=\"Merchant payments at a glance\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Money received\"\n value={<CountUp value={Number(now?.grossMinor ?? 0) / 100} format={(v) => money(Math.round(v * 100))} />}\n sub={now ? `${integer.format(now.payments)} payments that went through` : 'No payments in this period'}\n hint=\"Everything customers paid to merchants that went through, before any fees are taken.\"\n delta={delta(now?.grossMinor, before?.grossMinor)}\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Fees taken\"\n value={money(now?.feeMinor ?? 0)}\n hint=\"What the payment gateway and Olive kept from those payments.\"\n delta={delta(now?.feeMinor, before?.feeMinor, false)}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Merchants keep\"\n value={money(now?.netMinor ?? 0)}\n hint=\"What is left for merchants after the fees: money received minus fees taken.\"\n delta={delta(now?.netMinor, before?.netMinor)}\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Payments that worked\"\n value={now?.successRate == null ? '–' : `${now.successRate}%`}\n sub={now?.successRate == null ? 'Nothing was tried in this period' : `Refunded: ${money(now.refundsMinor)}`}\n hint=\"Out of every payment that was tried, the share that went through. A low number can mean a way of paying is broken.\"\n />\n </Reveal>\n </section>\n\n <h2 className={styles.sectionTitle}>How customers paid</h2>\n <p className={styles.sectionNote}>The ways of paying on the checkout, and how much came in through each.</p>\n <MethodCards now={data.byChannel.current} before={data.byChannel.previous} currency={focus} />\n\n <h2 className={styles.sectionTitle}>Every payment</h2>\n <div className={styles.toolbar}>\n <Filters label=\"Filter payments\">\n <FilterSearch\n value={search}\n onChange={setSearch}\n placeholder=\"Search a name, email, phone, reference or merchant\"\n label=\"Search payments\"\n />\n <FilterSelect\n value={values.merchant}\n onChange={(merchant) => set({ merchant })}\n options={merchantChoices}\n label=\"Merchant\"\n searchable\n />\n <FilterSelect value={values.method} onChange={(method) => set({ method })} options={METHODS} label=\"Way of paying\" />\n <FilterSelect value={values.status} onChange={(status) => set({ status })} options={STATUSES} label=\"Payment result\" />\n <FilterSelect value={values.type} onChange={(type) => set({ type })} options={TYPES} label=\"What it was for\" />\n </Filters>\n <DownloadButton\n filename={downloadName('merchant-payments', startDate, endDate)}\n load={() => platformAdminApi.merchantPaymentsCsv({ ...filters, page: undefined, limit: undefined })}\n />\n </div>\n\n {strip ? (\n <p className={styles.strip} aria-live=\"polite\">\n <span>\n Showing <strong>{integer.format(data.total)}</strong> {data.total === 1 ? 'payment' : 'payments'}\n </span>\n <span>\n <strong>{money(strip.amountMinor)}</strong> received\n </span>\n <span>\n <strong>{money(strip.feeMinor)}</strong> in fees\n </span>\n <span>\n <strong>{money(strip.netMinor)}</strong> to merchants\n </span>\n </p>\n ) : null}\n\n {data.rows.length === 0 ? (\n <EmptyNote className={table.empty}>No payments match. Try a wider date range or clear a filter.</EmptyNote>\n ) : (\n <TableShell\n minWidth={980}\n density=\"compact\"\n head={\n <tr>\n <Th>When</Th>\n <Th>Paid by</Th>\n <Th>How</Th>\n <Th>Merchant</Th>\n <Th>For</Th>\n <Th>Result</Th>\n <Th align=\"right\">Amount</Th>\n <Th align=\"right\">Fee</Th>\n <Th align=\"right\">Merchant gets</Th>\n </tr>\n }\n >\n {data.rows.map((row) => (\n <PaymentRow key={row.id} row={row} onOpen={setOpen} />\n ))}\n </TableShell>\n )}\n\n <Pagination page={page} totalPages={totalPages} total={data.total} noun=\"payments\" onPage={(next) => set({ page: next })} />\n <PaymentDrawer row={open} onClose={() => setOpen(null)} />\n </div>\n );\n}\n\nfunction PaymentRow({ row, onOpen }: { row: MerchantPaymentRow; onOpen: (row: MerchantPaymentRow) => void }) {\n const { payer } = row;\n const contact = [payer.email, payer.phone].filter(Boolean).join(' · ');\n const money = (minor: string) => minorToMoney(minor, row.currency);\n\n return (\n <TableRow onClick={() => onOpen(row)}>\n <td className={table.num}>\n {new Date(row.paidAt).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{payer.name || 'Name not recorded'}</span>\n {contact ? <span className={styles.whoSub}>{contact}</span> : null}\n </span>\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{row.channel.label}</span>\n {row.methodDetail ? <span className={styles.whoSub}>{row.methodDetail}</span> : null}\n </span>\n </td>\n <td className={table.name}>\n {row.merchant.name ? <Link href={`/admin/tenants/${row.merchant.id}`}>{row.merchant.name}</Link> : 'Unknown merchant'}\n </td>\n <td>{word(TYPE_WORDS, row.type)}</td>\n <td>\n <span className={`${styles.status} ${tone(row.status)}`}>{word(STATUS_WORDS, row.status)}</span>\n </td>\n <td className={table.numStrong}>{money(row.amountMinor)}</td>\n <td className={table.num}>{money(row.feeMinor)}</td>\n <td className={table.num}>{money(row.netMinor)}</td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/merchant-payments-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport Link from 'next/link';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport { platformAdminApi, type MerchantPaymentRow } from '@withpotter/lib/admin-api';\nimport { useMerchantOptions, useMerchantPayments } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { CountUp, Reveal } from '../_fx';\nimport {\n AdmLoading,\n FilterSearch,\n FilterSelect,\n Filters,\n Pagination,\n StatCard,\n focusCurrency,\n} from '../_ui';\nimport { DownloadButton } from './download-button';\nimport {\n STATUS_WORDS,\n TYPE_WORDS,\n downloadName,\n minorToMoney,\n pctChange,\n word,\n} from './ledger-model';\nimport { MethodCards } from './method-cards';\nimport { PaymentDrawer } from './payment-drawer';\nimport { useUrlFilters } from './use-url-filters';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst FILTERS = { q: '', method: '', status: '', type: '', merchant: '' };\nconst integer = new Intl.NumberFormat('en-US');\n\nconst METHODS = [\n { value: '', label: 'Every way of paying' },\n { value: 'card', label: 'Card' },\n { value: 'ussd', label: 'USSD' },\n { value: 'transfer', label: 'Bank transfer' },\n { value: 'opay', label: 'Opay' },\n];\nconst STATUSES = [\n { value: '', label: 'Paid' },\n { value: 'pending', label: 'Waiting' },\n { value: 'failed', label: 'Failed' },\n { value: 'refunded', label: 'Refunded' },\n];\nconst TYPES = [\n { value: '', label: 'Every kind' },\n ...['order', 'invoice', 'booking', 'membership', 'subscription', 'deposit', 'tip', 'refund'].map((value) => ({\n value,\n label: word(TYPE_WORDS, value),\n })),\n];\n\nconst tone = (status: string) =>\n status === 'success' ? styles.statusGood : status === 'failed' ? styles.statusBad : status === 'pending' ? styles.statusWait : styles.statusMuted;\n\n/** The merchant payments view: who paid, how, and which merchant received it. */\nexport function MerchantPaymentsView({\n startDate,\n endDate,\n currency,\n}: {\n startDate?: string;\n endDate?: string;\n /** The currency filter's choice; empty follows the only one there is. */\n currency: string;\n}) {\n // The filters live in the page address, so this view can be bookmarked or sent to someone.\n const { values, page, set } = useUrlFilters('mp', FILTERS);\n const [search, setSearch] = useState(values.q);\n const [open, setOpen] = useState<MerchantPaymentRow | null>(null);\n\n // Typing waits a moment, so a search is one request and not one per key.\n useEffect(() => {\n const timer = setTimeout(() => {\n if (search.trim() !== values.q) set({ q: search.trim() });\n }, 300);\n\n return () => clearTimeout(timer);\n // Only a change the person typed should write to the address.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [search]);\n\n const filters = useMemo(\n () => ({\n startDate,\n endDate,\n method: values.method,\n status: values.status,\n type: values.type,\n q: values.q,\n merchantId: values.merchant,\n page,\n limit: PAGE_SIZE,\n }),\n [startDate, endDate, values, page],\n );\n const merchants = useMerchantOptions({ startDate, endDate });\n const merchantChoices = [\n { value: '', label: 'Every merchant' },\n ...(merchants.data?.merchants ?? []).map((one) => ({ value: one.id, label: one.name })),\n ];\n\n const { data, isLoading, isError, refetch } = useMerchantPayments(filters);\n\n if (isError) {\n return (\n <LoadFailed\n message=\"Merchant payments could not be loaded. This box may not have them yet.\"\n onRetry={() => void refetch()}\n />\n );\n }\n if (isLoading || !data) return <AdmLoading label=\"Loading merchant payments…\" />;\n\n const currencies = [...new Set(data.cards.current.map((c) => c.currency))];\n const focus = focusCurrency(currencies, currency) ?? 'NGN';\n const now = data.cards.current.find((c) => c.currency === focus);\n const before = data.cards.previous?.find((c) => c.currency === focus) ?? null;\n const money = (minor: string | number) => minorToMoney(minor, focus);\n const delta = (a?: string, b?: string, goodWhenUp = true) =>\n before ? { pct: pctChange(a, b), positiveIsGood: goodWhenUp, caption: 'vs the period before' } : undefined;\n const totalPages = Math.max(Math.ceil(data.total / PAGE_SIZE), 1);\n\n return (\n <div>\n <p className={styles.lede}>\n This is money that customers paid to merchants. Use the filters to narrow it down, and download exactly what you\n see.\n </p>\n\n <section aria-label=\"Merchant payments at a glance\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Money received\"\n value={<CountUp value={Number(now?.grossMinor ?? 0) / 100} format={(v) => money(Math.round(v * 100))} />}\n sub={now ? `${integer.format(now.payments)} payments that went through` : 'No payments in this period'}\n hint=\"Everything customers paid to merchants that went through, before any fees are taken.\"\n delta={delta(now?.grossMinor, before?.grossMinor)}\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Fees taken\"\n value={money(now?.feeMinor ?? 0)}\n hint=\"What the payment gateway and Olive kept from those payments.\"\n delta={delta(now?.feeMinor, before?.feeMinor, false)}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Merchants keep\"\n value={money(now?.netMinor ?? 0)}\n hint=\"What is left for merchants after the fees: money received minus fees taken.\"\n delta={delta(now?.netMinor, before?.netMinor)}\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Payments that worked\"\n value={now?.successRate == null ? '–' : `${now.successRate}%`}\n sub={now?.successRate == null ? 'Nothing was tried in this period' : `Refunded: ${money(now.refundsMinor)}`}\n hint=\"Out of every payment that was tried, the share that went through. A low number can mean a way of paying is broken.\"\n />\n </Reveal>\n </section>\n\n <h2 className={styles.sectionTitle}>How customers paid</h2>\n <p className={styles.sectionNote}>The ways of paying on the checkout, and how much came in through each.</p>\n <MethodCards now={data.byChannel.current} before={data.byChannel.previous} currency={focus} />\n\n <h2 className={styles.sectionTitle}>Merchant revenue</h2>\n <div className={styles.toolbar}>\n <Filters label=\"Filter payments\">\n <FilterSearch\n value={search}\n onChange={setSearch}\n placeholder=\"Search a name, email, phone, reference or merchant\"\n label=\"Search payments\"\n />\n <FilterSelect\n value={values.merchant}\n onChange={(merchant) => set({ merchant })}\n options={merchantChoices}\n label=\"Merchant\"\n searchable\n />\n <FilterSelect value={values.method} onChange={(method) => set({ method })} options={METHODS} label=\"Way of paying\" />\n <FilterSelect value={values.status} onChange={(status) => set({ status })} options={STATUSES} label=\"Payment result\" />\n <FilterSelect value={values.type} onChange={(type) => set({ type })} options={TYPES} label=\"What it was for\" />\n </Filters>\n <DownloadButton\n filename={downloadName('merchant-payments', startDate, endDate)}\n load={() => platformAdminApi.merchantPaymentsCsv({ ...filters, page: undefined, limit: undefined })}\n />\n </div>\n\n {data.rows.length === 0 ? (\n <EmptyNote className={table.empty}>No payments match. Try a wider date range or clear a filter.</EmptyNote>\n ) : (\n <TableShell\n minWidth={980}\n density=\"compact\"\n head={\n <tr>\n <Th>When</Th>\n <Th>Paid by</Th>\n <Th>How</Th>\n <Th>Merchant</Th>\n <Th>For</Th>\n <Th>Result</Th>\n <Th align=\"right\">Amount</Th>\n <Th align=\"right\">Fee</Th>\n <Th align=\"right\">Merchant gets</Th>\n </tr>\n }\n >\n {data.rows.map((row) => (\n <PaymentRow key={row.id} row={row} onOpen={setOpen} />\n ))}\n </TableShell>\n )}\n\n <Pagination page={page} totalPages={totalPages} total={data.total} noun=\"payments\" onPage={(next) => set({ page: next })} />\n <PaymentDrawer row={open} onClose={() => setOpen(null)} />\n </div>\n );\n}\n\nfunction PaymentRow({ row, onOpen }: { row: MerchantPaymentRow; onOpen: (row: MerchantPaymentRow) => void }) {\n const { payer } = row;\n const contact = [payer.email, payer.phone].filter(Boolean).join(' · ');\n const money = (minor: string) => minorToMoney(minor, row.currency);\n\n return (\n <TableRow onClick={() => onOpen(row)}>\n <td className={table.num}>\n {new Date(row.paidAt).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{payer.name || 'Name not recorded'}</span>\n {contact ? <span className={styles.whoSub}>{contact}</span> : null}\n </span>\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{row.channel.label}</span>\n {row.methodDetail ? <span className={styles.whoSub}>{row.methodDetail}</span> : null}\n </span>\n </td>\n <td className={table.name}>\n {row.merchant.name ? <Link href={`/admin/tenants/${row.merchant.id}`}>{row.merchant.name}</Link> : 'Unknown merchant'}\n </td>\n <td>{word(TYPE_WORDS, row.type)}</td>\n <td>\n <span className={`${styles.status} ${tone(row.status)}`}>{word(STATUS_WORDS, row.status)}</span>\n </td>\n <td className={table.numStrong}>{money(row.amountMinor)}</td>\n <td className={table.num}>{money(row.feeMinor)}</td>\n <td className={table.num}>{money(row.netMinor)}</td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,34 +1,27 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { Button, useToast } from "@withpotter/ui-kit";
|
|
4
1
|
import { ADMIN_NAME } from "../_shell/admin-brand";
|
|
5
2
|
import { summaryHtml } from "./summary-print";
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
setTimeout(() => frame.remove(), 1e3);
|
|
27
|
-
}, 150);
|
|
28
|
-
};
|
|
29
|
-
return /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: run, children: "Save as PDF" });
|
|
3
|
+
function printSummary(model, onFail) {
|
|
4
|
+
const frame = document.createElement("iframe");
|
|
5
|
+
frame.setAttribute("aria-hidden", "true");
|
|
6
|
+
frame.style.cssText = "position:fixed;width:0;height:0;border:0;visibility:hidden";
|
|
7
|
+
document.body.appendChild(frame);
|
|
8
|
+
const doc = frame.contentDocument;
|
|
9
|
+
const view = frame.contentWindow;
|
|
10
|
+
if (!doc || !view) {
|
|
11
|
+
frame.remove();
|
|
12
|
+
onFail();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
doc.open();
|
|
16
|
+
doc.write(summaryHtml(model, ADMIN_NAME, (/* @__PURE__ */ new Date()).toLocaleDateString("en-GB", { dateStyle: "long" })));
|
|
17
|
+
doc.close();
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
view.focus();
|
|
20
|
+
view.print();
|
|
21
|
+
setTimeout(() => frame.remove(), 1e3);
|
|
22
|
+
}, 150);
|
|
30
23
|
}
|
|
31
24
|
export {
|
|
32
|
-
|
|
25
|
+
printSummary
|
|
33
26
|
};
|
|
34
27
|
//# sourceMappingURL=print-summary-button.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/print-summary-button.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/print-summary-button.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport { summaryHtml, type SummaryModel } from './summary-print';\n\n/**\n * Opens the one-page summary in the browser's print window, where \"Save as PDF\" makes the file. The page is drawn in a\n * hidden frame, so the admin itself is never printed and no pop-up is needed. `onFail` runs when the frame cannot open.\n */\nexport function printSummary(model: SummaryModel, onFail: () => void): void {\n const frame = document.createElement('iframe');\n frame.setAttribute('aria-hidden', 'true');\n frame.style.cssText = 'position:fixed;width:0;height:0;border:0;visibility:hidden';\n document.body.appendChild(frame);\n\n const doc = frame.contentDocument;\n const view = frame.contentWindow;\n\n if (!doc || !view) {\n frame.remove();\n onFail();\n\n return;\n }\n\n doc.open();\n doc.write(summaryHtml(model, ADMIN_NAME, new Date().toLocaleDateString('en-GB', { dateStyle: 'long' })));\n doc.close();\n\n // Print once the page has laid out, and tidy the frame up when the print window closes.\n setTimeout(() => {\n view.focus();\n view.print();\n setTimeout(() => frame.remove(), 1000);\n }, 150);\n}\n"],"mappings":"AAAA,SAAS,kBAAkB;AAC3B,SAAS,mBAAsC;AAMxC,SAAS,aAAa,OAAqB,QAA0B;AAC1E,QAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,QAAM,aAAa,eAAe,MAAM;AACxC,QAAM,MAAM,UAAU;AACtB,WAAS,KAAK,YAAY,KAAK;AAE/B,QAAM,MAAM,MAAM;AAClB,QAAM,OAAO,MAAM;AAEnB,MAAI,CAAC,OAAO,CAAC,MAAM;AACjB,UAAM,OAAO;AACb,WAAO;AAEP;AAAA,EACF;AAEA,MAAI,KAAK;AACT,MAAI,MAAM,YAAY,OAAO,aAAY,oBAAI,KAAK,GAAE,mBAAmB,SAAS,EAAE,WAAW,OAAO,CAAC,CAAC,CAAC;AACvG,MAAI,MAAM;AAGV,aAAW,MAAM;AACf,SAAK,MAAM;AACX,SAAK,MAAM;AACX,eAAW,MAAM,MAAM,OAAO,GAAG,GAAI;AAAA,EACvC,GAAG,GAAG;AACR;","names":[]}
|
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
import { TimeSeriesChart } from "@withpotter/components/charts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
MoneyLines,
|
|
7
|
+
PICK_CURRENCY,
|
|
8
|
+
PageTabs,
|
|
9
|
+
Panel,
|
|
10
|
+
StatCard,
|
|
11
|
+
moneyIn,
|
|
12
|
+
rowFor,
|
|
13
|
+
toTrend,
|
|
14
|
+
useTabParam
|
|
15
|
+
} from "../_ui";
|
|
6
16
|
import { Reveal } from "../_fx";
|
|
7
17
|
import { Tinted } from "../audit/tinted";
|
|
8
18
|
import { ChannelPanel } from "./channel-panel";
|
|
@@ -18,6 +28,13 @@ import { SubscriptionsPanel } from "./subscriptions-panel";
|
|
|
18
28
|
import { TopTenantsPanel } from "./top-tenants-panel";
|
|
19
29
|
import styles from "./revenue.module.css";
|
|
20
30
|
import ledgerStyles from "./ledger.module.css";
|
|
31
|
+
const VIEWS = [
|
|
32
|
+
{ id: "olive", label: "Money Olive earns" },
|
|
33
|
+
{ id: "customers", label: "How customers pay" },
|
|
34
|
+
{ id: "merchants", label: "Money merchants earn" },
|
|
35
|
+
{ id: "plans", label: "Plan subscriptions" }
|
|
36
|
+
];
|
|
37
|
+
const VIEW_KEYS = VIEWS.map((v) => v.id);
|
|
21
38
|
function RevenueOverview({
|
|
22
39
|
data,
|
|
23
40
|
health,
|
|
@@ -32,13 +49,76 @@ function RevenueOverview({
|
|
|
32
49
|
ledger,
|
|
33
50
|
plans
|
|
34
51
|
}) {
|
|
52
|
+
const [view, setView] = useTabParam(VIEW_KEYS, "view");
|
|
35
53
|
const gmv = rowFor(data.gmvByCurrency, focus);
|
|
36
54
|
const gmvSeries = useMemo(() => toTrend(gmv?.series, "gmv"), [gmv]);
|
|
37
55
|
const gmvCaption = focus ? `${period} \xB7 ${focus}` : period;
|
|
38
56
|
const recurringTracked = data.recurring.tracked !== false;
|
|
39
57
|
const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);
|
|
40
|
-
|
|
41
|
-
/* @__PURE__ */
|
|
58
|
+
const olive = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
59
|
+
/* @__PURE__ */ jsxs("p", { className: ledgerStyles.sectionNote, children: [
|
|
60
|
+
"What merchants pay Olive: their plan payments, and the fees Olive keeps.",
|
|
61
|
+
recurringTracked ? "" : " Plan income is not set up on this box yet."
|
|
62
|
+
] }),
|
|
63
|
+
/* @__PURE__ */ jsx(
|
|
64
|
+
RevenueKpis,
|
|
65
|
+
{
|
|
66
|
+
part: "olive",
|
|
67
|
+
data,
|
|
68
|
+
money,
|
|
69
|
+
money2,
|
|
70
|
+
currency,
|
|
71
|
+
focus,
|
|
72
|
+
gmvSpark: []
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
76
|
+
StatCard,
|
|
77
|
+
{
|
|
78
|
+
label: "Fees Olive keeps",
|
|
79
|
+
value: /* @__PURE__ */ jsx(
|
|
80
|
+
MoneyLines,
|
|
81
|
+
{
|
|
82
|
+
rows: platformFees,
|
|
83
|
+
currency: "",
|
|
84
|
+
amount: (r) => r.amount,
|
|
85
|
+
decimals: 2
|
|
86
|
+
}
|
|
87
|
+
),
|
|
88
|
+
hint: "What Olive keeps from payouts and plan payments, after costs."
|
|
89
|
+
}
|
|
90
|
+
) }) }),
|
|
91
|
+
recurringTracked && /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
|
|
92
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(HealthPanel, { health, money, caption: recurringCaption }) }),
|
|
93
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-5", children: plans ? /* @__PURE__ */ jsx(
|
|
94
|
+
PlansPanel,
|
|
95
|
+
{
|
|
96
|
+
lines: planLines(plans.plans, plans.currencies, data.recurring.planBreakdown),
|
|
97
|
+
money,
|
|
98
|
+
caption: recurringCaption
|
|
99
|
+
}
|
|
100
|
+
) : /* @__PURE__ */ jsx(
|
|
101
|
+
PlanMixPanel,
|
|
102
|
+
{
|
|
103
|
+
planBreakdown: data.recurring.planBreakdown,
|
|
104
|
+
money,
|
|
105
|
+
caption: recurringCaption
|
|
106
|
+
}
|
|
107
|
+
) })
|
|
108
|
+
] })
|
|
109
|
+
] });
|
|
110
|
+
const customers = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
111
|
+
/* @__PURE__ */ jsx("p", { className: ledgerStyles.sectionNote, children: "The ways of paying on the checkout, and how much came in through each." }),
|
|
112
|
+
ledger && focus ? /* @__PURE__ */ jsx(
|
|
113
|
+
MethodCards,
|
|
114
|
+
{
|
|
115
|
+
now: ledger.byChannel.current,
|
|
116
|
+
before: ledger.byChannel.previous,
|
|
117
|
+
currency: focus
|
|
118
|
+
}
|
|
119
|
+
) : channels && channels.length > 0 ? /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(ChannelPanel, { totals: channels, focus, caption: gmvCaption }) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: focus ? "Nothing to show for this period." : PICK_CURRENCY })
|
|
120
|
+
] });
|
|
121
|
+
const merchants = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
42
122
|
/* @__PURE__ */ jsx("p", { className: ledgerStyles.sectionNote, children: "What customers paid to merchants. This is the merchants' money, not Olive's own income." }),
|
|
43
123
|
/* @__PURE__ */ jsx(
|
|
44
124
|
RevenueKpis,
|
|
@@ -72,17 +152,6 @@ function RevenueOverview({
|
|
|
72
152
|
) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: PICK_CURRENCY })
|
|
73
153
|
}
|
|
74
154
|
) }) }),
|
|
75
|
-
ledger && focus ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
76
|
-
/* @__PURE__ */ jsx("h3", { className: ledgerStyles.subTitle, children: "How customers paid" }),
|
|
77
|
-
/* @__PURE__ */ jsx(
|
|
78
|
-
MethodCards,
|
|
79
|
-
{
|
|
80
|
-
now: ledger.byChannel.current,
|
|
81
|
-
before: ledger.byChannel.previous,
|
|
82
|
-
currency: focus
|
|
83
|
-
}
|
|
84
|
-
)
|
|
85
|
-
] }) : channels && channels.length > 0 && /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(ChannelPanel, { totals: channels, focus, caption: gmvCaption }) }) }),
|
|
86
155
|
/* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
|
|
87
156
|
TopTenantsPanel,
|
|
88
157
|
{
|
|
@@ -90,68 +159,15 @@ function RevenueOverview({
|
|
|
90
159
|
lifetime: gmv?.lifetime ?? 0,
|
|
91
160
|
focus
|
|
92
161
|
}
|
|
93
|
-
) }) })
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
part: "olive",
|
|
103
|
-
data,
|
|
104
|
-
money,
|
|
105
|
-
money2,
|
|
106
|
-
currency,
|
|
107
|
-
focus,
|
|
108
|
-
gmvSpark: []
|
|
109
|
-
}
|
|
110
|
-
),
|
|
111
|
-
platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
112
|
-
StatCard,
|
|
113
|
-
{
|
|
114
|
-
label: "Fees Olive keeps",
|
|
115
|
-
value: /* @__PURE__ */ jsx(
|
|
116
|
-
MoneyLines,
|
|
117
|
-
{
|
|
118
|
-
rows: platformFees,
|
|
119
|
-
currency: "",
|
|
120
|
-
amount: (r) => r.amount,
|
|
121
|
-
decimals: 2
|
|
122
|
-
}
|
|
123
|
-
),
|
|
124
|
-
hint: "What Olive keeps from payouts and plan payments, after costs."
|
|
125
|
-
}
|
|
126
|
-
) }) }),
|
|
127
|
-
recurringTracked && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
128
|
-
/* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
|
|
129
|
-
/* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(
|
|
130
|
-
HealthPanel,
|
|
131
|
-
{
|
|
132
|
-
health,
|
|
133
|
-
money,
|
|
134
|
-
caption: recurringCaption
|
|
135
|
-
}
|
|
136
|
-
) }),
|
|
137
|
-
/* @__PURE__ */ jsx("div", { className: "adm-span-5", children: plans ? /* @__PURE__ */ jsx(
|
|
138
|
-
PlansPanel,
|
|
139
|
-
{
|
|
140
|
-
lines: planLines(plans.plans, plans.currencies, data.recurring.planBreakdown),
|
|
141
|
-
money,
|
|
142
|
-
caption: recurringCaption
|
|
143
|
-
}
|
|
144
|
-
) : /* @__PURE__ */ jsx(
|
|
145
|
-
PlanMixPanel,
|
|
146
|
-
{
|
|
147
|
-
planBreakdown: data.recurring.planBreakdown,
|
|
148
|
-
money,
|
|
149
|
-
caption: recurringCaption
|
|
150
|
-
}
|
|
151
|
-
) })
|
|
152
|
-
] }),
|
|
153
|
-
/* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(SubscriptionsPanel, { recurring: data.recurring, span }) }) })
|
|
154
|
-
] })
|
|
162
|
+
) }) })
|
|
163
|
+
] });
|
|
164
|
+
const planSubscriptions = recurringTracked ? /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(SubscriptionsPanel, { recurring: data.recurring, span }) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "Plan income is not set up on this box yet." });
|
|
165
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
166
|
+
/* @__PURE__ */ jsx(PageTabs, { tabs: VIEWS, active: view, onChange: setView }),
|
|
167
|
+
view === "olive" && olive,
|
|
168
|
+
view === "customers" && customers,
|
|
169
|
+
view === "merchants" && merchants,
|
|
170
|
+
view === "plans" && planSubscriptions
|
|
155
171
|
] });
|
|
156
172
|
}
|
|
157
173
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type {\n GatewayChannelTotal,\n MerchantPaymentsPage,\n PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\nimport type { PlanList } from '@withpotter/lib/pricing-plans-types';\nimport { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend } from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\nimport { MerchantHeadline } from './merchant-headline';\nimport { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { planLines } from './plan-lines';\nimport { PlansPanel } from './plans-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\nimport ledgerStyles from './ledger.module.css';\n\n/**\n * Overview tab: the four headline figures, then the GMV trend as the one\n * full-width primary chart, then revenue health, plan mix, top merchants\n * and subscriptions beneath it. Merchant GMV is per currency: the chart and\n * the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n channels,\n ledger,\n plans,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n /** How customers paid, per channel and currency. Absent when the box cannot say. */\n channels?: readonly GatewayChannelTotal[];\n /** The merchant payments view's totals, with the period before; absent on a box without it. */\n ledger?: MerchantPaymentsPage;\n /** Every plan created, from Pricing. Absent while it loads or when it cannot be read. */\n plans?: PlanList;\n}) {\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n return (\n <>\n <h2 className={ledgerStyles.sectionTitle}>Money merchants earned</h2>\n <p className={ledgerStyles.sectionNote}>\n What customers paid to merchants. This is the merchants' money, not Olive's own income.\n </p>\n <RevenueKpis\n part=\"merchant\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n extra={ledger && focus ? <MerchantHeadline ledger={ledger} focus={focus} /> : null}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"Merchant sales over time\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n {ledger && focus ? (\n <>\n <h3 className={ledgerStyles.subTitle}>How customers paid</h3>\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n </>\n ) : (\n channels &&\n channels.length > 0 && (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <ChannelPanel totals={channels} focus={focus} caption={gmvCaption} />\n </div>\n </div>\n )\n )}\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n </div>\n\n <h2 className={ledgerStyles.sectionTitle}>Money Olive earns</h2>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive: their plan payments, and the fees Olive keeps.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Fees Olive keeps\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts and plan payments, after costs.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <>\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel\n health={health}\n money={money}\n caption={recurringCaption}\n />\n </div>\n <div className=\"adm-span-5\">\n {plans ? (\n <PlansPanel\n lines={planLines(plans.plans, plans.currencies, data.recurring.planBreakdown)}\n money={money}\n caption={recurringCaption}\n />\n ) : (\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n )}\n </div>\n </div>\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n </>\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type {\n GatewayChannelTotal,\n MerchantPaymentsPage,\n PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\nimport type { PlanList } from '@withpotter/lib/pricing-plans-types';\nimport {\n MoneyLines,\n PICK_CURRENCY,\n PageTabs,\n Panel,\n StatCard,\n moneyIn,\n rowFor,\n toTrend,\n useTabParam,\n} from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\nimport { MerchantHeadline } from './merchant-headline';\nimport { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { planLines } from './plan-lines';\nimport { PlansPanel } from './plans-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\nimport ledgerStyles from './ledger.module.css';\n\n/** The Overview's own tabs, in the order the money is read: Olive's, how customers pay, merchants', then plans. */\nconst VIEWS = [\n { id: 'olive', label: 'Money Olive earns' },\n { id: 'customers', label: 'How customers pay' },\n { id: 'merchants', label: 'Money merchants earn' },\n { id: 'plans', label: 'Plan subscriptions' },\n] as const;\n\nconst VIEW_KEYS = VIEWS.map((v) => v.id);\n\n/**\n * Overview tab, one view at a time: what Olive earns (headline figures, fees, revenue health and plan mix), how\n * customers pay, what merchants earn (the GMV trend and the leaderboard), and the plan subscriptions. Merchant GMV is\n * per currency: the chart and the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n channels,\n ledger,\n plans,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n /** How customers paid, per channel and currency. Absent when the box cannot say. */\n channels?: readonly GatewayChannelTotal[];\n /** The merchant payments view's totals, with the period before; absent on a box without it. */\n ledger?: MerchantPaymentsPage;\n /** Every plan created, from Pricing. Absent while it loads or when it cannot be read. */\n plans?: PlanList;\n}) {\n const [view, setView] = useTabParam(VIEW_KEYS, 'view');\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n const olive = (\n <>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive: their plan payments, and the fees Olive keeps.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Fees Olive keeps\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts and plan payments, after costs.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel health={health} money={money} caption={recurringCaption} />\n </div>\n <div className=\"adm-span-5\">\n {plans ? (\n <PlansPanel\n lines={planLines(plans.plans, plans.currencies, data.recurring.planBreakdown)}\n money={money}\n caption={recurringCaption}\n />\n ) : (\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n )}\n </div>\n </div>\n )}\n </>\n );\n\n const customers = (\n <>\n <p className={ledgerStyles.sectionNote}>\n The ways of paying on the checkout, and how much came in through each.\n </p>\n {ledger && focus ? (\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n ) : channels && channels.length > 0 ? (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <ChannelPanel totals={channels} focus={focus} caption={gmvCaption} />\n </div>\n </div>\n ) : (\n <p className={styles.empty}>{focus ? 'Nothing to show for this period.' : PICK_CURRENCY}</p>\n )}\n </>\n );\n\n const merchants = (\n <>\n <p className={ledgerStyles.sectionNote}>\n What customers paid to merchants. This is the merchants' money, not Olive's own income.\n </p>\n <RevenueKpis\n part=\"merchant\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n extra={ledger && focus ? <MerchantHeadline ledger={ledger} focus={focus} /> : null}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"Merchant sales over time\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n </div>\n </>\n );\n\n const planSubscriptions = recurringTracked ? (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n ) : (\n <p className={styles.empty}>Plan income is not set up on this box yet.</p>\n );\n\n return (\n <>\n <PageTabs tabs={VIEWS} active={view} onChange={setView} />\n {view === 'olive' && olive}\n {view === 'customers' && customers}\n {view === 'merchants' && merchants}\n {view === 'plans' && planSubscriptions}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
useTabParam
|
|
26
26
|
} from "../_ui";
|
|
27
27
|
import { MerchantPaymentsView } from "./merchant-payments-view";
|
|
28
|
-
import { MerchantsView } from "./merchants-view";
|
|
29
28
|
import { usePlanList } from "../pricing/use-plans";
|
|
30
29
|
import { RevenueOverview } from "./revenue-overview";
|
|
31
30
|
import { RevenueRetention } from "./revenue-retention";
|
|
@@ -34,7 +33,6 @@ import { SummaryDownload } from "./summary-download";
|
|
|
34
33
|
const TABS = [
|
|
35
34
|
{ id: "overview", label: "Overview" },
|
|
36
35
|
{ id: "merchants", label: "Merchant payments" },
|
|
37
|
-
{ id: "by-merchant", label: "Merchants" },
|
|
38
36
|
{ id: "subscriptions", label: "Subscriptions" },
|
|
39
37
|
{ id: "retention", label: "Retention" }
|
|
40
38
|
];
|
|
@@ -101,8 +99,6 @@ function RevenueWorkspace() {
|
|
|
101
99
|
currency
|
|
102
100
|
}
|
|
103
101
|
);
|
|
104
|
-
} else if (tab === "by-merchant") {
|
|
105
|
-
body = /* @__PURE__ */ jsx(MerchantsView, { startDate: range.startDate, endDate: range.endDate });
|
|
106
102
|
} else if (tab === "subscriptions") {
|
|
107
103
|
body = /* @__PURE__ */ jsx(
|
|
108
104
|
SubscriptionsView,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport {\n useGatewayChannels,\n useMerchantPayments,\n usePlatformCockpit,\n usePlatformRevenue,\n} from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { MerchantPaymentsView } from './merchant-payments-view';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport {\n useGatewayChannels,\n useMerchantPayments,\n usePlatformCockpit,\n usePlatformRevenue,\n} from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { MerchantPaymentsView } from './merchant-payments-view';\nimport { usePlanList } from '../pricing/use-plans';\nimport { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\nimport { SubscriptionsView } from './subscriptions-view';\nimport { SummaryDownload } from './summary-download';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'merchants', label: 'Merchant payments' },\n { id: 'subscriptions', label: 'Subscriptions' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * Revenue: the whole picture on Overview, what customers paid merchants on\n * Merchant payments, what merchants pay Olive on Subscriptions, and revenue\n * quality and cohorts on Retention.\n */\nexport function RevenueWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const {\n rangeKey,\n setRangeKey,\n customStart,\n customEnd,\n onCustomChange,\n } = usePersistedDateRange();\n // Merchant GMV comes back once per currency; `?currency=` narrows it.\n const [currency, setCurrency] = useCurrencyParam();\n\n const range = useMemo(\n () =>\n rangeKey === 'custom'\n ? customRange(customStart, customEnd)\n : rangeFromKey(rangeKey),\n [rangeKey, customStart, customEnd],\n );\n\n const { data, isLoading, isError, refetch } = usePlatformRevenue({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n // Revenue health (MRR movement, logo churn, concentration) is computed as\n // part of the cockpit, not the revenue endpoint — fetched here too rather\n // than duplicating that aggregation.\n const plans = usePlanList();\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n const channels = useGatewayChannels({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n // The same totals the Merchant payments tab reads, one row only, for the method cards.\n const ledger = useMerchantPayments({\n startDate: range.startDate,\n endDate: range.endDate,\n limit: 1,\n });\n\n const currencies = useMemo(() => data?.currencies ?? [], [data]);\n const focus = focusCurrency(currencies, currency);\n // Recurring revenue is the platform's own billing, in the currency the API\n // names for it (`currency`), shown as it comes, never converted.\n const recurringCurrency = data?.currency || 'NGN';\n const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);\n const money2 = useMemo(\n () => moneyIn(recurringCurrency, 2),\n [recurringCurrency],\n );\n\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n\n let body;\n if (isError) {\n body = (\n <LoadFailed\n message=\"Revenue for this period could not be loaded.\"\n onRetry={() => void refetch()}\n />\n );\n } else if (isLoading || !data) {\n body = <AdmLoading label=\"Loading revenue metrics…\" />;\n } else if (tab === 'merchants') {\n body = (\n <MerchantPaymentsView\n startDate={range.startDate}\n endDate={range.endDate}\n currency={currency}\n />\n );\n } else if (tab === 'subscriptions') {\n body = (\n <SubscriptionsView\n startDate={range.startDate}\n endDate={range.endDate}\n revenue={data}\n />\n );\n } else if (tab === 'retention') {\n body = (\n <RevenueRetention\n quality={data.quality}\n money={money}\n currency={currency}\n caption={currency ? `${period} · ${currency}` : period}\n />\n );\n } else {\n body = (\n <RevenueOverview\n data={data}\n health={cockpit.data?.health}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n period={period}\n span={span}\n recurringCaption={`${period} · ${recurringCurrency}`}\n channels={channels.data?.byChannel}\n ledger={ledger.data}\n plans={plans.data}\n />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`}\n actions={\n <div className=\"adm-toolbar\">\n {tab === 'overview' ? (\n <SummaryDownload\n ledger={ledger.data}\n revenue={data}\n currency={focus ?? 'NGN'}\n startDate={range.startDate}\n endDate={range.endDate}\n period={period}\n />\n ) : null}\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n <PeriodSelector\n value={rangeKey}\n onChange={setRangeKey}\n customStart={customStart}\n customEnd={customEnd}\n onCustomChange={onCustomChange}\n />\n </div>\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {body}\n </div>\n );\n}\n"],"mappings":";AA6GM,cAyDI,YAzDJ;AA3GN,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AACrC,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAEhC,MAAM,OAAO;AAAA,EACX,EAAE,IAAI,YAAY,OAAO,WAAW;AAAA,EACpC,EAAE,IAAI,aAAa,OAAO,oBAAoB;AAAA,EAC9C,EAAE,IAAI,iBAAiB,OAAO,gBAAgB;AAAA,EAC9C,EAAE,IAAI,aAAa,OAAO,YAAY;AACxC;AAEA,MAAM,OAAO,KAAK,IAAI,CAAC,MAAM,EAAE,EAAE;AAO1B,SAAS,mBAAmB;AACjC,QAAM,CAAC,KAAK,MAAM,IAAI,YAAY,IAAI;AACtC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,sBAAsB;AAE1B,QAAM,CAAC,UAAU,WAAW,IAAI,iBAAiB;AAEjD,QAAM,QAAQ;AAAA,IACZ,MACE,aAAa,WACT,YAAY,aAAa,SAAS,IAClC,aAAa,QAAQ;AAAA,IAC3B,CAAC,UAAU,aAAa,SAAS;AAAA,EACnC;AAEA,QAAM,EAAE,MAAM,WAAW,SAAS,QAAQ,IAAI,mBAAmB;AAAA,IAC/D,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AAID,QAAM,QAAQ,YAAY;AAC1B,QAAM,UAAU,mBAAmB;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AAED,QAAM,WAAW,mBAAmB;AAAA,IAClC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AAGD,QAAM,SAAS,oBAAoB;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,IACf,OAAO;AAAA,EACT,CAAC;AAED,QAAM,aAAa,QAAQ,MAAM,MAAM,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;AAC/D,QAAM,QAAQ,cAAc,YAAY,QAAQ;AAGhD,QAAM,oBAAoB,MAAM,YAAY;AAC5C,QAAM,QAAQ,QAAQ,MAAM,QAAQ,iBAAiB,GAAG,CAAC,iBAAiB,CAAC;AAC3E,QAAM,SAAS;AAAA,IACb,MAAM,QAAQ,mBAAmB,CAAC;AAAA,IAClC,CAAC,iBAAiB;AAAA,EACpB;AAEA,QAAM,OAAO,aAAa,KAAK;AAC/B,QAAM,SAAS,GAAG,KAAK,CAAC,EAAE,YAAY,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;AAEvD,MAAI;AACJ,MAAI,SAAS;AACX,WACE;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,SAAS,MAAM,KAAK,QAAQ;AAAA;AAAA,IAC9B;AAAA,EAEJ,WAAW,aAAa,CAAC,MAAM;AAC7B,WAAO,oBAAC,cAAW,OAAM,iCAA2B;AAAA,EACtD,WAAW,QAAQ,aAAa;AAC9B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,MAAM;AAAA,QACjB,SAAS,MAAM;AAAA,QACf;AAAA;AAAA,IACF;AAAA,EAEJ,WAAW,QAAQ,iBAAiB;AAClC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,MAAM;AAAA,QACjB,SAAS,MAAM;AAAA,QACf,SAAS;AAAA;AAAA,IACX;AAAA,EAEJ,WAAW,QAAQ,aAAa;AAC9B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,SAAS,KAAK;AAAA,QACd;AAAA,QACA;AAAA,QACA,SAAS,WAAW,GAAG,MAAM,SAAM,QAAQ,KAAK;AAAA;AAAA,IAClD;AAAA,EAEJ,OAAO;AACL,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,QAAQ,QAAQ,MAAM;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,kBAAkB,GAAG,MAAM,SAAM,iBAAiB;AAAA,QAClD,UAAU,SAAS,MAAM;AAAA,QACzB,QAAQ,OAAO;AAAA,QACf,OAAO,MAAM;AAAA;AAAA,IACf;AAAA,EAEJ;AAEA,SACE,qBAAC,SACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,aAAa,gCAAgC,UAAU,0DAA0D,UAAU;AAAA,QAC3H,SACE,qBAAC,SAAI,WAAU,eACZ;AAAA,kBAAQ,aACP;AAAA,YAAC;AAAA;AAAA,cACC,QAAQ,OAAO;AAAA,cACf,SAAS;AAAA,cACT,UAAU,SAAS;AAAA,cACnB,WAAW,MAAM;AAAA,cACjB,SAAS,MAAM;AAAA,cACf;AAAA;AAAA,UACF,IACE;AAAA,UACJ;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV;AAAA;AAAA,UACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV;AAAA,cACA;AAAA,cACA;AAAA;AAAA,UACF;AAAA,WACF;AAAA;AAAA,IAEJ;AAAA,IACA,oBAAC,YAAS,MAAM,MAAM,QAAQ,KAAK,UAAU,QAAQ;AAAA,IACpD;AAAA,KACH;AAEJ;","names":[]}
|
|
@@ -50,7 +50,7 @@ function SubscriptionsPanel({
|
|
|
50
50
|
value: integer.format(recurring.cancelledSubscriptions)
|
|
51
51
|
}
|
|
52
52
|
];
|
|
53
|
-
return /* @__PURE__ */ jsx(Panel, { title: "Plan subscriptions
|
|
53
|
+
return /* @__PURE__ */ jsx(Panel, { title: "Plan subscriptions", caption: "Right now", height: "var(--adm-panel-h)", children: /* @__PURE__ */ jsx(FigureList, { rows }) });
|
|
54
54
|
}
|
|
55
55
|
export {
|
|
56
56
|
SubscriptionsPanel
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/subscriptions-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Where the subscription base stands, and what started in the window. */\nexport function SubscriptionsPanel({\n recurring,\n span,\n}: {\n recurring: PlatformRevenue['recurring'];\n span: string;\n}) {\n const rows: Figure[] = [\n {\n label: 'Payment missed',\n status: 'past_due',\n note: 'Their last payment failed, so they may leave',\n value: integer.format(recurring.pastDueSubscriptions),\n tone: recurring.pastDueSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Set to end',\n status: 'non_renewing',\n note: 'Will not renew when the paid period ends',\n value: integer.format(recurring.nonRenewingSubscriptions),\n tone: recurring.nonRenewingSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Active',\n status: 'active',\n note: 'Paying for their plan',\n value: integer.format(recurring.activeSubscriptions),\n },\n {\n label: 'On a trial',\n status: 'trialing',\n note: 'Trying a plan, not yet paying',\n value: integer.format(recurring.trialingSubscriptions),\n },\n {\n label: 'Joined this period',\n note: `Started paying in ${span}`,\n value: integer.format(recurring.newInRange),\n },\n {\n label: 'Left this period',\n note: `Stopped paying in ${span}`,\n value: integer.format(recurring.churnedInRange),\n tone: recurring.churnedInRange > 0 ? 'danger' : 'default',\n },\n {\n label: 'Cancelled in total',\n note: 'Since the start',\n value: integer.format(recurring.cancelledSubscriptions),\n },\n ];\n\n return (\n <Panel title=\"Plan subscriptions
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/subscriptions-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Where the subscription base stands, and what started in the window. */\nexport function SubscriptionsPanel({\n recurring,\n span,\n}: {\n recurring: PlatformRevenue['recurring'];\n span: string;\n}) {\n const rows: Figure[] = [\n {\n label: 'Payment missed',\n status: 'past_due',\n note: 'Their last payment failed, so they may leave',\n value: integer.format(recurring.pastDueSubscriptions),\n tone: recurring.pastDueSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Set to end',\n status: 'non_renewing',\n note: 'Will not renew when the paid period ends',\n value: integer.format(recurring.nonRenewingSubscriptions),\n tone: recurring.nonRenewingSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Active',\n status: 'active',\n note: 'Paying for their plan',\n value: integer.format(recurring.activeSubscriptions),\n },\n {\n label: 'On a trial',\n status: 'trialing',\n note: 'Trying a plan, not yet paying',\n value: integer.format(recurring.trialingSubscriptions),\n },\n {\n label: 'Joined this period',\n note: `Started paying in ${span}`,\n value: integer.format(recurring.newInRange),\n },\n {\n label: 'Left this period',\n note: `Stopped paying in ${span}`,\n value: integer.format(recurring.churnedInRange),\n tone: recurring.churnedInRange > 0 ? 'danger' : 'default',\n },\n {\n label: 'Cancelled in total',\n note: 'Since the start',\n value: integer.format(recurring.cancelledSubscriptions),\n },\n ];\n\n return (\n <Panel title=\"Plan subscriptions\" caption=\"Right now\" height=\"var(--adm-panel-h)\">\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AA6DM;AA5DN,SAAS,aAAa;AACtB,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AACF,GAGG;AACD,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,oBAAoB;AAAA,MACpD,MAAM,UAAU,uBAAuB,IAAI,WAAW;AAAA,IACxD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,wBAAwB;AAAA,MACxD,MAAM,UAAU,2BAA2B,IAAI,WAAW;AAAA,IAC5D;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,mBAAmB;AAAA,IACrD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,qBAAqB;AAAA,IACvD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM,qBAAqB,IAAI;AAAA,MAC/B,OAAO,QAAQ,OAAO,UAAU,UAAU;AAAA,IAC5C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM,qBAAqB,IAAI;AAAA,MAC/B,OAAO,QAAQ,OAAO,UAAU,cAAc;AAAA,MAC9C,MAAM,UAAU,iBAAiB,IAAI,WAAW;AAAA,IAClD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,sBAAsB;AAAA,IACxD;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,sBAAqB,SAAQ,aAAY,QAAO,sBAC3D,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { DropdownMenu, useToast } from "@withpotter/ui-kit";
|
|
3
4
|
import { toCsv } from "@withpotter/lib/csv";
|
|
4
|
-
import {
|
|
5
|
+
import { saveCsv } from "./download-button";
|
|
5
6
|
import { downloadName } from "./ledger-model";
|
|
6
|
-
import {
|
|
7
|
+
import { printSummary } from "./print-summary-button";
|
|
7
8
|
import { SUMMARY_HEAD, summaryRows } from "./summary-rows";
|
|
8
9
|
import { summaryModel } from "./summary-print";
|
|
10
|
+
const CSV = "Download CSV";
|
|
11
|
+
const PDF = "Save PDF";
|
|
9
12
|
function SummaryDownload({
|
|
10
13
|
ledger,
|
|
11
14
|
revenue,
|
|
@@ -14,19 +17,20 @@ function SummaryDownload({
|
|
|
14
17
|
endDate,
|
|
15
18
|
period
|
|
16
19
|
}) {
|
|
20
|
+
const { toast } = useToast();
|
|
17
21
|
const rows = summaryRows(ledger, revenue, currency);
|
|
18
22
|
if (rows.length === 0) return null;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
] });
|
|
23
|
+
const choose = (item) => {
|
|
24
|
+
if (item === CSV) {
|
|
25
|
+
saveCsv(downloadName("revenue-summary", startDate, endDate), toCsv([SUMMARY_HEAD, ...rows]));
|
|
26
|
+
} else {
|
|
27
|
+
printSummary(
|
|
28
|
+
summaryModel(ledger, revenue, currency, period),
|
|
29
|
+
() => toast({ title: "The summary could not be opened. Please try again." })
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return /* @__PURE__ */ jsx(DropdownMenu, { label: "Download", items: [CSV, PDF], onSelect: choose });
|
|
30
34
|
}
|
|
31
35
|
export {
|
|
32
36
|
SummaryDownload
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/summary-download.tsx"],"sourcesContent":["'use client';\n\nimport { toCsv } from '@withpotter/lib/csv';\nimport type { MerchantPaymentsPage, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/summary-download.tsx"],"sourcesContent":["'use client';\n\nimport { DropdownMenu, useToast } from '@withpotter/ui-kit';\nimport { toCsv } from '@withpotter/lib/csv';\nimport type { MerchantPaymentsPage, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { saveCsv } from './download-button';\nimport { downloadName } from './ledger-model';\nimport { printSummary } from './print-summary-button';\nimport { SUMMARY_HEAD, summaryRows } from './summary-rows';\nimport { summaryModel } from './summary-print';\n\nconst CSV = 'Download CSV';\nconst PDF = 'Save PDF';\n\n/** The figures on the Overview as a small spreadsheet or a one-page PDF (through the print window), from one menu. */\nexport function SummaryDownload({\n ledger,\n revenue,\n currency,\n startDate,\n endDate,\n period,\n}: {\n ledger?: MerchantPaymentsPage;\n revenue?: PlatformRevenue;\n currency: string;\n startDate?: string;\n endDate?: string;\n /** The period in words, such as \"last 30 days\". */\n period: string;\n}) {\n const { toast } = useToast();\n const rows = summaryRows(ledger, revenue, currency);\n\n if (rows.length === 0) return null;\n\n const choose = (item: string) => {\n if (item === CSV) {\n saveCsv(downloadName('revenue-summary', startDate, endDate), toCsv([SUMMARY_HEAD, ...rows]));\n } else {\n printSummary(summaryModel(ledger, revenue, currency, period), () =>\n toast({ title: 'The summary could not be opened. Please try again.' }),\n );\n }\n };\n\n return <DropdownMenu label=\"Download\" items={[CSV, PDF]} onSelect={choose} />;\n}\n"],"mappings":";AA8CS;AA5CT,SAAS,cAAc,gBAAgB;AACvC,SAAS,aAAa;AAEtB,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,cAAc,mBAAmB;AAC1C,SAAS,oBAAoB;AAE7B,MAAM,MAAM;AACZ,MAAM,MAAM;AAGL,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,OAAO,YAAY,QAAQ,SAAS,QAAQ;AAElD,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,SAAS,CAAC,SAAiB;AAC/B,QAAI,SAAS,KAAK;AAChB,cAAQ,aAAa,mBAAmB,WAAW,OAAO,GAAG,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;AAAA,IAC7F,OAAO;AACL;AAAA,QAAa,aAAa,QAAQ,SAAS,UAAU,MAAM;AAAA,QAAG,MAC5D,MAAM,EAAE,OAAO,qDAAqD,CAAC;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AAEA,SAAO,oBAAC,gBAAa,OAAM,YAAW,OAAO,CAAC,KAAK,GAAG,GAAG,UAAU,QAAQ;AAC7E;","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.114",
|
|
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.114",
|
|
35
|
+
"@withpotter/platform-admin": "^0.13.0-next.114",
|
|
36
|
+
"@withpotter/ui-kit": "^0.13.0-next.114",
|
|
37
37
|
"lucide-react": "^0.562.0",
|
|
38
38
|
"motion": "^12.42.2"
|
|
39
39
|
},
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useState } from "react";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { EmptyNote, TableRow, TableShell, Th } from "@withpotter/ui-kit";
|
|
6
|
-
import { useMerchantRevenue } from "../hooks";
|
|
7
|
-
import { LoadFailed } from "../audit/load-failed";
|
|
8
|
-
import { AdmLoading, FilterSearch, Filters, Pagination } from "../_ui";
|
|
9
|
-
import { minorToMoney } from "./ledger-model";
|
|
10
|
-
import { useUrlFilters } from "./use-url-filters";
|
|
11
|
-
import styles from "./ledger.module.css";
|
|
12
|
-
import table from "./revenue.module.css";
|
|
13
|
-
const PAGE_SIZE = 25;
|
|
14
|
-
const FILTERS = { q: "" };
|
|
15
|
-
const integer = new Intl.NumberFormat("en-US");
|
|
16
|
-
const METHOD_COLOUR = {
|
|
17
|
-
card: "#4f7a28",
|
|
18
|
-
ussd: "#c68a1b",
|
|
19
|
-
transfer: "#2f6f9f",
|
|
20
|
-
opay: "#7a4fa0"
|
|
21
|
-
};
|
|
22
|
-
function MerchantsView({ startDate, endDate }) {
|
|
23
|
-
const { values, page, set } = useUrlFilters("mr", FILTERS);
|
|
24
|
-
const [search, setSearch] = useState(values.q);
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const timer = setTimeout(() => {
|
|
27
|
-
if (search.trim() !== values.q) set({ q: search.trim() });
|
|
28
|
-
}, 300);
|
|
29
|
-
return () => clearTimeout(timer);
|
|
30
|
-
}, [search]);
|
|
31
|
-
const filters = useMemo(
|
|
32
|
-
() => ({ startDate, endDate, q: values.q, page, limit: PAGE_SIZE }),
|
|
33
|
-
[startDate, endDate, values.q, page]
|
|
34
|
-
);
|
|
35
|
-
const { data, isLoading, isError, refetch } = useMerchantRevenue(filters);
|
|
36
|
-
if (isError) {
|
|
37
|
-
return /* @__PURE__ */ jsx(
|
|
38
|
-
LoadFailed,
|
|
39
|
-
{
|
|
40
|
-
message: "The merchants could not be loaded. This box may not have this view yet.",
|
|
41
|
-
onRetry: () => void refetch()
|
|
42
|
-
}
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (isLoading || !data) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading merchants\u2026" });
|
|
46
|
-
const hasMore = data.rows.length === PAGE_SIZE;
|
|
47
|
-
return /* @__PURE__ */ jsxs("div", { children: [
|
|
48
|
-
/* @__PURE__ */ jsx("p", { className: styles.lede, children: "This shows which merchants received the most money in this period, and how their customers paid them. Open a merchant to see each payment." }),
|
|
49
|
-
/* @__PURE__ */ jsx("div", { className: styles.toolbar, children: /* @__PURE__ */ jsx(Filters, { label: "Filter merchants", children: /* @__PURE__ */ jsx(FilterSearch, { value: search, onChange: setSearch, placeholder: "Search a merchant by name", label: "Search merchants" }) }) }),
|
|
50
|
-
data.rows.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: table.empty, children: "No merchant took a payment in this period. Try a wider date range." }) : /* @__PURE__ */ jsx(
|
|
51
|
-
TableShell,
|
|
52
|
-
{
|
|
53
|
-
minWidth: 760,
|
|
54
|
-
density: "compact",
|
|
55
|
-
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
56
|
-
/* @__PURE__ */ jsx(Th, { children: "Merchant" }),
|
|
57
|
-
/* @__PURE__ */ jsx(Th, { align: "right", children: "Money received" }),
|
|
58
|
-
/* @__PURE__ */ jsx(Th, { align: "right", children: "Payments" }),
|
|
59
|
-
/* @__PURE__ */ jsx(Th, { children: "How customers paid" }),
|
|
60
|
-
/* @__PURE__ */ jsx(Th, { children: " " })
|
|
61
|
-
] }),
|
|
62
|
-
children: data.rows.map((row) => /* @__PURE__ */ jsx(MerchantRow, { row }, `${row.merchant.id}-${row.currency}`))
|
|
63
|
-
}
|
|
64
|
-
),
|
|
65
|
-
/* @__PURE__ */ jsx(
|
|
66
|
-
Pagination,
|
|
67
|
-
{
|
|
68
|
-
page,
|
|
69
|
-
totalPages: hasMore ? page + 1 : page,
|
|
70
|
-
noun: "merchants",
|
|
71
|
-
onPage: (next) => set({ page: next })
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
] });
|
|
75
|
-
}
|
|
76
|
-
function MerchantRow({ row }) {
|
|
77
|
-
const total = Number(row.grossMinor) || 1;
|
|
78
|
-
return /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
79
|
-
/* @__PURE__ */ jsx("td", { className: table.name, children: row.merchant.name ? /* @__PURE__ */ jsx(Link, { href: `/admin/tenants/${row.merchant.id}`, children: row.merchant.name }) : "Unknown merchant" }),
|
|
80
|
-
/* @__PURE__ */ jsx("td", { className: table.numStrong, children: minorToMoney(row.grossMinor, row.currency) }),
|
|
81
|
-
/* @__PURE__ */ jsx("td", { className: table.num, children: integer.format(row.payments) }),
|
|
82
|
-
/* @__PURE__ */ jsxs("td", { children: [
|
|
83
|
-
/* @__PURE__ */ jsx("div", { className: styles.methodBar, role: "img", "aria-label": row.byMethod.map((m) => `${m.label} ${minorToMoney(m.amountMinor, row.currency)}`).join(", "), children: row.byMethod.map((m) => /* @__PURE__ */ jsx(
|
|
84
|
-
"span",
|
|
85
|
-
{
|
|
86
|
-
className: styles.methodSeg,
|
|
87
|
-
style: { width: `${Number(m.amountMinor) / total * 100}%`, background: METHOD_COLOUR[m.key] ?? "#8a8f85" }
|
|
88
|
-
},
|
|
89
|
-
m.key
|
|
90
|
-
)) }),
|
|
91
|
-
/* @__PURE__ */ jsx("div", { className: styles.methodText, children: row.byMethod.map((m) => `${m.label} ${Math.round(Number(m.amountMinor) / total * 100)}%`).join(" \xB7 ") })
|
|
92
|
-
] }),
|
|
93
|
-
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Link, { href: `?tab=merchants&mp_merchant=${row.merchant.id}`, children: "See payments" }) })
|
|
94
|
-
] });
|
|
95
|
-
}
|
|
96
|
-
export {
|
|
97
|
-
MerchantsView
|
|
98
|
-
};
|
|
99
|
-
//# sourceMappingURL=merchants-view.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/merchants-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport Link from 'next/link';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport type { MerchantRevenueRow } from '@withpotter/lib/admin-api';\nimport { useMerchantRevenue } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { AdmLoading, FilterSearch, Filters, Pagination } from '../_ui';\nimport { minorToMoney } from './ledger-model';\nimport { useUrlFilters } from './use-url-filters';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst FILTERS = { q: '' };\nconst integer = new Intl.NumberFormat('en-US');\n\n/** One colour for each way of paying, so a merchant's mix reads at a glance. The words sit under the bar too. */\nconst METHOD_COLOUR: Record<string, string> = {\n card: '#4f7a28',\n ussd: '#c68a1b',\n transfer: '#2f6f9f',\n opay: '#7a4fa0',\n};\n\n/** Which merchant received how much, and how their customers paid them. */\nexport function MerchantsView({ startDate, endDate }: { startDate?: string; endDate?: string }) {\n const { values, page, set } = useUrlFilters('mr', FILTERS);\n const [search, setSearch] = useState(values.q);\n\n // Typing waits a moment, so a search is one request and not one per key.\n useEffect(() => {\n const timer = setTimeout(() => {\n if (search.trim() !== values.q) set({ q: search.trim() });\n }, 300);\n\n return () => clearTimeout(timer);\n // Only a change the person typed should write to the address.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [search]);\n\n const filters = useMemo(\n () => ({ startDate, endDate, q: values.q, page, limit: PAGE_SIZE }),\n [startDate, endDate, values.q, page],\n );\n const { data, isLoading, isError, refetch } = useMerchantRevenue(filters);\n\n if (isError) {\n return (\n <LoadFailed\n message=\"The merchants could not be loaded. This box may not have this view yet.\"\n onRetry={() => void refetch()}\n />\n );\n }\n if (isLoading || !data) return <AdmLoading label=\"Loading merchants…\" />;\n\n const hasMore = data.rows.length === PAGE_SIZE;\n\n return (\n <div>\n <p className={styles.lede}>\n This shows which merchants received the most money in this period, and how their customers paid them. Open a\n merchant to see each payment.\n </p>\n <div className={styles.toolbar}>\n <Filters label=\"Filter merchants\">\n <FilterSearch value={search} onChange={setSearch} placeholder=\"Search a merchant by name\" label=\"Search merchants\" />\n </Filters>\n </div>\n\n {data.rows.length === 0 ? (\n <EmptyNote className={table.empty}>No merchant took a payment in this period. Try a wider date range.</EmptyNote>\n ) : (\n <TableShell\n minWidth={760}\n density=\"compact\"\n head={\n <tr>\n <Th>Merchant</Th>\n <Th align=\"right\">Money received</Th>\n <Th align=\"right\">Payments</Th>\n <Th>How customers paid</Th>\n <Th> </Th>\n </tr>\n }\n >\n {data.rows.map((row) => (\n <MerchantRow key={`${row.merchant.id}-${row.currency}`} row={row} />\n ))}\n </TableShell>\n )}\n\n <Pagination\n page={page}\n totalPages={hasMore ? page + 1 : page}\n noun=\"merchants\"\n onPage={(next) => set({ page: next })}\n />\n </div>\n );\n}\n\nfunction MerchantRow({ row }: { row: MerchantRevenueRow }) {\n const total = Number(row.grossMinor) || 1;\n\n return (\n <TableRow>\n <td className={table.name}>\n {row.merchant.name ? <Link href={`/admin/tenants/${row.merchant.id}`}>{row.merchant.name}</Link> : 'Unknown merchant'}\n </td>\n <td className={table.numStrong}>{minorToMoney(row.grossMinor, row.currency)}</td>\n <td className={table.num}>{integer.format(row.payments)}</td>\n <td>\n <div className={styles.methodBar} role=\"img\" aria-label={row.byMethod.map((m) => `${m.label} ${minorToMoney(m.amountMinor, row.currency)}`).join(', ')}>\n {row.byMethod.map((m) => (\n <span\n key={m.key}\n className={styles.methodSeg}\n style={{ width: `${(Number(m.amountMinor) / total) * 100}%`, background: METHOD_COLOUR[m.key] ?? '#8a8f85' }}\n />\n ))}\n </div>\n <div className={styles.methodText}>\n {row.byMethod.map((m) => `${m.label} ${Math.round((Number(m.amountMinor) / total) * 100)}%`).join(' · ')}\n </div>\n </td>\n <td>\n <Link href={`?tab=merchants&mp_merchant=${row.merchant.id}`}>See payments</Link>\n </td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}
|