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:
parent
2eb7cc6314
commit
d00c47f41e
@ -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();
|
||||||
|
|||||||
@ -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 || {};
|
||||||
|
|
||||||
@ -77,7 +74,7 @@ export class OrderBuilder {
|
|||||||
|
|
||||||
// Note: Removed fields that can be derived from OrderItems:
|
// Note: Removed fields that can be derived from OrderItems:
|
||||||
// - internetPlanTier: derive from service product metadata
|
// - internetPlanTier: derive from service product metadata
|
||||||
// - installationType: derive from install product name
|
// - installationType: derive from install product name
|
||||||
// - weekendInstall: derive from SKU analysis
|
// - weekendInstall: derive from SKU analysis
|
||||||
// - hikariDenwa: derive from SKU analysis
|
// - hikariDenwa: derive from SKU analysis
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
{
|
||||||
pbeId: meta.pbeId,
|
sku,
|
||||||
unitPrice: meta.unitPrice
|
pbeId: meta.pbeId,
|
||||||
}, "Creating OrderItem");
|
unitPrice: meta.unitPrice,
|
||||||
|
},
|
||||||
|
"Creating OrderItem"
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Salesforce requires explicit UnitPrice even with PricebookEntryId
|
// Salesforce requires explicit UnitPrice even with PricebookEntryId
|
||||||
@ -122,7 +125,7 @@ export class OrderItemBuilder {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.logger.debug({ sku, pricebookId }, "Querying PricebookEntry for SKU");
|
this.logger.debug({ sku, pricebookId }, "Querying PricebookEntry for SKU");
|
||||||
|
|
||||||
const res = (await this.sf.query(soql)) as {
|
const res = (await this.sf.query(soql)) as {
|
||||||
records?: Array<{
|
records?: Array<{
|
||||||
Id?: string;
|
Id?: string;
|
||||||
@ -132,11 +135,14 @@ export class OrderItemBuilder {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.debug({
|
this.logger.debug(
|
||||||
sku,
|
{
|
||||||
found: !!(res.records?.length),
|
sku,
|
||||||
hasPrice: !!(res.records?.[0] as any)?.UnitPrice
|
found: !!res.records?.length,
|
||||||
}, "PricebookEntry query result");
|
hasPrice: !!(res.records?.[0] as any)?.UnitPrice,
|
||||||
|
},
|
||||||
|
"PricebookEntry query result"
|
||||||
|
);
|
||||||
|
|
||||||
const rec = res.records?.[0];
|
const rec = res.records?.[0];
|
||||||
if (!rec?.Id) return null;
|
if (!rec?.Id) return null;
|
||||||
|
|||||||
@ -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),
|
{
|
||||||
orderType: orderFields.Type
|
error: error instanceof Error ? error.message : String(error),
|
||||||
}, "Failed to create Salesforce Order");
|
orderType: orderFields.Type,
|
||||||
|
},
|
||||||
|
"Failed to create Salesforce Order"
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -76,11 +76,11 @@ export class OrderValidator {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
{
|
{
|
||||||
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"
|
||||||
);
|
);
|
||||||
return validatedBody;
|
return validatedBody;
|
||||||
|
|||||||
@ -567,7 +567,7 @@ export class UsersService {
|
|||||||
|
|
||||||
// Get client details from WHMCS
|
// Get client details from WHMCS
|
||||||
const clientDetails = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
|
const clientDetails = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
company: clientDetails.companyname || null,
|
company: clientDetails.companyname || null,
|
||||||
email: clientDetails.email,
|
email: clientDetails.email,
|
||||||
|
|||||||
@ -3,13 +3,13 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
||||||
import { authenticatedApi } from "@/lib/api";
|
import { authenticatedApi } from "@/lib/api";
|
||||||
import {
|
import {
|
||||||
CreditCardIcon,
|
CreditCardIcon,
|
||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
ExclamationTriangleIcon
|
ExclamationTriangleIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
interface Address {
|
interface Address {
|
||||||
@ -55,19 +55,21 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
setEditedAddress(billingInfo?.address || {
|
setEditedAddress(
|
||||||
street: "",
|
billingInfo?.address || {
|
||||||
streetLine2: "",
|
street: "",
|
||||||
city: "",
|
streetLine2: "",
|
||||||
state: "",
|
city: "",
|
||||||
postalCode: "",
|
state: "",
|
||||||
country: "",
|
postalCode: "",
|
||||||
});
|
country: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!editedAddress) return;
|
if (!editedAddress) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
// TODO: Implement when WHMCS update is available
|
// TODO: Implement when WHMCS update is available
|
||||||
@ -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,12 +163,14 @@ 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"
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
Street Address Line 2
|
Street Address Line 2
|
||||||
@ -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,18 +190,18 @@ 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"
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
State/Prefecture *
|
State/Prefecture *
|
||||||
@ -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,19 +226,27 @@ 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"
|
||||||
/>
|
/>
|
||||||
</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 *
|
Country *
|
||||||
</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,26 +284,29 @@ 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>
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{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>
|
||||||
@ -341,7 +364,7 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
|
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
|
||||||
<p className="text-sm text-blue-800">
|
<p className="text-sm text-blue-800">
|
||||||
<strong>Note:</strong> Contact information is managed through your account settings.
|
<strong>Note:</strong> Contact information is managed through your account settings.
|
||||||
Address updates will be available soon.
|
Address updates will be available soon.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -82,13 +82,13 @@ function InternetConfigureContent() {
|
|||||||
setPlan(selectedPlan);
|
setPlan(selectedPlan);
|
||||||
setAddons(addonsData);
|
setAddons(addonsData);
|
||||||
setInstallations(installationsData);
|
setInstallations(installationsData);
|
||||||
|
|
||||||
// Restore state from URL parameters
|
// Restore state from URL parameters
|
||||||
const accessModeParam = searchParams.get("accessMode");
|
const accessModeParam = searchParams.get("accessMode");
|
||||||
if (accessModeParam) {
|
if (accessModeParam) {
|
||||||
setMode(accessModeParam as AccessMode);
|
setMode(accessModeParam as AccessMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const installationSkuParam = searchParams.get("installationSku");
|
const installationSkuParam = searchParams.get("installationSku");
|
||||||
if (installationSkuParam) {
|
if (installationSkuParam) {
|
||||||
const installation = installationsData.find(i => i.sku === installationSkuParam);
|
const installation = installationsData.find(i => i.sku === installationSkuParam);
|
||||||
@ -96,7 +96,7 @@ function InternetConfigureContent() {
|
|||||||
setInstallPlan(installation.type);
|
setInstallPlan(installation.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore selected addons from URL parameters
|
// Restore selected addons from URL parameters
|
||||||
const addonSkuParams = searchParams.getAll("addonSku");
|
const addonSkuParams = searchParams.getAll("addonSku");
|
||||||
if (addonSkuParams.length > 0) {
|
if (addonSkuParams.length > 0) {
|
||||||
@ -413,7 +413,7 @@ function InternetConfigureContent() {
|
|||||||
|
|
||||||
{/* Continue Button */}
|
{/* Continue Button */}
|
||||||
<div className="flex justify-end mt-6">
|
<div className="flex justify-end mt-6">
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
return;
|
return;
|
||||||
@ -517,7 +517,7 @@ function InternetConfigureContent() {
|
|||||||
|
|
||||||
{/* Continue Button */}
|
{/* Continue Button */}
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => transitionToStep(1)}
|
onClick={() => transitionToStep(1)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
@ -525,7 +525,7 @@ function InternetConfigureContent() {
|
|||||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||||
Back to Service Details
|
Back to Service Details
|
||||||
</AnimatedButton>
|
</AnimatedButton>
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!installPlan) {
|
if (!installPlan) {
|
||||||
return;
|
return;
|
||||||
@ -566,7 +566,7 @@ function InternetConfigureContent() {
|
|||||||
/>
|
/>
|
||||||
{/* Navigation Buttons */}
|
{/* Navigation Buttons */}
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => transitionToStep(2)}
|
onClick={() => transitionToStep(2)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -88,39 +88,42 @@ function SimConfigureContent() {
|
|||||||
setPlan(foundPlan);
|
setPlan(foundPlan);
|
||||||
setActivationFees(fees);
|
setActivationFees(fees);
|
||||||
setAddons(addonsData);
|
setAddons(addonsData);
|
||||||
|
|
||||||
// Restore state from URL parameters
|
// Restore state from URL parameters
|
||||||
const simTypeParam = searchParams.get("simType");
|
const simTypeParam = searchParams.get("simType");
|
||||||
if (simTypeParam && (simTypeParam === "eSIM" || simTypeParam === "Physical SIM")) {
|
if (simTypeParam && (simTypeParam === "eSIM" || simTypeParam === "Physical SIM")) {
|
||||||
setSimType(simTypeParam);
|
setSimType(simTypeParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
const eidParam = searchParams.get("eid");
|
const eidParam = searchParams.get("eid");
|
||||||
if (eidParam) {
|
if (eidParam) {
|
||||||
setEid(eidParam);
|
setEid(eidParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduledAtParam = searchParams.get("scheduledAt");
|
const scheduledAtParam = searchParams.get("scheduledAt");
|
||||||
if (scheduledAtParam) {
|
if (scheduledAtParam) {
|
||||||
setScheduledActivationDate(scheduledAtParam);
|
setScheduledActivationDate(scheduledAtParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore selected addons from URL parameters
|
// Restore selected addons from URL parameters
|
||||||
const addonSkuParams = searchParams.getAll("addonSku");
|
const addonSkuParams = searchParams.getAll("addonSku");
|
||||||
if (addonSkuParams.length > 0) {
|
if (addonSkuParams.length > 0) {
|
||||||
setSelectedAddons(addonSkuParams);
|
setSelectedAddons(addonSkuParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore MNP data from URL parameters
|
// Restore MNP data from URL parameters
|
||||||
const isMnpParam = searchParams.get("isMnp");
|
const isMnpParam = searchParams.get("isMnp");
|
||||||
if (isMnpParam === "true") {
|
if (isMnpParam === "true") {
|
||||||
setWantsMnp(true);
|
setWantsMnp(true);
|
||||||
|
|
||||||
// Restore all MNP fields
|
// Restore all MNP fields
|
||||||
const reservationNumber = searchParams.get("reservationNumber");
|
const reservationNumber = searchParams.get("reservationNumber");
|
||||||
const expiryDate = searchParams.get("expiryDate");
|
const expiryDate = searchParams.get("expiryDate");
|
||||||
@ -132,7 +135,7 @@ function SimConfigureContent() {
|
|||||||
const portingFirstNameKatakana = searchParams.get("portingFirstNameKatakana");
|
const portingFirstNameKatakana = searchParams.get("portingFirstNameKatakana");
|
||||||
const portingGender = searchParams.get("portingGender");
|
const portingGender = searchParams.get("portingGender");
|
||||||
const portingDateOfBirth = searchParams.get("portingDateOfBirth");
|
const portingDateOfBirth = searchParams.get("portingDateOfBirth");
|
||||||
|
|
||||||
setMnpData({
|
setMnpData({
|
||||||
reservationNumber: reservationNumber || "",
|
reservationNumber: reservationNumber || "",
|
||||||
expiryDate: expiryDate || "",
|
expiryDate: expiryDate || "",
|
||||||
@ -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 || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -236,7 +244,7 @@ function SimConfigureContent() {
|
|||||||
// Smooth step transition function - preserves user data
|
// Smooth step transition function - preserves user data
|
||||||
const transitionToStep = (nextStep: number) => {
|
const transitionToStep = (nextStep: number) => {
|
||||||
setIsTransitioning(true);
|
setIsTransitioning(true);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCurrentStep(nextStep);
|
setCurrentStep(nextStep);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -434,14 +442,14 @@ function SimConfigureContent() {
|
|||||||
isTransitioning ? "opacity-0 translate-y-4" : "opacity-100 translate-y-0"
|
isTransitioning ? "opacity-0 translate-y-4" : "opacity-100 translate-y-0"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<StepHeader
|
<StepHeader
|
||||||
stepNumber={1}
|
stepNumber={1}
|
||||||
title="SIM Type Selection"
|
title="SIM Type Selection"
|
||||||
description="Choose the type of SIM card for your device"
|
description="Choose the type of SIM card for your device"
|
||||||
/>
|
/>
|
||||||
<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}
|
||||||
@ -449,7 +457,7 @@ function SimConfigureContent() {
|
|||||||
|
|
||||||
{/* Continue Button */}
|
{/* Continue Button */}
|
||||||
<div className="flex justify-end mt-6">
|
<div className="flex justify-end mt-6">
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Validate step 1 before proceeding
|
// Validate step 1 before proceeding
|
||||||
if (!simType) {
|
if (!simType) {
|
||||||
@ -499,7 +507,7 @@ function SimConfigureContent() {
|
|||||||
|
|
||||||
{/* Navigation Buttons */}
|
{/* Navigation Buttons */}
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => transitionToStep(1)}
|
onClick={() => transitionToStep(1)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
@ -507,7 +515,7 @@ function SimConfigureContent() {
|
|||||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||||
Back to SIM Type
|
Back to SIM Type
|
||||||
</AnimatedButton>
|
</AnimatedButton>
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Validate step 2 before proceeding
|
// Validate step 2 before proceeding
|
||||||
if (!activationType) {
|
if (!activationType) {
|
||||||
@ -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>
|
||||||
|
|
||||||
@ -567,8 +581,8 @@ function SimConfigureContent() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
{plan.planType === "DataOnly"
|
{plan.planType === "DataOnly"
|
||||||
? "No add-ons are available for data-only plans."
|
? "No add-ons are available for data-only plans."
|
||||||
: "No add-ons are available for this plan."}
|
: "No add-ons are available for this plan."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -576,7 +590,7 @@ function SimConfigureContent() {
|
|||||||
|
|
||||||
{/* Navigation Buttons */}
|
{/* Navigation Buttons */}
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => transitionToStep(2)}
|
onClick={() => transitionToStep(2)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
@ -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>
|
||||||
@ -621,7 +632,7 @@ function SimConfigureContent() {
|
|||||||
|
|
||||||
{/* Navigation Buttons */}
|
{/* Navigation Buttons */}
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => transitionToStep(3)}
|
onClick={() => transitionToStep(3)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
@ -629,7 +640,7 @@ function SimConfigureContent() {
|
|||||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||||
Back to Add-ons
|
Back to Add-ons
|
||||||
</AnimatedButton>
|
</AnimatedButton>
|
||||||
<AnimatedButton
|
<AnimatedButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Validate MNP form if MNP is selected
|
// Validate MNP form if MNP is selected
|
||||||
if (wantsMnp && !validateForm()) {
|
if (wantsMnp && !validateForm()) {
|
||||||
@ -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,14 +709,16 @@ 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">
|
||||||
<span className="text-gray-600">Activation:</span>
|
<span className="text-gray-600">Activation:</span>
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">
|
||||||
{activationType === "Scheduled" && scheduledActivationDate
|
{activationType === "Scheduled" && scheduledActivationDate
|
||||||
? `${new Date(scheduledActivationDate).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`
|
? `${new Date(scheduledActivationDate).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`
|
||||||
: activationType || "Not selected"}
|
: activationType || "Not selected"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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,21 +118,23 @@ 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[] = [];
|
||||||
if (selections.addonSku) {
|
if (selections.addonSku) {
|
||||||
// Single addon (legacy support)
|
// Single addon (legacy support)
|
||||||
addonSkus.push(selections.addonSku);
|
addonSkus.push(selections.addonSku);
|
||||||
|
}
|
||||||
|
// Check for multiple addonSku parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlParams.getAll("addonSku").forEach(sku => {
|
||||||
|
if (sku && !addonSkus.includes(sku)) {
|
||||||
|
addonSkus.push(sku);
|
||||||
}
|
}
|
||||||
// Check for multiple addonSku parameters
|
});
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
urlParams.getAll("addonSku").forEach(sku => {
|
|
||||||
if (sku && !addonSkus.includes(sku)) {
|
|
||||||
addonSkus.push(sku);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
orderItems = buildSimOrderItems(plan, activationFees, addons, {
|
orderItems = buildSimOrderItems(plan, activationFees, addons, {
|
||||||
addonSkus: addonSkus.length > 0 ? addonSkus : undefined,
|
addonSkus: addonSkus.length > 0 ? addonSkus : undefined,
|
||||||
@ -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>
|
||||||
@ -300,7 +304,7 @@ function CheckoutContent() {
|
|||||||
<p>• You'll receive confirmation once everything is ready</p>
|
<p>• You'll receive confirmation once everything is ready</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Totals Summary */}
|
{/* Quick Totals Summary */}
|
||||||
<div className="mt-4 bg-white rounded-lg p-4 border border-gray-200">
|
<div className="mt-4 bg-white rounded-lg p-4 border border-gray-200">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@ -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>
|
||||||
@ -347,10 +355,11 @@ function CheckoutContent() {
|
|||||||
const urlParams = new URLSearchParams(params.toString());
|
const urlParams = new URLSearchParams(params.toString());
|
||||||
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 =
|
||||||
? `/catalog/internet/configure?${urlParams.toString()}`
|
orderType === "Internet"
|
||||||
: `/catalog/sim/configure?${urlParams.toString()}`;
|
? `/catalog/internet/configure?${urlParams.toString()}`
|
||||||
|
: `/catalog/sim/configure?${urlParams.toString()}`;
|
||||||
router.push(configureUrl);
|
router.push(configureUrl);
|
||||||
}}
|
}}
|
||||||
className="flex-1 px-6 py-4 border border-gray-300 rounded-lg text-center hover:bg-gray-50 transition-colors font-medium"
|
className="flex-1 px-6 py-4 border border-gray-300 rounded-lg text-center hover:bg-gray-50 transition-colors font-medium"
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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,19 +123,23 @@ 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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Items:</span>
|
<span className="font-medium text-gray-700">Items:</span>
|
||||||
<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>
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -30,14 +30,16 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt
|
|||||||
step.completed
|
step.completed
|
||||||
? "bg-green-500 border-green-500 text-white shadow-lg scale-110"
|
? "bg-green-500 border-green-500 text-white shadow-lg scale-110"
|
||||||
: currentStep === step.number
|
: currentStep === step.number
|
||||||
? "border-blue-500 text-blue-500 bg-blue-50 scale-105 shadow-md"
|
? "border-blue-500 text-blue-500 bg-blue-50 scale-105 shadow-md"
|
||||||
: "border-gray-300 text-gray-400 scale-100"
|
: "border-gray-300 text-gray-400 scale-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{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
|
||||||
@ -45,8 +47,8 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt
|
|||||||
step.completed
|
step.completed
|
||||||
? "text-green-600"
|
? "text-green-600"
|
||||||
: currentStep === step.number
|
: currentStep === step.number
|
||||||
? "text-blue-600"
|
? "text-blue-600"
|
||||||
: "text-gray-400"
|
: "text-gray-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{step.title}
|
{step.title}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { authenticatedApi } from "@/lib/api";
|
import { authenticatedApi } from "@/lib/api";
|
||||||
import {
|
import {
|
||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
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);
|
||||||
@ -48,7 +51,7 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await authenticatedApi.get<BillingInfo>("/users/billing");
|
const data = await authenticatedApi.get<BillingInfo>("/users/billing");
|
||||||
setBillingInfo(data);
|
setBillingInfo(data);
|
||||||
|
|
||||||
if (!data.isComplete) {
|
if (!data.isComplete) {
|
||||||
onAddressIncomplete();
|
onAddressIncomplete();
|
||||||
} else {
|
} else {
|
||||||
@ -63,25 +66,27 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
|
|||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
setEditedAddress(billingInfo?.address || {
|
setEditedAddress(
|
||||||
street: "",
|
billingInfo?.address || {
|
||||||
streetLine2: "",
|
street: "",
|
||||||
city: "",
|
streetLine2: "",
|
||||||
state: "",
|
city: "",
|
||||||
postalCode: "",
|
state: "",
|
||||||
country: "",
|
postalCode: "",
|
||||||
});
|
country: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!editedAddress) return;
|
if (!editedAddress) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For now, just use the edited address for the order
|
// For now, just use the edited address for the order
|
||||||
// TODO: Implement WHMCS update when available
|
// TODO: Implement WHMCS update when available
|
||||||
onAddressConfirmed(editedAddress);
|
onAddressConfirmed(editedAddress);
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
|
|
||||||
// Update local state to show the new address
|
// Update local state to show the new address
|
||||||
if (billingInfo) {
|
if (billingInfo) {
|
||||||
setBillingInfo({
|
setBillingInfo({
|
||||||
@ -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,18 +180,18 @@ 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"
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
Street Address Line 2
|
Street Address Line 2
|
||||||
@ -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,18 +209,18 @@ 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"
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
State/Prefecture *
|
State/Prefecture *
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 ;;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user