tema f49e5d7574 Enhance SIM management features and introduce new cancellation and plan change flows
- Added new models and request types for enhanced SIM cancellation and plan change functionalities.
- Implemented full cancellation flow with email notifications and confirmation handling.
- Introduced enhanced plan change request with Salesforce product mapping and scheduling.
- Updated UI components for better user experience during SIM management actions.
- Improved error handling and validation for cancellation and plan change requests.
2025-11-29 18:27:58 +09:00

407 lines
17 KiB
TypeScript

"use client";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState, type ReactNode } from "react";
import { simActionsService, type CancellationPreview } from "@/features/subscriptions/services/sim-actions.service";
type Step = 1 | 2 | 3;
function Notice({ title, children }: { title: string; children: ReactNode }) {
return (
<div className="bg-yellow-50 border border-yellow-200 rounded p-4">
<div className="text-sm font-semibold text-yellow-900 mb-2">{title}</div>
<div className="text-sm text-yellow-800 leading-relaxed">{children}</div>
</div>
);
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div>
<div className="text-xs text-gray-500">{label}</div>
<div className="text-sm font-medium text-gray-900">{value}</div>
</div>
);
}
export function SimCancelContainer() {
const params = useParams();
const router = useRouter();
const subscriptionId = params.id as string;
const [step, setStep] = useState<Step>(1);
const [loading, setLoading] = useState(false);
const [preview, setPreview] = useState<CancellationPreview | null>(null);
const [error, setError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [acceptTerms, setAcceptTerms] = useState(false);
const [confirmMonthEnd, setConfirmMonthEnd] = useState(false);
const [selectedMonth, setSelectedMonth] = useState<string>("");
const [alternativeEmail, setAlternativeEmail] = useState<string>("");
const [alternativeEmail2, setAlternativeEmail2] = useState<string>("");
const [comments, setComments] = useState<string>("");
const [loadingPreview, setLoadingPreview] = useState(true);
useEffect(() => {
const fetchPreview = async () => {
try {
const data = await simActionsService.getCancellationPreview(subscriptionId);
setPreview(data);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to load cancellation information");
} finally {
setLoadingPreview(false);
}
};
void fetchPreview();
}, [subscriptionId]);
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const emailProvided = alternativeEmail.trim().length > 0 || alternativeEmail2.trim().length > 0;
const emailValid =
!emailProvided ||
(emailPattern.test(alternativeEmail.trim()) && emailPattern.test(alternativeEmail2.trim()));
const emailsMatch = !emailProvided || alternativeEmail.trim() === alternativeEmail2.trim();
const canProceedStep2 = !!preview && !!selectedMonth;
const canProceedStep3 = acceptTerms && confirmMonthEnd && emailValid && emailsMatch;
const selectedMonthInfo = preview?.availableMonths.find(m => m.value === selectedMonth);
const submit = async () => {
setLoading(true);
setError(null);
setMessage(null);
if (!selectedMonth) {
setError("Please select a cancellation month before submitting.");
setLoading(false);
return;
}
try {
await simActionsService.cancelFull(subscriptionId, {
cancellationMonth: selectedMonth,
confirmRead: acceptTerms,
confirmCancel: confirmMonthEnd,
alternativeEmail: alternativeEmail.trim() || undefined,
comments: comments.trim() || undefined,
});
setMessage("Cancellation request submitted. You will receive a confirmation email.");
setTimeout(() => router.push(`/subscriptions/${subscriptionId}#sim-management`), 2000);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to submit cancellation");
} finally {
setLoading(false);
}
};
if (loadingPreview) {
return (
<div className="max-w-3xl mx-auto p-6">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-1/3"></div>
<div className="h-64 bg-gray-200 rounded"></div>
</div>
</div>
);
}
return (
<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 className="flex items-center gap-2 mt-2">
{[1, 2, 3].map(s => (
<div
key={s}
className={`h-2 flex-1 rounded-full ${
s <= step ? "bg-blue-600" : "bg-gray-200"
}`}
></div>
))}
</div>
<div className="text-sm text-gray-500 mt-1">Step {step} of 3</div>
</div>
{error && (
<div className="text-red-700 bg-red-50 border border-red-200 rounded p-4 mb-4">{error}</div>
)}
{message && (
<div className="text-green-700 bg-green-50 border border-green-200 rounded p-4 mb-4">
{message}
</div>
)}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h1 className="text-xl font-semibold text-gray-900 mb-2">Cancel SIM</h1>
<p className="text-sm text-gray-600 mb-6">
Cancel your SIM subscription. Please read all the information carefully before proceeding.
</p>
{/* Minimum Contract Warning */}
{preview?.isWithinMinimumTerm && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div className="text-sm font-semibold text-red-900 mb-1">Minimum Contract Term Warning</div>
<div className="text-sm text-red-800">
Your subscription is still within the minimum contract period (ends {preview.minimumContractEndDate}).
Early cancellation may result in additional charges for the remaining months.
</div>
</div>
)}
{step === 1 && (
<div className="space-y-6">
{/* SIM Info */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg">
<InfoRow label="SIM Number" value={preview?.simNumber || "—"} />
<InfoRow label="Serial #" value={preview?.serialNumber || "—"} />
<InfoRow label="Start Date" value={preview?.startDate || "—"} />
</div>
{/* Month Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Cancellation Month
</label>
<select
value={selectedMonth}
onChange={e => {
setSelectedMonth(e.target.value);
setConfirmMonthEnd(false);
}}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
>
<option value="">Select month</option>
{preview?.availableMonths.map(month => (
<option key={month.value} value={month.value}>
{month.label}
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">
Your subscription will be cancelled at the end of the selected month.
</p>
</div>
<div className="flex justify-end">
<button
disabled={!canProceedStep2}
onClick={() => setStep(2)}
className="px-6 py-2 rounded-md bg-blue-600 text-white text-sm font-medium disabled:opacity-50 hover:bg-blue-700 transition-colors"
>
Next
</button>
</div>
</div>
)}
{step === 2 && (
<div className="space-y-6">
<div className="space-y-4">
<Notice title="[Cancellation Procedure]">
Online cancellations must be made from this website by the 25th of the desired
cancellation month. Once a request of a cancellation of the SONIXNET SIM is accepted
from this online form, a confirmation email containing details of the SIM plan will
be sent to the registered email address. The SIM card is a rental piece of hardware
and must be returned to Assist Solutions upon cancellation. The cancellation request
through this website retains to your SIM subscriptions only. To cancel any other
services with Assist Solutions (home internet etc.) please contact Assist Solutions
at info@asolutions.co.jp
</Notice>
<Notice title="[Minimum Contract Term]">
The SONIXNET SIM has a minimum contract term agreement of three months (sign-up
month is not included in the minimum term of three months; ie. sign-up in January =
minimum term is February, March, April). If the minimum contract term is not
fulfilled, the monthly fees of the remaining months will be charged upon
cancellation.
</Notice>
<Notice title="[Cancellation of Option Services (for Data+SMS/Voice Plan)]">
Cancellation of option services only (Voice Mail, Call Waiting) while keeping the
base plan active is not possible from this online form. Please contact Assist
Solutions Customer Support (info@asolutions.co.jp) for more information. Upon
cancelling the base plan, all additional options associated with the requested SIM
plan will be cancelled.
</Notice>
<Notice title="[MNP Transfer (for Data+SMS/Voice Plan)]">
Upon cancellation the SIM phone number will be lost. In order to keep the phone
number active to be used with a different cellular provider, a request for an MNP
transfer (administrative fee ¥1,000+tax) is necessary. The MNP cannot be requested
from this online form. Please contact Assist Solutions Customer Support
(info@asolutions.co.jp) for more information.
</Notice>
</div>
<div className="space-y-3 bg-gray-50 rounded-lg p-4">
<div className="flex items-start gap-3">
<input
id="acceptTerms"
type="checkbox"
checked={acceptTerms}
onChange={e => setAcceptTerms(e.target.checked)}
className="h-4 w-4 text-blue-600 border-gray-300 rounded mt-0.5"
/>
<label htmlFor="acceptTerms" className="text-sm text-gray-700">
I have read and accepted the conditions above.
</label>
</div>
<div className="flex items-start gap-3">
<input
id="confirmMonthEnd"
type="checkbox"
checked={confirmMonthEnd}
onChange={e => setConfirmMonthEnd(e.target.checked)}
disabled={!selectedMonth}
className="h-4 w-4 text-blue-600 border-gray-300 rounded mt-0.5"
/>
<label htmlFor="confirmMonthEnd" className="text-sm text-gray-700">
I would like to cancel my SonixNet SIM subscription at the end of{" "}
<strong>{selectedMonthInfo?.label || "the selected month"}</strong>.
</label>
</div>
</div>
<div className="flex justify-between">
<button
onClick={() => setStep(1)}
className="px-6 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50 transition-colors"
>
Back
</button>
<button
disabled={!canProceedStep3}
onClick={() => setStep(3)}
className="px-6 py-2 rounded-md bg-blue-600 text-white text-sm font-medium disabled:opacity-50 hover:bg-blue-700 transition-colors"
>
Next
</button>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-6">
{/* Voice SIM Notice */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-sm font-semibold text-blue-900 mb-2">
For Voice-enabled SIM subscriptions:
</div>
<div className="text-sm text-blue-800">
Calling charges are post payment. Your bill for the final month&apos;s calling charges
will be charged on your credit card on file during the first week of the second month
after the cancellation.
</div>
<div className="text-sm text-blue-800 mt-2">
If you would like to make the payment with a different credit card, please contact
Assist Solutions at info@asolutions.co.jp
</div>
</div>
{/* Registered Email */}
<div className="text-sm text-gray-800">
Your registered email address is:{" "}
<span className="font-medium">{preview?.customerEmail || "—"}</span>
</div>
<div className="text-sm text-gray-600">
You will receive a cancellation confirmation email. If you would like to receive this
email on a different address, please enter the address below.
</div>
{/* Alternative Email */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email address:
</label>
<input
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
value={alternativeEmail}
onChange={e => setAlternativeEmail(e.target.value)}
placeholder="you@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">(Confirm):</label>
<input
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
value={alternativeEmail2}
onChange={e => setAlternativeEmail2(e.target.value)}
placeholder="you@example.com"
/>
</div>
</div>
{emailProvided && !emailValid && (
<div className="text-xs text-red-600">Please enter a valid email address in both fields.</div>
)}
{emailProvided && emailValid && !emailsMatch && (
<div className="text-xs text-red-600">Email addresses do not match.</div>
)}
{/* Comments */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
If you have any other questions/comments/requests regarding your cancellation, please
note them below and an Assist Solutions staff will contact you shortly.
</label>
<textarea
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
rows={4}
value={comments}
onChange={e => setComments(e.target.value)}
placeholder="Optional: Enter any questions or requests here."
/>
</div>
{/* Final Warning */}
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="text-sm font-semibold text-red-900 mb-1">
Your cancellation request is not confirmed yet.
</div>
<div className="text-sm text-red-800">
This is the final page. To finalize your cancellation request please proceed from
REQUEST CANCELLATION below.
</div>
</div>
<div className="flex justify-between">
<button
onClick={() => setStep(2)}
className="px-6 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50 transition-colors"
>
Back
</button>
<button
onClick={() => {
if (
window.confirm(
`Are you sure you want to cancel your SIM subscription? This will take effect at the end of ${selectedMonthInfo?.label || selectedMonth}.`
)
) {
void submit();
}
}}
disabled={loading || !canProceedStep3}
className="px-6 py-2 rounded-md bg-red-600 text-white text-sm font-medium disabled:opacity-50 hover:bg-red-700 transition-colors"
>
{loading ? "Processing…" : "REQUEST CANCELLATION"}
</button>
</div>
</div>
)}
</div>
</div>
);
}
export default SimCancelContainer;