@carlonicora/nextjs-jsonapi 2.4.0 → 3.0.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.
- package/dist/{BlockNoteEditor-7WGCH2KB.js → BlockNoteEditor-CDTU6DRN.js} +19 -19
- package/dist/{BlockNoteEditor-7WGCH2KB.js.map → BlockNoteEditor-CDTU6DRN.js.map} +1 -1
- package/dist/{BlockNoteEditor-GWAV5ESO.mjs → BlockNoteEditor-VVAY73V2.mjs} +4 -4
- package/dist/billing/index.d.mts +127 -46
- package/dist/billing/index.d.ts +127 -46
- package/dist/billing/index.js +1243 -1084
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +983 -824
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-V66AZWPG.js → chunk-53B6NPGA.js} +278 -8
- package/dist/chunk-53B6NPGA.js.map +1 -0
- package/dist/{chunk-M2EGUO2F.mjs → chunk-J54546YC.mjs} +1608 -1419
- package/dist/chunk-J54546YC.mjs.map +1 -0
- package/dist/{chunk-TOKHHZSP.js → chunk-JKGEJGSD.js} +7 -7
- package/dist/{chunk-TOKHHZSP.js.map → chunk-JKGEJGSD.js.map} +1 -1
- package/dist/{chunk-3LVSA7IM.mjs → chunk-PHEFWL6L.mjs} +2 -2
- package/dist/{chunk-LCCRUWWY.mjs → chunk-TWXKAY34.mjs} +273 -3
- package/dist/chunk-TWXKAY34.mjs.map +1 -0
- package/dist/{chunk-GYGMEDVS.js → chunk-UGNLKDI6.js} +1012 -823
- package/dist/chunk-UGNLKDI6.js.map +1 -0
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +16 -1
- package/dist/components/index.d.ts +16 -1
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/{tokenusage-admin.module-5wJ_tZTj.d.ts → config-BIjrg5wd.d.ts} +39 -1
- package/dist/{tokenusage-admin.module-DQVT4O4j.d.mts → config-BRUjtCd1.d.mts} +39 -1
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +22 -3
- package/dist/core/index.d.ts +22 -3
- package/dist/core/index.js +12 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +11 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.d.mts +413 -14
- package/dist/features/tokenusage/index.d.ts +413 -14
- package/dist/features/tokenusage/index.js +481 -120
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +431 -70
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +12 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -0
- package/src/core/index.ts +10 -0
- package/src/core/registry/ModuleRegistry.ts +4 -0
- package/src/features/administration/components/AdminIndexContainer.tsx +1 -1
- package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +1 -1
- package/src/features/billing/contexts/PriceContext.tsx +202 -0
- package/src/features/billing/contexts/ProductContext.tsx +185 -0
- package/src/features/billing/contexts/__tests__/PriceContext.spec.tsx +89 -0
- package/src/features/billing/contexts/__tests__/ProductContext.spec.tsx +81 -0
- package/src/features/billing/contexts/index.ts +2 -1
- package/src/features/billing/i18n-keys.ts +88 -0
- package/src/features/billing/stripe-price/components/containers/PriceContainer.tsx +21 -0
- package/src/features/billing/stripe-price/components/containers/index.ts +1 -0
- package/src/features/billing/stripe-price/components/details/PriceDetails.tsx +131 -0
- package/src/features/billing/stripe-price/components/details/index.ts +1 -0
- package/src/features/billing/stripe-price/components/forms/PriceArchiver.tsx +55 -0
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +307 -374
- package/src/features/billing/stripe-price/components/forms/index.ts +3 -1
- package/src/features/billing/stripe-price/components/index.ts +2 -0
- package/src/features/billing/stripe-price/components/lists/PricesList.tsx +49 -267
- package/src/features/billing/stripe-price/data/index.ts +1 -0
- package/src/features/billing/stripe-price/data/stripe-price.fields.ts +8 -0
- package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +119 -0
- package/src/features/billing/stripe-price/stripe-price.module.ts +5 -1
- package/src/features/billing/stripe-product/components/containers/ProductContainer.tsx +45 -0
- package/src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx +27 -0
- package/src/features/billing/stripe-product/components/containers/index.ts +2 -2
- package/src/features/billing/stripe-product/components/details/ProductDetails.tsx +45 -0
- package/src/features/billing/stripe-product/components/details/index.ts +1 -0
- package/src/features/billing/stripe-product/components/forms/ProductArchiver.tsx +55 -0
- package/src/features/billing/stripe-product/components/forms/ProductEditor.tsx +92 -81
- package/src/features/billing/stripe-product/components/forms/index.ts +3 -1
- package/src/features/billing/stripe-product/components/index.ts +1 -0
- package/src/features/billing/stripe-product/components/lists/ProductsList.tsx +45 -187
- package/src/features/billing/stripe-product/data/index.ts +1 -0
- package/src/features/billing/stripe-product/data/stripe-product.fields.ts +6 -0
- package/src/features/billing/stripe-product/hooks/useStripeProductTableStructure.tsx +80 -0
- package/src/features/billing/stripe-product/stripe-product.module.ts +5 -1
- package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +20 -4
- package/src/features/tokenusage/components/TokenUsageAdminTiles.tsx +31 -18
- package/src/features/tokenusage/components/TokenUsageBreakdownTable.tsx +13 -13
- package/src/features/tokenusage/components/TokenUsageRankedBar.tsx +24 -7
- package/src/features/tokenusage/components/TokenUsageReportContainer.tsx +102 -0
- package/src/features/tokenusage/components/TokenUsageReportFilterBar.tsx +34 -0
- package/src/features/tokenusage/components/TokenUsageReportTiles.tsx +161 -0
- package/src/features/tokenusage/components/TokenUsageTimelineChart.tsx +15 -13
- package/src/features/tokenusage/components/__tests__/TokenUsageAdminTiles.spec.tsx +4 -2
- package/src/features/tokenusage/components/__tests__/TokenUsageRankedBar.spec.tsx +3 -1
- package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +6 -3
- package/src/features/tokenusage/contexts/TokenUsageReportContext.tsx +199 -0
- package/src/features/tokenusage/data/TokenUsageReportService.ts +65 -0
- package/src/features/tokenusage/data/index.ts +9 -0
- package/src/features/tokenusage/data/tokenusage-report-breakdown.interface.ts +12 -0
- package/src/features/tokenusage/data/tokenusage-report-breakdown.ts +94 -0
- package/src/features/tokenusage/data/tokenusage-report-summary.interface.ts +12 -0
- package/src/features/tokenusage/data/tokenusage-report-summary.ts +87 -0
- package/src/features/tokenusage/data/tokenusage-report-timeline.interface.ts +13 -0
- package/src/features/tokenusage/data/tokenusage-report-timeline.ts +94 -0
- package/src/features/tokenusage/data/tokenusage-report.types.ts +51 -0
- package/src/features/tokenusage/i18n-keys.ts +28 -0
- package/src/features/tokenusage/index.ts +18 -0
- package/src/features/tokenusage/lib/config.ts +22 -0
- package/src/features/tokenusage/lib/formatters.ts +100 -0
- package/src/features/tokenusage/lib/metrics.ts +8 -26
- package/src/features/tokenusage/lib/palette.ts +41 -14
- package/src/features/tokenusage/tokenusage-admin.module.ts +4 -0
- package/src/features/tokenusage/tokenusage.modules.ts +40 -0
- package/src/shadcnui/ui/chart.tsx +30 -3
- package/dist/chunk-GYGMEDVS.js.map +0 -1
- package/dist/chunk-LCCRUWWY.mjs.map +0 -1
- package/dist/chunk-M2EGUO2F.mjs.map +0 -1
- package/dist/chunk-V66AZWPG.js.map +0 -1
- package/src/features/billing/contexts/BillingContext.tsx +0 -95
- package/src/features/billing/stripe-product/components/containers/ProductsAdminContainer.tsx +0 -98
- package/src/features/billing/stripe-product/components/containers/ProductsAdminPageContainer.tsx +0 -27
- /package/dist/{BlockNoteEditor-GWAV5ESO.mjs.map → BlockNoteEditor-VVAY73V2.mjs.map} +0 -0
- /package/dist/{chunk-3LVSA7IM.mjs.map → chunk-PHEFWL6L.mjs.map} +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArchiveIcon, RotateCcwIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { errorToast } from "../../../../../components/errors";
|
|
7
|
+
import { Button, ConfirmDialog } from "../../../../../shadcnui";
|
|
8
|
+
import { useProductContext } from "../../../contexts/ProductContext";
|
|
9
|
+
import { StripeProductInterface } from "../../data/stripe-product.interface";
|
|
10
|
+
|
|
11
|
+
type ProductArchiverProps = {
|
|
12
|
+
product: StripeProductInterface;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Archive / restore control for a product. Built on ConfirmDialog, which is
|
|
17
|
+
* controlled and trigger-less by design — the caller owns the trigger button,
|
|
18
|
+
* so none of Base UI's trigger-composition pitfalls apply (no asChild, no
|
|
19
|
+
* nested <button>).
|
|
20
|
+
*/
|
|
21
|
+
export function ProductArchiver({ product }: ProductArchiverProps) {
|
|
22
|
+
const t = useTranslations();
|
|
23
|
+
const { archiveProduct, restoreProduct } = useProductContext();
|
|
24
|
+
const [open, setOpen] = useState<boolean>(false);
|
|
25
|
+
|
|
26
|
+
const isArchived = !product.active;
|
|
27
|
+
const scope = isArchived ? "restore" : "archive";
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
|
|
32
|
+
{isArchived ? <RotateCcwIcon /> : <ArchiveIcon />}
|
|
33
|
+
{t(`billing.admin.products.${scope}.confirm`)}
|
|
34
|
+
</Button>
|
|
35
|
+
<ConfirmDialog
|
|
36
|
+
open={open}
|
|
37
|
+
onOpenChange={setOpen}
|
|
38
|
+
title={t(`billing.admin.products.${scope}.title`)}
|
|
39
|
+
description={t(`billing.admin.products.${scope}.description`, { name: product.name })}
|
|
40
|
+
confirmLabel={t(`billing.admin.products.${scope}.confirm`)}
|
|
41
|
+
cancelLabel={t("ui.buttons.cancel")}
|
|
42
|
+
destructive={!isArchived}
|
|
43
|
+
onConfirm={async () => {
|
|
44
|
+
try {
|
|
45
|
+
if (isArchived) await restoreProduct(product.id);
|
|
46
|
+
else await archiveProduct(product.id);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
errorToast({ title: t(`billing.admin.products.${scope}.title`), error });
|
|
49
|
+
throw error; // keeps the dialog open — ConfirmDialog swallows rejections
|
|
50
|
+
}
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -1,100 +1,111 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ReactNode, useCallback, useMemo } from "react";
|
|
6
|
+
import { useForm } from "react-hook-form";
|
|
6
7
|
import { v4 } from "uuid";
|
|
7
8
|
import { z } from "zod";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { EditorSheet, FormInput, FormTextarea } from "../../../../../components";
|
|
10
|
+
import { Modules } from "../../../../../core";
|
|
11
|
+
import { useI18nRouter } from "../../../../../i18n";
|
|
12
|
+
import { useProductContext } from "../../../contexts/ProductContext";
|
|
11
13
|
import { StripeProductInterface } from "../../data/stripe-product.interface";
|
|
12
|
-
import { StripeProductService } from "../../data/stripe-product.service";
|
|
13
14
|
|
|
14
|
-
type ProductEditorProps = {
|
|
15
|
+
export type ProductEditorProps = {
|
|
15
16
|
product?: StripeProductInterface;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
propagateChanges?: (product: StripeProductInterface) => void;
|
|
18
|
+
onSuccess?: () => void | Promise<void>;
|
|
19
|
+
trigger?: ReactNode;
|
|
20
|
+
forceShow?: boolean;
|
|
21
|
+
onClose?: () => void;
|
|
22
|
+
dialogOpen?: boolean;
|
|
23
|
+
onDialogOpenChange?: (open: boolean) => void;
|
|
19
24
|
};
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
function ProductEditorInternal({
|
|
27
|
+
product,
|
|
28
|
+
propagateChanges,
|
|
29
|
+
onSuccess,
|
|
30
|
+
trigger,
|
|
31
|
+
forceShow,
|
|
32
|
+
onClose,
|
|
33
|
+
dialogOpen,
|
|
34
|
+
onDialogOpenChange,
|
|
35
|
+
}: ProductEditorProps) {
|
|
36
|
+
const t = useTranslations();
|
|
37
|
+
const router = useI18nRouter();
|
|
38
|
+
const { createProduct, updateProduct } = useProductContext();
|
|
39
|
+
const isEdit = !!product;
|
|
23
40
|
|
|
24
|
-
const formSchema =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
const formSchema = useMemo(
|
|
42
|
+
() =>
|
|
43
|
+
z.object({
|
|
44
|
+
id: z.uuidv4(),
|
|
45
|
+
name: z.string().min(1, { message: t("billing.admin.products.errors.name") }),
|
|
46
|
+
description: z.string().optional(),
|
|
47
|
+
}),
|
|
48
|
+
[t],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// `active` is deliberately absent: archiving is the single deactivation path
|
|
52
|
+
// (ProductArchiver), and a second toggle here would let the two disagree.
|
|
53
|
+
const getDefaultValues = useCallback(
|
|
54
|
+
() => ({
|
|
55
|
+
id: product?.id ?? v4(),
|
|
56
|
+
name: product?.name ?? "",
|
|
57
|
+
description: product?.description ?? "",
|
|
58
|
+
}),
|
|
59
|
+
[product],
|
|
60
|
+
);
|
|
29
61
|
|
|
30
62
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
31
63
|
resolver: zodResolver(formSchema),
|
|
32
|
-
defaultValues:
|
|
33
|
-
name: product?.name || "",
|
|
34
|
-
description: product?.description || "",
|
|
35
|
-
active: product?.active ?? true,
|
|
36
|
-
},
|
|
64
|
+
defaultValues: getDefaultValues(),
|
|
37
65
|
});
|
|
38
66
|
|
|
39
|
-
const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (values) => {
|
|
40
|
-
setIsSubmitting(true);
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
if (product) {
|
|
44
|
-
// Update existing product
|
|
45
|
-
await StripeProductService.updateProduct({
|
|
46
|
-
id: product.id,
|
|
47
|
-
name: values.name,
|
|
48
|
-
description: values.description,
|
|
49
|
-
active: values.active,
|
|
50
|
-
});
|
|
51
|
-
} else {
|
|
52
|
-
// Create new product
|
|
53
|
-
await StripeProductService.createProduct({
|
|
54
|
-
id: v4(),
|
|
55
|
-
name: values.name,
|
|
56
|
-
description: values.description,
|
|
57
|
-
active: values.active,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
onSuccess();
|
|
62
|
-
onOpenChange(false);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.error("[ProductEditor] Failed to save product:", error);
|
|
65
|
-
} finally {
|
|
66
|
-
setIsSubmitting(false);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
67
|
return (
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
68
|
+
<EditorSheet
|
|
69
|
+
form={form}
|
|
70
|
+
entityType={t("billing.admin.products.entity")}
|
|
71
|
+
entityName={product?.name}
|
|
72
|
+
isEdit={isEdit}
|
|
73
|
+
module={Modules.StripeProduct}
|
|
74
|
+
size="md"
|
|
75
|
+
propagateChanges={propagateChanges}
|
|
76
|
+
onSuccess={onSuccess}
|
|
77
|
+
onNavigate={(url) => router.push(url)}
|
|
78
|
+
onSubmit={async (values) => {
|
|
79
|
+
const payload = { id: values.id, name: values.name, description: values.description };
|
|
80
|
+
return isEdit ? await updateProduct(payload) : await createProduct(payload);
|
|
81
|
+
}}
|
|
82
|
+
onReset={getDefaultValues}
|
|
83
|
+
trigger={trigger}
|
|
84
|
+
forceShow={forceShow}
|
|
85
|
+
onClose={onClose}
|
|
86
|
+
dialogOpen={dialogOpen}
|
|
87
|
+
onDialogOpenChange={onDialogOpenChange}
|
|
88
|
+
>
|
|
89
|
+
<div className="flex w-full flex-col gap-y-4">
|
|
90
|
+
<FormInput
|
|
91
|
+
form={form}
|
|
92
|
+
id="name"
|
|
93
|
+
name={t("billing.admin.products.fields.name")}
|
|
94
|
+
placeholder={t("billing.admin.products.placeholders.name")}
|
|
95
|
+
isRequired
|
|
96
|
+
/>
|
|
97
|
+
<FormTextarea
|
|
98
|
+
form={form}
|
|
99
|
+
id="description"
|
|
100
|
+
name={t("billing.admin.products.fields.description")}
|
|
101
|
+
placeholder={t("billing.admin.products.placeholders.description")}
|
|
102
|
+
className="min-h-32"
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
</EditorSheet>
|
|
99
106
|
);
|
|
100
107
|
}
|
|
108
|
+
|
|
109
|
+
export default function ProductEditor(props: ProductEditorProps) {
|
|
110
|
+
return <ProductEditorInternal {...props} />;
|
|
111
|
+
}
|
|
@@ -1,203 +1,61 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
AlertDialogHeader,
|
|
13
|
-
AlertDialogTitle,
|
|
14
|
-
Badge,
|
|
15
|
-
Button,
|
|
16
|
-
} from "../../../../../shadcnui";
|
|
17
|
-
import { PricesList } from "../../../stripe-price/components/lists/PricesList";
|
|
3
|
+
import { PackageIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ReactNode, useEffect } from "react";
|
|
6
|
+
import { ContentListTable } from "../../../../../components";
|
|
7
|
+
import { Modules } from "../../../../../core";
|
|
8
|
+
import { DataListRetriever, useDataListRetriever } from "../../../../../hooks";
|
|
9
|
+
import { EmptyState } from "../../../../../shadcnui";
|
|
10
|
+
import { useProductContext } from "../../../contexts/ProductContext";
|
|
11
|
+
import { StripeProductFields } from "../../data/stripe-product.fields";
|
|
18
12
|
import { StripeProductInterface } from "../../data/stripe-product.interface";
|
|
19
13
|
import { StripeProductService } from "../../data/stripe-product.service";
|
|
20
|
-
import
|
|
14
|
+
import ProductEditor from "../forms/ProductEditor";
|
|
21
15
|
|
|
22
16
|
type ProductsListProps = {
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Pass this whenever the list is the page's own content inside a
|
|
19
|
+
* `RoundPageContainer fullWidth`. Without it ContentListTable wraps itself in
|
|
20
|
+
* `rounded-md border`, drawing a second bordered card inside the page's
|
|
21
|
+
* rounded shell.
|
|
22
|
+
*/
|
|
23
|
+
fullWidth?: boolean;
|
|
25
24
|
};
|
|
26
25
|
|
|
27
|
-
export function ProductsList({
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const [archivingProductId, setArchivingProductId] = useState<string | null>(null);
|
|
31
|
-
const [reactivatingProductId, setReactivatingProductId] = useState<string | null>(null);
|
|
32
|
-
const [productToArchive, setProductToArchive] = useState<StripeProductInterface | null>(null);
|
|
33
|
-
const [productToReactivate, setProductToReactivate] = useState<StripeProductInterface | null>(null);
|
|
26
|
+
export function ProductsList({ fullWidth }: ProductsListProps = {}) {
|
|
27
|
+
const t = useTranslations();
|
|
28
|
+
const { catalogueVersion } = useProductContext();
|
|
34
29
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
const data: DataListRetriever<StripeProductInterface> = useDataListRetriever({
|
|
31
|
+
retriever: (params) => StripeProductService.listProducts(params),
|
|
32
|
+
retrieverParams: {},
|
|
33
|
+
module: Modules.StripeProduct,
|
|
34
|
+
});
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} catch (_error) {
|
|
46
|
-
console.error("[ProductsList] Failed to archive product:", _error);
|
|
47
|
-
// Keep dialog open on error so user can retry or cancel
|
|
48
|
-
} finally {
|
|
49
|
-
setArchivingProductId(null);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
36
|
+
// Archiving and restoring happen on the detail page, so the list has to learn
|
|
37
|
+
// about them through the provider's counter rather than a direct callback.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (catalogueVersion > 0) void data.refresh();
|
|
40
|
+
}, [catalogueVersion]);
|
|
52
41
|
|
|
53
|
-
const
|
|
54
|
-
if (!productToReactivate) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
setReactivatingProductId(productToReactivate.id);
|
|
59
|
-
try {
|
|
60
|
-
const _reactivatedProduct = await StripeProductService.reactivateProduct({ id: productToReactivate.id });
|
|
61
|
-
setProductToReactivate(null); // Close dialog on success
|
|
62
|
-
onProductsChange();
|
|
63
|
-
} catch (_error) {
|
|
64
|
-
// Keep dialog open on error so user can retry or cancel
|
|
65
|
-
} finally {
|
|
66
|
-
setReactivatingProductId(null);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const toggleExpand = (productId: string) => {
|
|
71
|
-
setExpandedProductId(expandedProductId === productId ? null : productId);
|
|
72
|
-
};
|
|
42
|
+
const functions: ReactNode[] = [<ProductEditor key="create-product" />];
|
|
73
43
|
|
|
74
44
|
return (
|
|
75
|
-
<
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<div className="flex-1">
|
|
88
|
-
<div className="flex items-center gap-x-3">
|
|
89
|
-
<h3 className="text-sm font-medium">{product.name}</h3>
|
|
90
|
-
{product.active ? (
|
|
91
|
-
<Badge variant="softGreen">Active</Badge>
|
|
92
|
-
) : (
|
|
93
|
-
<Badge variant="softGray">Inactive</Badge>
|
|
94
|
-
)}
|
|
95
|
-
</div>
|
|
96
|
-
{product.description && <p className="text-muted-foreground text-sm mt-1">{product.description}</p>}
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
{/* Action Buttons */}
|
|
101
|
-
<div className="flex items-center gap-x-2">
|
|
102
|
-
<Button variant="outline" size="sm" onClick={() => setEditingProduct(product)}>
|
|
103
|
-
<Edit className="h-4 w-4 mr-1" />
|
|
104
|
-
Edit
|
|
105
|
-
</Button>
|
|
106
|
-
{product.active ? (
|
|
107
|
-
<Button
|
|
108
|
-
variant="outline"
|
|
109
|
-
size="sm"
|
|
110
|
-
onClick={() => setProductToArchive(product)}
|
|
111
|
-
disabled={isArchiving}
|
|
112
|
-
>
|
|
113
|
-
<Archive className="h-4 w-4 mr-1" />
|
|
114
|
-
{isArchiving ? "Archiving..." : "Archive"}
|
|
115
|
-
</Button>
|
|
116
|
-
) : (
|
|
117
|
-
<Button
|
|
118
|
-
variant="outline"
|
|
119
|
-
size="sm"
|
|
120
|
-
onClick={() => setProductToReactivate(product)}
|
|
121
|
-
disabled={isReactivating}
|
|
122
|
-
>
|
|
123
|
-
<RefreshCw className="h-4 w-4 mr-1" />
|
|
124
|
-
{isReactivating ? "Reactivating..." : "Reactivate"}
|
|
125
|
-
</Button>
|
|
126
|
-
)}
|
|
127
|
-
<Button variant="ghost" size="sm" onClick={() => toggleExpand(product.id)}>
|
|
128
|
-
{isExpanded ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
|
129
|
-
</Button>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
{/* Expandable Prices Section */}
|
|
134
|
-
{isExpanded && (
|
|
135
|
-
<div className="border-t bg-muted/30 p-6">
|
|
136
|
-
<PricesList productId={product.id} onPricesChange={onProductsChange} />
|
|
137
|
-
</div>
|
|
138
|
-
)}
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
141
|
-
})}
|
|
142
|
-
|
|
143
|
-
{/* Edit Product Modal */}
|
|
144
|
-
{editingProduct && (
|
|
145
|
-
<ProductEditor
|
|
146
|
-
product={editingProduct}
|
|
147
|
-
open={!!editingProduct}
|
|
148
|
-
onOpenChange={(open) => !open && setEditingProduct(null)}
|
|
149
|
-
onSuccess={() => {
|
|
150
|
-
onProductsChange();
|
|
151
|
-
setEditingProduct(null);
|
|
152
|
-
}}
|
|
45
|
+
<ContentListTable
|
|
46
|
+
data={data}
|
|
47
|
+
fields={[StripeProductFields.name, StripeProductFields.status, StripeProductFields.prices]}
|
|
48
|
+
tableGeneratorType={Modules.StripeProduct}
|
|
49
|
+
functions={functions}
|
|
50
|
+
fullWidth={fullWidth}
|
|
51
|
+
title={t("billing.admin.products.title")}
|
|
52
|
+
emptyState={
|
|
53
|
+
<EmptyState
|
|
54
|
+
icon={PackageIcon}
|
|
55
|
+
title={t("billing.admin.products.empty.title")}
|
|
56
|
+
description={t("billing.admin.products.empty.description")}
|
|
153
57
|
/>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{/* Archive Product Confirmation Dialog */}
|
|
157
|
-
<AlertDialog open={!!productToArchive} onOpenChange={(open) => !open && setProductToArchive(null)}>
|
|
158
|
-
<AlertDialogContent>
|
|
159
|
-
<AlertDialogHeader>
|
|
160
|
-
<AlertDialogTitle>Archive Product</AlertDialogTitle>
|
|
161
|
-
<AlertDialogDescription>
|
|
162
|
-
Are you sure you want to archive "{productToArchive?.name}"? This will deactivate it and it will
|
|
163
|
-
no longer be available for new subscriptions.
|
|
164
|
-
</AlertDialogDescription>
|
|
165
|
-
</AlertDialogHeader>
|
|
166
|
-
<AlertDialogFooter>
|
|
167
|
-
<AlertDialogCancel disabled={!!archivingProductId}>Cancel</AlertDialogCancel>
|
|
168
|
-
<AlertDialogAction
|
|
169
|
-
onClick={handleArchive}
|
|
170
|
-
disabled={!!archivingProductId}
|
|
171
|
-
className="bg-red-600 hover:bg-red-700"
|
|
172
|
-
>
|
|
173
|
-
{archivingProductId ? "Archiving..." : "Archive"}
|
|
174
|
-
</AlertDialogAction>
|
|
175
|
-
</AlertDialogFooter>
|
|
176
|
-
</AlertDialogContent>
|
|
177
|
-
</AlertDialog>
|
|
178
|
-
|
|
179
|
-
{/* Reactivate Product Confirmation Dialog */}
|
|
180
|
-
<AlertDialog open={!!productToReactivate} onOpenChange={(open) => !open && setProductToReactivate(null)}>
|
|
181
|
-
<AlertDialogContent>
|
|
182
|
-
<AlertDialogHeader>
|
|
183
|
-
<AlertDialogTitle>Reactivate Product</AlertDialogTitle>
|
|
184
|
-
<AlertDialogDescription>
|
|
185
|
-
Are you sure you want to reactivate "{productToReactivate?.name}"? This will make it available
|
|
186
|
-
for new subscriptions again.
|
|
187
|
-
</AlertDialogDescription>
|
|
188
|
-
</AlertDialogHeader>
|
|
189
|
-
<AlertDialogFooter>
|
|
190
|
-
<AlertDialogCancel disabled={!!reactivatingProductId}>Cancel</AlertDialogCancel>
|
|
191
|
-
<AlertDialogAction
|
|
192
|
-
onClick={handleReactivate}
|
|
193
|
-
disabled={!!reactivatingProductId}
|
|
194
|
-
className="bg-green-600 hover:bg-green-700"
|
|
195
|
-
>
|
|
196
|
-
{reactivatingProductId ? "Reactivating..." : "Reactivate"}
|
|
197
|
-
</AlertDialogAction>
|
|
198
|
-
</AlertDialogFooter>
|
|
199
|
-
</AlertDialogContent>
|
|
200
|
-
</AlertDialog>
|
|
201
|
-
</div>
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
202
60
|
);
|
|
203
61
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ColumnDef } from "@tanstack/react-table";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { Badge, Link } from "../../../../shadcnui";
|
|
7
|
+
import { Modules } from "../../../../core";
|
|
8
|
+
import { registerTableGenerator, TableContent, usePageUrlGenerator, UseTableStructureHook } from "../../../../hooks";
|
|
9
|
+
import { StripeProductFields } from "../data/stripe-product.fields";
|
|
10
|
+
import { StripeProductInterface } from "../data/stripe-product.interface";
|
|
11
|
+
|
|
12
|
+
export const useStripeProductTableStructure: UseTableStructureHook<StripeProductInterface, StripeProductFields> = (
|
|
13
|
+
params,
|
|
14
|
+
) => {
|
|
15
|
+
const t = useTranslations();
|
|
16
|
+
const generateUrl = usePageUrlGenerator();
|
|
17
|
+
|
|
18
|
+
const tableData = useMemo(() => {
|
|
19
|
+
return params.data.map((product: StripeProductInterface) => {
|
|
20
|
+
const entry: TableContent<StripeProductInterface> = { jsonApiData: product };
|
|
21
|
+
entry[StripeProductFields.stripeProductId] = product.id;
|
|
22
|
+
params.fields.forEach((field) => {
|
|
23
|
+
entry[field] = (product as any)[field as keyof StripeProductInterface];
|
|
24
|
+
});
|
|
25
|
+
return entry;
|
|
26
|
+
});
|
|
27
|
+
}, [params.data, params.fields]);
|
|
28
|
+
|
|
29
|
+
const fieldColumnMap: Partial<Record<StripeProductFields, () => any>> = {
|
|
30
|
+
[StripeProductFields.name]: () => ({
|
|
31
|
+
id: "name",
|
|
32
|
+
accessorKey: "name",
|
|
33
|
+
header: t("billing.admin.products.fields.name"),
|
|
34
|
+
cell: ({ row }: { row: TableContent<StripeProductInterface> }) => {
|
|
35
|
+
const product: StripeProductInterface = row.original.jsonApiData;
|
|
36
|
+
return <Link href={generateUrl({ page: Modules.StripeProduct, id: product.id })}>{product.name}</Link>;
|
|
37
|
+
},
|
|
38
|
+
enableSorting: false,
|
|
39
|
+
enableHiding: false,
|
|
40
|
+
}),
|
|
41
|
+
[StripeProductFields.status]: () => ({
|
|
42
|
+
id: "status",
|
|
43
|
+
accessorKey: "status",
|
|
44
|
+
header: t("billing.admin.products.fields.status"),
|
|
45
|
+
cell: ({ row }: { row: TableContent<StripeProductInterface> }) => {
|
|
46
|
+
const product: StripeProductInterface = row.original.jsonApiData;
|
|
47
|
+
return product.active ? (
|
|
48
|
+
<Badge variant="softGreen">{t("billing.admin.products.status.active")}</Badge>
|
|
49
|
+
) : (
|
|
50
|
+
<Badge variant="softGray">{t("billing.admin.products.status.archived")}</Badge>
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
enableSorting: false,
|
|
54
|
+
enableHiding: false,
|
|
55
|
+
}),
|
|
56
|
+
[StripeProductFields.prices]: () => ({
|
|
57
|
+
id: "prices",
|
|
58
|
+
accessorKey: "prices",
|
|
59
|
+
header: t("billing.admin.products.fields.prices"),
|
|
60
|
+
cell: ({ row }: { row: TableContent<StripeProductInterface> }) => {
|
|
61
|
+
const product: StripeProductInterface = row.original.jsonApiData;
|
|
62
|
+
// Typography role 15 (numeric): tabular-nums, right-aligned. NEVER
|
|
63
|
+
// font-mono — no monospace font is loaded in these apps.
|
|
64
|
+
return <span className="block text-xs tabular-nums text-right">{product.stripePrices.length}</span>;
|
|
65
|
+
},
|
|
66
|
+
enableSorting: false,
|
|
67
|
+
enableHiding: false,
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const columns = useMemo(() => {
|
|
72
|
+
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
73
|
+
TableContent<StripeProductInterface>
|
|
74
|
+
>[];
|
|
75
|
+
}, [params.fields, fieldColumnMap, t, generateUrl]);
|
|
76
|
+
|
|
77
|
+
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
registerTableGenerator("stripe-products", useStripeProductTableStructure);
|
|
@@ -4,7 +4,11 @@ import { StripeProduct } from "./data";
|
|
|
4
4
|
export const StripeProductModule = (factory: ModuleFactory) =>
|
|
5
5
|
factory({
|
|
6
6
|
name: "stripe-products",
|
|
7
|
-
|
|
7
|
+
// The administration route this entity is served at. `name` above stays the
|
|
8
|
+
// API endpoint; this drives generateUrl / rewriteUrl / EditorSheet
|
|
9
|
+
// navigation, so it must match the real Next.js route or RoundPageContainer
|
|
10
|
+
// rewrites the URL to a 404 on the first tab change.
|
|
11
|
+
pageUrl: "/administration/products",
|
|
8
12
|
model: StripeProduct,
|
|
9
13
|
moduleId: "f8c4a1e9-3b2d-4f7a-9e5c-1d8a6b3c9f2e",
|
|
10
14
|
});
|