diff --git a/vite/src/views/customers2/components/sheets/SubscriptionDetailSheet.tsx b/vite/src/views/customers2/components/sheets/SubscriptionDetailSheet.tsx
index 57c37f744..caec9f0fb 100644
--- a/vite/src/views/customers2/components/sheets/SubscriptionDetailSheet.tsx
+++ b/vite/src/views/customers2/components/sheets/SubscriptionDetailSheet.tsx
@@ -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 && (
-
-
-
-
-
- These changes are pending and will be applied when you save.
-
-
- {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 && (
+
+
+
+
+
+ These changes are pending and will be applied when you save.
+
+
+ {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 (
-
-
-
-
-
{display.primary_text}
- {prepaidQuantity !== null && (
-
- Qty: {prepaidQuantity}
-
+ return (
+
+
+
+
+ {display.primary_text}
+ {prepaidQuantity !== null && (
+
+ Qty: {prepaidQuantity}
+
+ )}
+
+ {display.secondary_text && (
+
+ {display.secondary_text}
+
)}
- {display.secondary_text && (
-
- {display.secondary_text}
-
- )}
-
- );
- })}
-
-
- )}
+ );
+ })}
+
+
+ )}
{/* Pricing Summary */}
@@ -385,10 +396,7 @@ export function SubscriptionDetailSheet() {
Edit Plan
-
+
>
) : (
<>
diff --git a/vite/src/views/customers2/components/sheets/SubscriptionUpdateSheet.tsx b/vite/src/views/customers2/components/sheets/SubscriptionUpdateSheet.tsx
index 146e08888..6d809e845 100644
--- a/vite/src/views/customers2/components/sheets/SubscriptionUpdateSheet.tsx
+++ b/vite/src/views/customers2/components/sheets/SubscriptionUpdateSheet.tsx
@@ -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;
+}) => {
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 (
+ <>
+
+
+ >
+ );
+};
+
+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,
+ ),
+ [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,
+ );
- const { isLoading: isPreviewLoading } = useAttachPreview();
+ form.setFieldValue(
+ "prepaidOptions",
+ newPrepaidOptions as Record,
+ );
+ }, [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 (
-
-
-
-
-
- );
- }
return (
@@ -155,22 +164,87 @@ export function SubscriptionUpdateSheet() {
- {prepaidItems.length > 0 && (
-
-
-
- )}
-
-
+
({
+ prepaidOptions: state.values.prepaidOptions,
+ })}
+ >
+ {() => (
+ <>
+ {prepaidItems.length > 0 && (
+
+
+
+ )}
+
+ >
+ )}
+
);
}
-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 (
+
+
+
+
+
+ );
+ }
+
+ if (!productV2) {
+ return null;
+ }
+
+ return (
+
+ );
}
diff --git a/vite/src/views/customers2/components/sheets/UpdatePlanButton.tsx b/vite/src/views/customers2/components/sheets/UpdatePlanButton.tsx
index 09e5756b9..23325b83b 100644
--- a/vite/src/views/customers2/components/sheets/UpdatePlanButton.tsx
+++ b/vite/src/views/customers2/components/sheets/UpdatePlanButton.tsx
@@ -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) {
diff --git a/vite/src/views/customers2/components/table/customer-list/CustomerListColumns.tsx b/vite/src/views/customers2/components/table/customer-list/CustomerListColumns.tsx
index fabb48c8f..96e7f620d 100644
--- a/vite/src/views/customers2/components/table/customer-list/CustomerListColumns.tsx
+++ b/vite/src/views/customers2/components/table/customer-list/CustomerListColumns.tsx
@@ -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 —;
}
- // const getProductBadge = ({
- // cusProduct,
- // versionCounts,
- // }: {
- // cusProduct: FullCusProduct;
- // versionCounts: Record;
- // }) => {
- // 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 && (
- //
- // v{version}
- //
- // )}
- // >
- // );
-
- // if (status === CusProductStatus.PastDue) {
- // return (
- // <>
- // {prodName}{" "}
- //
- // Past Due
- //
- // >
- // );
- // }
- // if (cusProduct.canceled_at) {
- // return (
- // <>
- // {prodName}{" "}
- //
- // Canceled
- //
- // >
- // );
- // }
- // if (cusProduct.trial_ends_at && !unixHasPassed(cusProduct.trial_ends_at)) {
- // return (
- // <>
- // {prodName}{" "}
- //
- // Trial
- //
- // >
- // );
- // }
- // return {prodName};
- // };
-
return (
{activeProducts
diff --git a/vite/src/views/customers2/components/table/customer-products/CustomerProductsColumns.tsx b/vite/src/views/customers2/components/table/customer-products/CustomerProductsColumns.tsx
index a73cc9671..0fdc5395f 100644
--- a/vite/src/views/customers2/components/table/customer-products/CustomerProductsColumns.tsx
+++ b/vite/src/views/customers2/components/table/customer-products/CustomerProductsColumns.tsx
@@ -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 (
+ {meta.hasEntities && meta.onTransferClick && (
+ {
+ e.stopPropagation();
+ meta.onTransferClick?.(row.original);
+ }}
+ >
+ Transfer
+
+ )}
{
diff --git a/vite/src/views/customers2/components/table/customer-products/CustomerProductsTable.tsx b/vite/src/views/customers2/components/table/customer-products/CustomerProductsTable.tsx
index 82fc7599a..a32a456a7 100644
--- a/vite/src/views/customers2/components/table/customer-products/CustomerProductsTable.tsx
+++ b/vite/src/views/customers2/components/table/customer-products/CustomerProductsTable.tsx
@@ -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(
null,
);
@@ -78,6 +80,25 @@ export function CustomerProductsTable() {
const entityProductsTableColumns = useMemo(
() => [
+ {
+ header: "Plan",
+ accessorKey: "plan",
+ cell: ({ row }: { row: Row }) => {
+ const quantity = row.original.quantity;
+ const showQuantity = quantity && quantity > 1;
+
+ return (
+
+ {row.original.product.name}
+ {showQuantity && (
+
+ {quantity}
+
+ )}
+
+ );
+ },
+ },
{
header: "Entity",
accessorKey: "entity",
@@ -101,7 +122,7 @@ export function CustomerProductsTable() {