diff --git a/apps/condo/domains/common/components/Comments/CommentForm.tsx b/apps/condo/domains/common/components/Comments/CommentForm.tsx index f283f4e4dee..70f13817794 100644 --- a/apps/condo/domains/common/components/Comments/CommentForm.tsx +++ b/apps/condo/domains/common/components/Comments/CommentForm.tsx @@ -12,7 +12,7 @@ import { FormWithAction } from '@condo/domains/common/components/containers/Form import { Module, useMultipleFileUploadHook } from '@condo/domains/common/components/MultipleFileUpload' import { useValidations } from '@condo/domains/common/hooks/useValidations' import { analytics } from '@condo/domains/common/utils/analytics' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' +import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components' import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' import { GENERATE_COMMENT_TOUR_STEP_CLOSED_COOKIE, UPDATE_COMMENT_TOUR_STEP_CLOSED_COOKIE } from '@condo/domains/ticket/constants/common' @@ -405,7 +405,23 @@ const CommentForm: React.FC = ({ /> , ...(rewriteCommentEnabled ? [ - hasAiFeature ? ( + + + } + > = ({ rewriteTextLoading={rewriteTextLoading} onClick={handleUpdateComment} /> - ) : ( - -
- -
-
- ), +
, ] : []), ]} /> diff --git a/apps/condo/domains/common/components/Comments/CommentsTabContent.tsx b/apps/condo/domains/common/components/Comments/CommentsTabContent.tsx index f61cb64c687..a3f14b4f3e2 100644 --- a/apps/condo/domains/common/components/Comments/CommentsTabContent.tsx +++ b/apps/condo/domains/common/components/Comments/CommentsTabContent.tsx @@ -12,8 +12,7 @@ import { Tooltip, Tour, Typography } from '@open-condo/ui' import { AIFlowButton } from '@condo/domains/ai/components/AIFlowButton' import { Loader } from '@condo/domains/common/components/Loader' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' -import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' +import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components' import { GENERATE_COMMENT_TOUR_STEP_CLOSED_COOKIE } from '@condo/domains/ticket/constants/common' import { Comment } from './Comment' @@ -80,8 +79,6 @@ export const CommentsTabContent: React.FC = ({ const GenerateCommentTourStepTitle = intl.formatMessage({ id: 'ai.generateComment.tourStepTitle' }) const GenerateCommentTourStepDescription = intl.formatMessage({ id: 'ai.generateComment.tourStepDescription' }) - const { isFeatureAvailable } = useOrganizationSubscription() - const hasAiFeature = isFeatureAvailable('ai') const lastComment = useMemo(() => comments?.[0], [comments]) const showGenerateAnswerButton = useMemo(() => @@ -114,7 +111,17 @@ export const CommentsTabContent: React.FC = ({ /> { showGenerateAnswerButton && lastComment?.id === comment.id && ( - hasAiFeature ? ( + + + {GenerateResponseMessage} + + + } + >
= ({
- ) : ( - -
- - {GenerateResponseMessage} - -
-
- ) +
) } @@ -141,7 +140,7 @@ export const CommentsTabContent: React.FC = ({ }), [ GenerateResponseMessage, GenerateResponseTooltipMessage, comments, editableComment, generateCommentLoading, generateCommentOnClickHandler, lastComment?.id, setEditableComment, - showGenerateAnswerButton, updateAction, hasAiFeature, + showGenerateAnswerButton, updateAction, ]) const { currentStep, setCurrentStep } = Tour.useTourContext() @@ -172,7 +171,17 @@ export const CommentsTabContent: React.FC = ({ PromptDescriptionMessage={PromptDescriptionMessage} AiButton={
{showGenerateCommentWithoutComments && ( - hasAiFeature ? ( + + + {GenerateCommentMessage} + +
+ } + > = ({ - ) : ( - -
- - {GenerateCommentMessage} - -
-
- ) + )} } /> diff --git a/apps/condo/domains/onboarding/components/TourPage/TourStepCard.module.css b/apps/condo/domains/onboarding/components/TourPage/TourStepCard.module.css new file mode 100644 index 00000000000..3db0dee74b2 --- /dev/null +++ b/apps/condo/domains/onboarding/components/TourPage/TourStepCard.module.css @@ -0,0 +1,3 @@ +.full-width { + width: 100%; +} diff --git a/apps/condo/domains/onboarding/components/TourPage/TourStepCard.tsx b/apps/condo/domains/onboarding/components/TourPage/TourStepCard.tsx index 2c506aa5739..6ee16967208 100644 --- a/apps/condo/domains/onboarding/components/TourPage/TourStepCard.tsx +++ b/apps/condo/domains/onboarding/components/TourPage/TourStepCard.tsx @@ -16,9 +16,11 @@ import { COMPLETED_STEP_LINK, TOUR_STEP_ACTION_PERMISSION, } from '@condo/domains/onboarding/utils/clientSchema/constants' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' +import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components' import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' +import styles from './TourStepCard.module.css' + import type { AvailableFeature } from '@condo/domains/subscription/constants/features' /** @@ -125,45 +127,49 @@ export const TourStepCard: React.FC = (props) => { return CompletePreviousStepMessage }, [CompletePreviousStepMessage, NoPermissionsMessage, SettingsMessage, hasPermission]) - const isDisabledStatus = useMemo(() => stepStatus === TourStepStatusType.Disabled || !hasPermission || !hasRequiredFeature || disabled, [disabled, hasPermission, hasRequiredFeature, stepStatus]) + const isDisabledStatus = useMemo(() => stepStatus === TourStepStatusType.Disabled || !hasPermission || disabled, [disabled, hasPermission, stepStatus]) + const isDisabledWithRequiredFeature = isDisabledStatus || !hasRequiredFeature const cardContent = ( ) - if (!hasRequiredFeature) { - return ( - -
- {cardContent} -
-
- ) - } - if (isDisabledStatus) { return ( -
+
{cardContent}
) } - return cardContent + return + {cardContent} +
+ } + > +
+ {cardContent} +
+ } \ No newline at end of file diff --git a/apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx b/apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx index e32f65faa40..7233409593b 100644 --- a/apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx +++ b/apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx @@ -1,45 +1,171 @@ +import { useGetAvailableSubscriptionPlansQuery } from '@app/condo/gql' import { useRouter } from 'next/router' -import React, { useCallback } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useIntl } from '@open-condo/next/intl' +import { useOrganization } from '@open-condo/next/organization' import { Button, Space, Tooltip, Typography } from '@open-condo/ui' -import type { TooltipProps } from '@open-condo/ui' +import { CURRENCY_SYMBOLS } from '@condo/domains/common/constants/currencies' import { SETTINGS_TAB_SUBSCRIPTION } from '@condo/domains/common/constants/settingsTabs' +import { getRequiredFeature } from '@condo/domains/subscription/constants/routeFeatureMapping' +import { useActivateSubscriptions, useOrganizationSubscription } from '@condo/domains/subscription/hooks' + +import type { AvailableFeature } from '@condo/domains/subscription/constants/features' export interface NoSubscriptionTooltipProps { children: React.ReactElement - placement?: TooltipProps['placement'] + placement?: 'top' | 'bottom' | 'left' | 'right' + feature?: AvailableFeature | AvailableFeature[] + path?: string + skipTooltip?: boolean } -export const NoSubscriptionTooltip: React.FC = ({ children, placement = 'right' }) => { +export const NoSubscriptionTooltip: React.FC = ({ children, placement = 'right', feature: featureProp, path, skipTooltip }) => { const intl = useIntl() const router = useRouter() + const { organization } = useOrganization() + const { trialSubscriptions, activatedSubscriptions, handleActivatePlan, activateLoading } = useActivateSubscriptions() + const { isFeatureAvailable, hasSubscription } = useOrganizationSubscription() + const [isActivating, setIsActivating] = useState(false) + + const requiredFeature = path ? getRequiredFeature(path) : null + const feature = (featureProp || requiredFeature) as AvailableFeature | undefined | null - const NoSubscriptionWarning = intl.formatMessage({ + const FeatureLockedMessage = intl.formatMessage({ id: 'subscription.warns.noActiveSubscription', }) - const ActivateSubscriptionButton = intl.formatMessage({ + const UpgradePlanMessage = intl.formatMessage({ + id: 'subscription.warns.upgradePlan', + }) + const ViewPlansButton = intl.formatMessage({ id: 'subscription.warns.activateSubscriptionButton', }) - const handleActivateClick = useCallback(async () => { + const { data: plansData } = useGetAvailableSubscriptionPlansQuery({ + variables: { + organization: { id: organization?.id }, + }, + skip: !organization?.id, + }) + + const hasActivatedAnyTrial = trialSubscriptions.length > 0 + const isAvailable = feature + ? Array.isArray(feature) + ? feature.every(f => isFeatureAvailable(f)) + : isFeatureAvailable(feature) + : false + + const bestPlanWithFeature = useMemo(() => { + if (!feature || isAvailable) return null + + const plans = plansData?.result?.plans || [] + + const plansWithFeature = plans + .filter(planInfo => { + const plan = planInfo?.plan + if (!plan) return false + + const hasFeature = Array.isArray(feature) + ? feature.every(f => plan[f] === true) + : plan[feature] === true + const hasTrialDays = plan.trialDays > 0 + const prices = planInfo?.prices || [] + const hasPrice = prices.length > 0 + + const alreadyActivated = activatedSubscriptions.some( + ctx => ctx.subscriptionPlan && ( + ctx.subscriptionPlan.id === plan.id || + ctx.subscriptionPlan.priority > (plan.priority || 0) + ) + ) + + return hasFeature && hasTrialDays && hasPrice && !alreadyActivated + }) + .sort((a, b) => (b.plan?.priority ?? 0) - (a.plan?.priority ?? 0)) + + return plansWithFeature[0] || null + }, [feature, isAvailable, plansData?.result?.plans, activatedSubscriptions]) + + const TryTrialButton = useMemo(() => { + const currencyCode = bestPlanWithFeature?.prices?.[0]?.currencyCode + return intl.formatMessage({ + id: 'subscription.warns.tryTrialButton', + }, { currency: CURRENCY_SYMBOLS[currencyCode] || '' }) + }, [bestPlanWithFeature, intl]) + + const handleActivateTrial = useCallback(async () => { + if (!bestPlanWithFeature) { + await router.push(`/settings?tab=${SETTINGS_TAB_SUBSCRIPTION}`) + return + } + + const price = bestPlanWithFeature.prices?.[0] + if (!price?.id) return + + setIsActivating(true) + try { + const planName = bestPlanWithFeature.plan?.name || '' + + await handleActivatePlan({ + priceId: price.id, + isTrial: true, + planName, + trialDays: bestPlanWithFeature.plan?.trialDays || 0, + isCustomPrice: false, + }) + } finally { + setIsActivating(false) + } + }, [bestPlanWithFeature, handleActivatePlan, router]) + + const handleViewPlans = useCallback(async () => { await router.push(`/settings?tab=${SETTINGS_TAB_SUBSCRIPTION}`) }, [router]) + const isLoading = activateLoading || isActivating + + const buttonText = hasActivatedAnyTrial ? ViewPlansButton : TryTrialButton + + const buttonAction = !feature || hasActivatedAnyTrial ? handleViewPlans : handleActivateTrial + const tooltipTitle = ( - - {NoSubscriptionWarning} - ) + if (skipTooltip) { + return children + } + + if (path && !requiredFeature) { + return children + } + + if (path && !hasSubscription) { + return children + } + + if (isAvailable) { + return children + } + return ( {children} diff --git a/apps/condo/domains/subscription/components/SubscriptionGuardWithTooltip.tsx b/apps/condo/domains/subscription/components/SubscriptionGuardWithTooltip.tsx new file mode 100644 index 00000000000..fc18e68783e --- /dev/null +++ b/apps/condo/domains/subscription/components/SubscriptionGuardWithTooltip.tsx @@ -0,0 +1,52 @@ +import React from 'react' + +import { NoSubscriptionTooltip, NoSubscriptionTooltipProps } from '@condo/domains/subscription/components/NoSubscriptionTooltip' +import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' + +interface SubscriptionFeatureGuardProps extends Omit { + children: React.ReactElement + feature: NoSubscriptionTooltipProps['feature'] + fallback: React.ReactElement +} + +/** + * Component that conditionally renders content based on subscription feature availability. + * + * If the organization has access to the specified feature(s), renders the children. + * Otherwise, renders the fallback element wrapped in a NoSubscriptionTooltip. + * + * @param children - Element to render when feature is available + * @param feature - Feature name or array of feature names to check + * @param fallback - Element to render when feature is not available (will be wrapped in tooltip) + * @param tooltipProps - Additional props passed to NoSubscriptionTooltip + * + * @example + * Analytics} + * > + * + * + */ +export const SubscriptionGuardWithTooltip: React.FC = ({ + children, + feature, + fallback, + ...tooltipProps +}) => { + const { isFeatureAvailable } = useOrganizationSubscription() + + const hasFeature = Array.isArray(feature) + ? feature.every(f => isFeatureAvailable(f)) + : isFeatureAvailable(feature) + + if (hasFeature) { + return children + } + + return ( + + {fallback} + + ) +} diff --git a/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionPlanCard/SubscriptionPlanCard.tsx b/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionPlanCard/SubscriptionPlanCard.tsx index c1af30f3de5..bba6feea2cb 100644 --- a/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionPlanCard/SubscriptionPlanCard.tsx +++ b/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionPlanCard/SubscriptionPlanCard.tsx @@ -92,6 +92,7 @@ interface SubscriptionPlanCardProps { b2bAppsMap: Map allB2BAppIds: string[] emoji?: string + trialActivateLoading?: boolean } interface SubscriptionPlanBadgeProps { @@ -172,7 +173,7 @@ const SubscriptionPlanBadge: React.FC = ({ plan, act ) } -export const SubscriptionPlanCard: React.FC = ({ planInfo, activatedTrial, pendingRequest, activatedSubscriptions, handleActivatePlan, b2bAppsMap, allB2BAppIds, emoji }) => { +export const SubscriptionPlanCard: React.FC = ({ planInfo, activatedTrial, pendingRequest, activatedSubscriptions, handleActivatePlan, b2bAppsMap, allB2BAppIds, emoji, trialActivateLoading = false }) => { const intl = useIntl() const RequestPendingMessage = intl.formatMessage({ id: 'subscription.planCard.requestPending' }) const SubmitRequestMessage = intl.formatMessage({ id: 'subscription.planCard.submitRequest' }) @@ -186,7 +187,6 @@ export const SubscriptionPlanCard: React.FC = ({ plan const { useFlagValue } = useFeatureFlags() const { subscriptionContext: activeSubscriptionContext, daysRemaining } = useOrganizationSubscription() const [activateLoading, setActivateLoading] = useState(false) - const [trialActivateLoading, setTrialActivateLoading] = useState(false) const { plan, prices } = planInfo const price = prices?.[0] @@ -290,18 +290,13 @@ export const SubscriptionPlanCard: React.FC = ({ plan const handleTrialActivateClick = useCallback(async () => { if (!price?.id) return - setTrialActivateLoading(true) - try { - await handleActivatePlan({ - priceId: price.id, - isTrial: true, - planName: plan.name, - trialDays: plan.trialDays, - isCustomPrice, - }) - } finally { - setTrialActivateLoading(false) - } + await handleActivatePlan({ + priceId: price.id, + isTrial: true, + planName: plan.name, + trialDays: plan.trialDays, + isCustomPrice, + }) }, [handleActivatePlan, price?.id, plan.name, plan.trialDays, isCustomPrice]) const renderFeature = useCallback(({ featureKey, label, hint }: FeatureConfig) => ( @@ -437,7 +432,7 @@ export const SubscriptionPlanCard: React.FC = ({ plan type='accent' onClick={handleTrialActivateClick} loading={trialActivateLoading} - disabled={!canManageSubscriptions} + disabled={!canManageSubscriptions || trialActivateLoading} > {TryFreeMessage} diff --git a/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionSettingsContent.tsx b/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionSettingsContent.tsx index c6162d175aa..602a4a690c5 100644 --- a/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionSettingsContent.tsx +++ b/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionSettingsContent.tsx @@ -1,14 +1,12 @@ -import { useGetAvailableSubscriptionPlansQuery, GetAvailableSubscriptionPlansQueryResult, useGetOrganizationTrialSubscriptionsQuery, useActivateSubscriptionPlanMutation, useGetPendingSubscriptionRequestsQuery, useGetB2BAppsByIdsQuery, useGetOrganizationActivatedSubscriptionsQuery } from '@app/condo/gql' -import { notification } from 'antd' -import dayjs from 'dayjs' +import { useGetAvailableSubscriptionPlansQuery, GetAvailableSubscriptionPlansQueryResult, useGetB2BAppsByIdsQuery } from '@app/condo/gql' import React, { useState, useMemo } from 'react' -import { getClientSideSenderInfo } from '@open-condo/miniapp-utils/helpers/sender' import { useIntl } from '@open-condo/next/intl' import { useOrganization } from '@open-condo/next/organization' -import { Space, Radio, Typography } from '@open-condo/ui' +import { Space, Radio } from '@open-condo/ui' import { Loader } from '@condo/domains/common/components/Loader' +import { useActivateSubscriptions } from '@condo/domains/subscription/hooks' import { PromoBanner } from './PromoBanner/PromoBanner' import { SubscriptionPlanCard } from './SubscriptionPlanCard/SubscriptionPlanCard' @@ -22,12 +20,10 @@ const PLAN_CARD_EMOJIS = ['🏠', '🚀', '👑'] export const SubscriptionSettingsContent: React.FC = () => { const intl = useIntl() - const { organization, employee, selectEmployee } = useOrganization() + const { organization } = useOrganization() const YearlyLabel = intl.formatMessage({ id: 'subscription.period.yearly' }) const MonthlyLabel = intl.formatMessage({ id: 'subscription.period.monthly' }) - const ActivationErrorTitle = intl.formatMessage({ id: 'subscription.activation.errorTitle' }) - const ActivationErrorMessage = intl.formatMessage({ id: 'subscription.activation.error' }) const [planPeriod, setPlanPeriod] = useState('year') @@ -73,108 +69,15 @@ export const SubscriptionSettingsContent: React.FC = () => { }, [b2bAppsData]) const { - data: trialSubscriptionsData, - loading: trialSubscriptionsLoading, - refetch: refetchTrialSubscriptions, - } = useGetOrganizationTrialSubscriptionsQuery({ - variables: { - organizationId: organization?.id, - }, - skip: !organization?.id, - }) - - const { data: pendingRequestsData, loading: pendingRequestsLoading, refetch: refetchPendingRequests } = useGetPendingSubscriptionRequestsQuery({ - variables: { organizationId: organization?.id }, - skip: !organization?.id, - }) - - const { data: activatedSubscriptionsData, loading: activatedSubscriptionsLoading, refetch: refetchActivatedSubscriptions } = useGetOrganizationActivatedSubscriptionsQuery({ - variables: { - organizationId: organization?.id || '', - }, - skip: !organization?.id, - }) - - const trialSubscriptions = trialSubscriptionsData?.trialSubscriptions || [] - const pendingRequests = pendingRequestsData?.pendingRequests || [] - const activatedSubscriptions = activatedSubscriptionsData?.activatedSubscriptions || [] - - const [activateSubscriptionPlan] = useActivateSubscriptionPlanMutation() - - const handleActivatePlan = async ({ priceId, isTrial = true, planName = '', trialDays = 0, isCustomPrice = false }: { - priceId: string - isTrial?: boolean - planName?: string - trialDays?: number - isCustomPrice?: boolean - }) => { - if (!organization) return - - try { - await activateSubscriptionPlan({ - variables: { - data: { - dv: 1, - sender: getClientSideSenderInfo(), - organization: { id: organization.id }, - pricingRule: { id: priceId }, - isTrial, - }, - }, - }) - - if (isTrial) { - await refetchTrialSubscriptions() - await refetchPendingRequests() - await refetchActivatedSubscriptions() - if (employee?.id) { - await selectEmployee(employee.id) - } - notification.success({ - message: ( - - {intl.formatMessage({ id: 'subscription.activation.trial.title' }, { planName })} - - ), - description: intl.formatMessage({ id: 'subscription.activation.trial.description' }, { planName, days: trialDays }), - duration: 5, - }) - } else { - await refetchPendingRequests() - if (isCustomPrice) { - notification.success({ - message: ( - - {intl.formatMessage({ id: 'subscription.activation.paid.custom.title' }, { planName })} - - ), - description: intl.formatMessage({ id: 'subscription.activation.paid.custom.description' }), - duration: 5, - }) - } else { - notification.success({ - message: ( - - {intl.formatMessage({ id: 'subscription.activation.paid.standard.title' })} - - ), - description: intl.formatMessage({ id: 'subscription.activation.paid.standard.description' }), - duration: 5, - }) - } - } - } catch (error) { - console.error('Failed to activate subscription:', error) - notification.error({ - message: ActivationErrorTitle, - description: error?.message || ActivationErrorMessage, - duration: 5, - }) - } - } - - - const isLoading = plansLoading || trialSubscriptionsLoading || pendingRequestsLoading || activatedSubscriptionsLoading + handleActivatePlan, + activateLoading, + trialSubscriptions, + pendingRequests, + activatedSubscriptions, + isLoading: trialActivationLoading, + } = useActivateSubscriptions() + + const isLoading = plansLoading || trialActivationLoading if (isLoading) return return ( @@ -210,6 +113,7 @@ export const SubscriptionSettingsContent: React.FC = () => { b2bAppsMap={b2bAppsMap} allB2BAppIds={allB2BAppIds} emoji={PLAN_CARD_EMOJIS?.[index]} + trialActivateLoading={activateLoading} /> ) })} diff --git a/apps/condo/domains/subscription/components/index.ts b/apps/condo/domains/subscription/components/index.ts index ddfed18db3f..a56d52b3266 100644 --- a/apps/condo/domains/subscription/components/index.ts +++ b/apps/condo/domains/subscription/components/index.ts @@ -1,5 +1,6 @@ export { NoSubscriptionTooltip } from './NoSubscriptionTooltip' export type { NoSubscriptionTooltipProps } from './NoSubscriptionTooltip' +export { SubscriptionGuardWithTooltip } from './SubscriptionGuardWithTooltip' export { SubscriptionAccessGuard } from './SubscriptionAccessGuard' export { SubscriptionWelcomeModal } from './SubscriptionWelcomeModal' export { SubscriptionTrialEndedModal } from './SubscriptionTrialEndedModal' diff --git a/apps/condo/domains/subscription/hooks/index.ts b/apps/condo/domains/subscription/hooks/index.ts index fac7d362e08..7ecf21fa152 100644 --- a/apps/condo/domains/subscription/hooks/index.ts +++ b/apps/condo/domains/subscription/hooks/index.ts @@ -1,2 +1,3 @@ export { useOrganizationSubscription } from './useOrganizationSubscription' export type { SubscriptionContext } from './useOrganizationSubscription' +export { useActivateSubscriptions } from './useActivateSubscriptions' diff --git a/apps/condo/domains/subscription/hooks/useActivateSubscriptions.tsx b/apps/condo/domains/subscription/hooks/useActivateSubscriptions.tsx new file mode 100644 index 00000000000..0075531b57e --- /dev/null +++ b/apps/condo/domains/subscription/hooks/useActivateSubscriptions.tsx @@ -0,0 +1,139 @@ +import { useActivateSubscriptionPlanMutation, useGetOrganizationTrialSubscriptionsQuery, useGetPendingSubscriptionRequestsQuery, useGetOrganizationActivatedSubscriptionsQuery } from '@app/condo/gql' +import { notification } from 'antd' +import { useCallback, useState } from 'react' + +import { getClientSideSenderInfo } from '@open-condo/miniapp-utils/helpers/sender' +import { useIntl } from '@open-condo/next/intl' +import { useOrganization } from '@open-condo/next/organization' +import { Typography } from '@open-condo/ui' + +interface ActivatePlanParams { + priceId: string + isTrial?: boolean + planName?: string + trialDays?: number + isCustomPrice?: boolean +} + +export const useActivateSubscriptions = () => { + const intl = useIntl() + const { organization, employee, selectEmployee } = useOrganization() + + const ActivationErrorTitle = intl.formatMessage({ id: 'subscription.activation.errorTitle' }) + const ActivationErrorMessage = intl.formatMessage({ id: 'subscription.activation.error' }) + + const [activateLoading, setActivateLoading] = useState(false) + const { + data: trialSubscriptionsData, + loading: trialSubscriptionsLoading, + refetch: refetchTrialSubscriptions, + } = useGetOrganizationTrialSubscriptionsQuery({ + variables: { + organizationId: organization?.id, + }, + skip: !organization?.id, + }) + + const { data: pendingRequestsData, loading: pendingRequestsLoading, refetch: refetchPendingRequests } = useGetPendingSubscriptionRequestsQuery({ + variables: { organizationId: organization?.id }, + skip: !organization?.id, + }) + + const { data: activatedSubscriptionsData, loading: activatedSubscriptionsLoading, refetch: refetchActivatedSubscriptions } = useGetOrganizationActivatedSubscriptionsQuery({ + variables: { + organizationId: organization?.id || '', + }, + skip: !organization?.id, + }) + + const [activateSubscriptionPlan] = useActivateSubscriptionPlanMutation() + + const trialSubscriptions = trialSubscriptionsData?.trialSubscriptions || [] + const pendingRequests = pendingRequestsData?.pendingRequests || [] + const activatedSubscriptions = activatedSubscriptionsData?.activatedSubscriptions || [] + + const showSuccessNotification = useCallback((isTrial: boolean, planName: string, trialDays: number, isCustomPrice: boolean) => { + if (isTrial) { + notification.success({ + message: ( + + {intl.formatMessage({ id: 'subscription.activation.trial.title' }, { planName })} + + ), + description: intl.formatMessage({ id: 'subscription.activation.trial.description' }, { planName, days: trialDays }), + duration: 5, + }) + } else if (isCustomPrice) { + notification.success({ + message: ( + + {intl.formatMessage({ id: 'subscription.activation.paid.custom.title' }, { planName })} + + ), + description: intl.formatMessage({ id: 'subscription.activation.paid.custom.description' }), + duration: 5, + }) + } else { + notification.success({ + message: ( + + {intl.formatMessage({ id: 'subscription.activation.paid.standard.title' })} + + ), + description: intl.formatMessage({ id: 'subscription.activation.paid.standard.description' }), + duration: 5, + }) + } + }, [intl]) + + const refetchData = useCallback(async (isTrial: boolean) => { + await refetchPendingRequests() + if (isTrial) { + await refetchTrialSubscriptions() + await refetchActivatedSubscriptions() + if (employee?.id) { + await selectEmployee(employee.id) + } + } + }, [refetchPendingRequests, refetchTrialSubscriptions, refetchActivatedSubscriptions, employee?.id, selectEmployee]) + + const handleActivatePlan = useCallback(async ({ priceId, isTrial = true, planName = '', trialDays = 0, isCustomPrice = false }: ActivatePlanParams) => { + if (!organization) return + + setActivateLoading(true) + try { + await activateSubscriptionPlan({ + variables: { + data: { + dv: 1, + sender: getClientSideSenderInfo(), + organization: { id: organization.id }, + pricingRule: { id: priceId }, + isTrial, + }, + }, + }) + + await refetchData(isTrial) + showSuccessNotification(isTrial, planName, trialDays, isCustomPrice) + } catch (error) { + console.error('Failed to activate subscription:', error) + notification.error({ + message: ActivationErrorTitle, + description: error?.message || ActivationErrorMessage, + duration: 5, + }) + } finally { + setActivateLoading(false) + } + }, [organization, activateSubscriptionPlan, refetchData, showSuccessNotification, ActivationErrorTitle, ActivationErrorMessage]) + + return { + handleActivatePlan, + activateLoading, + trialSubscriptions, + pendingRequests, + activatedSubscriptions, + isLoading: trialSubscriptionsLoading || pendingRequestsLoading || activatedSubscriptionsLoading, + } +} diff --git a/apps/condo/domains/ticket/components/BaseTicketForm/index.module.css b/apps/condo/domains/ticket/components/BaseTicketForm/index.module.css index 26a93a9e965..e006b30a2fe 100644 --- a/apps/condo/domains/ticket/components/BaseTicketForm/index.module.css +++ b/apps/condo/domains/ticket/components/BaseTicketForm/index.module.css @@ -1,4 +1,16 @@ .ticket-property-hint-card { max-width: 250px; max-height: 11em; -} \ No newline at end of file +} + +.cursor-not-allowed { + cursor: not-allowed; +} + +.cursor-pointer { + cursor: pointer; +} + +.invoice-row { + padding-bottom: 24px; +} diff --git a/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx b/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx index 7f8ace28874..92e9de2555d 100644 --- a/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx +++ b/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx @@ -66,7 +66,7 @@ import { MANAGING_COMPANY_TYPE, SERVICE_PROVIDER_TYPE } from '@condo/domains/org import { PropertyAddressSearchInput } from '@condo/domains/property/components/PropertyAddressSearchInput' import { UnitInfo, UnitInfoMode } from '@condo/domains/property/components/UnitInfo' import { PropertyFormItemTooltip } from '@condo/domains/property/PropertyFormItemTooltip' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' +import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components' import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' import { IncidentHints } from '@condo/domains/ticket/components/IncidentHints' import { useTicketThreeLevelsClassifierHook } from '@condo/domains/ticket/components/TicketClassifierSelect' @@ -89,6 +89,7 @@ import { TicketDeadlineField } from './TicketDeadlineField' import { TicketDeferredDateField } from './TicketDeferredDateField' import { useTicketValidations } from './useTicketValidations' + const HINTS_COL_PROPS: ColProps = { span: 24 } const CURRENT_FORM_VALUES_LOCAL_STORAGE_NAME = 'condoTicketCurrentFormValues' @@ -216,29 +217,29 @@ const AddInvoiceButton = ({ initialValues, form, organizationId, ticketCreatedBy return null } - if (!hasMarketplaceFeature) { - return ( - -
- - - - {AddInvoiceMessage} - - -
-
- ) - } - return ( <> - setCreateInvoiceModalOpen(true)}> - - - {AddInvoiceMessage} - - + + + + + {AddInvoiceMessage} + + + + } + > + setCreateInvoiceModalOpen(true)}> + + + {AddInvoiceMessage} + + + { createInvoiceModalOpen && ( {NoInvoicesMessage} - + = (props) => { = ({ inci rewriteIncidentTextForResident: rewriteIncidentTextForResidentEnabled, } } = useAIConfig() - const { isFeatureAvailable } = useOrganizationSubscription() - const hasAiFeature = isFeatureAvailable('ai') useEffect(() => { setRewriteText(rewriteTextData?.result?.answer) @@ -480,40 +477,40 @@ export const TextForResidentInput: React.FC = ({ inci icon={copied ? () : () } />
, - ...( - aiEnabled && rewriteIncidentTextForResidentEnabled ? [ - hasAiFeature ? ( - - ) : ( - -
- -
-
- ), - ] : [] - ), + ... + aiEnabled && rewriteIncidentTextForResidentEnabled ? [ + + + + } + > + + , + ] : [], ]} /> diff --git a/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx b/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx index 388d6b6907b..e976b300da6 100644 --- a/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx +++ b/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx @@ -11,8 +11,7 @@ import { ActionBar, Button, Space, Switch, Typography } from '@open-condo/ui' import { useAIConfig } from '@condo/domains/ai/hooks/useAIFlow' import { LabeledField } from '@condo/domains/common/components/LabeledField' import { AnalyticalNewsSources } from '@condo/domains/news/constants/sources' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' -import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' +import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components' import { BaseIncidentForm, BaseIncidentFormProps } from './BaseIncidentForm' @@ -30,10 +29,6 @@ export const CreateIncidentActionBar: React.FC, ...((withNewsGeneration && aiEnabled && generateNewsByIncidentEnabled && canManageNewsItems) ? [ - hasRequiredFeatures ? ( - + + + + + + {GenerateNewsLabel} + + + + + } + > + - ) : ( - -
- - - - - {GenerateNewsLabel} - - - -
-
- ), +
, ] : [] ), diff --git a/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx b/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx index 2f76489f2a6..0a1771560ff 100644 --- a/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx +++ b/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx @@ -19,6 +19,7 @@ import { LabeledField } from '@condo/domains/common/components/LabeledField' import DatePicker from '@condo/domains/common/components/Pickers/DatePicker' import { useValidations } from '@condo/domains/common/hooks/useValidations' import { analytics } from '@condo/domains/common/utils/analytics' +import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components' import { handleChangeDate } from '@condo/domains/ticket/components/IncidentForm/BaseIncidentForm' import type { FormRule as Rule } from 'antd' @@ -173,26 +174,45 @@ export const useIncidentUpdateStatusModal: UseIncidentUpdateStatusModalType = ({ { (withNewsGeneration && aiEnabled && generateNewsByIncidentEnabled && canManageNewsItems && isActual) && ( - + + + + + {GenerateNewsSwitchLabel} + + + + + } > - - - - - - {GenerateNewsSwitchLabel} - - - + + + + + + + {GenerateNewsSwitchLabel} + + + + ) }