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
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
||||
|
||||
// Enhanced cookie parser with security options
|
||||
app.use(cookieParser());
|
||||
|
||||
|
||||
|
||||
// Trust proxy configuration for reverse proxies
|
||||
if (configService.get("TRUST_PROXY", "false") === "true") {
|
||||
const httpAdapter = app.getHttpAdapter();
|
||||
|
||||
@ -63,10 +63,7 @@ export class OrderBuilder {
|
||||
orderFields[fields.order.activationStatus] = "Not Started";
|
||||
}
|
||||
|
||||
private addInternetFields(
|
||||
orderFields: Record<string, unknown>,
|
||||
body: CreateOrderBody
|
||||
): void {
|
||||
private addInternetFields(orderFields: Record<string, unknown>, body: CreateOrderBody): void {
|
||||
const fields = getSalesforceFieldMap();
|
||||
const config = body.configurations || {};
|
||||
|
||||
@ -77,7 +74,7 @@ export class OrderBuilder {
|
||||
|
||||
// Note: Removed fields that can be derived from OrderItems:
|
||||
// - internetPlanTier: derive from service product metadata
|
||||
// - installationType: derive from install product name
|
||||
// - installationType: derive from install product name
|
||||
// - weekendInstall: derive from SKU analysis
|
||||
// - hikariDenwa: derive from SKU analysis
|
||||
}
|
||||
@ -134,10 +131,7 @@ export class OrderBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
private addVpnFields(
|
||||
orderFields: Record<string, unknown>,
|
||||
body: CreateOrderBody
|
||||
): void {
|
||||
private addVpnFields(orderFields: Record<string, unknown>, body: CreateOrderBody): void {
|
||||
// Note: Removed vpnRegion field - can be derived from service product metadata in OrderItems
|
||||
// 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`);
|
||||
}
|
||||
|
||||
this.logger.log({
|
||||
sku,
|
||||
pbeId: meta.pbeId,
|
||||
unitPrice: meta.unitPrice
|
||||
}, "Creating OrderItem");
|
||||
this.logger.log(
|
||||
{
|
||||
sku,
|
||||
pbeId: meta.pbeId,
|
||||
unitPrice: meta.unitPrice,
|
||||
},
|
||||
"Creating OrderItem"
|
||||
);
|
||||
|
||||
try {
|
||||
// Salesforce requires explicit UnitPrice even with PricebookEntryId
|
||||
@ -122,7 +125,7 @@ export class OrderItemBuilder {
|
||||
|
||||
try {
|
||||
this.logger.debug({ sku, pricebookId }, "Querying PricebookEntry for SKU");
|
||||
|
||||
|
||||
const res = (await this.sf.query(soql)) as {
|
||||
records?: Array<{
|
||||
Id?: string;
|
||||
@ -132,11 +135,14 @@ export class OrderItemBuilder {
|
||||
}>;
|
||||
};
|
||||
|
||||
this.logger.debug({
|
||||
sku,
|
||||
found: !!(res.records?.length),
|
||||
hasPrice: !!(res.records?.[0] as any)?.UnitPrice
|
||||
}, "PricebookEntry query result");
|
||||
this.logger.debug(
|
||||
{
|
||||
sku,
|
||||
found: !!res.records?.length,
|
||||
hasPrice: !!(res.records?.[0] as any)?.UnitPrice,
|
||||
},
|
||||
"PricebookEntry query result"
|
||||
);
|
||||
|
||||
const rec = res.records?.[0];
|
||||
if (!rec?.Id) return null;
|
||||
|
||||
@ -44,11 +44,7 @@ export class OrderOrchestrator {
|
||||
);
|
||||
|
||||
// 2) Build order fields (simplified - no address snapshot for now)
|
||||
const orderFields = this.orderBuilder.buildOrderFields(
|
||||
validatedBody,
|
||||
userMapping,
|
||||
pricebookId
|
||||
);
|
||||
const orderFields = this.orderBuilder.buildOrderFields(validatedBody, userMapping, pricebookId);
|
||||
|
||||
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 };
|
||||
this.logger.log({ orderId: created.id }, "Salesforce Order created successfully");
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
orderType: orderFields.Type
|
||||
}, "Failed to create Salesforce Order");
|
||||
this.logger.error(
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
orderType: orderFields.Type,
|
||||
},
|
||||
"Failed to create Salesforce Order"
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
@ -76,11 +76,11 @@ export class OrderValidator {
|
||||
};
|
||||
|
||||
this.logger.debug(
|
||||
{
|
||||
{
|
||||
orderType: validatedBody.orderType,
|
||||
skuCount: validatedBody.skus.length,
|
||||
hasConfigurations: !!validatedBody.configurations
|
||||
},
|
||||
hasConfigurations: !!validatedBody.configurations,
|
||||
},
|
||||
"Request format validation passed"
|
||||
);
|
||||
return validatedBody;
|
||||
|
||||
@ -567,7 +567,7 @@ export class UsersService {
|
||||
|
||||
// Get client details from WHMCS
|
||||
const clientDetails = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
|
||||
|
||||
|
||||
return {
|
||||
company: clientDetails.companyname || null,
|
||||
email: clientDetails.email,
|
||||
|
||||
@ -3,13 +3,13 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
||||
import { authenticatedApi } from "@/lib/api";
|
||||
import {
|
||||
import {
|
||||
CreditCardIcon,
|
||||
MapPinIcon,
|
||||
PencilIcon,
|
||||
CheckIcon,
|
||||
XMarkIcon,
|
||||
ExclamationTriangleIcon
|
||||
ExclamationTriangleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
interface Address {
|
||||
@ -55,19 +55,21 @@ export default function BillingPage() {
|
||||
|
||||
const handleEdit = () => {
|
||||
setEditing(true);
|
||||
setEditedAddress(billingInfo?.address || {
|
||||
street: "",
|
||||
streetLine2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
postalCode: "",
|
||||
country: "",
|
||||
});
|
||||
setEditedAddress(
|
||||
billingInfo?.address || {
|
||||
street: "",
|
||||
streetLine2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
postalCode: "",
|
||||
country: "",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editedAddress) return;
|
||||
|
||||
|
||||
try {
|
||||
setSaving(true);
|
||||
// TODO: Implement when WHMCS update is available
|
||||
@ -91,7 +93,9 @@ export default function BillingPage() {
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<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>
|
||||
<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>
|
||||
</DashboardLayout>
|
||||
@ -159,12 +163,14 @@ export default function BillingPage() {
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
placeholder="123 Main Street"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Street Address Line 2
|
||||
@ -172,7 +178,11 @@ export default function BillingPage() {
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
City *
|
||||
</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">City *</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
placeholder="Tokyo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
State/Prefecture *
|
||||
@ -199,7 +209,9 @@ export default function BillingPage() {
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
placeholder="Tokyo"
|
||||
/>
|
||||
@ -214,19 +226,27 @@ export default function BillingPage() {
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
placeholder="100-0001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Country *
|
||||
</label>
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<option value="">Select Country</option>
|
||||
@ -264,26 +284,29 @@ export default function BillingPage() {
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="text-gray-900">
|
||||
<p className="font-medium">{billingInfo.address.street}</p>
|
||||
{billingInfo.address.streetLine2 && (
|
||||
<p>{billingInfo.address.streetLine2}</p>
|
||||
)}
|
||||
{billingInfo.address.streetLine2 && <p>{billingInfo.address.streetLine2}</p>}
|
||||
<p>
|
||||
{billingInfo.address.city}, {billingInfo.address.state} {billingInfo.address.postalCode}
|
||||
{billingInfo.address.city}, {billingInfo.address.state}{" "}
|
||||
{billingInfo.address.postalCode}
|
||||
</p>
|
||||
<p>{billingInfo.address.country}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
<div className="flex items-center space-x-2">
|
||||
{billingInfo.isComplete ? (
|
||||
<>
|
||||
<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" />
|
||||
<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>
|
||||
@ -341,7 +364,7 @@ export default function BillingPage() {
|
||||
|
||||
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -82,13 +82,13 @@ function InternetConfigureContent() {
|
||||
setPlan(selectedPlan);
|
||||
setAddons(addonsData);
|
||||
setInstallations(installationsData);
|
||||
|
||||
|
||||
// Restore state from URL parameters
|
||||
const accessModeParam = searchParams.get("accessMode");
|
||||
if (accessModeParam) {
|
||||
setMode(accessModeParam as AccessMode);
|
||||
}
|
||||
|
||||
|
||||
const installationSkuParam = searchParams.get("installationSku");
|
||||
if (installationSkuParam) {
|
||||
const installation = installationsData.find(i => i.sku === installationSkuParam);
|
||||
@ -96,7 +96,7 @@ function InternetConfigureContent() {
|
||||
setInstallPlan(installation.type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Restore selected addons from URL parameters
|
||||
const addonSkuParams = searchParams.getAll("addonSku");
|
||||
if (addonSkuParams.length > 0) {
|
||||
@ -413,7 +413,7 @@ function InternetConfigureContent() {
|
||||
|
||||
{/* Continue Button */}
|
||||
<div className="flex justify-end mt-6">
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => {
|
||||
if (!mode) {
|
||||
return;
|
||||
@ -517,7 +517,7 @@ function InternetConfigureContent() {
|
||||
|
||||
{/* Continue Button */}
|
||||
<div className="flex justify-between mt-6">
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => transitionToStep(1)}
|
||||
variant="outline"
|
||||
className="flex items-center"
|
||||
@ -525,7 +525,7 @@ function InternetConfigureContent() {
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
Back to Service Details
|
||||
</AnimatedButton>
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => {
|
||||
if (!installPlan) {
|
||||
return;
|
||||
@ -566,7 +566,7 @@ function InternetConfigureContent() {
|
||||
/>
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex justify-between mt-6">
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => transitionToStep(2)}
|
||||
variant="outline"
|
||||
className="flex items-center"
|
||||
@ -574,10 +574,7 @@ function InternetConfigureContent() {
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
Back to Installation
|
||||
</AnimatedButton>
|
||||
<AnimatedButton
|
||||
onClick={() => transitionToStep(4)}
|
||||
className="flex items-center"
|
||||
>
|
||||
<AnimatedButton onClick={() => transitionToStep(4)} className="flex items-center">
|
||||
Review Order
|
||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
||||
</AnimatedButton>
|
||||
@ -617,7 +614,9 @@ function InternetConfigureContent() {
|
||||
<p className="text-sm text-gray-600">Internet Service</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -645,7 +644,12 @@ function InternetConfigureContent() {
|
||||
<div key={addonSku} className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">{addon?.name || addonSku}</span>
|
||||
<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">
|
||||
/{addon?.monthlyPrice ? "mo" : "once"}
|
||||
</span>
|
||||
@ -686,7 +690,9 @@ function InternetConfigureContent() {
|
||||
{oneTimeTotal > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<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>
|
||||
@ -694,9 +700,7 @@ function InternetConfigureContent() {
|
||||
|
||||
{/* Receipt Footer */}
|
||||
<div className="text-center mt-6 pt-4 border-t border-gray-200">
|
||||
<p className="text-xs text-gray-500">
|
||||
High-speed internet service
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">High-speed internet service</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -88,39 +88,42 @@ function SimConfigureContent() {
|
||||
setPlan(foundPlan);
|
||||
setActivationFees(fees);
|
||||
setAddons(addonsData);
|
||||
|
||||
|
||||
// Restore state from URL parameters
|
||||
const simTypeParam = searchParams.get("simType");
|
||||
if (simTypeParam && (simTypeParam === "eSIM" || simTypeParam === "Physical SIM")) {
|
||||
setSimType(simTypeParam);
|
||||
}
|
||||
|
||||
|
||||
const eidParam = searchParams.get("eid");
|
||||
if (eidParam) {
|
||||
setEid(eidParam);
|
||||
}
|
||||
|
||||
|
||||
const activationTypeParam = searchParams.get("activationType");
|
||||
if (activationTypeParam && (activationTypeParam === "Immediate" || activationTypeParam === "Scheduled")) {
|
||||
if (
|
||||
activationTypeParam &&
|
||||
(activationTypeParam === "Immediate" || activationTypeParam === "Scheduled")
|
||||
) {
|
||||
setActivationType(activationTypeParam);
|
||||
}
|
||||
|
||||
|
||||
const scheduledAtParam = searchParams.get("scheduledAt");
|
||||
if (scheduledAtParam) {
|
||||
setScheduledActivationDate(scheduledAtParam);
|
||||
}
|
||||
|
||||
|
||||
// Restore selected addons from URL parameters
|
||||
const addonSkuParams = searchParams.getAll("addonSku");
|
||||
if (addonSkuParams.length > 0) {
|
||||
setSelectedAddons(addonSkuParams);
|
||||
}
|
||||
|
||||
|
||||
// Restore MNP data from URL parameters
|
||||
const isMnpParam = searchParams.get("isMnp");
|
||||
if (isMnpParam === "true") {
|
||||
setWantsMnp(true);
|
||||
|
||||
|
||||
// Restore all MNP fields
|
||||
const reservationNumber = searchParams.get("reservationNumber");
|
||||
const expiryDate = searchParams.get("expiryDate");
|
||||
@ -132,7 +135,7 @@ function SimConfigureContent() {
|
||||
const portingFirstNameKatakana = searchParams.get("portingFirstNameKatakana");
|
||||
const portingGender = searchParams.get("portingGender");
|
||||
const portingDateOfBirth = searchParams.get("portingDateOfBirth");
|
||||
|
||||
|
||||
setMnpData({
|
||||
reservationNumber: reservationNumber || "",
|
||||
expiryDate: expiryDate || "",
|
||||
@ -142,7 +145,12 @@ function SimConfigureContent() {
|
||||
portingFirstName: portingFirstName || "",
|
||||
portingLastNameKatakana: portingLastNameKatakana || "",
|
||||
portingFirstNameKatakana: portingFirstNameKatakana || "",
|
||||
portingGender: (portingGender === "Male" || portingGender === "Female" || portingGender === "Corporate/Other") ? portingGender : "",
|
||||
portingGender:
|
||||
portingGender === "Male" ||
|
||||
portingGender === "Female" ||
|
||||
portingGender === "Corporate/Other"
|
||||
? portingGender
|
||||
: "",
|
||||
portingDateOfBirth: portingDateOfBirth || "",
|
||||
});
|
||||
}
|
||||
@ -236,7 +244,7 @@ function SimConfigureContent() {
|
||||
// Smooth step transition function - preserves user data
|
||||
const transitionToStep = (nextStep: number) => {
|
||||
setIsTransitioning(true);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
setCurrentStep(nextStep);
|
||||
setTimeout(() => {
|
||||
@ -434,14 +442,14 @@ function SimConfigureContent() {
|
||||
isTransitioning ? "opacity-0 translate-y-4" : "opacity-100 translate-y-0"
|
||||
}`}
|
||||
>
|
||||
<StepHeader
|
||||
stepNumber={1}
|
||||
title="SIM Type Selection"
|
||||
description="Choose the type of SIM card for your device"
|
||||
/>
|
||||
<StepHeader
|
||||
stepNumber={1}
|
||||
title="SIM Type Selection"
|
||||
description="Choose the type of SIM card for your device"
|
||||
/>
|
||||
<SimTypeSelector
|
||||
simType={simType}
|
||||
onSimTypeChange={(type) => setSimType(type)}
|
||||
onSimTypeChange={type => setSimType(type)}
|
||||
eid={eid}
|
||||
onEidChange={setEid}
|
||||
errors={errors}
|
||||
@ -449,7 +457,7 @@ function SimConfigureContent() {
|
||||
|
||||
{/* Continue Button */}
|
||||
<div className="flex justify-end mt-6">
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => {
|
||||
// Validate step 1 before proceeding
|
||||
if (!simType) {
|
||||
@ -499,7 +507,7 @@ function SimConfigureContent() {
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex justify-between mt-6">
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => transitionToStep(1)}
|
||||
variant="outline"
|
||||
className="flex items-center"
|
||||
@ -507,7 +515,7 @@ function SimConfigureContent() {
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
Back to SIM Type
|
||||
</AnimatedButton>
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => {
|
||||
// Validate step 2 before proceeding
|
||||
if (!activationType) {
|
||||
@ -521,7 +529,9 @@ function SimConfigureContent() {
|
||||
setErrors({}); // Clear errors
|
||||
transitionToStep(3);
|
||||
}}
|
||||
disabled={!activationType || (activationType === "Scheduled" && !scheduledActivationDate)}
|
||||
disabled={
|
||||
!activationType || (activationType === "Scheduled" && !scheduledActivationDate)
|
||||
}
|
||||
className="flex items-center"
|
||||
>
|
||||
Continue to Add-ons
|
||||
@ -543,7 +553,11 @@ function SimConfigureContent() {
|
||||
<StepHeader
|
||||
stepNumber={3}
|
||||
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>
|
||||
|
||||
@ -567,8 +581,8 @@ function SimConfigureContent() {
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-600">
|
||||
{plan.planType === "DataOnly"
|
||||
? "No add-ons are available for data-only plans."
|
||||
{plan.planType === "DataOnly"
|
||||
? "No add-ons are available for data-only plans."
|
||||
: "No add-ons are available for this plan."}
|
||||
</p>
|
||||
</div>
|
||||
@ -576,7 +590,7 @@ function SimConfigureContent() {
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex justify-between mt-6">
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => transitionToStep(2)}
|
||||
variant="outline"
|
||||
className="flex items-center"
|
||||
@ -584,10 +598,7 @@ function SimConfigureContent() {
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
Back to Activation
|
||||
</AnimatedButton>
|
||||
<AnimatedButton
|
||||
onClick={() => transitionToStep(4)}
|
||||
className="flex items-center"
|
||||
>
|
||||
<AnimatedButton onClick={() => transitionToStep(4)} className="flex items-center">
|
||||
Continue to Number Porting
|
||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
||||
</AnimatedButton>
|
||||
@ -621,7 +632,7 @@ function SimConfigureContent() {
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex justify-between mt-6">
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => transitionToStep(3)}
|
||||
variant="outline"
|
||||
className="flex items-center"
|
||||
@ -629,7 +640,7 @@ function SimConfigureContent() {
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
Back to Add-ons
|
||||
</AnimatedButton>
|
||||
<AnimatedButton
|
||||
<AnimatedButton
|
||||
onClick={() => {
|
||||
// Validate MNP form if MNP is selected
|
||||
if (wantsMnp && !validateForm()) {
|
||||
@ -679,7 +690,9 @@ function SimConfigureContent() {
|
||||
<p className="text-sm text-gray-600">{plan.dataSize}</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -696,14 +709,16 @@ function SimConfigureContent() {
|
||||
{simType === "eSIM" && eid && (
|
||||
<div className="flex justify-between">
|
||||
<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 className="flex justify-between">
|
||||
<span className="text-gray-600">Activation:</span>
|
||||
<span className="text-gray-900">
|
||||
{activationType === "Scheduled" && scheduledActivationDate
|
||||
? `${new Date(scheduledActivationDate).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`
|
||||
{activationType === "Scheduled" && scheduledActivationDate
|
||||
? `${new Date(scheduledActivationDate).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`
|
||||
: activationType || "Not selected"}
|
||||
</span>
|
||||
</div>
|
||||
@ -764,7 +779,9 @@ function SimConfigureContent() {
|
||||
{oneTimeTotal > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<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>
|
||||
|
||||
@ -90,7 +90,9 @@ function CheckoutContent() {
|
||||
|
||||
const plan = plans.find(p => p.sku === selections.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
|
||||
@ -116,21 +118,23 @@ function CheckoutContent() {
|
||||
|
||||
const plan = plans.find(p => p.sku === selections.plan); // Look up by SKU instead of ID
|
||||
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
|
||||
const addonSkus: string[] = [];
|
||||
if (selections.addonSku) {
|
||||
// Single addon (legacy support)
|
||||
addonSkus.push(selections.addonSku);
|
||||
// Handle multiple addons from URL parameters
|
||||
const addonSkus: string[] = [];
|
||||
if (selections.addonSku) {
|
||||
// Single addon (legacy support)
|
||||
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, {
|
||||
addonSkus: addonSkus.length > 0 ? addonSkus : undefined,
|
||||
@ -210,7 +214,7 @@ function CheckoutContent() {
|
||||
...(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`);
|
||||
} catch (error) {
|
||||
console.error("Order submission failed:", error);
|
||||
@ -288,8 +292,8 @@ function CheckoutContent() {
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Submit Your Order for Review</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
You've configured your service and reviewed all details.
|
||||
Your order will be submitted for review and approval.
|
||||
You've configured your service and reviewed all details. Your order will be submitted
|
||||
for review and approval.
|
||||
</p>
|
||||
<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>
|
||||
@ -300,7 +304,7 @@ function CheckoutContent() {
|
||||
<p>• You'll receive confirmation once everything is ready</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Quick Totals Summary */}
|
||||
<div className="mt-4 bg-white rounded-lg p-4 border border-gray-200">
|
||||
<div className="flex justify-between items-center">
|
||||
@ -325,14 +329,18 @@ function CheckoutContent() {
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-green-800 text-sm font-medium">Payment method verified</p>
|
||||
<p className="text-green-700 text-sm mt-1">
|
||||
After order approval, payment will be automatically processed using your existing payment method on file.
|
||||
No additional payment steps required.
|
||||
After order approval, payment will be automatically processed using your existing
|
||||
payment method on file. No additional payment steps required.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -347,10 +355,11 @@ function CheckoutContent() {
|
||||
const urlParams = new URLSearchParams(params.toString());
|
||||
const reviewStep = orderType === "Internet" ? "4" : "5";
|
||||
urlParams.set("step", reviewStep);
|
||||
|
||||
const configureUrl = orderType === "Internet"
|
||||
? `/catalog/internet/configure?${urlParams.toString()}`
|
||||
: `/catalog/sim/configure?${urlParams.toString()}`;
|
||||
|
||||
const configureUrl =
|
||||
orderType === "Internet"
|
||||
? `/catalog/internet/configure?${urlParams.toString()}`
|
||||
: `/catalog/sim/configure?${urlParams.toString()}`;
|
||||
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"
|
||||
@ -365,9 +374,25 @@ function CheckoutContent() {
|
||||
>
|
||||
{submitting ? (
|
||||
<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">
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
Submitting Order...
|
||||
</span>
|
||||
|
||||
@ -43,7 +43,7 @@ export default function OrderStatusPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const [data, setData] = useState<OrderSummary | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isNewOrder = searchParams.get('status') === 'success';
|
||||
const isNewOrder = searchParams.get("status") === "success";
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
@ -83,7 +83,8 @@ export default function OrderStatusPage() {
|
||||
Order Submitted Successfully!
|
||||
</h3>
|
||||
<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>
|
||||
<div className="text-sm text-green-700">
|
||||
<p className="mb-1">
|
||||
|
||||
@ -30,7 +30,7 @@ export default function OrdersPage() {
|
||||
const [orders, setOrders] = useState<OrderSummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const showSuccess = searchParams.get('status') === 'success';
|
||||
const showSuccess = searchParams.get("status") === "success";
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOrders = async () => {
|
||||
@ -75,7 +75,8 @@ export default function OrdersPage() {
|
||||
Order Submitted Successfully!
|
||||
</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -107,7 +108,7 @@ export default function OrdersPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{orders.map((order) => (
|
||||
{orders.map(order => (
|
||||
<div
|
||||
key={order.id}
|
||||
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()}
|
||||
</p>
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Items:</span>
|
||||
<p className="text-gray-600">
|
||||
{order.itemSummary ||
|
||||
(order.itemsSummary && order.itemsSummary.length > 0
|
||||
? order.itemsSummary.map(item => `${item.name} (${item.quantity})`).join(', ')
|
||||
: 'No items')}
|
||||
{order.itemSummary ||
|
||||
(order.itemsSummary && order.itemsSummary.length > 0
|
||||
? order.itemsSummary
|
||||
.map(item => `${item.name} (${item.quantity})`)
|
||||
.join(", ")
|
||||
: "No items")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@ -144,7 +149,9 @@ export default function OrdersPage() {
|
||||
{order.totalAmount && (
|
||||
<div>
|
||||
<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>
|
||||
|
||||
@ -27,7 +27,7 @@ export function ActivationForm({
|
||||
name="activationType"
|
||||
value="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"
|
||||
/>
|
||||
<div>
|
||||
@ -50,7 +50,7 @@ export function ActivationForm({
|
||||
name="activationType"
|
||||
value="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"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
@ -71,19 +71,17 @@ export function ActivationForm({
|
||||
type="date"
|
||||
id="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
|
||||
max={
|
||||
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]
|
||||
} // 30 days from now
|
||||
max={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"
|
||||
/>
|
||||
{errors.scheduledActivationDate && (
|
||||
<p className="text-red-600 text-sm mt-1">{errors.scheduledActivationDate}</p>
|
||||
)}
|
||||
<p className="text-xs text-blue-700 mt-1">
|
||||
Note: Scheduled activation is subject to business day processing.
|
||||
Weekend/holiday requests may be processed on the next business day.
|
||||
Note: Scheduled activation is subject to business day processing. Weekend/holiday
|
||||
requests may be processed on the next business day.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -21,7 +21,13 @@ interface MnpFormProps {
|
||||
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) => {
|
||||
onMnpDataChange({
|
||||
...mnpData,
|
||||
@ -36,7 +42,7 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
<input
|
||||
type="checkbox"
|
||||
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"
|
||||
/>
|
||||
<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">
|
||||
<h4 className="font-medium text-blue-900 mb-4">Number Porting Information</h4>
|
||||
<p className="text-sm text-blue-800 mb-4">
|
||||
Please provide the following information from your current mobile carrier to complete the
|
||||
number porting process.
|
||||
Please provide the following information from your current mobile carrier to complete
|
||||
the number porting process.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* MNP Reservation Number */}
|
||||
<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 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="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"
|
||||
placeholder="10-digit reservation number"
|
||||
/>
|
||||
@ -87,7 +96,7 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
type="date"
|
||||
id="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"
|
||||
/>
|
||||
{errors.expiryDate && (
|
||||
@ -104,7 +113,7 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
type="tel"
|
||||
id="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"
|
||||
placeholder="090-1234-5678"
|
||||
/>
|
||||
@ -115,14 +124,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
|
||||
{/* MVNO Account Number */}
|
||||
<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
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="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"
|
||||
placeholder="Your current carrier account number"
|
||||
/>
|
||||
@ -130,14 +142,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
|
||||
{/* Last Name */}
|
||||
<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 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="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"
|
||||
placeholder="Tanaka"
|
||||
/>
|
||||
@ -148,14 +163,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
|
||||
{/* First Name */}
|
||||
<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 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="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"
|
||||
placeholder="Taro"
|
||||
/>
|
||||
@ -166,14 +184,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
|
||||
{/* Last Name Katakana */}
|
||||
<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) *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="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"
|
||||
placeholder="タナカ"
|
||||
/>
|
||||
@ -184,14 +205,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
|
||||
{/* First Name Katakana */}
|
||||
<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) *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="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"
|
||||
placeholder="タロウ"
|
||||
/>
|
||||
@ -202,13 +226,18 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
|
||||
{/* Gender */}
|
||||
<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 *
|
||||
</label>
|
||||
<select
|
||||
id="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"
|
||||
>
|
||||
<option value="">Select gender</option>
|
||||
@ -223,14 +252,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
|
||||
|
||||
{/* Date of Birth */}
|
||||
<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 *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="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"
|
||||
/>
|
||||
{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">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Important:</strong> Please ensure all information matches exactly with your current
|
||||
carrier records. Incorrect information may delay the porting process.
|
||||
<strong>Important:</strong> Please ensure all information matches exactly with your
|
||||
current carrier records. Incorrect information may delay the porting process.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -30,14 +30,16 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt
|
||||
step.completed
|
||||
? "bg-green-500 border-green-500 text-white shadow-lg scale-110"
|
||||
: currentStep === step.number
|
||||
? "border-blue-500 text-blue-500 bg-blue-50 scale-105 shadow-md"
|
||||
: "border-gray-300 text-gray-400 scale-100"
|
||||
? "border-blue-500 text-blue-500 bg-blue-50 scale-105 shadow-md"
|
||||
: "border-gray-300 text-gray-400 scale-100"
|
||||
}`}
|
||||
>
|
||||
{step.completed ? (
|
||||
<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>
|
||||
<span
|
||||
@ -45,8 +47,8 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt
|
||||
step.completed
|
||||
? "text-green-600"
|
||||
: currentStep === step.number
|
||||
? "text-blue-600"
|
||||
: "text-gray-400"
|
||||
? "text-blue-600"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{step.title}
|
||||
|
||||
@ -30,7 +30,7 @@ export function SimTypeSelector({
|
||||
name="simType"
|
||||
value="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"
|
||||
/>
|
||||
<DevicePhoneMobileIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||
@ -54,7 +54,7 @@ export function SimTypeSelector({
|
||||
name="simType"
|
||||
value="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"
|
||||
/>
|
||||
<CpuChipIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||
@ -79,15 +79,15 @@ export function SimTypeSelector({
|
||||
type="text"
|
||||
id="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"
|
||||
placeholder="32-digit EID number"
|
||||
maxLength={32}
|
||||
/>
|
||||
{errors.eid && <p className="text-red-600 text-sm mt-1">{errors.eid}</p>}
|
||||
<p className="text-xs text-blue-700 mt-1">
|
||||
Find your EID in: Settings → General → About → EID (iOS) or Settings → About Phone
|
||||
→ IMEI (Android)
|
||||
Find your EID in: Settings → General → About → EID (iOS) or Settings → About Phone →
|
||||
IMEI (Android)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { authenticatedApi } from "@/lib/api";
|
||||
import {
|
||||
MapPinIcon,
|
||||
PencilIcon,
|
||||
CheckIcon,
|
||||
import {
|
||||
MapPinIcon,
|
||||
PencilIcon,
|
||||
CheckIcon,
|
||||
XMarkIcon,
|
||||
ExclamationTriangleIcon
|
||||
ExclamationTriangleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
interface Address {
|
||||
@ -32,7 +32,10 @@ interface AddressConfirmationProps {
|
||||
onAddressIncomplete: () => void;
|
||||
}
|
||||
|
||||
export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }: AddressConfirmationProps) {
|
||||
export function AddressConfirmation({
|
||||
onAddressConfirmed,
|
||||
onAddressIncomplete,
|
||||
}: AddressConfirmationProps) {
|
||||
const [billingInfo, setBillingInfo] = useState<BillingInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editing, setEditing] = useState(false);
|
||||
@ -48,7 +51,7 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
|
||||
setLoading(true);
|
||||
const data = await authenticatedApi.get<BillingInfo>("/users/billing");
|
||||
setBillingInfo(data);
|
||||
|
||||
|
||||
if (!data.isComplete) {
|
||||
onAddressIncomplete();
|
||||
} else {
|
||||
@ -63,25 +66,27 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
|
||||
|
||||
const handleEdit = () => {
|
||||
setEditing(true);
|
||||
setEditedAddress(billingInfo?.address || {
|
||||
street: "",
|
||||
streetLine2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
postalCode: "",
|
||||
country: "",
|
||||
});
|
||||
setEditedAddress(
|
||||
billingInfo?.address || {
|
||||
street: "",
|
||||
streetLine2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
postalCode: "",
|
||||
country: "",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editedAddress) return;
|
||||
|
||||
|
||||
try {
|
||||
// For now, just use the edited address for the order
|
||||
// TODO: Implement WHMCS update when available
|
||||
onAddressConfirmed(editedAddress);
|
||||
setEditing(false);
|
||||
|
||||
|
||||
// Update local state to show the new address
|
||||
if (billingInfo) {
|
||||
setBillingInfo({
|
||||
@ -93,7 +98,7 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
|
||||
editedAddress.state &&
|
||||
editedAddress.postalCode &&
|
||||
editedAddress.country
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@ -175,18 +180,18 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
|
||||
{editing ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Street Address *
|
||||
</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Street Address *</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
placeholder="123 Main Street"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Street Address Line 2
|
||||
@ -194,7 +199,9 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
City *
|
||||
</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">City *</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
placeholder="Tokyo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
State/Prefecture *
|
||||
@ -221,20 +228,22 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
placeholder="Tokyo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Postal Code *
|
||||
</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Postal Code *</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
placeholder="100-0001"
|
||||
/>
|
||||
@ -242,12 +251,12 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Country *
|
||||
</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Country *</label>
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<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="text-gray-900">
|
||||
<p className="font-medium">{billingInfo.address.street}</p>
|
||||
{billingInfo.address.streetLine2 && (
|
||||
<p>{billingInfo.address.streetLine2}</p>
|
||||
)}
|
||||
{billingInfo.address.streetLine2 && <p>{billingInfo.address.streetLine2}</p>}
|
||||
<p>
|
||||
{billingInfo.address.city}, {billingInfo.address.state} {billingInfo.address.postalCode}
|
||||
{billingInfo.address.city}, {billingInfo.address.state}{" "}
|
||||
{billingInfo.address.postalCode}
|
||||
</p>
|
||||
<p>{billingInfo.address.country}</p>
|
||||
</div>
|
||||
|
||||
@ -32,7 +32,7 @@ export async function apiRequest<T>(endpoint: string, options: RequestInit = {})
|
||||
|
||||
const finalHeaders: Record<string, string> = {
|
||||
...defaultHeaders,
|
||||
...(options.headers as Record<string, string> || {}),
|
||||
...((options.headers as Record<string, string>) || {}),
|
||||
};
|
||||
|
||||
// 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";
|
||||
}
|
||||
|
||||
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: finalHeaders,
|
||||
|
||||
@ -51,7 +51,7 @@ issue_cert() {
|
||||
fi
|
||||
|
||||
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 \
|
||||
certonly --webroot -w /var/www/certbot -d "$domain" --agree-tos --no-eff-email
|
||||
log "✅ Certificate issuance process completed"
|
||||
@ -60,7 +60,7 @@ issue_cert() {
|
||||
# Build production images
|
||||
build_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"
|
||||
}
|
||||
|
||||
@ -73,13 +73,13 @@ deploy() {
|
||||
|
||||
# Start database and cache first
|
||||
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
|
||||
log "⏳ Waiting for database..."
|
||||
timeout=60
|
||||
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"
|
||||
break
|
||||
fi
|
||||
@ -93,17 +93,17 @@ deploy() {
|
||||
|
||||
# Run 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)
|
||||
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
|
||||
log "🏥 Performing health checks..."
|
||||
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"
|
||||
fi
|
||||
|
||||
@ -115,7 +115,7 @@ deploy() {
|
||||
# Stop production services
|
||||
stop() {
|
||||
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"
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ update() {
|
||||
build_images
|
||||
|
||||
# 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"
|
||||
}
|
||||
@ -134,35 +134,35 @@ update() {
|
||||
# Show status with health checks
|
||||
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:"
|
||||
# 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
|
||||
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"
|
||||
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"
|
||||
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
|
||||
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
|
||||
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() {
|
||||
log "💾 Creating database backup..."
|
||||
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"
|
||||
}
|
||||
|
||||
# Clean up
|
||||
cleanup() {
|
||||
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 image prune -f
|
||||
log "✅ Cleanup completed"
|
||||
@ -172,12 +172,12 @@ cleanup() {
|
||||
case "${1:-help}" in
|
||||
"deploy") deploy ;;
|
||||
"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"
|
||||
;;
|
||||
"stop") stop ;;
|
||||
"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"
|
||||
;;
|
||||
"update") update ;;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user