- 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.
350 lines
14 KiB
TypeScript
350 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useMemo, useState } from "react";
|
|
import { apiClient } from "@/lib/api";
|
|
|
|
interface SimFeatureTogglesProps {
|
|
subscriptionId: number;
|
|
voiceMailEnabled?: boolean;
|
|
callWaitingEnabled?: boolean;
|
|
internationalRoamingEnabled?: boolean;
|
|
networkType?: string; // '4G' | '5G'
|
|
onChanged?: () => void;
|
|
embedded?: boolean; // when true, render without outer card wrappers
|
|
}
|
|
|
|
export function SimFeatureToggles({
|
|
subscriptionId,
|
|
voiceMailEnabled,
|
|
callWaitingEnabled,
|
|
internationalRoamingEnabled,
|
|
networkType,
|
|
onChanged,
|
|
embedded = false,
|
|
}: SimFeatureTogglesProps) {
|
|
// Initial values
|
|
const initial = useMemo(
|
|
() => ({
|
|
vm: !!voiceMailEnabled,
|
|
cw: !!callWaitingEnabled,
|
|
ir: !!internationalRoamingEnabled,
|
|
nt: networkType === "5G" ? "5G" : "4G",
|
|
}),
|
|
[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 [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
setVm(initial.vm);
|
|
setCw(initial.cw);
|
|
setIr(initial.ir);
|
|
setNt(initial.nt as "4G" | "5G");
|
|
}, [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");
|
|
setError(null);
|
|
setSuccess(null);
|
|
};
|
|
|
|
const applyChanges = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
setSuccess(null);
|
|
try {
|
|
const featurePayload: {
|
|
voiceMailEnabled?: boolean;
|
|
callWaitingEnabled?: boolean;
|
|
internationalRoamingEnabled?: boolean;
|
|
networkType?: "4G" | "5G";
|
|
} = {};
|
|
if (vm !== initial.vm) featurePayload.voiceMailEnabled = vm;
|
|
if (cw !== initial.cw) featurePayload.callWaitingEnabled = cw;
|
|
if (ir !== initial.ir) featurePayload.internationalRoamingEnabled = ir;
|
|
if (nt !== initial.nt) featurePayload.networkType = nt;
|
|
|
|
if (Object.keys(featurePayload).length > 0) {
|
|
await apiClient.POST("/api/subscriptions/{id}/sim/features", {
|
|
params: { path: { id: subscriptionId } },
|
|
body: featurePayload,
|
|
});
|
|
}
|
|
|
|
setSuccess("Changes submitted successfully");
|
|
onChanged?.();
|
|
} catch (e: unknown) {
|
|
setError(e instanceof Error ? e.message : "Failed to submit changes");
|
|
} finally {
|
|
setLoading(false);
|
|
setTimeout(() => setSuccess(null), 3000);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Service Options */}
|
|
<div className={`${embedded ? "" : "bg-white rounded-xl border border-gray-100 shadow-md"}`}>
|
|
<div className={`${embedded ? "" : "p-6"} space-y-4`}>
|
|
{/* Voice Mail */}
|
|
<div className="flex items-center justify-between py-4">
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium text-slate-900">Voice Mail</div>
|
|
<div className="text-xs text-slate-500">¥300/month</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={vm}
|
|
onClick={() => setVm(!vm)}
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 ${
|
|
vm ? "bg-blue-600" : "bg-gray-200"
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
vm ? "translate-x-5" : "translate-x-0"
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Call Waiting */}
|
|
<div className="flex items-center justify-between py-4">
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium text-slate-900">Call Waiting</div>
|
|
<div className="text-xs text-slate-500">¥300/month</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={cw}
|
|
onClick={() => setCw(!cw)}
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 ${
|
|
cw ? "bg-blue-600" : "bg-gray-200"
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
cw ? "translate-x-5" : "translate-x-0"
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* International Roaming */}
|
|
<div className="flex items-center justify-between py-4">
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium text-slate-900">International Roaming</div>
|
|
<div className="text-xs text-slate-500">Global connectivity</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={ir}
|
|
onClick={() => setIr(!ir)}
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 ${
|
|
ir ? "bg-blue-600" : "bg-gray-200"
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
ir ? "translate-x-5" : "translate-x-0"
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<div className="mb-4">
|
|
<div className="text-sm font-medium text-slate-900 mb-1">Network Type</div>
|
|
<div className="text-xs text-slate-500">Choose your preferred connectivity</div>
|
|
<div className="text-xs text-red-600 mt-1">
|
|
Voice, network, and plan changes must be requested at least 30 minutes apart. If you just changed another option, you may need to wait before submitting.
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-4">
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="radio"
|
|
id="4g"
|
|
name="networkType"
|
|
value="4G"
|
|
checked={nt === "4G"}
|
|
onChange={() => setNt("4G")}
|
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
|
/>
|
|
<label htmlFor="4g" className="text-sm text-slate-700">
|
|
4G
|
|
</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="radio"
|
|
id="5g"
|
|
name="networkType"
|
|
value="5G"
|
|
checked={nt === "5G"}
|
|
onChange={() => setNt("5G")}
|
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
|
/>
|
|
<label htmlFor="5g" className="text-sm text-slate-700">
|
|
5G
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-2">5G connectivity for enhanced speeds</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notes and Actions */}
|
|
<div className={`${embedded ? "" : "bg-white rounded-xl border border-gray-200 p-6"}`}>
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
<h4 className="text-sm font-medium text-blue-900 mb-2 flex items-center gap-2">
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
Important Notes
|
|
</h4>
|
|
<ul className="text-xs text-blue-800 space-y-1">
|
|
<li className="flex items-start gap-2">
|
|
<span className="w-1 h-1 bg-blue-600 rounded-full mt-1.5 flex-shrink-0"></span>
|
|
Changes will take effect instantaneously (approx. 30min)
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<span className="w-1 h-1 bg-blue-600 rounded-full mt-1.5 flex-shrink-0"></span>
|
|
May require smartphone device restart after changes are applied
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<span className="w-1 h-1 bg-red-600 rounded-full mt-1.5 flex-shrink-0"></span>
|
|
<span className="text-red-600">Voice, network, and plan changes must be requested at least 30 minutes apart.</span>
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<span className="w-1 h-1 bg-blue-600 rounded-full mt-1.5 flex-shrink-0"></span>
|
|
Changes to Voice Mail / Call Waiting must be requested before the 25th of the month
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{success && (
|
|
<div className="mb-4 bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<div className="flex items-center">
|
|
<svg
|
|
className="h-5 w-5 text-green-500 mr-3"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
<p className="text-sm font-medium text-green-800">{success}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div className="flex items-center">
|
|
<svg
|
|
className="h-5 w-5 text-red-500 mr-3"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
/>
|
|
</svg>
|
|
<p className="text-sm font-medium text-red-800">{error}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|
<button
|
|
onClick={() => void applyChanges()}
|
|
disabled={loading}
|
|
className="flex-1 inline-flex items-center justify-center px-6 py-3 border border-transparent rounded-lg text-sm font-semibold text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<svg
|
|
className="animate-spin -ml-1 mr-3 h-4 w-4 text-white"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<circle
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
className="opacity-25"
|
|
></circle>
|
|
<path
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
className="opacity-75"
|
|
></path>
|
|
</svg>
|
|
Applying Changes...
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M5 13l4 4L19 7"
|
|
/>
|
|
</svg>
|
|
Apply Changes
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => reset()}
|
|
disabled={loading}
|
|
className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 rounded-lg text-sm font-semibold 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 disabled:cursor-not-allowed transition-colors duration-200"
|
|
>
|
|
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
/>
|
|
</svg>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|