attach flows done
This commit is contained in:
@@ -71,11 +71,12 @@ export const handleTransferProductV2 = createRoute({
|
||||
: nullish(cp.internal_entity_id)) && cp.product.id === product_id,
|
||||
);
|
||||
|
||||
const toCusProduct = customer.customer_products.find(
|
||||
(cp: any) =>
|
||||
cp.internal_entity_id === toEntity.internal_id &&
|
||||
cp.product.group === product.group,
|
||||
);
|
||||
const toCusProduct = customer.customer_products.find((cp: any) => {
|
||||
const productMatch = cusProduct?.product.is_add_on
|
||||
? cp.product.product_id === product.id
|
||||
: cp.product.group === product.group;
|
||||
return cp.internal_entity_id === toEntity.internal_id && productMatch;
|
||||
});
|
||||
|
||||
if (toCusProduct) {
|
||||
throw new CusProductAlreadyExistsError({
|
||||
|
||||
@@ -34,14 +34,14 @@ export const freeTrialsAreSame = ({
|
||||
|
||||
const freeTrialsAreDiff = Object.values(diffs).some((d) => d.condition);
|
||||
|
||||
if (freeTrialsAreDiff) {
|
||||
console.log("Free trials different");
|
||||
console.log(
|
||||
"Differences:",
|
||||
Object.values(diffs)
|
||||
.filter((d) => d.condition)
|
||||
.map((d) => d.message),
|
||||
);
|
||||
}
|
||||
// if (freeTrialsAreDiff) {
|
||||
// console.log("Free trials different");
|
||||
// console.log(
|
||||
// "Differences:",
|
||||
// Object.values(diffs)
|
||||
// .filter((d) => d.condition)
|
||||
// .map((d) => d.message),
|
||||
// );
|
||||
// }
|
||||
return !freeTrialsAreDiff;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { CheckoutResponse } from "@autumn/shared";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
useHasChanges,
|
||||
useIsLatestVersion,
|
||||
} from "@/hooks/stores/useProductStore";
|
||||
import { useIsLatestVersion } from "@/hooks/stores/useProductStore";
|
||||
import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox";
|
||||
import { useAttachPreview } from "./use-attach-preview";
|
||||
|
||||
export const AttachConfirmationInfo = () => {
|
||||
const { data: previewData } = useAttachPreview();
|
||||
const hasChanges = useHasChanges();
|
||||
export const AttachConfirmationInfo = ({
|
||||
previewData,
|
||||
}: {
|
||||
previewData?: CheckoutResponse | null;
|
||||
}) => {
|
||||
const isLatestVersion = useIsLatestVersion(previewData?.product);
|
||||
|
||||
const renderInfoBoxes = (): ReactNode[] => {
|
||||
@@ -18,14 +17,6 @@ export const AttachConfirmationInfo = () => {
|
||||
if (!previewData) {
|
||||
return boxes;
|
||||
}
|
||||
console.log("hasChanges", hasChanges);
|
||||
if (hasChanges) {
|
||||
boxes.push(
|
||||
<InfoBox key="changes" variant="success">
|
||||
This plan has been customized for this customer
|
||||
</InfoBox>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLatestVersion) {
|
||||
boxes.push(
|
||||
@@ -78,13 +69,14 @@ export const AttachConfirmationInfo = () => {
|
||||
| "upgrade"
|
||||
| "downgrade"
|
||||
| "cancel"
|
||||
| "new"
|
||||
| string;
|
||||
|
||||
switch (scenario) {
|
||||
case "upgrade":
|
||||
boxes.push(
|
||||
<InfoBox key="product-upgrade" variant="note">
|
||||
This upgrade will replace the customer's current plan:{" "}
|
||||
This upgrade will immediately replace the customer's current plan:{" "}
|
||||
{previewData.current_product.name}
|
||||
</InfoBox>,
|
||||
);
|
||||
@@ -105,11 +97,10 @@ export const AttachConfirmationInfo = () => {
|
||||
</InfoBox>,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
case "new":
|
||||
boxes.push(
|
||||
<InfoBox key="product-switch" variant="info">
|
||||
This will replace the customer's current plan:{" "}
|
||||
{previewData.current_product.name}
|
||||
<InfoBox key="new-product" variant="info">
|
||||
This will be enabled alongside existing plans{" "}
|
||||
</InfoBox>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { FullCusProduct, ProductItem } from "@autumn/shared";
|
||||
import type {
|
||||
CheckoutResponse,
|
||||
FullCusEntWithFullCusProduct,
|
||||
FullCusProduct,
|
||||
ProductItem,
|
||||
} from "@autumn/shared";
|
||||
import { getCusEntBalance, ProductItemFeatureType } from "@autumn/shared";
|
||||
import { useMemo } from "react";
|
||||
import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
@@ -6,7 +11,6 @@ import {
|
||||
deduplicateEntitlements,
|
||||
flattenCustomerEntitlements,
|
||||
} from "@/views/customers2/components/table/customer-feature-usage/customerFeatureUsageUtils";
|
||||
import { useAttachPreview } from "./use-attach-preview";
|
||||
|
||||
interface FeatureBalanceChange {
|
||||
featureId: string;
|
||||
@@ -20,9 +24,12 @@ interface FeatureBalanceChange {
|
||||
};
|
||||
}
|
||||
|
||||
export function AttachFeaturePreview({ customerId }: { customerId: string }) {
|
||||
const { customer } = useCusQuery({ enabled: !!customerId });
|
||||
const { data: previewData } = useAttachPreview();
|
||||
export function AttachFeaturePreview({
|
||||
previewData,
|
||||
}: {
|
||||
previewData?: CheckoutResponse | null;
|
||||
}) {
|
||||
const { customer } = useCusQuery();
|
||||
|
||||
const featureChanges = useMemo((): FeatureBalanceChange[] => {
|
||||
if (!previewData?.product || !customer) return [];
|
||||
@@ -91,7 +98,14 @@ export function AttachFeaturePreview({ customerId }: { customerId: string }) {
|
||||
]),
|
||||
);
|
||||
|
||||
const newFeaturesMap = new Map(
|
||||
const newFeaturesMap = new Map<
|
||||
string,
|
||||
{
|
||||
includedUsage: number;
|
||||
feature: ProductItem["feature"];
|
||||
display: Record<string, unknown>;
|
||||
}
|
||||
>(
|
||||
newFeatureItems
|
||||
.filter((item: ProductItem) => item.feature_id)
|
||||
.map((item: ProductItem) => [
|
||||
@@ -99,7 +113,7 @@ export function AttachFeaturePreview({ customerId }: { customerId: string }) {
|
||||
{
|
||||
includedUsage: item.included_usage || 0,
|
||||
feature: item.feature,
|
||||
display: item.feature?.display || {},
|
||||
display: (item.feature?.display as Record<string, unknown>) || {},
|
||||
},
|
||||
]),
|
||||
);
|
||||
@@ -113,14 +127,18 @@ export function AttachFeaturePreview({ customerId }: { customerId: string }) {
|
||||
const changes: FeatureBalanceChange[] = [];
|
||||
|
||||
for (const featureId of allFeatureIds) {
|
||||
const currentIncludedUsage = currentFeaturesMap.get(featureId);
|
||||
const newFeatureData = newFeaturesMap.get(featureId);
|
||||
const currentBalanceData = currentBalanceMap.get(featureId);
|
||||
const featureIdStr = String(featureId);
|
||||
const currentIncludedUsage = currentFeaturesMap.get(featureIdStr);
|
||||
const newFeatureData = newFeaturesMap.get(featureIdStr);
|
||||
const currentBalanceData = currentBalanceMap.get(featureIdStr);
|
||||
|
||||
// Use actual current balance if available, otherwise use included usage from product
|
||||
const currentBalance =
|
||||
currentBalanceData?.balance ?? currentIncludedUsage ?? null;
|
||||
const newBalance = newFeatureData?.includedUsage ?? null;
|
||||
const currentBalance: number | null =
|
||||
currentBalanceData?.balance ??
|
||||
(typeof currentIncludedUsage === "number"
|
||||
? currentIncludedUsage
|
||||
: null);
|
||||
const newBalance: number | null = newFeatureData?.includedUsage ?? null;
|
||||
|
||||
// Determine status
|
||||
let status: "changed" | "added" | "removed";
|
||||
@@ -133,24 +151,25 @@ export function AttachFeaturePreview({ customerId }: { customerId: string }) {
|
||||
}
|
||||
|
||||
// Get feature name and display info
|
||||
let featureName = "";
|
||||
let display = {};
|
||||
|
||||
if (newFeatureData?.feature) {
|
||||
featureName = newFeatureData.feature.name;
|
||||
display = newFeatureData.display;
|
||||
} else if (currentBalanceData) {
|
||||
featureName = currentBalanceData.feature.entitlement.feature.name;
|
||||
display = currentBalanceData.feature.entitlement.feature.display || {};
|
||||
}
|
||||
const featureName =
|
||||
newFeatureData?.feature?.name ??
|
||||
currentBalanceData?.feature.entitlement.feature.name ??
|
||||
"";
|
||||
const display =
|
||||
newFeatureData?.display ??
|
||||
(currentBalanceData?.feature.entitlement.feature.display as Record<
|
||||
string,
|
||||
unknown
|
||||
>) ??
|
||||
{};
|
||||
|
||||
changes.push({
|
||||
featureId,
|
||||
featureName,
|
||||
featureId: featureIdStr,
|
||||
featureName: featureName ?? "",
|
||||
currentBalance,
|
||||
newBalance,
|
||||
status,
|
||||
display,
|
||||
display: display as { singular?: string; plural?: string },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { CheckoutResponse, ProductV2 } from "@autumn/shared";
|
||||
import { ArrowUpRightFromSquare } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useAttachProductMutation } from "@/components/forms/attach-product/use-attach-product-mutation";
|
||||
import {
|
||||
@@ -7,36 +9,54 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { useOrg } from "@/hooks/common/useOrg";
|
||||
import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery";
|
||||
import { useEntity } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { useEnv } from "@/utils/envUtils";
|
||||
import { getStripeInvoiceLink } from "@/utils/linkUtils";
|
||||
import { useAttachPreview } from "./use-attach-preview";
|
||||
import type { UseAttachProductForm } from "./use-attach-product-form";
|
||||
|
||||
interface AttachProductActionsProps {
|
||||
form: UseAttachProductForm;
|
||||
product: ProductV2;
|
||||
customerId: string;
|
||||
onSuccess?: () => void;
|
||||
isPreviewLoading: boolean;
|
||||
previewData?: CheckoutResponse | null;
|
||||
isPreviewLoading?: boolean;
|
||||
}
|
||||
|
||||
export function AttachProductActions({
|
||||
form,
|
||||
product,
|
||||
customerId,
|
||||
onSuccess,
|
||||
previewData,
|
||||
isPreviewLoading,
|
||||
}: AttachProductActionsProps) {
|
||||
const { stripeAccount } = useOrgStripeQuery();
|
||||
const env = useEnv();
|
||||
const { data: previewData, isLoading: isAttachPreviewLoading } =
|
||||
useAttachPreview();
|
||||
const org = useOrg();
|
||||
const { entityId } = useEntity();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [buttonWidth, setButtonWidth] = useState<number>(0);
|
||||
const [activeAction, setActiveAction] = useState<"invoice" | "attach" | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
console.log("org", org);
|
||||
const ownStripeAccount = org.org?.stripe_connection !== "default";
|
||||
|
||||
useEffect(() => {
|
||||
if (buttonRef.current) {
|
||||
setButtonWidth(buttonRef.current.offsetWidth);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const attachMutation = useAttachProductMutation({
|
||||
customerId,
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
setActiveAction(null);
|
||||
onSuccess?.();
|
||||
},
|
||||
});
|
||||
@@ -44,52 +64,57 @@ export function AttachProductActions({
|
||||
const handleAttach = async ({
|
||||
useInvoice,
|
||||
enableProductImmediately,
|
||||
action,
|
||||
}: {
|
||||
useInvoice: boolean;
|
||||
enableProductImmediately?: boolean;
|
||||
action: "invoice" | "attach";
|
||||
}) => {
|
||||
const { productId, prepaidOptions } = form.state.values;
|
||||
const { prepaidOptions } = form.state.values;
|
||||
setActiveAction(action);
|
||||
|
||||
if (!productId) {
|
||||
toast.error("Please select a product");
|
||||
return;
|
||||
}
|
||||
|
||||
if (previewData?.url) {
|
||||
if (previewData?.url && action === "attach") {
|
||||
window.open(previewData.url, "_blank");
|
||||
setActiveAction(null);
|
||||
return;
|
||||
}
|
||||
|
||||
//does the attach
|
||||
const result = await attachMutation.mutateAsync({
|
||||
productId,
|
||||
prepaidOptions: prepaidOptions || {},
|
||||
useInvoice,
|
||||
enableProductImmediately,
|
||||
entityId: entityId ?? undefined,
|
||||
});
|
||||
try {
|
||||
//does the attach
|
||||
const result = await attachMutation.mutateAsync({
|
||||
product,
|
||||
prepaidOptions: prepaidOptions || {},
|
||||
useInvoice,
|
||||
enableProductImmediately,
|
||||
entityId: entityId ?? undefined,
|
||||
});
|
||||
|
||||
// Handle checkout URLs and invoice links
|
||||
if (result.data.invoice) {
|
||||
window.open(
|
||||
getStripeInvoiceLink({
|
||||
stripeInvoice: result.data.invoice,
|
||||
env,
|
||||
accountId: stripeAccount?.id,
|
||||
}),
|
||||
"_blank",
|
||||
);
|
||||
console.log("result", result);
|
||||
|
||||
// Handle checkout URLs and invoice links
|
||||
if (result.data.invoice) {
|
||||
window.open(
|
||||
getStripeInvoiceLink({
|
||||
stripeInvoice: result.data.invoice,
|
||||
env,
|
||||
accountId: stripeAccount?.id,
|
||||
}),
|
||||
"_blank",
|
||||
);
|
||||
toast.success("Redirected to Stripe to finalize the invoice");
|
||||
}
|
||||
} catch (error) {
|
||||
setActiveAction(null);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
if (isAttachPreviewLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLoading = attachMutation.isPending;
|
||||
const isInvoiceLoading = isLoading && activeAction === "invoice";
|
||||
const isAttachLoading = isLoading && activeAction === "attach";
|
||||
|
||||
// Don't show buttons if preview is loading
|
||||
if (isPreviewLoading || !form.state.values.productId) {
|
||||
if (isPreviewLoading || !product) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -100,23 +125,30 @@ export function AttachProductActions({
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
isLoading={isLoading}
|
||||
disabled={isLoading}
|
||||
isLoading={isInvoiceLoading}
|
||||
disabled={isLoading || !ownStripeAccount}
|
||||
type="button"
|
||||
>
|
||||
Send an Invoice
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-0" align="start">
|
||||
<PopoverContent
|
||||
className="p-0 z-100 rounded-lg"
|
||||
align="start"
|
||||
style={{ width: buttonWidth > 0 ? `${buttonWidth}px` : "auto" }}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
onClick={() =>
|
||||
handleAttach({
|
||||
useInvoice: true,
|
||||
enableProductImmediately: true,
|
||||
action: "invoice",
|
||||
})
|
||||
}
|
||||
className="px-4 py-3 text-left text-sm hover:bg-accent"
|
||||
@@ -124,15 +156,17 @@ export function AttachProductActions({
|
||||
<div className="font-medium">Enable plan immediately</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Enable the plan immediately and redirect to Stripe to finalize
|
||||
the invoice
|
||||
the invoice. Customer can pay by the invoice due date.
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
onClick={() =>
|
||||
handleAttach({
|
||||
useInvoice: true,
|
||||
enableProductImmediately: false,
|
||||
action: "invoice",
|
||||
})
|
||||
}
|
||||
className="px-4 py-3 text-left text-sm hover:bg-accent border-t"
|
||||
@@ -150,11 +184,12 @@ export function AttachProductActions({
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full flex items-center gap-2"
|
||||
isLoading={isLoading}
|
||||
isLoading={isAttachLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() =>
|
||||
handleAttach({
|
||||
useInvoice: false,
|
||||
action: "attach",
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
@@ -36,25 +36,32 @@ function FormContent({
|
||||
form,
|
||||
onSuccess,
|
||||
}: FormContentProps) {
|
||||
const { isLoading: isPreviewLoading } = useAttachPreview();
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
const storeProduct = useProductStore((s) => s.product);
|
||||
|
||||
// Use customizedProduct if available, otherwise find from products by productId
|
||||
const product =
|
||||
customizedProduct ??
|
||||
products.find((p) => p.id === productId && !p.archived);
|
||||
// Use customized product if it exists and has changes, otherwise find by form productId
|
||||
const product = storeProduct?.id
|
||||
? storeProduct
|
||||
: products.find((p) => p.id === productId && !p.archived);
|
||||
|
||||
const prepaidItems = usePrepaidItems({ product });
|
||||
const prepaidOptions = useAttachProductStore((s) => s.prepaidOptions);
|
||||
const prepaidOptions = form.state.values.prepaidOptions;
|
||||
|
||||
//get is loading from useAttachPreview
|
||||
const { isLoading } = useAttachPreview();
|
||||
const { entityId } = useEntity();
|
||||
|
||||
// Call preview once here and pass data down to children
|
||||
const previewQuery = useAttachPreview({
|
||||
customerId,
|
||||
product,
|
||||
entityId: entityId ?? undefined,
|
||||
prepaidOptions: prepaidOptions ?? undefined,
|
||||
version: product?.version,
|
||||
});
|
||||
|
||||
// Check if there are prepaid items and if any are not set (undefined/null)
|
||||
// Note: 0 is a valid quantity value
|
||||
if (prepaidItems.length > 0) {
|
||||
const hasUnsetPrepaidQuantity = prepaidItems.some((item) => {
|
||||
const quantity = prepaidOptions[item.feature_id as string];
|
||||
const quantity = prepaidOptions?.[item.feature_id as string];
|
||||
return quantity === undefined || quantity === null;
|
||||
});
|
||||
|
||||
@@ -63,23 +70,25 @@ function FormContent({
|
||||
}
|
||||
}
|
||||
|
||||
if (!form.state.values.productId) {
|
||||
if (!form.state.values.productId || !product) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AttachProductSummary
|
||||
productId={productId}
|
||||
products={products}
|
||||
customerId={customerId}
|
||||
previewData={previewQuery.data}
|
||||
isLoading={previewQuery.isLoading}
|
||||
product={product}
|
||||
/>
|
||||
|
||||
<AttachProductActions
|
||||
form={form}
|
||||
product={product}
|
||||
customerId={customerId}
|
||||
onSuccess={onSuccess}
|
||||
isPreviewLoading={isPreviewLoading}
|
||||
previewData={previewQuery.data}
|
||||
isPreviewLoading={previewQuery.isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -93,12 +102,9 @@ export function AttachProductForm({
|
||||
onSuccess?: () => void;
|
||||
}) {
|
||||
const itemId = useSheetStore((s) => s.itemId); // The productId being customized
|
||||
const form = useAttachProductForm({ initialProductId: itemId || undefined }); //load from cusplaneditorbar if its being customized
|
||||
const form = useAttachProductForm({ initialProductId: itemId || undefined });
|
||||
const { products, isLoading } = useProductsQuery();
|
||||
const resetProductStore = useProductStore((s) => s.reset);
|
||||
|
||||
// Get store setters
|
||||
const setFormValues = useAttachProductStore((s) => s.setFormValues);
|
||||
const setCustomerId = useAttachProductStore((s) => s.setCustomerId);
|
||||
|
||||
const activeProducts = products.filter((p) => !p.archived);
|
||||
@@ -115,33 +121,19 @@ export function AttachProductForm({
|
||||
useEffect(() => {
|
||||
// Set customerId on mount
|
||||
setCustomerId(customerId);
|
||||
}, [customerId, setCustomerId]);
|
||||
|
||||
// Subscribe to form changes
|
||||
useEffect(() => {
|
||||
// Reset product store when productId changes (unless it matches itemId from customization)
|
||||
const subscription = form.store.subscribe(() => {
|
||||
const values = form.store.state.values;
|
||||
const productId = values.productId;
|
||||
|
||||
// Sync form values to store (no need to pass customerId again)
|
||||
setFormValues({
|
||||
productId: values.productId,
|
||||
prepaidOptions: values.prepaidOptions ?? {},
|
||||
});
|
||||
|
||||
// Reset product store when productId changes (unless it matches itemId from customization)
|
||||
const productId = form.store.state.values.productId;
|
||||
if (productId && productId !== itemId) {
|
||||
resetProductStore();
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription();
|
||||
}, [
|
||||
customerId,
|
||||
form.store,
|
||||
itemId,
|
||||
resetProductStore,
|
||||
setFormValues,
|
||||
setCustomerId,
|
||||
]);
|
||||
}, [form.store, itemId, resetProductStore]);
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-sm text-t3">Loading products...</div>;
|
||||
@@ -172,6 +164,7 @@ export function AttachProductForm({
|
||||
<form.Subscribe
|
||||
selector={(state) => ({
|
||||
productId: state.values.productId,
|
||||
prepaidOptions: state.values.prepaidOptions,
|
||||
})}
|
||||
>
|
||||
{(values) => (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
|
||||
import { usePrepaidItems } from "@/hooks/stores/useProductStore";
|
||||
import { useAttachProductStore } from "@/hooks/stores/useSubscriptionStore";
|
||||
import {
|
||||
usePrepaidItems,
|
||||
useProductStore,
|
||||
} from "@/hooks/stores/useProductStore";
|
||||
import type { UseAttachProductForm } from "./use-attach-product-form";
|
||||
|
||||
interface PrepaidOptionsFieldProps {
|
||||
@@ -10,14 +12,14 @@ interface PrepaidOptionsFieldProps {
|
||||
export function AttachProductPrepaidOptions({
|
||||
form,
|
||||
}: PrepaidOptionsFieldProps) {
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
const storeProduct = useProductStore((s) => s.product);
|
||||
const { products = [] } = useProductsQuery();
|
||||
const selectedProductId = form.state.values.productId;
|
||||
|
||||
// Use customizedProduct if available, otherwise find from products by productId
|
||||
const product =
|
||||
customizedProduct ??
|
||||
products.find((p) => p.id === selectedProductId && !p.archived);
|
||||
// Use customized product if it has changes, otherwise find by form productId
|
||||
const product = storeProduct?.id
|
||||
? storeProduct
|
||||
: products.find((p) => p.id === selectedProductId && !p.archived);
|
||||
|
||||
const prepaidItems = usePrepaidItems({ product });
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { PencilSimpleIcon } from "@phosphor-icons/react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { IconButton } from "@/components/v2/buttons/IconButton";
|
||||
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
|
||||
import { useHasChanges } from "@/hooks/stores/useProductStore";
|
||||
import { useAttachProductStore } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { pushPage } from "@/utils/genUtils";
|
||||
import type { UseAttachProductForm } from "./use-attach-product-form";
|
||||
|
||||
@@ -20,21 +18,9 @@ export function AttachProductSelection({
|
||||
const { products } = useProductsQuery();
|
||||
const activeProducts = products.filter((p) => !p.archived);
|
||||
const navigate = useNavigate();
|
||||
const setCustomizedProduct = useAttachProductStore(
|
||||
(s) => s.setCustomizedProduct,
|
||||
);
|
||||
const productId = form.state.values.productId;
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
const hasChanges = useHasChanges();
|
||||
|
||||
//reset customized product to prevent any stale edited products (from SubscriptionDetailSheet)
|
||||
useEffect(() => {
|
||||
// Only reset if productId is not undefined/null (meaning user changed it)
|
||||
if (productId !== undefined && customizedProduct?.id !== productId) {
|
||||
setCustomizedProduct(null);
|
||||
}
|
||||
}, [productId, setCustomizedProduct]);
|
||||
|
||||
const handleCustomize = ({ productId }: { productId: string }) => {
|
||||
if (!productId || !customerId) {
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ProductV2 } from "@autumn/shared";
|
||||
import type { CheckoutResponse, ProductV2 } from "@autumn/shared";
|
||||
import SmallSpinner from "@/components/general/SmallSpinner";
|
||||
import { Separator } from "@/components/v2/separator";
|
||||
import {
|
||||
@@ -8,29 +8,24 @@ import {
|
||||
import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { AttachConfirmationInfo } from "./attach-confirmation-info";
|
||||
import { AttachFeaturePreview } from "./attach-feature-preview";
|
||||
import { useAttachPreview } from "./use-attach-preview";
|
||||
|
||||
export function AttachProductSummary({
|
||||
productId,
|
||||
products,
|
||||
customerId,
|
||||
product,
|
||||
previewData,
|
||||
isLoading,
|
||||
}: {
|
||||
productId: string;
|
||||
products: ProductV2[];
|
||||
customerId: string;
|
||||
product: ProductV2;
|
||||
previewData?: CheckoutResponse | null;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const { data: previewData, isLoading } = useAttachPreview();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<SmallSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const product = products.find((p) => p.id === productId);
|
||||
|
||||
// Use preview data if available, otherwise calculate from product prices
|
||||
const lineItems =
|
||||
previewData?.lines?.map((line) => {
|
||||
@@ -48,9 +43,9 @@ export function AttachProductSummary({
|
||||
|
||||
return (
|
||||
<div className="space-y-3 text-sm">
|
||||
<AttachConfirmationInfo />
|
||||
<AttachConfirmationInfo previewData={previewData} />
|
||||
|
||||
<AttachFeaturePreview customerId={customerId} />
|
||||
<AttachFeaturePreview previewData={previewData} />
|
||||
<Separator />
|
||||
<SheetAccordion type="single" withSeparator={false} collapsible={true}>
|
||||
{lineItems.length > 0 && (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CheckoutResponse } from "@autumn/shared";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
useHasBillingChanges,
|
||||
@@ -5,10 +6,12 @@ import {
|
||||
} from "@/hooks/stores/useProductStore";
|
||||
import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox";
|
||||
import { useAttachPreview } from "./use-attach-preview";
|
||||
|
||||
export const UpdateConfirmationInfo = () => {
|
||||
const { data: previewData } = useAttachPreview();
|
||||
export const UpdateConfirmationInfo = ({
|
||||
previewData,
|
||||
}: {
|
||||
previewData?: CheckoutResponse | null;
|
||||
}) => {
|
||||
const hasChanges = useHasChanges();
|
||||
const hasBillingChanges = useHasBillingChanges({
|
||||
baseProduct: previewData?.current_product,
|
||||
@@ -45,13 +48,7 @@ export const UpdateConfirmationInfo = () => {
|
||||
if (!hasBillingChanges) {
|
||||
boxes.push(
|
||||
<InfoBox key="no-billing-changes" variant="success">
|
||||
No billing changes will be made
|
||||
</InfoBox>,
|
||||
);
|
||||
} else {
|
||||
boxes.push(
|
||||
<InfoBox key="billing-changes" variant="warning">
|
||||
Billing changes will be made
|
||||
No changes to billing will be made
|
||||
</InfoBox>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CheckoutResponse, ProductV2 } from "@autumn/shared";
|
||||
import { ArrowUpRightFromSquare } from "lucide-react";
|
||||
import { useAttachProductMutation } from "@/components/forms/attach-product/use-attach-product-mutation";
|
||||
import {
|
||||
@@ -7,31 +8,36 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery";
|
||||
import { useAttachProductStore } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { useEnv } from "@/utils/envUtils";
|
||||
import { getStripeInvoiceLink } from "@/utils/linkUtils";
|
||||
import { useAttachPreview } from "./use-attach-preview";
|
||||
import type { UseAttachProductForm } from "./use-attach-product-form";
|
||||
|
||||
interface UpdateProductActionsProps {
|
||||
customerId: string;
|
||||
product?: ProductV2;
|
||||
customerId?: string;
|
||||
entityId?: string;
|
||||
onSuccess?: () => void;
|
||||
isPreviewLoading: boolean;
|
||||
previewData?: CheckoutResponse | null;
|
||||
isPreviewLoading?: boolean;
|
||||
version?: number;
|
||||
form: UseAttachProductForm;
|
||||
}
|
||||
|
||||
export function UpdateProductActions({
|
||||
form,
|
||||
product,
|
||||
customerId,
|
||||
entityId,
|
||||
onSuccess,
|
||||
previewData,
|
||||
isPreviewLoading,
|
||||
version,
|
||||
}: UpdateProductActionsProps) {
|
||||
const { stripeAccount } = useOrgStripeQuery();
|
||||
const env = useEnv();
|
||||
const { data: previewData } = useAttachPreview();
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
|
||||
const attachMutation = useAttachProductMutation({
|
||||
customerId,
|
||||
customerId: customerId ?? "",
|
||||
successMessage: "Plan updated successfully",
|
||||
onSuccess: () => {
|
||||
onSuccess?.();
|
||||
@@ -52,10 +58,12 @@ export function UpdateProductActions({
|
||||
|
||||
// Does the update
|
||||
const result = await attachMutation.mutateAsync({
|
||||
product: customizedProduct ?? undefined,
|
||||
product,
|
||||
entityId,
|
||||
useInvoice,
|
||||
enableProductImmediately,
|
||||
prepaidOptions: form.state.values.prepaidOptions ?? undefined,
|
||||
version,
|
||||
});
|
||||
|
||||
// Handle checkout URLs and invoice links
|
||||
@@ -74,7 +82,7 @@ export function UpdateProductActions({
|
||||
const isLoading = attachMutation.isPending;
|
||||
|
||||
// Don't show buttons if preview is loading
|
||||
if (isPreviewLoading || !customizedProduct) {
|
||||
if (isPreviewLoading || !product) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { usePrepaidItems } from "@/hooks/stores/useProductStore";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import {
|
||||
useAttachProductStore,
|
||||
useSubscriptionById,
|
||||
} from "@/hooks/stores/useSubscriptionStore";
|
||||
usePrepaidItems,
|
||||
useProductStore,
|
||||
} from "@/hooks/stores/useProductStore";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import { useSubscriptionById } from "@/hooks/stores/useSubscriptionStore";
|
||||
import type { UseAttachProductForm } from "./use-attach-product-form";
|
||||
|
||||
export function UpdateProductPrepaidOptions({
|
||||
@@ -11,14 +11,15 @@ export function UpdateProductPrepaidOptions({
|
||||
}: {
|
||||
form: UseAttachProductForm;
|
||||
}) {
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
const storeProduct = useProductStore((s) => s.product);
|
||||
const itemId = useSheetStore((s) => s.itemId);
|
||||
|
||||
const { productV2: product } = useSubscriptionById({ itemId });
|
||||
const { productV2 } = useSubscriptionById({ itemId });
|
||||
|
||||
const prepaidItems = usePrepaidItems({
|
||||
product: product ?? customizedProduct ?? undefined,
|
||||
});
|
||||
// Use store product if it has a real ID, otherwise use productV2 from subscription
|
||||
const product = storeProduct?.id ? storeProduct : (productV2 ?? undefined);
|
||||
|
||||
const prepaidItems = usePrepaidItems({ product });
|
||||
|
||||
if (prepaidItems.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import type { CheckoutResponse, ProductV2 } from "@autumn/shared";
|
||||
import SmallSpinner from "@/components/general/SmallSpinner";
|
||||
import { Separator } from "@/components/v2/separator";
|
||||
import {
|
||||
SheetAccordion,
|
||||
SheetAccordionItem,
|
||||
} from "@/components/v2/sheets/SheetAccordion";
|
||||
import { useAttachProductStore } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { UpdateConfirmationInfo } from "./update-confirmation-info";
|
||||
import { useAttachPreview } from "./use-attach-preview";
|
||||
|
||||
export function UpdateProductSummary() {
|
||||
const { data: previewData, isLoading } = useAttachPreview();
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
|
||||
const storeProductId = useAttachProductStore((s) => s.productId);
|
||||
|
||||
export function UpdateProductSummary({
|
||||
product,
|
||||
previewData,
|
||||
isLoading,
|
||||
}: {
|
||||
product?: ProductV2;
|
||||
previewData?: CheckoutResponse | null;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
@@ -23,11 +25,12 @@ export function UpdateProductSummary() {
|
||||
);
|
||||
}
|
||||
|
||||
console.log("previewData", previewData);
|
||||
// Use preview data if available, otherwise calculate from product prices
|
||||
const lineItems =
|
||||
previewData?.lines?.map((line) => {
|
||||
return {
|
||||
name: line.description || customizedProduct?.name || "Unknown",
|
||||
name: line.description || product?.name || "Unknown",
|
||||
total: line.amount,
|
||||
};
|
||||
}) || [];
|
||||
@@ -40,7 +43,7 @@ export function UpdateProductSummary() {
|
||||
|
||||
return (
|
||||
<div className="space-y-3 text-sm">
|
||||
<UpdateConfirmationInfo />
|
||||
<UpdateConfirmationInfo previewData={previewData} />
|
||||
|
||||
<Separator />
|
||||
<SheetAccordion type="single" withSeparator={false} collapsible={true}>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { ProductV2 } from "@autumn/shared";
|
||||
import { useMemo } from "react";
|
||||
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
|
||||
import { useHasChanges } from "@/hooks/stores/useProductStore";
|
||||
import {
|
||||
useAttachProductStore,
|
||||
useEntity,
|
||||
} from "@/hooks/stores/useSubscriptionStore";
|
||||
import { useHasChanges, useProductStore } from "@/hooks/stores/useProductStore";
|
||||
import { useEntity } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { getAttachBody } from "@/views/customers/customer/product/components/attachProductUtils";
|
||||
|
||||
interface AttachBodyBuilderParams {
|
||||
@@ -20,31 +17,36 @@ interface AttachBodyBuilderParams {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared hook to build attach body from various sources (params, store, products list)
|
||||
* Shared hook to build attach body from explicit params
|
||||
* Used by both useAttachPreview and useAttachProductMutation to keep logic DRY
|
||||
*/
|
||||
export function useAttachBodyBuilder(params: AttachBodyBuilderParams = {}) {
|
||||
const { products } = useProductsQuery();
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
const hasChanges = useHasChanges();
|
||||
const storeProduct = useProductStore((s) => s.product);
|
||||
const { entityId: storeEntityId } = useEntity();
|
||||
const storeProductId = useAttachProductStore((s) => s.productId);
|
||||
|
||||
// Memoized builder function that can be called with runtime params
|
||||
const buildAttachBody = useMemo(
|
||||
() => (runtimeParams?: AttachBodyBuilderParams) => {
|
||||
const mergedParams = { ...params, ...runtimeParams };
|
||||
|
||||
// Resolve the product: use provided product, or customized product from store, or find by ID
|
||||
// Resolve the product: use provided product or find by ID
|
||||
const product =
|
||||
mergedParams.product ||
|
||||
customizedProduct ||
|
||||
products.find((p) => p.id === mergedParams.productId);
|
||||
|
||||
if (!product || !mergedParams.customerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine if this is a custom product (from store with changes)
|
||||
const isCustom =
|
||||
hasChanges && !!storeProduct?.id && product === storeProduct
|
||||
? true
|
||||
: undefined;
|
||||
const version = storeProduct?.id ? storeProduct.version : undefined;
|
||||
|
||||
// Convert prepaidOptions to options array
|
||||
const options = mergedParams.prepaidOptions
|
||||
? Object.entries(mergedParams.prepaidOptions).map(
|
||||
@@ -61,13 +63,13 @@ export function useAttachBodyBuilder(params: AttachBodyBuilderParams = {}) {
|
||||
product,
|
||||
entityId: mergedParams.entityId ?? storeEntityId ?? undefined,
|
||||
optionsInput: options.length > 0 ? options : undefined,
|
||||
isCustom: !!customizedProduct,
|
||||
version: mergedParams.version ?? product.version,
|
||||
isCustom,
|
||||
version,
|
||||
useInvoice: mergedParams.useInvoice,
|
||||
enableProductImmediately: mergedParams.enableProductImmediately,
|
||||
});
|
||||
},
|
||||
[customizedProduct, products, hasChanges, storeEntityId, params],
|
||||
[products, hasChanges, storeProduct, storeEntityId, params],
|
||||
);
|
||||
|
||||
// For simple usage, return the built body with current params
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { CheckoutResponse, ProductV2 } from "@autumn/shared";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useAttachProductStore } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { useAttachBodyBuilder } from "./use-attach-body-builder";
|
||||
|
||||
interface AttachPreviewParams {
|
||||
// Override store values
|
||||
// Required params - no fallbacks
|
||||
customerId?: string;
|
||||
productId?: string;
|
||||
product?: ProductV2;
|
||||
entityId?: string;
|
||||
prepaidOptions?: Record<string, number>;
|
||||
@@ -21,38 +19,25 @@ interface AttachPreviewParams {
|
||||
export function useAttachPreview(params: AttachPreviewParams = {}) {
|
||||
const axiosInstance = useAxiosInstance();
|
||||
|
||||
// Get form values from store (can be overridden by params)
|
||||
const storeCustomerId = useAttachProductStore((s) => s.customerId);
|
||||
const storeProductId = useAttachProductStore((s) => s.productId);
|
||||
const storePrepaidOptions = useAttachProductStore((s) => s.prepaidOptions);
|
||||
|
||||
// Use params if provided, otherwise fall back to store
|
||||
const customerId = params.customerId ?? storeCustomerId;
|
||||
const productId = params.productId ?? storeProductId;
|
||||
const prepaidOptions = params.prepaidOptions ?? storePrepaidOptions;
|
||||
|
||||
// Build attach body using shared hook
|
||||
// Build attach body using shared hook with explicit params
|
||||
const { attachBody } = useAttachBodyBuilder({
|
||||
customerId: customerId ?? undefined,
|
||||
productId: productId ?? undefined,
|
||||
product: params.product, //customizedProduct is accessed within the hook, but can be overridden here
|
||||
customerId: params.customerId,
|
||||
product: params.product,
|
||||
entityId: params.entityId,
|
||||
prepaidOptions: prepaidOptions ?? undefined,
|
||||
prepaidOptions: params.prepaidOptions,
|
||||
version: params.version,
|
||||
});
|
||||
|
||||
console.log("attachBody", attachBody);
|
||||
|
||||
// Auto-enable if not explicitly set and all required data is present
|
||||
const shouldEnable =
|
||||
params.enabled !== undefined
|
||||
? params.enabled
|
||||
: !!(customerId && (productId || params.product) && attachBody);
|
||||
: !!(params.customerId && params.product && attachBody);
|
||||
|
||||
// Create a stable serialized key from attachBody (which already captures all dependencies)
|
||||
const queryKeyDeps = useMemo(() => JSON.stringify(attachBody), [attachBody]);
|
||||
|
||||
// Debounce the query key to delay API calls by 200ms
|
||||
// Debounce the query key to delay API calls by 150ms
|
||||
const [debouncedQueryKey, setDebouncedQueryKey] = useState(queryKeyDeps);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -65,7 +50,7 @@ export function useAttachPreview(params: AttachPreviewParams = {}) {
|
||||
return useQuery({
|
||||
queryKey: ["attach-checkout", debouncedQueryKey],
|
||||
queryFn: async () => {
|
||||
if (!attachBody || !customerId) {
|
||||
if (!attachBody || !params.customerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ProductV2 } from "@autumn/shared";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import { CusService } from "@/services/customers/CusService";
|
||||
@@ -73,7 +74,10 @@ export function useAttachProductMutation({
|
||||
if (onError) {
|
||||
onError(error);
|
||||
} else {
|
||||
toast.error("Failed to attach product");
|
||||
toast.error(
|
||||
(error as AxiosError<{ message: string }>)?.response?.data?.message ??
|
||||
"Failed to attach product",
|
||||
);
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -47,9 +47,9 @@ function TooltipContent({
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
`z-50 overflow-hidden rounded-sm px-4 py-2 text-xs max-w-56 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2
|
||||
`z-50 overflow-hidden rounded-lg px-4 py-2 text-xs max-w-56 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2
|
||||
|
||||
bg-white backdrop-blur-sm shadow-lg text-t2
|
||||
backdrop-blur-sm shadow-lg text-t2 border bg-outer-background
|
||||
`,
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const SheetCloseButton = ({ onClose }: SheetCloseButtonProps) => {
|
||||
onClick={onClose}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className="ring-offset-background focus:ring-ring absolute top-4 right-2 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
|
||||
className="ring-offset-background focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
|
||||
@@ -103,7 +103,12 @@ export const useHasBillingChanges = ({
|
||||
features,
|
||||
});
|
||||
|
||||
return !comparison.onlyEntsChanged;
|
||||
console.log("comparison", comparison);
|
||||
|
||||
const hasBillingChanges =
|
||||
!comparison.onlyEntsChanged || !comparison.freeTrialsSame;
|
||||
|
||||
return hasBillingChanges;
|
||||
}, [baseProduct, newProduct, features]);
|
||||
};
|
||||
|
||||
@@ -219,7 +224,6 @@ export const useSetCurrentItem = () => {
|
||||
|
||||
/**
|
||||
* Hook to check if the current product is the latest version.
|
||||
* Checks customizedProduct first, then fallbacks to product from store.
|
||||
*/
|
||||
export const useIsLatestVersion = (product: FrontendProduct) => {
|
||||
const { products = [] } = useProductsQuery();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
cusProductToProduct,
|
||||
type Entity,
|
||||
type FrontendProduct,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
mapToProductV2,
|
||||
@@ -13,35 +12,18 @@ import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
|
||||
interface AttachProductFormValues {
|
||||
customerId: string | null;
|
||||
productId: string;
|
||||
prepaidOptions: Record<string, number>;
|
||||
customizedProduct: FrontendProduct | null;
|
||||
customerProductId: string | null;
|
||||
selectedEntityId: string | null;
|
||||
}
|
||||
|
||||
interface AttachProductState extends AttachProductFormValues {
|
||||
// Actions
|
||||
setCustomerId: (customerId: string | null) => void;
|
||||
setProductId: (productId: string) => void;
|
||||
setPrepaidOptions: (options: Record<string, number>) => void;
|
||||
setCustomizedProduct: (
|
||||
product: {
|
||||
product: FrontendProduct;
|
||||
customer_product_id?: string | null;
|
||||
} | null,
|
||||
) => void;
|
||||
setSelectedEntityId: (entityId: string | null) => void;
|
||||
setFormValues: (values: Partial<AttachProductFormValues>) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const initialState: AttachProductFormValues = {
|
||||
customerId: null,
|
||||
productId: "",
|
||||
prepaidOptions: {},
|
||||
customizedProduct: null,
|
||||
customerProductId: null,
|
||||
selectedEntityId: null,
|
||||
};
|
||||
|
||||
@@ -49,31 +31,11 @@ export const useAttachProductStore = create<AttachProductState>((set) => ({
|
||||
...initialState,
|
||||
|
||||
setCustomerId: (customerId) => set({ customerId }),
|
||||
setProductId: (productId) => set({ productId }),
|
||||
setPrepaidOptions: (prepaidOptions) => set({ prepaidOptions }),
|
||||
setCustomizedProduct: (customizedProduct) =>
|
||||
set({
|
||||
customizedProduct: customizedProduct?.product ?? null,
|
||||
customerProductId: customizedProduct?.customer_product_id ?? null,
|
||||
}),
|
||||
setSelectedEntityId: (selectedEntityId) => set({ selectedEntityId }),
|
||||
|
||||
// Convenience method to set multiple values at once
|
||||
setFormValues: (values) => set(values),
|
||||
|
||||
reset: () => set(initialState),
|
||||
}));
|
||||
|
||||
// Convenience selector hook to get all form values
|
||||
export const useAttachProductFormValues = () =>
|
||||
useAttachProductStore((s) => ({
|
||||
customerId: s.customerId,
|
||||
productId: s.productId,
|
||||
prepaidOptions: s.prepaidOptions,
|
||||
customizedProduct: s.customizedProduct,
|
||||
customerProductId: s.customerProductId,
|
||||
}));
|
||||
|
||||
// Hook to sync entity_id between query params and store
|
||||
export const useEntity = () => {
|
||||
const [{ entity_id }, setQueryStates] = useQueryStates({
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import { useEffect } from "react";
|
||||
import { AttachProductForm } from "@/components/forms/attach-product/attach-product-form";
|
||||
import { SheetHeader, SheetSection } from "@/components/v2/sheets/InlineSheet";
|
||||
import { useProductStore } from "@/hooks/stores/useProductStore";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import { useCustomerContext } from "../../customer/CustomerContext";
|
||||
|
||||
export function AttachProductSheet() {
|
||||
const { customer } = useCustomerContext();
|
||||
const sheetType = useSheetStore((s) => s.type);
|
||||
const resetProductStore = useProductStore((s) => s.reset);
|
||||
//remove any stale customized product data from store
|
||||
useEffect(() => {
|
||||
if (sheetType !== "attach-product") {
|
||||
resetProductStore();
|
||||
}
|
||||
}, [sheetType, resetProductStore]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
XCircle,
|
||||
} from "@phosphor-icons/react";
|
||||
import { format } from "date-fns";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
// import { Badge } from "@/components/v2/Badge";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
@@ -23,12 +24,13 @@ import { SheetHeader, SheetSection } from "@/components/v2/sheets/InlineSheet";
|
||||
import { SheetFooter } from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import { useOrg } from "@/hooks/common/useOrg";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import { usePrepaidItems } from "@/hooks/stores/useProductStore";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import {
|
||||
useAttachProductStore,
|
||||
useSubscriptionById,
|
||||
} from "@/hooks/stores/useSubscriptionStore";
|
||||
useHasChanges,
|
||||
usePrepaidItems,
|
||||
useProductStore,
|
||||
} from "@/hooks/stores/useProductStore";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import { useSubscriptionById } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { pushPage } from "@/utils/genUtils";
|
||||
import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
@@ -42,20 +44,27 @@ export function SubscriptionDetailSheet() {
|
||||
const itemId = useSheetStore((s) => s.itemId);
|
||||
const setSheet = useSheetStore((s) => s.setSheet);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const resetProductStore = useProductStore((s) => s.reset);
|
||||
const sheetType = useSheetStore((s) => s.type);
|
||||
// Get edited product from store
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
const editedCustomerProductId = useAttachProductStore(
|
||||
(s) => s.customerProductId,
|
||||
);
|
||||
const hasChanges = useHasChanges();
|
||||
const storeProduct = useProductStore((s) => s.product);
|
||||
|
||||
// Check if the edited product is for this subscription
|
||||
const shouldShowEditedProduct =
|
||||
customizedProduct && editedCustomerProductId === itemId;
|
||||
// Check if there are changes in the product store
|
||||
const shouldShowEditedProduct = hasChanges && !!storeProduct;
|
||||
|
||||
// Get customer product and productV2 by itemId
|
||||
const { cusProduct, productV2 } = useSubscriptionById({ itemId });
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
sheetType !== "subscription-detail" &&
|
||||
sheetType !== "subscription-update"
|
||||
) {
|
||||
resetProductStore();
|
||||
}
|
||||
}, [sheetType, resetProductStore]);
|
||||
|
||||
// Check for prepaid items in the product (must be called before any returns)
|
||||
const prepaidItems = usePrepaidItems({ product: productV2 ?? undefined });
|
||||
const hasPrepaidItems = prepaidItems.length > 0;
|
||||
@@ -248,70 +257,72 @@ export function SubscriptionDetailSheet() {
|
||||
)}
|
||||
|
||||
{/* Edited Plan Items - Show pending changes */}
|
||||
{shouldShowEditedProduct && customizedProduct.items.length > 0 && (
|
||||
<SheetSection title="Edited Plan Items (Pending)">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-2 p-2 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
||||
<Info size={16} weight="duotone" className="text-blue-600" />
|
||||
<span className="text-xs text-t2">
|
||||
These changes are pending and will be applied when you save.
|
||||
</span>
|
||||
</div>
|
||||
{customizedProduct.items.map((item, index) => {
|
||||
const display = getProductItemDisplay({
|
||||
item,
|
||||
features,
|
||||
currency: org?.default_currency || "USD",
|
||||
fullDisplay: true,
|
||||
amountFormatOptions: { currencyDisplay: "narrowSymbol" },
|
||||
});
|
||||
{shouldShowEditedProduct &&
|
||||
storeProduct &&
|
||||
storeProduct.items.length > 0 && (
|
||||
<SheetSection title="Edited Plan Items (Pending)">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-2 p-2 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
||||
<Info size={16} weight="duotone" className="text-blue-600" />
|
||||
<span className="text-xs text-t2">
|
||||
These changes are pending and will be applied when you save.
|
||||
</span>
|
||||
</div>
|
||||
{storeProduct.items.map((item, index) => {
|
||||
const display = getProductItemDisplay({
|
||||
item,
|
||||
features,
|
||||
currency: org?.default_currency || "USD",
|
||||
fullDisplay: true,
|
||||
amountFormatOptions: { currencyDisplay: "narrowSymbol" },
|
||||
});
|
||||
|
||||
const isFeatureItem =
|
||||
item.type === ProductItemType.Feature ||
|
||||
item.type === ProductItemType.FeaturePrice;
|
||||
const isFeatureItem =
|
||||
item.type === ProductItemType.Feature ||
|
||||
item.type === ProductItemType.FeaturePrice;
|
||||
|
||||
// Find prepaid quantity from cusProduct.options
|
||||
const prepaidOption = cusProduct?.options?.find(
|
||||
(opt: FeatureOptions) => opt.feature_id === item.feature_id,
|
||||
);
|
||||
const prepaidQuantity = prepaidOption
|
||||
? prepaidOption.quantity / (item.billing_units || 1)
|
||||
: null;
|
||||
// Find prepaid quantity from cusProduct.options
|
||||
const prepaidOption = cusProduct?.options?.find(
|
||||
(opt: FeatureOptions) => opt.feature_id === item.feature_id,
|
||||
);
|
||||
const prepaidQuantity = prepaidOption
|
||||
? prepaidOption.quantity / (item.billing_units || 1)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.feature_id || item.price_id || index}
|
||||
className="flex items-start gap-2 p-2 rounded-lg bg-blue-500/5 border border-blue-500/20"
|
||||
>
|
||||
<CheckCircle
|
||||
size={16}
|
||||
weight="fill"
|
||||
className={cn(
|
||||
"text-blue-600 mt-0.5 shrink-0",
|
||||
!isFeatureItem && "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-t1 flex items-center gap-2 flex-wrap">
|
||||
<span>{display.primary_text}</span>
|
||||
{prepaidQuantity !== null && (
|
||||
<span className="text-t3 bg-background/50 rounded-sm px-2 py-0.5 text-xs font-normal">
|
||||
Qty: {prepaidQuantity}
|
||||
</span>
|
||||
return (
|
||||
<div
|
||||
key={item.feature_id || item.price_id || index}
|
||||
className="flex items-start gap-2 p-2 rounded-lg bg-blue-500/5 border border-blue-500/20"
|
||||
>
|
||||
<CheckCircle
|
||||
size={16}
|
||||
weight="fill"
|
||||
className={cn(
|
||||
"text-blue-600 mt-0.5 shrink-0",
|
||||
!isFeatureItem && "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-t1 flex items-center gap-2 flex-wrap">
|
||||
<span>{display.primary_text}</span>
|
||||
{prepaidQuantity !== null && (
|
||||
<span className="text-t3 bg-background/50 rounded-sm px-2 py-0.5 text-xs font-normal">
|
||||
Qty: {prepaidQuantity}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{display.secondary_text && (
|
||||
<div className="text-xs text-t3 mt-0.5">
|
||||
{display.secondary_text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{display.secondary_text && (
|
||||
<div className="text-xs text-t3 mt-0.5">
|
||||
{display.secondary_text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SheetSection>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SheetSection>
|
||||
)}
|
||||
|
||||
{/* Pricing Summary */}
|
||||
<SheetSection title="Pricing">
|
||||
@@ -385,10 +396,7 @@ export function SubscriptionDetailSheet() {
|
||||
<PencilSimple size={16} weight="duotone" />
|
||||
Edit Plan
|
||||
</Button>
|
||||
<UpdatePlanButton
|
||||
cusProduct={cusProduct}
|
||||
customizedProduct={customizedProduct}
|
||||
/>
|
||||
<UpdatePlanButton cusProduct={cusProduct} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,140 +1,149 @@
|
||||
import type { FrontendProduct } from "@autumn/shared";
|
||||
import type { FullCusProduct, ProductV2 } from "@autumn/shared";
|
||||
import { ArrowLeft } from "@phosphor-icons/react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { UpdateProductActions } from "@/components/forms/attach-product/update-product-actions";
|
||||
import { UpdateProductPrepaidOptions } from "@/components/forms/attach-product/update-product-prepaid-options";
|
||||
import { UpdateProductSummary } from "@/components/forms/attach-product/update-product-summary";
|
||||
import { useAttachPreview } from "@/components/forms/attach-product/use-attach-preview";
|
||||
import { useAttachProductForm } from "@/components/forms/attach-product/use-attach-product-form";
|
||||
import {
|
||||
type UseAttachProductForm,
|
||||
useAttachProductForm,
|
||||
} from "@/components/forms/attach-product/use-attach-product-form";
|
||||
import { FormWrapper } from "@/components/general/form/form-wrapper";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { SheetHeader, SheetSection } from "@/components/v2/sheets/InlineSheet";
|
||||
import { usePrepaidItems } from "@/hooks/stores/useProductStore";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import {
|
||||
useAttachProductStore,
|
||||
useSubscriptionById,
|
||||
} from "@/hooks/stores/useSubscriptionStore";
|
||||
usePrepaidItems,
|
||||
useProductStore,
|
||||
} from "@/hooks/stores/useProductStore";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import { useSubscriptionById } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
|
||||
export function SubscriptionUpdateSheet() {
|
||||
const FormContent = ({
|
||||
productV2,
|
||||
cusProduct,
|
||||
form,
|
||||
initialPrepaidOptions,
|
||||
}: {
|
||||
productV2: ProductV2;
|
||||
cusProduct: FullCusProduct;
|
||||
form: UseAttachProductForm;
|
||||
initialPrepaidOptions: Record<string, number>;
|
||||
}) => {
|
||||
const { customer } = useCusQuery();
|
||||
const itemId = useSheetStore((s) => s.itemId);
|
||||
const setSheet = useSheetStore((s) => s.setSheet);
|
||||
const customerId = customer?.id;
|
||||
const storeProduct = useProductStore((s) => s.product);
|
||||
const product = storeProduct?.id ? storeProduct : (productV2 ?? undefined);
|
||||
const entityId = cusProduct?.entity_id ?? undefined;
|
||||
const prepaidItems = usePrepaidItems({ product });
|
||||
|
||||
// Get edited product from store
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
const setCustomerId = useAttachProductStore((s) => s.setCustomerId);
|
||||
const setFormValues = useAttachProductStore((s) => s.setFormValues);
|
||||
const setProductId = useAttachProductStore((s) => s.setProductId);
|
||||
const productId = useAttachProductStore((s) => s.productId);
|
||||
const setCustomizedProduct = useAttachProductStore(
|
||||
(s) => s.setCustomizedProduct,
|
||||
const prepaidOptions = form.state.values.prepaidOptions;
|
||||
|
||||
const previewQuery = useAttachPreview({
|
||||
customerId,
|
||||
product,
|
||||
entityId,
|
||||
prepaidOptions: prepaidOptions ?? undefined,
|
||||
version: product?.version,
|
||||
});
|
||||
|
||||
if (prepaidItems.length > 0) {
|
||||
const hasUnsetPrepaidQuantity = prepaidItems.some((item) => {
|
||||
const quantity = prepaidOptions?.[item.feature_id as string];
|
||||
return quantity === undefined || quantity === null;
|
||||
});
|
||||
|
||||
if (hasUnsetPrepaidQuantity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if there are any changes from initial values
|
||||
const hasQuantityChanges = prepaidItems.some((item) => {
|
||||
const currentQuantity = prepaidOptions?.[item.feature_id as string];
|
||||
const initialQuantity = initialPrepaidOptions[item.feature_id as string];
|
||||
return currentQuantity !== initialQuantity;
|
||||
});
|
||||
|
||||
if (!hasQuantityChanges && !storeProduct?.id) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateProductSummary
|
||||
previewData={previewQuery.data}
|
||||
isLoading={previewQuery.isLoading}
|
||||
product={product}
|
||||
/>
|
||||
<UpdateProductActions
|
||||
product={product}
|
||||
customerId={customerId}
|
||||
entityId={entityId}
|
||||
previewData={previewQuery.data}
|
||||
isPreviewLoading={previewQuery.isLoading}
|
||||
form={form}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function SheetContent({
|
||||
cusProduct,
|
||||
productV2,
|
||||
|
||||
itemId,
|
||||
}: {
|
||||
cusProduct: FullCusProduct;
|
||||
productV2: ProductV2;
|
||||
itemId: string | null;
|
||||
}) {
|
||||
const setSheet = useSheetStore((s) => s.setSheet);
|
||||
const storeProduct = useProductStore((s) => s.product);
|
||||
|
||||
const form = useAttachProductForm({
|
||||
initialProductId: cusProduct?.product.id ?? undefined,
|
||||
});
|
||||
|
||||
// Memoize initial prepaid options from cusProduct
|
||||
const initialPrepaidOptions = useMemo(
|
||||
() =>
|
||||
cusProduct.options.reduce(
|
||||
(acc, option) => {
|
||||
acc[option.feature_id] = option.quantity;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
[cusProduct.options],
|
||||
);
|
||||
|
||||
const { cusProduct, productV2: product } = useSubscriptionById({ itemId });
|
||||
const product = storeProduct?.id ? storeProduct : (productV2 ?? undefined);
|
||||
const prepaidItems = usePrepaidItems({ product });
|
||||
|
||||
setProductId(cusProduct?.product.id ?? "");
|
||||
// This gets the prepaid items from the product, sees if there are existing quantities from the cusProduct/subscription, and sets them if so
|
||||
useEffect(() => {
|
||||
// Build prepaid options based on current product's prepaid items
|
||||
const newPrepaidOptions = prepaidItems.reduce(
|
||||
(acc, item) => {
|
||||
const featureId = item.feature_id as string;
|
||||
// Use initial value if this feature existed in original, otherwise undefined
|
||||
acc[featureId] = initialPrepaidOptions[featureId] ?? undefined;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number | undefined>,
|
||||
);
|
||||
|
||||
const { isLoading: isPreviewLoading } = useAttachPreview();
|
||||
form.setFieldValue(
|
||||
"prepaidOptions",
|
||||
newPrepaidOptions as Record<string, number>,
|
||||
);
|
||||
}, [prepaidItems, initialPrepaidOptions, form]);
|
||||
|
||||
const handleBackToDetail = () => {
|
||||
setSheet({ type: "subscription-detail", itemId });
|
||||
|
||||
setCustomizedProduct({
|
||||
product: product as unknown as FrontendProduct,
|
||||
});
|
||||
};
|
||||
const form = useAttachProductForm({
|
||||
initialProductId: cusProduct?.id ?? undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!customer?.id && !customer?.internal_id) return;
|
||||
|
||||
const customerId = customer.id || customer.internal_id;
|
||||
setCustomerId(customerId);
|
||||
|
||||
// Subscribe to form changes
|
||||
const subscription = form.store.subscribe(() => {
|
||||
const values = form.store.state.values;
|
||||
|
||||
// Sync form values to store
|
||||
setFormValues({
|
||||
productId: values.productId ?? productId,
|
||||
prepaidOptions: values.prepaidOptions ?? {},
|
||||
});
|
||||
});
|
||||
|
||||
return () => subscription();
|
||||
}, [customer, form.store, setCustomerId, setFormValues, productId]);
|
||||
// const cusProduct = useMemo(() => {
|
||||
// if (!itemId || !customer?.customer_products) return null;
|
||||
// return customer.customer_products.find(
|
||||
// (p: FullCusProduct) =>
|
||||
// p.id === itemId || p.internal_product_id === itemId,
|
||||
// );
|
||||
// }, [itemId, customer?.customer_products]);
|
||||
|
||||
// const entity = customer?.entities?.find(
|
||||
// (e: Entity) =>
|
||||
// e.internal_id === cusProduct?.internal_entity_id ||
|
||||
// e.id === cusProduct?.entity_id,
|
||||
// );
|
||||
|
||||
// const customerId = customer?.id || customer?.internal_id;
|
||||
// const entityId = entity ? entity.id || entity.internal_id : undefined;
|
||||
|
||||
// Get prepaid items from the customized product
|
||||
const prepaidItems = usePrepaidItems({
|
||||
product: product ?? undefined,
|
||||
});
|
||||
|
||||
// Create form with prepopulated values
|
||||
|
||||
// Initialize store and form on mount
|
||||
// useEffect(() => {
|
||||
// if (customerId && customizedProduct) {
|
||||
// setCustomerId(customerId);
|
||||
// setFormValues({
|
||||
// productId: customizedProduct.id,
|
||||
// prepaidOptions: initialPrepaidOptions,
|
||||
// });
|
||||
|
||||
// // Set form values
|
||||
// form.setFieldValue("productId", customizedProduct.id);
|
||||
// form.setFieldValue("prepaidOptions", initialPrepaidOptions);
|
||||
// }
|
||||
// }, [
|
||||
// customerId,
|
||||
// customizedProduct,
|
||||
// setCustomerId,
|
||||
// setFormValues,
|
||||
// initialPrepaidOptions,
|
||||
// ]);
|
||||
|
||||
// Sync form changes to store
|
||||
|
||||
if (!cusProduct) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<SheetHeader
|
||||
title="Update Plan"
|
||||
description="Loading plan information..."
|
||||
>
|
||||
<Button
|
||||
variant="skeleton"
|
||||
size="sm"
|
||||
onClick={handleBackToDetail}
|
||||
className="mt-2 w-fit"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
Back to Details
|
||||
</Button>
|
||||
</SheetHeader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormWrapper form={form}>
|
||||
@@ -155,22 +164,87 @@ export function SubscriptionUpdateSheet() {
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{prepaidItems.length > 0 && (
|
||||
<SheetSection title="Prepaid Quantities" withSeparator={false}>
|
||||
<UpdateProductPrepaidOptions form={form} />
|
||||
</SheetSection>
|
||||
)}
|
||||
<UpdateProductSummary />
|
||||
<UpdateProductActions
|
||||
customerId={customer?.id || customer?.internal_id}
|
||||
entityId={cusProduct.entity_id ?? undefined}
|
||||
isPreviewLoading={isPreviewLoading}
|
||||
/>
|
||||
<form.Subscribe
|
||||
selector={(state) => ({
|
||||
prepaidOptions: state.values.prepaidOptions,
|
||||
})}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
{prepaidItems.length > 0 && (
|
||||
<SheetSection
|
||||
title="Prepaid Quantities"
|
||||
withSeparator={false}
|
||||
>
|
||||
<UpdateProductPrepaidOptions form={form} />
|
||||
</SheetSection>
|
||||
)}
|
||||
<FormContent
|
||||
productV2={productV2}
|
||||
cusProduct={cusProduct}
|
||||
form={form}
|
||||
initialPrepaidOptions={initialPrepaidOptions}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
function resetProductStore() {
|
||||
throw new Error("Function not implemented.");
|
||||
|
||||
export function SubscriptionUpdateSheet() {
|
||||
const itemId = useSheetStore((s) => s.itemId);
|
||||
const setSheet = useSheetStore((s) => s.setSheet);
|
||||
|
||||
const { cusProduct, productV2 } = useSubscriptionById({ itemId });
|
||||
|
||||
const entityId = cusProduct?.entity_id ?? undefined;
|
||||
const sheetType = useSheetStore((s) => s.type);
|
||||
const resetProductStore = useProductStore((s) => s.reset);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
sheetType !== "subscription-detail" &&
|
||||
sheetType !== "subscription-update"
|
||||
) {
|
||||
resetProductStore();
|
||||
}
|
||||
}, [sheetType, resetProductStore]);
|
||||
|
||||
// Load subscription's product into store on mount
|
||||
|
||||
if (!cusProduct) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<SheetHeader
|
||||
title="Update Plan"
|
||||
description="Loading plan information..."
|
||||
>
|
||||
<Button
|
||||
variant="skeleton"
|
||||
size="sm"
|
||||
onClick={() => setSheet({ type: "subscription-detail", itemId })}
|
||||
className="mt-2 w-fit"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
Back to Details
|
||||
</Button>
|
||||
</SheetHeader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!productV2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SheetContent
|
||||
cusProduct={cusProduct}
|
||||
productV2={productV2}
|
||||
itemId={itemId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { FullCusProduct, ProductV2 } from "@autumn/shared";
|
||||
import type { FullCusProduct } from "@autumn/shared";
|
||||
import { CheckCircle } from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
|
||||
interface UpdatePlanButtonProps {
|
||||
cusProduct: FullCusProduct;
|
||||
customizedProduct: ProductV2;
|
||||
}
|
||||
|
||||
export function UpdatePlanButton({ cusProduct }: UpdatePlanButtonProps) {
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
} from "@autumn/shared";
|
||||
import type { Row } from "@tanstack/react-table";
|
||||
import type { z } from "zod/v4";
|
||||
import { MiniCopyButton } from "@/components/v2/buttons/CopyButton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { MiniCopyButton } from "@/components/v2/buttons/CopyButton";
|
||||
} from "@/components/v2/tooltips/Tooltip";
|
||||
import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { CustomerProductsStatus } from "../customer-products/CustomerProductsStatus";
|
||||
import { CustomerListRowToolbar } from "./CustomerListRowToolbar";
|
||||
@@ -47,66 +47,6 @@ const getCusProductsInfo = ({
|
||||
return <span className="text-t3">—</span>;
|
||||
}
|
||||
|
||||
// const getProductBadge = ({
|
||||
// cusProduct,
|
||||
// versionCounts,
|
||||
// }: {
|
||||
// cusProduct: FullCusProduct;
|
||||
// versionCounts: Record<string, number>;
|
||||
// }) => {
|
||||
// const name = cusProduct.product.name;
|
||||
// const status = cusProduct.status;
|
||||
|
||||
// const versionCount = versionCounts[cusProduct.product.id];
|
||||
// const version = cusProduct.product.version;
|
||||
|
||||
// const prodName = (
|
||||
// <>
|
||||
// {name}
|
||||
// {versionCount > 1 && (
|
||||
// <Badge
|
||||
// variant="outline"
|
||||
// className="text-xs bg-stone-50 text-t3 px-2 ml-2 font-mono py-0"
|
||||
// >
|
||||
// v{version}
|
||||
// </Badge>
|
||||
// )}
|
||||
// </>
|
||||
// );
|
||||
|
||||
// if (status === CusProductStatus.PastDue) {
|
||||
// return (
|
||||
// <>
|
||||
// <span>{prodName}</span>{" "}
|
||||
// <Badge variant="status" className="bg-red-500">
|
||||
// Past Due
|
||||
// </Badge>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
// if (cusProduct.canceled_at) {
|
||||
// return (
|
||||
// <>
|
||||
// <span>{prodName}</span>{" "}
|
||||
// <Badge variant="status" className="bg-yellow-500">
|
||||
// Canceled
|
||||
// </Badge>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
// if (cusProduct.trial_ends_at && !unixHasPassed(cusProduct.trial_ends_at)) {
|
||||
// return (
|
||||
// <>
|
||||
// <span>{prodName}</span>{" "}
|
||||
// <Badge variant="status" className="bg-lime-500">
|
||||
// Trial
|
||||
// </Badge>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
// return <span>{prodName}</span>;
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="flex ">
|
||||
{activeProducts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FullCusProduct, isTrialing } from "@autumn/shared";
|
||||
import type { Row, Table } from "@tanstack/react-table";
|
||||
import { Delete } from "lucide-react";
|
||||
import { ArrowRightLeft, Delete } from "lucide-react";
|
||||
import { TableDropdownMenuCell } from "@/components/general/table/table-dropdown-menu-cell";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { createDateTimeColumn } from "@/views/customers2/utils/ColumnHelpers";
|
||||
@@ -68,12 +68,25 @@ export const CustomerProductsColumns = [
|
||||
}) => {
|
||||
const meta = table.options.meta as {
|
||||
onCancelClick?: (product: FullCusProduct) => void;
|
||||
onTransferClick?: (product: FullCusProduct) => void;
|
||||
hasEntities?: boolean;
|
||||
};
|
||||
|
||||
if (!meta?.onCancelClick) return null;
|
||||
|
||||
return (
|
||||
<TableDropdownMenuCell>
|
||||
{meta.hasEntities && meta.onTransferClick && (
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
meta.onTransferClick?.(row.original);
|
||||
}}
|
||||
>
|
||||
<ArrowRightLeft size={16} /> Transfer
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 text-xs text-red-500 dark:text-red-400"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CustomerProductPrice } from "./CustomerProductPrice";
|
||||
import { CustomerProductsColumns } from "./CustomerProductsColumns";
|
||||
import { filterCustomerProductsByEntity } from "./customerProductsTableFilters";
|
||||
import { ShowExpiredActionButton } from "./ShowExpiredActionButton";
|
||||
import { TransferProductDialog } from "./TransferProductDialog";
|
||||
|
||||
export function CustomerProductsTable() {
|
||||
const { customer, isLoading } = useCusQuery();
|
||||
@@ -29,6 +30,7 @@ export function CustomerProductsTable() {
|
||||
);
|
||||
|
||||
const [cancelOpen, setCancelOpen] = useState(false);
|
||||
const [transferOpen, setTransferOpen] = useState(false);
|
||||
const [selectedProduct, setSelectedProduct] = useState<FullCusProduct | null>(
|
||||
null,
|
||||
);
|
||||
@@ -78,6 +80,25 @@ export function CustomerProductsTable() {
|
||||
|
||||
const entityProductsTableColumns = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: "Plan",
|
||||
accessorKey: "plan",
|
||||
cell: ({ row }: { row: Row<FullCusProduct> }) => {
|
||||
const quantity = row.original.quantity;
|
||||
const showQuantity = quantity && quantity > 1;
|
||||
|
||||
return (
|
||||
<div className="font-semibold flex items-center gap-2 text-t1">
|
||||
{row.original.product.name}
|
||||
{showQuantity && (
|
||||
<div className="text-t3 bg-muted rounded-sm p-1 py-0">
|
||||
{quantity}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Entity",
|
||||
accessorKey: "entity",
|
||||
@@ -101,7 +122,7 @@ export function CustomerProductsTable() {
|
||||
<Button
|
||||
variant="skeleton"
|
||||
onClick={handleEntityClick}
|
||||
className="text-t1 font-medium hover:text-purple-600 cursor-pointer max-w-full px-0! hover:bg-transparent active:bg-transparent active:border-none"
|
||||
className="font-medium hover:text-purple-600 cursor-pointer max-w-full px-0! hover:bg-transparent active:bg-transparent active:border-none"
|
||||
>
|
||||
<span className="truncate w-full">
|
||||
{entity.name || entity.id || entity.internal_id}
|
||||
@@ -110,25 +131,7 @@ export function CustomerProductsTable() {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
cell: ({ row }: { row: Row<FullCusProduct> }) => {
|
||||
const quantity = row.original.quantity;
|
||||
const showQuantity = quantity && quantity > 1;
|
||||
|
||||
return (
|
||||
<div className="font-semibold flex items-center gap-2">
|
||||
{row.original.product.name}
|
||||
{showQuantity && (
|
||||
<div className="text-t3 bg-muted rounded-sm p-1 py-0">
|
||||
{quantity}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Price",
|
||||
accessorKey: "price",
|
||||
@@ -150,6 +153,11 @@ export function CustomerProductsTable() {
|
||||
setCancelOpen(true);
|
||||
};
|
||||
|
||||
const handleTransferClick = (product: FullCusProduct) => {
|
||||
setSelectedProduct(product);
|
||||
setTransferOpen(true);
|
||||
};
|
||||
|
||||
const handleRowClick = (cusProduct: FullCusProduct) => {
|
||||
setSheet({
|
||||
type: "subscription-detail",
|
||||
@@ -157,6 +165,8 @@ export function CustomerProductsTable() {
|
||||
});
|
||||
};
|
||||
|
||||
const hasEntities = customer.entities.length > 0;
|
||||
|
||||
const enableSorting = false;
|
||||
const table = useCustomerTable({
|
||||
data: displayedProducts,
|
||||
@@ -166,6 +176,8 @@ export function CustomerProductsTable() {
|
||||
enableGlobalFilter: true,
|
||||
meta: {
|
||||
onCancelClick: handleCancelClick,
|
||||
onTransferClick: handleTransferClick,
|
||||
hasEntities,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -178,6 +190,8 @@ export function CustomerProductsTable() {
|
||||
enableGlobalFilter: true,
|
||||
meta: {
|
||||
onCancelClick: handleCancelClick,
|
||||
onTransferClick: handleTransferClick,
|
||||
hasEntities,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -192,11 +206,18 @@ export function CustomerProductsTable() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{selectedProduct && (
|
||||
<CancelProductDialog
|
||||
cusProduct={selectedProduct}
|
||||
open={cancelOpen}
|
||||
setOpen={setCancelOpen}
|
||||
/>
|
||||
<>
|
||||
<CancelProductDialog
|
||||
cusProduct={selectedProduct}
|
||||
open={cancelOpen}
|
||||
setOpen={setCancelOpen}
|
||||
/>
|
||||
<TransferProductDialog
|
||||
cusProduct={selectedProduct}
|
||||
open={transferOpen}
|
||||
setOpen={setTransferOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Table.Provider
|
||||
config={{
|
||||
@@ -259,6 +280,7 @@ export function CustomerProductsTable() {
|
||||
enableSorting,
|
||||
isLoading,
|
||||
onRowClick: handleRowClick,
|
||||
emptyStateText: "No entity-level plans found",
|
||||
}}
|
||||
>
|
||||
<Table.Container>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import type { Entity, FullCusProduct } from "@autumn/shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/v2/dialogs/Dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/v2/selects/Select";
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { getBackendErr } from "@/utils/genUtils";
|
||||
import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
|
||||
const CUSTOMER_LEVEL_VALUE = "__customer__";
|
||||
|
||||
export const TransferProductDialog = ({
|
||||
cusProduct,
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
cusProduct: FullCusProduct;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) => {
|
||||
const { customer, refetch } = useCusQuery();
|
||||
const axiosInstance = useAxiosInstance();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<string>("");
|
||||
|
||||
const filteredEntities = customer.entities.filter(
|
||||
(entity: Entity) => entity.internal_id !== cusProduct.internal_entity_id,
|
||||
);
|
||||
|
||||
// Check if product is currently on an entity
|
||||
const isOnEntity = !!cusProduct.entity_id || !!cusProduct.internal_entity_id;
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSelectedValue("");
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleTransfer = async () => {
|
||||
if (!selectedValue) {
|
||||
toast.error("Please select a destination");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const fromEntity = customer.entities.find(
|
||||
(e: Entity) => e.internal_id === cusProduct.internal_entity_id,
|
||||
);
|
||||
|
||||
const isMovingToCustomer = selectedValue === CUSTOMER_LEVEL_VALUE;
|
||||
const toEntity = isMovingToCustomer
|
||||
? null
|
||||
: customer.entities.find((e: Entity) => e.id === selectedValue);
|
||||
|
||||
await axiosInstance.post(
|
||||
`/v1/customers/${cusProduct.customer_id}/transfer`,
|
||||
{
|
||||
from_entity_id: fromEntity?.id,
|
||||
to_entity_id: isMovingToCustomer ? null : toEntity?.id,
|
||||
product_id: cusProduct.product_id,
|
||||
},
|
||||
);
|
||||
|
||||
await refetch();
|
||||
toast.success("Plan transferred successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(getBackendErr(error, "Failed to transfer plan"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
className="w-md bg-card"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Transfer Plan</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-t2">
|
||||
{isOnEntity
|
||||
? "Transfer this plan to another entity or move it back to the customer level."
|
||||
: "Transfer this plan to an entity."}
|
||||
</p>
|
||||
|
||||
<Select value={selectedValue} onValueChange={setSelectedValue}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select destination" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* Move to Customer option - only show if product is currently on an entity */}
|
||||
{isOnEntity && (
|
||||
<SelectItem value={CUSTOMER_LEVEL_VALUE}>
|
||||
<span className="font-medium">Move to Customer</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
|
||||
{/* Entity options */}
|
||||
{filteredEntities.map((entity: Entity) => (
|
||||
<SelectItem
|
||||
key={entity.id || entity.internal_id}
|
||||
value={entity.id}
|
||||
>
|
||||
<div className="flex gap-2 items-center min-w-0">
|
||||
{entity.name && (
|
||||
<span className="truncate max-w-[120px]">
|
||||
{entity.name}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate text-t3 font-mono text-xs">
|
||||
{entity.id || entity.internal_id}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={handleTransfer}
|
||||
isLoading={loading}
|
||||
disabled={!selectedValue}
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
useProductStore,
|
||||
} from "@/hooks/stores/useProductStore";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import { useAttachProductStore } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { pushPage } from "@/utils/genUtils";
|
||||
import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
import { useCusProductQuery } from "@/views/customers/customer/product/hooks/useCusProductQuery";
|
||||
@@ -24,9 +23,6 @@ export const CustomerPlanEditorBar = () => {
|
||||
const { customer } = useCusQuery();
|
||||
const hasChanges = useHasChanges();
|
||||
const isLatestVersion = useIsLatestVersion(product);
|
||||
const setCustomizedProduct = useAttachProductStore(
|
||||
(s) => s.setCustomizedProduct,
|
||||
);
|
||||
const { type: sheetType } = useSheetStore();
|
||||
|
||||
const [queryStates, setQueryStates] = useQueryStates({
|
||||
@@ -72,24 +68,17 @@ export const CustomerPlanEditorBar = () => {
|
||||
{ history: "replace" },
|
||||
);
|
||||
};
|
||||
if (sheetType) return null;
|
||||
|
||||
if (!changesMade) {
|
||||
return <GoBackBar returnToCustomer={returnToCustomer} />;
|
||||
}
|
||||
|
||||
// const selectedEntity = useSelectedEntity();
|
||||
|
||||
const handleSaveClicked = async () => {
|
||||
setCustomizedProduct({
|
||||
product,
|
||||
customer_product_id: queryStates.id || null,
|
||||
});
|
||||
// Product is already in store, just navigate back
|
||||
returnToCustomer();
|
||||
};
|
||||
|
||||
if (sheetType) return null;
|
||||
if (isLoading) return null;
|
||||
|
||||
// if (!hasChanges && activeVersion === product.version) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
@@ -5,11 +5,9 @@ import { useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Link } from "react-router";
|
||||
import { useCustomerBalanceSheetStore } from "@/hooks/stores/useCustomerBalanceSheetStore";
|
||||
import { useHasChanges } from "@/hooks/stores/useProductStore";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import {
|
||||
useAttachProductStore,
|
||||
useEntity,
|
||||
} from "@/hooks/stores/useSubscriptionStore";
|
||||
import { useEntity } from "@/hooks/stores/useSubscriptionStore";
|
||||
import { pushPage } from "@/utils/genUtils";
|
||||
import ErrorScreen from "@/views/general/ErrorScreen";
|
||||
import LoadingScreen from "@/views/general/LoadingScreen";
|
||||
@@ -37,7 +35,7 @@ export default function CustomerView2() {
|
||||
const closeSheet = useCustomerBalanceSheetStore((s) => s.closeSheet);
|
||||
const sheetType = useSheetStore((s) => s.type);
|
||||
const closeProductSheet = useSheetStore((s) => s.closeSheet);
|
||||
const customizedProduct = useAttachProductStore((s) => s.customizedProduct);
|
||||
const hasChanges = useHasChanges();
|
||||
|
||||
// Close modal on mount
|
||||
useEffect(() => {
|
||||
@@ -123,10 +121,10 @@ export default function CustomerView2() {
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-background/90"
|
||||
className="fixed inset-0 bg-background/60"
|
||||
style={{ zIndex: 40 }}
|
||||
onMouseDown={() => {
|
||||
!customizedProduct && closeProductSheet();
|
||||
!hasChanges && closeProductSheet();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -67,10 +67,6 @@ export default function PlanEditorView() {
|
||||
<ConfirmNewVersionDialog
|
||||
open={showNewVersionDialog}
|
||||
setOpen={setShowNewVersionDialog}
|
||||
onVersionCreated={() => {
|
||||
// Reset sheet when new version is created
|
||||
setSheet({ type: "edit-plan" });
|
||||
}}
|
||||
/>
|
||||
<PlanEditor />
|
||||
</ProductContext.Provider>
|
||||
|
||||
@@ -75,8 +75,8 @@ export const ProductSheets = () => {
|
||||
return <NewFeatureSheet />;
|
||||
case "select-feature":
|
||||
return <SelectFeatureSheet />;
|
||||
default:
|
||||
return <EditPlanSheet />;
|
||||
// default:
|
||||
// return <EditPlanSheet />;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user