@withpotter/pages-admin 0.13.0-next.113 → 0.13.0-next.115
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/plan-lines.js +4 -3
- package/dist/admin/revenue/plan-lines.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":"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","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":"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","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":[]}
|
|
@@ -7,8 +7,9 @@ function priceList(prices, currencies) {
|
|
|
7
7
|
function planLines(plans, currencies, breakdown) {
|
|
8
8
|
const earned = /* @__PURE__ */ new Map();
|
|
9
9
|
for (const row of breakdown) {
|
|
10
|
-
const
|
|
11
|
-
earned.
|
|
10
|
+
const key = row.planId.toLowerCase();
|
|
11
|
+
const seen = earned.get(key);
|
|
12
|
+
earned.set(key, {
|
|
12
13
|
subscribers: (seen?.subscribers ?? 0) + row.subscribers,
|
|
13
14
|
mrr: (seen?.mrr ?? 0) + row.mrr
|
|
14
15
|
});
|
|
@@ -16,7 +17,7 @@ function planLines(plans, currencies, breakdown) {
|
|
|
16
17
|
return plans.map((plan) => {
|
|
17
18
|
const version = plan.live ?? plan.open;
|
|
18
19
|
const prices = version?.prices ?? {};
|
|
19
|
-
const hit = earned.get(plan.id);
|
|
20
|
+
const hit = earned.get(plan.id.toLowerCase()) ?? earned.get(plan.slug.toLowerCase()) ?? (version ? earned.get(version.name.toLowerCase()) : void 0);
|
|
20
21
|
return {
|
|
21
22
|
id: plan.id,
|
|
22
23
|
name: version?.name ?? plan.slug,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/plan-lines.ts"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport type { PriceCurrency } from '@withpotter/lib/admin-pricing-api';\nimport { byCurrency, formatPlanPrice } from '@withpotter/lib/plan-price';\nimport type { PlanSummary } from '@withpotter/lib/pricing-plans-types';\n\n/** One line per currency, at that currency's exponent. */\nfunction priceList(prices: Record<string, string>, currencies: PriceCurrency[]): string[] {\n return Object.keys(prices)\n .sort(byCurrency)\n .map((code) =>\n formatPlanPrice(prices[code], code, currencies.find((c) => c.code === code)?.exponent),\n );\n}\n\n/** What one plan shows: its catalogue entry joined with what it earns. */\nexport interface PlanLine {\n id: string;\n name: string;\n slug: string;\n prices: string[];\n /** Nothing to pay: every price is zero. */\n free: boolean;\n /** Created but not live yet. */\n draft: boolean;\n subscribers: number;\n mrr: number;\n}\n\n/**\n * Every plan an admin has created, one line each, with the name it was given\n * (Free, Basic, Premium and so on). The plans list is the source of the rows, so\n * a plan nobody is on yet still shows, with 0 subscribers; the revenue figures\n * for it are joined in by plan id.\n */\nexport function planLines(\n plans: readonly PlanSummary[],\n currencies: PriceCurrency[],\n breakdown: PlatformRevenue['recurring']['planBreakdown'],\n): PlanLine[] {\n const earned = new Map<string, { subscribers: number; mrr: number }>();\n\n for (const row of breakdown) {\n const seen = earned.get(
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/plan-lines.ts"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport type { PriceCurrency } from '@withpotter/lib/admin-pricing-api';\nimport { byCurrency, formatPlanPrice } from '@withpotter/lib/plan-price';\nimport type { PlanSummary } from '@withpotter/lib/pricing-plans-types';\n\n/** One line per currency, at that currency's exponent. */\nfunction priceList(prices: Record<string, string>, currencies: PriceCurrency[]): string[] {\n return Object.keys(prices)\n .sort(byCurrency)\n .map((code) =>\n formatPlanPrice(prices[code], code, currencies.find((c) => c.code === code)?.exponent),\n );\n}\n\n/** What one plan shows: its catalogue entry joined with what it earns. */\nexport interface PlanLine {\n id: string;\n name: string;\n slug: string;\n prices: string[];\n /** Nothing to pay: every price is zero. */\n free: boolean;\n /** Created but not live yet. */\n draft: boolean;\n subscribers: number;\n mrr: number;\n}\n\n/**\n * Every plan an admin has created, one line each, with the name it was given\n * (Free, Basic, Premium and so on). The plans list is the source of the rows, so\n * a plan nobody is on yet still shows, with 0 subscribers; the revenue figures\n * for it are joined in by plan id, code or name.\n */\nexport function planLines(\n plans: readonly PlanSummary[],\n currencies: PriceCurrency[],\n breakdown: PlatformRevenue['recurring']['planBreakdown'],\n): PlanLine[] {\n const earned = new Map<string, { subscribers: number; mrr: number }>();\n\n for (const row of breakdown) {\n const key = row.planId.toLowerCase();\n const seen = earned.get(key);\n\n // A plan billed monthly and yearly comes as two rows: add them up.\n earned.set(key, {\n subscribers: (seen?.subscribers ?? 0) + row.subscribers,\n mrr: (seen?.mrr ?? 0) + row.mrr,\n });\n }\n\n return plans\n .map((plan): PlanLine => {\n const version = plan.live ?? plan.open;\n const prices: Record<string, string> = version?.prices ?? {};\n // The revenue breakdown names a plan by its code (\"basic\"), the plans list by its id: match either, or the name.\n const hit =\n earned.get(plan.id.toLowerCase()) ??\n earned.get(plan.slug.toLowerCase()) ??\n (version ? earned.get(version.name.toLowerCase()) : undefined);\n\n return {\n id: plan.id,\n name: version?.name ?? plan.slug,\n slug: plan.slug,\n prices: version ? priceList(prices, currencies) : [],\n free:\n Object.keys(prices).length > 0 &&\n Object.values(prices).every((minor) => /^0*$/.test(minor)),\n draft: plan.live === null,\n subscribers: hit?.subscribers ?? plan.subscribers,\n mrr: hit?.mrr ?? 0,\n };\n })\n .sort((a, b) => b.mrr - a.mrr || b.subscribers - a.subscribers || a.name.localeCompare(b.name));\n}\n"],"mappings":"AAEA,SAAS,YAAY,uBAAuB;AAI5C,SAAS,UAAU,QAAgC,YAAuC;AACxF,SAAO,OAAO,KAAK,MAAM,EACtB,KAAK,UAAU,EACf;AAAA,IAAI,CAAC,SACJ,gBAAgB,OAAO,IAAI,GAAG,MAAM,WAAW,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG,QAAQ;AAAA,EACvF;AACJ;AAsBO,SAAS,UACd,OACA,YACA,WACY;AACZ,QAAM,SAAS,oBAAI,IAAkD;AAErE,aAAW,OAAO,WAAW;AAC3B,UAAM,MAAM,IAAI,OAAO,YAAY;AACnC,UAAM,OAAO,OAAO,IAAI,GAAG;AAG3B,WAAO,IAAI,KAAK;AAAA,MACd,cAAc,MAAM,eAAe,KAAK,IAAI;AAAA,MAC5C,MAAM,MAAM,OAAO,KAAK,IAAI;AAAA,IAC9B,CAAC;AAAA,EACH;AAEA,SAAO,MACJ,IAAI,CAAC,SAAmB;AACvB,UAAM,UAAU,KAAK,QAAQ,KAAK;AAClC,UAAM,SAAiC,SAAS,UAAU,CAAC;AAE3D,UAAM,MACJ,OAAO,IAAI,KAAK,GAAG,YAAY,CAAC,KAChC,OAAO,IAAI,KAAK,KAAK,YAAY,CAAC,MACjC,UAAU,OAAO,IAAI,QAAQ,KAAK,YAAY,CAAC,IAAI;AAEtD,WAAO;AAAA,MACL,IAAI,KAAK;AAAA,MACT,MAAM,SAAS,QAAQ,KAAK;AAAA,MAC5B,MAAM,KAAK;AAAA,MACX,QAAQ,UAAU,UAAU,QAAQ,UAAU,IAAI,CAAC;AAAA,MACnD,MACE,OAAO,KAAK,MAAM,EAAE,SAAS,KAC7B,OAAO,OAAO,MAAM,EAAE,MAAM,CAAC,UAAU,OAAO,KAAK,KAAK,CAAC;AAAA,MAC3D,OAAO,KAAK,SAAS;AAAA,MACrB,aAAa,KAAK,eAAe,KAAK;AAAA,MACtC,KAAK,KAAK,OAAO;AAAA,IACnB;AAAA,EACF,CAAC,EACA,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAClG;","names":[]}
|