diff --git a/server/src/internal/customers/handlers/handleTransferProductV2.ts b/server/src/internal/customers/handlers/handleTransferProductV2.ts index 46022c3db..aecaf0b5e 100644 --- a/server/src/internal/customers/handlers/handleTransferProductV2.ts +++ b/server/src/internal/customers/handlers/handleTransferProductV2.ts @@ -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({ diff --git a/shared/utils/productUtils/freeTrialUtils.ts b/shared/utils/productUtils/freeTrialUtils.ts index 53aade237..b8704d58e 100644 --- a/shared/utils/productUtils/freeTrialUtils.ts +++ b/shared/utils/productUtils/freeTrialUtils.ts @@ -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; }; diff --git a/vite/src/components/forms/attach-product/attach-confirmation-info.tsx b/vite/src/components/forms/attach-product/attach-confirmation-info.tsx index 821ecc5b9..ae02ce155 100644 --- a/vite/src/components/forms/attach-product/attach-confirmation-info.tsx +++ b/vite/src/components/forms/attach-product/attach-confirmation-info.tsx @@ -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( - - This plan has been customized for this customer - , - ); - } if (!isLatestVersion) { boxes.push( @@ -78,13 +69,14 @@ export const AttachConfirmationInfo = () => { | "upgrade" | "downgrade" | "cancel" + | "new" | string; switch (scenario) { case "upgrade": boxes.push( - This upgrade will replace the customer's current plan:{" "} + This upgrade will immediately replace the customer's current plan:{" "} {previewData.current_product.name} , ); @@ -105,11 +97,10 @@ export const AttachConfirmationInfo = () => { , ); break; - default: + case "new": boxes.push( - - This will replace the customer's current plan:{" "} - {previewData.current_product.name} + + This will be enabled alongside existing plans{" "} , ); } diff --git a/vite/src/components/forms/attach-product/attach-feature-preview.tsx b/vite/src/components/forms/attach-product/attach-feature-preview.tsx index da6f96ad0..e97f1647d 100644 --- a/vite/src/components/forms/attach-product/attach-feature-preview.tsx +++ b/vite/src/components/forms/attach-product/attach-feature-preview.tsx @@ -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; + } + >( 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) || {}, }, ]), ); @@ -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 }, }); } diff --git a/vite/src/components/forms/attach-product/attach-product-actions.tsx b/vite/src/components/forms/attach-product/attach-product-actions.tsx index 2847918b5..5c6520919 100644 --- a/vite/src/components/forms/attach-product/attach-product-actions.tsx +++ b/vite/src/components/forms/attach-product/attach-product-actions.tsx @@ -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(null); + const [buttonWidth, setButtonWidth] = useState(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({ - + 0 ? `${buttonWidth}px` : "auto" }} + >
- + ) : ( <> 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() { + + + + ); +}; diff --git a/vite/src/views/customers2/customer-plan/CustomerPlanEditorBar.tsx b/vite/src/views/customers2/customer-plan/CustomerPlanEditorBar.tsx index 1f6cbcfa9..0a72aed20 100644 --- a/vite/src/views/customers2/customer-plan/CustomerPlanEditorBar.tsx +++ b/vite/src/views/customers2/customer-plan/CustomerPlanEditorBar.tsx @@ -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 ; } - // 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; // } diff --git a/vite/src/views/customers2/customer/CustomerView2.tsx b/vite/src/views/customers2/customer/CustomerView2.tsx index 214b8d8cb..ba10f4404 100644 --- a/vite/src/views/customers2/customer/CustomerView2.tsx +++ b/vite/src/views/customers2/customer/CustomerView2.tsx @@ -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(); }} /> )} diff --git a/vite/src/views/products/plan/PlanEditorView.tsx b/vite/src/views/products/plan/PlanEditorView.tsx index f77447050..1c29de3fb 100644 --- a/vite/src/views/products/plan/PlanEditorView.tsx +++ b/vite/src/views/products/plan/PlanEditorView.tsx @@ -67,10 +67,6 @@ export default function PlanEditorView() { { - // Reset sheet when new version is created - setSheet({ type: "edit-plan" }); - }} /> diff --git a/vite/src/views/products/plan/ProductSheets.tsx b/vite/src/views/products/plan/ProductSheets.tsx index 716732018..72c05caf9 100644 --- a/vite/src/views/products/plan/ProductSheets.tsx +++ b/vite/src/views/products/plan/ProductSheets.tsx @@ -75,8 +75,8 @@ export const ProductSheets = () => { return ; case "select-feature": return ; - default: - return ; + // default: + // return ; } };