Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
"start": "next start",
"lint": "next lint --fix",
"type-check": "tsc --noEmit",
"test": "echo \"No tests yet\"",
"prepare": "husky install && (chmod -R +x .husky || echo 'Skipping chmod')"
"test": "echo \"No tests yet\""
},
"dependencies": {
"@emotion/react": "^11.14.0",
Expand Down
5 changes: 3 additions & 2 deletions src/app/admin/billing/components/BillingCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import { styled } from '@mui/material/styles';
import { padding } from '@mui/system';
import Image from 'next/image';

import CommonButton from '@/components/ui/CommonButton';
Expand Down Expand Up @@ -226,7 +225,9 @@ export default function PricingCard({
? 'green'
: btn.variant === 'cancel'
? 'cancel'
: 'disabled'
: btn.variant === 'retry'
? 'retry'
: 'disabled'
}
onClick={
btn.variant === 'disabled'
Expand Down
6 changes: 3 additions & 3 deletions src/app/admin/billing/components/BillingHistorySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ const BillingHistorySection = () => {
timestamp: new Date(ref.date).getTime(),
}));

const combined: BillingEntry[] = [...invoiceEntries, ...refundEntries].sort(
(a, b) => b.timestamp - a.timestamp,
);
const combined: BillingEntry[] = [...invoiceEntries, ...refundEntries]
.filter(entry => ['paid', 'unpaid', 'refunded'].includes(entry.status))
.sort((a, b) => b.timestamp - a.timestamp);

return (
<Box sx={{ width: '100%' }}>
Expand Down
105 changes: 98 additions & 7 deletions src/app/admin/billing/components/BillingSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { useKeenSlider } from 'keen-slider/react';
import { useEffect, useState } from 'react';

import CancelConfirmModal from '@/components/ui/CancelConfirmModal';
import PaymentFailedModal from '@/components/ui/PaymentFailedModal';
import { useGetPlansQuery } from '@/features/public/publicApiSlice';
import {
useChangePlan,
useCreateSubscription,
useDowngradeToFree,
useRetryPayment,
useSubscription,
} from '@/features/subscription/useSubscription';
import type { Plan, PlanButton } from '@/types/plan.types';
Expand All @@ -38,21 +40,64 @@ function getPrice(pricing: { rrule: string; price: number }[]) {
function getButtonsByPlan(
plan: Plan,
currentPlanId: string,
pendingPlanId: string | undefined,
isSubscribed: boolean,
isCancelled: boolean,
isPendingCancellation: boolean,
isPendingDowngrade: boolean,
isFailed: boolean,
): PlanButton[] {
const isCurrent = plan._id === currentPlanId;
const isPendingDowngradeToPlan =
isPendingDowngrade && plan._id === pendingPlanId;

if (isCancelled) {
if (plan.tier === 'FREE')
return [{ label: 'Your current plan', variant: 'disabled' }];
return [{ label: `Go with ${plan.tier}`, variant: 'primary' }];
}

if (isFailed) {
// For failed subscriptions, show retry payment button for current plan
if (isCurrent) {
return [{ label: 'Retry Payment', variant: 'retry' }];
}
// For other plans, disable them during payment failure
return [{ label: `Go with ${plan.tier}`, variant: 'disabled' }];
}

if (isSubscribed) {
// Current plan display
if (isCurrent) {
if (isPendingCancellation) {
return [{ label: 'Cancels at period end', variant: 'disabled' }];
}
if (isPendingDowngrade) {
// Current plan during pending downgrade - show cancel downgrade option
return [{ label: 'Cancel downgrade', variant: 'primary' }];
}
return [{ label: 'Cancel Subscription', variant: 'cancel' }];
}

// Special handling for pending downgrade
if (isPendingDowngrade) {
if (isPendingDowngradeToPlan) {
// Target plan (Basic) - show downgrade info
return [{ label: 'Downgrades next cycle', variant: 'disabled' }];
} else {
// Other plans during pending downgrade - show switch option
return [{ label: `Go with ${plan.tier}`, variant: 'primary' }];
}
}

// If pending cancellation, show "Go with" to allow reactivation
if (isPendingCancellation) {
return [{ label: `Go with ${plan.tier}`, variant: 'primary' }];
}

return [{ label: `Switch to ${plan.tier}`, variant: 'primary' }];
}

return [{ label: 'Try for Free', variant: 'primary' }];
}

Expand All @@ -69,8 +114,18 @@ export default function BillingSection() {
const { create } = useCreateSubscription();
const { change } = useChangePlan();
const { downgrade } = useDowngradeToFree();
const { subscription, isSubscribed, isCancelled, currentPlanId } =
useSubscription();
const { retryPayment } = useRetryPayment();
const {
subscription,
isSubscribed,
isCancelled,
isFailed,
isPendingCancellation,
isPendingDowngrade,
currentPlanId,
} = useSubscription();

const pendingPlanId = subscription?.pendingPlanId?._id;

const tierOrder = { FREE: 0, BASIC: 1, PRO: 2 };
const sortedPlans = [...plans].sort(
Expand All @@ -80,7 +135,6 @@ export default function BillingSection() {
{
/* slide */
}
const [currentSlide, setCurrentSlide] = useState(0);
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
slides: { perView: 'auto', spacing: 0, origin: 'center' },
rubberband: false,
Expand All @@ -89,9 +143,6 @@ export default function BillingSection() {
'(min-width: 1000px)': { slides: { perView: 2, spacing: 0 } },
'(min-width: 1420px)': { slides: { perView: 2, spacing: 0 } },
},
slideChanged(sliderInstance) {
setCurrentSlide(sliderInstance.track.details.rel);
},
});

useEffect(() => {
Expand All @@ -102,6 +153,7 @@ export default function BillingSection() {
}, [slider]);

const [showCancelModal, setShowCancelModal] = useState(false);
const [showPaymentFailedModal, setShowPaymentFailedModal] = useState(false);

const handleClick = async (
label: string,
Expand All @@ -111,19 +163,39 @@ export default function BillingSection() {
if (label.startsWith('Go with')) {
if (!subscription || subscription.status === 'cancelled') {
await create(planId);
} else if (
subscription.status === 'pending_cancellation' ||
subscription.status === 'pending_downgrade'
) {
// If pending cancellation or downgrade, use change to cancel the pending change and switch plan
await change(planId);
window.location.reload();
} else if (subscription.planId._id !== planId) {
await change(planId);
}
}
if (label === 'Cancel Subscription') {
if (label === 'Cancel Subscription' || label === 'Cancel Instead') {
setShowCancelModal(true);
return;
}
if (label === 'Retry Payment') {
setShowPaymentFailedModal(true);
return;
}
if (label === 'Cancel downgrade') {
// Cancel the pending downgrade by switching back to current plan
if (subscription?.planId._id) {
await change(subscription.planId._id);
window.location.reload();
}
return;
}
if (label.startsWith('Switch to')) {
if (tier === 'FREE') await downgrade();
else await change(planId);
window.location.reload();
}
// Do nothing for disabled states ('Cancels at period end', 'Downgrades next cycle', 'Your current plan')
};

const handleConfirmCancel = async () => {
Expand All @@ -136,6 +208,15 @@ export default function BillingSection() {
}
};

const handleRetryPayment = async () => {
try {
await retryPayment();
setShowPaymentFailedModal(false);
} catch {
// Handle error silently
}
};

return (
<Box
sx={{
Expand Down Expand Up @@ -182,8 +263,12 @@ export default function BillingSection() {
buttons={getButtonsByPlan(
plan,
currentPlanId,
pendingPlanId,
isSubscribed,
isCancelled,
isPendingCancellation,
isPendingDowngrade,
isFailed,
)}
onButtonClick={label =>
void handleClick(label, plan.tier, plan._id)
Expand All @@ -204,6 +289,12 @@ export default function BillingSection() {
onClose={() => setShowCancelModal(false)}
onConfirm={handleConfirmCancel}
/>

<PaymentFailedModal
open={showPaymentFailedModal}
onClose={() => setShowPaymentFailedModal(false)}
onRetryPayment={handleRetryPayment}
/>
</Box>
);
}
24 changes: 20 additions & 4 deletions src/app/admin/overview/components/ActivitySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import Image from 'next/image';

import HalfCircleProgress from '@/components/ui/HalfCircleProgress';
import { useGetTodayMetricsQuery } from '@/features/callog/calllogApi';
import { useGetTwilioPhoneNumberQuery } from '@/features/overview/overviewApi';
import { useGetBookingsQuery } from '@/features/service/serviceBookingApi';
import { useSubscription } from '@/features/subscription/useSubscription';
import { useGetTwilioPhoneNumberQuery } from '@/features/twilio-phone-number/twilioPhoneNumberApi';
import { useAppSelector } from '@/redux/hooks';
import { getPlanTier, isFreeOrBasicPlan } from '@/utils/planUtils';
import {
getRemainingMinutes,
getTotalMinutes,
} from '@/utils/subscriptionUtils';

function formatSubscriptionPeriod(
start?: string | Date,
Expand Down Expand Up @@ -160,6 +164,18 @@ export default function ActivitySection() {
const planTier = getPlanTier(subscription);
const shouldHideBookingFeatures = isFreeOrBasicPlan(planTier);

// Calculate minutes data for the progress circle
const remainingMinutes = getRemainingMinutes(subscription);
const totalMinutes = getTotalMinutes(subscription?.planId);

// Determine display values and unit text
const isUnlimited = totalMinutes === Number.MAX_SAFE_INTEGER;
const displayValue = remainingMinutes; // Always show remaining minutes
const displayMaxValue = isUnlimited
? Math.max(remainingMinutes, 1000)
: totalMinutes;
const unitText = isUnlimited ? '/Unlimited' : `/${totalMinutes}`;

const { data: bookings } = useGetBookingsQuery({ userId }, { skip: !userId });

const todayBookings = (bookings ?? []).filter(booking => {
Expand Down Expand Up @@ -262,9 +278,9 @@ export default function ActivitySection() {
</Box>
<Box sx={{ justifyItems: 'center' }}>
<HalfCircleProgress
value={523}
maxValue={1000}
unitText="/Unlimited"
value={displayValue}
maxValue={displayMaxValue}
unitText={unitText}
/>
</Box>
</InfoCard>
Expand Down
2 changes: 0 additions & 2 deletions src/app/auth/callback/AuthCallbackContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ export default function AuthCallbackContent() {
// Clear any persisted auth state to prevent old user ID from being used
localStorage.removeItem('persist:root');

console.log('[AuthCallback] Setting user with ID:', parsedUser._id);

dispatch(
setCredentials({
csrfToken,
Expand Down
13 changes: 12 additions & 1 deletion src/components/ui/CommonButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button } from '@mui/material';
import { styled } from '@mui/material/styles';
import React from 'react';

type ButtonVariant = 'black' | 'green' | 'disabled' | 'cancel';
type ButtonVariant = 'black' | 'green' | 'disabled' | 'cancel' | 'retry';

interface CommonButtonProps extends Omit<ButtonProps, 'variant'> {
children: React.ReactNode;
Expand Down Expand Up @@ -50,6 +50,17 @@ const StyledButton = styled(Button, {
};
}

if (buttonVariant === 'retry') {
return {
...baseStyle,
backgroundColor: '#ff4444',
color: '#ffffff',
'&:hover': {
backgroundColor: '#cc3333',
},
};
}

const isBlack = buttonVariant === 'black';

return {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/HalfCircleProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default function HalfCircleProgress({
marginLeft: '3px',
}}
>
{unitText}
{unitText} min
</Typography>
</Typography>
</Box>
Expand Down
Loading