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,
|
||||
WifiIcon,
|
||||
GlobeAltIcon,
|
||||
SignalIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { AnimatedCard } from "@/components/catalog/animated-card";
|
||||
import { AnimatedButton } from "@/components/catalog/animated-button";
|
||||
@ -32,7 +31,7 @@ export default function CatalogPage() {
|
||||
</span>
|
||||
</h1>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@ -57,13 +56,13 @@ export default function CatalogPage() {
|
||||
{/* SIM/eSIM Service */}
|
||||
<ServiceHeroCard
|
||||
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" />}
|
||||
features={[
|
||||
"Physical SIM & eSIM",
|
||||
"Data + Voice plans",
|
||||
"Data + SMS/Voice plans",
|
||||
"Family discounts",
|
||||
"Flexible data sizes",
|
||||
"Multiple data options",
|
||||
]}
|
||||
href="/catalog/sim"
|
||||
color="green"
|
||||
@ -95,17 +94,12 @@ export default function CatalogPage() {
|
||||
</p>
|
||||
</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
|
||||
icon={<WifiIcon className="h-10 w-10 text-blue-600" />}
|
||||
title="Location-Based Plans"
|
||||
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
|
||||
icon={<GlobeAltIcon className="h-10 w-10 text-purple-600" />}
|
||||
title="Seamless Integration"
|
||||
|
||||
@ -45,7 +45,7 @@ function PlanTypeSection({
|
||||
const familyPlans = plans.filter(p => p.hasFamilyDiscount);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="animate-in fade-in duration-500">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
{icon}
|
||||
<div>
|
||||
@ -224,7 +224,7 @@ export default function SimPlansPage() {
|
||||
<div className="text-center mb-12">
|
||||
<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">
|
||||
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>
|
||||
</div>
|
||||
{/* Family Discount Banner */}
|
||||
@ -267,48 +267,54 @@ export default function SimPlansPage() {
|
||||
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
||||
<button
|
||||
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"
|
||||
? "border-blue-500 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<PhoneIcon className="h-5 w-5" />
|
||||
Data + Voice
|
||||
<PhoneIcon className={`h-5 w-5 transition-transform duration-300 ${activeTab === "data-voice" ? "scale-110" : ""}`} />
|
||||
Data + SMS/Voice
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
? "border-purple-500 text-purple-600"
|
||||
: "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
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
? "border-orange-500 text-orange-600"
|
||||
: "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
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@ -318,49 +324,74 @@ export default function SimPlansPage() {
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="min-h-[400px]">
|
||||
{activeTab === "data-voice" && (
|
||||
<PlanTypeSection
|
||||
title="Data + Voice Plans"
|
||||
description="Internet, calling, and SMS included"
|
||||
icon={<PhoneIcon className="h-8 w-8 text-blue-600" />}
|
||||
plans={plansByType.DataSmsVoice}
|
||||
showFamilyDiscount={hasExistingSim}
|
||||
/>
|
||||
)}
|
||||
<div className="min-h-[400px] relative">
|
||||
<div className={`transition-all duration-500 ease-in-out ${
|
||||
activeTab === "data-voice"
|
||||
? "opacity-100 translate-y-0"
|
||||
: "opacity-0 translate-y-4 absolute inset-0 pointer-events-none"
|
||||
}`}>
|
||||
{activeTab === "data-voice" && (
|
||||
<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" && (
|
||||
<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 className={`transition-all duration-500 ease-in-out ${
|
||||
activeTab === "data-only"
|
||||
? "opacity-100 translate-y-0"
|
||||
: "opacity-0 translate-y-4 absolute inset-0 pointer-events-none"
|
||||
}`}>
|
||||
{activeTab === "data-only" && (
|
||||
<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" && (
|
||||
<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 className={`transition-all duration-500 ease-in-out ${
|
||||
activeTab === "voice-only"
|
||||
? "opacity-100 translate-y-0"
|
||||
: "opacity-0 translate-y-4 absolute inset-0 pointer-events-none"
|
||||
}`}>
|
||||
{activeTab === "voice-only" && (
|
||||
<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>
|
||||
|
||||
{/* Features Section */}
|
||||
<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">
|
||||
All SIM Plans Include
|
||||
Plan Features & Terms
|
||||
</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">
|
||||
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">No Contract</div>
|
||||
<div className="text-gray-600">Cancel anytime</div>
|
||||
<div className="font-medium text-gray-900">3-Month Contract</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 className="flex items-start gap-3">
|
||||
@ -384,19 +415,53 @@ export default function SimPlansPage() {
|
||||
<div className="text-gray-600">Multi-line savings</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>
|
||||
|
||||
{/* 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">
|
||||
<InformationCircleIcon className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-blue-900 mb-1">Getting Started</div>
|
||||
<p className="text-blue-800">
|
||||
Choose your plan size, select eSIM or physical SIM, and configure optional add-ons
|
||||
like voice mail and call waiting. Number porting is available if you want to keep your
|
||||
existing phone number.
|
||||
</p>
|
||||
<div className="mt-8 p-6 rounded-lg border border-blue-200 bg-blue-50 max-w-4xl mx-auto">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<InformationCircleIcon className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-blue-900 mb-2">Important Terms & Conditions</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<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>
|
||||
|
||||
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 {
|
||||
subscriptionId: number;
|
||||
currentPlanCode?: string;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
onError: (message: string) => void;
|
||||
}
|
||||
|
||||
export function ChangePlanModal({ subscriptionId, onClose, onSuccess, onError }: ChangePlanModalProps) {
|
||||
const [newPlanCode, setNewPlanCode] = useState("");
|
||||
export function ChangePlanModal({ subscriptionId, currentPlanCode, onClose, onSuccess, onError }: ChangePlanModalProps) {
|
||||
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 [scheduledAt, setScheduledAt] = useState(""); // YYYY-MM-DD
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
if (!newPlanCode.trim()) {
|
||||
onError("Please enter a new plan code");
|
||||
if (!newPlanCode) {
|
||||
onError("Please select a new plan");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/change-plan`, {
|
||||
newPlanCode: newPlanCode.trim(),
|
||||
newPlanCode: newPlanCode,
|
||||
assignGlobalIp,
|
||||
scheduledAt: scheduledAt ? scheduledAt.replaceAll("-", "") : undefined,
|
||||
});
|
||||
@ -55,14 +67,18 @@ export function ChangePlanModal({ subscriptionId, onClose, onSuccess, onError }:
|
||||
</div>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">New Plan Code</label>
|
||||
<input
|
||||
type="text"
|
||||
<label className="block text-sm font-medium text-gray-700">Select New Plan</label>
|
||||
<select
|
||||
value={newPlanCode}
|
||||
onChange={(e) => setNewPlanCode(e.target.value)}
|
||||
placeholder="e.g. LTE3G_P01"
|
||||
onChange={(e) => setNewPlanCode(e.target.value as PlanCode)}
|
||||
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 className="flex items-center">
|
||||
<input
|
||||
@ -105,7 +121,7 @@ export function ChangePlanModal({ subscriptionId, onClose, onSuccess, onError }:
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -113,4 +129,3 @@ export function ChangePlanModal({ subscriptionId, onClose, onSuccess, onError }:
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
PlusIcon,
|
||||
ArrowPathIcon,
|
||||
@ -21,6 +22,7 @@ interface SimActionsProps {
|
||||
onCancelSuccess?: () => void;
|
||||
onReissueSuccess?: () => void;
|
||||
embedded?: boolean; // when true, render content without card container
|
||||
currentPlanCode?: string;
|
||||
}
|
||||
|
||||
export function SimActions({
|
||||
@ -31,8 +33,10 @@ export function SimActions({
|
||||
onPlanChangeSuccess,
|
||||
onCancelSuccess,
|
||||
onReissueSuccess,
|
||||
embedded = false
|
||||
embedded = false,
|
||||
currentPlanCode
|
||||
}: SimActionsProps) {
|
||||
const router = useRouter();
|
||||
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
||||
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
||||
const [showReissueConfirm, setShowReissueConfirm] = useState(false);
|
||||
@ -40,6 +44,9 @@ export function SimActions({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [showChangePlanModal, setShowChangePlanModal] = useState(false);
|
||||
const [activeInfo, setActiveInfo] = useState<
|
||||
'topup' | 'reissue' | 'cancel' | 'changePlan' | null
|
||||
>(null);
|
||||
|
||||
const isActive = status === 'active';
|
||||
const canTopUp = isActive;
|
||||
@ -145,7 +152,14 @@ export function SimActions({
|
||||
<div className={`grid gap-4 ${embedded ? 'grid-cols-1' : 'grid-cols-2'}`}>
|
||||
{/* Top Up Data - Primary Action */}
|
||||
<button
|
||||
onClick={() => setShowTopUpModal(true)}
|
||||
onClick={() => {
|
||||
setActiveInfo('topup');
|
||||
try {
|
||||
router.push(`/subscriptions/${subscriptionId}/sim/top-up`);
|
||||
} catch {
|
||||
setShowTopUpModal(true);
|
||||
}
|
||||
}}
|
||||
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 ${
|
||||
canTopUp && loading === null
|
||||
@ -164,7 +178,14 @@ export function SimActions({
|
||||
{/* Reissue eSIM (only for eSIMs) */}
|
||||
{simType === 'esim' && (
|
||||
<button
|
||||
onClick={() => setShowReissueConfirm(true)}
|
||||
onClick={() => {
|
||||
setActiveInfo('reissue');
|
||||
try {
|
||||
router.push(`/subscriptions/${subscriptionId}/sim/reissue`);
|
||||
} catch {
|
||||
setShowReissueConfirm(true);
|
||||
}
|
||||
}}
|
||||
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 ${
|
||||
canReissue && loading === null
|
||||
@ -183,7 +204,11 @@ export function SimActions({
|
||||
|
||||
{/* Cancel SIM - Destructive Action */}
|
||||
<button
|
||||
onClick={() => setShowCancelConfirm(true)}
|
||||
onClick={() => {
|
||||
setActiveInfo('cancel');
|
||||
// keep inline confirm for cancel to avoid accidental navigation
|
||||
setShowCancelConfirm(true);
|
||||
}}
|
||||
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 ${
|
||||
canCancel && loading === null
|
||||
@ -201,10 +226,17 @@ export function SimActions({
|
||||
|
||||
{/* Change Plan - Secondary Action */}
|
||||
<button
|
||||
onClick={() => {/* Add change plan functionality */}}
|
||||
disabled={!isActive || loading !== null}
|
||||
onClick={() => {
|
||||
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 ${
|
||||
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-gray-400 bg-gray-50 border-gray-300 cursor-not-allowed'
|
||||
}`}
|
||||
@ -220,47 +252,52 @@ export function SimActions({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Action Descriptions */}
|
||||
<div className="mt-6 space-y-3 text-sm text-gray-600">
|
||||
<div className="flex items-start">
|
||||
<PlusIcon className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<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>
|
||||
</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.
|
||||
{/* Action Description (contextual) */}
|
||||
{activeInfo && (
|
||||
<div className="mt-6 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
{activeInfo === 'topup' && (
|
||||
<div className="flex items-start">
|
||||
<PlusIcon className="h-4 w-4 text-blue-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-start">
|
||||
<XMarkIcon className="h-4 w-4 text-red-500 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>
|
||||
)}
|
||||
{activeInfo === 'reissue' && (
|
||||
<div className="flex items-start">
|
||||
<ArrowPathIcon className="h-4 w-4 text-green-600 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>
|
||||
)}
|
||||
{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 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>
|
||||
|
||||
{/* Top Up Modal */}
|
||||
{showTopUpModal && (
|
||||
<TopUpModal
|
||||
subscriptionId={subscriptionId}
|
||||
onClose={() => setShowTopUpModal(false)}
|
||||
onClose={() => { setShowTopUpModal(false); setActiveInfo(null); }}
|
||||
onSuccess={() => {
|
||||
setShowTopUpModal(false);
|
||||
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 */}
|
||||
{showReissueConfirm && (
|
||||
@ -304,11 +354,11 @@ export function SimActions({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowReissueConfirm(false)}
|
||||
onClick={() => { setShowReissueConfirm(false); setActiveInfo(null); }}
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -348,11 +398,11 @@ export function SimActions({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCancelConfirm(false)}
|
||||
onClick={() => { setShowCancelConfirm(false); setActiveInfo(null); }}
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -47,6 +47,15 @@ interface 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) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
@ -146,7 +155,7 @@ export function SimDetailsCard({ simDetails, isLoading, error, embedded = false,
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<h3 className="text-lg font-medium text-gray-900">Physical SIM Details</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{simDetails.planCode} • {`${simDetails.size} SIM`}
|
||||
{formatPlan(simDetails.planCode)} • {`${simDetails.size} SIM`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { authenticatedApi } from "@/lib/api";
|
||||
import type { SimPlan } from "@/shared/types/catalog.types";
|
||||
|
||||
interface SimFeatureTogglesProps {
|
||||
subscriptionId: number;
|
||||
@ -10,8 +9,8 @@ interface SimFeatureTogglesProps {
|
||||
callWaitingEnabled?: boolean;
|
||||
internationalRoamingEnabled?: boolean;
|
||||
networkType?: string; // '4G' | '5G'
|
||||
currentPlanCode?: string;
|
||||
onChanged?: () => void;
|
||||
embedded?: boolean; // when true, render without outer card wrappers
|
||||
}
|
||||
|
||||
export function SimFeatureToggles({
|
||||
@ -20,8 +19,8 @@ export function SimFeatureToggles({
|
||||
callWaitingEnabled,
|
||||
internationalRoamingEnabled,
|
||||
networkType,
|
||||
currentPlanCode,
|
||||
onChanged,
|
||||
embedded = false,
|
||||
}: SimFeatureTogglesProps) {
|
||||
// Initial values
|
||||
const initial = useMemo(() => ({
|
||||
@ -29,18 +28,13 @@ export function SimFeatureToggles({
|
||||
cw: !!callWaitingEnabled,
|
||||
ir: !!internationalRoamingEnabled,
|
||||
nt: networkType === '5G' ? '5G' : '4G',
|
||||
plan: currentPlanCode || '',
|
||||
}), [voiceMailEnabled, callWaitingEnabled, internationalRoamingEnabled, networkType, currentPlanCode]);
|
||||
}), [voiceMailEnabled, callWaitingEnabled, internationalRoamingEnabled, networkType]);
|
||||
|
||||
// Working values
|
||||
const [vm, setVm] = useState(initial.vm);
|
||||
const [cw, setCw] = useState(initial.cw);
|
||||
const [ir, setIr] = useState(initial.ir);
|
||||
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 [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
@ -50,28 +44,13 @@ export function SimFeatureToggles({
|
||||
setCw(initial.cw);
|
||||
setIr(initial.ir);
|
||||
setNt(initial.nt as '4G' | '5G');
|
||||
setPlan(initial.plan);
|
||||
}, [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; };
|
||||
}, []);
|
||||
}, [initial.vm, initial.cw, initial.ir, initial.nt]);
|
||||
|
||||
const reset = () => {
|
||||
setVm(initial.vm);
|
||||
setCw(initial.cw);
|
||||
setIr(initial.ir);
|
||||
setNt(initial.nt as '4G' | '5G');
|
||||
setPlan(initial.plan);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
};
|
||||
@ -91,10 +70,6 @@ export function SimFeatureToggles({
|
||||
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');
|
||||
onChanged?.();
|
||||
} catch (e: any) {
|
||||
@ -109,9 +84,9 @@ export function SimFeatureToggles({
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* 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 */}
|
||||
<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">
|
||||
@ -249,7 +224,7 @@ export function SimFeatureToggles({
|
||||
</div>
|
||||
|
||||
{/* 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="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">
|
||||
|
||||
@ -128,49 +128,54 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
|
||||
{/* SIM Details and Usage - Main Content */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||||
{/* Main Content Area - Actions and Settings (Left Side) */}
|
||||
<div className="xl:col-span-2 xl:order-1 space-y-8">
|
||||
{/* 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="order-2 xl:col-span-2 xl:order-1">
|
||||
<div className="bg-white shadow-lg rounded-xl border border-gray-100 p-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<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
|
||||
<SimActions
|
||||
subscriptionId={subscriptionId}
|
||||
voiceMailEnabled={simInfo.details.voiceMailEnabled}
|
||||
callWaitingEnabled={simInfo.details.callWaitingEnabled}
|
||||
internationalRoamingEnabled={simInfo.details.internationalRoamingEnabled}
|
||||
networkType={simInfo.details.networkType}
|
||||
simType={simInfo.details.simType}
|
||||
status={simInfo.details.status}
|
||||
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>
|
||||
|
||||
{/* 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 */}
|
||||
<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">
|
||||
@ -203,21 +208,7 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
/>
|
||||
{/* (On desktop, details+usage are above; on mobile they appear first since this section is above actions) */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -248,7 +248,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
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