@carlonicora/nextjs-jsonapi 6.10.2 → 6.11.0

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.
Files changed (108) hide show
  1. package/dist/{BlockNoteEditor-7GQWHXBC.mjs → BlockNoteEditor-5WM5DX3J.mjs} +4 -4
  2. package/dist/{BlockNoteEditor-LX7NMBV6.js → BlockNoteEditor-GY5R25YI.js} +19 -19
  3. package/dist/{BlockNoteEditor-LX7NMBV6.js.map → BlockNoteEditor-GY5R25YI.js.map} +1 -1
  4. package/dist/{BlockNoteViewer-NXV5PKR4.js → BlockNoteViewer-FLUHEXE6.js} +3 -3
  5. package/dist/{BlockNoteViewer-NXV5PKR4.js.map → BlockNoteViewer-FLUHEXE6.js.map} +1 -1
  6. package/dist/{BlockNoteViewer-D3Y4FUST.mjs → BlockNoteViewer-UCA3LV3N.mjs} +2 -2
  7. package/dist/billing/index.js +491 -491
  8. package/dist/billing/index.mjs +3 -3
  9. package/dist/{chunk-LDUTS7SJ.mjs → chunk-3PRLRCMF.mjs} +1281 -603
  10. package/dist/chunk-3PRLRCMF.mjs.map +1 -0
  11. package/dist/{chunk-QB2XU64X.mjs → chunk-6RLATJ2L.mjs} +2 -2
  12. package/dist/{chunk-62P4KIR2.mjs → chunk-BGLOFZ3K.mjs} +282 -2
  13. package/dist/chunk-BGLOFZ3K.mjs.map +1 -0
  14. package/dist/{chunk-62VVBUIL.js → chunk-FNPXCYHO.js} +8 -8
  15. package/dist/{chunk-62VVBUIL.js.map → chunk-FNPXCYHO.js.map} +1 -1
  16. package/dist/{chunk-3I5JCFPY.js → chunk-MP3DV2I5.js} +1647 -969
  17. package/dist/chunk-MP3DV2I5.js.map +1 -0
  18. package/dist/{chunk-TOPGUZSX.js → chunk-OHJSI6L2.js} +4 -4
  19. package/dist/{chunk-TOPGUZSX.js.map → chunk-OHJSI6L2.js.map} +1 -1
  20. package/dist/{chunk-Z4OVEA3N.js → chunk-YGEOX2XX.js} +283 -3
  21. package/dist/chunk-YGEOX2XX.js.map +1 -0
  22. package/dist/{chunk-GHAZOQTT.mjs → chunk-YP6W4AB5.mjs} +2 -2
  23. package/dist/client/index.js +4 -4
  24. package/dist/client/index.mjs +3 -3
  25. package/dist/components/index.d.mts +67 -29
  26. package/dist/components/index.d.ts +67 -29
  27. package/dist/components/index.js +10 -4
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/index.mjs +9 -3
  30. package/dist/contexts/index.js +4 -4
  31. package/dist/contexts/index.mjs +3 -3
  32. package/dist/core/index.d.mts +159 -3
  33. package/dist/core/index.d.ts +159 -3
  34. package/dist/core/index.js +14 -2
  35. package/dist/core/index.js.map +1 -1
  36. package/dist/core/index.mjs +13 -1
  37. package/dist/email-automations/index.js +93 -93
  38. package/dist/email-automations/index.mjs +3 -3
  39. package/dist/features/analytics/index.js +120 -122
  40. package/dist/features/analytics/index.js.map +1 -1
  41. package/dist/features/analytics/index.mjs +7 -9
  42. package/dist/features/analytics/index.mjs.map +1 -1
  43. package/dist/features/analytics/server.js +4 -4
  44. package/dist/features/analytics/server.mjs +2 -2
  45. package/dist/features/blog/index.js +150 -150
  46. package/dist/features/blog/index.mjs +3 -3
  47. package/dist/features/blog/server.js +16 -16
  48. package/dist/features/blog/server.mjs +1 -1
  49. package/dist/features/help/index.js +37 -37
  50. package/dist/features/help/index.mjs +3 -3
  51. package/dist/features/tokenusage/index.js +126 -128
  52. package/dist/features/tokenusage/index.js.map +1 -1
  53. package/dist/features/tokenusage/index.mjs +14 -16
  54. package/dist/features/tokenusage/index.mjs.map +1 -1
  55. package/dist/index.d.mts +2 -2
  56. package/dist/index.d.ts +2 -2
  57. package/dist/index.js +15 -3
  58. package/dist/index.js.map +1 -1
  59. package/dist/index.mjs +14 -2
  60. package/dist/{HandbookSectionInterface-5I57rC1b.d.mts → referral-admin.interface-C87ApGMR.d.mts} +41 -1
  61. package/dist/{HandbookSectionInterface-IDYEzQMi.d.ts → referral-admin.interface-CMwl5NiV.d.ts} +41 -1
  62. package/dist/server/index.js +3 -3
  63. package/dist/server/index.mjs +2 -2
  64. package/messages/en.json +78 -1
  65. package/messages/it.json +78 -1
  66. package/package.json +1 -1
  67. package/src/core/index.ts +2 -0
  68. package/src/core/registry/ModuleRegistry.ts +5 -0
  69. package/src/features/referral/__tests__/referral-admin-models.spec.ts +122 -0
  70. package/src/features/referral/__tests__/referral-admin-service.spec.ts +70 -0
  71. package/src/features/referral/__tests__/referral-code-capture.spec.tsx +105 -0
  72. package/src/features/referral/__tests__/referral-invite-errors.spec.tsx +82 -0
  73. package/src/features/referral/__tests__/referral-visit-service.spec.ts +47 -0
  74. package/src/features/referral/__tests__/referrals-admin-container.spec.tsx +230 -0
  75. package/src/features/referral/components/ReferralCodeCapture.tsx +3 -0
  76. package/src/features/referral/components/ReferralDialog.tsx +9 -13
  77. package/src/features/referral/components/ReferralWidget.tsx +75 -45
  78. package/src/features/referral/components/ReferralsAdminFunnel.tsx +74 -0
  79. package/src/features/referral/components/ReferralsAdminTiles.tsx +128 -0
  80. package/src/features/referral/components/containers/ReferralsAdminContainer.tsx +205 -0
  81. package/src/features/referral/components/index.ts +3 -0
  82. package/src/features/referral/data/ReferralAdminReferral.ts +65 -0
  83. package/src/features/referral/data/ReferralAdminReferrer.ts +65 -0
  84. package/src/features/referral/data/ReferralAdminService.ts +51 -0
  85. package/src/features/referral/data/ReferralAdminSummary.ts +60 -0
  86. package/src/features/referral/data/ReferralService.ts +20 -0
  87. package/src/features/referral/data/ReferralVisit.ts +23 -0
  88. package/src/features/referral/data/index.ts +5 -0
  89. package/src/features/referral/hooks/index.ts +2 -0
  90. package/src/features/referral/hooks/useReferralAdminReferralTableStructure.tsx +127 -0
  91. package/src/features/referral/hooks/useReferralAdminReferrerTableStructure.tsx +229 -0
  92. package/src/features/referral/hooks/useReferralInvite.ts +2 -0
  93. package/src/features/referral/index.ts +1 -0
  94. package/src/features/referral/interfaces/index.ts +1 -0
  95. package/src/features/referral/interfaces/referral-admin.interface.ts +45 -0
  96. package/src/features/referral/referral-admin.modules.ts +32 -0
  97. package/dist/chunk-3I5JCFPY.js.map +0 -1
  98. package/dist/chunk-62P4KIR2.mjs.map +0 -1
  99. package/dist/chunk-D6UX37IA.mjs +0 -115
  100. package/dist/chunk-D6UX37IA.mjs.map +0 -1
  101. package/dist/chunk-KEHP2OBQ.js +0 -115
  102. package/dist/chunk-KEHP2OBQ.js.map +0 -1
  103. package/dist/chunk-LDUTS7SJ.mjs.map +0 -1
  104. package/dist/chunk-Z4OVEA3N.js.map +0 -1
  105. /package/dist/{BlockNoteEditor-7GQWHXBC.mjs.map → BlockNoteEditor-5WM5DX3J.mjs.map} +0 -0
  106. /package/dist/{BlockNoteViewer-D3Y4FUST.mjs.map → BlockNoteViewer-UCA3LV3N.mjs.map} +0 -0
  107. /package/dist/{chunk-QB2XU64X.mjs.map → chunk-6RLATJ2L.mjs.map} +0 -0
  108. /package/dist/{chunk-GHAZOQTT.mjs.map → chunk-YP6W4AB5.mjs.map} +0 -0
@@ -0,0 +1,127 @@
1
+ "use client";
2
+
3
+ import { ColumnDef } from "@tanstack/react-table";
4
+ import { useTranslations } from "next-intl";
5
+ import { useMemo } from "react";
6
+ import { cellDate } from "../../../components/tables/cells/cell.date";
7
+ import { registerTableGenerator, TableContent, UseTableStructureHook } from "../../../hooks";
8
+ import { Badge } from "../../../shadcnui";
9
+ import { useUsageFormatters } from "../../tokenusage/lib/formatters";
10
+ import { ReferralAdminReferralInterface } from "../interfaces/referral-admin.interface";
11
+
12
+ /**
13
+ * Columns of "Every referral". The values double as the TanStack column ids:
14
+ * ContentListTable builds its `columnVisibility` from the `fields` array.
15
+ */
16
+ export enum ReferralAdminReferralFields {
17
+ account = "account",
18
+ referrer = "referrer",
19
+ source = "source",
20
+ date = "date",
21
+ status = "status",
22
+ credits = "credits",
23
+ }
24
+
25
+ type Row = { row: { original: TableContent<ReferralAdminReferralInterface> } };
26
+
27
+ export const useReferralAdminReferralTableStructure: UseTableStructureHook<
28
+ ReferralAdminReferralInterface,
29
+ ReferralAdminReferralFields
30
+ > = (params) => {
31
+ const t = useTranslations();
32
+ const { decimal } = useUsageFormatters();
33
+
34
+ const tableData = useMemo(() => {
35
+ return params.data.map((entry: ReferralAdminReferralInterface) => {
36
+ const row: TableContent<ReferralAdminReferralInterface> = { jsonApiData: entry };
37
+ // cellDate reads the value from the row by the column name.
38
+ row[ReferralAdminReferralFields.date] = entry.referredAt;
39
+ return row;
40
+ });
41
+ }, [params.data]);
42
+
43
+ const fieldColumnMap: Record<ReferralAdminReferralFields, () => any> = {
44
+ [ReferralAdminReferralFields.account]: () => ({
45
+ id: ReferralAdminReferralFields.account,
46
+ header: t("referral.admin.referrals.columns.account"),
47
+ cell: ({ row }: Row) => {
48
+ const entry = row.original.jsonApiData;
49
+ return (
50
+ <div className="flex flex-col">
51
+ <span className="text-sm">{entry.name}</span>
52
+ <span className="text-muted-foreground text-xs">{entry.email}</span>
53
+ </div>
54
+ );
55
+ },
56
+ enableSorting: false,
57
+ enableHiding: false,
58
+ }),
59
+ [ReferralAdminReferralFields.referrer]: () => ({
60
+ id: ReferralAdminReferralFields.referrer,
61
+ header: t("referral.admin.referrals.columns.referrer"),
62
+ cell: ({ row }: Row) => {
63
+ const entry = row.original.jsonApiData;
64
+ return (
65
+ <div className="flex flex-col">
66
+ <span className="text-sm">{entry.referrerName}</span>
67
+ <span className="text-muted-foreground text-xs">{entry.referrerEmail}</span>
68
+ </div>
69
+ );
70
+ },
71
+ enableSorting: false,
72
+ enableHiding: false,
73
+ }),
74
+ [ReferralAdminReferralFields.source]: () => ({
75
+ id: ReferralAdminReferralFields.source,
76
+ header: t("referral.admin.referrals.columns.source"),
77
+ cell: ({ row }: Row) => {
78
+ const source = row.original.jsonApiData.source;
79
+ if (source === "link") return <Badge variant="softBlue">{t("referral.admin.source.link")}</Badge>;
80
+ if (source === "direct") return <Badge variant="softGray">{t("referral.admin.source.direct")}</Badge>;
81
+ // A referral written before the source was recorded: unknown, not guessed.
82
+ return <span className="text-muted-foreground text-xs">—</span>;
83
+ },
84
+ enableSorting: false,
85
+ enableHiding: false,
86
+ }),
87
+ [ReferralAdminReferralFields.date]: () =>
88
+ cellDate({ name: ReferralAdminReferralFields.date, title: t("referral.admin.referrals.columns.date") }),
89
+ [ReferralAdminReferralFields.status]: () => ({
90
+ id: ReferralAdminReferralFields.status,
91
+ header: t("referral.admin.referrals.columns.status"),
92
+ cell: ({ row }: Row) =>
93
+ row.original.jsonApiData.status === "completed" ? (
94
+ <Badge variant="softGreen">{t("referral.admin.status.completed")}</Badge>
95
+ ) : (
96
+ <Badge variant="softYellow">{t("referral.admin.status.pending")}</Badge>
97
+ ),
98
+ enableSorting: false,
99
+ enableHiding: false,
100
+ }),
101
+ [ReferralAdminReferralFields.credits]: () => ({
102
+ id: ReferralAdminReferralFields.credits,
103
+ header: t("referral.admin.referrals.columns.credits"),
104
+ cell: ({ row }: Row) => {
105
+ const entry = row.original.jsonApiData;
106
+ // Both sides receive the same award: shown as "200 + 200", as in the approved mock.
107
+ if (entry.status !== "completed" || !entry.tokensAwarded)
108
+ return <span className="block text-muted-foreground text-xs text-end">—</span>;
109
+ const award = decimal(entry.tokensAwarded, 0);
110
+ return <span className="block text-xs tabular-nums text-end">{`${award} + ${award}`}</span>;
111
+ },
112
+ meta: { className: "text-end" },
113
+ enableSorting: false,
114
+ enableHiding: false,
115
+ }),
116
+ };
117
+
118
+ const columns = useMemo(() => {
119
+ return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
120
+ TableContent<ReferralAdminReferralInterface>
121
+ >[];
122
+ }, [params.fields, fieldColumnMap, t, decimal]);
123
+
124
+ return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
125
+ };
126
+
127
+ registerTableGenerator("referrals/administration/referrals", useReferralAdminReferralTableStructure);
@@ -0,0 +1,229 @@
1
+ "use client";
2
+
3
+ import { ColumnDef } from "@tanstack/react-table";
4
+ import { CopyIcon } from "lucide-react";
5
+ import { useTranslations } from "next-intl";
6
+ import { useMemo } from "react";
7
+ import { cellDate } from "../../../components/tables/cells/cell.date";
8
+ import { registerTableGenerator, TableContent, UseTableStructureHook } from "../../../hooks";
9
+ import { Button } from "../../../shadcnui";
10
+ import { showError, showToast } from "../../../utils";
11
+ import { useUsageFormatters } from "../../tokenusage/lib/formatters";
12
+ import { ReferralAdminReferrerInterface, ReferralAdminSort } from "../interfaces/referral-admin.interface";
13
+
14
+ /**
15
+ * Columns of the referrers ranking. The values double as the TanStack column
16
+ * ids: ContentListTable builds its `columnVisibility` from the `fields` array,
17
+ * so a column whose id differs from its field is invisible.
18
+ */
19
+ export enum ReferralAdminReferrerFields {
20
+ rank = "rank",
21
+ referrer = "referrer",
22
+ visits = "visits",
23
+ invites = "invites",
24
+ signups = "signups",
25
+ paying = "paying",
26
+ conversion = "conversion",
27
+ credits = "credits",
28
+ lastInvite = "lastInvite",
29
+ }
30
+
31
+ /** What the container passes through ContentListTable's `context`: the active sort and its setter. */
32
+ export type ReferralAdminReferrerTableContext = {
33
+ sort: ReferralAdminSort;
34
+ onSort: (sort: ReferralAdminSort) => void;
35
+ };
36
+
37
+ /**
38
+ * Copies text to the clipboard from inside a click handler, with the toast on
39
+ * success and the error toast on refusal (a denied permission or an insecure
40
+ * context). Shared by the per-row button and the container's "Copy all emails".
41
+ */
42
+ export async function copyReferrerEmails(
43
+ text: string,
44
+ t: (key: string, values?: Record<string, string | number>) => string,
45
+ ): Promise<void> {
46
+ try {
47
+ await navigator.clipboard.writeText(text);
48
+ showToast(t("referral.admin.referrers.copied"));
49
+ } catch {
50
+ showError(t("referral.admin.referrers.copyFailed"));
51
+ }
52
+ }
53
+
54
+ const NUMERIC = { className: "text-end" };
55
+
56
+ export const useReferralAdminReferrerTableStructure: UseTableStructureHook<
57
+ ReferralAdminReferrerInterface,
58
+ ReferralAdminReferrerFields
59
+ > = (params) => {
60
+ const t = useTranslations();
61
+ const { decimal } = useUsageFormatters();
62
+ const context = params.context as ReferralAdminReferrerTableContext | undefined;
63
+
64
+ const tableData = useMemo(() => {
65
+ return params.data.map((entry: ReferralAdminReferrerInterface, index: number) => {
66
+ const row: TableContent<ReferralAdminReferrerInterface> = { jsonApiData: entry };
67
+ row[ReferralAdminReferrerFields.rank] = index + 1;
68
+ // cellDate reads the value from the row by the column name.
69
+ row[ReferralAdminReferrerFields.lastInvite] = entry.lastInviteAt;
70
+ return row;
71
+ });
72
+ }, [params.data]);
73
+
74
+ /** Header of a sortable column: a button that sets the sort the API reads (D8). */
75
+ const sortableHeader = (sort: ReferralAdminSort, label: string) => () => {
76
+ const active = context?.sort === sort;
77
+ return (
78
+ <Button
79
+ type="button"
80
+ variant="ghost"
81
+ size="xs"
82
+ className={active ? "text-primary" : "text-muted-foreground"}
83
+ title={t("referral.admin.referrers.sortBy")}
84
+ aria-sort={active ? "descending" : undefined}
85
+ data-testid={active ? "referrals-referrers-sort" : undefined}
86
+ onClick={() => context?.onSort(sort)}
87
+ >
88
+ {label}
89
+ </Button>
90
+ );
91
+ };
92
+
93
+ // Typography role 15 (numeric): tabular-nums, end-aligned.
94
+ const numericCell = (read: (entry: ReferralAdminReferrerInterface) => number) => {
95
+ return ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => (
96
+ <span className="block text-xs tabular-nums text-end">{decimal(read(row.original.jsonApiData), 0)}</span>
97
+ );
98
+ };
99
+
100
+ const fieldColumnMap: Record<ReferralAdminReferrerFields, () => any> = {
101
+ [ReferralAdminReferrerFields.rank]: () => ({
102
+ id: ReferralAdminReferrerFields.rank,
103
+ header: "#",
104
+ cell: ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => (
105
+ <span className="text-muted-foreground text-xs tabular-nums">
106
+ {row.original[ReferralAdminReferrerFields.rank]}
107
+ </span>
108
+ ),
109
+ enableSorting: false,
110
+ enableHiding: false,
111
+ }),
112
+ [ReferralAdminReferrerFields.referrer]: () => ({
113
+ id: ReferralAdminReferrerFields.referrer,
114
+ header: t("referral.admin.referrers.columns.referrer"),
115
+ cell: ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => {
116
+ const entry = row.original.jsonApiData;
117
+ return (
118
+ <div className="flex items-center gap-2">
119
+ <div className="flex flex-col">
120
+ <span className="text-sm">{entry.name}</span>
121
+ <span className="text-muted-foreground text-xs">{entry.email}</span>
122
+ </div>
123
+ {entry.email && (
124
+ <Button
125
+ type="button"
126
+ variant="ghost"
127
+ size="icon-xs"
128
+ aria-label={t("referral.admin.referrers.copyEmail")}
129
+ title={t("referral.admin.referrers.copyEmail")}
130
+ data-testid={`referrals-copy-email-${entry.id}`}
131
+ onClick={() => copyReferrerEmails(entry.email, t)}
132
+ >
133
+ <CopyIcon />
134
+ </Button>
135
+ )}
136
+ </div>
137
+ );
138
+ },
139
+ enableSorting: false,
140
+ enableHiding: false,
141
+ }),
142
+ [ReferralAdminReferrerFields.visits]: () => ({
143
+ id: ReferralAdminReferrerFields.visits,
144
+ header: t("referral.admin.referrers.columns.visits"),
145
+ cell: numericCell((entry) => entry.visits),
146
+ meta: NUMERIC,
147
+ enableSorting: false,
148
+ enableHiding: false,
149
+ }),
150
+ [ReferralAdminReferrerFields.invites]: () => ({
151
+ id: ReferralAdminReferrerFields.invites,
152
+ header: sortableHeader("invites", t("referral.admin.referrers.columns.invites")),
153
+ cell: numericCell((entry) => entry.invitesSent),
154
+ meta: NUMERIC,
155
+ enableSorting: false,
156
+ enableHiding: false,
157
+ }),
158
+ [ReferralAdminReferrerFields.signups]: () => ({
159
+ id: ReferralAdminReferrerFields.signups,
160
+ header: sortableHeader("signups", t("referral.admin.referrers.columns.signups")),
161
+ cell: ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => {
162
+ const entry = row.original.jsonApiData;
163
+ return (
164
+ <div className="flex flex-col items-end">
165
+ <span className="text-xs tabular-nums">{decimal(entry.signupsLink + entry.signupsDirect, 0)}</span>
166
+ <span className="text-muted-foreground text-xs">
167
+ {t("referral.admin.referrers.split", { link: entry.signupsLink, direct: entry.signupsDirect })}
168
+ </span>
169
+ </div>
170
+ );
171
+ },
172
+ meta: NUMERIC,
173
+ enableSorting: false,
174
+ enableHiding: false,
175
+ }),
176
+ [ReferralAdminReferrerFields.paying]: () => ({
177
+ id: ReferralAdminReferrerFields.paying,
178
+ header: sortableHeader("paying", t("referral.admin.referrers.columns.paying")),
179
+ cell: numericCell((entry) => entry.paying),
180
+ meta: NUMERIC,
181
+ enableSorting: false,
182
+ enableHiding: false,
183
+ }),
184
+ [ReferralAdminReferrerFields.conversion]: () => ({
185
+ id: ReferralAdminReferrerFields.conversion,
186
+ header: sortableHeader("conversion", t("referral.admin.referrers.columns.conversion")),
187
+ cell: ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => {
188
+ const entry = row.original.jsonApiData;
189
+ // Conversion = paying / emails sent; a referrer who sent none has no rate.
190
+ if (!entry.invitesSent) return <span className="block text-muted-foreground text-xs text-end">—</span>;
191
+ const rate = Math.round((entry.paying / entry.invitesSent) * 100);
192
+ return (
193
+ <span className="flex items-center justify-end gap-1.5">
194
+ <span className="bg-primary/15 relative h-1.5 w-12 overflow-hidden rounded-full">
195
+ <span className="bg-primary absolute inset-y-0 start-0" style={{ width: `${Math.min(rate, 100)}%` }} />
196
+ </span>
197
+ <span className="text-xs tabular-nums">{`${decimal(rate, 0)}%`}</span>
198
+ </span>
199
+ );
200
+ },
201
+ meta: NUMERIC,
202
+ enableSorting: false,
203
+ enableHiding: false,
204
+ }),
205
+ [ReferralAdminReferrerFields.credits]: () => ({
206
+ id: ReferralAdminReferrerFields.credits,
207
+ header: sortableHeader("credits", t("referral.admin.referrers.columns.credits")),
208
+ cell: numericCell((entry) => entry.creditsEarned),
209
+ meta: NUMERIC,
210
+ enableSorting: false,
211
+ enableHiding: false,
212
+ }),
213
+ [ReferralAdminReferrerFields.lastInvite]: () =>
214
+ cellDate({
215
+ name: ReferralAdminReferrerFields.lastInvite,
216
+ title: t("referral.admin.referrers.columns.lastInvite"),
217
+ }),
218
+ };
219
+
220
+ const columns = useMemo(() => {
221
+ return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
222
+ TableContent<ReferralAdminReferrerInterface>
223
+ >[];
224
+ }, [params.fields, fieldColumnMap, t, decimal, context?.sort]);
225
+
226
+ return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
227
+ };
228
+
229
+ registerTableGenerator("referrals/administration/referrers", useReferralAdminReferrerTableStructure);
@@ -23,6 +23,8 @@ export function useReferralInvite() {
23
23
  setSuccess(true);
24
24
  } catch (err) {
25
25
  setError(err as Error);
26
+ // Rethrown so the caller can tell a refused invite from a sent one.
27
+ throw err;
26
28
  } finally {
27
29
  setSending(false);
28
30
  }
@@ -19,3 +19,4 @@ export * from "./components";
19
19
  // Modules
20
20
  export * from "./referral.module";
21
21
  export * from "./referral-stats.module";
22
+ export * from "./referral-admin.modules";
@@ -1 +1,2 @@
1
1
  export * from "./referral.interface";
2
+ export * from "./referral-admin.interface";
@@ -0,0 +1,45 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ /** Report window of the administrative referral page. */
4
+ export type ReferralAdminPeriod = "30d" | "90d" | "all";
5
+
6
+ /** The referrers ranking order the API allowlists. */
7
+ export type ReferralAdminSort = "paying" | "signups" | "invites" | "conversion" | "credits";
8
+
9
+ /** One window (`current` or `previous`) of the headline tiles. */
10
+ export interface ReferralAdminSummaryInterface extends ApiDataInterface {
11
+ get window(): string;
12
+ get visits(): number;
13
+ get invitesSent(): number;
14
+ get signups(): number;
15
+ get paying(): number;
16
+ get creditsGiven(): number;
17
+ get sharingCompanies(): number;
18
+ get eligibleCompanies(): number;
19
+ }
20
+
21
+ /** One referrer company in the ranking; the id is the company id. */
22
+ export interface ReferralAdminReferrerInterface extends ApiDataInterface {
23
+ get name(): string;
24
+ get email(): string;
25
+ get visits(): number;
26
+ get invitesSent(): number;
27
+ get signupsLink(): number;
28
+ get signupsDirect(): number;
29
+ get paying(): number;
30
+ get creditsEarned(): number;
31
+ get lastInviteAt(): Date | undefined;
32
+ }
33
+
34
+ /** One referral (new account and who brought it); the id is the Referral id. */
35
+ export interface ReferralAdminReferralInterface extends ApiDataInterface {
36
+ get name(): string;
37
+ get email(): string;
38
+ get referrerName(): string;
39
+ get referrerEmail(): string;
40
+ get source(): string;
41
+ get status(): string;
42
+ get referredAt(): Date;
43
+ get completedAt(): Date | undefined;
44
+ get tokensAwarded(): number | undefined;
45
+ }
@@ -0,0 +1,32 @@
1
+ import { GiftIcon } from "lucide-react";
2
+ import { ModuleFactory, ModuleWithPermissions } from "../../permissions";
3
+ import { ReferralAdminReferral } from "./data/ReferralAdminReferral";
4
+ import { ReferralAdminReferrer } from "./data/ReferralAdminReferrer";
5
+ import { ReferralAdminSummary } from "./data/ReferralAdminSummary";
6
+ import { ReferralVisit } from "./data/ReferralVisit";
7
+
8
+ /**
9
+ * The referral visit and administrative report resources, as one object a
10
+ * consuming app spreads into its `allModules` (same rationale as
11
+ * `analyticsModules`). An app records link visits only once it spreads this.
12
+ * None has a pageUrl: these are not navigable resources.
13
+ */
14
+ export const referralAdminModules = (factory: ModuleFactory) =>
15
+ ({
16
+ ReferralVisit: factory({ name: "referrals/visits", model: ReferralVisit }),
17
+ ReferralAdminSummary: factory({
18
+ name: "referrals/administration/summary",
19
+ model: ReferralAdminSummary,
20
+ icon: GiftIcon,
21
+ }),
22
+ ReferralAdminReferrer: factory({
23
+ name: "referrals/administration/referrers",
24
+ model: ReferralAdminReferrer,
25
+ icon: GiftIcon,
26
+ }),
27
+ ReferralAdminReferral: factory({
28
+ name: "referrals/administration/referrals",
29
+ model: ReferralAdminReferral,
30
+ icon: GiftIcon,
31
+ }),
32
+ }) satisfies Record<string, ModuleWithPermissions>;