Enhance Service Pages with New FAQ Sections and Content Updates
- Added new FAQ sections to the TV Services and Public Internet Plans pages, improving user assistance and engagement. - Updated the Onsite Support page to utilize a dedicated content component for better organization and maintainability. - Included device compatibility information in the SIM Plans content, enhancing user clarity on service offerings. - Improved the structure and functionality of FAQ items across various service pages for a more interactive user experience.
This commit is contained in:
parent
1f57ca4906
commit
7139e0489d
@ -1 +1,3 @@
|
|||||||
Timestamp,API Endpoint,API Method,Phone Number,SIM Identifier,Request Payload,Response Status,Error,Additional Info
|
Timestamp,API Endpoint,API Method,Phone Number,SIM Identifier,Request Payload,Response Status,Error,Additional Info
|
||||||
|
2026-01-19T04:05:41.856Z,/mvno/getTrafficInfo/,POST,02000524104652,02000524104652,"{""account"":""02000524104652""}",Success,,OK
|
||||||
|
2026-01-19T04:05:41.945Z,/mvno/getTrafficInfo/,POST,02000524104652,02000524104652,"{""account"":""02000524104652""}",Success,,OK
|
||||||
|
|||||||
|
@ -0,0 +1,164 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/atoms";
|
||||||
|
import { Users, Monitor, Tv, Headset, ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
export function OnsiteSupportContent() {
|
||||||
|
return (
|
||||||
|
<div className="max-w-6xl mx-auto px-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-16 pt-8">
|
||||||
|
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-6 tracking-tight">
|
||||||
|
Onsite Support
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
||||||
|
We dispatch our skillful in-house tech staff to your residence or office for your needs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Services */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h2 className="text-3xl font-bold text-foreground">Need Our Technical Support?</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
We can provide you with on-site technical support service. If you would like for our
|
||||||
|
technicians to visit your residence and provide technical assistance, please let us
|
||||||
|
know.
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
We also provide "Remote Access Services" which allows our technicians to do
|
||||||
|
support via Remote Access Software over the Internet connection to fix up the issue
|
||||||
|
(depends on what the issue is).
|
||||||
|
</p>
|
||||||
|
<div className="pt-4">
|
||||||
|
<Button as="a" href="/contact" size="lg">
|
||||||
|
Request Support
|
||||||
|
</Button>
|
||||||
|
</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 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-20">
|
||||||
|
{/* Onsite Network & Computer Support */}
|
||||||
|
<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">
|
||||||
|
<Monitor className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-foreground mb-2">
|
||||||
|
Onsite Network & Computer Support
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* Remote Support */}
|
||||||
|
<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">
|
||||||
|
<Headset className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-foreground mb-2">
|
||||||
|
Remote Network & Computer Support
|
||||||
|
</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">5,000 JPY</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Onsite TV Support */}
|
||||||
|
<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">
|
||||||
|
<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 */}
|
||||||
|
<div className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 max-w-3xl mx-auto">
|
||||||
|
<FaqItem
|
||||||
|
question="My home requires multiple Wi-Fi routers. Would you be able to assist with this?"
|
||||||
|
answer={
|
||||||
|
<>
|
||||||
|
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="text-center py-12 bg-muted/20 rounded-3xl">
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-4">Ready to get started?</h2>
|
||||||
|
<Button as="a" href="/contact" size="lg">
|
||||||
|
Contact Us for Support
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ Item component with expand/collapse functionality
|
||||||
|
*/
|
||||||
|
function FaqItem({ question, answer }: { question: string; answer: React.ReactNode }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-border rounded-xl overflow-hidden bg-card">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="w-full flex items-start justify-between gap-3 p-4 text-left hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="font-medium text-foreground">{question}</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="px-4 pb-4 text-sm text-muted-foreground leading-relaxed">{answer}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Button } from "@/components/atoms";
|
import { OnsiteSupportContent } from "./OnsiteSupportContent";
|
||||||
import { Users, Monitor, Tv, Headset } from "lucide-react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Onsite Support - Tech Support in Tokyo | Assist Solutions",
|
title: "Onsite Support - Tech Support in Tokyo | Assist Solutions",
|
||||||
@ -21,93 +20,5 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function OnsiteSupportPage() {
|
export default function OnsiteSupportPage() {
|
||||||
return (
|
return <OnsiteSupportContent />;
|
||||||
<div className="max-w-6xl mx-auto px-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="text-center mb-16 pt-8">
|
|
||||||
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-6 tracking-tight">
|
|
||||||
Onsite Support
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
|
||||||
We dispatch our skillful in-house tech staff to your residence or office for your needs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Services */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<h2 className="text-3xl font-bold text-foreground">Need Our Technical Support?</h2>
|
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
|
||||||
We can provide you with on-site technical support service. If you would like for our
|
|
||||||
technicians to visit your residence and provide technical assistance, please let us
|
|
||||||
know.
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
|
||||||
We also provide "Remote Access Services" which allows our technicians to do support via
|
|
||||||
Remote Access Software over the Internet connection to fix up the issue (depends on what
|
|
||||||
the issue is).
|
|
||||||
</p>
|
|
||||||
<div className="pt-4">
|
|
||||||
<Button as="a" href="/contact" size="lg">
|
|
||||||
Request Support
|
|
||||||
</Button>
|
|
||||||
</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 */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-20">
|
|
||||||
{/* Onsite Network & Computer Support */}
|
|
||||||
<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">
|
|
||||||
<Monitor className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-bold text-foreground mb-2">
|
|
||||||
Onsite Network & Computer Support
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* Remote Support */}
|
|
||||||
<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">
|
|
||||||
<Headset className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-bold text-foreground mb-2">
|
|
||||||
Remote Network & Computer Support
|
|
||||||
</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">5,000 JPY</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Onsite TV Support */}
|
|
||||||
<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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* CTA */}
|
|
||||||
<div className="text-center py-12 bg-muted/20 rounded-3xl">
|
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-4">Ready to get started?</h2>
|
|
||||||
<Button as="a" href="/contact" size="lg">
|
|
||||||
Contact Us for Support
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -670,6 +670,23 @@ export default function TVServicesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
<div className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 max-w-3xl mx-auto">
|
||||||
|
<FaqItem
|
||||||
|
question="Is Assist Solutions directly providing the TV service?"
|
||||||
|
answer="As partners, we are able to refer you to each cable TV company available at your home. However, once the service starts, the cable TV service itself will be directly provided by each cable TV company."
|
||||||
|
/>
|
||||||
|
<FaqItem
|
||||||
|
question="Would I be able to choose any cable TV service that Assist Solutions is partnered with?"
|
||||||
|
answer="In Japan, most cable TV companies have predetermined service areas. We will be able to check which services are available for your home. Please contact us at info@asolutions.co.jp for a free consultation."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<div className="text-center py-10 bg-muted/30 rounded-2xl">
|
<div className="text-center py-10 bg-muted/30 rounded-2xl">
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-3">
|
<h2 className="text-2xl font-bold text-foreground mb-3">
|
||||||
@ -683,3 +700,28 @@ export default function TVServicesPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ Item component with expand/collapse functionality
|
||||||
|
*/
|
||||||
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-border rounded-xl overflow-hidden bg-card">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="w-full flex items-start justify-between gap-3 p-4 text-left hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="font-medium text-foreground">{question}</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="px-4 pb-4 text-sm text-muted-foreground leading-relaxed">{answer}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,344 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import { Search, Smartphone, Check, X } from "lucide-react";
|
||||||
|
import { cn } from "@/shared/utils";
|
||||||
|
|
||||||
|
// Device categories with their devices
|
||||||
|
const DEVICE_CATEGORIES = [
|
||||||
|
{
|
||||||
|
name: "Apple iPhone",
|
||||||
|
devices: [
|
||||||
|
"iPhone 16 Series (Standard/Plus/Pro/Pro Max)",
|
||||||
|
"iPhone 15 Series (Standard/Plus/Pro/Pro Max)",
|
||||||
|
"iPhone 14 Series (Standard/Plus/Pro/Pro Max)",
|
||||||
|
"iPhone SE (3rd Generation, 2022)",
|
||||||
|
"iPhone 13 Series (Standard/Mini/Pro/Pro Max)",
|
||||||
|
"iPhone 12 Series (Standard/Mini/Pro/Pro Max)",
|
||||||
|
"iPhone SE (2nd Generation, 2020)",
|
||||||
|
"iPhone 11 Series (Standard/Pro/Pro Max)",
|
||||||
|
"iPhone XS Series (Standard/Max)",
|
||||||
|
"iPhone XR",
|
||||||
|
"iPhone X (4G Only)",
|
||||||
|
"iPhone 8 / 8 Plus (4G Only)",
|
||||||
|
"iPhone 7 / 7 Plus (4G Only)",
|
||||||
|
"iPhone 6s / 6s Plus (4G Only)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Apple iPad",
|
||||||
|
devices: [
|
||||||
|
"iPad Pro 13-inch (M4)",
|
||||||
|
'iPad Pro 12.9" (6th/5th/4th/3rd Generations)',
|
||||||
|
'iPad Pro 11" (4th/3rd/2nd/1st Generations)',
|
||||||
|
"iPad Air 13-inch (M2)",
|
||||||
|
"iPad Air (5th/4th/3rd Generations)",
|
||||||
|
"iPad Mini (6th/5th Generations, A17 Pro)",
|
||||||
|
"iPad Standard (10th/9th/8th/7th Generations)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Google Pixel",
|
||||||
|
devices: [
|
||||||
|
"Pixel 9 Series (Pro XL/Pro/Fold/Standard)",
|
||||||
|
"Pixel 8 / 8a / 8 Pro",
|
||||||
|
"Pixel 7a / 7 / 7 Pro",
|
||||||
|
"Pixel Fold",
|
||||||
|
"Pixel 6a / 6 / 6 Pro",
|
||||||
|
"Pixel 5a (5G) / 5",
|
||||||
|
"Pixel 4a (5G) / 4a",
|
||||||
|
"Pixel 4 XL / 4",
|
||||||
|
"Pixel 3a XL / 3a",
|
||||||
|
"Pixel 3 XL / 3",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Samsung Galaxy S Series",
|
||||||
|
devices: [
|
||||||
|
"Galaxy S25 Edge",
|
||||||
|
"Galaxy S24 Ultra / S24 / S24 FE",
|
||||||
|
"Galaxy S23 Ultra / S23 / S23 FE",
|
||||||
|
"Galaxy S22 Ultra 5G / S22+ 5G / S22 5G",
|
||||||
|
"Galaxy S21 Ultra 5G / S21+ 5G / S21 5G",
|
||||||
|
"Galaxy S20 Ultra / S20+ 5G / S20+ / S20 5G / S20",
|
||||||
|
"Galaxy S10",
|
||||||
|
"Galaxy S7 edge / S6 / S6 edge",
|
||||||
|
"Galaxy S5 ACTIVE / S5 / S4",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Samsung Galaxy Z / Note / A Series",
|
||||||
|
devices: [
|
||||||
|
"Galaxy Z Fold 6 / 5 / 4 / 3 / 2",
|
||||||
|
"Galaxy Z Flip 6 / 5 / 4 / 3 5G",
|
||||||
|
"Galaxy Note 20 Ultra 5G / Note 20 5G",
|
||||||
|
"Galaxy A56 5G / A55 5G / A54 5G / A53 5G",
|
||||||
|
"Galaxy A52s 5G / A51 5G / A35 5G / A23 5G",
|
||||||
|
"Galaxy M23 5G",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sony Xperia",
|
||||||
|
devices: [
|
||||||
|
"Xperia 1 VI / 1 V / 1 IV / 1 III / 1 II",
|
||||||
|
"Xperia 5 V / 5 IV / 5 III / 5 II",
|
||||||
|
"Xperia 10 VI / 10 V / 10 IV / 10 III Lite",
|
||||||
|
"Xperia Pro-I / Pro",
|
||||||
|
"Xperia Ace III / Ace II / Ace",
|
||||||
|
"Xperia 8 Lite / XZ Premium",
|
||||||
|
"Xperia X Performance / Z5 Premium / Z5 Compact / Z5",
|
||||||
|
"Xperia Z4 / Z3 Compact / Z2 / Z",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sharp AQUOS",
|
||||||
|
devices: ["AQUOS R9 / R8 / R7", "AQUOS sense9 / sense8 / sense7", "AQUOS wish4 / wish3"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Xiaomi / Redmi",
|
||||||
|
devices: [
|
||||||
|
"Xiaomi 14T Pro / 14T / 14 Ultra / 14 Pro / 14 Pro+",
|
||||||
|
"Xiaomi 13T Pro / 13T / 13 Pro / 13 / 13 Lite",
|
||||||
|
"Xiaomi 12T Pro",
|
||||||
|
"Redmi Note 14 Pro / 13 Pro+ / 13 Pro 5G",
|
||||||
|
"Redmi Note 11 Pro 5G / 10T",
|
||||||
|
"Redmi 12 5G",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Motorola",
|
||||||
|
devices: [
|
||||||
|
"Edge 50 Ultra / 50s Pro / 50 Pro / 50 Neo / 50 Fusion",
|
||||||
|
"Edge 40 Pro / 40 Neo / 40",
|
||||||
|
"Edge+ (2024) / Edge+ (2023)",
|
||||||
|
"Razr 50 Ultra / 50 / 40 Ultra / 40",
|
||||||
|
"Razr 2024 / 2022 / 5G / 2019",
|
||||||
|
"Moto G85 / G64y 5G / G55 / G54 / G35",
|
||||||
|
"Moto G53J 5G / G52J 5G",
|
||||||
|
"ThinkPhone 25",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPPO / OnePlus",
|
||||||
|
devices: [
|
||||||
|
"OPPO Find X8 / X5 Pro / X5 / X3 Pro",
|
||||||
|
"OPPO Find N2 Flip",
|
||||||
|
"OPPO Reno11 A / 10 Pro 5G / 9 A / 7 A",
|
||||||
|
"OPPO Reno6 Pro 5G / Reno 5 A / Reno A",
|
||||||
|
"OPPO A79 5G / A73 / A55s 5G / A3 5G",
|
||||||
|
"OnePlus 13 / 12 / 11",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ASUS",
|
||||||
|
devices: [
|
||||||
|
"Zenfone 9 / 8 Flip / 8",
|
||||||
|
"ROG Phone 7 / 6 / 5 / 3 / II",
|
||||||
|
"ZenFone 7 Pro / 7 / 6",
|
||||||
|
"ZenFone 5Z / 5 / 5Q",
|
||||||
|
"ZenFone 4 Series / 3 Series",
|
||||||
|
"ZenFone Max Series",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Vivo / Nokia",
|
||||||
|
devices: ["Vivo X100 Pro / X90 Pro", "Vivo V40 / V29 / V29 Lite 5G", "Nokia XR21 / X30 / G60"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HUAWEI",
|
||||||
|
devices: [
|
||||||
|
"P40 Pro 5G / P40 / P40 lite 5G / P40 lite E",
|
||||||
|
"Mate 40 Pro+ / Mate 40 Pro / Mate 40",
|
||||||
|
"Mate 20 Pro / Mate 20 lite / Mate 10 Pro",
|
||||||
|
"P30 / P30 lite / P20 / P20 lite",
|
||||||
|
"nova 5T / nova lite 3+ / nova lite 3 / nova 3",
|
||||||
|
"MediaPad M5 / M3 / T5 Series",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fujitsu arrows",
|
||||||
|
devices: [
|
||||||
|
"arrows We2 Plus / We2 / N",
|
||||||
|
"arrows NX9 F-52A",
|
||||||
|
"arrows M05 / M04 / M03 / M02",
|
||||||
|
"arrows SV F-03H / NX F-02H",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Other Devices",
|
||||||
|
devices: [
|
||||||
|
"DuraForce EX KY-51D / PRO",
|
||||||
|
"Kids Phones (Compact/KY-41C/SH-03M)",
|
||||||
|
"ASUS Chromebook CM30 Detachable",
|
||||||
|
"dtab Compact (d-52C/d-42A) / Standard (d-51C)",
|
||||||
|
"Essential Phone PH-1",
|
||||||
|
"HTC U12+ / U11 / U11 life",
|
||||||
|
"CAT S60 / S41 / S40",
|
||||||
|
"BlackBerry PRIV / Passport / Classic",
|
||||||
|
"Microsoft Surface Pro LTE / Surface 3 (4G)",
|
||||||
|
"Lenovo Tab4 8 / YOGA Series",
|
||||||
|
"LG Nexus 5X / Nexus 5",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Flatten all devices for search
|
||||||
|
const ALL_DEVICES = DEVICE_CATEGORIES.flatMap(category =>
|
||||||
|
category.devices.map(device => ({
|
||||||
|
device,
|
||||||
|
category: category.name,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
export function DeviceCompatibility() {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const filteredDevices = useMemo(() => {
|
||||||
|
if (!searchQuery.trim()) return [];
|
||||||
|
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return ALL_DEVICES.filter(
|
||||||
|
item =>
|
||||||
|
item.device.toLowerCase().includes(query) || item.category.toLowerCase().includes(query)
|
||||||
|
).slice(0, 20); // Limit results for performance
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
const hasResults = filteredDevices.length > 0;
|
||||||
|
const showNoResults = searchQuery.trim().length > 0 && !hasResults;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mt-12 mb-8">
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-2 text-center">Device Compatibility</h2>
|
||||||
|
<p className="text-sm text-muted-foreground text-center mb-6">
|
||||||
|
Check if your device is compatible with our SIM service
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Search Box */}
|
||||||
|
<div className="max-w-xl mx-auto mb-6">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search your device (e.g., iPhone 15, Galaxy S24, Pixel 8)"
|
||||||
|
className="w-full pl-12 pr-4 py-3 rounded-xl border border-border bg-card text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Results */}
|
||||||
|
{searchQuery.trim() && (
|
||||||
|
<div className="mt-3 rounded-xl border border-border bg-card overflow-hidden">
|
||||||
|
{hasResults ? (
|
||||||
|
<div className="divide-y divide-border">
|
||||||
|
{filteredDevices.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-3 p-3 hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 h-8 w-8 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
|
||||||
|
<Check className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-foreground truncate">{item.device}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{item.category}</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-medium text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/30 px-2 py-1 rounded-full">
|
||||||
|
Compatible
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filteredDevices.length === 20 && (
|
||||||
|
<p className="text-xs text-muted-foreground text-center py-2">
|
||||||
|
Showing first 20 results. Try a more specific search.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : showNoResults ? (
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<div className="flex-shrink-0 h-12 w-12 mx-auto rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center mb-3">
|
||||||
|
<X className="h-6 w-6 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<p className="font-medium text-foreground mb-1">Device not found in our list</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Your device may still be compatible. Please{" "}
|
||||||
|
<a href="mailto:info@asolutions.co.jp" className="text-primary hover:underline">
|
||||||
|
contact us
|
||||||
|
</a>{" "}
|
||||||
|
to verify compatibility.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expandable Full Device List */}
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="w-full flex items-center justify-center gap-2 py-3 text-sm font-medium text-primary hover:text-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
<Smartphone className="h-4 w-4" />
|
||||||
|
{isExpanded ? "Hide full device list" : "View all compatible devices"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-4 rounded-xl border border-border bg-card overflow-hidden">
|
||||||
|
<div className="p-4 bg-muted/30 border-b border-border">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Below is a comprehensive list of devices confirmed to work with our SIM service.
|
||||||
|
Devices not listed may still be compatible.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-border">
|
||||||
|
{DEVICE_CATEGORIES.map((category, catIndex) => (
|
||||||
|
<DeviceCategorySection key={catIndex} category={category} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeviceCategorySection({ category }: { category: (typeof DEVICE_CATEGORIES)[number] }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="w-full flex items-center justify-between gap-3 p-4 text-left hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="font-medium text-foreground">{category.name}</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-xs text-muted-foreground transition-transform",
|
||||||
|
isOpen && "rotate-180"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
▼
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
{category.devices.map((device, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
|
||||||
|
<span>{device}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeviceCompatibility;
|
||||||
@ -26,6 +26,7 @@ import type { SimCatalogProduct } from "@customer-portal/domain/services";
|
|||||||
import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink";
|
import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink";
|
||||||
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
||||||
import { CardPricing } from "@/features/services/components/base/CardPricing";
|
import { CardPricing } from "@/features/services/components/base/CardPricing";
|
||||||
|
import { DeviceCompatibility } from "./DeviceCompatibility";
|
||||||
import {
|
import {
|
||||||
ServiceHighlights,
|
ServiceHighlights,
|
||||||
type HighlightFeature,
|
type HighlightFeature,
|
||||||
@ -613,6 +614,9 @@ export function SimPlansContent({
|
|||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Device Compatibility Section */}
|
||||||
|
<DeviceCompatibility />
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
<div className="mt-12 mb-8">
|
<div className="mt-12 mb-8">
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">
|
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">
|
||||||
@ -620,24 +624,40 @@ export function SimPlansContent({
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4 max-w-3xl mx-auto">
|
<div className="space-y-4 max-w-3xl mx-auto">
|
||||||
<FaqItem
|
<FaqItem
|
||||||
question="Can I keep my existing phone number when switching to your SIM?"
|
question="What is the service contract period?"
|
||||||
answer="Yes, we support Mobile Number Portability (MNP). You'll need to obtain an MNP reservation number from your current carrier and provide it during signup. The transfer typically completes within a few days."
|
answer="The minimum service requirement period is activation month + 3 billing months. After this period, the service will switch to a monthly service and you will be able to cancel at the end of each month."
|
||||||
/>
|
/>
|
||||||
<FaqItem
|
<FaqItem
|
||||||
question="Do I need a Japanese bank account or credit history?"
|
question="I've changed my phone and the SIM card is not working on the new device."
|
||||||
answer="No Japanese bank account or credit history is required. We accept international credit cards (Visa, Mastercard, American Express) for payment. ID verification is done through document submission."
|
answer="Whenever the SIM card is used with a new device, the APN profile would need to be installed on said device. Please refer to the APN Setup Guide in the Documents section to check how the profile can be installed."
|
||||||
/>
|
/>
|
||||||
<FaqItem
|
<FaqItem
|
||||||
question="Can I use my SIM card overseas?"
|
question="Are international calling features available?"
|
||||||
answer="International data roaming is not available. However, voice calls and SMS can be enabled for international roaming upon request, with a monthly limit of ¥50,000 to prevent bill shock."
|
answer={
|
||||||
|
<>
|
||||||
|
Enter "+" or "010", "recipient's country code",
|
||||||
|
and "recipient's phone number (regular phone number/mobile phone
|
||||||
|
number)" → Make a call.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
If the recipient's phone number begins with a 0, enter it without the first 0
|
||||||
|
(except in some countries and regions).
|
||||||
|
<br />
|
||||||
|
International calling rate is on the following Docomo's website:{" "}
|
||||||
|
<a
|
||||||
|
href="https://www.docomo.ne.jp/english/service/world/roaming/charges/kaigai/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Docomo International Calling Rates
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<FaqItem
|
<FaqItem
|
||||||
question="How do I check my remaining data balance?"
|
question="How do I cancel the service?"
|
||||||
answer="You can check your data usage anytime through our customer portal. We also send notifications when you reach 80% and 100% of your data allowance."
|
answer="To cancel, please log into the SonixNet SIM Management Website and send us a cancellation request before the 25th to cancel your account at the end of the month. For example, cancellation requests will need to be sent in by May 25th, in order to cancel at the end of May."
|
||||||
/>
|
|
||||||
<FaqItem
|
|
||||||
question="What happens if I exceed my data limit?"
|
|
||||||
answer="When you reach your data limit, speeds are throttled to 200kbps for the rest of the month. You can purchase additional data top-ups through your account portal to restore full speed."
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -654,7 +674,7 @@ export function SimPlansContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
function FaqItem({ question, answer }: { question: string; answer: React.ReactNode }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,7 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { ArrowRight, Sparkles, Wifi, Zap, Languages, FileText, Wrench, Globe } from "lucide-react";
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Sparkles,
|
||||||
|
Wifi,
|
||||||
|
Zap,
|
||||||
|
Languages,
|
||||||
|
FileText,
|
||||||
|
Wrench,
|
||||||
|
Globe,
|
||||||
|
ChevronDown,
|
||||||
|
} from "lucide-react";
|
||||||
import { usePublicInternetCatalog } from "@/features/services/hooks";
|
import { usePublicInternetCatalog } from "@/features/services/hooks";
|
||||||
import type {
|
import type {
|
||||||
InternetInstallationCatalogItem,
|
InternetInstallationCatalogItem,
|
||||||
@ -359,6 +369,9 @@ export function PublicInternetPlansContent({
|
|||||||
{/* How It Works Section */}
|
{/* How It Works Section */}
|
||||||
<HowItWorksSection />
|
<HowItWorksSection />
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
<InternetFaqSection />
|
||||||
|
|
||||||
{/* Final CTA - Polished */}
|
{/* Final CTA - Polished */}
|
||||||
<section className="text-center py-8 mt-4 rounded-2xl bg-gradient-to-br from-primary/5 via-transparent to-info/5 border border-border/50">
|
<section className="text-center py-8 mt-4 rounded-2xl bg-gradient-to-br from-primary/5 via-transparent to-info/5 border border-border/50">
|
||||||
<div className="inline-flex items-center gap-1.5 text-xs font-medium text-primary mb-3 px-3 py-1 rounded-full bg-primary/10">
|
<div className="inline-flex items-center gap-1.5 text-xs font-medium text-primary mb-3 px-3 py-1 rounded-full bg-primary/10">
|
||||||
@ -421,3 +434,82 @@ function getTierFeatures(tier: string): string[] {
|
|||||||
};
|
};
|
||||||
return features[tier] ?? [];
|
return features[tier] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Internet FAQ data
|
||||||
|
const INTERNET_FAQ_ITEMS = [
|
||||||
|
{
|
||||||
|
question: "How can I check if the 10Gbps service is available in my apartment or home?",
|
||||||
|
answer:
|
||||||
|
"Service availability depends on your address and building's network infrastructure. Please contact us (info@asolutions.co.jp) with your full address (including apartment name and room number) so we can confirm if our 10Gbps plans can be installed at your location.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question:
|
||||||
|
"My home requires multiple Wi-Fi routers for full residence coverage. Would you be able to assist with this?",
|
||||||
|
answer:
|
||||||
|
"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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question:
|
||||||
|
"We already have an Internet contract with a different company. Would it be possible to transfer our current service over to Assist Solutions?",
|
||||||
|
answer:
|
||||||
|
"Depending on the service that you are currently subscribed to, you may be able to transfer the service without cancelling. Please contact us at info@asolutions.co.jp for more information.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What is the service contract period?",
|
||||||
|
answer:
|
||||||
|
"We offer our home Internet service on a monthly basis and thus, you will not be bound by yearly contracts. A designated cancellation form will need to be submitted by the 25th to close the account at the end of the month. For example, cancellation requests will need to be sent in by May 25th, in order to cancel at the end of May.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "How are invoices sent every month?",
|
||||||
|
answer:
|
||||||
|
"E-statements (available only in English) will be sent to your primary Email address. The service fee will be charged automatically to your registered credit card on file. For corporate plans, please contact us with your requests.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "My Internet is not working as expected. What should I do?",
|
||||||
|
answer:
|
||||||
|
"Our Chatbot will be able to assist you 24/7 with the initial diagnosis and offer troubleshooting tips based on your situation. You are also welcome to contact us at info@asolutions.co.jp for tech support (our business hours are 10AM-6PM/Mon-Sat). When contacting us, please be sure to let us know of the account holder's name and residence address so that we would be able to locate your account quicker.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internet FAQ Section
|
||||||
|
*/
|
||||||
|
function InternetFaqSection() {
|
||||||
|
return (
|
||||||
|
<section className="mt-12 mb-8">
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 max-w-3xl mx-auto">
|
||||||
|
{INTERNET_FAQ_ITEMS.map((item, index) => (
|
||||||
|
<FaqItem key={index} question={item.question} answer={item.answer} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ Item component with expand/collapse functionality
|
||||||
|
*/
|
||||||
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-border rounded-xl overflow-hidden bg-card">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="w-full flex items-start justify-between gap-3 p-4 text-left hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="font-medium text-foreground">{question}</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="px-4 pb-4 text-sm text-muted-foreground leading-relaxed">{answer}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
271
docs/integrations/salesforce/apex/SIMInventoryImporter.cls
Normal file
271
docs/integrations/salesforce/apex/SIMInventoryImporter.cls
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
/**
|
||||||
|
* SIMInventoryImporter
|
||||||
|
* Invocable Apex class for importing Physical SIM inventory from CSV files.
|
||||||
|
* Used by Screen Flow to allow employees to bulk import physical SIMs.
|
||||||
|
*
|
||||||
|
* CSV Format Expected (matching ASI_N6_PASI_*.csv):
|
||||||
|
* Row,Phone_Number,PT_Number,OEM_ID,Batch_Date,,,,,
|
||||||
|
* 1,02000002470001,PT0220024700010,PASI,20251229,,,,
|
||||||
|
*
|
||||||
|
* Note: No header row expected. All imports are Physical SIM type.
|
||||||
|
*
|
||||||
|
* @author Customer Portal Team
|
||||||
|
* @version 1.1
|
||||||
|
*/
|
||||||
|
public with sharing class SIMInventoryImporter {
|
||||||
|
|
||||||
|
// Hardcoded values for Physical SIM imports
|
||||||
|
private static final String SIM_TYPE = 'Physical SIM';
|
||||||
|
private static final Boolean SKIP_HEADER = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input wrapper for the invocable method
|
||||||
|
*/
|
||||||
|
public class ImportRequest {
|
||||||
|
@InvocableVariable(label='Content Document IDs' description='IDs from File Upload component' required=true)
|
||||||
|
public List<String> contentDocumentIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output wrapper for the invocable method
|
||||||
|
*/
|
||||||
|
public class ImportResult {
|
||||||
|
@InvocableVariable(label='Success')
|
||||||
|
public Boolean success;
|
||||||
|
|
||||||
|
@InvocableVariable(label='Records Created')
|
||||||
|
public Integer recordsCreated;
|
||||||
|
|
||||||
|
@InvocableVariable(label='Records Failed')
|
||||||
|
public Integer recordsFailed;
|
||||||
|
|
||||||
|
@InvocableVariable(label='Error Messages')
|
||||||
|
public String errorMessages;
|
||||||
|
|
||||||
|
@InvocableVariable(label='Summary Message')
|
||||||
|
public String summaryMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main invocable method called by Flow
|
||||||
|
*/
|
||||||
|
@InvocableMethod(label='Import SIM Inventory from CSV'
|
||||||
|
description='Parses CSV content and creates SIM_Inventory__c records'
|
||||||
|
category='SIM Management')
|
||||||
|
public static List<ImportResult> importFromCSV(List<ImportRequest> requests) {
|
||||||
|
List<ImportResult> results = new List<ImportResult>();
|
||||||
|
|
||||||
|
for (ImportRequest request : requests) {
|
||||||
|
results.add(processCSV(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single CSV import request
|
||||||
|
*/
|
||||||
|
private static ImportResult processCSV(ImportRequest request) {
|
||||||
|
ImportResult result = new ImportResult();
|
||||||
|
result.success = true;
|
||||||
|
result.recordsCreated = 0;
|
||||||
|
result.recordsFailed = 0;
|
||||||
|
result.errorMessages = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the first Content Document ID from the list
|
||||||
|
if (request.contentDocumentIds == null || request.contentDocumentIds.isEmpty()) {
|
||||||
|
result.success = false;
|
||||||
|
result.errorMessages = 'No file was uploaded. Please select a CSV file.';
|
||||||
|
result.summaryMessage = 'Import failed: No file uploaded';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
String contentDocumentId = request.contentDocumentIds[0];
|
||||||
|
|
||||||
|
// Retrieve file content from ContentVersion
|
||||||
|
List<ContentVersion> cvList = [
|
||||||
|
SELECT VersionData
|
||||||
|
FROM ContentVersion
|
||||||
|
WHERE ContentDocumentId = :contentDocumentId
|
||||||
|
AND IsLatest = true
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
|
||||||
|
if (cvList.isEmpty()) {
|
||||||
|
result.success = false;
|
||||||
|
result.errorMessages = 'Could not find the uploaded file. Please try again.';
|
||||||
|
result.summaryMessage = 'Import failed: File not found';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String csvContent = cvList[0].VersionData.toString();
|
||||||
|
|
||||||
|
// Parse CSV content
|
||||||
|
List<String> lines = csvContent.split('\n');
|
||||||
|
List<SIM_Inventory__c> simsToInsert = new List<SIM_Inventory__c>();
|
||||||
|
List<String> errors = new List<String>();
|
||||||
|
|
||||||
|
// Start from first row (no header row in Physical SIM CSV files)
|
||||||
|
Integer startIndex = SKIP_HEADER ? 1 : 0;
|
||||||
|
|
||||||
|
// Collect existing phone numbers to check for duplicates
|
||||||
|
Set<String> existingPhoneNumbers = new Set<String>();
|
||||||
|
for (SIM_Inventory__c existing : [SELECT Phone_Number__c FROM SIM_Inventory__c WHERE Phone_Number__c != null]) {
|
||||||
|
existingPhoneNumbers.add(existing.Phone_Number__c);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> phoneNumbersInBatch = new Set<String>();
|
||||||
|
|
||||||
|
for (Integer i = startIndex; i < lines.size(); i++) {
|
||||||
|
String line = lines[i].trim();
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if (String.isBlank(line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove carriage return if present (Windows line endings)
|
||||||
|
line = line.replace('\r', '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse CSV line
|
||||||
|
List<String> columns = parseCSVLine(line);
|
||||||
|
|
||||||
|
// Expected format: Row,Phone_Number,PT_Number,OEM_ID,Batch_Date,,,,,
|
||||||
|
if (columns.size() < 2) {
|
||||||
|
errors.add('Row ' + (i + 1) + ': Not enough columns (need at least phone number)');
|
||||||
|
result.recordsFailed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String phoneNumber = columns.size() > 1 ? columns[1].trim() : '';
|
||||||
|
String ptNumber = columns.size() > 2 ? columns[2].trim() : '';
|
||||||
|
String oemId = columns.size() > 3 ? columns[3].trim() : '';
|
||||||
|
String batchDateStr = columns.size() > 4 ? columns[4].trim() : '';
|
||||||
|
|
||||||
|
// Validate phone number
|
||||||
|
if (String.isBlank(phoneNumber)) {
|
||||||
|
errors.add('Row ' + (i + 1) + ': Phone number is empty');
|
||||||
|
result.recordsFailed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicates in database
|
||||||
|
if (existingPhoneNumbers.contains(phoneNumber)) {
|
||||||
|
errors.add('Row ' + (i + 1) + ': Phone number ' + phoneNumber + ' already exists in database');
|
||||||
|
result.recordsFailed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicates within the CSV
|
||||||
|
if (phoneNumbersInBatch.contains(phoneNumber)) {
|
||||||
|
errors.add('Row ' + (i + 1) + ': Duplicate phone number ' + phoneNumber + ' in CSV file');
|
||||||
|
result.recordsFailed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse batch date (format: YYYYMMDD)
|
||||||
|
Date batchDate = null;
|
||||||
|
if (String.isNotBlank(batchDateStr) && batchDateStr.length() >= 8) {
|
||||||
|
try {
|
||||||
|
Integer year = Integer.valueOf(batchDateStr.substring(0, 4));
|
||||||
|
Integer month = Integer.valueOf(batchDateStr.substring(4, 6));
|
||||||
|
Integer day = Integer.valueOf(batchDateStr.substring(6, 8));
|
||||||
|
batchDate = Date.newInstance(year, month, day);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Leave as null if parsing fails - not critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SIM_Inventory__c record
|
||||||
|
SIM_Inventory__c sim = new SIM_Inventory__c();
|
||||||
|
sim.Phone_Number__c = phoneNumber;
|
||||||
|
sim.PT_Number__c = ptNumber;
|
||||||
|
sim.OEM_ID__c = oemId;
|
||||||
|
sim.Batch_Date__c = batchDate;
|
||||||
|
sim.Status__c = 'Available';
|
||||||
|
sim.SIM_Type__c = SIM_TYPE; // Always Physical SIM
|
||||||
|
sim.Name = phoneNumber; // Use phone number as name for easy identification
|
||||||
|
|
||||||
|
simsToInsert.add(sim);
|
||||||
|
phoneNumbersInBatch.add(phoneNumber);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
errors.add('Row ' + (i + 1) + ': ' + e.getMessage());
|
||||||
|
result.recordsFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert records with partial success allowed
|
||||||
|
if (!simsToInsert.isEmpty()) {
|
||||||
|
Database.SaveResult[] saveResults = Database.insert(simsToInsert, false);
|
||||||
|
|
||||||
|
for (Integer i = 0; i < saveResults.size(); i++) {
|
||||||
|
if (saveResults[i].isSuccess()) {
|
||||||
|
result.recordsCreated++;
|
||||||
|
} else {
|
||||||
|
result.recordsFailed++;
|
||||||
|
for (Database.Error err : saveResults[i].getErrors()) {
|
||||||
|
errors.add('Insert error for ' + simsToInsert[i].Phone_Number__c + ': ' + err.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build error message string (limit to first 10 errors for readability)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
if (errors.size() <= 10) {
|
||||||
|
result.errorMessages = String.join(errors, '\n');
|
||||||
|
} else {
|
||||||
|
List<String> firstTen = new List<String>();
|
||||||
|
for (Integer i = 0; i < 10; i++) {
|
||||||
|
firstTen.add(errors[i]);
|
||||||
|
}
|
||||||
|
result.errorMessages = String.join(firstTen, '\n')
|
||||||
|
+ '\n\n... and ' + (errors.size() - 10) + ' more errors';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build summary message
|
||||||
|
result.summaryMessage = 'Import completed: ' + result.recordsCreated + ' records created successfully.';
|
||||||
|
if (result.recordsFailed > 0) {
|
||||||
|
result.summaryMessage += ' ' + result.recordsFailed + ' records failed.';
|
||||||
|
result.success = (result.recordsCreated > 0); // Partial success if any records created
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.success = false;
|
||||||
|
result.errorMessages = 'Critical error: ' + e.getMessage() + '\n\nStack trace: ' + e.getStackTraceString();
|
||||||
|
result.summaryMessage = 'Import failed due to an unexpected error.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a single CSV line, handling quoted fields properly
|
||||||
|
*/
|
||||||
|
private static List<String> parseCSVLine(String line) {
|
||||||
|
List<String> result = new List<String>();
|
||||||
|
Boolean inQuotes = false;
|
||||||
|
String currentField = '';
|
||||||
|
|
||||||
|
for (Integer i = 0; i < line.length(); i++) {
|
||||||
|
String c = line.substring(i, i + 1);
|
||||||
|
|
||||||
|
if (c == '"') {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
} else if (c == ',' && !inQuotes) {
|
||||||
|
result.add(currentField);
|
||||||
|
currentField = '';
|
||||||
|
} else {
|
||||||
|
currentField += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last field
|
||||||
|
result.add(currentField);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
209
docs/integrations/salesforce/apex/SIMInventoryImporterTest.cls
Normal file
209
docs/integrations/salesforce/apex/SIMInventoryImporterTest.cls
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* Test class for SIMInventoryImporter
|
||||||
|
* Provides code coverage for deployment to production.
|
||||||
|
*
|
||||||
|
* @author Customer Portal Team
|
||||||
|
* @version 1.1
|
||||||
|
*/
|
||||||
|
@isTest
|
||||||
|
private class SIMInventoryImporterTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create a ContentVersion (file) for testing
|
||||||
|
*/
|
||||||
|
private static String createTestFile(String csvContent) {
|
||||||
|
ContentVersion cv = new ContentVersion();
|
||||||
|
cv.Title = 'Test SIM Import';
|
||||||
|
cv.PathOnClient = 'test_sims.csv';
|
||||||
|
cv.VersionData = Blob.valueOf(csvContent);
|
||||||
|
insert cv;
|
||||||
|
|
||||||
|
// Get the ContentDocumentId
|
||||||
|
cv = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id];
|
||||||
|
return cv.ContentDocumentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testSuccessfulImport() {
|
||||||
|
// Prepare test CSV content (matches ASI_N6_PASI format - no header row)
|
||||||
|
String csvContent = '1,02000002470001,PT0220024700010,PASI,20251229,,,,\n' +
|
||||||
|
'2,02000002470002,PT0220024700020,PASI,20251229,,,,\n' +
|
||||||
|
'3,02000002470003,PT0220024700030,PASI,20251229,,,,';
|
||||||
|
|
||||||
|
String contentDocId = createTestFile(csvContent);
|
||||||
|
|
||||||
|
SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest();
|
||||||
|
request.contentDocumentIds = new List<String>{ contentDocId };
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<SIMInventoryImporter.ImportResult> results =
|
||||||
|
SIMInventoryImporter.importFromCSV(new List<SIMInventoryImporter.ImportRequest>{ request });
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(1, results.size(), 'Should return one result');
|
||||||
|
System.assertEquals(true, results[0].success, 'Import should succeed');
|
||||||
|
System.assertEquals(3, results[0].recordsCreated, 'Should create 3 records');
|
||||||
|
System.assertEquals(0, results[0].recordsFailed, 'Should have no failures');
|
||||||
|
|
||||||
|
// Verify records were created correctly
|
||||||
|
List<SIM_Inventory__c> sims = [
|
||||||
|
SELECT Phone_Number__c, PT_Number__c, OEM_ID__c, Status__c, SIM_Type__c, Batch_Date__c
|
||||||
|
FROM SIM_Inventory__c
|
||||||
|
ORDER BY Phone_Number__c
|
||||||
|
];
|
||||||
|
System.assertEquals(3, sims.size(), 'Should have 3 SIM records');
|
||||||
|
System.assertEquals('02000002470001', sims[0].Phone_Number__c);
|
||||||
|
System.assertEquals('PT0220024700010', sims[0].PT_Number__c);
|
||||||
|
System.assertEquals('PASI', sims[0].OEM_ID__c);
|
||||||
|
System.assertEquals('Available', sims[0].Status__c);
|
||||||
|
System.assertEquals('Physical SIM', sims[0].SIM_Type__c);
|
||||||
|
System.assertEquals(Date.newInstance(2025, 12, 29), sims[0].Batch_Date__c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testDuplicateDetectionInDatabase() {
|
||||||
|
// Create existing record
|
||||||
|
insert new SIM_Inventory__c(
|
||||||
|
Name = '02000002470001',
|
||||||
|
Phone_Number__c = '02000002470001',
|
||||||
|
PT_Number__c = 'PT0220024700010',
|
||||||
|
Status__c = 'Available',
|
||||||
|
SIM_Type__c = 'Physical SIM'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to import same phone number
|
||||||
|
String csvContent = '1,02000002470001,PT0220024700010,PASI,20251229,,,,';
|
||||||
|
String contentDocId = createTestFile(csvContent);
|
||||||
|
|
||||||
|
SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest();
|
||||||
|
request.contentDocumentIds = new List<String>{ contentDocId };
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<SIMInventoryImporter.ImportResult> results =
|
||||||
|
SIMInventoryImporter.importFromCSV(new List<SIMInventoryImporter.ImportRequest>{ request });
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(0, results[0].recordsCreated, 'Should not create duplicate');
|
||||||
|
System.assertEquals(1, results[0].recordsFailed, 'Should report 1 failure');
|
||||||
|
System.assert(results[0].errorMessages.contains('already exists'), 'Should mention duplicate');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testDuplicateDetectionInCSV() {
|
||||||
|
// CSV with duplicate phone numbers
|
||||||
|
String csvContent = '1,02000002470001,PT0220024700010,PASI,20251229,,,,\n' +
|
||||||
|
'2,02000002470001,PT0220024700010,PASI,20251229,,,,';
|
||||||
|
|
||||||
|
String contentDocId = createTestFile(csvContent);
|
||||||
|
|
||||||
|
SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest();
|
||||||
|
request.contentDocumentIds = new List<String>{ contentDocId };
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<SIMInventoryImporter.ImportResult> results =
|
||||||
|
SIMInventoryImporter.importFromCSV(new List<SIMInventoryImporter.ImportRequest>{ request });
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(1, results[0].recordsCreated, 'Should create first record only');
|
||||||
|
System.assertEquals(1, results[0].recordsFailed, 'Should fail on duplicate');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testEmptyPhoneNumber() {
|
||||||
|
String csvContent = '1,,PT0220024700010,PASI,20251229,,,,';
|
||||||
|
String contentDocId = createTestFile(csvContent);
|
||||||
|
|
||||||
|
SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest();
|
||||||
|
request.contentDocumentIds = new List<String>{ contentDocId };
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<SIMInventoryImporter.ImportResult> results =
|
||||||
|
SIMInventoryImporter.importFromCSV(new List<SIMInventoryImporter.ImportRequest>{ request });
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(0, results[0].recordsCreated, 'Should not create record without phone');
|
||||||
|
System.assertEquals(1, results[0].recordsFailed, 'Should report failure');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testEmptyFile() {
|
||||||
|
String csvContent = '';
|
||||||
|
String contentDocId = createTestFile(csvContent);
|
||||||
|
|
||||||
|
SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest();
|
||||||
|
request.contentDocumentIds = new List<String>{ contentDocId };
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<SIMInventoryImporter.ImportResult> results =
|
||||||
|
SIMInventoryImporter.importFromCSV(new List<SIMInventoryImporter.ImportRequest>{ request });
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(0, results[0].recordsCreated, 'Should create no records');
|
||||||
|
System.assertEquals(0, results[0].recordsFailed, 'Should have no failures for empty file');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testAlwaysPhysicalSIM() {
|
||||||
|
// Verify that all imported SIMs are set to Physical SIM type
|
||||||
|
String csvContent = '1,02000002470001,PT0220024700010,PASI,20251229,,,,';
|
||||||
|
String contentDocId = createTestFile(csvContent);
|
||||||
|
|
||||||
|
SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest();
|
||||||
|
request.contentDocumentIds = new List<String>{ contentDocId };
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<SIMInventoryImporter.ImportResult> results =
|
||||||
|
SIMInventoryImporter.importFromCSV(new List<SIMInventoryImporter.ImportRequest>{ request });
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
SIM_Inventory__c sim = [SELECT SIM_Type__c FROM SIM_Inventory__c LIMIT 1];
|
||||||
|
System.assertEquals('Physical SIM', sim.SIM_Type__c, 'Should always be Physical SIM');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testLargeImport() {
|
||||||
|
// Test with 50 records (matches real CSV file size)
|
||||||
|
String csvContent = '';
|
||||||
|
for (Integer i = 1; i <= 50; i++) {
|
||||||
|
String phoneNum = '0200000247' + String.valueOf(i).leftPad(4, '0');
|
||||||
|
String ptNum = 'PT022002470' + String.valueOf(i).leftPad(4, '0') + '0';
|
||||||
|
csvContent += i + ',' + phoneNum + ',' + ptNum + ',PASI,20251229,,,,\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentDocId = createTestFile(csvContent);
|
||||||
|
|
||||||
|
SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest();
|
||||||
|
request.contentDocumentIds = new List<String>{ contentDocId };
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<SIMInventoryImporter.ImportResult> results =
|
||||||
|
SIMInventoryImporter.importFromCSV(new List<SIMInventoryImporter.ImportRequest>{ request });
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(50, results[0].recordsCreated, 'Should create all 50 records');
|
||||||
|
System.assertEquals(0, results[0].recordsFailed, 'Should have no failures');
|
||||||
|
|
||||||
|
Integer count = [SELECT COUNT() FROM SIM_Inventory__c];
|
||||||
|
System.assertEquals(50, count, 'Database should have 50 records');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testInvalidDateFormat() {
|
||||||
|
// Invalid date format should not fail the import, just leave date null
|
||||||
|
String csvContent = '1,02000002470001,PT0220024700010,PASI,invalid_date,,,,';
|
||||||
|
String contentDocId = createTestFile(csvContent);
|
||||||
|
|
||||||
|
SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest();
|
||||||
|
request.contentDocumentIds = new List<String>{ contentDocId };
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<SIMInventoryImporter.ImportResult> results =
|
||||||
|
SIMInventoryImporter.importFromCSV(new List<SIMInventoryImporter.ImportRequest>{ request });
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(1, results[0].recordsCreated, 'Should still create record');
|
||||||
|
|
||||||
|
SIM_Inventory__c sim = [SELECT Batch_Date__c FROM SIM_Inventory__c LIMIT 1];
|
||||||
|
System.assertEquals(null, sim.Batch_Date__c, 'Date should be null for invalid format');
|
||||||
|
}
|
||||||
|
}
|
||||||
256
docs/integrations/salesforce/sim-inventory-import.md
Normal file
256
docs/integrations/salesforce/sim-inventory-import.md
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
# SIM Inventory CSV Import - Screen Flow Setup
|
||||||
|
|
||||||
|
This guide provides the Apex class and Screen Flow configuration to enable employees to import Physical SIM data via CSV file upload.
|
||||||
|
|
||||||
|
**Simplified for Physical SIM imports only - no header row expected.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The solution consists of:
|
||||||
|
|
||||||
|
1. **Apex Invocable Class** - Parses CSV and creates SIM_Inventory\_\_c records
|
||||||
|
2. **Screen Flow** - Simple UI with just file upload and results display
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Deploy the Apex Classes
|
||||||
|
|
||||||
|
Copy the Apex classes from:
|
||||||
|
|
||||||
|
- `docs/integrations/salesforce/apex/SIMInventoryImporter.cls`
|
||||||
|
- `docs/integrations/salesforce/apex/SIMInventoryImporterTest.cls`
|
||||||
|
|
||||||
|
### Deploy Steps:
|
||||||
|
|
||||||
|
1. Go to **Setup → Apex Classes → New**
|
||||||
|
2. Paste the content of `SIMInventoryImporter.cls` → Save
|
||||||
|
3. Create another class, paste `SIMInventoryImporterTest.cls` → Save
|
||||||
|
4. Run tests to verify (Setup → Apex Test Execution)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Create the Screen Flow
|
||||||
|
|
||||||
|
### Flow Configuration
|
||||||
|
|
||||||
|
1. Go to **Setup → Flows → New Flow**
|
||||||
|
2. Select **Screen Flow**
|
||||||
|
3. Click **Create**
|
||||||
|
|
||||||
|
### Flow Elements
|
||||||
|
|
||||||
|
#### Element 1: Screen - File Upload
|
||||||
|
|
||||||
|
**Screen Properties:**
|
||||||
|
|
||||||
|
- Label: `Upload Physical SIM CSV`
|
||||||
|
- API Name: `Upload_SIM_CSV`
|
||||||
|
|
||||||
|
**Components on Screen:**
|
||||||
|
|
||||||
|
1. **Display Text** (Header)
|
||||||
|
- API Name: `Header_Text`
|
||||||
|
- Content:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Import Physical SIM Inventory
|
||||||
|
|
||||||
|
Upload a CSV file containing Physical SIM data.
|
||||||
|
|
||||||
|
**Expected format (no header row):**
|
||||||
|
`Row,Phone_Number,PT_Number,OEM_ID,Batch_Date,,,,,`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
`1,02000002470001,PT0220024700010,PASI,20251229,,,,`
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **File Upload**
|
||||||
|
- API Name: `CSV_File_Upload`
|
||||||
|
- Label: `Select CSV File`
|
||||||
|
- Allow Multiple Files: `No`
|
||||||
|
- Accept: `.csv`
|
||||||
|
- Required: `Yes`
|
||||||
|
|
||||||
|
#### Element 2: Action - Call Apex Importer
|
||||||
|
|
||||||
|
**Action Properties:**
|
||||||
|
|
||||||
|
- Category: `Apex`
|
||||||
|
- Action: `Import SIM Inventory from CSV`
|
||||||
|
|
||||||
|
**Input Values:**
|
||||||
|
|
||||||
|
- `contentDocumentId` → `{!CSV_File_Upload}` (the file upload component returns the ContentDocumentId)
|
||||||
|
|
||||||
|
**Store Output:**
|
||||||
|
|
||||||
|
- Create variables to store the output:
|
||||||
|
- `ImportResult_Success` (Boolean)
|
||||||
|
- `ImportResult_RecordsCreated` (Number)
|
||||||
|
- `ImportResult_RecordsFailed` (Number)
|
||||||
|
- `ImportResult_ErrorMessages` (Text)
|
||||||
|
- `ImportResult_SummaryMessage` (Text)
|
||||||
|
|
||||||
|
#### Element 3: Screen - Results
|
||||||
|
|
||||||
|
**Screen Properties:**
|
||||||
|
|
||||||
|
- Label: `Import Results`
|
||||||
|
- API Name: `Import_Results`
|
||||||
|
|
||||||
|
**Components:**
|
||||||
|
|
||||||
|
1. **Display Text** (Success Message)
|
||||||
|
- API Name: `Success_Message`
|
||||||
|
- Visibility: Show when `{!ImportResult_Success} Equals true`
|
||||||
|
- Content:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ **Import Successful**
|
||||||
|
|
||||||
|
**Records Created:** {!ImportResult_RecordsCreated}
|
||||||
|
|
||||||
|
{!ImportResult_SummaryMessage}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Display Text** (Error Details)
|
||||||
|
- API Name: `Error_Details`
|
||||||
|
- Visibility: Show when `{!ImportResult_RecordsFailed} Greater than 0`
|
||||||
|
- Content:
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ **Some records had issues:**
|
||||||
|
|
||||||
|
{!ImportResult_ErrorMessages}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Display Text** (Failure Message)
|
||||||
|
- API Name: `Failure_Message`
|
||||||
|
- Visibility: Show when `{!ImportResult_Success} Equals false`
|
||||||
|
- Content:
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ **Import Failed**
|
||||||
|
|
||||||
|
{!ImportResult_ErrorMessages}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Flow Diagram (Simplified)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Start │
|
||||||
|
└───────────┬─────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Screen: Upload CSV │
|
||||||
|
│ - File Upload only │
|
||||||
|
└───────────┬─────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Action: Import SIM │
|
||||||
|
│ Inventory from CSV │
|
||||||
|
│ (Apex Invocable) │
|
||||||
|
└───────────┬─────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Screen: Import Results │
|
||||||
|
│ - Success/Fail Message │
|
||||||
|
│ - Records Created │
|
||||||
|
│ - Error Details │
|
||||||
|
└───────────┬─────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ End │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Add Flow to Lightning App
|
||||||
|
|
||||||
|
1. Go to **Setup → App Manager**
|
||||||
|
2. Edit your app (e.g., "Sales" or custom app)
|
||||||
|
3. Add the Flow to utility items or create a Tab
|
||||||
|
4. Alternatively, embed in a Lightning Page:
|
||||||
|
- Edit any Lightning Record Page
|
||||||
|
- Add "Flow" component
|
||||||
|
- Select your "Import SIM Inventory" flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternative: Quick Action Button
|
||||||
|
|
||||||
|
Create a Quick Action to launch the flow from the SIM Inventory list view:
|
||||||
|
|
||||||
|
1. **Setup → Object Manager → SIM Inventory → Buttons, Links, and Actions**
|
||||||
|
2. Click **New Action**
|
||||||
|
3. Action Type: `Flow`
|
||||||
|
4. Flow: Select your import flow
|
||||||
|
5. Label: `Import SIMs from CSV`
|
||||||
|
6. Add to Page Layout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CSV File Format Reference
|
||||||
|
|
||||||
|
Your CSV files should follow this format:
|
||||||
|
|
||||||
|
| Column | Field | Example | Required |
|
||||||
|
| ------ | ------------ | --------------- | ------------ |
|
||||||
|
| 1 | Row Number | 1 | No (ignored) |
|
||||||
|
| 2 | Phone Number | 02000002470001 | Yes |
|
||||||
|
| 3 | PT Number | PT0220024700010 | No |
|
||||||
|
| 4 | OEM ID | PASI | No |
|
||||||
|
| 5 | Batch Date | 20251229 | No |
|
||||||
|
| 6-9 | Empty | | No |
|
||||||
|
|
||||||
|
**Example CSV:**
|
||||||
|
|
||||||
|
```csv
|
||||||
|
1,02000002470001,PT0220024700010,PASI,20251229,,,,
|
||||||
|
2,02000002470002,PT0220024700020,PASI,20251229,,,,
|
||||||
|
3,02000002470003,PT0220024700030,PASI,20251229,,,,
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **"Not enough columns" error**
|
||||||
|
- Ensure CSV has at least 5 columns (even if some are empty)
|
||||||
|
- Check for proper comma separators
|
||||||
|
|
||||||
|
2. **"Phone number already exists" error**
|
||||||
|
- The phone number is already in SIM_Inventory\_\_c
|
||||||
|
- Check existing records before importing
|
||||||
|
|
||||||
|
3. **File upload not working**
|
||||||
|
- Ensure file is .csv format
|
||||||
|
- Check file size (Salesforce limit: 25MB for files)
|
||||||
|
|
||||||
|
4. **Permission errors**
|
||||||
|
- User needs Create permission on SIM_Inventory\_\_c
|
||||||
|
- User needs access to the Flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- The Apex class uses `with sharing` to respect record-level security
|
||||||
|
- Only users with appropriate permissions can run the Flow
|
||||||
|
- Consider adding a Permission Set for SIM Inventory management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** January 2025
|
||||||
Loading…
x
Reference in New Issue
Block a user