fix: resolve SIM management modal, plan change, and voice feature issues

- Fix Tailwind v4 modal stacking bug by adding relative z-10 to modal
  content divs (CancellationFlow, ChangePlanModal, TopUpModal, SimActions)
- Add test mode for immediate plan changes (SIM_BILLING_TEST_MODE) instead
  of scheduling for 1st of next month
- Bypass rate limiter spacing/cancellation checks in test mode
- Hide voice feature toggles for data-only SIMs using hasVoice flag
- Guard BFF voice feature updates to reject early for data-only SIMs
- Fix Freebit retry logic to not retry business errors (e.g. resultCode 260)
- Add user-friendly error message for resultCode 260 (voice not active)
- Update plan change page text to reflect test mode behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Temuulen Ankhbayar 2026-02-07 14:53:07 +09:00
parent 191a377657
commit df017d520f
13 changed files with 1392 additions and 398 deletions

View File

@ -49,13 +49,27 @@ export class FreebitError extends Error {
} }
/** /**
* Check if error is retryable * Check if error is retryable.
*
* IMPORTANT: Freebit uses statusCode "500" for business logic errors (e.g. 210, 211, 260),
* not just HTTP 500 server errors. Only resultCode 900 (unexpected error) is truly retryable.
* We should NOT retry known business errors even though their statusCode is "500".
*/ */
isRetryable(): boolean { isRetryable(): boolean {
const retryableCodes = ["500", "502", "503", "504", "408", "429"]; // Freebit-specific: only retry unexpected errors (900) or actual HTTP transport errors
if (this.resultCode === "900") {
return true;
}
// If we have a Freebit resultCode, it's a business error — don't retry
if (this.resultCode && this.resultCode !== "900") {
return false;
}
// For non-Freebit errors (HTTP transport failures), use standard retry logic
const retryableHttpCodes = ["500", "502", "503", "504", "408", "429"];
return ( return (
retryableCodes.includes(String(this.resultCode)) || retryableHttpCodes.includes(String(this.statusCode)) ||
retryableCodes.includes(String(this.statusCode)) ||
this.message.toLowerCase().includes("timeout") || this.message.toLowerCase().includes("timeout") ||
this.message.toLowerCase().includes("network") this.message.toLowerCase().includes("network")
); );
@ -94,6 +108,10 @@ export class FreebitError extends Error {
return "Plan change failed. This may be due to: (1) Account has existing scheduled operations, (2) Invalid plan code for this account, (3) Account restrictions. Please check the Freebit Partner Tools for account status or contact support."; return "Plan change failed. This may be due to: (1) Account has existing scheduled operations, (2) Invalid plan code for this account, (3) Account restrictions. Please check the Freebit Partner Tools for account status or contact support.";
} }
if (this.resultCode === "260" || this.statusCode === "260") {
return "Voice options cannot be changed because voice service is not active on this account. Voice options must first be registered via the initial setup process.";
}
if (this.resultCode === "381" || this.statusCode === "381") { if (this.resultCode === "381" || this.statusCode === "381") {
return "Network type change rejected. The current plan does not allow switching to the requested contract line. Adjust the plan first or contact support."; return "Network type change rejected. The current plan does not allow switching to the requested contract line. Adjust the plan first or contact support.";
} }

View File

@ -1,4 +1,5 @@
import { Injectable, Inject, BadRequestException } from "@nestjs/common"; import { Injectable, Inject, BadRequestException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino"; import { Logger } from "nestjs-pino";
import type { Redis } from "ioredis"; import type { Redis } from "ioredis";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
@ -32,10 +33,20 @@ export class FreebitRateLimiterService {
private readonly windowMs = 30 * 60 * 1000; // 30 minute window between operations private readonly windowMs = 30 * 60 * 1000; // 30 minute window between operations
private readonly lockTtlMs = Math.min(this.windowMs, 10 * 60 * 1000); private readonly lockTtlMs = Math.min(this.windowMs, 10 * 60 * 1000);
private readonly testMode: boolean;
constructor( constructor(
@Inject("REDIS_CLIENT") private readonly redis: Redis, @Inject("REDIS_CLIENT") private readonly redis: Redis,
private readonly configService: ConfigService,
@Inject(Logger) private readonly logger: Logger @Inject(Logger) private readonly logger: Logger
) {} ) {
this.testMode = this.configService.get<string>("SIM_BILLING_TEST_MODE") === "true";
if (this.testMode) {
this.logger.warn(
"Freebit rate limiter is in TEST MODE - spacing checks and cancellation blocks are bypassed"
);
}
}
/** /**
* Cleanup stale entries from operationTimestamps to prevent memory leak. * Cleanup stale entries from operationTimestamps to prevent memory leak.
@ -174,6 +185,13 @@ export class FreebitRateLimiterService {
* Throws BadRequestException if the operation violates timing rules. * Throws BadRequestException if the operation violates timing rules.
*/ */
async assertOperationSpacing(account: string, op: OperationType): Promise<void> { async assertOperationSpacing(account: string, op: OperationType): Promise<void> {
if (this.testMode) {
this.logger.debug(
`TEST MODE: Skipping operation spacing check for ${op} on account ${account}`
);
return;
}
const now = Date.now(); const now = Date.now();
const entry = await this.getOperationWindow(account); const entry = await this.getOperationWindow(account);

View File

@ -34,6 +34,8 @@ const FREEBIT_PLAN_CODE_TO_SKU: Record<string, string> = Object.fromEntries(
@Injectable() @Injectable()
export class SimPlanService { export class SimPlanService {
private readonly testMode: boolean;
// eslint-disable-next-line max-params -- NestJS dependency injection requires all services to be injected via constructor // eslint-disable-next-line max-params -- NestJS dependency injection requires all services to be injected via constructor
constructor( constructor(
private readonly freebitService: FreebitFacade, private readonly freebitService: FreebitFacade,
@ -44,7 +46,14 @@ export class SimPlanService {
private readonly simCatalog: SimServicesService, private readonly simCatalog: SimServicesService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
@Inject(Logger) private readonly logger: Logger @Inject(Logger) private readonly logger: Logger
) {} ) {
this.testMode = this.configService.get<string>("SIM_BILLING_TEST_MODE") === "true";
if (this.testMode) {
this.logger.warn(
"SIM Plan service is in TEST MODE - plan changes will take effect immediately"
);
}
}
private get freebitBaseUrl(): string { private get freebitBaseUrl(): string {
return this.configService.get<string>("FREEBIT_BASE_URL") || "https://i1.mvno.net/emptool/api"; return this.configService.get<string>("FREEBIT_BASE_URL") || "https://i1.mvno.net/emptool/api";
@ -194,13 +203,22 @@ export class SimPlanService {
throw new BadRequestException("Invalid plan code"); throw new BadRequestException("Invalid plan code");
} }
// Always schedule for 1st of following month // In test mode, schedule for today (immediate effect); otherwise 1st of following month
const nextMonth = new Date(); let scheduledAt: string;
nextMonth.setMonth(nextMonth.getMonth() + 1); if (this.testMode) {
nextMonth.setDate(1); const today = new Date();
const year = nextMonth.getFullYear(); const year = today.getFullYear();
const month = String(nextMonth.getMonth() + 1).padStart(2, "0"); const month = String(today.getMonth() + 1).padStart(2, "0");
const scheduledAt = `${year}${month}01`; const day = String(today.getDate()).padStart(2, "0");
scheduledAt = `${year}${month}${day}`;
} else {
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
nextMonth.setDate(1);
const year = nextMonth.getFullYear();
const month = String(nextMonth.getMonth() + 1).padStart(2, "0");
scheduledAt = `${year}${month}01`;
}
this.logger.log("Submitting SIM plan change request (full)", { this.logger.log("Submitting SIM plan change request (full)", {
userId, userId,
@ -288,6 +306,16 @@ export class SimPlanService {
const hasVoiceChanges = this.hasVoiceFeatureChanges(request); const hasVoiceChanges = this.hasVoiceFeatureChanges(request);
const hasContractChange = typeof request.networkType === "string"; const hasContractChange = typeof request.networkType === "string";
// Guard: reject voice feature changes for data-only SIMs (voice not active)
if (hasVoiceChanges) {
const simDetails = await this.freebitService.getSimDetails(account);
if (!simDetails.hasVoice) {
throw new BadRequestException(
"Voice features cannot be changed because voice service is not active on this data-only SIM."
);
}
}
if (hasVoiceChanges && hasContractChange) { if (hasVoiceChanges && hasContractChange) {
await this.applyVoiceThenContractChanges(account, request, userId, subscriptionId); await this.applyVoiceThenContractChanges(account, request, userId, subscriptionId);
} else { } else {

View File

@ -1,109 +1,513 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Button } from "@/components/atoms"; import { Button } from "@/components/atoms";
import { Server, Monitor, Wrench, Globe } from "lucide-react"; import Link from "next/link";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "IT Solutions for International Businesses in Japan | Assist Solutions", title: "IT Solutions for International Businesses in Japan | Assist Solutions",
description: description:
"Enterprise IT for foreign companies in Japan. Dedicated internet, office networks, data center hosting, all with bilingual support. We understand international business needs.", "Enterprise IT for foreign companies in Japan. Dedicated internet, office networks, data center hosting, web construction & maintenance, all with bilingual support.",
keywords: [ keywords: [
"IT for foreign companies Japan", "IT for foreign companies Japan",
"international business IT Tokyo", "international business IT Tokyo",
"bilingual IT support Japan", "bilingual IT support Japan",
"office network setup foreigners", "office network setup foreigners",
"enterprise IT English support", "enterprise IT English support",
"web design Japan English",
"website maintenance Tokyo",
"bilingual web development",
], ],
openGraph: { openGraph: {
title: "Business IT for International Companies - Assist Solutions", title: "Business IT for International Companies - Assist Solutions",
description: description:
"Enterprise IT with bilingual support. Dedicated internet, office networks, and data center services for foreign companies in Japan.", "Enterprise IT with bilingual support. Dedicated internet, office networks, web construction, and data center services for foreign companies in Japan.",
type: "website", type: "website",
}, },
}; };
export default function BusinessSolutionsPage() { // Abstract tech illustration components
function NetworkNodesIllustration({ className }: { className?: string }) {
return ( return (
<div className="max-w-6xl mx-auto px-4"> <svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* Header */} <circle cx="100" cy="100" r="8" fill="currentColor" fillOpacity="0.3" />
<div className="text-center mb-16 pt-8"> <circle cx="40" cy="60" r="6" fill="currentColor" fillOpacity="0.2" />
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-6 tracking-tight"> <circle cx="160" cy="50" r="5" fill="currentColor" fillOpacity="0.25" />
IT for International Businesses <circle cx="150" cy="140" r="7" fill="currentColor" fillOpacity="0.2" />
</h1> <circle cx="50" cy="150" r="5" fill="currentColor" fillOpacity="0.25" />
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed"> <circle cx="170" cy="100" r="4" fill="currentColor" fillOpacity="0.15" />
Running an international company in Japan? We provide enterprise IT with bilingual <circle cx="30" cy="110" r="4" fill="currentColor" fillOpacity="0.15" />
support, so your team can focus on business, not navigating Japanese tech providers. <line
</p> x1="100"
y1="100"
x2="40"
y2="60"
stroke="currentColor"
strokeOpacity="0.15"
strokeWidth="1"
/>
<line
x1="100"
y1="100"
x2="160"
y2="50"
stroke="currentColor"
strokeOpacity="0.15"
strokeWidth="1"
/>
<line
x1="100"
y1="100"
x2="150"
y2="140"
stroke="currentColor"
strokeOpacity="0.15"
strokeWidth="1"
/>
<line
x1="100"
y1="100"
x2="50"
y2="150"
stroke="currentColor"
strokeOpacity="0.15"
strokeWidth="1"
/>
<line
x1="100"
y1="100"
x2="170"
y2="100"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="1"
/>
<line
x1="100"
y1="100"
x2="30"
y2="110"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="1"
/>
<circle
cx="100"
cy="100"
r="40"
stroke="currentColor"
strokeOpacity="0.08"
strokeWidth="1"
strokeDasharray="4 4"
/>
<circle
cx="100"
cy="100"
r="70"
stroke="currentColor"
strokeOpacity="0.05"
strokeWidth="1"
strokeDasharray="4 4"
/>
</svg>
);
}
function GearIllustration({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 130c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30-16.569 0-30 13.431-30 30 0 16.569 13.431 30 30 30z"
stroke="currentColor"
strokeOpacity="0.2"
strokeWidth="2"
/>
<path
d="M100 60V40M100 160v-20M140 100h20M40 100h20M128.28 71.72l14.14-14.14M57.58 142.42l14.14-14.14M128.28 128.28l14.14 14.14M57.58 57.58l14.14 14.14"
stroke="currentColor"
strokeOpacity="0.15"
strokeWidth="2"
strokeLinecap="round"
/>
<circle cx="100" cy="100" r="15" stroke="currentColor" strokeOpacity="0.25" strokeWidth="2" />
<circle
cx="155"
cy="155"
r="20"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="1.5"
/>
<circle cx="155" cy="155" r="8" fill="currentColor" fillOpacity="0.08" />
</svg>
);
}
function ShieldIllustration({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 30L40 55v45c0 38.66 25.64 74.64 60 85 34.36-10.36 60-46.34 60-85V55L100 30z"
stroke="currentColor"
strokeOpacity="0.15"
strokeWidth="2"
fill="currentColor"
fillOpacity="0.03"
/>
<path
d="M100 50L55 70v35c0 30.13 19.98 58.15 45 66.25V50z"
fill="currentColor"
fillOpacity="0.05"
/>
<path
d="M85 100l10 10 20-25"
stroke="currentColor"
strokeOpacity="0.25"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function CodeIllustration({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
x="30"
y="40"
width="140"
height="100"
rx="8"
stroke="currentColor"
strokeOpacity="0.15"
strokeWidth="2"
/>
<line
x1="30"
y1="60"
x2="170"
y2="60"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="1"
/>
<circle cx="45" cy="50" r="4" fill="currentColor" fillOpacity="0.15" />
<circle cx="60" cy="50" r="4" fill="currentColor" fillOpacity="0.15" />
<circle cx="75" cy="50" r="4" fill="currentColor" fillOpacity="0.15" />
<path
d="M70 95l-20 15 20 15"
stroke="currentColor"
strokeOpacity="0.2"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M130 95l20 15-20 15"
stroke="currentColor"
strokeOpacity="0.2"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<line
x1="90"
y1="130"
x2="110"
y2="80"
stroke="currentColor"
strokeOpacity="0.25"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
function ServerIllustration({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
x="50"
y="30"
width="100"
height="35"
rx="4"
stroke="currentColor"
strokeOpacity="0.2"
strokeWidth="2"
fill="currentColor"
fillOpacity="0.03"
/>
<rect
x="50"
y="75"
width="100"
height="35"
rx="4"
stroke="currentColor"
strokeOpacity="0.2"
strokeWidth="2"
fill="currentColor"
fillOpacity="0.03"
/>
<rect
x="50"
y="120"
width="100"
height="35"
rx="4"
stroke="currentColor"
strokeOpacity="0.2"
strokeWidth="2"
fill="currentColor"
fillOpacity="0.03"
/>
<circle cx="65" cy="47" r="4" fill="currentColor" fillOpacity="0.3" />
<circle cx="65" cy="92" r="4" fill="currentColor" fillOpacity="0.2" />
<circle cx="65" cy="137" r="4" fill="currentColor" fillOpacity="0.25" />
<line
x1="80"
y1="47"
x2="130"
y2="47"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="2"
strokeLinecap="round"
/>
<line
x1="80"
y1="92"
x2="120"
y2="92"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="2"
strokeLinecap="round"
/>
<line
x1="80"
y1="137"
x2="125"
y2="137"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M100 165v20M85 185h30"
stroke="currentColor"
strokeOpacity="0.15"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
// Service card component
interface ServiceCardProps {
category: string;
title: string;
description: string;
bgColor: string;
textColor: string;
accentColor: string;
illustration: React.ReactNode;
href: string;
}
function ServiceCard({
category,
title,
description,
bgColor,
textColor,
accentColor,
illustration,
href,
}: ServiceCardProps) {
return (
<div
className={`relative overflow-hidden rounded-2xl ${bgColor} p-8 flex flex-col justify-between min-h-[420px] group transition-all duration-300 hover:-translate-y-1 border border-border/60 shadow-[0_4px_20px_-4px_rgba(0,0,0,0.08)] hover:shadow-[0_8px_30px_-4px_rgba(0,0,0,0.12)]`}
>
{/* Tech illustration overlay */}
<div className={`absolute top-0 right-0 w-48 h-48 ${accentColor} pointer-events-none`}>
{illustration}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-16"> {/* Content */}
{/* Office LAN Setup */} <div className="relative z-10 flex flex-col h-full">
<div className="bg-card rounded-2xl border border-border/60 p-8 shadow-sm"> {/* Category label */}
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-6"> <span
<Monitor className="h-6 w-6" /> className={`text-xs font-semibold tracking-widest uppercase ${textColor} opacity-70 mb-4`}
</div> >
<h2 className="text-2xl font-bold text-foreground mb-4">Office LAN Setup</h2> {category}
<p className="text-muted-foreground leading-relaxed mb-6"> </span>
Setting up a new office or upgrading your network? We handle everything in English. from
planning to installation. Cable runs, switches, routers, and firewalls configured by
bilingual technicians who understand international business needs.
</p>
</div>
{/* Onsite & Remote Tech Support */} {/* Title */}
<div className="bg-card rounded-2xl border border-border/60 p-8 shadow-sm"> <h2
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-6"> className={`text-2xl lg:text-3xl font-bold ${textColor} mb-4 leading-tight max-w-[85%]`}
<Wrench className="h-6 w-6" /> >
</div> {title}
<h2 className="text-2xl font-bold text-foreground mb-4">Onsite & Remote Tech Support</h2>
<p className="text-muted-foreground leading-relaxed mb-6">
IT issues don&apos;t wait, and neither do we. Our English-speaking technicians provide
fast onsite and remote support for your business. Network problems, hardware issues,
software setup. We keep your operations running smoothly.
</p>
</div>
{/* Dedicated Internet Access (DIA) */}
<div className="bg-card rounded-2xl border border-border/60 p-8 shadow-sm">
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-6">
<Globe className="h-6 w-6" />
</div>
<h2 className="text-2xl font-bold text-foreground mb-4">
Dedicated Internet Access (DIA)
</h2>
<p className="text-muted-foreground leading-relaxed mb-6">
Need guaranteed bandwidth for your business? Our Dedicated Internet Access provides
enterprise-grade connectivity with SLA guarantees. Perfect for companies requiring
reliable, high-capacity connections with English contracts and support.
</p>
</div>
{/* Data Center Service */}
<div className="bg-card rounded-2xl border border-border/60 p-8 shadow-sm">
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-6">
<Server className="h-6 w-6" />
</div>
<h2 className="text-2xl font-bold text-foreground mb-4">Data Center Service</h2>
<p className="text-muted-foreground leading-relaxed mb-6">
Host your infrastructure in world-class Tokyo data centers (Equinix, GDC Gotenyama). We
provide colocation and managed services with English support, making it easy for
international companies to establish reliable IT infrastructure in Japan.
</p>
</div>
</div>
{/* CTA */}
<div className="text-center py-12 bg-muted/20 rounded-3xl mb-16">
<h2 className="text-2xl font-bold text-foreground mb-4">
Let&apos;s Talk About Your IT Needs
</h2> </h2>
<p className="text-muted-foreground mb-8 max-w-2xl mx-auto">
Running an international business in Japan comes with unique challenges. We&apos;ve been {/* Description */}
helping foreign companies navigate Japanese IT for over 20 years. Let&apos;s discuss how <p className={`${textColor} opacity-80 text-sm leading-relaxed mb-8 max-w-[90%]`}>
we can support your operations. {description}
</p> </p>
<Button as="a" href="/contact" size="lg">
Get in Touch {/* Spacer */}
</Button> <div className="flex-grow" />
{/* CTA */}
<Link
href={href}
className={`inline-flex items-center gap-2 text-sm font-medium ${textColor} opacity-90 hover:opacity-100 transition-opacity group/link`}
>
Contact Us
<svg
className="w-4 h-4 transition-transform group-hover/link:translate-x-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</Link>
</div>
</div>
);
}
export default function BusinessSolutionsPage() {
const services: ServiceCardProps[] = [
{
category: "Infrastructure",
title: "Office LAN Setup",
description:
"Complete network infrastructure from planning to installation. Routers, switches, and firewalls configured by bilingual technicians.",
bgColor: "bg-[#1E3A5F]",
textColor: "text-white",
accentColor: "text-blue-300",
illustration: <NetworkNodesIllustration className="w-full h-full" />,
href: "/contact",
},
{
category: "Support",
title: "Onsite & Remote Tech Support",
description:
"Fast response from English-speaking technicians. Network issues, hardware problems, software setup — we keep you running.",
bgColor: "bg-white",
textColor: "text-gray-900",
accentColor: "text-gray-400",
illustration: <GearIllustration className="w-full h-full" />,
href: "/contact",
},
{
category: "Connectivity",
title: "Dedicated Internet Access",
description:
"Enterprise-grade connectivity with SLA guarantees. Reliable, high-capacity connections with English contracts and support.",
bgColor: "bg-[#F5F0E8]",
textColor: "text-gray-900",
accentColor: "text-amber-700",
illustration: <ShieldIllustration className="w-full h-full" />,
href: "/contact",
},
{
category: "Hosting",
title: "Data Center Service",
description:
"World-class Tokyo data centers including Equinix and GDC Gotenyama. Colocation and managed services with full English support.",
bgColor: "bg-white",
textColor: "text-gray-900",
accentColor: "text-gray-400",
illustration: <ServerIllustration className="w-full h-full" />,
href: "/contact",
},
{
category: "Digital",
title: "Web Construction & Maintenance",
description:
"Modern, responsive websites with bilingual support. From initial design to ongoing maintenance — your complete web partner.",
bgColor: "bg-[#1E3A5F]",
textColor: "text-white",
accentColor: "text-blue-300",
illustration: <CodeIllustration className="w-full h-full" />,
href: "/contact",
},
];
return (
<div className="min-h-screen">
{/* Hero Section */}
<div className="bg-gradient-to-b from-muted/30 to-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-12 pb-16">
<div className="max-w-3xl">
<p className="text-sm font-semibold tracking-widest uppercase text-primary mb-4">
Enterprise Solutions
</p>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-foreground mb-6 leading-[1.1] tracking-tight">
IT for International
<br />
<span className="text-primary">Businesses in Japan</span>
</h1>
<p className="text-lg text-muted-foreground leading-relaxed max-w-2xl">
Running an international company in Japan? We provide enterprise IT with bilingual
support, so your team can focus on business not navigating Japanese tech providers.
</p>
</div>
</div>
</div>
{/* Services Card Grid */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-16">
{/* Desktop: Horizontal scroll layout */}
<div className="hidden lg:block">
<div className="grid grid-cols-5 gap-4">
{services.map((service, index) => (
<ServiceCard key={index} {...service} />
))}
</div>
</div>
{/* Tablet: 2-column grid */}
<div className="hidden md:grid lg:hidden grid-cols-2 gap-4">
{services.map((service, index) => (
<ServiceCard key={index} {...service} />
))}
</div>
{/* Mobile: Single column */}
<div className="md:hidden flex flex-col gap-4">
{services.map((service, index) => (
<ServiceCard key={index} {...service} />
))}
</div>
</div>
{/* Bottom CTA Section */}
<div className="bg-muted/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="flex flex-col lg:flex-row items-center justify-between gap-8">
<div className="text-center lg:text-left">
<h2 className="text-2xl sm:text-3xl font-bold text-foreground mb-3">
Let&apos;s Talk About Your IT Needs
</h2>
<p className="text-muted-foreground max-w-xl">
We&apos;ve been helping foreign companies navigate Japanese IT for over 20 years.
Let&apos;s discuss how we can support your operations.
</p>
</div>
<Button as="a" href="/contact" size="lg" className="whitespace-nowrap px-8">
Get in Touch
</Button>
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,146 +1,427 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link";
import { Button } from "@/components/atoms"; import { Button } from "@/components/atoms";
import { Users, Monitor, Tv, Headset, ChevronDown } from "lucide-react"; import {
Monitor,
Tv,
Headset,
ChevronDown,
Wrench,
Wifi,
Printer,
HardDrive,
CheckCircle2,
Calendar,
UserCheck,
ArrowRight,
Clock,
Shield,
} from "lucide-react";
// Service types for the pricing cards
const services = [
{
id: "onsite",
icon: Monitor,
title: "Onsite Support",
subtitle: "Home & Office Visits",
description:
"We come to your location for hands-on help with networks, computers, and devices.",
price: "15,000",
features: [
"Home or office visit",
"Network setup & troubleshooting",
"Device configuration",
"Same-week scheduling",
],
},
{
id: "remote",
icon: Headset,
title: "Remote Support",
subtitle: "Quick Online Help",
description: "We connect securely to your device to diagnose and fix issues remotely.",
price: "5,000",
popular: true,
features: [
"Secure remote connection",
"Quick turnaround",
"Software troubleshooting",
"No waiting for visit",
],
},
{
id: "tv",
icon: Tv,
title: "TV & Streaming Setup",
subtitle: "Entertainment Systems",
description: "Setup and configuration of smart TVs, streaming devices, and home entertainment.",
price: "15,000",
features: [
"Smart TV setup",
"Streaming device config",
"Apple TV, Fire TV, etc.",
"Content access help",
],
},
];
// What we help with
const helpItems = [
{ icon: Wifi, label: "Wi-Fi & Routers" },
{ icon: Monitor, label: "Computers" },
{ icon: Printer, label: "Printers" },
{ icon: Tv, label: "Smart TVs" },
{ icon: HardDrive, label: "Storage & Backup" },
{ icon: Wrench, label: "General Tech" },
];
// How it works steps
const steps = [
{
number: "1",
icon: Calendar,
title: "Book a Visit",
description:
"Contact us to schedule a time that works for you. Same-week appointments available.",
},
{
number: "2",
icon: UserCheck,
title: "We Come to You",
description:
"An English-speaking technician arrives at your home or office at the scheduled time.",
},
{
number: "3",
icon: CheckCircle2,
title: "Problem Solved",
description: "We fix the issue and explain everything clearly so you understand what was done.",
},
];
export function OnsiteSupportContent() { export function OnsiteSupportContent() {
return ( return (
<div className="max-w-6xl mx-auto px-4"> <div className="relative">
{/* Header */} {/* Hero Section */}
<div className="text-center mb-16 pt-8"> <section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 overflow-hidden bg-gradient-to-br from-slate-50 via-white to-sky-50/80 pt-10 pb-20">
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-6 tracking-tight"> {/* Dot grid pattern */}
Tech Help in English, At Your Door <div
</h1> className="absolute inset-0 pointer-events-none"
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed"> style={{
Need help with your router, computer, or home network? Our English-speaking technicians backgroundImage: `radial-gradient(circle at center, oklch(0.65 0.05 234.4 / 0.12) 1px, transparent 1px)`,
come to your home or office to solve tech problems, explained in a language you backgroundSize: "24px 24px",
understand. }}
</p> />
</div>
{/* Main Services */} {/* Gradient orb accents */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20"> <div
<div className="space-y-6"> className="absolute -top-32 -right-32 w-96 h-96 rounded-full pointer-events-none opacity-50"
<h2 className="text-3xl font-bold text-foreground">We Come to You</h2> style={{
<p className="text-muted-foreground leading-relaxed"> background: "radial-gradient(circle, oklch(0.85 0.08 200 / 0.4) 0%, transparent 70%)",
Living in Japan without strong Japanese skills can make tech problems frustrating. }}
That&apos;s where we come in. Our English-speaking technicians visit your home or office />
to help with setup, troubleshooting, and configuration. <div
className="absolute -bottom-32 -left-32 w-80 h-80 rounded-full pointer-events-none opacity-40"
style={{
background: "radial-gradient(circle, oklch(0.85 0.06 80 / 0.3) 0%, transparent 70%)",
}}
/>
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Eyebrow badge */}
<div className="flex justify-center mb-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<span className="inline-flex items-center gap-2 rounded-full bg-white/80 backdrop-blur-sm border border-primary/20 px-4 py-2 text-sm text-primary font-medium shadow-sm">
<UserCheck className="h-4 w-4" />
English-Speaking Technicians
</span>
</div>
{/* Main heading */}
<h1
className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-center text-foreground mb-6 tracking-tight animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "100ms" }}
>
Tech Help in English,
<br />
<span className="text-primary">At Your Door</span>
</h1>
{/* Description */}
<p
className="text-lg sm:text-xl text-muted-foreground text-center max-w-2xl mx-auto mb-10 leading-relaxed animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "200ms" }}
>
Need help with your router, computer, or home network? Our technicians visit your home
or office to solve tech problems explained in a language you understand.
</p> </p>
<p className="text-muted-foreground leading-relaxed">
For quick fixes, we also offer remote support. We connect to your device securely over {/* What we help with pills */}
the internet to diagnose and resolve issues without a home visit. <div
</p> className="flex flex-wrap justify-center gap-3 mb-10 animate-in fade-in slide-in-from-bottom-4 duration-500"
<div className="pt-4"> style={{ animationDelay: "300ms" }}
<Button as="a" href="/contact" size="lg"> >
Request Support {helpItems.map(item => {
const Icon = item.icon;
return (
<div
key={item.label}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/70 backdrop-blur-sm border border-border/40 shadow-sm"
>
<Icon className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-foreground">{item.label}</span>
</div>
);
})}
</div>
{/* CTA button */}
<div
className="flex justify-center animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "400ms" }}
>
<Button as="a" href="/contact" size="lg" rightIcon={<ArrowRight className="h-4 w-4" />}>
Get in Touch
</Button> </Button>
</div> </div>
</div> </div>
<div className="relative h-[300px] bg-muted/30 rounded-2xl overflow-hidden flex items-center justify-center">
<Users className="h-32 w-32 text-muted-foreground/20" />
</div>
</div>
{/* Pricing Cards */} {/* Gradient fade to next section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-20"> <div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-white pointer-events-none" />
{/* Onsite Network & Computer Support */} </section>
<div className="bg-card rounded-2xl border border-border/60 p-6 shadow-sm flex flex-col">
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-4"> {/* How It Works Section */}
<Monitor className="h-6 w-6" /> <section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-16">
</div> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<h3 className="text-lg font-bold text-foreground mb-2"> <h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
Onsite Network & Computer Support How It Works
</h3> </h2>
<div className="mt-auto pt-4"> <p className="text-muted-foreground text-center max-w-xl mx-auto mb-12">
<div className="text-sm text-muted-foreground mb-1">Basic Service Fee</div> Getting tech help shouldn&apos;t be complicated. Here&apos;s our simple process.
<div className="text-3xl font-bold text-foreground">15,000 JPY</div> </p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative">
{/* Connecting line (desktop only) */}
<div className="hidden md:block absolute top-16 left-[20%] right-[20%] h-0.5 bg-border" />
{steps.map((step, index) => {
const Icon = step.icon;
return (
<div
key={step.number}
className="relative text-center animate-in fade-in slide-in-from-bottom-8 duration-700"
style={{ animationDelay: `${index * 150}ms` }}
>
{/* Step number circle */}
<div className="relative inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary text-xl font-bold mb-5">
<Icon className="h-7 w-7" />
<span className="absolute -top-1 -right-1 w-6 h-6 rounded-full bg-white text-primary text-xs font-bold flex items-center justify-center shadow-sm border border-border">
{step.number}
</span>
</div>
<h3 className="text-lg font-bold text-foreground mb-2">{step.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed max-w-xs mx-auto">
{step.description}
</p>
</div>
);
})}
</div> </div>
</div> </div>
{/* Remote Support */} {/* Gradient fade to pricing section */}
<div className="bg-card rounded-2xl border border-border/60 p-6 shadow-sm flex flex-col"> <div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-[#f7f7f7] pointer-events-none" />
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-4"> </section>
<Headset className="h-6 w-6" />
</div> {/* Pricing Cards Section */}
<h3 className="text-lg font-bold text-foreground mb-2"> <section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-16">
Remote Network & Computer Support {/* Subtle pattern overlay */}
</h3> <div
<div className="mt-auto pt-4"> className="absolute inset-0 pointer-events-none opacity-30"
<div className="text-sm text-muted-foreground mb-1">Basic Service Fee</div> style={{
<div className="text-3xl font-bold text-foreground">5,000 JPY</div> backgroundImage: `radial-gradient(circle at center, oklch(0.6 0.0 0 / 0.08) 1px, transparent 1px)`,
backgroundSize: "32px 32px",
}}
/>
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
Support Options
</h2>
<p className="text-muted-foreground text-center max-w-xl mx-auto mb-12">
Choose the support type that fits your needs. All services include full English
communication.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{services.map((service, index) => {
const Icon = service.icon;
return (
<div
key={service.id}
className="group relative overflow-hidden rounded-2xl bg-white border border-border/60 shadow-md p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg animate-in fade-in slide-in-from-bottom-8 duration-700 flex flex-col"
style={{ animationDelay: `${index * 100}ms` }}
>
{/* Popular badge */}
{service.popular && (
<div className="absolute top-4 right-4">
<span className="inline-flex items-center rounded-full bg-primary px-3 py-1 text-xs font-bold text-white uppercase tracking-wide shadow-sm">
Most Popular
</span>
</div>
)}
{/* Icon */}
<div className="w-14 h-14 rounded-2xl bg-primary/10 flex items-center justify-center mb-5 transition-transform duration-300 group-hover:scale-105">
<Icon className="h-7 w-7 text-primary" />
</div>
{/* Subtitle */}
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1">
{service.subtitle}
</span>
{/* Title */}
<h3 className="text-xl font-bold text-foreground mb-2">{service.title}</h3>
{/* Description */}
<p className="text-sm text-muted-foreground leading-relaxed mb-4">
{service.description}
</p>
{/* Price */}
<div className="mb-5">
<span className="text-3xl font-bold text-foreground">{service.price}</span>
<span className="text-sm text-muted-foreground ml-1">JPY</span>
<span className="text-xs text-muted-foreground block mt-0.5">
Basic service fee
</span>
</div>
{/* Features */}
<div className="space-y-2 mb-6 flex-grow">
{service.features.map(feature => (
<div
key={feature}
className="flex items-center gap-2 text-sm text-foreground"
>
<CheckCircle2 className="h-4 w-4 text-primary flex-shrink-0" />
<span>{feature}</span>
</div>
))}
</div>
{/* CTA */}
<Link
href="/contact"
className={`inline-flex items-center justify-center gap-2 rounded-full ${service.popular ? "bg-primary text-white hover:bg-primary/90" : "bg-muted text-foreground hover:bg-muted/80"} px-5 py-2.5 font-semibold transition-colors text-sm`}
>
Request This Service
<ArrowRight className="h-4 w-4" />
</Link>
</div>
);
})}
</div> </div>
</div> </div>
{/* Onsite TV Support */} {/* Gradient fade to FAQ section */}
<div className="bg-card rounded-2xl border border-border/60 p-6 shadow-sm flex flex-col"> <div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-white pointer-events-none" />
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-4"> </section>
<Tv className="h-6 w-6" />
</div>
<h3 className="text-lg font-bold text-foreground mb-2">Onsite TV Support Service</h3>
<div className="mt-auto pt-4">
<div className="text-sm text-muted-foreground mb-1">Basic Service Fee</div>
<div className="text-3xl font-bold text-foreground">15,000 JPY</div>
</div>
</div>
</div>
{/* FAQ Section */} {/* FAQ Section */}
<div className="mb-12"> <section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-16">
<h2 className="text-2xl font-bold text-foreground mb-6 text-center"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
Frequently Asked Questions <h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
</h2> Frequently Asked Questions
<div className="space-y-4 max-w-3xl mx-auto"> </h2>
<FaqItem <p className="text-muted-foreground text-center max-w-xl mx-auto mb-10">
question="My home requires multiple Wi-Fi routers. Would you be able to assist with this?" Common questions about our onsite support services.
answer={ </p>
<>
Yes, the Assist Solutions technical team is able to visit your residence for device
set up including Wi-Fi routers, printers, Apple TVs etc.
<br />
<br />
Our tech consulting team will be able to make suggestions based on your residence
layout and requirements. Please contact us at info@asolutions.co.jp for a free
consultation.
</>
}
/>
<FaqItem
question="I am already subscribed to a different Internet provider but require more Wi-Fi coverage. Would I be able to just opt for the Onsite Support service without switching over my entire home Internet service?"
answer="Yes, we are able to offer the Onsite Support service as a standalone service."
/>
<FaqItem
question="Do you offer this service outside of Tokyo?"
answer={
<>
Our In-Home Technical Assistance service can be provided in Tokyo, Saitama and
Kanagawa prefecture.
<br />
<br />
*Please note that this service may not available in some areas within the above
prefectures.
<br />
For more information, please contact us at info@asolutions.co.jp
</>
}
/>
</div>
</div>
{/* CTA */} <div className="space-y-4">
<div className="text-center py-12 bg-muted/20 rounded-3xl"> <FaqItem
<h2 className="text-2xl font-bold text-foreground mb-4"> question="My home requires multiple Wi-Fi routers. Would you be able to assist with this?"
Tech Problems? We Speak Your Language. answer={
</h2> <>
<p className="text-muted-foreground mb-6 max-w-xl mx-auto"> Yes, our technical team can visit your residence for device setup including Wi-Fi
Don&apos;t struggle with Japanese-only support lines. Get help from technicians who routers, mesh systems, printers, Apple TVs, and more.
explain things clearly in English. <br />
</p> <br />
<Button as="a" href="/contact" size="lg"> We&apos;ll assess your residence layout and recommend the best solution for
Request Support complete coverage. Contact us at info@asolutions.co.jp for a free consultation.
</Button> </>
</div> }
/>
<FaqItem
question="I use a different Internet provider. Can I still use your onsite support?"
answer="Absolutely. Our onsite support is available as a standalone service regardless of your internet provider. We help with any networking, computer, or device issues."
/>
<FaqItem
question="How quickly can you come to my location?"
answer={
<>
We typically schedule visits within the same week. For urgent issues, we do our
best to accommodate faster appointments based on availability.
<br />
<br />
Remote support is often available within 1-2 business days if you need faster
help.
</>
}
/>
<FaqItem
question="What if the problem can't be fixed in one visit?"
answer="If additional work is needed, we'll explain what's required and provide a clear quote for any follow-up visits. We don't charge extra for the same issue if it takes more time than expected during a single visit."
/>
</div>
</div>
{/* Gradient fade to CTA section */}
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-[#f7f7f7] pointer-events-none" />
</section>
{/* CTA Section */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2 text-sm text-primary font-medium mb-6">
<Shield className="h-4 w-4" />
Trusted by expats across Japan
</div>
<h2 className="text-2xl sm:text-3xl font-bold text-foreground mb-4">
Tech Problems? We Speak Your Language.
</h2>
<p className="text-muted-foreground mb-8 max-w-lg mx-auto leading-relaxed">
Don&apos;t struggle with Japanese-only support lines. Get help from technicians who
explain things clearly in English.
</p>
<div className="flex justify-center">
<Button as="a" href="/contact" size="lg" rightIcon={<ArrowRight className="h-4 w-4" />}>
Get in Touch
</Button>
</div>
{/* Trust indicators */}
<div className="flex flex-wrap items-center justify-center gap-6 mt-10 pt-8 border-t border-border/60">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4 text-primary" />
Same-week appointments
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<UserCheck className="h-4 w-4 text-primary" />
English-speaking technicians
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Shield className="h-4 w-4 text-primary" />
20+ years in Japan
</div>
</div>
</div>
</section>
</div> </div>
); );
} }
@ -152,20 +433,30 @@ function FaqItem({ question, answer }: { question: string; answer: React.ReactNo
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
return ( return (
<div className="border border-border rounded-xl overflow-hidden bg-card"> <div
className={`border rounded-xl overflow-hidden bg-white shadow-sm transition-all duration-200 ${isOpen ? "border-primary/30 shadow-md" : "border-border/60"}`}
>
<button <button
type="button" type="button"
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-start justify-between gap-3 p-4 text-left hover:bg-muted/50 transition-colors" className="w-full flex items-start justify-between gap-4 p-5 text-left hover:bg-muted/30 transition-colors"
> >
<span className="font-medium text-foreground">{question}</span> <span className="font-semibold text-foreground">{question}</span>
<ChevronDown <div
className={`w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`} className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 transition-colors ${isOpen ? "bg-primary text-white" : "bg-muted text-muted-foreground"}`}
/> >
<ChevronDown
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
/>
</div>
</button> </button>
{isOpen && ( <div
<div className="px-4 pb-4 text-sm text-muted-foreground leading-relaxed">{answer}</div> className={`overflow-hidden transition-all duration-200 ${isOpen ? "max-h-96" : "max-h-0"}`}
)} >
<div className="px-5 pb-5 text-sm text-muted-foreground leading-relaxed border-t border-border/40 pt-4">
{answer}
</div>
</div>
</div> </div>
); );
} }

View File

@ -1,3 +1,5 @@
"use client";
import Link from "next/link"; import Link from "next/link";
import { import {
Wifi, Wifi,
@ -10,10 +12,9 @@ import {
Headphones, Headphones,
Building2, Building2,
Wrench, Wrench,
Tv, Zap,
Check,
} from "lucide-react"; } from "lucide-react";
import { ServiceCard } from "@/components/molecules/ServiceCard";
import { ServicesHero } from "@/features/services/components/base/ServicesHero";
interface ServicesOverviewContentProps { interface ServicesOverviewContentProps {
/** Base path for service links ("/services" or "/account/services") */ /** Base path for service links ("/services" or "/account/services") */
@ -24,11 +25,74 @@ interface ServicesOverviewContentProps {
showCta?: boolean; showCta?: boolean;
} }
// Service data with enhanced information
const services = [
{
id: "internet",
icon: Wifi,
title: "Internet",
subtitle: "Fiber Optic",
description:
"NTT Optical Fiber for homes and apartments. Speeds up to 10Gbps with professional installation.",
price: "¥3,200",
priceUnit: "/mo",
features: ["Up to 10Gbps", "NTT Network", "Pro Install"],
useBasePath: true,
},
{
id: "sim",
icon: Smartphone,
title: "SIM & eSIM",
subtitle: "Mobile Data",
description:
"Data, voice & SMS on NTT Docomo network. Physical SIM or instant eSIM activation.",
price: "¥1,100",
priceUnit: "/mo",
badge: "1st month free",
features: ["Docomo Network", "Voice + Data", "eSIM Ready"],
useBasePath: true,
},
{
id: "vpn",
icon: ShieldCheck,
title: "VPN Router",
subtitle: "Streaming Access",
description:
"Access US & UK streaming content with a pre-configured router. Simple plug-and-play.",
price: "¥2,500",
priceUnit: "/mo",
features: ["US/UK Content", "Pre-configured", "Plug & Play"],
useBasePath: true,
},
{
id: "business",
icon: Building2,
title: "Business",
subtitle: "Enterprise IT",
description:
"Enterprise solutions for offices and commercial spaces. Dedicated support and SLAs.",
features: ["Office Setup", "Dedicated Support", "Custom SLAs"],
useBasePath: false,
fixedPath: "/services/business",
},
{
id: "onsite",
icon: Wrench,
title: "Onsite Support",
subtitle: "Tech Assistance",
description:
"Professional technicians visit your location for setup, troubleshooting, and maintenance.",
features: ["Home Visits", "Setup Help", "Troubleshooting"],
useBasePath: false,
fixedPath: "/services/onsite",
},
];
/** /**
* ServicesOverviewContent - Shared content component for services overview pages. * ServicesOverviewContent - Enhanced services overview with rich visual design.
* *
* Used by both PublicServicesOverview and AccountServicesOverview to ensure * Features full-width sections, gradient backgrounds, and polished card treatments
* consistent design across public and authenticated service listing pages. * matching the website's landing page aesthetic.
*/ */
export function ServicesOverviewContent({ export function ServicesOverviewContent({
basePath, basePath,
@ -36,128 +100,258 @@ export function ServicesOverviewContent({
showCta = true, showCta = true,
}: ServicesOverviewContentProps) { }: ServicesOverviewContentProps) {
return ( return (
<div className="space-y-12 pb-16"> <div className="relative">
{/* Hero */} {/* Hero Section with gradient background */}
{showHero && ( {showHero && (
<> <section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 overflow-hidden bg-gradient-to-br from-slate-50 via-white to-sky-50/80 pt-8 pb-16">
<ServicesHero {/* Dot grid pattern */}
title="Our Services" <div
description="Connectivity and support solutions for Japan's international community." className="absolute inset-0 pointer-events-none"
eyebrow={ style={{
<span className="inline-flex items-center gap-2 rounded-full bg-primary/8 border border-primary/15 px-4 py-2 text-sm text-primary font-medium normal-case tracking-normal"> backgroundImage: `radial-gradient(circle at center, oklch(0.65 0.05 234.4 / 0.12) 1px, transparent 1px)`,
backgroundSize: "24px 24px",
}}
/>
{/* Gradient orb accent */}
<div
className="absolute -top-32 -right-32 w-96 h-96 rounded-full pointer-events-none opacity-60"
style={{
background: "radial-gradient(circle, oklch(0.85 0.08 200 / 0.4) 0%, transparent 70%)",
}}
/>
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Eyebrow badge */}
<div className="flex justify-center mb-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<span className="inline-flex items-center gap-2 rounded-full bg-white/80 backdrop-blur-sm border border-primary/20 px-4 py-2 text-sm text-primary font-medium shadow-sm">
<CheckCircle2 className="h-4 w-4" /> <CheckCircle2 className="h-4 w-4" />
Full English Support Full English Support
</span> </span>
} </div>
animated
/>
{/* Value Props - Compact */} {/* Main heading */}
<section <h1
className="flex flex-wrap justify-center gap-6 text-sm animate-in fade-in slide-in-from-bottom-8 duration-700" className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-center text-foreground mb-6 tracking-tight animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "300ms" }} style={{ animationDelay: "100ms" }}
> >
<div className="flex items-center gap-2 text-muted-foreground"> Our Services
<Globe className="h-4 w-4 text-primary" /> </h1>
<span>One provider, all services</span>
{/* Description */}
<p
className="text-lg sm:text-xl text-muted-foreground text-center max-w-2xl mx-auto mb-10 leading-relaxed animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "200ms" }}
>
Connectivity and support solutions designed for Japan&apos;s international community.
One provider for all your needs.
</p>
{/* Value propositions */}
<div
className="flex flex-wrap justify-center gap-4 sm:gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "300ms" }}
>
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-border/40">
<Globe className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-foreground">
One provider, all services
</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-border/40">
<Headphones className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-foreground">English support</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-border/40">
<Zap className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-foreground">Fast activation</span>
</div>
</div> </div>
<div className="flex items-center gap-2 text-muted-foreground"> </div>
<Headphones className="h-4 w-4 text-success" />
<span>English support</span> {/* Gradient fade to services section */}
</div> <div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-[#f7f7f7] pointer-events-none" />
<div className="flex items-center gap-2 text-muted-foreground"> </section>
<CheckCircle2 className="h-4 w-4 text-info" />
<span>No hidden fees</span>
</div>
</section>
</>
)} )}
{/* All Services - Clean Grid with staggered animations */} {/* Services Section */}
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 cp-stagger-children"> <section
<ServiceCard className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] ${showHero ? "pt-8" : "pt-12"} pb-16`}
href={`${basePath}/internet`} >
icon={<Wifi className="h-6 w-6" />} {/* Subtle pattern overlay */}
title="Internet" <div
description="NTT Optical Fiber for homes and apartments. Speeds up to 10Gbps with professional installation." className="absolute inset-0 pointer-events-none opacity-30"
price="¥3,200/mo" style={{
accentColor="blue" backgroundImage: `radial-gradient(circle at center, oklch(0.6 0.0 0 / 0.08) 1px, transparent 1px)`,
backgroundSize: "32px 32px",
}}
/> />
<ServiceCard <div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
href={`${basePath}/sim`} {/* Section header */}
icon={<Smartphone className="h-6 w-6" />} <div className="flex items-center justify-between mb-8">
title="SIM & eSIM" <h2 className="text-2xl sm:text-3xl font-bold text-foreground">Choose Your Service</h2>
description="Data, voice & SMS on NTT Docomo network. Physical SIM or instant eSIM activation." <div className="hidden sm:flex items-center gap-2 text-sm text-muted-foreground">
price="¥1,100/mo" <CheckCircle2 className="h-4 w-4 text-primary" />
badge="1st month free" <span>No hidden fees</span>
accentColor="green" </div>
/> </div>
<ServiceCard {/* Featured services - Top row (Internet & SIM) */}
href={`${basePath}/vpn`} <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
icon={<ShieldCheck className="h-6 w-6" />} {services.slice(0, 2).map((service, index) => {
title="VPN Router" const Icon = service.icon;
description="Access US & UK streaming content with a pre-configured router. Simple plug-and-play." const href = service.useBasePath ? `${basePath}/${service.id}` : service.fixedPath;
price="¥2,500/mo"
accentColor="purple"
/>
{/* Business, Onsite, and TV services only have public pages (no account-specific routes) */} return (
<ServiceCard <Link
href="/services/business" key={service.id}
icon={<Building2 className="h-6 w-6" />} href={href!}
title="Business" className="group relative overflow-hidden rounded-2xl bg-white border border-border/60 shadow-md p-6 sm:p-8 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg animate-in fade-in slide-in-from-bottom-8 duration-700"
description="Enterprise solutions for offices and commercial spaces. Dedicated support and SLAs." style={{ animationDelay: `${index * 100}ms` }}
accentColor="orange" >
/> {/* Badge */}
{service.badge && (
<div className="absolute top-4 right-4 sm:top-6 sm:right-6">
<span className="inline-flex items-center rounded-full bg-primary px-3 py-1 text-xs font-bold text-white uppercase tracking-wide shadow-sm">
{service.badge}
</span>
</div>
)}
<ServiceCard {/* Icon */}
href="/services/onsite" <div className="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl bg-primary/10 flex items-center justify-center mb-5 transition-transform duration-300 group-hover:scale-105">
icon={<Wrench className="h-6 w-6" />} <Icon className="h-7 w-7 sm:h-8 sm:w-8 text-primary" />
title="Onsite Support" </div>
description="Professional technicians visit your location for setup, troubleshooting, and maintenance."
accentColor="cyan"
/>
<ServiceCard {/* Subtitle */}
href="/services/tv" <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1 block">
icon={<Tv className="h-6 w-6" />} {service.subtitle}
title="TV" </span>
description="Streaming TV packages with international channels. Watch content from home countries."
accentColor="pink" {/* Title */}
/> <h3 className="text-xl sm:text-2xl font-bold text-foreground mb-3">
{service.title}
</h3>
{/* Description */}
<p className="text-muted-foreground leading-relaxed mb-5">
{service.description}
</p>
{/* Features */}
<div className="flex flex-wrap gap-2 mb-5">
{service.features.map(feature => (
<span
key={feature}
className="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1.5 text-xs font-medium text-foreground"
>
<Check className="h-3 w-3 text-primary" />
{feature}
</span>
))}
</div>
{/* CTA */}
<div className="flex items-center gap-2 text-primary font-semibold group-hover:gap-3 transition-all duration-300">
<span>View Plans</span>
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</div>
</Link>
);
})}
</div>
{/* Secondary services - Bottom row */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{services.slice(2).map((service, index) => {
const Icon = service.icon;
const href = service.useBasePath ? `${basePath}/${service.id}` : service.fixedPath;
return (
<Link
key={service.id}
href={href!}
className="group relative overflow-hidden rounded-2xl bg-white border border-border/60 shadow-sm p-5 sm:p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-md animate-in fade-in slide-in-from-bottom-8 duration-700"
style={{ animationDelay: `${(index + 2) * 100}ms` }}
>
<div className="flex items-start gap-4">
{/* Icon */}
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center flex-shrink-0 transition-transform duration-300 group-hover:scale-105">
<Icon className="h-6 w-6 text-primary" />
</div>
<div className="flex-1 min-w-0">
{/* Subtitle */}
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-0.5 block">
{service.subtitle}
</span>
{/* Title */}
<h3 className="text-lg font-bold text-foreground mb-2">{service.title}</h3>
{/* Description */}
<p className="text-sm text-muted-foreground leading-relaxed mb-3 line-clamp-2">
{service.description}
</p>
{/* Features as pills */}
<div className="flex flex-wrap gap-1.5">
{service.features.slice(0, 2).map(feature => (
<span
key={feature}
className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-foreground"
>
{feature}
</span>
))}
</div>
</div>
</div>
{/* Hover arrow indicator */}
<div className="absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ArrowRight className="h-4 w-4 text-primary" />
</div>
</Link>
);
})}
</div>
</div>
{/* Gradient fade to CTA section */}
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-white pointer-events-none" />
</section> </section>
{/* CTA */} {/* CTA Section */}
{showCta && ( {showCta && (
<section <section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-12 sm:py-16">
className="rounded-2xl bg-gradient-to-br from-muted/50 to-muted/80 p-8 text-center animate-in fade-in slide-in-from-bottom-8 duration-700" <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
style={{ animationDelay: "500ms" }} <h2 className="text-2xl sm:text-3xl font-bold text-foreground mb-4">
> Need help choosing?
<h2 className="text-xl font-bold text-foreground font-display mb-3"> </h2>
Need help choosing? <p className="text-muted-foreground mb-8 max-w-lg mx-auto leading-relaxed">
</h2> Our bilingual team is ready to help you find the perfect solution for your needs. Get
<p className="text-muted-foreground mb-6 max-w-md mx-auto"> personalized recommendations in English.
Our bilingual team can help you find the right solution. </p>
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-3"> <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link <Link
href="/contact" href="/contact"
className="inline-flex items-center gap-2 rounded-lg bg-primary px-5 py-2.5 font-medium text-primary-foreground hover:bg-primary-hover transition-colors" className="inline-flex items-center gap-2 rounded-full bg-primary px-6 py-3 font-semibold text-white hover:bg-primary/90 transition-colors shadow-md"
> >
Contact Us Get in Touch
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
</Link> </Link>
<a <a
href="tel:0120660470" href="tel:0120660470"
className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors" className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
aria-label="Call us toll free at 0120-660-470" aria-label="Call us toll free at 0120-660-470"
> >
<Phone className="h-4 w-4" aria-hidden="true" /> <Phone className="h-4 w-4" aria-hidden="true" />
0120-660-470 (Toll Free) 0120-660-470 (Toll Free)
</a> </a>
</div>
</div> </div>
</section> </section>
)} )}

View File

@ -408,7 +408,7 @@ export function CancellationFlow({
<div className="fixed inset-0 z-50 overflow-y-auto"> <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="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="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="relative z-10 inline-block align-bottom bg-card rounded-lg text-left overflow-hidden shadow-[var(--cp-shadow-3)] border border-border 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="bg-card px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start"> <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"> <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">

View File

@ -64,7 +64,7 @@ export function ChangePlanModal({
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203; &#8203;
</span> </span>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> <div className="relative z-10 inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start"> <div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full"> <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">

View File

@ -217,7 +217,7 @@ function CancelConfirmModal({
<div className="fixed inset-0 z-50 overflow-y-auto"> <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="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="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="relative z-10 inline-block align-bottom bg-card rounded-lg text-left overflow-hidden shadow-[var(--cp-shadow-3)] border border-border 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="bg-card px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start"> <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"> <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">

View File

@ -319,40 +319,48 @@ export function SimDetailsCard({
</p> </p>
</div> </div>
<div className="flex items-center space-x-4"> {simDetails.hasVoice === false ? (
<div className="flex items-center"> <p className="text-sm text-muted-foreground">
<SignalIcon Data-only plan (no voice features)
className={`h-4 w-4 mr-1 ${simDetails.voiceMailEnabled ? "text-success" : "text-muted-foreground"}`} </p>
/> ) : (
<span <>
className={`text-sm ${simDetails.voiceMailEnabled ? "text-success" : "text-muted-foreground"}`} <div className="flex items-center space-x-4">
> <div className="flex items-center">
Voicemail {simDetails.voiceMailEnabled ? "Enabled" : "Disabled"} <SignalIcon
</span> className={`h-4 w-4 mr-1 ${simDetails.voiceMailEnabled ? "text-success" : "text-muted-foreground"}`}
</div> />
<div className="flex items-center"> <span
<DevicePhoneMobileIcon className={`text-sm ${simDetails.voiceMailEnabled ? "text-success" : "text-muted-foreground"}`}
className={`h-4 w-4 mr-1 ${simDetails.callWaitingEnabled ? "text-success" : "text-muted-foreground"}`} >
/> Voicemail {simDetails.voiceMailEnabled ? "Enabled" : "Disabled"}
<span </span>
className={`text-sm ${simDetails.callWaitingEnabled ? "text-success" : "text-muted-foreground"}`} </div>
> <div className="flex items-center">
Call Waiting {simDetails.callWaitingEnabled ? "Enabled" : "Disabled"} <DevicePhoneMobileIcon
</span> className={`h-4 w-4 mr-1 ${simDetails.callWaitingEnabled ? "text-success" : "text-muted-foreground"}`}
</div> />
</div> <span
className={`text-sm ${simDetails.callWaitingEnabled ? "text-success" : "text-muted-foreground"}`}
>
Call Waiting {simDetails.callWaitingEnabled ? "Enabled" : "Disabled"}
</span>
</div>
</div>
<div className="flex items-center"> <div className="flex items-center">
<WifiIcon <WifiIcon
className={`h-4 w-4 mr-1 ${simDetails.internationalRoamingEnabled ? "text-success" : "text-muted-foreground"}`} className={`h-4 w-4 mr-1 ${simDetails.internationalRoamingEnabled ? "text-success" : "text-muted-foreground"}`}
/> />
<span <span
className={`text-sm ${simDetails.internationalRoamingEnabled ? "text-success" : "text-muted-foreground"}`} className={`text-sm ${simDetails.internationalRoamingEnabled ? "text-success" : "text-muted-foreground"}`}
> >
Int&apos;l Roaming{" "} Int&apos;l Roaming{" "}
{simDetails.internationalRoamingEnabled ? "Enabled" : "Disabled"} {simDetails.internationalRoamingEnabled ? "Enabled" : "Disabled"}
</span> </span>
</div> </div>
</>
)}
</div> </div>
</div> </div>
)} )}

View File

@ -37,6 +37,8 @@ interface SimDetails {
callWaitingEnabled?: boolean; callWaitingEnabled?: boolean;
internationalRoamingEnabled?: boolean; internationalRoamingEnabled?: boolean;
networkType?: string; networkType?: string;
hasVoice?: boolean;
hasSms?: boolean;
} }
interface SimInfo { interface SimInfo {
@ -304,22 +306,26 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
/> />
</div> </div>
{/* Voice toggles */} {/* Voice & Network toggles */}
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-md font-semibold text-foreground">Voice Status</h4> <h4 className="text-md font-semibold text-foreground">
{simInfo.details.hasVoice === false ? "Network Status" : "Voice Status"}
</h4>
{featureError && ( {featureError && (
<div className="p-3 bg-danger-soft border border-danger/25 rounded-lg text-sm text-danger"> <div className="p-3 bg-danger-soft border border-danger/25 rounded-lg text-sm text-danger">
{featureError} {featureError}
</div> </div>
)} )}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<StatusToggle {simInfo.details.hasVoice !== false && (
label="Voice Mail" <StatusToggle
subtitle={simInfo.details.voiceMailEnabled ? "Enabled" : "Disabled"} label="Voice Mail"
checked={simInfo.details.voiceMailEnabled || false} subtitle={simInfo.details.voiceMailEnabled ? "Enabled" : "Disabled"}
loading={featureLoading.voiceMail ?? false} checked={simInfo.details.voiceMailEnabled || false}
onChange={checked => void updateFeature("voiceMail", checked)} loading={featureLoading.voiceMail ?? false}
/> onChange={checked => void updateFeature("voiceMail", checked)}
/>
)}
<StatusToggle <StatusToggle
label="Network Type" label="Network Type"
subtitle={simInfo.details.networkType === "5G" ? "5G" : "4G"} subtitle={simInfo.details.networkType === "5G" ? "5G" : "4G"}
@ -327,21 +333,30 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
loading={featureLoading.networkType ?? false} loading={featureLoading.networkType ?? false}
onChange={checked => void updateFeature("networkType", checked ? "5G" : "4G")} onChange={checked => void updateFeature("networkType", checked ? "5G" : "4G")}
/> />
<StatusToggle {simInfo.details.hasVoice !== false && (
label="Call Waiting" <StatusToggle
subtitle={simInfo.details.callWaitingEnabled ? "Enabled" : "Disabled"} label="Call Waiting"
checked={simInfo.details.callWaitingEnabled || false} subtitle={simInfo.details.callWaitingEnabled ? "Enabled" : "Disabled"}
loading={featureLoading.callWaiting ?? false} checked={simInfo.details.callWaitingEnabled || false}
onChange={checked => void updateFeature("callWaiting", checked)} loading={featureLoading.callWaiting ?? false}
/> onChange={checked => void updateFeature("callWaiting", checked)}
<StatusToggle />
label="International Roaming" )}
subtitle={simInfo.details.internationalRoamingEnabled ? "Enabled" : "Disabled"} {simInfo.details.hasVoice !== false && (
checked={simInfo.details.internationalRoamingEnabled || false} <StatusToggle
loading={featureLoading.internationalRoaming ?? false} label="International Roaming"
onChange={checked => void updateFeature("internationalRoaming", checked)} subtitle={simInfo.details.internationalRoamingEnabled ? "Enabled" : "Disabled"}
/> checked={simInfo.details.internationalRoamingEnabled || false}
loading={featureLoading.internationalRoaming ?? false}
onChange={checked => void updateFeature("internationalRoaming", checked)}
/>
)}
</div> </div>
{simInfo.details.hasVoice === false && (
<p className="text-xs text-muted-foreground">
Voice features are not available on data-only plans.
</p>
)}
</div> </div>
</div> </div>

View File

@ -76,7 +76,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
<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="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-gray-500 bg-opacity-75 transition-opacity"></div> <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> <div className="relative z-10 inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
{/* Header */} {/* Header */}
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">

View File

@ -61,7 +61,15 @@ export function SimChangePlanContainer() {
newPlanSku: selectedPlan.sku, newPlanSku: selectedPlan.sku,
newPlanName: selectedPlan.name, newPlanName: selectedPlan.name,
}); });
setMessage(`Plan change scheduled for ${result.scheduledAt || "the 1st of next month"}`); const scheduled = result.scheduledAt;
const today = new Date();
const todayStr = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, "0")}${String(today.getDate()).padStart(2, "0")}`;
const isImmediate = scheduled === todayStr;
setMessage(
isImmediate
? `Plan change submitted for immediate processing (${selectedPlan.name})`
: `Plan change scheduled for ${scheduled || "the 1st of next month"}`
);
setSelectedPlan(null); setSelectedPlan(null);
} catch (e: unknown) { } catch (e: unknown) {
setError( setError(
@ -96,8 +104,9 @@ export function SimChangePlanContainer() {
<div className="mb-6"> <div className="mb-6">
<h2 className="text-lg font-semibold text-foreground mb-2">Change Your Plan</h2> <h2 className="text-lg font-semibold text-foreground mb-2">Change Your Plan</h2>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Select a new plan below. Plan changes will take effect on the 1st of the following {process.env.NODE_ENV === "development"
month. Changes must be requested before the 25th of the current month. ? "Select a new plan below. In test mode, plan changes are submitted for immediate processing."
: "Select a new plan below. Plan changes will take effect on the 1st of the following month. Changes must be requested before the 25th of the current month."}
</p> </p>
</div> </div>
@ -205,8 +214,17 @@ export function SimChangePlanContainer() {
<div className="bg-warning-soft border border-warning/25 rounded-lg p-4"> <div className="bg-warning-soft border border-warning/25 rounded-lg p-4">
<h3 className="text-sm font-medium text-foreground mb-1">Important Notes</h3> <h3 className="text-sm font-medium text-foreground mb-1">Important Notes</h3>
<ul className="text-sm text-muted-foreground space-y-1"> <ul className="text-sm text-muted-foreground space-y-1">
<li> Plan changes take effect on the 1st of the following month</li> {process.env.NODE_ENV === "development" ? (
<li> Requests must be made before the 25th of the current month</li> <>
<li> Test mode: plan changes are submitted for immediate processing</li>
<li> Changes may take a few hours to appear on the Freebit dashboard</li>
</>
) : (
<>
<li> Plan changes take effect on the 1st of the following month</li>
<li> Requests must be made before the 25th of the current month</li>
</>
)}
<li> Your current data balance will be reset when the new plan activates</li> <li> Your current data balance will be reset when the new plan activates</li>
</ul> </ul>
</div> </div>