- Deleted migration file that removed cached profile fields from the users table, centralizing profile data retrieval from WHMCS. - Updated CsrfMiddleware to include new public authentication endpoints for password reset, setting password, and WHMCS account linking. - Enhanced error handling in password and WHMCS linking workflows to provide clearer feedback on missing mappings and improve user experience. - Adjusted user creation and update methods in UsersFacade to handle cases where WHMCS mappings are not yet available, ensuring smoother account setup.
235 lines
8.9 KiB
TypeScript
235 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import {
|
|
DevicePhoneMobileIcon,
|
|
ExclamationTriangleIcon,
|
|
ArrowPathIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import { SimDetailsCard, type SimDetails } from "./SimDetailsCard";
|
|
import { SimActions } from "./SimActions";
|
|
import { apiClient } from "@/lib/api";
|
|
import { SimFeatureToggles } from "./SimFeatureToggles";
|
|
|
|
interface SimManagementSectionProps {
|
|
subscriptionId: number;
|
|
}
|
|
|
|
interface SimInfo {
|
|
details: SimDetails;
|
|
}
|
|
|
|
export function SimManagementSection({ subscriptionId }: SimManagementSectionProps) {
|
|
const [simInfo, setSimInfo] = useState<SimInfo | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchSimInfo = useCallback(async () => {
|
|
try {
|
|
setError(null);
|
|
|
|
const response = await apiClient.GET("/api/subscriptions/{id}/sim", {
|
|
params: { path: { id: subscriptionId } },
|
|
});
|
|
|
|
const payload = response.data as { details: SimDetails; usage: any } | undefined;
|
|
|
|
if (!payload) {
|
|
throw new Error("Failed to load SIM information");
|
|
}
|
|
|
|
// Only use the details part, ignore usage data
|
|
setSimInfo({ details: payload.details });
|
|
} catch (err: unknown) {
|
|
const hasStatus = (v: unknown): v is { status: number } =>
|
|
typeof v === "object" &&
|
|
v !== null &&
|
|
"status" in v &&
|
|
typeof (v as { status: unknown }).status === "number";
|
|
if (hasStatus(err) && err.status === 400) {
|
|
// Not a SIM subscription - this component shouldn't be shown
|
|
setError("This subscription is not a SIM service");
|
|
} else {
|
|
setError(err instanceof Error ? err.message : "Failed to load SIM information");
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [subscriptionId]);
|
|
|
|
useEffect(() => {
|
|
void fetchSimInfo();
|
|
}, [fetchSimInfo]);
|
|
|
|
const handleRefresh = () => {
|
|
setLoading(true);
|
|
void fetchSimInfo();
|
|
};
|
|
|
|
const handleActionSuccess = () => {
|
|
// Refresh SIM info after any successful action
|
|
void fetchSimInfo();
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-8">
|
|
<div className="bg-white shadow-lg rounded-xl border border-gray-100 p-8">
|
|
<div className="flex items-center mb-6">
|
|
<div className="bg-blue-50 rounded-xl p-2 mr-4">
|
|
<DevicePhoneMobileIcon className="h-6 w-6 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900">SIM Management</h2>
|
|
<p className="text-gray-600 mt-1">Loading your SIM service details...</p>
|
|
</div>
|
|
</div>
|
|
<div className="animate-pulse space-y-6">
|
|
<div className="h-6 bg-gradient-to-r from-gray-200 to-gray-300 rounded-lg w-3/4"></div>
|
|
<div className="h-5 bg-gradient-to-r from-gray-200 to-gray-300 rounded-lg w-1/2"></div>
|
|
<div className="h-48 bg-gradient-to-r from-gray-200 to-gray-300 rounded-xl"></div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="h-32 bg-gradient-to-r from-gray-200 to-gray-300 rounded-xl"></div>
|
|
<div className="h-32 bg-gradient-to-r from-gray-200 to-gray-300 rounded-xl"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="bg-white shadow-lg rounded-xl border border-red-100 p-8">
|
|
<div className="flex items-center mb-6">
|
|
<div className="bg-blue-50 rounded-xl p-2 mr-4">
|
|
<DevicePhoneMobileIcon className="h-6 w-6 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900">SIM Management</h2>
|
|
<p className="text-gray-600 mt-1">Unable to load SIM information</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-center py-12">
|
|
<div className="bg-red-50 rounded-full p-4 w-20 h-20 mx-auto mb-6">
|
|
<ExclamationTriangleIcon className="h-12 w-12 text-red-500 mx-auto" />
|
|
</div>
|
|
<h3 className="text-xl font-semibold text-gray-900 mb-3">
|
|
Unable to Load SIM Information
|
|
</h3>
|
|
<p className="text-gray-600 mb-8 max-w-md mx-auto">{error}</p>
|
|
<button
|
|
onClick={handleRefresh}
|
|
className="inline-flex items-center px-6 py-3 border border-transparent text-sm font-semibold rounded-xl text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 hover:shadow-lg hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
|
>
|
|
<ArrowPathIcon className="h-5 w-5 mr-2" />
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!simInfo) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div id="sim-management" className="space-y-6">
|
|
{/* Header Section */}
|
|
<div className="bg-white shadow-sm rounded-lg border border-gray-200 p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">
|
|
{simInfo.details.simType === "esim" ? "eSIM" : "Physical SIM"} Service
|
|
</h1>
|
|
<p className="text-gray-600 mt-1">Subscription ID {subscriptionId}</p>
|
|
</div>
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
|
{simInfo.details.status.charAt(0).toUpperCase() + simInfo.details.status.slice(1)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Two Column Layout */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Left Column - Main Actions */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{/* Subscription Details Card */}
|
|
<div className="bg-white shadow-sm rounded-lg border border-gray-200 p-6">
|
|
<div className="mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">Subscription Details</h2>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="text-center">
|
|
<p className="text-sm text-gray-500 uppercase tracking-wide">Monthly Cost</p>
|
|
<p className="text-lg font-semibold text-gray-900">¥3,100</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-sm text-gray-500 uppercase tracking-wide">Next Billing</p>
|
|
<p className="text-lg font-semibold text-gray-900">Jul 1, 2024</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-sm text-gray-500 uppercase tracking-wide">Registration</p>
|
|
<p className="text-lg font-semibold text-gray-900">Aug 2, 2023</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SIM Management Actions Card */}
|
|
<div className="bg-white shadow-sm rounded-lg border border-gray-200 p-6">
|
|
<div className="mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">SIM Management Actions</h2>
|
|
</div>
|
|
<SimActions
|
|
subscriptionId={subscriptionId}
|
|
simType={simInfo.details.simType}
|
|
status={simInfo.details.status}
|
|
currentPlanCode={simInfo.details.planCode}
|
|
onTopUpSuccess={handleActionSuccess}
|
|
onPlanChangeSuccess={handleActionSuccess}
|
|
onCancelSuccess={handleActionSuccess}
|
|
onReissueSuccess={handleActionSuccess}
|
|
embedded={true}
|
|
/>
|
|
</div>
|
|
|
|
{/* Voice Status Card */}
|
|
<div className="bg-white shadow-sm rounded-lg border border-gray-200 p-6">
|
|
<div className="mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">Voice Status</h2>
|
|
</div>
|
|
<SimFeatureToggles
|
|
subscriptionId={subscriptionId}
|
|
voiceMailEnabled={simInfo.details.voiceMailEnabled}
|
|
callWaitingEnabled={simInfo.details.callWaitingEnabled}
|
|
internationalRoamingEnabled={simInfo.details.internationalRoamingEnabled}
|
|
networkType={simInfo.details.networkType}
|
|
onChanged={handleActionSuccess}
|
|
embedded
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Column - SIM Details & Usage */}
|
|
<div className="space-y-6">
|
|
{/* SIM Details Card */}
|
|
<div className="bg-white shadow-sm rounded-lg border border-gray-200 p-6">
|
|
<div className="mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">SIM Details</h2>
|
|
</div>
|
|
<SimDetailsCard
|
|
simDetails={simInfo.details}
|
|
isLoading={false}
|
|
error={null}
|
|
embedded={true}
|
|
showFeaturesSummary={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
}
|