Refactor code for consistency and readability across multiple files

- Standardized JSON and URL-encoded body parser configurations in main.ts.
- Simplified method signatures in order-builder.service.ts and order-orchestrator.service.ts.
- Enhanced logging format in order-item-builder.service.ts and order-validator.service.ts.
- Cleaned up whitespace and formatting in various components and services for improved readability.
- Updated Docker commands in manage.sh to use the new syntax.
This commit is contained in:
T. Narantuya 2025-08-28 18:27:11 +09:00
parent 2eb7cc6314
commit d00c47f41e
19 changed files with 381 additions and 269 deletions

View File

@ -53,14 +53,12 @@ async function bootstrap() {
} }
// Configure JSON body parser with proper limits // Configure JSON body parser with proper limits
app.use(express.json({ limit: '10mb' })); app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// Enhanced cookie parser with security options // Enhanced cookie parser with security options
app.use(cookieParser()); app.use(cookieParser());
// Trust proxy configuration for reverse proxies // Trust proxy configuration for reverse proxies
if (configService.get("TRUST_PROXY", "false") === "true") { if (configService.get("TRUST_PROXY", "false") === "true") {
const httpAdapter = app.getHttpAdapter(); const httpAdapter = app.getHttpAdapter();

View File

@ -63,10 +63,7 @@ export class OrderBuilder {
orderFields[fields.order.activationStatus] = "Not Started"; orderFields[fields.order.activationStatus] = "Not Started";
} }
private addInternetFields( private addInternetFields(orderFields: Record<string, unknown>, body: CreateOrderBody): void {
orderFields: Record<string, unknown>,
body: CreateOrderBody
): void {
const fields = getSalesforceFieldMap(); const fields = getSalesforceFieldMap();
const config = body.configurations || {}; const config = body.configurations || {};
@ -134,10 +131,7 @@ export class OrderBuilder {
} }
} }
private addVpnFields( private addVpnFields(orderFields: Record<string, unknown>, body: CreateOrderBody): void {
orderFields: Record<string, unknown>,
body: CreateOrderBody
): void {
// Note: Removed vpnRegion field - can be derived from service product metadata in OrderItems // Note: Removed vpnRegion field - can be derived from service product metadata in OrderItems
// VPN orders only need user configuration choices (none currently defined) // VPN orders only need user configuration choices (none currently defined)
} }

View File

@ -42,11 +42,14 @@ export class OrderItemBuilder {
throw new Error(`PricebookEntry for SKU ${sku} has no UnitPrice set`); throw new Error(`PricebookEntry for SKU ${sku} has no UnitPrice set`);
} }
this.logger.log({ this.logger.log(
{
sku, sku,
pbeId: meta.pbeId, pbeId: meta.pbeId,
unitPrice: meta.unitPrice unitPrice: meta.unitPrice,
}, "Creating OrderItem"); },
"Creating OrderItem"
);
try { try {
// Salesforce requires explicit UnitPrice even with PricebookEntryId // Salesforce requires explicit UnitPrice even with PricebookEntryId
@ -132,11 +135,14 @@ export class OrderItemBuilder {
}>; }>;
}; };
this.logger.debug({ this.logger.debug(
{
sku, sku,
found: !!(res.records?.length), found: !!res.records?.length,
hasPrice: !!(res.records?.[0] as any)?.UnitPrice hasPrice: !!(res.records?.[0] as any)?.UnitPrice,
}, "PricebookEntry query result"); },
"PricebookEntry query result"
);
const rec = res.records?.[0]; const rec = res.records?.[0];
if (!rec?.Id) return null; if (!rec?.Id) return null;

View File

@ -44,11 +44,7 @@ export class OrderOrchestrator {
); );
// 2) Build order fields (simplified - no address snapshot for now) // 2) Build order fields (simplified - no address snapshot for now)
const orderFields = this.orderBuilder.buildOrderFields( const orderFields = this.orderBuilder.buildOrderFields(validatedBody, userMapping, pricebookId);
validatedBody,
userMapping,
pricebookId
);
this.logger.log({ orderType: orderFields.Type }, "Creating Salesforce Order"); this.logger.log({ orderType: orderFields.Type }, "Creating Salesforce Order");
@ -58,10 +54,13 @@ export class OrderOrchestrator {
created = (await this.sf.sobject("Order").create(orderFields)) as { id: string }; created = (await this.sf.sobject("Order").create(orderFields)) as { id: string };
this.logger.log({ orderId: created.id }, "Salesforce Order created successfully"); this.logger.log({ orderId: created.id }, "Salesforce Order created successfully");
} catch (error) { } catch (error) {
this.logger.error({ this.logger.error(
{
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
orderType: orderFields.Type orderType: orderFields.Type,
}, "Failed to create Salesforce Order"); },
"Failed to create Salesforce Order"
);
throw error; throw error;
} }

View File

@ -79,7 +79,7 @@ export class OrderValidator {
{ {
orderType: validatedBody.orderType, orderType: validatedBody.orderType,
skuCount: validatedBody.skus.length, skuCount: validatedBody.skus.length,
hasConfigurations: !!validatedBody.configurations hasConfigurations: !!validatedBody.configurations,
}, },
"Request format validation passed" "Request format validation passed"
); );

View File

@ -9,7 +9,7 @@ import {
PencilIcon, PencilIcon,
CheckIcon, CheckIcon,
XMarkIcon, XMarkIcon,
ExclamationTriangleIcon ExclamationTriangleIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
interface Address { interface Address {
@ -55,14 +55,16 @@ export default function BillingPage() {
const handleEdit = () => { const handleEdit = () => {
setEditing(true); setEditing(true);
setEditedAddress(billingInfo?.address || { setEditedAddress(
billingInfo?.address || {
street: "", street: "",
streetLine2: "", streetLine2: "",
city: "", city: "",
state: "", state: "",
postalCode: "", postalCode: "",
country: "", country: "",
}); }
);
}; };
const handleSave = async () => { const handleSave = async () => {
@ -91,7 +93,9 @@ export default function BillingPage() {
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="flex items-center space-x-3 mb-6"> <div className="flex items-center space-x-3 mb-6">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="text-xl font-semibold text-gray-900">Loading billing information...</span> <span className="text-xl font-semibold text-gray-900">
Loading billing information...
</span>
</div> </div>
</div> </div>
</DashboardLayout> </DashboardLayout>
@ -159,7 +163,9 @@ export default function BillingPage() {
<input <input
type="text" type="text"
value={editedAddress?.street || ""} value={editedAddress?.street || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, street: e.target.value} : null)} onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, street: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="123 Main Street" placeholder="123 Main Street"
/> />
@ -172,7 +178,11 @@ export default function BillingPage() {
<input <input
type="text" type="text"
value={editedAddress?.streetLine2 || ""} value={editedAddress?.streetLine2 || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, streetLine2: e.target.value} : null)} onChange={e =>
setEditedAddress(prev =>
prev ? { ...prev, streetLine2: e.target.value } : null
)
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Apartment, suite, etc. (optional)" placeholder="Apartment, suite, etc. (optional)"
/> />
@ -180,13 +190,13 @@ export default function BillingPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">City *</label>
City *
</label>
<input <input
type="text" type="text"
value={editedAddress?.city || ""} value={editedAddress?.city || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, city: e.target.value} : null)} onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, city: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tokyo" placeholder="Tokyo"
/> />
@ -199,7 +209,9 @@ export default function BillingPage() {
<input <input
type="text" type="text"
value={editedAddress?.state || ""} value={editedAddress?.state || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, state: e.target.value} : null)} onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, state: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tokyo" placeholder="Tokyo"
/> />
@ -214,7 +226,11 @@ export default function BillingPage() {
<input <input
type="text" type="text"
value={editedAddress?.postalCode || ""} value={editedAddress?.postalCode || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, postalCode: e.target.value} : null)} onChange={e =>
setEditedAddress(prev =>
prev ? { ...prev, postalCode: e.target.value } : null
)
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="100-0001" placeholder="100-0001"
/> />
@ -226,7 +242,11 @@ export default function BillingPage() {
</label> </label>
<select <select
value={editedAddress?.country || ""} value={editedAddress?.country || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, country: e.target.value} : null)} onChange={e =>
setEditedAddress(prev =>
prev ? { ...prev, country: e.target.value } : null
)
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="">Select Country</option> <option value="">Select Country</option>
@ -264,11 +284,10 @@ export default function BillingPage() {
<div className="bg-gray-50 rounded-lg p-4"> <div className="bg-gray-50 rounded-lg p-4">
<div className="text-gray-900"> <div className="text-gray-900">
<p className="font-medium">{billingInfo.address.street}</p> <p className="font-medium">{billingInfo.address.street}</p>
{billingInfo.address.streetLine2 && ( {billingInfo.address.streetLine2 && <p>{billingInfo.address.streetLine2}</p>}
<p>{billingInfo.address.streetLine2}</p>
)}
<p> <p>
{billingInfo.address.city}, {billingInfo.address.state} {billingInfo.address.postalCode} {billingInfo.address.city}, {billingInfo.address.state}{" "}
{billingInfo.address.postalCode}
</p> </p>
<p>{billingInfo.address.country}</p> <p>{billingInfo.address.country}</p>
</div> </div>
@ -278,12 +297,16 @@ export default function BillingPage() {
{billingInfo.isComplete ? ( {billingInfo.isComplete ? (
<> <>
<CheckIcon className="h-4 w-4 text-green-500" /> <CheckIcon className="h-4 w-4 text-green-500" />
<span className="text-sm text-green-700 font-medium">Address Complete</span> <span className="text-sm text-green-700 font-medium">
Address Complete
</span>
</> </>
) : ( ) : (
<> <>
<ExclamationTriangleIcon className="h-4 w-4 text-yellow-500" /> <ExclamationTriangleIcon className="h-4 w-4 text-yellow-500" />
<span className="text-sm text-yellow-700 font-medium">Address Incomplete</span> <span className="text-sm text-yellow-700 font-medium">
Address Incomplete
</span>
</> </>
)} )}
</div> </div>

View File

@ -574,10 +574,7 @@ function InternetConfigureContent() {
<ArrowLeftIcon className="w-4 h-4 mr-2" /> <ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Installation Back to Installation
</AnimatedButton> </AnimatedButton>
<AnimatedButton <AnimatedButton onClick={() => transitionToStep(4)} className="flex items-center">
onClick={() => transitionToStep(4)}
className="flex items-center"
>
Review Order Review Order
<ArrowRightIcon className="w-4 h-4 ml-2" /> <ArrowRightIcon className="w-4 h-4 ml-2" />
</AnimatedButton> </AnimatedButton>
@ -617,7 +614,9 @@ function InternetConfigureContent() {
<p className="text-sm text-gray-600">Internet Service</p> <p className="text-sm text-gray-600">Internet Service</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="font-semibold text-gray-900">¥{plan.monthlyPrice?.toLocaleString()}</p> <p className="font-semibold text-gray-900">
¥{plan.monthlyPrice?.toLocaleString()}
</p>
<p className="text-xs text-gray-500">per month</p> <p className="text-xs text-gray-500">per month</p>
</div> </div>
</div> </div>
@ -645,7 +644,12 @@ function InternetConfigureContent() {
<div key={addonSku} className="flex justify-between text-sm"> <div key={addonSku} className="flex justify-between text-sm">
<span className="text-gray-600">{addon?.name || addonSku}</span> <span className="text-gray-600">{addon?.name || addonSku}</span>
<span className="text-gray-900"> <span className="text-gray-900">
¥{(addon?.monthlyPrice || addon?.activationPrice || 0).toLocaleString()} ¥
{(
addon?.monthlyPrice ||
addon?.activationPrice ||
0
).toLocaleString()}
<span className="text-xs text-gray-500 ml-1"> <span className="text-xs text-gray-500 ml-1">
/{addon?.monthlyPrice ? "mo" : "once"} /{addon?.monthlyPrice ? "mo" : "once"}
</span> </span>
@ -686,7 +690,9 @@ function InternetConfigureContent() {
{oneTimeTotal > 0 && ( {oneTimeTotal > 0 && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-600">One-time Total</span> <span className="text-gray-600">One-time Total</span>
<span className="text-orange-600 font-semibold">¥{oneTimeTotal.toLocaleString()}</span> <span className="text-orange-600 font-semibold">
¥{oneTimeTotal.toLocaleString()}
</span>
</div> </div>
)} )}
</div> </div>
@ -694,9 +700,7 @@ function InternetConfigureContent() {
{/* Receipt Footer */} {/* Receipt Footer */}
<div className="text-center mt-6 pt-4 border-t border-gray-200"> <div className="text-center mt-6 pt-4 border-t border-gray-200">
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">High-speed internet service</p>
High-speed internet service
</p>
</div> </div>
</div> </div>

View File

@ -101,7 +101,10 @@ function SimConfigureContent() {
} }
const activationTypeParam = searchParams.get("activationType"); const activationTypeParam = searchParams.get("activationType");
if (activationTypeParam && (activationTypeParam === "Immediate" || activationTypeParam === "Scheduled")) { if (
activationTypeParam &&
(activationTypeParam === "Immediate" || activationTypeParam === "Scheduled")
) {
setActivationType(activationTypeParam); setActivationType(activationTypeParam);
} }
@ -142,7 +145,12 @@ function SimConfigureContent() {
portingFirstName: portingFirstName || "", portingFirstName: portingFirstName || "",
portingLastNameKatakana: portingLastNameKatakana || "", portingLastNameKatakana: portingLastNameKatakana || "",
portingFirstNameKatakana: portingFirstNameKatakana || "", portingFirstNameKatakana: portingFirstNameKatakana || "",
portingGender: (portingGender === "Male" || portingGender === "Female" || portingGender === "Corporate/Other") ? portingGender : "", portingGender:
portingGender === "Male" ||
portingGender === "Female" ||
portingGender === "Corporate/Other"
? portingGender
: "",
portingDateOfBirth: portingDateOfBirth || "", portingDateOfBirth: portingDateOfBirth || "",
}); });
} }
@ -441,7 +449,7 @@ function SimConfigureContent() {
/> />
<SimTypeSelector <SimTypeSelector
simType={simType} simType={simType}
onSimTypeChange={(type) => setSimType(type)} onSimTypeChange={type => setSimType(type)}
eid={eid} eid={eid}
onEidChange={setEid} onEidChange={setEid}
errors={errors} errors={errors}
@ -521,7 +529,9 @@ function SimConfigureContent() {
setErrors({}); // Clear errors setErrors({}); // Clear errors
transitionToStep(3); transitionToStep(3);
}} }}
disabled={!activationType || (activationType === "Scheduled" && !scheduledActivationDate)} disabled={
!activationType || (activationType === "Scheduled" && !scheduledActivationDate)
}
className="flex items-center" className="flex items-center"
> >
Continue to Add-ons Continue to Add-ons
@ -543,7 +553,11 @@ function SimConfigureContent() {
<StepHeader <StepHeader
stepNumber={3} stepNumber={3}
title={plan.planType === "DataOnly" ? "Add-ons" : "Voice Add-ons"} title={plan.planType === "DataOnly" ? "Add-ons" : "Voice Add-ons"}
description={plan.planType === "DataOnly" ? "No add-ons available for data-only plans" : "Enhance your voice services with these optional features"} description={
plan.planType === "DataOnly"
? "No add-ons available for data-only plans"
: "Enhance your voice services with these optional features"
}
/> />
</div> </div>
@ -584,10 +598,7 @@ function SimConfigureContent() {
<ArrowLeftIcon className="w-4 h-4 mr-2" /> <ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Activation Back to Activation
</AnimatedButton> </AnimatedButton>
<AnimatedButton <AnimatedButton onClick={() => transitionToStep(4)} className="flex items-center">
onClick={() => transitionToStep(4)}
className="flex items-center"
>
Continue to Number Porting Continue to Number Porting
<ArrowRightIcon className="w-4 h-4 ml-2" /> <ArrowRightIcon className="w-4 h-4 ml-2" />
</AnimatedButton> </AnimatedButton>
@ -679,7 +690,9 @@ function SimConfigureContent() {
<p className="text-sm text-gray-600">{plan.dataSize}</p> <p className="text-sm text-gray-600">{plan.dataSize}</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="font-semibold text-gray-900">¥{plan.monthlyPrice?.toLocaleString()}</p> <p className="font-semibold text-gray-900">
¥{plan.monthlyPrice?.toLocaleString()}
</p>
<p className="text-xs text-gray-500">per month</p> <p className="text-xs text-gray-500">per month</p>
</div> </div>
</div> </div>
@ -696,7 +709,9 @@ function SimConfigureContent() {
{simType === "eSIM" && eid && ( {simType === "eSIM" && eid && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">EID:</span> <span className="text-gray-600">EID:</span>
<span className="text-gray-900 font-mono text-xs">{eid.substring(0, 12)}...</span> <span className="text-gray-900 font-mono text-xs">
{eid.substring(0, 12)}...
</span>
</div> </div>
)} )}
<div className="flex justify-between"> <div className="flex justify-between">
@ -764,7 +779,9 @@ function SimConfigureContent() {
{oneTimeTotal > 0 && ( {oneTimeTotal > 0 && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-600">One-time Total</span> <span className="text-gray-600">One-time Total</span>
<span className="text-orange-600 font-semibold">¥{oneTimeTotal.toLocaleString()}</span> <span className="text-orange-600 font-semibold">
¥{oneTimeTotal.toLocaleString()}
</span>
</div> </div>
)} )}
</div> </div>

View File

@ -90,7 +90,9 @@ function CheckoutContent() {
const plan = plans.find(p => p.sku === selections.plan); const plan = plans.find(p => p.sku === selections.plan);
if (!plan) { if (!plan) {
throw new Error(`Internet plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`); throw new Error(
`Internet plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`
);
} }
// Handle addon SKUs like SIM flow // Handle addon SKUs like SIM flow
@ -116,7 +118,9 @@ function CheckoutContent() {
const plan = plans.find(p => p.sku === selections.plan); // Look up by SKU instead of ID const plan = plans.find(p => p.sku === selections.plan); // Look up by SKU instead of ID
if (!plan) { if (!plan) {
throw new Error(`SIM plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`); throw new Error(
`SIM plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`
);
} }
// Handle multiple addons from URL parameters // Handle multiple addons from URL parameters
const addonSkus: string[] = []; const addonSkus: string[] = [];
@ -210,7 +214,7 @@ function CheckoutContent() {
...(confirmedAddress && { address: confirmedAddress }), ...(confirmedAddress && { address: confirmedAddress }),
}; };
const response = await authenticatedApi.post<{sfOrderId: string}>("/orders", orderData); const response = await authenticatedApi.post<{ sfOrderId: string }>("/orders", orderData);
router.push(`/orders/${response.sfOrderId}?status=success`); router.push(`/orders/${response.sfOrderId}?status=success`);
} catch (error) { } catch (error) {
console.error("Order submission failed:", error); console.error("Order submission failed:", error);
@ -288,8 +292,8 @@ function CheckoutContent() {
</div> </div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Submit Your Order for Review</h2> <h2 className="text-2xl font-bold text-gray-900 mb-2">Submit Your Order for Review</h2>
<p className="text-gray-600 mb-4"> <p className="text-gray-600 mb-4">
You've configured your service and reviewed all details. You've configured your service and reviewed all details. Your order will be submitted
Your order will be submitted for review and approval. for review and approval.
</p> </p>
<div className="bg-white rounded-lg p-4 border border-blue-200"> <div className="bg-white rounded-lg p-4 border border-blue-200">
<h3 className="font-semibold text-gray-900 mb-2">What happens next?</h3> <h3 className="font-semibold text-gray-900 mb-2">What happens next?</h3>
@ -325,14 +329,18 @@ function CheckoutContent() {
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5"> <div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> <path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg> </svg>
</div> </div>
<div> <div>
<p className="text-green-800 text-sm font-medium">Payment method verified</p> <p className="text-green-800 text-sm font-medium">Payment method verified</p>
<p className="text-green-700 text-sm mt-1"> <p className="text-green-700 text-sm mt-1">
After order approval, payment will be automatically processed using your existing payment method on file. After order approval, payment will be automatically processed using your existing
No additional payment steps required. payment method on file. No additional payment steps required.
</p> </p>
</div> </div>
</div> </div>
@ -348,7 +356,8 @@ function CheckoutContent() {
const reviewStep = orderType === "Internet" ? "4" : "5"; const reviewStep = orderType === "Internet" ? "4" : "5";
urlParams.set("step", reviewStep); urlParams.set("step", reviewStep);
const configureUrl = orderType === "Internet" const configureUrl =
orderType === "Internet"
? `/catalog/internet/configure?${urlParams.toString()}` ? `/catalog/internet/configure?${urlParams.toString()}`
: `/catalog/sim/configure?${urlParams.toString()}`; : `/catalog/sim/configure?${urlParams.toString()}`;
router.push(configureUrl); router.push(configureUrl);
@ -365,9 +374,25 @@ function CheckoutContent() {
> >
{submitting ? ( {submitting ? (
<span className="flex items-center justify-center"> <span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
Submitting Order... Submitting Order...
</span> </span>

View File

@ -43,7 +43,7 @@ export default function OrderStatusPage() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [data, setData] = useState<OrderSummary | null>(null); const [data, setData] = useState<OrderSummary | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const isNewOrder = searchParams.get('status') === 'success'; const isNewOrder = searchParams.get("status") === "success";
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
@ -83,7 +83,8 @@ export default function OrderStatusPage() {
Order Submitted Successfully! Order Submitted Successfully!
</h3> </h3>
<p className="text-green-800 mb-3"> <p className="text-green-800 mb-3">
Your order has been created and submitted for processing. We'll notify you as soon as it's approved and ready for activation. Your order has been created and submitted for processing. We'll notify you as soon
as it's approved and ready for activation.
</p> </p>
<div className="text-sm text-green-700"> <div className="text-sm text-green-700">
<p className="mb-1"> <p className="mb-1">

View File

@ -30,7 +30,7 @@ export default function OrdersPage() {
const [orders, setOrders] = useState<OrderSummary[]>([]); const [orders, setOrders] = useState<OrderSummary[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const showSuccess = searchParams.get('status') === 'success'; const showSuccess = searchParams.get("status") === "success";
useEffect(() => { useEffect(() => {
const fetchOrders = async () => { const fetchOrders = async () => {
@ -75,7 +75,8 @@ export default function OrdersPage() {
Order Submitted Successfully! Order Submitted Successfully!
</h3> </h3>
<p className="text-green-800"> <p className="text-green-800">
Your order has been created and is now being processed. You can track its progress below. Your order has been created and is now being processed. You can track its progress
below.
</p> </p>
</div> </div>
</div> </div>
@ -107,7 +108,7 @@ export default function OrdersPage() {
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{orders.map((order) => ( {orders.map(order => (
<div <div
key={order.id} key={order.id}
className="bg-white border rounded-xl p-6 hover:shadow-md transition-shadow cursor-pointer" className="bg-white border rounded-xl p-6 hover:shadow-md transition-shadow cursor-pointer"
@ -122,7 +123,9 @@ export default function OrdersPage() {
{order.orderType} {new Date(order.createdDate).toLocaleDateString()} {order.orderType} {new Date(order.createdDate).toLocaleDateString()}
</p> </p>
</div> </div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(order.status)}`}> <span
className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(order.status)}`}
>
{order.status} {order.status}
</span> </span>
</div> </div>
@ -133,8 +136,10 @@ export default function OrdersPage() {
<p className="text-gray-600"> <p className="text-gray-600">
{order.itemSummary || {order.itemSummary ||
(order.itemsSummary && order.itemsSummary.length > 0 (order.itemsSummary && order.itemsSummary.length > 0
? order.itemsSummary.map(item => `${item.name} (${item.quantity})`).join(', ') ? order.itemsSummary
: 'No items')} .map(item => `${item.name} (${item.quantity})`)
.join(", ")
: "No items")}
</p> </p>
</div> </div>
<div> <div>
@ -144,7 +149,9 @@ export default function OrdersPage() {
{order.totalAmount && ( {order.totalAmount && (
<div> <div>
<span className="font-medium text-gray-700">Total:</span> <span className="font-medium text-gray-700">Total:</span>
<p className="text-gray-900 font-semibold">¥{order.totalAmount.toLocaleString()}</p> <p className="text-gray-900 font-semibold">
¥{order.totalAmount.toLocaleString()}
</p>
</div> </div>
)} )}
</div> </div>

View File

@ -27,7 +27,7 @@ export function ActivationForm({
name="activationType" name="activationType"
value="Immediate" value="Immediate"
checked={activationType === "Immediate"} checked={activationType === "Immediate"}
onChange={(e) => onActivationTypeChange(e.target.value as "Immediate")} onChange={e => onActivationTypeChange(e.target.value as "Immediate")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500" className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/> />
<div> <div>
@ -50,7 +50,7 @@ export function ActivationForm({
name="activationType" name="activationType"
value="Scheduled" value="Scheduled"
checked={activationType === "Scheduled"} checked={activationType === "Scheduled"}
onChange={(e) => onActivationTypeChange(e.target.value as "Scheduled")} onChange={e => onActivationTypeChange(e.target.value as "Scheduled")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500" className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/> />
<div className="flex-1"> <div className="flex-1">
@ -71,19 +71,17 @@ export function ActivationForm({
type="date" type="date"
id="scheduledActivationDate" id="scheduledActivationDate"
value={scheduledActivationDate} value={scheduledActivationDate}
onChange={(e) => onScheduledActivationDateChange(e.target.value)} onChange={e => onScheduledActivationDateChange(e.target.value)}
min={new Date().toISOString().split("T")[0]} // Today's date min={new Date().toISOString().split("T")[0]} // Today's date
max={ max={new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]} // 30 days from now
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]
} // 30 days from now
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/> />
{errors.scheduledActivationDate && ( {errors.scheduledActivationDate && (
<p className="text-red-600 text-sm mt-1">{errors.scheduledActivationDate}</p> <p className="text-red-600 text-sm mt-1">{errors.scheduledActivationDate}</p>
)} )}
<p className="text-xs text-blue-700 mt-1"> <p className="text-xs text-blue-700 mt-1">
Note: Scheduled activation is subject to business day processing. Note: Scheduled activation is subject to business day processing. Weekend/holiday
Weekend/holiday requests may be processed on the next business day. requests may be processed on the next business day.
</p> </p>
</div> </div>
)} )}

View File

@ -21,7 +21,13 @@ interface MnpFormProps {
errors: Record<string, string>; errors: Record<string, string>;
} }
export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange, errors }: MnpFormProps) { export function MnpForm({
wantsMnp,
onWantsMnpChange,
mnpData,
onMnpDataChange,
errors,
}: MnpFormProps) {
const handleInputChange = (field: keyof MnpData, value: string) => { const handleInputChange = (field: keyof MnpData, value: string) => {
onMnpDataChange({ onMnpDataChange({
...mnpData, ...mnpData,
@ -36,7 +42,7 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
<input <input
type="checkbox" type="checkbox"
checked={wantsMnp} checked={wantsMnp}
onChange={(e) => onWantsMnpChange(e.target.checked)} onChange={e => onWantsMnpChange(e.target.checked)}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/> />
<div> <div>
@ -55,21 +61,24 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
<div className="mt-6 p-6 bg-blue-50 rounded-lg border border-blue-200"> <div className="mt-6 p-6 bg-blue-50 rounded-lg border border-blue-200">
<h4 className="font-medium text-blue-900 mb-4">Number Porting Information</h4> <h4 className="font-medium text-blue-900 mb-4">Number Porting Information</h4>
<p className="text-sm text-blue-800 mb-4"> <p className="text-sm text-blue-800 mb-4">
Please provide the following information from your current mobile carrier to complete the Please provide the following information from your current mobile carrier to complete
number porting process. the number porting process.
</p> </p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* MNP Reservation Number */} {/* MNP Reservation Number */}
<div> <div>
<label htmlFor="reservationNumber" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="reservationNumber"
className="block text-sm font-medium text-gray-700 mb-1"
>
MNP Reservation Number * MNP Reservation Number *
</label> </label>
<input <input
type="text" type="text"
id="reservationNumber" id="reservationNumber"
value={mnpData.reservationNumber} value={mnpData.reservationNumber}
onChange={(e) => handleInputChange("reservationNumber", e.target.value)} onChange={e => handleInputChange("reservationNumber", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="10-digit reservation number" placeholder="10-digit reservation number"
/> />
@ -87,7 +96,7 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
type="date" type="date"
id="expiryDate" id="expiryDate"
value={mnpData.expiryDate} value={mnpData.expiryDate}
onChange={(e) => handleInputChange("expiryDate", e.target.value)} onChange={e => handleInputChange("expiryDate", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/> />
{errors.expiryDate && ( {errors.expiryDate && (
@ -104,7 +113,7 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
type="tel" type="tel"
id="phoneNumber" id="phoneNumber"
value={mnpData.phoneNumber} value={mnpData.phoneNumber}
onChange={(e) => handleInputChange("phoneNumber", e.target.value)} onChange={e => handleInputChange("phoneNumber", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="090-1234-5678" placeholder="090-1234-5678"
/> />
@ -115,14 +124,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* MVNO Account Number */} {/* MVNO Account Number */}
<div> <div>
<label htmlFor="mvnoAccountNumber" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="mvnoAccountNumber"
className="block text-sm font-medium text-gray-700 mb-1"
>
Account Number Account Number
</label> </label>
<input <input
type="text" type="text"
id="mvnoAccountNumber" id="mvnoAccountNumber"
value={mnpData.mvnoAccountNumber} value={mnpData.mvnoAccountNumber}
onChange={(e) => handleInputChange("mvnoAccountNumber", e.target.value)} onChange={e => handleInputChange("mvnoAccountNumber", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Your current carrier account number" placeholder="Your current carrier account number"
/> />
@ -130,14 +142,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* Last Name */} {/* Last Name */}
<div> <div>
<label htmlFor="portingLastName" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="portingLastName"
className="block text-sm font-medium text-gray-700 mb-1"
>
Last Name * Last Name *
</label> </label>
<input <input
type="text" type="text"
id="portingLastName" id="portingLastName"
value={mnpData.portingLastName} value={mnpData.portingLastName}
onChange={(e) => handleInputChange("portingLastName", e.target.value)} onChange={e => handleInputChange("portingLastName", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Tanaka" placeholder="Tanaka"
/> />
@ -148,14 +163,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* First Name */} {/* First Name */}
<div> <div>
<label htmlFor="portingFirstName" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="portingFirstName"
className="block text-sm font-medium text-gray-700 mb-1"
>
First Name * First Name *
</label> </label>
<input <input
type="text" type="text"
id="portingFirstName" id="portingFirstName"
value={mnpData.portingFirstName} value={mnpData.portingFirstName}
onChange={(e) => handleInputChange("portingFirstName", e.target.value)} onChange={e => handleInputChange("portingFirstName", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Taro" placeholder="Taro"
/> />
@ -166,14 +184,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* Last Name Katakana */} {/* Last Name Katakana */}
<div> <div>
<label htmlFor="portingLastNameKatakana" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="portingLastNameKatakana"
className="block text-sm font-medium text-gray-700 mb-1"
>
Last Name (Katakana) * Last Name (Katakana) *
</label> </label>
<input <input
type="text" type="text"
id="portingLastNameKatakana" id="portingLastNameKatakana"
value={mnpData.portingLastNameKatakana} value={mnpData.portingLastNameKatakana}
onChange={(e) => handleInputChange("portingLastNameKatakana", e.target.value)} onChange={e => handleInputChange("portingLastNameKatakana", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="タナカ" placeholder="タナカ"
/> />
@ -184,14 +205,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* First Name Katakana */} {/* First Name Katakana */}
<div> <div>
<label htmlFor="portingFirstNameKatakana" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="portingFirstNameKatakana"
className="block text-sm font-medium text-gray-700 mb-1"
>
First Name (Katakana) * First Name (Katakana) *
</label> </label>
<input <input
type="text" type="text"
id="portingFirstNameKatakana" id="portingFirstNameKatakana"
value={mnpData.portingFirstNameKatakana} value={mnpData.portingFirstNameKatakana}
onChange={(e) => handleInputChange("portingFirstNameKatakana", e.target.value)} onChange={e => handleInputChange("portingFirstNameKatakana", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="タロウ" placeholder="タロウ"
/> />
@ -202,13 +226,18 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* Gender */} {/* Gender */}
<div> <div>
<label htmlFor="portingGender" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="portingGender"
className="block text-sm font-medium text-gray-700 mb-1"
>
Gender * Gender *
</label> </label>
<select <select
id="portingGender" id="portingGender"
value={mnpData.portingGender} value={mnpData.portingGender}
onChange={(e) => handleInputChange("portingGender", e.target.value as MnpData["portingGender"])} onChange={e =>
handleInputChange("portingGender", e.target.value as MnpData["portingGender"])
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
> >
<option value="">Select gender</option> <option value="">Select gender</option>
@ -223,14 +252,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* Date of Birth */} {/* Date of Birth */}
<div> <div>
<label htmlFor="portingDateOfBirth" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="portingDateOfBirth"
className="block text-sm font-medium text-gray-700 mb-1"
>
Date of Birth * Date of Birth *
</label> </label>
<input <input
type="date" type="date"
id="portingDateOfBirth" id="portingDateOfBirth"
value={mnpData.portingDateOfBirth} value={mnpData.portingDateOfBirth}
onChange={(e) => handleInputChange("portingDateOfBirth", e.target.value)} onChange={e => handleInputChange("portingDateOfBirth", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/> />
{errors.portingDateOfBirth && ( {errors.portingDateOfBirth && (
@ -241,8 +273,8 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md"> <div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-sm text-yellow-800"> <p className="text-sm text-yellow-800">
<strong>Important:</strong> Please ensure all information matches exactly with your current <strong>Important:</strong> Please ensure all information matches exactly with your
carrier records. Incorrect information may delay the porting process. current carrier records. Incorrect information may delay the porting process.
</p> </p>
</div> </div>
</div> </div>

View File

@ -37,7 +37,9 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt
{step.completed ? ( {step.completed ? (
<CheckCircleIcon className="w-5 h-5 md:w-7 md:h-7 transition-all duration-300" /> <CheckCircleIcon className="w-5 h-5 md:w-7 md:h-7 transition-all duration-300" />
) : ( ) : (
<span className="font-bold text-sm md:text-base transition-all duration-300">{step.number}</span> <span className="font-bold text-sm md:text-base transition-all duration-300">
{step.number}
</span>
)} )}
</div> </div>
<span <span

View File

@ -30,7 +30,7 @@ export function SimTypeSelector({
name="simType" name="simType"
value="Physical SIM" value="Physical SIM"
checked={simType === "Physical SIM"} checked={simType === "Physical SIM"}
onChange={(e) => onSimTypeChange(e.target.value as "Physical SIM")} onChange={e => onSimTypeChange(e.target.value as "Physical SIM")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500" className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/> />
<DevicePhoneMobileIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" /> <DevicePhoneMobileIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" />
@ -54,7 +54,7 @@ export function SimTypeSelector({
name="simType" name="simType"
value="eSIM" value="eSIM"
checked={simType === "eSIM"} checked={simType === "eSIM"}
onChange={(e) => onSimTypeChange(e.target.value as "eSIM")} onChange={e => onSimTypeChange(e.target.value as "eSIM")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500" className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/> />
<CpuChipIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" /> <CpuChipIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" />
@ -79,15 +79,15 @@ export function SimTypeSelector({
type="text" type="text"
id="eid" id="eid"
value={eid} value={eid}
onChange={(e) => onEidChange(e.target.value)} onChange={e => onEidChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="32-digit EID number" placeholder="32-digit EID number"
maxLength={32} maxLength={32}
/> />
{errors.eid && <p className="text-red-600 text-sm mt-1">{errors.eid}</p>} {errors.eid && <p className="text-red-600 text-sm mt-1">{errors.eid}</p>}
<p className="text-xs text-blue-700 mt-1"> <p className="text-xs text-blue-700 mt-1">
Find your EID in: Settings General About EID (iOS) or Settings About Phone Find your EID in: Settings General About EID (iOS) or Settings About Phone
IMEI (Android) IMEI (Android)
</p> </p>
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@ import {
PencilIcon, PencilIcon,
CheckIcon, CheckIcon,
XMarkIcon, XMarkIcon,
ExclamationTriangleIcon ExclamationTriangleIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
interface Address { interface Address {
@ -32,7 +32,10 @@ interface AddressConfirmationProps {
onAddressIncomplete: () => void; onAddressIncomplete: () => void;
} }
export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }: AddressConfirmationProps) { export function AddressConfirmation({
onAddressConfirmed,
onAddressIncomplete,
}: AddressConfirmationProps) {
const [billingInfo, setBillingInfo] = useState<BillingInfo | null>(null); const [billingInfo, setBillingInfo] = useState<BillingInfo | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
@ -63,14 +66,16 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
const handleEdit = () => { const handleEdit = () => {
setEditing(true); setEditing(true);
setEditedAddress(billingInfo?.address || { setEditedAddress(
billingInfo?.address || {
street: "", street: "",
streetLine2: "", streetLine2: "",
city: "", city: "",
state: "", state: "",
postalCode: "", postalCode: "",
country: "", country: "",
}); }
);
}; };
const handleSave = async () => { const handleSave = async () => {
@ -93,7 +98,7 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
editedAddress.state && editedAddress.state &&
editedAddress.postalCode && editedAddress.postalCode &&
editedAddress.country editedAddress.country
) ),
}); });
} }
} catch (err) { } catch (err) {
@ -175,13 +180,13 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
{editing ? ( {editing ? (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Street Address *</label>
Street Address *
</label>
<input <input
type="text" type="text"
value={editedAddress?.street || ""} value={editedAddress?.street || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, street: e.target.value} : null)} onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, street: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="123 Main Street" placeholder="123 Main Street"
/> />
@ -194,7 +199,9 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
<input <input
type="text" type="text"
value={editedAddress?.streetLine2 || ""} value={editedAddress?.streetLine2 || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, streetLine2: e.target.value} : null)} onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, streetLine2: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Apartment, suite, etc. (optional)" placeholder="Apartment, suite, etc. (optional)"
/> />
@ -202,13 +209,13 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">City *</label>
City *
</label>
<input <input
type="text" type="text"
value={editedAddress?.city || ""} value={editedAddress?.city || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, city: e.target.value} : null)} onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, city: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tokyo" placeholder="Tokyo"
/> />
@ -221,20 +228,22 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
<input <input
type="text" type="text"
value={editedAddress?.state || ""} value={editedAddress?.state || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, state: e.target.value} : null)} onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, state: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tokyo" placeholder="Tokyo"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Postal Code *</label>
Postal Code *
</label>
<input <input
type="text" type="text"
value={editedAddress?.postalCode || ""} value={editedAddress?.postalCode || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, postalCode: e.target.value} : null)} onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, postalCode: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="100-0001" placeholder="100-0001"
/> />
@ -242,12 +251,12 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Country *</label>
Country *
</label>
<select <select
value={editedAddress?.country || ""} value={editedAddress?.country || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, country: e.target.value} : null)} onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, country: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="">Select Country</option> <option value="">Select Country</option>
@ -282,11 +291,10 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
<div className="bg-gray-50 rounded-lg p-4"> <div className="bg-gray-50 rounded-lg p-4">
<div className="text-gray-900"> <div className="text-gray-900">
<p className="font-medium">{billingInfo.address.street}</p> <p className="font-medium">{billingInfo.address.street}</p>
{billingInfo.address.streetLine2 && ( {billingInfo.address.streetLine2 && <p>{billingInfo.address.streetLine2}</p>}
<p>{billingInfo.address.streetLine2}</p>
)}
<p> <p>
{billingInfo.address.city}, {billingInfo.address.state} {billingInfo.address.postalCode} {billingInfo.address.city}, {billingInfo.address.state}{" "}
{billingInfo.address.postalCode}
</p> </p>
<p>{billingInfo.address.country}</p> <p>{billingInfo.address.country}</p>
</div> </div>

View File

@ -32,7 +32,7 @@ export async function apiRequest<T>(endpoint: string, options: RequestInit = {})
const finalHeaders: Record<string, string> = { const finalHeaders: Record<string, string> = {
...defaultHeaders, ...defaultHeaders,
...(options.headers as Record<string, string> || {}), ...((options.headers as Record<string, string>) || {}),
}; };
// Force Content-Type to application/json for POST requests with body // Force Content-Type to application/json for POST requests with body
@ -40,8 +40,6 @@ export async function apiRequest<T>(endpoint: string, options: RequestInit = {})
finalHeaders["Content-Type"] = "application/json"; finalHeaders["Content-Type"] = "application/json";
} }
const response = await fetch(url, { const response = await fetch(url, {
...options, ...options,
headers: finalHeaders, headers: finalHeaders,

View File

@ -51,7 +51,7 @@ issue_cert() {
fi fi
log "🔐 Issuing certificate for $domain ..." log "🔐 Issuing certificate for $domain ..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" run --rm -p 80:80 \ docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" run --rm -p 80:80 \
-e CERTBOT_DOMAIN="$domain" -e CERTBOT_EMAIL="$email" certbot \ -e CERTBOT_DOMAIN="$domain" -e CERTBOT_EMAIL="$email" certbot \
certonly --webroot -w /var/www/certbot -d "$domain" --agree-tos --no-eff-email certonly --webroot -w /var/www/certbot -d "$domain" --agree-tos --no-eff-email
log "✅ Certificate issuance process completed" log "✅ Certificate issuance process completed"
@ -60,7 +60,7 @@ issue_cert() {
# Build production images # Build production images
build_images() { build_images() {
log "🔨 Building production images..." log "🔨 Building production images..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" build --no-cache docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" build --no-cache
log "✅ Images built successfully" log "✅ Images built successfully"
} }
@ -73,13 +73,13 @@ deploy() {
# Start database and cache first # Start database and cache first
log "🗄️ Starting database and cache..." log "🗄️ Starting database and cache..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d database cache docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d database cache
# Wait for database # Wait for database
log "⏳ Waiting for database..." log "⏳ Waiting for database..."
timeout=60 timeout=60
while [ $timeout -gt 0 ]; do while [ $timeout -gt 0 ]; do
if docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec -T database pg_isready -U portal -d portal_prod 2>/dev/null; then if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec -T database pg_isready -U portal -d portal_prod 2>/dev/null; then
log "✅ Database is ready" log "✅ Database is ready"
break break
fi fi
@ -93,17 +93,17 @@ deploy() {
# Run migrations # Run migrations
log "🔄 Running database migrations..." log "🔄 Running database migrations..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" run --rm backend pnpm db:migrate || warn "Migration may have failed" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" run --rm backend pnpm db:migrate || warn "Migration may have failed"
# Start application services (apps only; Plesk handles proxy) # Start application services (apps only; Plesk handles proxy)
log "🚀 Starting application services..." log "🚀 Starting application services..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d frontend backend docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d frontend backend
# Health checks # Health checks
log "🏥 Performing health checks..." log "🏥 Performing health checks..."
sleep 15 sleep 15
if docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps | grep -q "unhealthy"; then if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps | grep -q "unhealthy"; then
warn "Some services may not be healthy - check logs" warn "Some services may not be healthy - check logs"
fi fi
@ -115,7 +115,7 @@ deploy() {
# Stop production services # Stop production services
stop() { stop() {
log "⏹️ Stopping production services..." log "⏹️ Stopping production services..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" down docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" down
log "✅ Services stopped" log "✅ Services stopped"
} }
@ -126,7 +126,7 @@ update() {
build_images build_images
# Zero-downtime update # Zero-downtime update
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d --force-recreate --no-deps backend frontend docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d --force-recreate --no-deps backend frontend
log "✅ Production update completed" log "✅ Production update completed"
} }
@ -134,35 +134,35 @@ update() {
# Show status with health checks # Show status with health checks
status() { status() {
log "📊 Production Services Status:" log "📊 Production Services Status:"
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps
log "🏥 Health Status:" log "🏥 Health Status:"
# If proxy service exists (non-Plesk mode), check it; otherwise, check frontend directly # If proxy service exists (non-Plesk mode), check it; otherwise, check frontend directly
if docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps proxy >/dev/null 2>&1; then if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps proxy >/dev/null 2>&1; then
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec proxy wget --spider -q http://localhost/healthz && echo "✅ Proxy/Frontend healthy" || echo "❌ Proxy/Frontend unhealthy" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec proxy wget --spider -q http://localhost/healthz && echo "✅ Proxy/Frontend healthy" || echo "❌ Proxy/Frontend unhealthy"
else else
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec frontend wget --spider -q http://localhost:3000/api/health && echo "✅ Frontend healthy" || echo "❌ Frontend unhealthy" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec frontend wget --spider -q http://localhost:3000/api/health && echo "✅ Frontend healthy" || echo "❌ Frontend unhealthy"
fi fi
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec backend wget --spider -q http://localhost:4000/health && echo "✅ Backend healthy" || echo "❌ Backend unhealthy" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec backend wget --spider -q http://localhost:4000/health && echo "✅ Backend healthy" || echo "❌ Backend unhealthy"
} }
# Show logs # Show logs
logs() { logs() {
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" logs -f "${@:2}" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" logs -f "${@:2}"
} }
# Backup database # Backup database
backup() { backup() {
log "💾 Creating database backup..." log "💾 Creating database backup..."
backup_file="backup_$(date +%Y%m%d_%H%M%S).sql" backup_file="backup_$(date +%Y%m%d_%H%M%S).sql"
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec -T database pg_dump -U portal portal_prod > "$backup_file" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec -T database pg_dump -U portal portal_prod > "$backup_file"
log "✅ Database backup created: $backup_file" log "✅ Database backup created: $backup_file"
} }
# Clean up # Clean up
cleanup() { cleanup() {
log "🧹 Cleaning up old containers and images..." log "🧹 Cleaning up old containers and images..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" down --remove-orphans docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" down --remove-orphans
docker system prune -f docker system prune -f
docker image prune -f docker image prune -f
log "✅ Cleanup completed" log "✅ Cleanup completed"
@ -172,12 +172,12 @@ cleanup() {
case "${1:-help}" in case "${1:-help}" in
"deploy") deploy ;; "deploy") deploy ;;
"start") "start")
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d
log "✅ Production services started" log "✅ Production services started"
;; ;;
"stop") stop ;; "stop") stop ;;
"restart") "restart")
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" restart docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" restart
log "✅ Services restarted" log "✅ Services restarted"
;; ;;
"update") update ;; "update") update ;;