Refactor code for consistency and readability across multiple files

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

View File

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

View File

@ -63,10 +63,7 @@ export class OrderBuilder {
orderFields[fields.order.activationStatus] = "Not Started"; orderFields[fields.order.activationStatus] = "Not Started";
} }
private addInternetFields( private addInternetFields(orderFields: Record<string, unknown>, body: CreateOrderBody): void {
orderFields: Record<string, unknown>,
body: CreateOrderBody
): void {
const fields = getSalesforceFieldMap(); const fields = getSalesforceFieldMap();
const config = body.configurations || {}; const config = body.configurations || {};
@ -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)
} }

View File

@ -42,11 +42,14 @@ export class OrderItemBuilder {
throw new Error(`PricebookEntry for SKU ${sku} has no UnitPrice set`); throw new Error(`PricebookEntry for SKU ${sku} has no UnitPrice set`);
} }
this.logger.log({ this.logger.log(
sku, {
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;

View File

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

View File

@ -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;

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -90,7 +90,9 @@ function CheckoutContent() {
const plan = plans.find(p => p.sku === selections.plan); const plan = plans.find(p => p.sku === selections.plan);
if (!plan) { if (!plan) {
throw new Error(`Internet plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`); throw new Error(
`Internet plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`
);
} }
// Handle addon SKUs like SIM flow // Handle addon SKUs like SIM flow
@ -116,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>

View File

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

View File

@ -30,7 +30,7 @@ export default function OrdersPage() {
const [orders, setOrders] = useState<OrderSummary[]>([]); const [orders, setOrders] = useState<OrderSummary[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const showSuccess = searchParams.get('status') === 'success'; const showSuccess = searchParams.get("status") === "success";
useEffect(() => { useEffect(() => {
const fetchOrders = async () => { const fetchOrders = async () => {
@ -75,7 +75,8 @@ export default function OrdersPage() {
Order Submitted Successfully! Order Submitted Successfully!
</h3> </h3>
<p className="text-green-800"> <p className="text-green-800">
Your order has been created and is now being processed. You can track its progress below. Your order has been created and is now being processed. You can track its progress
below.
</p> </p>
</div> </div>
</div> </div>
@ -107,7 +108,7 @@ export default function OrdersPage() {
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{orders.map((order) => ( {orders.map(order => (
<div <div
key={order.id} key={order.id}
className="bg-white border rounded-xl p-6 hover:shadow-md transition-shadow cursor-pointer" className="bg-white border rounded-xl p-6 hover:shadow-md transition-shadow cursor-pointer"
@ -122,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>

View File

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

View File

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

View File

@ -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}

View File

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

View File

@ -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>

View File

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

View File

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