- Removed legacy color aliases from globals.css to streamline design tokens. - Updated various components, including Badge, Button, Checkbox, and Input, to utilize new color tokens for error states. - Enhanced error messaging styles in components like ErrorBoundary and AlertBanner for better visual coherence. - Standardized color usage in billing and subscription components to align with updated design tokens. - Improved overall styling consistency across the application by adopting the new color system.
429 lines
16 KiB
TypeScript
429 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import React, { useMemo, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import {
|
|
PlusIcon,
|
|
ArrowPathIcon,
|
|
XMarkIcon,
|
|
ExclamationTriangleIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import { TopUpModal } from "./TopUpModal";
|
|
import { ChangePlanModal } from "./ChangePlanModal";
|
|
import { ReissueSimModal } from "./ReissueSimModal";
|
|
import { apiClient } from "@/lib/api";
|
|
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
|
import { Button } from "@/components/atoms/button";
|
|
|
|
interface SimActionsProps {
|
|
subscriptionId: number;
|
|
simType: "physical" | "esim";
|
|
status: string;
|
|
onTopUpSuccess?: () => void;
|
|
onPlanChangeSuccess?: () => void;
|
|
onCancelSuccess?: () => void;
|
|
onReissueSuccess?: () => void;
|
|
embedded?: boolean; // when true, render content without card container
|
|
currentPlanCode?: string;
|
|
}
|
|
|
|
export function SimActions({
|
|
subscriptionId,
|
|
simType,
|
|
status,
|
|
onTopUpSuccess,
|
|
onPlanChangeSuccess,
|
|
onCancelSuccess,
|
|
onReissueSuccess,
|
|
embedded = false,
|
|
currentPlanCode,
|
|
}: SimActionsProps) {
|
|
const router = useRouter();
|
|
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
|
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
|
const [showReissueModal, setShowReissueModal] = useState(false);
|
|
const [loading, setLoading] = useState<string | null>(null);
|
|
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;
|
|
const canReissue = isActive;
|
|
const canCancel = isActive;
|
|
|
|
const reissueDisabledReason = useMemo(() => {
|
|
if (!isActive) {
|
|
return "SIM must be active to request a reissue.";
|
|
}
|
|
return null;
|
|
}, [isActive]);
|
|
|
|
const handleCancelSim = async () => {
|
|
setLoading("cancel");
|
|
setError(null);
|
|
|
|
try {
|
|
await apiClient.POST("/api/subscriptions/{id}/sim/cancel", {
|
|
params: { path: { id: subscriptionId } },
|
|
body: {},
|
|
});
|
|
|
|
setSuccess("SIM service cancelled successfully");
|
|
setShowCancelConfirm(false);
|
|
onCancelSuccess?.();
|
|
} catch (error: unknown) {
|
|
setError(
|
|
process.env.NODE_ENV === "development"
|
|
? error instanceof Error
|
|
? error.message
|
|
: "Failed to cancel SIM service"
|
|
: "Unable to cancel SIM service right now. Please try again."
|
|
);
|
|
} finally {
|
|
setLoading(null);
|
|
}
|
|
};
|
|
|
|
// Clear success/error messages after 5 seconds
|
|
React.useEffect(() => {
|
|
if (success || error) {
|
|
const timer = setTimeout(() => {
|
|
setSuccess(null);
|
|
setError(null);
|
|
}, 5000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
return;
|
|
}, [success, error]);
|
|
|
|
return (
|
|
<div
|
|
id="sim-actions"
|
|
className={`${embedded ? "" : "bg-card shadow-[var(--cp-shadow-1)] rounded-xl border border-border"}`}
|
|
>
|
|
{/* Header */}
|
|
{!embedded && (
|
|
<div className="px-6 py-6 border-b border-border">
|
|
<h3 className="text-lg font-semibold tracking-tight text-foreground mb-1">
|
|
SIM Management Actions
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">Manage your SIM service</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Content */}
|
|
<div className={`${embedded ? "" : "px-6 lg:px-8 py-6"}`}>
|
|
{/* Status Messages */}
|
|
{success && (
|
|
<div className="mb-4">
|
|
<AlertBanner variant="success" title="Success" size="sm" elevated>
|
|
{success}
|
|
</AlertBanner>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="mb-4">
|
|
<AlertBanner variant="error" title="Unable to complete action" size="sm" elevated>
|
|
{error}
|
|
</AlertBanner>
|
|
</div>
|
|
)}
|
|
|
|
{!isActive && (
|
|
<div className="mb-4">
|
|
<AlertBanner variant="warning" title="Not available" size="sm" elevated>
|
|
SIM management actions are only available for active services.
|
|
</AlertBanner>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="space-y-3">
|
|
{/* Top Up Data - Primary Action */}
|
|
<button
|
|
onClick={() => {
|
|
setActiveInfo("topup");
|
|
try {
|
|
router.push(`/subscriptions/${subscriptionId}/sim/top-up`);
|
|
} catch {
|
|
setShowTopUpModal(true);
|
|
}
|
|
}}
|
|
disabled={!canTopUp || loading !== null}
|
|
className={`w-full flex items-center justify-start px-4 py-4 rounded-lg text-sm font-medium transition-colors duration-[var(--cp-duration-normal)] ${
|
|
canTopUp && loading === null
|
|
? "text-primary-foreground bg-primary hover:bg-primary-hover shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)] focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background"
|
|
: "text-muted-foreground bg-muted cursor-not-allowed"
|
|
}`}
|
|
>
|
|
<div className="flex items-center">
|
|
<PlusIcon className="h-4 w-4 mr-3" />
|
|
<div className="text-left">
|
|
<div className="font-medium">
|
|
{loading === "topup" ? "Processing..." : "Top Up Data"}
|
|
</div>
|
|
<div className="text-xs opacity-90">Add more data to your plan</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Change Plan - Secondary Action */}
|
|
<button
|
|
onClick={() => {
|
|
setActiveInfo("changePlan");
|
|
try {
|
|
router.push(`/subscriptions/${subscriptionId}/sim/change-plan`);
|
|
} catch {
|
|
setShowChangePlanModal(true);
|
|
}
|
|
}}
|
|
disabled={!isActive || loading !== null}
|
|
className={`w-full flex items-center justify-start px-4 py-4 rounded-lg text-sm font-medium transition-colors duration-[var(--cp-duration-normal)] ${
|
|
isActive && loading === null
|
|
? "text-secondary-foreground bg-secondary hover:bg-secondary/80 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)] focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background"
|
|
: "text-muted-foreground bg-muted cursor-not-allowed"
|
|
}`}
|
|
>
|
|
<div className="flex items-center">
|
|
<ArrowPathIcon className="h-4 w-4 mr-3" />
|
|
<div className="text-left">
|
|
<div className="font-medium">
|
|
{loading === "change-plan" ? "Processing..." : "Change Plan"}
|
|
</div>
|
|
<div className="text-xs opacity-70">Switch to a different plan</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Reissue SIM */}
|
|
<button
|
|
onClick={() => {
|
|
setActiveInfo("reissue");
|
|
setShowReissueModal(true);
|
|
}}
|
|
disabled={!canReissue || loading !== null}
|
|
className={`w-full flex flex-col items-start justify-start rounded-lg border px-4 py-4 text-left text-sm font-medium transition-colors duration-[var(--cp-duration-normal)] ${
|
|
canReissue && loading === null
|
|
? "border-success/30 bg-success-soft text-foreground hover:bg-success-soft/80 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)] focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background"
|
|
: "text-muted-foreground bg-muted border-border cursor-not-allowed"
|
|
}`}
|
|
>
|
|
<div className="flex w-full items-center justify-between">
|
|
<div className="flex items-center">
|
|
<ArrowPathIcon className="h-4 w-4 mr-3" />
|
|
<div className="text-left">
|
|
<div className="font-medium">{"Reissue SIM"}</div>
|
|
<div className="text-xs opacity-70">
|
|
Configure replacement options and submit your request.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{!canReissue && reissueDisabledReason && (
|
|
<div className="mt-3 w-full rounded-md border border-warning/25 bg-warning-soft px-3 py-2 text-xs text-muted-foreground">
|
|
{reissueDisabledReason}
|
|
</div>
|
|
)}
|
|
</button>
|
|
|
|
{/* Cancel SIM - Destructive Action */}
|
|
<button
|
|
onClick={() => {
|
|
setActiveInfo("cancel");
|
|
try {
|
|
router.push(`/subscriptions/${subscriptionId}/sim/cancel`);
|
|
} catch {
|
|
// Fallback to inline confirmation modal if navigation is unavailable
|
|
setShowCancelConfirm(true);
|
|
}
|
|
}}
|
|
disabled={!canCancel || loading !== null}
|
|
className={`w-full flex items-center justify-start px-4 py-4 rounded-lg text-sm font-medium transition-colors duration-[var(--cp-duration-normal)] ${
|
|
canCancel && loading === null
|
|
? "text-danger bg-danger-soft border border-danger/30 hover:bg-danger-soft/80 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)] focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background"
|
|
: "text-muted-foreground bg-muted border border-border cursor-not-allowed"
|
|
}`}
|
|
>
|
|
<div className="flex items-center">
|
|
<XMarkIcon className="h-4 w-4 mr-3" />
|
|
<div className="text-left">
|
|
<div className="font-medium">
|
|
{loading === "cancel" ? "Processing..." : "Cancel SIM"}
|
|
</div>
|
|
<div className="text-xs opacity-70">Permanently cancel service</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Action Description (contextual) */}
|
|
{activeInfo && (
|
|
<div className="mt-6 text-sm text-muted-foreground bg-muted border border-border rounded-lg p-4">
|
|
{activeInfo === "topup" && (
|
|
<div className="flex items-start">
|
|
<PlusIcon className="h-4 w-4 text-primary 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>
|
|
)}
|
|
{activeInfo === "reissue" && (
|
|
<div className="flex items-start">
|
|
<ArrowPathIcon className="h-4 w-4 text-success mr-2 mt-0.5 flex-shrink-0" />
|
|
<div>
|
|
<strong>Reissue SIM:</strong> Submit a replacement request for either a physical
|
|
SIM or an eSIM. eSIM users can optionally supply a new EID to pair with the
|
|
replacement profile.
|
|
</div>
|
|
</div>
|
|
)}
|
|
{activeInfo === "cancel" && (
|
|
<div className="flex items-start">
|
|
<XMarkIcon className="h-4 w-4 text-danger 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-primary 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-warning 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);
|
|
setActiveInfo(null);
|
|
}}
|
|
onSuccess={() => {
|
|
setShowTopUpModal(false);
|
|
setSuccess("Data top-up completed successfully");
|
|
onTopUpSuccess?.();
|
|
}}
|
|
onError={message => setError(message)}
|
|
/>
|
|
)}
|
|
|
|
{/* 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 SIM Modal */}
|
|
{showReissueModal && (
|
|
<ReissueSimModal
|
|
subscriptionId={subscriptionId}
|
|
currentSimType={simType}
|
|
onClose={() => {
|
|
setShowReissueModal(false);
|
|
setActiveInfo(null);
|
|
}}
|
|
onSuccess={() => {
|
|
setShowReissueModal(false);
|
|
setSuccess("SIM reissue request submitted successfully");
|
|
onReissueSuccess?.();
|
|
}}
|
|
onError={message => {
|
|
setError(message);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Cancel Confirmation */}
|
|
{showCancelConfirm && (
|
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
<div className="fixed inset-0 bg-background/70 backdrop-blur-sm transition-opacity"></div>
|
|
<div className="inline-block align-bottom bg-card rounded-lg text-left overflow-hidden shadow-[var(--cp-shadow-3)] border border-border transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
|
<div className="bg-card px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
<div className="sm:flex sm:items-start">
|
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-danger-soft sm:mx-0 sm:h-10 sm:w-10">
|
|
<ExclamationTriangleIcon className="h-6 w-6 text-danger" />
|
|
</div>
|
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
<h3 className="text-lg leading-6 font-medium text-foreground">
|
|
Cancel SIM Service
|
|
</h3>
|
|
<div className="mt-2">
|
|
<p className="text-sm text-muted-foreground">
|
|
Are you sure you want to cancel this SIM service? This action cannot be
|
|
undone and will permanently terminate your service.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-muted px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-3">
|
|
<Button
|
|
variant="destructive"
|
|
onClick={() => void handleCancelSim()}
|
|
loading={loading === "cancel"}
|
|
loadingText="Processing…"
|
|
>
|
|
Cancel SIM
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setShowCancelConfirm(false);
|
|
setActiveInfo(null);
|
|
}}
|
|
disabled={loading === "cancel"}
|
|
>
|
|
Back
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|