259 lines
10 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { PageLayout } from "@/components/templates/PageLayout";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { DevicePhoneMobileIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
import {
simActionsService,
type AvailablePlan,
} from "@/features/subscriptions/services/sim-actions.service";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { Formatting } from "@customer-portal/domain/toolkit";
import { Button } from "@/components/atoms";
const { formatCurrency } = Formatting;
export function SimChangePlanContainer() {
const params = useParams();
const subscriptionId = params.id as string;
const [plans, setPlans] = useState<AvailablePlan[]>([]);
const [selectedPlan, setSelectedPlan] = useState<AvailablePlan | null>(null);
const [assignGlobalIp, setAssignGlobalIp] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [loadingPlans, setLoadingPlans] = useState(true);
useEffect(() => {
const fetchPlans = async () => {
try {
const availablePlans = await simActionsService.getAvailablePlans(subscriptionId);
setPlans(availablePlans);
} catch (e: unknown) {
setError(
process.env.NODE_ENV === "development"
? e instanceof Error
? e.message
: "Failed to load available plans"
: "Unable to load available plans right now. Please try again."
);
} finally {
setLoadingPlans(false);
}
};
void fetchPlans();
}, [subscriptionId]);
const currentPlan = plans.find(p => p.isCurrentPlan);
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedPlan) {
setError("Please select a new plan");
return;
}
setLoading(true);
setMessage(null);
setError(null);
try {
const result = await simActionsService.changePlanFull(subscriptionId, {
newPlanCode: selectedPlan.freebitPlanCode,
newPlanSku: selectedPlan.sku,
newPlanName: selectedPlan.name,
assignGlobalIp,
});
setMessage(`Plan change scheduled for ${result.scheduledAt || "the 1st of next month"}`);
setSelectedPlan(null);
} catch (e: unknown) {
setError(
process.env.NODE_ENV === "development"
? e instanceof Error
? e.message
: "Failed to change plan"
: "Unable to submit your plan change right now. Please try again."
);
} finally {
setLoading(false);
}
};
return (
<PageLayout
icon={<DevicePhoneMobileIcon />}
title="Change Plan"
description="Switch to a different data plan"
>
<div className="max-w-4xl mx-auto">
<div className="mb-4">
<Link
href={`/account/subscriptions/${subscriptionId}#sim-management`}
className="text-primary hover:underline"
>
Back to SIM Management
</Link>
</div>
<SubCard>
<div className="mb-6">
<h2 className="text-lg font-semibold text-foreground mb-2">Change Your Plan</h2>
<p className="text-sm text-muted-foreground">
Select a new plan below. Plan changes will take effect on the 1st of the following
month. Changes must be requested before the 25th of the current month.
</p>
</div>
{message && (
<div className="mb-4">
<AlertBanner variant="success" title="Plan Change Submitted">
{message}
</AlertBanner>
</div>
)}
{error && (
<div className="mb-4">
<AlertBanner variant="error" title="Plan Change Failed">
{error}
</AlertBanner>
</div>
)}
{loadingPlans ? (
<div className="animate-pulse space-y-4">
{[1, 2, 3, 4].map(i => (
<div key={i} className="h-24 bg-muted rounded-lg"></div>
))}
</div>
) : (
<form onSubmit={e => void submit(e)} className="space-y-6">
{/* Current Plan */}
{currentPlan && (
<div className="bg-info-soft border border-info/25 rounded-lg p-4 mb-6">
<div className="flex items-center justify-between">
<div>
<div className="text-xs text-info font-medium uppercase">Current Plan</div>
<div className="text-lg font-semibold text-foreground">
{currentPlan.name}
</div>
<div className="text-sm text-muted-foreground">{currentPlan.simDataSize}</div>
</div>
<div className="text-right">
<div className="text-xl font-bold text-foreground">
{currentPlan.monthlyPrice ? formatCurrency(currentPlan.monthlyPrice) : "—"}
</div>
<div className="text-xs text-muted-foreground">/month</div>
</div>
</div>
</div>
)}
{/* Available Plans */}
<div className="space-y-3">
<label className="block text-sm font-medium text-muted-foreground">
Select a New Plan
</label>
<div className="grid gap-3">
{plans
.filter(p => !p.isCurrentPlan)
.map(plan => (
<label
key={plan.id}
className={`relative flex items-center justify-between p-4 border rounded-lg cursor-pointer transition-colors ${
selectedPlan?.id === plan.id
? "border-primary bg-primary-soft"
: "border-border hover:bg-muted bg-card"
}`}
>
<div className="flex items-center">
<input
type="radio"
name="plan"
value={plan.id}
checked={selectedPlan?.id === plan.id}
onChange={() => setSelectedPlan(plan)}
className="sr-only"
/>
<div
className={`w-5 h-5 rounded-full border-2 mr-4 flex items-center justify-center ${
selectedPlan?.id === plan.id ? "border-primary" : "border-border"
}`}
>
{selectedPlan?.id === plan.id && (
<div className="w-3 h-3 rounded-full bg-primary"></div>
)}
</div>
<div>
<div className="font-medium text-foreground">{plan.name}</div>
<div className="text-sm text-muted-foreground">
{plan.simDataSize} {plan.simPlanType}
</div>
</div>
</div>
<div className="text-right">
<div className="text-lg font-semibold text-foreground">
{plan.monthlyPrice ? formatCurrency(plan.monthlyPrice) : "—"}
</div>
<div className="text-xs text-muted-foreground">/month</div>
</div>
{selectedPlan?.id === plan.id && (
<CheckCircleIcon className="absolute top-2 right-2 h-5 w-5 text-primary" />
)}
</label>
))}
</div>
</div>
{/* Global IP Option */}
<div className="flex items-center p-4 bg-muted border border-border rounded-lg">
<input
id="globalip"
type="checkbox"
checked={assignGlobalIp}
onChange={e => setAssignGlobalIp(e.target.checked)}
className="h-4 w-4 text-primary border-input rounded focus:ring-ring focus:ring-2"
/>
<label htmlFor="globalip" className="ml-3 text-sm text-foreground/80">
Assign a global IP address (additional charges may apply)
</label>
</div>
{/* Info Box */}
<div className="bg-warning-soft border border-warning/25 rounded-lg p-4">
<h3 className="text-sm font-medium text-foreground mb-1">Important Notes</h3>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Plan changes take effect on the 1st of the following month</li>
<li> Requests must be made before the 25th of the current month</li>
<li> Your current data balance will be reset when the new plan activates</li>
</ul>
</div>
{/* Submit */}
<div className="flex gap-3">
<Button
type="submit"
disabled={loading || !selectedPlan}
loading={loading}
loadingText="Processing…"
>
Confirm Plan Change
</Button>
<Button
as="a"
href={`/account/subscriptions/${subscriptionId}#sim-management`}
variant="outline"
>
Cancel
</Button>
</div>
</form>
)}
</SubCard>
</div>
</PageLayout>
);
}
export default SimChangePlanContainer;