@powerhousedao/service-offering 1.0.0-dev.4 → 1.0.0-dev.6
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/document-models/facet/v1/actions.d.ts +3 -1
- package/dist/document-models/facet/v1/actions.d.ts.map +1 -1
- package/dist/document-models/facet/v1/gen/controller.d.ts +4 -0
- package/dist/document-models/facet/v1/gen/controller.d.ts.map +1 -0
- package/dist/document-models/facet/v1/gen/controller.js +3 -0
- package/dist/document-models/facet/v1/gen/document-model.d.ts.map +1 -1
- package/dist/document-models/facet/v1/gen/document-model.js +31 -7
- package/dist/document-models/facet/v1/gen/document-schema.d.ts +6 -6
- package/dist/document-models/facet/v1/gen/index.d.ts +1 -0
- package/dist/document-models/facet/v1/gen/index.d.ts.map +1 -1
- package/dist/document-models/facet/v1/gen/index.js +1 -0
- package/dist/document-models/facet/v1/gen/option-management/error.d.ts +27 -1
- package/dist/document-models/facet/v1/gen/option-management/error.d.ts.map +1 -1
- package/dist/document-models/facet/v1/gen/option-management/error.js +23 -1
- package/dist/document-models/facet/v1/gen/ph-factories.js +2 -2
- package/dist/document-models/facet/v1/gen/schema/types.d.ts +2 -2
- package/dist/document-models/facet/v1/gen/schema/types.d.ts.map +1 -1
- package/dist/document-models/facet/v1/gen/schema/zod.js +2 -2
- package/dist/document-models/facet/v1/gen/utils.js +2 -2
- package/dist/document-models/facet/v1/module.d.ts +1 -1
- package/dist/document-models/facet/v1/module.d.ts.map +1 -1
- package/dist/document-models/facet/v1/module.js +4 -1
- package/dist/document-models/resource-instance/v1/actions.d.ts +4 -1
- package/dist/document-models/resource-instance/v1/actions.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/configuration-management/error.d.ts +34 -1
- package/dist/document-models/resource-instance/v1/gen/configuration-management/error.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/configuration-management/error.js +32 -1
- package/dist/document-models/resource-instance/v1/gen/controller.d.ts +4 -0
- package/dist/document-models/resource-instance/v1/gen/controller.d.ts.map +1 -0
- package/dist/document-models/resource-instance/v1/gen/controller.js +3 -0
- package/dist/document-models/resource-instance/v1/gen/document-model.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/document-model.js +222 -70
- package/dist/document-models/resource-instance/v1/gen/document-schema.d.ts +0 -6
- package/dist/document-models/resource-instance/v1/gen/document-schema.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/index.d.ts +1 -0
- package/dist/document-models/resource-instance/v1/gen/index.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/index.js +1 -0
- package/dist/document-models/resource-instance/v1/gen/instance-management/actions.d.ts +6 -2
- package/dist/document-models/resource-instance/v1/gen/instance-management/actions.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/instance-management/creators.d.ts +3 -2
- package/dist/document-models/resource-instance/v1/gen/instance-management/creators.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/instance-management/creators.js +2 -1
- package/dist/document-models/resource-instance/v1/gen/instance-management/error.d.ts +98 -1
- package/dist/document-models/resource-instance/v1/gen/instance-management/error.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/instance-management/error.js +112 -1
- package/dist/document-models/resource-instance/v1/gen/instance-management/operations.d.ts +2 -1
- package/dist/document-models/resource-instance/v1/gen/instance-management/operations.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/ph-factories.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/ph-factories.js +0 -2
- package/dist/document-models/resource-instance/v1/gen/reducer.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/reducer.js +6 -1
- package/dist/document-models/resource-instance/v1/gen/schema/types.d.ts +3 -3
- package/dist/document-models/resource-instance/v1/gen/schema/types.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/schema/zod.d.ts +2 -1
- package/dist/document-models/resource-instance/v1/gen/schema/zod.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/schema/zod.js +5 -3
- package/dist/document-models/resource-instance/v1/gen/utils.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/gen/utils.js +0 -2
- package/dist/document-models/resource-instance/v1/module.d.ts +1 -1
- package/dist/document-models/resource-instance/v1/module.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/module.js +4 -1
- package/dist/document-models/resource-instance/v1/src/reducers/configuration-management.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/src/reducers/configuration-management.js +57 -53
- package/dist/document-models/resource-instance/v1/src/reducers/instance-management.d.ts.map +1 -1
- package/dist/document-models/resource-instance/v1/src/reducers/instance-management.js +57 -21
- package/dist/document-models/resource-instance/v1/tests/instance-management.test.js +11 -1
- package/dist/document-models/resource-template/v1/actions.d.ts +3 -1
- package/dist/document-models/resource-template/v1/actions.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/gen/audience-management/error.d.ts +20 -1
- package/dist/document-models/resource-template/v1/gen/audience-management/error.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/gen/audience-management/error.js +16 -1
- package/dist/document-models/resource-template/v1/gen/controller.d.ts +4 -0
- package/dist/document-models/resource-template/v1/gen/controller.d.ts.map +1 -0
- package/dist/document-models/resource-template/v1/gen/controller.js +3 -0
- package/dist/document-models/resource-template/v1/gen/document-model.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/gen/document-model.js +207 -89
- package/dist/document-models/resource-template/v1/gen/document-schema.d.ts +12 -12
- package/dist/document-models/resource-template/v1/gen/facet-targeting/error.d.ts +27 -1
- package/dist/document-models/resource-template/v1/gen/facet-targeting/error.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/gen/facet-targeting/error.js +23 -1
- package/dist/document-models/resource-template/v1/gen/index.d.ts +1 -0
- package/dist/document-models/resource-template/v1/gen/index.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/gen/index.js +1 -0
- package/dist/document-models/resource-template/v1/gen/option-group-management/error.d.ts +27 -1
- package/dist/document-models/resource-template/v1/gen/option-group-management/error.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/gen/option-group-management/error.js +23 -1
- package/dist/document-models/resource-template/v1/gen/ph-factories.js +3 -3
- package/dist/document-models/resource-template/v1/gen/schema/types.d.ts +4 -4
- package/dist/document-models/resource-template/v1/gen/schema/types.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/gen/schema/zod.js +4 -4
- package/dist/document-models/resource-template/v1/gen/service-management/error.d.ts +51 -1
- package/dist/document-models/resource-template/v1/gen/service-management/error.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/gen/service-management/error.js +49 -1
- package/dist/document-models/resource-template/v1/gen/utils.js +3 -3
- package/dist/document-models/resource-template/v1/module.d.ts +1 -1
- package/dist/document-models/resource-template/v1/module.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/module.js +4 -1
- package/dist/document-models/resource-template/v1/src/reducers/option-group-management.d.ts.map +1 -1
- package/dist/document-models/resource-template/v1/src/reducers/option-group-management.js +2 -21
- package/dist/document-models/service-offering/v1/actions.d.ts +3 -1
- package/dist/document-models/service-offering/v1/actions.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/gen/controller.d.ts +4 -0
- package/dist/document-models/service-offering/v1/gen/controller.d.ts.map +1 -0
- package/dist/document-models/service-offering/v1/gen/controller.js +3 -0
- package/dist/document-models/service-offering/v1/gen/document-model.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/gen/document-model.js +421 -199
- package/dist/document-models/service-offering/v1/gen/document-schema.d.ts +9 -9
- package/dist/document-models/service-offering/v1/gen/index.d.ts +1 -0
- package/dist/document-models/service-offering/v1/gen/index.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/gen/index.js +1 -0
- package/dist/document-models/service-offering/v1/gen/offering/error.d.ts +41 -1
- package/dist/document-models/service-offering/v1/gen/offering/error.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/gen/offering/error.js +37 -1
- package/dist/document-models/service-offering/v1/gen/option-groups/error.d.ts +55 -1
- package/dist/document-models/service-offering/v1/gen/option-groups/error.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/gen/option-groups/error.js +53 -1
- package/dist/document-models/service-offering/v1/gen/ph-factories.js +3 -3
- package/dist/document-models/service-offering/v1/gen/schema/types.d.ts +134 -61
- package/dist/document-models/service-offering/v1/gen/schema/types.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/gen/schema/zod.d.ts +35 -10
- package/dist/document-models/service-offering/v1/gen/schema/zod.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/gen/schema/zod.js +182 -64
- package/dist/document-models/service-offering/v1/gen/services/error.d.ts +20 -1
- package/dist/document-models/service-offering/v1/gen/services/error.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/gen/services/error.js +16 -1
- package/dist/document-models/service-offering/v1/gen/tiers/error.d.ts +100 -1
- package/dist/document-models/service-offering/v1/gen/tiers/error.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/gen/tiers/error.js +106 -1
- package/dist/document-models/service-offering/v1/gen/utils.js +4 -4
- package/dist/document-models/service-offering/v1/module.d.ts +1 -1
- package/dist/document-models/service-offering/v1/module.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/module.js +4 -1
- package/dist/document-models/service-offering/v1/src/reducers/offering.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/src/reducers/offering.js +20 -12
- package/dist/document-models/service-offering/v1/src/reducers/option-groups.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/src/reducers/option-groups.js +157 -39
- package/dist/document-models/service-offering/v1/src/reducers/services.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/src/reducers/services.js +17 -14
- package/dist/document-models/service-offering/v1/src/reducers/tiers.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/src/reducers/tiers.js +111 -78
- package/dist/document-models/service-offering/v1/src/utils.d.ts +60 -1
- package/dist/document-models/service-offering/v1/src/utils.d.ts.map +1 -1
- package/dist/document-models/service-offering/v1/src/utils.js +173 -1
- package/dist/document-models/service-offering/v1/tests/option-groups.test.js +1 -1
- package/dist/document-models/service-offering/v1/utils.d.ts +3 -0
- package/dist/document-models/service-offering/v1/utils.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/actions.d.ts +3 -1
- package/dist/document-models/subscription-instance/v1/actions.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/gen/controller.d.ts +4 -0
- package/dist/document-models/subscription-instance/v1/gen/controller.d.ts.map +1 -0
- package/dist/document-models/subscription-instance/v1/gen/controller.js +3 -0
- package/dist/document-models/subscription-instance/v1/gen/document-model.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/gen/document-model.js +488 -246
- package/dist/document-models/subscription-instance/v1/gen/document-schema.d.ts +3 -3
- package/dist/document-models/subscription-instance/v1/gen/index.d.ts +1 -0
- package/dist/document-models/subscription-instance/v1/gen/index.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/gen/index.js +1 -0
- package/dist/document-models/subscription-instance/v1/gen/metrics/error.d.ts +73 -1
- package/dist/document-models/subscription-instance/v1/gen/metrics/error.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/gen/metrics/error.js +86 -1
- package/dist/document-models/subscription-instance/v1/gen/ph-factories.js +1 -1
- package/dist/document-models/subscription-instance/v1/gen/schema/types.d.ts +199 -82
- package/dist/document-models/subscription-instance/v1/gen/schema/types.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/gen/schema/zod.d.ts +22 -12
- package/dist/document-models/subscription-instance/v1/gen/schema/zod.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/gen/schema/zod.js +230 -84
- package/dist/document-models/subscription-instance/v1/gen/service/error.d.ts +62 -1
- package/dist/document-models/subscription-instance/v1/gen/service/error.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/gen/service/error.js +60 -1
- package/dist/document-models/subscription-instance/v1/gen/service-group/error.d.ts +39 -1
- package/dist/document-models/subscription-instance/v1/gen/service-group/error.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/gen/service-group/error.js +39 -1
- package/dist/document-models/subscription-instance/v1/gen/subscription/error.d.ts +55 -1
- package/dist/document-models/subscription-instance/v1/gen/subscription/error.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/gen/subscription/error.js +51 -1
- package/dist/document-models/subscription-instance/v1/gen/utils.js +2 -2
- package/dist/document-models/subscription-instance/v1/module.d.ts +1 -1
- package/dist/document-models/subscription-instance/v1/module.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/module.js +4 -1
- package/dist/document-models/subscription-instance/v1/src/reducers/customer.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/src/reducers/customer.js +1 -0
- package/dist/document-models/subscription-instance/v1/src/reducers/metrics.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/src/reducers/metrics.js +70 -45
- package/dist/document-models/subscription-instance/v1/src/reducers/service-group.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/src/reducers/service-group.js +108 -30
- package/dist/document-models/subscription-instance/v1/src/reducers/service.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/src/reducers/service.js +108 -39
- package/dist/document-models/subscription-instance/v1/src/reducers/subscription.d.ts.map +1 -1
- package/dist/document-models/subscription-instance/v1/src/reducers/subscription.js +193 -35
- package/dist/editors/resource-instance-editor/editor.d.ts.map +1 -1
- package/dist/editors/resource-instance-editor/editor.js +13 -3
- package/dist/editors/service-offering-editor/components/ResourceTemplateSelector.d.ts.map +1 -1
- package/dist/editors/service-offering-editor/components/ResourceTemplateSelector.js +4 -2
- package/dist/editors/service-offering-editor/components/ServiceCatalog.d.ts.map +1 -1
- package/dist/editors/service-offering-editor/components/ServiceCatalog.js +189 -32
- package/dist/editors/service-offering-editor/components/TheMatrix.d.ts +1 -1
- package/dist/editors/service-offering-editor/components/TheMatrix.d.ts.map +1 -1
- package/dist/editors/service-offering-editor/components/TheMatrix.js +295 -140
- package/dist/editors/service-offering-editor/components/TierDefinition.d.ts.map +1 -1
- package/dist/editors/service-offering-editor/components/TierDefinition.js +2 -0
- package/dist/editors/service-offering-editor/components/TierPricingOptionsPanel.js +3 -3
- package/dist/editors/service-offering-editor/components/pricing-utils.d.ts.map +1 -1
- package/dist/editors/service-offering-editor/components/pricing-utils.js +26 -7
- package/dist/editors/subscription-instance-editor/components/BillingPanel.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/BillingPanel.js +4 -4
- package/dist/editors/subscription-instance-editor/components/CustomerInfo.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/CustomerInfo.js +3 -2
- package/dist/editors/subscription-instance-editor/components/ImportServiceConfigButton.d.ts +3 -0
- package/dist/editors/subscription-instance-editor/components/ImportServiceConfigButton.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/ImportServiceConfigButton.js +12 -0
- package/dist/editors/subscription-instance-editor/components/MetricActions.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/MetricActions.js +4 -2
- package/dist/editors/subscription-instance-editor/components/MockDataButton.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/MockDataButton.js +214 -2
- package/dist/editors/subscription-instance-editor/components/OperatorNotes.js +1 -1
- package/dist/editors/subscription-instance-editor/components/ServicesPanel.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/ServicesPanel.js +9 -20
- package/dist/editors/subscription-instance-editor/components/SubscriptionActions.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/SubscriptionActions.js +8 -9
- package/dist/editors/subscription-instance-editor/components/SubscriptionHeader.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/SubscriptionHeader.js +1 -1
- package/dist/editors/subscription-instance-editor/components/billing-utils.d.ts +14 -6
- package/dist/editors/subscription-instance-editor/components/billing-utils.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/billing-utils.js +19 -23
- package/dist/editors/subscription-instance-editor/components/mapOfferingToSubscription.d.ts +16 -2
- package/dist/editors/subscription-instance-editor/components/mapOfferingToSubscription.d.ts.map +1 -1
- package/dist/editors/subscription-instance-editor/components/mapOfferingToSubscription.js +155 -6
- package/dist/powerhouse.manifest.json +29 -3
- package/dist/style.css +14 -0
- package/dist/subgraphs/resources-services/resolvers.d.ts +1 -1
- package/dist/subgraphs/resources-services/resolvers.d.ts.map +1 -1
- package/dist/subgraphs/resources-services/resolvers.js +273 -158
- package/dist/subgraphs/resources-services/schema.d.ts.map +1 -1
- package/dist/subgraphs/resources-services/schema.js +107 -41
- package/package.json +22 -18
|
@@ -2,8 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState, useMemo, useEffect, useRef, useCallback } from "react";
|
|
3
3
|
import { generateId } from "document-model/core";
|
|
4
4
|
import { usePHToast, } from "@powerhousedao/reactor-browser";
|
|
5
|
+
import {} from "@powerhousedao/service-offering/document-models/service-offering";
|
|
5
6
|
import { BILLING_CYCLE_SHORT_LABELS, BILLING_CYCLE_LABELS, BILLING_CYCLE_MONTHS, RECURRING_BILLING_CYCLES, formatPrice, detectMajorityCycle, } from "./pricing-utils.js";
|
|
6
7
|
import { addServiceLevel, updateServiceLevel, addUsageLimit, updateUsageLimit, removeUsageLimit, addService, updateService, } from "../../../document-models/service-offering/v1/gen/creators.js";
|
|
8
|
+
import { getUserSelectionPriceBreakdown, } from "../../../document-models/service-offering/v1/index.js";
|
|
7
9
|
import { InfoIcon } from "./InfoIcon.js";
|
|
8
10
|
import { ConfirmDialog } from "./ConfirmDialog.js";
|
|
9
11
|
const SERVICE_LEVELS = [
|
|
@@ -237,44 +239,16 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
237
239
|
const addonGroups = useMemo(() => {
|
|
238
240
|
return optionGroups.filter((g) => g.isAddOn);
|
|
239
241
|
}, [optionGroups]);
|
|
242
|
+
// Precompute price breakdowns for all tiers using the centralized utility
|
|
240
243
|
const tierBreakdowns = useMemo(() => {
|
|
241
|
-
const
|
|
242
|
-
return tiers.map((tier) => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
monthlyBase += amount;
|
|
250
|
-
ogBreakdowns.push({
|
|
251
|
-
optionGroupId: group.id,
|
|
252
|
-
cycleAmount: amount * months,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
let addOnTotal = 0;
|
|
256
|
-
const addOnBreakdowns = [];
|
|
257
|
-
for (const group of optionGroups.filter((g) => g.isAddOn)) {
|
|
258
|
-
if (!enabledOptionalGroups.has(group.id))
|
|
259
|
-
continue;
|
|
260
|
-
const tp = group.tierDependentPricing?.find((p) => p.tierId === tier.id);
|
|
261
|
-
const amount = tp?.amount ?? group.standalonePricing?.amount ?? 0;
|
|
262
|
-
addOnTotal += amount * months;
|
|
263
|
-
addOnBreakdowns.push({
|
|
264
|
-
optionGroupId: group.id,
|
|
265
|
-
cycleAmount: amount * months,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
const tierCycleTotal = monthlyBase * months;
|
|
269
|
-
return {
|
|
270
|
-
tierMonthlyBase: monthlyBase,
|
|
271
|
-
tierCycleTotal,
|
|
272
|
-
tierCurrency: tier.pricing?.currency || "USD",
|
|
273
|
-
addOnBreakdowns,
|
|
274
|
-
optionGroupBreakdowns: ogBreakdowns,
|
|
275
|
-
totals: { grandRecurringTotal: tierCycleTotal + addOnTotal },
|
|
276
|
-
};
|
|
277
|
-
});
|
|
244
|
+
const addonIds = [...enabledOptionalGroups];
|
|
245
|
+
return tiers.map((tier) => getUserSelectionPriceBreakdown(state, {
|
|
246
|
+
tierId: tier.id,
|
|
247
|
+
billingCycle: activeBillingCycle,
|
|
248
|
+
optionGroupIds: addonIds,
|
|
249
|
+
groupBillingCycleOverrides: groupBillingCycles,
|
|
250
|
+
addonBillingCycleOverrides: addonBillingCycles,
|
|
251
|
+
}));
|
|
278
252
|
}, [
|
|
279
253
|
tiers,
|
|
280
254
|
optionGroups,
|
|
@@ -292,10 +266,12 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
292
266
|
const getServiceLevelForTier = (serviceId, tier) => {
|
|
293
267
|
return tier.serviceLevels.find((sl) => sl.serviceId === serviceId);
|
|
294
268
|
};
|
|
295
|
-
const getUniqueMetricsForService = (
|
|
269
|
+
const getUniqueMetricsForService = (serviceId) => {
|
|
296
270
|
const metricsSet = new Set();
|
|
297
271
|
tiers.forEach((tier) => {
|
|
298
|
-
tier.usageLimits
|
|
272
|
+
tier.usageLimits
|
|
273
|
+
.filter((ul) => ul.serviceId === serviceId)
|
|
274
|
+
.forEach((ul) => metricsSet.add(ul.metric));
|
|
299
275
|
});
|
|
300
276
|
return Array.from(metricsSet);
|
|
301
277
|
};
|
|
@@ -309,8 +285,8 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
309
285
|
return !isIncludedAnywhere;
|
|
310
286
|
});
|
|
311
287
|
}, [services, tiers]);
|
|
312
|
-
const getUsageLimitForMetric = (
|
|
313
|
-
return tier.usageLimits.find((ul) => ul.
|
|
288
|
+
const getUsageLimitForMetric = (serviceId, metric, tier) => {
|
|
289
|
+
return tier.usageLimits.find((ul) => ul.serviceId === serviceId && ul.metric === metric);
|
|
314
290
|
};
|
|
315
291
|
// Derive tier display pricing from precomputed breakdown
|
|
316
292
|
const getTierDisplayPrice = (tierIdx) => {
|
|
@@ -334,11 +310,11 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
334
310
|
discountLabel: savingsPercent > 0 ? `SAVE ${savingsPercent}%` : "",
|
|
335
311
|
};
|
|
336
312
|
};
|
|
337
|
-
const handleSetServiceLevel = (serviceId, tierId, level, existingLevelId,
|
|
313
|
+
const handleSetServiceLevel = (serviceId, tierId, level, existingLevelId, optionGroupId) => {
|
|
338
314
|
if (existingLevelId) {
|
|
339
315
|
dispatch(updateServiceLevel({
|
|
340
316
|
tierId,
|
|
341
|
-
|
|
317
|
+
serviceLevelId: existingLevelId,
|
|
342
318
|
level,
|
|
343
319
|
lastModified: new Date().toISOString(),
|
|
344
320
|
}));
|
|
@@ -346,9 +322,10 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
346
322
|
else {
|
|
347
323
|
dispatch(addServiceLevel({
|
|
348
324
|
tierId,
|
|
349
|
-
|
|
325
|
+
serviceLevelId: generateId(),
|
|
350
326
|
serviceId,
|
|
351
327
|
level,
|
|
328
|
+
optionGroupId,
|
|
352
329
|
lastModified: new Date().toISOString(),
|
|
353
330
|
}));
|
|
354
331
|
}
|
|
@@ -381,12 +358,16 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
381
358
|
: undefined,
|
|
382
359
|
lastModified: now,
|
|
383
360
|
}));
|
|
361
|
+
// Create ServiceLevelBindings for each selected tier
|
|
384
362
|
newServiceSelectedTiers.forEach((tierId) => {
|
|
385
363
|
dispatch(addServiceLevel({
|
|
386
364
|
tierId,
|
|
387
|
-
|
|
365
|
+
serviceLevelId: generateId(),
|
|
388
366
|
serviceId: newServiceId,
|
|
389
367
|
level: "INCLUDED",
|
|
368
|
+
optionGroupId: addServiceModal.groupId !== UNGROUPED_ID
|
|
369
|
+
? addServiceModal.groupId
|
|
370
|
+
: undefined,
|
|
390
371
|
lastModified: now,
|
|
391
372
|
}));
|
|
392
373
|
});
|
|
@@ -431,20 +412,23 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
431
412
|
const existingLevel = tier.serviceLevels.find((sl) => sl.serviceId === editServiceModal.id);
|
|
432
413
|
const shouldBeIncluded = editServiceSelectedTiers.has(tier.id);
|
|
433
414
|
if (shouldBeIncluded && !existingLevel) {
|
|
415
|
+
// Add to tier
|
|
434
416
|
dispatch(addServiceLevel({
|
|
435
417
|
tierId: tier.id,
|
|
436
|
-
|
|
418
|
+
serviceLevelId: generateId(),
|
|
437
419
|
serviceId: editServiceModal.id,
|
|
438
420
|
level: "INCLUDED",
|
|
421
|
+
optionGroupId: editServiceModal.optionGroupId || undefined,
|
|
439
422
|
lastModified: now,
|
|
440
423
|
}));
|
|
441
424
|
}
|
|
442
425
|
else if (shouldBeIncluded &&
|
|
443
426
|
existingLevel &&
|
|
444
427
|
existingLevel.level !== "INCLUDED") {
|
|
428
|
+
// Update to included
|
|
445
429
|
dispatch(updateServiceLevel({
|
|
446
430
|
tierId: tier.id,
|
|
447
|
-
|
|
431
|
+
serviceLevelId: existingLevel.id,
|
|
448
432
|
level: "INCLUDED",
|
|
449
433
|
lastModified: now,
|
|
450
434
|
}));
|
|
@@ -452,9 +436,10 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
452
436
|
else if (!shouldBeIncluded &&
|
|
453
437
|
existingLevel &&
|
|
454
438
|
existingLevel.level === "INCLUDED") {
|
|
439
|
+
// Remove from tier (set to NOT_INCLUDED)
|
|
455
440
|
dispatch(updateServiceLevel({
|
|
456
441
|
tierId: tier.id,
|
|
457
|
-
|
|
442
|
+
serviceLevelId: existingLevel.id,
|
|
458
443
|
level: "NOT_INCLUDED",
|
|
459
444
|
lastModified: now,
|
|
460
445
|
}));
|
|
@@ -494,23 +479,33 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
494
479
|
const service = services.find((s) => s.id === serviceId);
|
|
495
480
|
setMetricResetCycle(service?.isSetupFormation ? "NONE" : "MONTHLY");
|
|
496
481
|
};
|
|
497
|
-
const handleEditMetric = (
|
|
498
|
-
setMetricModal({ serviceId
|
|
482
|
+
const handleEditMetric = (serviceId, metric) => {
|
|
483
|
+
setMetricModal({ serviceId, metric });
|
|
499
484
|
setMetricName(metric);
|
|
485
|
+
// Initialize limits with existing values and track which tiers have this metric
|
|
500
486
|
const existingLimits = {};
|
|
501
487
|
const existingPaidLimits = {};
|
|
502
488
|
const existingOveragePrices = {};
|
|
503
489
|
const enabledTiers = new Set();
|
|
504
490
|
let existingUnitName = "";
|
|
491
|
+
let existingResetCycle = "MONTHLY";
|
|
505
492
|
tiers.forEach((tier) => {
|
|
506
|
-
const usageLimit = tier.usageLimits.find((ul) => ul.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
493
|
+
const usageLimit = tier.usageLimits.find((ul) => ul.serviceId === serviceId && ul.metric === metric);
|
|
494
|
+
// Load value from either limit (numeric) or notes (string)
|
|
495
|
+
existingLimits[tier.id] =
|
|
496
|
+
usageLimit?.freeLimit?.toString() || usageLimit?.notes || "";
|
|
497
|
+
existingPaidLimits[tier.id] = usageLimit?.paidLimit?.toString() || "";
|
|
498
|
+
// Load per-tier overage pricing
|
|
499
|
+
existingOveragePrices[tier.id] = usageLimit?.unitPrice?.toString() || "";
|
|
510
500
|
if (usageLimit) {
|
|
511
501
|
enabledTiers.add(tier.id);
|
|
512
|
-
|
|
513
|
-
|
|
502
|
+
// Get unit name from first tier that has it
|
|
503
|
+
if (!existingUnitName && usageLimit.unitName) {
|
|
504
|
+
existingUnitName = usageLimit.unitName;
|
|
505
|
+
}
|
|
506
|
+
// Get reset cycle from first tier that has it
|
|
507
|
+
if (usageLimit.resetCycle) {
|
|
508
|
+
existingResetCycle = usageLimit.resetCycle;
|
|
514
509
|
}
|
|
515
510
|
}
|
|
516
511
|
});
|
|
@@ -519,7 +514,7 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
519
514
|
setMetricEnabledTiers(enabledTiers);
|
|
520
515
|
setMetricOveragePrices(existingOveragePrices);
|
|
521
516
|
setMetricUnitName(existingUnitName);
|
|
522
|
-
setMetricResetCycle(
|
|
517
|
+
setMetricResetCycle(existingResetCycle);
|
|
523
518
|
};
|
|
524
519
|
const handleRemoveMetric = (serviceId, metric) => {
|
|
525
520
|
setPendingRemoveMetric({ serviceId, metric });
|
|
@@ -527,13 +522,14 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
527
522
|
const confirmRemoveMetric = () => {
|
|
528
523
|
if (!pendingRemoveMetric)
|
|
529
524
|
return;
|
|
530
|
-
const { metric } = pendingRemoveMetric;
|
|
525
|
+
const { serviceId, metric } = pendingRemoveMetric;
|
|
526
|
+
// Remove this metric from all tiers
|
|
531
527
|
tiers.forEach((tier) => {
|
|
532
|
-
const usageLimit = tier.usageLimits.find((ul) => ul.
|
|
528
|
+
const usageLimit = tier.usageLimits.find((ul) => ul.serviceId === serviceId && ul.metric === metric);
|
|
533
529
|
if (usageLimit) {
|
|
534
530
|
dispatch(removeUsageLimit({
|
|
535
531
|
tierId: tier.id,
|
|
536
|
-
|
|
532
|
+
limitId: usageLimit.id,
|
|
537
533
|
lastModified: new Date().toISOString(),
|
|
538
534
|
}));
|
|
539
535
|
}
|
|
@@ -573,40 +569,67 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
573
569
|
const handleSaveMetric = () => {
|
|
574
570
|
if (!metricModal || !metricName.trim())
|
|
575
571
|
return;
|
|
576
|
-
const { metric: originalMetric } = metricModal;
|
|
572
|
+
const { serviceId, metric: originalMetric } = metricModal;
|
|
577
573
|
const now = new Date().toISOString();
|
|
578
574
|
tiers.forEach((tier) => {
|
|
579
575
|
const isEnabled = metricEnabledTiers.has(tier.id);
|
|
580
576
|
const limitValue = metricLimits[tier.id];
|
|
581
577
|
const existingLimit = originalMetric
|
|
582
|
-
? tier.usageLimits.find((ul) => ul.
|
|
578
|
+
? tier.usageLimits.find((ul) => ul.serviceId === serviceId && ul.metric === originalMetric)
|
|
579
|
+
: null;
|
|
580
|
+
// Check if value is numeric or string
|
|
581
|
+
const parsedLimit = limitValue ? parseInt(limitValue, 10) : null;
|
|
582
|
+
const isNumeric = parsedLimit !== null && !isNaN(parsedLimit);
|
|
583
|
+
// Parse paid limit
|
|
584
|
+
const paidLimitValue = metricPaidLimits[tier.id];
|
|
585
|
+
const parsedPaidLimit = paidLimitValue
|
|
586
|
+
? parseInt(paidLimitValue, 10)
|
|
583
587
|
: null;
|
|
584
|
-
const
|
|
585
|
-
|
|
588
|
+
const isPaidNumeric = parsedPaidLimit !== null && !isNaN(parsedPaidLimit);
|
|
589
|
+
// Get per-tier overage pricing
|
|
590
|
+
const tierOveragePrice = metricOveragePrices[tier.id];
|
|
591
|
+
const parsedOveragePrice = tierOveragePrice
|
|
592
|
+
? parseFloat(tierOveragePrice)
|
|
593
|
+
: null;
|
|
594
|
+
const hasOveragePricing = parsedOveragePrice !== null && !isNaN(parsedOveragePrice);
|
|
586
595
|
if (existingLimit && !isEnabled) {
|
|
596
|
+
// Remove limit - tier was disabled
|
|
587
597
|
dispatch(removeUsageLimit({
|
|
588
598
|
tierId: tier.id,
|
|
589
|
-
|
|
599
|
+
limitId: existingLimit.id,
|
|
590
600
|
lastModified: now,
|
|
591
601
|
}));
|
|
592
602
|
}
|
|
593
603
|
else if (existingLimit && isEnabled) {
|
|
604
|
+
// Update existing limit - use limit for numeric values, notes for strings
|
|
594
605
|
dispatch(updateUsageLimit({
|
|
595
606
|
tierId: tier.id,
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
607
|
+
limitId: existingLimit.id,
|
|
608
|
+
metric: metricName.trim(),
|
|
609
|
+
unitName: metricUnitName.trim() || undefined,
|
|
610
|
+
freeLimit: isNumeric ? parsedLimit : null,
|
|
611
|
+
paidLimit: isPaidNumeric ? parsedPaidLimit : null,
|
|
612
|
+
notes: !isNumeric && limitValue ? limitValue.trim() : null,
|
|
613
|
+
resetCycle: metricResetCycle,
|
|
614
|
+
unitPrice: hasOveragePricing ? parsedOveragePrice : null,
|
|
615
|
+
unitPriceCurrency: hasOveragePricing ? "USD" : undefined,
|
|
600
616
|
lastModified: now,
|
|
601
617
|
}));
|
|
602
618
|
}
|
|
603
619
|
else if (!existingLimit && isEnabled) {
|
|
620
|
+
// Add new limit - use limit for numeric values, notes for strings
|
|
604
621
|
dispatch(addUsageLimit({
|
|
605
622
|
tierId: tier.id,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
623
|
+
limitId: generateId(),
|
|
624
|
+
serviceId,
|
|
625
|
+
metric: metricName.trim(),
|
|
626
|
+
unitName: metricUnitName.trim() || undefined,
|
|
627
|
+
freeLimit: isNumeric ? parsedLimit : null,
|
|
628
|
+
paidLimit: isPaidNumeric ? parsedPaidLimit : null,
|
|
629
|
+
notes: !isNumeric && limitValue ? limitValue.trim() : null,
|
|
630
|
+
resetCycle: metricResetCycle,
|
|
631
|
+
unitPrice: hasOveragePricing ? parsedOveragePrice : undefined,
|
|
632
|
+
unitPriceCurrency: hasOveragePricing ? "USD" : undefined,
|
|
610
633
|
lastModified: now,
|
|
611
634
|
}));
|
|
612
635
|
}
|
|
@@ -625,6 +648,12 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
625
648
|
return { label: "—", color: "#cbd5e1" };
|
|
626
649
|
const level = serviceLevel.level;
|
|
627
650
|
const config = SERVICE_LEVELS.find((l) => l.value === level);
|
|
651
|
+
if (level === "CUSTOM" && serviceLevel.customValue) {
|
|
652
|
+
return {
|
|
653
|
+
label: serviceLevel.customValue,
|
|
654
|
+
color: config?.color || "#d97706",
|
|
655
|
+
};
|
|
656
|
+
}
|
|
628
657
|
return {
|
|
629
658
|
label: config?.shortLabel || level,
|
|
630
659
|
color: config?.color || "#475569",
|
|
@@ -715,9 +744,9 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
715
744
|
costType: null,
|
|
716
745
|
currency: null,
|
|
717
746
|
price: null,
|
|
718
|
-
pricingMode:
|
|
747
|
+
pricingMode: null,
|
|
719
748
|
standalonePricing: null,
|
|
720
|
-
tierDependentPricing:
|
|
749
|
+
tierDependentPricing: null,
|
|
721
750
|
discountMode: null,
|
|
722
751
|
}, services: ungroupedSetupServices, tiers: tiers, isSetupFormation: true, isOptional: false, isEnabled: true, onToggle: () => { }, getServiceLevelForTier: getServiceLevelForTier, getUniqueMetricsForService: getUniqueMetricsForService, getUsageLimitForMetric: getUsageLimitForMetric, getLevelDisplay: getLevelDisplay, selectedCell: selectedCell, setSelectedCell: setSelectedCell, handleSetServiceLevel: handleSetServiceLevel, dispatch: dispatch, selectedTierIdx: selectedTierIdx, onAddMetric: handleAddMetric, onEditMetric: handleEditMetric, onRemoveMetric: handleRemoveMetric, onEditService: openEditServiceModal, onReorderService: handleReorderService, activeBillingCycle: activeBillingCycle }, "ungrouped-setup")), (regularGroups.length > 0 ||
|
|
723
752
|
ungroupedRegularServices.length > 0) && (_jsx("tr", { children: _jsxs("td", { colSpan: tiers.length + 1, className: "bg-slate-50 py-3 px-4 text-xs font-semibold text-slate-700 border-b border-slate-200 flex items-center gap-2", children: [_jsx("span", { className: "flex items-center justify-center w-5 h-5 text-slate-500", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", className: "w-full h-full", children: _jsx("path", { d: "M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" }) }) }), "Recurring Services"] }) })), regularGroups.map((group) => (_jsx(ServiceGroupSection, { group: group, services: groupedServices.get(group.id) || [], tiers: tiers, isSetupFormation: false, isOptional: false, isEnabled: true, onToggle: () => { }, getServiceLevelForTier: getServiceLevelForTier, getUniqueMetricsForService: getUniqueMetricsForService, getUsageLimitForMetric: getUsageLimitForMetric, getLevelDisplay: getLevelDisplay, selectedCell: selectedCell, setSelectedCell: setSelectedCell, handleSetServiceLevel: handleSetServiceLevel, onAddService: openAddServiceModal, selectedTierIdx: selectedTierIdx, dispatch: dispatch, onAddMetric: handleAddMetric, onEditMetric: handleEditMetric, onRemoveMetric: handleRemoveMetric, onEditService: openEditServiceModal, onReorderService: handleReorderService, activeBillingCycle: activeBillingCycle, groupActiveCycle: groupBillingCycles[group.id], onGroupCycleChange: (cycle) => handleGroupCycleChange(group.id, cycle), groupBreakdown: tierBreakdowns[selectedTierIdx]?.optionGroupBreakdowns.find((b) => b.optionGroupId === group.id) }, group.id))), ungroupedRegularServices.length > 0 && (_jsx(ServiceGroupSection, { group: {
|
|
@@ -731,19 +760,23 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
731
760
|
costType: null,
|
|
732
761
|
currency: null,
|
|
733
762
|
price: null,
|
|
734
|
-
pricingMode:
|
|
763
|
+
pricingMode: null,
|
|
735
764
|
standalonePricing: null,
|
|
736
|
-
tierDependentPricing:
|
|
765
|
+
tierDependentPricing: null,
|
|
737
766
|
discountMode: null,
|
|
738
767
|
}, services: ungroupedRegularServices, tiers: tiers, isSetupFormation: false, isOptional: false, isEnabled: true, onToggle: () => { }, getServiceLevelForTier: getServiceLevelForTier, getUniqueMetricsForService: getUniqueMetricsForService, getUsageLimitForMetric: getUsageLimitForMetric, getLevelDisplay: getLevelDisplay, selectedCell: selectedCell, setSelectedCell: setSelectedCell, handleSetServiceLevel: handleSetServiceLevel, dispatch: dispatch, selectedTierIdx: selectedTierIdx, onAddMetric: handleAddMetric, onEditMetric: handleEditMetric, onRemoveMetric: handleRemoveMetric, onEditService: openEditServiceModal, onReorderService: handleReorderService, activeBillingCycle: activeBillingCycle }, "ungrouped-regular")), _jsxs("tr", { className: "bg-slate-100 [&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-300 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-slate-100", children: [_jsx("td", { children: "SUBTOTAL" }), tiers.map((tier, idx) => {
|
|
739
768
|
if (tier.isCustomPricing) {
|
|
740
769
|
return (_jsx("td", { style: { textAlign: "center" }, children: "Custom" }, tier.id));
|
|
741
770
|
}
|
|
742
771
|
const groupSum = tierBreakdowns[idx].tierMonthlyBase;
|
|
743
|
-
const tierPrice = tier.pricing
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
|
|
772
|
+
const tierPrice = tier.pricing.amount ?? 0;
|
|
773
|
+
const isCalculated = tier.pricingMode === "CALCULATED";
|
|
774
|
+
const currency = tier.pricing.currency || "USD";
|
|
775
|
+
const isOver = !isCalculated && tierPrice > 0 && groupSum > tierPrice;
|
|
776
|
+
return (_jsx("td", { style: { textAlign: "center" }, children: _jsxs("div", { className: "flex flex-col items-center gap-0.5", children: [_jsx("span", { className: "font-semibold", children: formatPrice(isCalculated ? groupSum : tierPrice, currency) }), isCalculated && (_jsx("span", { className: "inline-block ml-1 px-1 text-[0.5rem] font-semibold text-emerald-700 bg-emerald-100 rounded-md align-middle uppercase", title: "Calculated from service groups", style: {
|
|
777
|
+
fontFamily: "'DM Mono', 'SF Mono', monospace",
|
|
778
|
+
}, children: "calc" })), !isCalculated &&
|
|
779
|
+
groupSum > 0 &&
|
|
747
780
|
tierPrice > 0 &&
|
|
748
781
|
groupSum !== tierPrice && (_jsx("span", { className: `text-[0.5625rem] py-px px-1.5 rounded-md ${isOver ? "text-rose-700 bg-rose-100 font-semibold" : "text-slate-500 bg-slate-100"}`, style: {
|
|
749
782
|
fontFamily: "'DM Mono', 'SF Mono', monospace",
|
|
@@ -776,11 +809,69 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
776
809
|
}, children: idx === selectedTierIdx ? (tier.isCustomPricing ? ("Custom") : (_jsxs(_Fragment, { children: [formatPrice(discountedTotal, breakdown.tierCurrency), savingsPct > 0 && (_jsxs("span", { className: "inline-block ml-1.5 py-px px-1.5 text-[0.5625rem] font-semibold text-emerald-700 bg-emerald-100 rounded-md align-middle", style: {
|
|
777
810
|
fontFamily: "'DM Mono', 'SF Mono', monospace",
|
|
778
811
|
}, children: ["SAVE ", savingsPct, "%"] }))] }))) : null }, tier.id));
|
|
779
|
-
})] })) : (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
812
|
+
})] })) : (
|
|
813
|
+
/* Custom billing mode: itemized per-group rows from breakdown */
|
|
814
|
+
tierBreakdowns[selectedTierIdx]?.optionGroupBreakdowns.map((ogb) => (_jsxs("tr", { className: "bg-violet-100 [&>td]:py-3.5 [&>td]:px-4 [&>td]:font-bold [&>td]:text-violet-900 [&>td]:border-t-2 [&>td]:border-violet-300 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-violet-100", children: [_jsxs("td", { children: [ogb.optionGroupName, _jsxs("span", { className: "font-normal text-[0.6875rem] text-slate-400 ml-1", children: ["/", BILLING_CYCLE_SHORT_LABELS[ogb.effectiveBillingCycle].toLowerCase()] })] }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx
|
|
815
|
+
? "text-white relative"
|
|
816
|
+
: "", style: {
|
|
817
|
+
textAlign: "center",
|
|
818
|
+
...(idx === selectedTierIdx
|
|
819
|
+
? {
|
|
820
|
+
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)",
|
|
821
|
+
boxShadow: "inset 0 1px 0 rgba(255, 255, 255, 0.15)",
|
|
822
|
+
}
|
|
823
|
+
: {}),
|
|
824
|
+
}, children: idx === selectedTierIdx ? (tier.isCustomPricing ? ("Custom") : ogb.monthlyBase > 0 ? (_jsxs(_Fragment, { children: [formatPrice(ogb.recurringAmount, ogb.currency), ogb.discount &&
|
|
825
|
+
ogb.discount.discountValue > 0 && (_jsxs("span", { className: "inline-block ml-1.5 py-px px-1.5 text-[0.5625rem] font-semibold text-emerald-700 bg-emerald-100 rounded-md align-middle", style: {
|
|
826
|
+
fontFamily: "'DM Mono', 'SF Mono', monospace",
|
|
827
|
+
}, children: ["SAVE", " ", Math.round(ogb.discount.discountType ===
|
|
828
|
+
"PERCENTAGE"
|
|
829
|
+
? ogb.discount.discountValue
|
|
830
|
+
: ogb.cycleAmount > 0
|
|
831
|
+
? ((ogb.cycleAmount -
|
|
832
|
+
ogb.recurringAmount) /
|
|
833
|
+
ogb.cycleAmount) *
|
|
834
|
+
100
|
|
835
|
+
: 0), "%"] }))] })) : ("—")) : null }, tier.id)))] }, `group-${ogb.optionGroupId}`)))), tierBreakdowns[selectedTierIdx]?.addOnBreakdowns
|
|
836
|
+
.filter((ab) => ab.monthlyBase > 0)
|
|
837
|
+
.map((ab) => (_jsxs("tr", { className: "bg-violet-50 [&>td]:border-t [&>td]:border-dashed [&>td]:border-violet-200 [&>td]:font-semibold [&>td]:text-[0.8125rem] [&>td]:text-violet-700 [&>td]:py-2 [&>td]:px-4 [&>td:first-child]:bg-violet-50", children: [_jsxs("td", { children: ["+ ", ab.optionGroupName, _jsxs("span", { className: "font-normal text-[0.6875rem] text-slate-400 ml-1", children: ["/", BILLING_CYCLE_SHORT_LABELS[ab.selectedBillingCycle].toLowerCase()] })] }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx ? "text-white relative" : "", style: {
|
|
838
|
+
textAlign: "center",
|
|
839
|
+
...(idx === selectedTierIdx
|
|
840
|
+
? {
|
|
841
|
+
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)",
|
|
842
|
+
boxShadow: "inset 0 1px 0 rgba(255, 255, 255, 0.15)",
|
|
843
|
+
}
|
|
844
|
+
: {}),
|
|
845
|
+
}, children: idx === selectedTierIdx ? (_jsxs(_Fragment, { children: ["+", formatPrice(ab.recurringAmount, ab.currency), ab.discount && ab.discount.discountValue > 0 && (_jsxs("span", { className: "inline-block ml-1.5 py-px px-1.5 text-[0.5625rem] font-semibold text-emerald-700 bg-emerald-100 rounded-md align-middle", style: {
|
|
846
|
+
fontFamily: "'DM Mono', 'SF Mono', monospace",
|
|
847
|
+
}, children: ["SAVE", " ", Math.round(ab.discount.discountType === "PERCENTAGE"
|
|
848
|
+
? ab.discount.discountValue
|
|
849
|
+
: ab.cycleAmount > 0
|
|
850
|
+
? ((ab.cycleAmount -
|
|
851
|
+
ab.recurringAmount) /
|
|
852
|
+
ab.cycleAmount) *
|
|
853
|
+
100
|
|
854
|
+
: 0), "%"] }))] })) : null }, tier.id)))] }, `addon-recurring-${ab.optionGroupId}`))), tierBreakdowns[selectedTierIdx]?.addOnBreakdowns
|
|
855
|
+
.filter((ab) => ab.setupCost !== null && ab.setupCost > 0)
|
|
856
|
+
.map((ab) => (_jsxs("tr", { className: "bg-violet-50 [&>td]:border-t [&>td]:border-dashed [&>td]:border-violet-200 [&>td]:font-semibold [&>td]:text-[0.8125rem] [&>td]:text-violet-700 [&>td]:py-2 [&>td]:px-4 [&>td:first-child]:bg-violet-50", children: [_jsxs("td", { children: ["+ ", ab.optionGroupName, " ", _jsx("span", { className: "font-normal text-[0.6875rem] text-slate-400 ml-1", children: "(one-time setup)" })] }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx ? "text-white relative" : "", style: {
|
|
857
|
+
textAlign: "center",
|
|
858
|
+
...(idx === selectedTierIdx
|
|
859
|
+
? {
|
|
860
|
+
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)",
|
|
861
|
+
boxShadow: "inset 0 1px 0 rgba(255, 255, 255, 0.15)",
|
|
862
|
+
}
|
|
863
|
+
: {}),
|
|
864
|
+
}, children: idx === selectedTierIdx
|
|
865
|
+
? `${formatPrice(ab.setupCost, ab.setupCostCurrency || "USD")} one-time`
|
|
866
|
+
: null }, tier.id)))] }, `addon-setup-${ab.optionGroupId}`))), (() => {
|
|
867
|
+
const setupBds = tierBreakdowns[selectedTierIdx]?.setupGroupBreakdowns ?? [];
|
|
868
|
+
const totalSetupBase = setupBds.reduce((sum, s) => sum +
|
|
869
|
+
(s.setupCostDiscount?.originalAmount ?? s.setupCost ?? 0), 0);
|
|
870
|
+
const totalSetupEffective = setupBds.reduce((sum, s) => sum + (s.setupCost ?? 0), 0);
|
|
871
|
+
if (totalSetupBase === 0)
|
|
872
|
+
return null;
|
|
873
|
+
const hasDiscount = totalSetupEffective !== totalSetupBase;
|
|
874
|
+
return (_jsxs("tr", { className: "bg-violet-50 [&>td]:border-t [&>td]:border-dashed [&>td]:border-violet-200 [&>td]:font-semibold [&>td]:text-[0.8125rem] [&>td]:text-violet-700 [&>td]:py-2 [&>td]:px-4 [&>td:first-child]:bg-violet-50", children: [_jsx("td", { children: "+ Setup & Formation Fees" }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx ? "text-white relative" : "", style: {
|
|
784
875
|
textAlign: "center",
|
|
785
876
|
...(idx === selectedTierIdx
|
|
786
877
|
? {
|
|
@@ -789,23 +880,11 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
789
880
|
}
|
|
790
881
|
: {}),
|
|
791
882
|
}, children: idx === selectedTierIdx
|
|
792
|
-
?
|
|
793
|
-
? "
|
|
794
|
-
:
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
: null }, tier.id)))] }, `group-${ogb.optionGroupId}`));
|
|
798
|
-
})), tierBreakdowns[selectedTierIdx]?.addOnBreakdowns
|
|
799
|
-
.filter((ab) => ab.cycleAmount > 0)
|
|
800
|
-
.map((ab, abIdx) => (_jsxs("tr", { className: "bg-violet-50 [&>td]:border-t [&>td]:border-dashed [&>td]:border-violet-200 [&>td]:font-semibold [&>td]:text-[0.8125rem] [&>td]:text-violet-700 [&>td]:py-2 [&>td]:px-4 [&>td:first-child]:bg-violet-50", children: [_jsx("td", { children: "+ Add-on" }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx ? "text-white relative" : "", style: {
|
|
801
|
-
textAlign: "center",
|
|
802
|
-
...(idx === selectedTierIdx
|
|
803
|
-
? {
|
|
804
|
-
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)",
|
|
805
|
-
boxShadow: "inset 0 1px 0 rgba(255, 255, 255, 0.15)",
|
|
806
|
-
}
|
|
807
|
-
: {}),
|
|
808
|
-
}, children: idx === selectedTierIdx ? (_jsxs(_Fragment, { children: ["+", formatPrice(ab.cycleAmount)] })) : null }, tier.id)))] }, `addon-recurring-${abIdx}`)))] }) }) })] }), selectedCell && (_jsx(ServiceLevelDetailPanel, { serviceId: selectedCell.serviceId, tierId: selectedCell.tierId, services: services, tiers: tiers, optionGroups: optionGroups, dispatch: dispatch, onClose: () => setSelectedCell(null) })), addServiceModal && (_jsx("div", { className: "fixed inset-0 bg-slate-900/75 backdrop-blur-sm flex items-center justify-center z-[100]", style: { animation: "modal-backdrop 0.2s ease-out" }, children: _jsxs("div", { className: "bg-white rounded-xl p-6 max-h-[85vh] overflow-y-auto", style: {
|
|
883
|
+
? hasDiscount
|
|
884
|
+
? `${formatPrice(totalSetupEffective, "USD")} one-time`
|
|
885
|
+
: `${formatPrice(totalSetupBase, "USD")} one-time`
|
|
886
|
+
: null }, tier.id)))] }));
|
|
887
|
+
})()] }) }) })] }), selectedCell && (_jsx(ServiceLevelDetailPanel, { serviceId: selectedCell.serviceId, tierId: selectedCell.tierId, services: services, tiers: tiers, optionGroups: optionGroups, dispatch: dispatch, onClose: () => setSelectedCell(null) })), addServiceModal && (_jsx("div", { className: "fixed inset-0 bg-slate-900/75 backdrop-blur-sm flex items-center justify-center z-[100]", style: { animation: "modal-backdrop 0.2s ease-out" }, children: _jsxs("div", { className: "bg-white rounded-xl p-6 max-h-[85vh] overflow-y-auto", style: {
|
|
809
888
|
width: "min(32rem, calc(100vw - 2rem))",
|
|
810
889
|
maxWidth: "32rem",
|
|
811
890
|
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(0, 0, 0, 0.08)",
|
|
@@ -821,7 +900,7 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
821
900
|
newSet.delete(tier.id);
|
|
822
901
|
}
|
|
823
902
|
setNewServiceSelectedTiers(newSet);
|
|
824
|
-
}, className: "relative w-5 h-5 shrink-0 appearance-none bg-white border-2 border-slate-400 rounded-md cursor-pointer transition-all duration-150 checked:bg-violet-600 checked:border-violet-600" }), _jsx("span", { className: "flex-1 text-sm font-semibold text-slate-800 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: tier.name }), tier.pricing
|
|
903
|
+
}, className: "relative w-5 h-5 shrink-0 appearance-none bg-white border-2 border-slate-400 rounded-md cursor-pointer transition-all duration-150 checked:bg-violet-600 checked:border-violet-600" }), _jsx("span", { className: "flex-1 text-sm font-semibold text-slate-800 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: tier.name }), tier.pricing.amount !== null && (_jsxs("span", { className: "text-xs font-semibold text-slate-500 whitespace-nowrap shrink-0", children: ["$", tier.pricing.amount, "/mo"] }))] }, tier.id));
|
|
825
904
|
}) }), newServiceSelectedTiers.size === 0 && (_jsx("p", { className: "text-[0.8125rem] text-slate-500 mt-3 italic", children: "Select at least one tier to include this service" }))] })), _jsxs("p", { className: "text-[0.8125rem] text-slate-600 mb-5 leading-6", children: ["This service will be added to", " ", _jsx("strong", { children: addServiceModal.groupId !== UNGROUPED_ID
|
|
826
905
|
? optionGroups.find((g) => g.id === addServiceModal.groupId)
|
|
827
906
|
?.name || "Unknown Group"
|
|
@@ -848,7 +927,7 @@ export function TheMatrix({ document, dispatch }) {
|
|
|
848
927
|
newSet.delete(tier.id);
|
|
849
928
|
}
|
|
850
929
|
setEditServiceSelectedTiers(newSet);
|
|
851
|
-
}, className: "relative w-5 h-5 shrink-0 appearance-none bg-white border-2 border-slate-400 rounded-md cursor-pointer transition-all duration-150 checked:bg-violet-600 checked:border-violet-600" }), _jsx("span", { className: "flex-1 text-sm font-semibold text-slate-800 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: tier.name }), tier.pricing
|
|
930
|
+
}, className: "relative w-5 h-5 shrink-0 appearance-none bg-white border-2 border-slate-400 rounded-md cursor-pointer transition-all duration-150 checked:bg-violet-600 checked:border-violet-600" }), _jsx("span", { className: "flex-1 text-sm font-semibold text-slate-800 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: tier.name }), tier.pricing.amount !== null && (_jsxs("span", { className: "text-xs font-semibold text-slate-500 whitespace-nowrap shrink-0", children: ["$", tier.pricing.amount, "/mo"] }))] }, tier.id));
|
|
852
931
|
}) })] })), _jsxs("div", { className: "flex gap-3 justify-end pt-2", children: [_jsx("button", { onClick: () => {
|
|
853
932
|
setEditServiceModal(null);
|
|
854
933
|
setEditServiceName("");
|
|
@@ -995,16 +1074,22 @@ function ServiceGroupSection({ group, services, tiers, isSetupFormation, isOptio
|
|
|
995
1074
|
(() => {
|
|
996
1075
|
if (!groupBreakdown)
|
|
997
1076
|
return null;
|
|
998
|
-
const {
|
|
999
|
-
if (
|
|
1077
|
+
const { monthlyBase, recurringAmount, discount, currency } = groupBreakdown;
|
|
1078
|
+
if (monthlyBase <= 0 && !group.standalonePricing?.setupCost)
|
|
1000
1079
|
return null;
|
|
1080
|
+
const setupCost = group.standalonePricing?.setupCost;
|
|
1001
1081
|
const months = BILLING_CYCLE_MONTHS[effectiveBillingCycle];
|
|
1002
1082
|
const monthlyEq = months > 0
|
|
1003
|
-
? Math.round((
|
|
1004
|
-
:
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1083
|
+
? Math.round((recurringAmount / months) * 100) / 100
|
|
1084
|
+
: recurringAmount;
|
|
1085
|
+
const savingsPct = discount && discount.originalAmount > 0
|
|
1086
|
+
? Math.round(((discount.originalAmount - discount.discountedAmount) /
|
|
1087
|
+
discount.originalAmount) *
|
|
1088
|
+
100)
|
|
1089
|
+
: 0;
|
|
1090
|
+
return (_jsxs("div", { className: "flex items-center gap-3 ml-auto", children: [monthlyBase > 0 && (_jsxs("span", { className: "text-sm font-bold text-slate-800 whitespace-nowrap", children: [formatPrice(effectiveBillingCycle === "MONTHLY"
|
|
1091
|
+
? monthlyBase
|
|
1092
|
+
: monthlyEq, currency), "/mo"] })), effectiveBillingCycle !== "MONTHLY" && monthlyBase > 0 && (_jsxs("span", { className: "text-[0.6875rem] font-medium text-slate-500 whitespace-nowrap", children: ["Billed ", formatPrice(recurringAmount, currency), " ", BILLING_CYCLE_LABELS[effectiveBillingCycle]] })), savingsPct > 0 && (_jsxs("span", { className: "text-[0.6875rem] font-semibold text-emerald-600 whitespace-nowrap", children: ["SAVE ", Math.round(savingsPct), "%"] })), setupCost && setupCost.amount > 0 && (_jsxs("span", { className: "text-[0.6875rem] font-medium text-slate-500 whitespace-nowrap", children: ["+", " ", formatPrice(setupCost.amount, setupCost.currency || "USD"), " ", "Setup"] }))] }));
|
|
1008
1093
|
})()] }) }), _jsx("td", { colSpan: tiers.length, className: headerClass, style: { textAlign: "center" }, children: _jsx("span", { className: `inline-block py-1 px-2.5 rounded-md text-[0.625rem] font-semibold uppercase tracking-[0.04em] ${isSetupFormation || !isOptional
|
|
1009
1094
|
? "bg-emerald-100 text-emerald-700"
|
|
1010
1095
|
: "bg-sky-200 text-sky-700"}`, children: isSetupFormation
|
|
@@ -1021,20 +1106,53 @@ function ServiceGroupSection({ group, services, tiers, isSetupFormation, isOptio
|
|
|
1021
1106
|
return (_jsxs("tr", { className: "bg-slate-50 [&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-200 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-slate-50", children: [_jsx("td", { children: "TOTAL SETUP FEE" }), _jsx("td", { colSpan: tiers.length, style: { textAlign: "center" }, children: "No setup fee configured" })] }));
|
|
1022
1107
|
}
|
|
1023
1108
|
const selectedTier = tiers[selectedTierIdx] ?? null;
|
|
1109
|
+
const tierPricing = selectedTier
|
|
1110
|
+
? group.tierDependentPricing?.find((tp) => tp.tierId === selectedTier.id)
|
|
1111
|
+
: null;
|
|
1112
|
+
const cycleDiscount = tierPricing?.setupCostDiscounts?.find((d) => d.billingCycle === activeBillingCycle);
|
|
1113
|
+
const genericDiscount = tierPricing?.setupCost?.discount;
|
|
1114
|
+
const discount = cycleDiscount?.discountRule ?? genericDiscount;
|
|
1115
|
+
let effectivePrice = basePrice;
|
|
1116
|
+
if (discount && discount.discountValue > 0) {
|
|
1117
|
+
if (discount.discountType === "PERCENTAGE") {
|
|
1118
|
+
effectivePrice = basePrice * (1 - discount.discountValue / 100);
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
effectivePrice = Math.max(0, basePrice - discount.discountValue);
|
|
1122
|
+
}
|
|
1123
|
+
effectivePrice = Math.round(effectivePrice * 100) / 100;
|
|
1124
|
+
}
|
|
1024
1125
|
const curr = group.currency || "USD";
|
|
1025
|
-
|
|
1126
|
+
const hasDiscount = effectivePrice !== basePrice;
|
|
1127
|
+
return (_jsxs("tr", { className: "bg-slate-50 [&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-200 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-slate-50", children: [_jsx("td", { children: "TOTAL SETUP FEE" }), _jsx("td", { colSpan: tiers.length, style: { textAlign: "center" }, children: hasDiscount ? (_jsxs(_Fragment, { children: [_jsx("span", { style: {
|
|
1128
|
+
textDecoration: "line-through",
|
|
1129
|
+
opacity: 0.5,
|
|
1130
|
+
marginRight: 6,
|
|
1131
|
+
}, children: formatPrice(basePrice, curr) }), formatPrice(effectivePrice, curr), " flat fee", discount?.discountType === "PERCENTAGE"
|
|
1132
|
+
? ` (${discount.discountValue}% off)`
|
|
1133
|
+
: ` (${formatPrice(discount?.discountValue ?? 0, curr)} off)`] })) : (`${formatPrice(basePrice, curr)} flat fee (applied to all ${tiers.some((t) => {
|
|
1134
|
+
const tp = group.tierDependentPricing?.find((p) => p.tierId === t.id);
|
|
1135
|
+
const monthlyAmt = tp?.recurringPricing?.find((r) => r.billingCycle === "MONTHLY")?.amount;
|
|
1136
|
+
return !monthlyAmt || monthlyAmt === 0;
|
|
1137
|
+
})
|
|
1138
|
+
? "priced "
|
|
1139
|
+
: ""}tiers)`) })] }));
|
|
1026
1140
|
})(), isOptional &&
|
|
1027
1141
|
(() => {
|
|
1028
|
-
const
|
|
1029
|
-
? (groupBreakdown?.
|
|
1142
|
+
const baseMonthly = isEnabled
|
|
1143
|
+
? (groupBreakdown?.monthlyBase ?? 0)
|
|
1144
|
+
: 0;
|
|
1145
|
+
const adjustedTotal = isEnabled
|
|
1146
|
+
? (groupBreakdown?.recurringAmount ?? 0)
|
|
1147
|
+
: 0;
|
|
1148
|
+
const setupCost = isEnabled
|
|
1149
|
+
? (group.standalonePricing?.setupCost?.amount ?? 0)
|
|
1030
1150
|
: 0;
|
|
1031
1151
|
const billingLabel = `/${BILLING_CYCLE_SHORT_LABELS[effectiveBillingCycle].toLowerCase()}`;
|
|
1032
|
-
const currency = group.currency || "USD";
|
|
1033
|
-
return (_jsxs("tr", { className: `[&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-300 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 ${headerClass}`, children: [_jsx("td", { className: headerClass, children: "SUBTOTAL" }), _jsx("td", { colSpan: tiers.length, style: { textAlign: "center" }, children: isEnabled &&
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
? "Included"
|
|
1037
|
-
: "—" })] }));
|
|
1152
|
+
const currency = groupBreakdown?.currency || group.currency || "USD";
|
|
1153
|
+
return (_jsxs("tr", { className: `[&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-300 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 ${headerClass}`, children: [_jsx("td", { className: headerClass, children: "SUBTOTAL" }), _jsx("td", { colSpan: tiers.length, style: { textAlign: "center" }, children: isEnabled && (baseMonthly > 0 || setupCost > 0) ? (_jsxs(_Fragment, { children: [baseMonthly > 0 &&
|
|
1154
|
+
`+${formatPrice(adjustedTotal, currency)}${billingLabel}`, baseMonthly > 0 && setupCost > 0 && " + ", setupCost > 0 &&
|
|
1155
|
+
`${formatPrice(setupCost, currency)} setup`] })) : isEnabled ? ("Included") : ("—") })] }));
|
|
1038
1156
|
})()] }));
|
|
1039
1157
|
}
|
|
1040
1158
|
function ServiceRowWithMetrics({ service, metrics, tiers, rowClass, getServiceLevelForTier, getUsageLimitForMetric, getLevelDisplay, selectedCell, setSelectedCell, selectedTierIdx, onAddMetric, onEditMetric, onRemoveMetric, onEditService, onReorderService, groupServices, serviceIndex, }) {
|
|
@@ -1095,7 +1213,10 @@ function ServiceRowWithMetrics({ service, metrics, tiers, rowClass, getServiceLe
|
|
|
1095
1213
|
? {
|
|
1096
1214
|
background: "linear-gradient(180deg, rgba(139, 92, 246, 0.06) 0%, rgba(139, 92, 246, 0.12) 100%)",
|
|
1097
1215
|
}
|
|
1098
|
-
: undefined, children: _jsx("div", { className: "inline-flex flex-col border border-slate-200 rounded-[10px] overflow-hidden min-w-[10rem]", children: usageLimit ? (_jsxs("div", { className: "flex justify-between items-center py-1.5 px-3 gap-4", children: [_jsx("span", { className: "text-[0.6875rem] text-slate-500 whitespace-nowrap", children: "
|
|
1216
|
+
: undefined, children: _jsx("div", { className: "inline-flex flex-col border border-slate-200 rounded-[10px] overflow-hidden min-w-[10rem]", children: usageLimit ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex justify-between items-center py-1.5 px-3 gap-4", children: [_jsx("span", { className: "text-[0.6875rem] text-slate-500 whitespace-nowrap", children: "Included" }), _jsx("span", { className: "text-xs text-slate-700 text-right whitespace-nowrap", children: usageLimit.freeLimit != null ? (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [usageLimit.freeLimit, usageLimit.unitName
|
|
1217
|
+
? ` ${usageLimit.unitName}`
|
|
1218
|
+
: ""] }), usageLimit.resetCycle &&
|
|
1219
|
+
usageLimit.resetCycle !== "NONE" && (_jsxs("span", { className: "text-[0.625rem] font-normal text-slate-400", children: [" ", "/ ", usageLimit.resetCycle.toLowerCase()] }))] })) : (_jsx("strong", { children: usageLimit.notes || "Unlimited" })) })] }), usageLimit.unitPrice != null && (_jsxs("div", { className: "flex justify-between items-center py-1.5 px-3 gap-4 border-t border-slate-100", children: [_jsx("span", { className: "text-[0.6875rem] text-slate-500 whitespace-nowrap", children: "Overage" }), _jsxs("span", { className: "text-xs text-emerald-600 font-medium text-right whitespace-nowrap", children: [formatPrice(usageLimit.unitPrice, usageLimit.unitPriceCurrency || "USD"), _jsxs("span", { className: "text-[0.625rem] font-normal text-slate-400", children: [" ", "/ extra"] })] })] }))] })) : (_jsx("span", { className: "text-xs text-slate-300", children: "\u2014" })) }) }, tier.id));
|
|
1099
1220
|
})] }, `${service.id}-${metric}`)))] }));
|
|
1100
1221
|
}
|
|
1101
1222
|
function ServiceLevelDetailPanel({ serviceId, tierId, services, tiers, optionGroups: _optionGroups, dispatch, onClose, }) {
|
|
@@ -1161,21 +1282,28 @@ function ServiceLevelDetailPanel({ serviceId, tierId, services, tiers, optionGro
|
|
|
1161
1282
|
const serviceLevel = service
|
|
1162
1283
|
? tier?.serviceLevels.find((sl) => sl.serviceId === serviceId)
|
|
1163
1284
|
: undefined;
|
|
1164
|
-
const usageLimits = service
|
|
1285
|
+
const usageLimits = service
|
|
1286
|
+
? tier?.usageLimits.filter((ul) => ul.serviceId === serviceId) || []
|
|
1287
|
+
: [];
|
|
1165
1288
|
const [isAddingMetric, setIsAddingMetric] = useState(false);
|
|
1166
1289
|
const [newMetric, setNewMetric] = useState("");
|
|
1167
1290
|
const [newLimit, setNewLimit] = useState("");
|
|
1291
|
+
const [customValue, setCustomValue] = useState(serviceLevel?.customValue || "");
|
|
1168
1292
|
if (!service || !tier)
|
|
1169
1293
|
return null;
|
|
1170
1294
|
const handleAddLimit = () => {
|
|
1171
1295
|
if (!newMetric.trim())
|
|
1172
1296
|
return;
|
|
1173
|
-
const parsedLimit = newLimit ? parseInt(newLimit, 10) :
|
|
1297
|
+
const parsedLimit = newLimit ? parseInt(newLimit, 10) : null;
|
|
1298
|
+
const isNumeric = parsedLimit !== null && !isNaN(parsedLimit);
|
|
1174
1299
|
dispatch(addUsageLimit({
|
|
1175
1300
|
tierId: tier.id,
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1301
|
+
limitId: generateId(),
|
|
1302
|
+
serviceId: service.id,
|
|
1303
|
+
metric: newMetric.trim(),
|
|
1304
|
+
freeLimit: isNumeric ? parsedLimit : undefined,
|
|
1305
|
+
notes: !isNumeric && newLimit ? newLimit.trim() : undefined,
|
|
1306
|
+
resetCycle: "MONTHLY",
|
|
1179
1307
|
lastModified: new Date().toISOString(),
|
|
1180
1308
|
}));
|
|
1181
1309
|
setNewMetric("");
|
|
@@ -1185,7 +1313,7 @@ function ServiceLevelDetailPanel({ serviceId, tierId, services, tiers, optionGro
|
|
|
1185
1313
|
const handleRemoveLimit = (limitId) => {
|
|
1186
1314
|
dispatch(removeUsageLimit({
|
|
1187
1315
|
tierId: tier.id,
|
|
1188
|
-
|
|
1316
|
+
limitId,
|
|
1189
1317
|
lastModified: new Date().toISOString(),
|
|
1190
1318
|
}));
|
|
1191
1319
|
};
|
|
@@ -1200,29 +1328,56 @@ function ServiceLevelDetailPanel({ serviceId, tierId, services, tiers, optionGro
|
|
|
1200
1328
|
}
|
|
1201
1329
|
function MetricLimitItem({ limit, tierId, dispatch, onRemove, }) {
|
|
1202
1330
|
const [isEditing, setIsEditing] = useState(false);
|
|
1203
|
-
const [editMetric, setEditMetric] = useState(limit.
|
|
1204
|
-
const [
|
|
1205
|
-
const [editLimit, setEditLimit] = useState(limit.
|
|
1331
|
+
const [editMetric, setEditMetric] = useState(limit.metric);
|
|
1332
|
+
const [editUnitName, setEditUnitName] = useState(limit.unitName || "");
|
|
1333
|
+
const [editLimit, setEditLimit] = useState(limit.freeLimit?.toString() || limit.notes || "");
|
|
1334
|
+
const [editPaidLimit, setEditPaidLimit] = useState(limit.paidLimit?.toString() || "");
|
|
1335
|
+
const [editResetCycle, setEditResetCycle] = useState(limit.resetCycle || "MONTHLY");
|
|
1336
|
+
// Overage pricing state
|
|
1337
|
+
const [editUnitPrice, setEditUnitPrice] = useState(limit.unitPrice?.toString() || "");
|
|
1338
|
+
const [editUnitPriceCurrency] = useState(limit.unitPriceCurrency || "USD");
|
|
1206
1339
|
const handleSave = () => {
|
|
1207
1340
|
const parsedLimit = editLimit ? parseInt(editLimit, 10) : null;
|
|
1341
|
+
const isNumeric = parsedLimit !== null && !isNaN(parsedLimit);
|
|
1342
|
+
const parsedPaidLimit = editPaidLimit ? parseInt(editPaidLimit, 10) : null;
|
|
1343
|
+
const isPaidNumeric = parsedPaidLimit !== null && !isNaN(parsedPaidLimit);
|
|
1344
|
+
const parsedUnitPrice = editUnitPrice ? parseFloat(editUnitPrice) : null;
|
|
1208
1345
|
dispatch(updateUsageLimit({
|
|
1209
1346
|
tierId,
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1347
|
+
limitId: limit.id,
|
|
1348
|
+
metric: editMetric.trim() || limit.metric,
|
|
1349
|
+
unitName: editUnitName.trim() || undefined,
|
|
1350
|
+
freeLimit: isNumeric ? parsedLimit : undefined,
|
|
1351
|
+
paidLimit: isPaidNumeric ? parsedPaidLimit : undefined,
|
|
1352
|
+
notes: !isNumeric && editLimit ? editLimit.trim() : undefined,
|
|
1353
|
+
resetCycle: editResetCycle,
|
|
1354
|
+
unitPrice: parsedUnitPrice,
|
|
1355
|
+
unitPriceCurrency: parsedUnitPrice ? editUnitPriceCurrency : undefined,
|
|
1214
1356
|
lastModified: new Date().toISOString(),
|
|
1215
1357
|
}));
|
|
1216
1358
|
setIsEditing(false);
|
|
1217
1359
|
};
|
|
1218
1360
|
const handleCancel = () => {
|
|
1219
|
-
setEditMetric(limit.
|
|
1220
|
-
|
|
1221
|
-
setEditLimit(limit.
|
|
1361
|
+
setEditMetric(limit.metric);
|
|
1362
|
+
setEditUnitName(limit.unitName || "");
|
|
1363
|
+
setEditLimit(limit.freeLimit?.toString() || limit.notes || "");
|
|
1364
|
+
setEditPaidLimit(limit.paidLimit?.toString() || "");
|
|
1365
|
+
setEditResetCycle(limit.resetCycle || "MONTHLY");
|
|
1366
|
+
setEditUnitPrice(limit.unitPrice?.toString() || "");
|
|
1222
1367
|
setIsEditing(false);
|
|
1223
1368
|
};
|
|
1369
|
+
// Format overage display string
|
|
1370
|
+
const getOverageDisplay = () => {
|
|
1371
|
+
if (!limit.unitPrice)
|
|
1372
|
+
return null;
|
|
1373
|
+
const unitLabel = limit.unitName || "unit";
|
|
1374
|
+
return `+${formatPrice(limit.unitPrice, limit.unitPriceCurrency || "USD")} per ${unitLabel}`;
|
|
1375
|
+
};
|
|
1376
|
+
const overageDisplay = getOverageDisplay();
|
|
1224
1377
|
if (isEditing) {
|
|
1225
|
-
return (_jsxs("div", { className: "p-3 bg-violet-50 rounded-[10px] mb-3 [&>div]:mb-2.5 [&>div:last-child]:mb-0", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Metric Name" }), _jsx("input", { type: "text", value: editMetric, onChange: (e) => setEditMetric(e.target.value), placeholder: "e.g., Number of Entities", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, autoFocus: true })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Unit" }), _jsx("input", { type: "text", value:
|
|
1378
|
+
return (_jsxs("div", { className: "p-3 bg-violet-50 rounded-[10px] mb-3 [&>div]:mb-2.5 [&>div:last-child]:mb-0", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Metric Name" }), _jsx("input", { type: "text", value: editMetric, onChange: (e) => setEditMetric(e.target.value), placeholder: "e.g., Number of Entities", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, autoFocus: true })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Unit Name" }), _jsx("input", { type: "text", value: editUnitName, onChange: (e) => setEditUnitName(e.target.value), placeholder: "e.g., entity, credit card, contractor", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" } }), _jsx("p", { className: "text-[0.6875rem] text-slate-400 mt-1", children: "Used for overage pricing display (e.g., \"$50 per entity\")" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Free Limit" }), _jsx("input", { type: "text", value: editLimit, onChange: (e) => setEditLimit(e.target.value), placeholder: "e.g., 100, Unlimited, Custom", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" } }), _jsx("p", { className: "text-[0.6875rem] text-slate-400 mt-1", children: "Included free limit for this tier" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Paid Limit" }), _jsx("input", { type: "text", value: editPaidLimit, onChange: (e) => setEditPaidLimit(e.target.value), placeholder: "e.g., 500, 1000", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" } }), _jsx("p", { className: "text-[0.6875rem] text-slate-400 mt-1", children: "Maximum paid usage beyond the free limit (optional)" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Reset Cycle" }), _jsxs("select", { value: editResetCycle, onChange: (e) => setEditResetCycle(e.target.value), className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)] cursor-pointer", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: [_jsx("option", { value: "NONE", children: "None (One-time)" }), _jsx("option", { value: "DAILY", children: "Daily" }), _jsx("option", { value: "WEEKLY", children: "Weekly" }), _jsx("option", { value: "MONTHLY", children: "Monthly" })] })] }), _jsxs("div", { className: "mt-2 pt-3 border-t border-dashed border-slate-300", children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Overage Pricing (Optional)" }), _jsx("p", { className: "text-[0.6875rem] text-slate-400 mt-1", style: { marginBottom: "0.5rem" }, children: "Set a price for usage beyond the included limit" }), _jsxs("div", { className: "flex items-center gap-1.5 flex-wrap", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "text-sm text-slate-500", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "$" }), _jsx("input", { type: "number", value: editUnitPrice, onChange: (e) => setEditUnitPrice(e.target.value), placeholder: "0.00", step: "0.01", className: "w-[4.5rem] text-sm font-medium text-slate-900 bg-white border border-slate-300 rounded-md py-1.5 px-2 outline-none transition-colors duration-150 focus:border-violet-600", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" } })] }), _jsxs("span", { className: "text-xs text-slate-500", children: ["per ", editUnitName || "unit"] })] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: handleSave, className: "flex-1 py-2 px-3 text-[0.8125rem] font-semibold rounded-[10px] cursor-pointer transition-all duration-150 bg-violet-600 text-white border-none hover:enabled:bg-violet-700 disabled:opacity-50 disabled:cursor-not-allowed", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: "Save" }), _jsx("button", { onClick: handleCancel, className: "flex-1 py-2 px-3 text-[0.8125rem] font-semibold rounded-[10px] cursor-pointer transition-all duration-150 bg-slate-200 text-slate-700 border-none hover:bg-slate-300", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: "Cancel" })] })] }));
|
|
1226
1379
|
}
|
|
1227
|
-
return (_jsxs("div", { className: "group/limititem flex items-center gap-3 p-3 bg-slate-50 border border-slate-200 rounded-[10px] mb-3", children: [_jsxs("div", { className: "flex-1 cursor-pointer p-1 -m-1 rounded-md transition-all duration-150 hover:bg-slate-200", onClick: () => setIsEditing(true), children: [_jsx("div", { className: "text-sm font-semibold text-slate-900", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: limit.
|
|
1380
|
+
return (_jsxs("div", { className: "group/limititem flex items-center gap-3 p-3 bg-slate-50 border border-slate-200 rounded-[10px] mb-3", children: [_jsxs("div", { className: "flex-1 cursor-pointer p-1 -m-1 rounded-md transition-all duration-150 hover:bg-slate-200", onClick: () => setIsEditing(true), children: [_jsx("div", { className: "text-sm font-semibold text-slate-900", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: limit.metric }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("div", { className: "text-[0.8125rem] text-slate-500", children: limit.freeLimit != null
|
|
1381
|
+
? `Free: ${limit.freeLimit}${limit.paidLimit != null ? ` / Paid: ${limit.paidLimit}` : ""}`
|
|
1382
|
+
: (limit.notes ?? "—") }), limit.resetCycle && (_jsxs("div", { style: { fontSize: "0.6875rem", color: "#64748b" }, children: ["Resets ", limit.resetCycle.toLowerCase()] })), overageDisplay && (_jsx("div", { className: "text-[0.6875rem] text-emerald-600 font-medium", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: overageDisplay }))] })] }), _jsxs("div", { className: "flex gap-1 opacity-0 group-hover/limititem:opacity-100 transition-all duration-150", children: [_jsx("button", { onClick: () => setIsEditing(true), className: "p-1 bg-transparent border-none text-slate-400 cursor-pointer rounded-md transition-all duration-150 hover:bg-slate-200 hover:text-violet-600", title: "Edit metric", children: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" }) }) }), _jsx("button", { onClick: onRemove, className: "p-1 bg-transparent border-none text-slate-400 cursor-pointer rounded-md transition-all duration-150 hover:bg-slate-200 hover:text-rose-600", title: "Remove metric", children: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] })] }));
|
|
1228
1383
|
}
|