Refactor SIM management components and enhance user experience
- Updated CatalogPage and SimPlansPage to improve descriptions and layout for better clarity. - Removed unused SignalIcon from CatalogPage. - Enhanced ChangePlanModal to allow selection of new plans with improved validation and user feedback. - Updated SimActions to handle navigation and contextual information for actions. - Improved SimDetailsCard to format current plan codes for better readability. - Enhanced SimFeatureToggles to support embedded rendering and streamlined service options management. - Refactored SimManagementSection layout for better organization of SIM details and actions.
This commit is contained in:
parent
735828cf32
commit
9d4505d6be
@ -9,7 +9,6 @@ import {
|
|||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
WifiIcon,
|
WifiIcon,
|
||||||
GlobeAltIcon,
|
GlobeAltIcon,
|
||||||
SignalIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { AnimatedCard } from "@/components/catalog/animated-card";
|
import { AnimatedCard } from "@/components/catalog/animated-card";
|
||||||
import { AnimatedButton } from "@/components/catalog/animated-button";
|
import { AnimatedButton } from "@/components/catalog/animated-button";
|
||||||
@ -32,7 +31,7 @@ export default function CatalogPage() {
|
|||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed">
|
<p className="text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed">
|
||||||
Discover high-speed internet, flexible mobile plans, and secure VPN services. Each
|
Discover high-speed internet, wide range of mobile data options, and secure VPN services. Each
|
||||||
solution is personalized based on your location and account eligibility.
|
solution is personalized based on your location and account eligibility.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -57,13 +56,13 @@ export default function CatalogPage() {
|
|||||||
{/* SIM/eSIM Service */}
|
{/* SIM/eSIM Service */}
|
||||||
<ServiceHeroCard
|
<ServiceHeroCard
|
||||||
title="SIM & eSIM"
|
title="SIM & eSIM"
|
||||||
description="Flexible mobile data and voice plans with both physical SIM and eSIM options. Family discounts available."
|
description="Wide range of data options and voice plans with both physical SIM and eSIM options. Family discounts available."
|
||||||
icon={<DevicePhoneMobileIcon className="h-12 w-12" />}
|
icon={<DevicePhoneMobileIcon className="h-12 w-12" />}
|
||||||
features={[
|
features={[
|
||||||
"Physical SIM & eSIM",
|
"Physical SIM & eSIM",
|
||||||
"Data + Voice plans",
|
"Data + SMS/Voice plans",
|
||||||
"Family discounts",
|
"Family discounts",
|
||||||
"Flexible data sizes",
|
"Multiple data options",
|
||||||
]}
|
]}
|
||||||
href="/catalog/sim"
|
href="/catalog/sim"
|
||||||
color="green"
|
color="green"
|
||||||
@ -95,17 +94,12 @@ export default function CatalogPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
icon={<WifiIcon className="h-10 w-10 text-blue-600" />}
|
icon={<WifiIcon className="h-10 w-10 text-blue-600" />}
|
||||||
title="Location-Based Plans"
|
title="Location-Based Plans"
|
||||||
description="Internet plans tailored to your house type and available infrastructure"
|
description="Internet plans tailored to your house type and available infrastructure"
|
||||||
/>
|
/>
|
||||||
<FeatureCard
|
|
||||||
icon={<SignalIcon className="h-10 w-10 text-green-600" />}
|
|
||||||
title="Smart Recommendations"
|
|
||||||
description="Personalized plan suggestions based on your account and usage patterns"
|
|
||||||
/>
|
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
icon={<GlobeAltIcon className="h-10 w-10 text-purple-600" />}
|
icon={<GlobeAltIcon className="h-10 w-10 text-purple-600" />}
|
||||||
title="Seamless Integration"
|
title="Seamless Integration"
|
||||||
|
|||||||
@ -45,7 +45,7 @@ function PlanTypeSection({
|
|||||||
const familyPlans = plans.filter(p => p.hasFamilyDiscount);
|
const familyPlans = plans.filter(p => p.hasFamilyDiscount);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="animate-in fade-in duration-500">
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
{icon}
|
{icon}
|
||||||
<div>
|
<div>
|
||||||
@ -224,7 +224,7 @@ export default function SimPlansPage() {
|
|||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">Choose Your SIM Plan</h1>
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">Choose Your SIM Plan</h1>
|
||||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||||
Flexible mobile data and voice plans with both physical SIM and eSIM options.
|
Wide range of data options and voice plans with both physical SIM and eSIM options.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/* Family Discount Banner */}
|
{/* Family Discount Banner */}
|
||||||
@ -267,48 +267,54 @@ export default function SimPlansPage() {
|
|||||||
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("data-voice")}
|
onClick={() => setActiveTab("data-voice")}
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 ${
|
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-300 ease-in-out transform hover:scale-105 ${
|
||||||
activeTab === "data-voice"
|
activeTab === "data-voice"
|
||||||
? "border-blue-500 text-blue-600"
|
? "border-blue-500 text-blue-600"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<PhoneIcon className="h-5 w-5" />
|
<PhoneIcon className={`h-5 w-5 transition-transform duration-300 ${activeTab === "data-voice" ? "scale-110" : ""}`} />
|
||||||
Data + Voice
|
Data + SMS/Voice
|
||||||
{plansByType.DataSmsVoice.length > 0 && (
|
{plansByType.DataSmsVoice.length > 0 && (
|
||||||
<span className="bg-blue-100 text-blue-600 text-xs px-2 py-0.5 rounded-full">
|
<span className={`bg-blue-100 text-blue-600 text-xs px-2 py-0.5 rounded-full transition-all duration-300 ${
|
||||||
|
activeTab === "data-voice" ? "scale-110 bg-blue-200" : ""
|
||||||
|
}`}>
|
||||||
{plansByType.DataSmsVoice.length}
|
{plansByType.DataSmsVoice.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("data-only")}
|
onClick={() => setActiveTab("data-only")}
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 ${
|
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-300 ease-in-out transform hover:scale-105 ${
|
||||||
activeTab === "data-only"
|
activeTab === "data-only"
|
||||||
? "border-purple-500 text-purple-600"
|
? "border-purple-500 text-purple-600"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<GlobeAltIcon className="h-5 w-5" />
|
<GlobeAltIcon className={`h-5 w-5 transition-transform duration-300 ${activeTab === "data-only" ? "scale-110" : ""}`} />
|
||||||
Data Only
|
Data Only
|
||||||
{plansByType.DataOnly.length > 0 && (
|
{plansByType.DataOnly.length > 0 && (
|
||||||
<span className="bg-purple-100 text-purple-600 text-xs px-2 py-0.5 rounded-full">
|
<span className={`bg-purple-100 text-purple-600 text-xs px-2 py-0.5 rounded-full transition-all duration-300 ${
|
||||||
|
activeTab === "data-only" ? "scale-110 bg-purple-200" : ""
|
||||||
|
}`}>
|
||||||
{plansByType.DataOnly.length}
|
{plansByType.DataOnly.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("voice-only")}
|
onClick={() => setActiveTab("voice-only")}
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 ${
|
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-300 ease-in-out transform hover:scale-105 ${
|
||||||
activeTab === "voice-only"
|
activeTab === "voice-only"
|
||||||
? "border-orange-500 text-orange-600"
|
? "border-orange-500 text-orange-600"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<PhoneIcon className="h-5 w-5" />
|
<PhoneIcon className={`h-5 w-5 transition-transform duration-300 ${activeTab === "voice-only" ? "scale-110" : ""}`} />
|
||||||
Voice Only
|
Voice Only
|
||||||
{plansByType.VoiceOnly.length > 0 && (
|
{plansByType.VoiceOnly.length > 0 && (
|
||||||
<span className="bg-orange-100 text-orange-600 text-xs px-2 py-0.5 rounded-full">
|
<span className={`bg-orange-100 text-orange-600 text-xs px-2 py-0.5 rounded-full transition-all duration-300 ${
|
||||||
|
activeTab === "voice-only" ? "scale-110 bg-orange-200" : ""
|
||||||
|
}`}>
|
||||||
{plansByType.VoiceOnly.length}
|
{plansByType.VoiceOnly.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -318,49 +324,74 @@ export default function SimPlansPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
<div className="min-h-[400px]">
|
<div className="min-h-[400px] relative">
|
||||||
{activeTab === "data-voice" && (
|
<div className={`transition-all duration-500 ease-in-out ${
|
||||||
<PlanTypeSection
|
activeTab === "data-voice"
|
||||||
title="Data + Voice Plans"
|
? "opacity-100 translate-y-0"
|
||||||
description="Internet, calling, and SMS included"
|
: "opacity-0 translate-y-4 absolute inset-0 pointer-events-none"
|
||||||
icon={<PhoneIcon className="h-8 w-8 text-blue-600" />}
|
}`}>
|
||||||
plans={plansByType.DataSmsVoice}
|
{activeTab === "data-voice" && (
|
||||||
showFamilyDiscount={hasExistingSim}
|
<PlanTypeSection
|
||||||
/>
|
title="Data + SMS/Voice Plans"
|
||||||
)}
|
description="Internet, calling, and SMS included"
|
||||||
|
icon={<PhoneIcon className="h-8 w-8 text-blue-600" />}
|
||||||
|
plans={plansByType.DataSmsVoice}
|
||||||
|
showFamilyDiscount={hasExistingSim}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{activeTab === "data-only" && (
|
<div className={`transition-all duration-500 ease-in-out ${
|
||||||
<PlanTypeSection
|
activeTab === "data-only"
|
||||||
title="Data Only Plans"
|
? "opacity-100 translate-y-0"
|
||||||
description="Internet access for tablets, laptops, and IoT devices"
|
: "opacity-0 translate-y-4 absolute inset-0 pointer-events-none"
|
||||||
icon={<GlobeAltIcon className="h-8 w-8 text-purple-600" />}
|
}`}>
|
||||||
plans={plansByType.DataOnly}
|
{activeTab === "data-only" && (
|
||||||
showFamilyDiscount={hasExistingSim}
|
<PlanTypeSection
|
||||||
/>
|
title="Data Only Plans"
|
||||||
)}
|
description="Internet access for tablets, laptops, and IoT devices"
|
||||||
|
icon={<GlobeAltIcon className="h-8 w-8 text-purple-600" />}
|
||||||
|
plans={plansByType.DataOnly}
|
||||||
|
showFamilyDiscount={hasExistingSim}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{activeTab === "voice-only" && (
|
<div className={`transition-all duration-500 ease-in-out ${
|
||||||
<PlanTypeSection
|
activeTab === "voice-only"
|
||||||
title="Voice Only Plans"
|
? "opacity-100 translate-y-0"
|
||||||
description="Traditional calling and SMS without internet"
|
: "opacity-0 translate-y-4 absolute inset-0 pointer-events-none"
|
||||||
icon={<PhoneIcon className="h-8 w-8 text-orange-600" />}
|
}`}>
|
||||||
plans={plansByType.VoiceOnly}
|
{activeTab === "voice-only" && (
|
||||||
showFamilyDiscount={hasExistingSim}
|
<PlanTypeSection
|
||||||
/>
|
title="Voice Only Plans"
|
||||||
)}
|
description="Traditional calling and SMS without internet"
|
||||||
|
icon={<PhoneIcon className="h-8 w-8 text-orange-600" />}
|
||||||
|
plans={plansByType.VoiceOnly}
|
||||||
|
showFamilyDiscount={hasExistingSim}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features Section */}
|
{/* Features Section */}
|
||||||
<div className="mt-8 bg-gray-50 rounded-2xl p-8 max-w-4xl mx-auto">
|
<div className="mt-8 bg-gray-50 rounded-2xl p-8 max-w-4xl mx-auto">
|
||||||
<h3 className="font-bold text-gray-900 text-xl mb-6 text-center">
|
<h3 className="font-bold text-gray-900 text-xl mb-6 text-center">
|
||||||
All SIM Plans Include
|
Plan Features & Terms
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 text-sm">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 text-sm">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900">No Contract</div>
|
<div className="font-medium text-gray-900">3-Month Contract</div>
|
||||||
<div className="text-gray-600">Cancel anytime</div>
|
<div className="text-gray-600">Minimum 3 billing months</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-900">First Month Free</div>
|
||||||
|
<div className="text-gray-600">Basic fee waived initially</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
@ -384,19 +415,53 @@ export default function SimPlansPage() {
|
|||||||
<div className="text-gray-600">Multi-line savings</div>
|
<div className="text-gray-600">Multi-line savings</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-900">Plan Switching</div>
|
||||||
|
<div className="text-gray-600">Free data plan changes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info Section */}
|
{/* Info Section */}
|
||||||
<div className="mt-8 p-4 rounded-lg border border-blue-200 bg-blue-50 flex items-start gap-3 max-w-4xl mx-auto">
|
<div className="mt-8 p-6 rounded-lg border border-blue-200 bg-blue-50 max-w-4xl mx-auto">
|
||||||
<InformationCircleIcon className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
<div className="flex items-start gap-3 mb-4">
|
||||||
<div className="text-sm">
|
<InformationCircleIcon className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||||
<div className="font-medium text-blue-900 mb-1">Getting Started</div>
|
<div className="text-sm">
|
||||||
<p className="text-blue-800">
|
<div className="font-medium text-blue-900 mb-2">Important Terms & Conditions</div>
|
||||||
Choose your plan size, select eSIM or physical SIM, and configure optional add-ons
|
</div>
|
||||||
like voice mail and call waiting. Number porting is available if you want to keep your
|
</div>
|
||||||
existing phone number.
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
</p>
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-blue-900">Contract Period</div>
|
||||||
|
<p className="text-blue-800">Minimum 3 full billing months required. First month (sign-up to end of month) is free and doesn't count toward contract.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-blue-900">Billing Cycle</div>
|
||||||
|
<p className="text-blue-800">Monthly billing from 1st to end of month. Regular billing starts on 1st of following month after sign-up.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-blue-900">Cancellation</div>
|
||||||
|
<p className="text-blue-800">Can be requested online after 3rd month. Service terminates at end of billing cycle.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-blue-900">Plan Changes</div>
|
||||||
|
<p className="text-blue-800">Data plan switching is free and takes effect next month. Voice plan changes require new SIM and cancellation policies apply.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-blue-900">Calling/SMS Charges</div>
|
||||||
|
<p className="text-blue-800">Pay-per-use charges apply separately. Billed 5-6 weeks after usage within billing cycle.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-blue-900">SIM Replacement</div>
|
||||||
|
<p className="text-blue-800">Reissue fee of 1,500 JPY applies for damaged, lost, or replacement SIM cards.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
101
apps/portal/src/app/subscriptions/[id]/sim/change-plan/page.tsx
Normal file
101
apps/portal/src/app/subscriptions/[id]/sim/change-plan/page.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
||||||
|
import { authenticatedApi } from "@/lib/api";
|
||||||
|
|
||||||
|
const PLAN_CODES = ["PASI_5G", "PASI_10G", "PASI_25G", "PASI_50G"] as const;
|
||||||
|
type PlanCode = typeof PLAN_CODES[number];
|
||||||
|
const PLAN_LABELS: Record<PlanCode, string> = {
|
||||||
|
PASI_5G: "5GB",
|
||||||
|
PASI_10G: "10GB",
|
||||||
|
PASI_25G: "25GB",
|
||||||
|
PASI_50G: "50GB",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SimChangePlanPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const subscriptionId = parseInt(params.id as string);
|
||||||
|
const [currentPlanCode] = useState<string>("");
|
||||||
|
const [newPlanCode, setNewPlanCode] = useState<"" | PlanCode>("");
|
||||||
|
const [assignGlobalIp, setAssignGlobalIp] = useState(false);
|
||||||
|
const [scheduledAt, setScheduledAt] = useState("");
|
||||||
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const options = useMemo(() => (PLAN_CODES as readonly PlanCode[]).filter(c => c !== (currentPlanCode as PlanCode)), [currentPlanCode]);
|
||||||
|
|
||||||
|
const submit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newPlanCode) {
|
||||||
|
setError("Please select a new plan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
setMessage(null);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/change-plan`, {
|
||||||
|
newPlanCode,
|
||||||
|
assignGlobalIp,
|
||||||
|
scheduledAt: scheduledAt ? scheduledAt.replace(/-/g, "") : undefined,
|
||||||
|
});
|
||||||
|
setMessage("Plan change submitted successfully");
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e instanceof Error ? e.message : "Failed to change plan");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<div className="max-w-3xl mx-auto p-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Link href={`/subscriptions/${subscriptionId}#sim-management`} className="text-blue-600 hover:text-blue-700">← Back to SIM Management</Link>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||||
|
<h1 className="text-xl font-semibold text-gray-900 mb-1">Change Plan</h1>
|
||||||
|
<p className="text-sm text-gray-600 mb-6">Switch to a different data plan. Important: request before the 25th; takes effect on the 1st.</p>
|
||||||
|
|
||||||
|
{message && <div className="mb-4 text-green-700 bg-green-50 border border-green-200 rounded p-3">{message}</div>}
|
||||||
|
{error && <div className="mb-4 text-red-700 bg-red-50 border border-red-200 rounded p-3">{error}</div>}
|
||||||
|
|
||||||
|
<form onSubmit={submit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">New Plan</label>
|
||||||
|
<select
|
||||||
|
value={newPlanCode}
|
||||||
|
onChange={(e) => setNewPlanCode(e.target.value as PlanCode)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option value="">Choose a plan</option>
|
||||||
|
{options.map(code => (
|
||||||
|
<option key={code} value={code}>{PLAN_LABELS[code]}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input id="globalip" type="checkbox" checked={assignGlobalIp} onChange={(e)=>setAssignGlobalIp(e.target.checked)} className="h-4 w-4 text-blue-600 border-gray-300 rounded" />
|
||||||
|
<label htmlFor="globalip" className="ml-2 text-sm text-gray-700">Assign global IP</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Schedule (optional)</label>
|
||||||
|
<input type="date" value={scheduledAt} onChange={(e)=>setScheduledAt(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button type="submit" disabled={loading} className="px-4 py-2 rounded-md bg-blue-600 text-white text-sm disabled:opacity-50">{loading ? 'Processing…' : 'Submit Plan Change'}</button>
|
||||||
|
<Link href={`/subscriptions/${subscriptionId}#sim-management`} className="px-4 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50">Back</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export default function Page(){return null}
|
||||||
103
apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx
Normal file
103
apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
||||||
|
import { authenticatedApi } from "@/lib/api";
|
||||||
|
|
||||||
|
const PRESETS = [1024, 2048, 5120, 10240, 20480, 51200];
|
||||||
|
|
||||||
|
export default function SimTopUpPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const subscriptionId = parseInt(params.id as string);
|
||||||
|
const [amountMb, setAmountMb] = useState<number>(2048);
|
||||||
|
const [scheduledAt, setScheduledAt] = useState("");
|
||||||
|
const [campaignCode, setCampaignCode] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const format = (mb: number) => (mb % 1024 === 0 ? `${mb / 1024} GB` : `${mb} MB`);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setMessage(null);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/top-up`, {
|
||||||
|
quotaMb: amountMb,
|
||||||
|
campaignCode: campaignCode || undefined,
|
||||||
|
scheduledAt: scheduledAt ? scheduledAt.replace(/-/g, "") : undefined,
|
||||||
|
});
|
||||||
|
setMessage("Top-up submitted successfully");
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e instanceof Error ? e.message : "Failed to submit top-up");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<div className="max-w-3xl mx-auto p-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Link href={`/subscriptions/${subscriptionId}#sim-management`} className="text-blue-600 hover:text-blue-700">← Back to SIM Management</Link>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||||
|
<h1 className="text-xl font-semibold text-gray-900 mb-1">Top Up Data</h1>
|
||||||
|
<p className="text-sm text-gray-600 mb-6">Add data quota to your SIM service</p>
|
||||||
|
|
||||||
|
{message && <div className="mb-4 text-green-700 bg-green-50 border border-green-200 rounded p-3">{message}</div>}
|
||||||
|
{error && <div className="mb-4 text-red-700 bg-red-50 border border-red-200 rounded p-3">{error}</div>}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Amount</label>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
{PRESETS.map(mb => (
|
||||||
|
<button
|
||||||
|
key={mb}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAmountMb(mb)}
|
||||||
|
className={`px-4 py-2 rounded-lg border text-sm ${amountMb === mb ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'}`}
|
||||||
|
>
|
||||||
|
{format(mb)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Campaign Code (optional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={campaignCode}
|
||||||
|
onChange={(e) => setCampaignCode(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||||
|
placeholder="Enter code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Schedule (optional)</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={scheduledAt}
|
||||||
|
onChange={(e) => setScheduledAt(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Leave empty to apply immediately</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button type="submit" disabled={loading} className="px-4 py-2 rounded-md bg-blue-600 text-white text-sm disabled:opacity-50">{loading ? 'Processing…' : 'Submit Top-Up'}</button>
|
||||||
|
<Link href={`/subscriptions/${subscriptionId}#sim-management`} className="px-4 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50">Back</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -6,26 +6,38 @@ import { XMarkIcon } from "@heroicons/react/24/outline";
|
|||||||
|
|
||||||
interface ChangePlanModalProps {
|
interface ChangePlanModalProps {
|
||||||
subscriptionId: number;
|
subscriptionId: number;
|
||||||
|
currentPlanCode?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
onError: (message: string) => void;
|
onError: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChangePlanModal({ subscriptionId, onClose, onSuccess, onError }: ChangePlanModalProps) {
|
export function ChangePlanModal({ subscriptionId, currentPlanCode, onClose, onSuccess, onError }: ChangePlanModalProps) {
|
||||||
const [newPlanCode, setNewPlanCode] = useState("");
|
const PLAN_CODES = ["PASI_5G", "PASI_10G", "PASI_25G", "PASI_50G"] as const;
|
||||||
|
type PlanCode = typeof PLAN_CODES[number];
|
||||||
|
const PLAN_LABELS: Record<PlanCode, string> = {
|
||||||
|
PASI_5G: "5GB",
|
||||||
|
PASI_10G: "10GB",
|
||||||
|
PASI_25G: "25GB",
|
||||||
|
PASI_50G: "50GB",
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowedPlans = (PLAN_CODES as readonly PlanCode[]).filter(code => code !== (currentPlanCode || ''));
|
||||||
|
|
||||||
|
const [newPlanCode, setNewPlanCode] = useState<"" | PlanCode>("");
|
||||||
const [assignGlobalIp, setAssignGlobalIp] = useState(false);
|
const [assignGlobalIp, setAssignGlobalIp] = useState(false);
|
||||||
const [scheduledAt, setScheduledAt] = useState(""); // YYYY-MM-DD
|
const [scheduledAt, setScheduledAt] = useState(""); // YYYY-MM-DD
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!newPlanCode.trim()) {
|
if (!newPlanCode) {
|
||||||
onError("Please enter a new plan code");
|
onError("Please select a new plan");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/change-plan`, {
|
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/change-plan`, {
|
||||||
newPlanCode: newPlanCode.trim(),
|
newPlanCode: newPlanCode,
|
||||||
assignGlobalIp,
|
assignGlobalIp,
|
||||||
scheduledAt: scheduledAt ? scheduledAt.replaceAll("-", "") : undefined,
|
scheduledAt: scheduledAt ? scheduledAt.replaceAll("-", "") : undefined,
|
||||||
});
|
});
|
||||||
@ -55,14 +67,18 @@ export function ChangePlanModal({ subscriptionId, onClose, onSuccess, onError }:
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">New Plan Code</label>
|
<label className="block text-sm font-medium text-gray-700">Select New Plan</label>
|
||||||
<input
|
<select
|
||||||
type="text"
|
|
||||||
value={newPlanCode}
|
value={newPlanCode}
|
||||||
onChange={(e) => setNewPlanCode(e.target.value)}
|
onChange={(e) => setNewPlanCode(e.target.value as PlanCode)}
|
||||||
placeholder="e.g. LTE3G_P01"
|
|
||||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
|
||||||
/>
|
>
|
||||||
|
<option value="">Choose a plan</option>
|
||||||
|
{allowedPlans.map(code => (
|
||||||
|
<option key={code} value={code}>{PLAN_LABELS[code]}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">Only plans different from your current plan are listed.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
@ -105,7 +121,7 @@ export function ChangePlanModal({ subscriptionId, onClose, onSuccess, onError }:
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
>
|
>
|
||||||
Cancel
|
Back
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -113,4 +129,3 @@ export function ChangePlanModal({ subscriptionId, onClose, onSuccess, onError }:
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
ArrowPathIcon,
|
ArrowPathIcon,
|
||||||
@ -21,6 +22,7 @@ interface SimActionsProps {
|
|||||||
onCancelSuccess?: () => void;
|
onCancelSuccess?: () => void;
|
||||||
onReissueSuccess?: () => void;
|
onReissueSuccess?: () => void;
|
||||||
embedded?: boolean; // when true, render content without card container
|
embedded?: boolean; // when true, render content without card container
|
||||||
|
currentPlanCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SimActions({
|
export function SimActions({
|
||||||
@ -31,8 +33,10 @@ export function SimActions({
|
|||||||
onPlanChangeSuccess,
|
onPlanChangeSuccess,
|
||||||
onCancelSuccess,
|
onCancelSuccess,
|
||||||
onReissueSuccess,
|
onReissueSuccess,
|
||||||
embedded = false
|
embedded = false,
|
||||||
|
currentPlanCode
|
||||||
}: SimActionsProps) {
|
}: SimActionsProps) {
|
||||||
|
const router = useRouter();
|
||||||
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
||||||
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
||||||
const [showReissueConfirm, setShowReissueConfirm] = useState(false);
|
const [showReissueConfirm, setShowReissueConfirm] = useState(false);
|
||||||
@ -40,6 +44,9 @@ export function SimActions({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
const [showChangePlanModal, setShowChangePlanModal] = useState(false);
|
const [showChangePlanModal, setShowChangePlanModal] = useState(false);
|
||||||
|
const [activeInfo, setActiveInfo] = useState<
|
||||||
|
'topup' | 'reissue' | 'cancel' | 'changePlan' | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const isActive = status === 'active';
|
const isActive = status === 'active';
|
||||||
const canTopUp = isActive;
|
const canTopUp = isActive;
|
||||||
@ -145,7 +152,14 @@ export function SimActions({
|
|||||||
<div className={`grid gap-4 ${embedded ? 'grid-cols-1' : 'grid-cols-2'}`}>
|
<div className={`grid gap-4 ${embedded ? 'grid-cols-1' : 'grid-cols-2'}`}>
|
||||||
{/* Top Up Data - Primary Action */}
|
{/* Top Up Data - Primary Action */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowTopUpModal(true)}
|
onClick={() => {
|
||||||
|
setActiveInfo('topup');
|
||||||
|
try {
|
||||||
|
router.push(`/subscriptions/${subscriptionId}/sim/top-up`);
|
||||||
|
} catch {
|
||||||
|
setShowTopUpModal(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
disabled={!canTopUp || loading !== null}
|
disabled={!canTopUp || loading !== null}
|
||||||
className={`group relative flex items-center justify-center px-6 py-4 border rounded-xl text-sm font-semibold transition-all duration-200 ${
|
className={`group relative flex items-center justify-center px-6 py-4 border rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||||
canTopUp && loading === null
|
canTopUp && loading === null
|
||||||
@ -164,7 +178,14 @@ export function SimActions({
|
|||||||
{/* Reissue eSIM (only for eSIMs) */}
|
{/* Reissue eSIM (only for eSIMs) */}
|
||||||
{simType === 'esim' && (
|
{simType === 'esim' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowReissueConfirm(true)}
|
onClick={() => {
|
||||||
|
setActiveInfo('reissue');
|
||||||
|
try {
|
||||||
|
router.push(`/subscriptions/${subscriptionId}/sim/reissue`);
|
||||||
|
} catch {
|
||||||
|
setShowReissueConfirm(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
disabled={!canReissue || loading !== null}
|
disabled={!canReissue || loading !== null}
|
||||||
className={`group relative flex items-center justify-center px-6 py-4 border rounded-xl text-sm font-semibold transition-all duration-200 ${
|
className={`group relative flex items-center justify-center px-6 py-4 border rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||||
canReissue && loading === null
|
canReissue && loading === null
|
||||||
@ -183,7 +204,11 @@ export function SimActions({
|
|||||||
|
|
||||||
{/* Cancel SIM - Destructive Action */}
|
{/* Cancel SIM - Destructive Action */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCancelConfirm(true)}
|
onClick={() => {
|
||||||
|
setActiveInfo('cancel');
|
||||||
|
// keep inline confirm for cancel to avoid accidental navigation
|
||||||
|
setShowCancelConfirm(true);
|
||||||
|
}}
|
||||||
disabled={!canCancel || loading !== null}
|
disabled={!canCancel || loading !== null}
|
||||||
className={`group relative flex items-center justify-center px-6 py-4 border rounded-xl text-sm font-semibold transition-all duration-200 ${
|
className={`group relative flex items-center justify-center px-6 py-4 border rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||||
canCancel && loading === null
|
canCancel && loading === null
|
||||||
@ -201,10 +226,17 @@ export function SimActions({
|
|||||||
|
|
||||||
{/* Change Plan - Secondary Action */}
|
{/* Change Plan - Secondary Action */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {/* Add change plan functionality */}}
|
onClick={() => {
|
||||||
disabled={!isActive || loading !== null}
|
setActiveInfo('changePlan');
|
||||||
|
try {
|
||||||
|
router.push(`/subscriptions/${subscriptionId}/sim/change-plan`);
|
||||||
|
} catch {
|
||||||
|
setShowChangePlanModal(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={loading !== null}
|
||||||
className={`group relative flex items-center justify-center px-6 py-4 border-2 border-dashed rounded-xl text-sm font-semibold transition-all duration-200 ${
|
className={`group relative flex items-center justify-center px-6 py-4 border-2 border-dashed rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||||
isActive && loading === null
|
loading === null
|
||||||
? 'text-purple-700 bg-purple-50 border-purple-300 hover:bg-purple-100 hover:border-purple-400 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500'
|
? 'text-purple-700 bg-purple-50 border-purple-300 hover:bg-purple-100 hover:border-purple-400 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500'
|
||||||
: 'text-gray-400 bg-gray-50 border-gray-300 cursor-not-allowed'
|
: 'text-gray-400 bg-gray-50 border-gray-300 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
@ -220,47 +252,52 @@ export function SimActions({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Descriptions */}
|
{/* Action Description (contextual) */}
|
||||||
<div className="mt-6 space-y-3 text-sm text-gray-600">
|
{activeInfo && (
|
||||||
<div className="flex items-start">
|
<div className="mt-6 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||||
<PlusIcon className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
|
{activeInfo === 'topup' && (
|
||||||
<div>
|
<div className="flex items-start">
|
||||||
<strong>Top Up Data:</strong> Add additional data quota to your SIM service. You can choose the amount and schedule it for later if needed.
|
<PlusIcon className="h-4 w-4 text-blue-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<strong>Top Up Data:</strong> Add additional data quota to your SIM service. You can choose the amount and schedule it for later if needed.
|
||||||
|
</div>
|
||||||
{simType === 'esim' && (
|
|
||||||
<div className="flex items-start">
|
|
||||||
<ArrowPathIcon className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
|
|
||||||
<div>
|
|
||||||
<strong>Reissue eSIM:</strong> Generate a new eSIM profile for download. Use this if your previous download failed or you need to install on a new device.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{activeInfo === 'reissue' && (
|
||||||
|
<div className="flex items-start">
|
||||||
<div className="flex items-start">
|
<ArrowPathIcon className="h-4 w-4 text-green-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||||
<XMarkIcon className="h-4 w-4 text-red-500 mr-2 mt-0.5 flex-shrink-0" />
|
<div>
|
||||||
<div>
|
<strong>Reissue eSIM:</strong> Generate a new eSIM profile for download. Use this if your previous download failed or you need to install on a new device.
|
||||||
<strong>Cancel SIM:</strong> Permanently cancel your SIM service. This action cannot be undone and will terminate your service immediately.
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{activeInfo === 'cancel' && (
|
||||||
|
<div className="flex items-start">
|
||||||
|
<XMarkIcon className="h-4 w-4 text-red-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<strong>Cancel SIM:</strong> Permanently cancel your SIM service. This action cannot be undone and will terminate your service immediately.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activeInfo === 'changePlan' && (
|
||||||
|
<div className="flex items-start">
|
||||||
|
<svg className="h-4 w-4 text-purple-600 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<strong>Change Plan:</strong> Switch to a different data plan. <span className="text-red-600 font-medium">Important: Plan changes must be requested before the 25th of the month. Changes will take effect on the 1st of the following month.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-start">
|
|
||||||
<svg className="h-4 w-4 text-purple-500 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<strong>Change Plan:</strong> Switch to a different data plan. <span className="text-red-600 font-medium">Important: Plan changes must be requested before the 25th of the month. Changes will take effect on the 1st of the following month.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Top Up Modal */}
|
{/* Top Up Modal */}
|
||||||
{showTopUpModal && (
|
{showTopUpModal && (
|
||||||
<TopUpModal
|
<TopUpModal
|
||||||
subscriptionId={subscriptionId}
|
subscriptionId={subscriptionId}
|
||||||
onClose={() => setShowTopUpModal(false)}
|
onClose={() => { setShowTopUpModal(false); setActiveInfo(null); }}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setShowTopUpModal(false);
|
setShowTopUpModal(false);
|
||||||
setSuccess('Data top-up completed successfully');
|
setSuccess('Data top-up completed successfully');
|
||||||
@ -270,7 +307,20 @@ export function SimActions({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Change Plan handled in Feature Toggles */}
|
{/* Change Plan Modal */}
|
||||||
|
{showChangePlanModal && (
|
||||||
|
<ChangePlanModal
|
||||||
|
subscriptionId={subscriptionId}
|
||||||
|
currentPlanCode={currentPlanCode}
|
||||||
|
onClose={() => { setShowChangePlanModal(false); setActiveInfo(null); }}
|
||||||
|
onSuccess={() => {
|
||||||
|
setShowChangePlanModal(false);
|
||||||
|
setSuccess('SIM plan change submitted successfully');
|
||||||
|
onPlanChangeSuccess?.();
|
||||||
|
}}
|
||||||
|
onError={(message) => setError(message)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Reissue eSIM Confirmation */}
|
{/* Reissue eSIM Confirmation */}
|
||||||
{showReissueConfirm && (
|
{showReissueConfirm && (
|
||||||
@ -304,11 +354,11 @@ export function SimActions({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowReissueConfirm(false)}
|
onClick={() => { setShowReissueConfirm(false); setActiveInfo(null); }}
|
||||||
disabled={loading === 'reissue'}
|
disabled={loading === 'reissue'}
|
||||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
>
|
>
|
||||||
Cancel
|
Back
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -348,11 +398,11 @@ export function SimActions({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowCancelConfirm(false)}
|
onClick={() => { setShowCancelConfirm(false); setActiveInfo(null); }}
|
||||||
disabled={loading === 'cancel'}
|
disabled={loading === 'cancel'}
|
||||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
>
|
>
|
||||||
Cancel
|
Back
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -47,6 +47,15 @@ interface SimDetailsCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SimDetailsCard({ simDetails, isLoading, error, embedded = false, showFeaturesSummary = true }: SimDetailsCardProps) {
|
export function SimDetailsCard({ simDetails, isLoading, error, embedded = false, showFeaturesSummary = true }: SimDetailsCardProps) {
|
||||||
|
const formatPlan = (code?: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
PASI_5G: '5GB Plan',
|
||||||
|
PASI_10G: '10GB Plan',
|
||||||
|
PASI_25G: '25GB Plan',
|
||||||
|
PASI_50G: '50GB Plan',
|
||||||
|
};
|
||||||
|
return (code && map[code]) || code || '—';
|
||||||
|
};
|
||||||
const getStatusIcon = (status: string) => {
|
const getStatusIcon = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'active':
|
case 'active':
|
||||||
@ -146,7 +155,7 @@ export function SimDetailsCard({ simDetails, isLoading, error, embedded = false,
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold text-gray-900">eSIM Details</h3>
|
<h3 className="text-xl font-semibold text-gray-900">eSIM Details</h3>
|
||||||
<p className="text-sm text-gray-600 font-medium">Current Plan: {simDetails.planCode}</p>
|
<p className="text-sm text-gray-600 font-medium">Current Plan: {formatPlan(simDetails.planCode)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={`inline-flex px-4 py-2 text-sm font-semibold rounded-full ${getStatusColor(simDetails.status)} self-start sm:self-auto`}>
|
<span className={`inline-flex px-4 py-2 text-sm font-semibold rounded-full ${getStatusColor(simDetails.status)} self-start sm:self-auto`}>
|
||||||
@ -229,7 +238,7 @@ export function SimDetailsCard({ simDetails, isLoading, error, embedded = false,
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-gray-900">Physical SIM Details</h3>
|
<h3 className="text-lg font-medium text-gray-900">Physical SIM Details</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{simDetails.planCode} • {`${simDetails.size} SIM`}
|
{formatPlan(simDetails.planCode)} • {`${simDetails.size} SIM`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { authenticatedApi } from "@/lib/api";
|
import { authenticatedApi } from "@/lib/api";
|
||||||
import type { SimPlan } from "@/shared/types/catalog.types";
|
|
||||||
|
|
||||||
interface SimFeatureTogglesProps {
|
interface SimFeatureTogglesProps {
|
||||||
subscriptionId: number;
|
subscriptionId: number;
|
||||||
@ -10,8 +9,8 @@ interface SimFeatureTogglesProps {
|
|||||||
callWaitingEnabled?: boolean;
|
callWaitingEnabled?: boolean;
|
||||||
internationalRoamingEnabled?: boolean;
|
internationalRoamingEnabled?: boolean;
|
||||||
networkType?: string; // '4G' | '5G'
|
networkType?: string; // '4G' | '5G'
|
||||||
currentPlanCode?: string;
|
|
||||||
onChanged?: () => void;
|
onChanged?: () => void;
|
||||||
|
embedded?: boolean; // when true, render without outer card wrappers
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SimFeatureToggles({
|
export function SimFeatureToggles({
|
||||||
@ -20,8 +19,8 @@ export function SimFeatureToggles({
|
|||||||
callWaitingEnabled,
|
callWaitingEnabled,
|
||||||
internationalRoamingEnabled,
|
internationalRoamingEnabled,
|
||||||
networkType,
|
networkType,
|
||||||
currentPlanCode,
|
|
||||||
onChanged,
|
onChanged,
|
||||||
|
embedded = false,
|
||||||
}: SimFeatureTogglesProps) {
|
}: SimFeatureTogglesProps) {
|
||||||
// Initial values
|
// Initial values
|
||||||
const initial = useMemo(() => ({
|
const initial = useMemo(() => ({
|
||||||
@ -29,18 +28,13 @@ export function SimFeatureToggles({
|
|||||||
cw: !!callWaitingEnabled,
|
cw: !!callWaitingEnabled,
|
||||||
ir: !!internationalRoamingEnabled,
|
ir: !!internationalRoamingEnabled,
|
||||||
nt: networkType === '5G' ? '5G' : '4G',
|
nt: networkType === '5G' ? '5G' : '4G',
|
||||||
plan: currentPlanCode || '',
|
}), [voiceMailEnabled, callWaitingEnabled, internationalRoamingEnabled, networkType]);
|
||||||
}), [voiceMailEnabled, callWaitingEnabled, internationalRoamingEnabled, networkType, currentPlanCode]);
|
|
||||||
|
|
||||||
// Working values
|
// Working values
|
||||||
const [vm, setVm] = useState(initial.vm);
|
const [vm, setVm] = useState(initial.vm);
|
||||||
const [cw, setCw] = useState(initial.cw);
|
const [cw, setCw] = useState(initial.cw);
|
||||||
const [ir, setIr] = useState(initial.ir);
|
const [ir, setIr] = useState(initial.ir);
|
||||||
const [nt, setNt] = useState<'4G' | '5G'>(initial.nt as '4G' | '5G');
|
const [nt, setNt] = useState<'4G' | '5G'>(initial.nt as '4G' | '5G');
|
||||||
const [plan, setPlan] = useState(initial.plan);
|
|
||||||
|
|
||||||
// Plans list
|
|
||||||
const [plans, setPlans] = useState<SimPlan[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
@ -50,28 +44,13 @@ export function SimFeatureToggles({
|
|||||||
setCw(initial.cw);
|
setCw(initial.cw);
|
||||||
setIr(initial.ir);
|
setIr(initial.ir);
|
||||||
setNt(initial.nt as '4G' | '5G');
|
setNt(initial.nt as '4G' | '5G');
|
||||||
setPlan(initial.plan);
|
}, [initial.vm, initial.cw, initial.ir, initial.nt]);
|
||||||
}, [initial.vm, initial.cw, initial.ir, initial.nt, initial.plan]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let ignore = false;
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const data = await authenticatedApi.get<SimPlan[]>("/catalog/sim/plans");
|
|
||||||
if (!ignore) setPlans(data);
|
|
||||||
} catch (e) {
|
|
||||||
// silent; leave plans empty
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return () => { ignore = true; };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setVm(initial.vm);
|
setVm(initial.vm);
|
||||||
setCw(initial.cw);
|
setCw(initial.cw);
|
||||||
setIr(initial.ir);
|
setIr(initial.ir);
|
||||||
setNt(initial.nt as '4G' | '5G');
|
setNt(initial.nt as '4G' | '5G');
|
||||||
setPlan(initial.plan);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setSuccess(null);
|
setSuccess(null);
|
||||||
};
|
};
|
||||||
@ -91,10 +70,6 @@ export function SimFeatureToggles({
|
|||||||
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/features`, featurePayload);
|
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/features`, featurePayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plan && plan !== initial.plan) {
|
|
||||||
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/change-plan`, { newPlanCode: plan });
|
|
||||||
}
|
|
||||||
|
|
||||||
setSuccess('Changes submitted successfully');
|
setSuccess('Changes submitted successfully');
|
||||||
onChanged?.();
|
onChanged?.();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -109,9 +84,9 @@ export function SimFeatureToggles({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
||||||
{/* Service Options */}
|
{/* Service Options */}
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
<div className={`${embedded ? '' : 'bg-white rounded-xl border border-gray-200 overflow-hidden'}`}>
|
||||||
|
|
||||||
<div className="p-6 space-y-6">
|
<div className={`${embedded ? '' : 'p-6'} space-y-6`}>
|
||||||
{/* Voice Mail */}
|
{/* Voice Mail */}
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -249,7 +224,7 @@ export function SimFeatureToggles({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Notes and Actions */}
|
{/* Notes and Actions */}
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
<div className={`${embedded ? '' : 'bg-white rounded-xl border border-gray-200 p-6'}`}>
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<svg className="h-5 w-5 text-yellow-600 mt-0.5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="h-5 w-5 text-yellow-600 mt-0.5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@ -128,49 +128,54 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
|
|||||||
{/* SIM Details and Usage - Main Content */}
|
{/* SIM Details and Usage - Main Content */}
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||||||
{/* Main Content Area - Actions and Settings (Left Side) */}
|
{/* Main Content Area - Actions and Settings (Left Side) */}
|
||||||
<div className="xl:col-span-2 xl:order-1 space-y-8">
|
<div className="order-2 xl:col-span-2 xl:order-1">
|
||||||
{/* SIM Management Actions */}
|
|
||||||
<SimActions
|
|
||||||
subscriptionId={subscriptionId}
|
|
||||||
simType={simInfo.details.simType}
|
|
||||||
status={simInfo.details.status}
|
|
||||||
onTopUpSuccess={handleActionSuccess}
|
|
||||||
onPlanChangeSuccess={handleActionSuccess}
|
|
||||||
onCancelSuccess={handleActionSuccess}
|
|
||||||
onReissueSuccess={handleActionSuccess}
|
|
||||||
embedded={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Plan Settings Card */}
|
|
||||||
<div className="bg-white shadow-lg rounded-xl border border-gray-100 p-6">
|
<div className="bg-white shadow-lg rounded-xl border border-gray-100 p-6">
|
||||||
<div className="flex items-center mb-6">
|
<SimActions
|
||||||
<div className="bg-green-50 rounded-xl p-2 mr-3">
|
|
||||||
<svg className="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Plan Settings</h3>
|
|
||||||
<p className="text-sm text-gray-600">Modify service options</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SimFeatureToggles
|
|
||||||
subscriptionId={subscriptionId}
|
subscriptionId={subscriptionId}
|
||||||
voiceMailEnabled={simInfo.details.voiceMailEnabled}
|
simType={simInfo.details.simType}
|
||||||
callWaitingEnabled={simInfo.details.callWaitingEnabled}
|
status={simInfo.details.status}
|
||||||
internationalRoamingEnabled={simInfo.details.internationalRoamingEnabled}
|
|
||||||
networkType={simInfo.details.networkType}
|
|
||||||
currentPlanCode={simInfo.details.planCode}
|
currentPlanCode={simInfo.details.planCode}
|
||||||
onChanged={handleActionSuccess}
|
onTopUpSuccess={handleActionSuccess}
|
||||||
|
onPlanChangeSuccess={handleActionSuccess}
|
||||||
|
onCancelSuccess={handleActionSuccess}
|
||||||
|
onReissueSuccess={handleActionSuccess}
|
||||||
|
embedded={true}
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-6">
|
||||||
|
<p className="text-sm text-gray-600 font-medium mb-3">Modify service options</p>
|
||||||
|
<SimFeatureToggles
|
||||||
|
subscriptionId={subscriptionId}
|
||||||
|
voiceMailEnabled={simInfo.details.voiceMailEnabled}
|
||||||
|
callWaitingEnabled={simInfo.details.callWaitingEnabled}
|
||||||
|
internationalRoamingEnabled={simInfo.details.internationalRoamingEnabled}
|
||||||
|
networkType={simInfo.details.networkType}
|
||||||
|
onChanged={handleActionSuccess}
|
||||||
|
embedded
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar - Compact Info (Right Side) */}
|
{/* Sidebar - Compact Info (Right Side) */}
|
||||||
<div className="xl:order-2 space-y-8">
|
<div className="order-1 xl:order-2 space-y-8">
|
||||||
|
{/* Details + Usage combined card for mobile-first */}
|
||||||
|
<div className="bg-white shadow-lg rounded-xl border border-gray-100 p-6 space-y-6">
|
||||||
|
<SimDetailsCard
|
||||||
|
simDetails={simInfo.details}
|
||||||
|
isLoading={false}
|
||||||
|
error={null}
|
||||||
|
embedded={true}
|
||||||
|
showFeaturesSummary={false}
|
||||||
|
/>
|
||||||
|
<DataUsageChart
|
||||||
|
usage={simInfo.usage}
|
||||||
|
remainingQuotaMb={simInfo.details.remainingQuotaMb}
|
||||||
|
isLoading={false}
|
||||||
|
error={null}
|
||||||
|
embedded={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Important Information Card */}
|
{/* Important Information Card */}
|
||||||
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200 rounded-xl p-6">
|
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200 rounded-xl p-6">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
@ -203,21 +208,7 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SimDetailsCard
|
{/* (On desktop, details+usage are above; on mobile they appear first since this section is above actions) */}
|
||||||
simDetails={simInfo.details}
|
|
||||||
isLoading={false}
|
|
||||||
error={null}
|
|
||||||
embedded={true}
|
|
||||||
showFeaturesSummary={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataUsageChart
|
|
||||||
usage={simInfo.usage}
|
|
||||||
remainingQuotaMb={simInfo.details.remainingQuotaMb}
|
|
||||||
isLoading={false}
|
|
||||||
error={null}
|
|
||||||
embedded={true}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -248,7 +248,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full sm:w-auto px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
className="w-full sm:w-auto px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Cancel
|
Back
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
466
docs/SIM-MANAGEMENT-API-DATA-FLOW.md
Normal file
466
docs/SIM-MANAGEMENT-API-DATA-FLOW.md
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
# SIM Management Page - API Data Flow & System Architecture
|
||||||
|
|
||||||
|
*Technical documentation explaining the API integration and data flow for the SIM Management interface*
|
||||||
|
|
||||||
|
**Purpose**: This document provides a detailed explanation of how the SIM Management page retrieves, processes, and displays data through various API integrations.
|
||||||
|
|
||||||
|
**Audience**: Management, Technical Teams, System Architects
|
||||||
|
**Last Updated**: September 2025
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Executive Summary
|
||||||
|
|
||||||
|
Change Log (2025-09-05)
|
||||||
|
- Adopted official Freebit API names across all callouts (e.g., "Add Specs & Quota", "MVNO Plan Change").
|
||||||
|
- Added Freebit API Quick Reference (Portal Operations) table.
|
||||||
|
- Documented Top‑Up Payment Flow (WHMCS invoice + auto‑capture then Freebit AddSpec).
|
||||||
|
- Listed additional Freebit APIs not used by the portal today.
|
||||||
|
|
||||||
|
The SIM Management page integrates with multiple backend systems to provide real-time SIM data, usage statistics, and management capabilities. The system uses a **Backend-for-Frontend (BFF)** architecture that aggregates data from Freebit APIs and WHMCS, providing a unified interface for SIM management operations.
|
||||||
|
|
||||||
|
### Key Systems Integration:
|
||||||
|
- **WHMCS**: Subscription and billing data
|
||||||
|
- **Freebit API**: SIM details, usage, and management operations
|
||||||
|
- **Customer Portal BFF**: Data aggregation and API orchestration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ System Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Customer Portal Frontend │
|
||||||
|
│ (Next.js - Port 3000) │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ SIM Management Page Components: │
|
||||||
|
│ • SimManagementSection.tsx │
|
||||||
|
│ • SimDetailsCard.tsx │
|
||||||
|
│ • DataUsageChart.tsx │
|
||||||
|
│ • SimActions.tsx │
|
||||||
|
│ • SimFeatureToggles.tsx │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ HTTP Requests
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Backend-for-Frontend (BFF) │
|
||||||
|
│ (Port 4000) │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ API Endpoints: │
|
||||||
|
│ • /api/subscriptions/{id}/sim │
|
||||||
|
│ • /api/subscriptions/{id}/sim/details │
|
||||||
|
│ • /api/subscriptions/{id}/sim/usage │
|
||||||
|
│ • /api/subscriptions/{id}/sim/top-up │
|
||||||
|
│ • /api/subscriptions/{id}/sim/top-up-history │
|
||||||
|
│ • /api/subscriptions/{id}/sim/change-plan │
|
||||||
|
│ • /api/subscriptions/{id}/sim/features │
|
||||||
|
│ • /api/subscriptions/{id}/sim/cancel │
|
||||||
|
│ • /api/subscriptions/{id}/sim/reissue-esim │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ Data Aggregation
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ External Systems │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ WHMCS │ │ Freebit API │ │
|
||||||
|
│ │ (Billing) │ │ (SIM Services) │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ • Subscriptions │ │ • SIM Details │ │
|
||||||
|
│ │ • Customer Data │ │ • Usage Data │ │
|
||||||
|
│ │ • Billing Info │ │ • Management │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Data Flow by Section
|
||||||
|
|
||||||
|
### 1. **SIM Management Actions Section**
|
||||||
|
|
||||||
|
**Purpose**: Provides action buttons for SIM operations (Top Up, Reissue, Cancel, Change Plan)
|
||||||
|
|
||||||
|
**Data Sources**:
|
||||||
|
- **WHMCS**: Subscription status and customer permissions
|
||||||
|
- **Freebit API**: SIM type (physical/eSIM) and current status
|
||||||
|
|
||||||
|
**API Calls**:
|
||||||
|
```typescript
|
||||||
|
// Initial Load - Get SIM details for action availability
|
||||||
|
GET /api/subscriptions/{id}/sim/details
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data Flow**:
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Frontend │ │ BFF │ │ Freebit API │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ SimActions.tsx │───▶│ /sim/details │───▶│ /mvno/getDetail/│
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ • Check SIM │ │ • Authenticate │ │ • Return SIM │
|
||||||
|
│ type & status │ │ • Map response │ │ details │
|
||||||
|
│ • Enable/disable│ │ • Handle errors │ │ • Status info │
|
||||||
|
│ buttons │ │ │ │ │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Action-Specific APIs**:
|
||||||
|
- **Top Up Data**: `POST /api/subscriptions/{id}/sim/top-up` → Freebit `/master/addSpec/`
|
||||||
|
- **Reissue eSIM**: `POST /api/subscriptions/{id}/sim/reissue-esim` → Freebit `/mvno/esim/addAcnt/`
|
||||||
|
- **Cancel SIM**: `POST /api/subscriptions/{id}/sim/cancel` → Freebit `/mvno/releasePlan/`
|
||||||
|
- **Change Plan**: `POST /api/subscriptions/{id}/sim/change-plan` → Freebit `/mvno/changePlan/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **eSIM Details Card (Right Sidebar)**
|
||||||
|
|
||||||
|
**Purpose**: Displays essential SIM information in compact format
|
||||||
|
|
||||||
|
**Data Sources**:
|
||||||
|
- **WHMCS**: Subscription product name and billing info
|
||||||
|
- **Freebit API**: SIM technical details and status
|
||||||
|
|
||||||
|
**API Calls**:
|
||||||
|
```typescript
|
||||||
|
// Get comprehensive SIM information
|
||||||
|
GET /api/subscriptions/{id}/sim
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data Flow**:
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Frontend │ │ BFF │ │ External │
|
||||||
|
│ │ │ Systems │ │ Systems │
|
||||||
|
│ SimDetailsCard │───▶│ /sim │───▶│ ┌─────────────┐ │
|
||||||
|
│ │ │ │ │ │ WHMCS │ │
|
||||||
|
│ • Phone number │ │ • Aggregate │ │ │ • Product │ │
|
||||||
|
│ • Data remaining│ │ data from │ │ │ name │ │
|
||||||
|
│ • Service status│ │ multiple │ │ │ • Billing │ │
|
||||||
|
│ • Plan info │ │ sources │ │ └─────────────┘ │
|
||||||
|
│ │ │ • Transform │ │ ┌─────────────┐ │
|
||||||
|
│ │ │ responses │ │ │ Freebit │ │
|
||||||
|
│ │ │ • Handle errors │ │ │ • ICCID │ │
|
||||||
|
│ │ │ │ │ │ • MSISDN │ │
|
||||||
|
│ │ │ │ │ │ • Status │ │
|
||||||
|
│ │ │ │ │ │ • Plan code │ │
|
||||||
|
│ │ │ │ │ └─────────────┘ │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data Mapping**:
|
||||||
|
```typescript
|
||||||
|
// BFF Response Structure
|
||||||
|
{
|
||||||
|
"details": {
|
||||||
|
"iccid": "8944504101234567890", // From Freebit
|
||||||
|
"msisdn": "08077052946", // From Freebit
|
||||||
|
"planCode": "PASI_50G", // From Freebit
|
||||||
|
"status": "active", // From Freebit
|
||||||
|
"simType": "esim", // From Freebit
|
||||||
|
"productName": "SonixNet SIM Service", // From WHMCS
|
||||||
|
"remainingQuotaMb": 48256 // Calculated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Data Usage Chart (Right Sidebar)**
|
||||||
|
|
||||||
|
**Purpose**: Visual representation of data consumption and remaining quota
|
||||||
|
|
||||||
|
**Data Sources**:
|
||||||
|
- **Freebit API**: Real-time usage statistics and quota information
|
||||||
|
|
||||||
|
**API Calls**:
|
||||||
|
```typescript
|
||||||
|
// Get usage data
|
||||||
|
GET /api/subscriptions/{id}/sim/usage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data Flow**:
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Frontend │ │ BFF │ │ Freebit API │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ DataUsageChart │───▶│ /sim/usage │───▶│ /mvno/getTraffic│
|
||||||
|
│ │ │ │ │ Info/ │
|
||||||
|
│ • Progress bar │ │ • Authenticate │ │ │
|
||||||
|
│ • Usage stats │ │ • Format data │ │ • Today's usage │
|
||||||
|
│ • History chart │ │ • Calculate │ │ • Total quota │
|
||||||
|
│ • Remaining GB │ │ percentages │ │ • Usage history │
|
||||||
|
│ │ │ • Handle errors │ │ │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data Processing**:
|
||||||
|
```typescript
|
||||||
|
// Freebit API Response
|
||||||
|
{
|
||||||
|
"todayUsageMb": 748.47,
|
||||||
|
"totalQuotaMb": 51200,
|
||||||
|
"usageHistory": [
|
||||||
|
{ "date": "2025-01-04", "usageMb": 1228.8 },
|
||||||
|
{ "date": "2025-01-03", "usageMb": 595.2 },
|
||||||
|
{ "date": "2025-01-02", "usageMb": 448.0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFF Processing
|
||||||
|
const usagePercentage = (usedMb / totalQuotaMb) * 100;
|
||||||
|
const remainingMb = totalQuotaMb - usedMb;
|
||||||
|
const formattedRemaining = formatQuota(remainingMb); // "47.1 GB"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **Plan & Service Options**
|
||||||
|
|
||||||
|
**Purpose**: Manage SIM plan and optional features (Voice Mail, Call Waiting, International Roaming, 4G/5G).
|
||||||
|
|
||||||
|
**Data Sources**:
|
||||||
|
- **Freebit API**: Current service settings and options
|
||||||
|
- **WHMCS**: Plan catalog and billing context
|
||||||
|
|
||||||
|
**API Calls**:
|
||||||
|
```typescript
|
||||||
|
// Get current service settings
|
||||||
|
GET /api/subscriptions/{id}/sim/details
|
||||||
|
|
||||||
|
// Update optional features (flags)
|
||||||
|
POST /api/subscriptions/{id}/sim/features
|
||||||
|
|
||||||
|
// Change plan
|
||||||
|
POST /api/subscriptions/{id}/sim/change-plan
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data Flow**:
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────────────┐
|
||||||
|
│ Frontend │ │ BFF │ │ Freebit API │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ SimFeatureToggles│───▶│ /sim/details │───▶│ /mvno/getDetail/ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ Apply Changes │───▶│ /sim/features │───▶│ /master/addSpec/ (flags) │
|
||||||
|
│ Change Plan │───▶│ /sim/change-plan│───▶│ /mvno/changePlan/ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ • Validate │ │ • Authenticate │ │ • Apply changes │
|
||||||
|
│ • Update UI │ │ • Transform │ │ • Return resultCode=100 │
|
||||||
|
│ • Refresh data │ │ • Handle errors │ │ │
|
||||||
|
└─────────────────┘ └─────────────────┘ └──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed plans and mapping
|
||||||
|
- The portal currently supports the following SIM data plans from Salesforce:
|
||||||
|
- SIM Data-only 5GB → Freebit planCode `PASI_5G`
|
||||||
|
- SIM Data-only 10GB → `PASI_10G`
|
||||||
|
- SIM Data-only 25GB → `PASI_25G`
|
||||||
|
- SIM Data-only 50GB → `PASI_50G`
|
||||||
|
- UI behavior: The Change Plan action lives inside the “SIM Management Actions” card. Clicking it opens a modal listing only “other” plans. For example, if the current plan is `PASI_50G`, options will be 5GB, 10GB, 25GB. If the current plan is not 50GB, the 50GB option is included.
|
||||||
|
- Request payload sent to BFF:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"newPlanCode": "PASI_25G"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- BFF calls MVNO Plan Change with fields per the API spec (account, planCode, optional globalIP, optional runTime).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. **Top-Up Payment Flow (Invoice + Auto-Capture)**
|
||||||
|
|
||||||
|
When a user tops up data, the portal bills through WHMCS before applying the quota via Freebit. Unit price is fixed: 1 GB = ¥500.
|
||||||
|
|
||||||
|
Endpoints used
|
||||||
|
- Frontend → BFF: `POST /api/subscriptions/{id}/sim/top-up` with `{ quotaMb, campaignCode?, expiryDate? }`
|
||||||
|
- BFF → WHMCS: `createInvoice` then `capturePayment` (gateway-selected SSO or stored method)
|
||||||
|
- BFF → Freebit: `PA04-04 Add Spec & Quota` (`/master/addSpec/`) if payment succeeds
|
||||||
|
|
||||||
|
Pricing
|
||||||
|
- Amount in JPY = ceil(quotaMb / 1024) × 500
|
||||||
|
- Example: 1024MB → ¥500, 3072MB → ¥1,500
|
||||||
|
|
||||||
|
Happy-path sequence
|
||||||
|
```
|
||||||
|
Frontend BFF WHMCS Freebit
|
||||||
|
────────── ──────────────── ──────────────── ────────────────
|
||||||
|
TopUpModal ───────▶ POST /sim/top-up ───────▶ createInvoice ─────▶
|
||||||
|
(quotaMb) (validate + map) (amount=ceil(MB/1024)*500)
|
||||||
|
│ │
|
||||||
|
│ invoiceId
|
||||||
|
▼ │
|
||||||
|
capturePayment ───────────────▶ │
|
||||||
|
│ paid (or failed)
|
||||||
|
├── on success ─────────────────────────────▶ /master/addSpec/
|
||||||
|
│ (quota in KB)
|
||||||
|
└── on failure ──┐
|
||||||
|
└──── return error (no Freebit call)
|
||||||
|
```
|
||||||
|
|
||||||
|
Failure handling
|
||||||
|
- If `capturePayment` fails, BFF responds with 402/400 and does NOT call Freebit. UI shows error and invoice link for manual payment.
|
||||||
|
- If Freebit returns non-100 `resultCode`, BFF logs, returns 502/500, and may void/refund invoice in future enhancement.
|
||||||
|
|
||||||
|
BFF responsibilities
|
||||||
|
- Validate `quotaMb` (1–100000)
|
||||||
|
- Price computation and invoice line creation (description includes quota)
|
||||||
|
- Attempt payment capture (stored method or SSO handoff)
|
||||||
|
- On success, call Freebit AddSpec with `quota=quotaMb*1024` and optional `expire`
|
||||||
|
- Return success to UI and refresh SIM info
|
||||||
|
|
||||||
|
Freebit PA04-04 (Add Spec & Quota) request fields
|
||||||
|
- `account`: MSISDN (phone number)
|
||||||
|
- `quota`: integer KB (100MB–51200MB in screenshot spec; environment-dependent)
|
||||||
|
- `quotaCode` (optional): campaign code
|
||||||
|
- `expire` (optional): YYYYMMDD
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Scheduled top-ups use `/mvno/eachQuota/` with `runTime`; immediate uses `/master/addSpec/`.
|
||||||
|
- For development, amounts and gateway can be simulated; production requires real WHMCS gateway configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Real-Time Data Updates
|
||||||
|
|
||||||
|
### Automatic Refresh Mechanism
|
||||||
|
```typescript
|
||||||
|
// After any action (top-up, cancel, etc.)
|
||||||
|
const handleActionSuccess = () => {
|
||||||
|
// Refresh all data
|
||||||
|
refetchSimDetails();
|
||||||
|
refetchUsageData();
|
||||||
|
refetchSubscriptionData();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Consistency
|
||||||
|
- **Immediate Updates**: UI updates optimistically
|
||||||
|
- **Background Sync**: Real data fetched after actions
|
||||||
|
- **Error Handling**: Rollback on API failures
|
||||||
|
- **Loading States**: Visual feedback during operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Performance Considerations
|
||||||
|
|
||||||
|
### Caching Strategy
|
||||||
|
```typescript
|
||||||
|
// BFF Level Caching
|
||||||
|
- SIM Details: 5 minutes TTL
|
||||||
|
- Usage Data: 1 minute TTL
|
||||||
|
- Subscription Info: 10 minutes TTL
|
||||||
|
|
||||||
|
// Frontend Caching
|
||||||
|
- React Query: 30 seconds stale time
|
||||||
|
- Background refetch: Every 2 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Optimization
|
||||||
|
- **Batch Requests**: Single endpoint for comprehensive data
|
||||||
|
- **Selective Updates**: Only refresh changed sections
|
||||||
|
- **Error Recovery**: Retry failed requests with exponential backoff
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Security & Authentication
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Frontend │ │ BFF │ │ External │
|
||||||
|
│ │ │ │ │ Systems │
|
||||||
|
│ • JWT Token │───▶│ • Validate JWT │───▶│ • WHMCS API Key │
|
||||||
|
│ • User Context │ │ • Map to WHMCS │ │ • Freebit Auth │
|
||||||
|
│ • Permissions │ │ Client ID │ │ • Rate Limiting │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Protection
|
||||||
|
- **Input Validation**: All user inputs sanitized
|
||||||
|
- **Rate Limiting**: API calls throttled per user
|
||||||
|
- **Audit Logging**: All actions logged for compliance
|
||||||
|
- **Error Masking**: Sensitive data not exposed in errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoring & Analytics
|
||||||
|
|
||||||
|
### Key Metrics Tracked
|
||||||
|
- **API Response Times**: < 500ms target
|
||||||
|
- **Error Rates**: < 1% target
|
||||||
|
- **User Actions**: Top-up frequency, plan changes
|
||||||
|
- **Data Usage Patterns**: Peak usage times, quota consumption
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
```typescript
|
||||||
|
// BFF Health Endpoints
|
||||||
|
GET /health/sim-management
|
||||||
|
GET /health/freebit-api
|
||||||
|
GET /health/whmcs-api
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Future Enhancements
|
||||||
|
|
||||||
|
### Planned Improvements
|
||||||
|
1. **Real-time WebSocket Updates**: Live usage data without refresh
|
||||||
|
2. **Advanced Analytics**: Usage predictions and recommendations
|
||||||
|
3. **Bulk Operations**: Manage multiple SIMs simultaneously
|
||||||
|
4. **Mobile App Integration**: Native mobile SIM management
|
||||||
|
|
||||||
|
### Scalability Considerations
|
||||||
|
- **Microservices**: Split BFF into domain-specific services
|
||||||
|
- **CDN Integration**: Cache static SIM data globally
|
||||||
|
- **Database Optimization**: Implement read replicas for usage data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support & Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
1. **API Timeouts**: Check Freebit API status
|
||||||
|
2. **Data Inconsistency**: Verify WHMCS sync
|
||||||
|
3. **Authentication Errors**: Validate JWT tokens
|
||||||
|
4. **Rate Limiting**: Monitor API quotas
|
||||||
|
|
||||||
|
### Debug Endpoints
|
||||||
|
```typescript
|
||||||
|
// Development only
|
||||||
|
GET /api/subscriptions/{id}/sim/debug
|
||||||
|
GET /api/health/sim-management/detailed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 **Summary for Your Managers**
|
||||||
|
|
||||||
|
This comprehensive documentation explains:
|
||||||
|
|
||||||
|
### **🏗️ System Architecture**
|
||||||
|
- **3-Tier Architecture**: Frontend → BFF → External APIs (WHMCS + Freebit)
|
||||||
|
- **Data Aggregation**: BFF combines data from multiple sources
|
||||||
|
- **Real-time Updates**: Automatic refresh after user actions
|
||||||
|
|
||||||
|
### **📊 Key Data Flows**
|
||||||
|
1. **SIM Actions**: Button availability based on SIM type and status
|
||||||
|
2. **SIM Details**: Phone number, data remaining, service status
|
||||||
|
3. **Usage Chart**: Real-time consumption and quota visualization
|
||||||
|
4. **Service Options**: Voice mail, call waiting, roaming settings
|
||||||
|
|
||||||
|
### **🔧 Technical Benefits**
|
||||||
|
- **Performance**: Caching and optimized API calls
|
||||||
|
- **Security**: JWT authentication and input validation
|
||||||
|
- **Reliability**: Error handling and retry mechanisms
|
||||||
|
- **Monitoring**: Health checks and performance metrics
|
||||||
|
|
||||||
|
### **💼 Business Value**
|
||||||
|
- **User Experience**: Real-time data and intuitive interface
|
||||||
|
- **Operational Efficiency**: Automated SIM management operations
|
||||||
|
- **Data Accuracy**: Direct integration with Freebit and WHMCS
|
||||||
|
- **Scalability**: Architecture supports future enhancements
|
||||||
|
|
||||||
|
This documentation will help your managers understand the technical complexity and business value of the SIM Management system!
|
||||||
Loading…
x
Reference in New Issue
Block a user