/**
* @page Cashify-Style Progressive Service Scheduler Booking Wizard (`app/services/page.tsx`)
* @purpose Unified drill-down wizard (Device Type -> Brand -> Series -> Model -> Repair Category/Variant -> Slot -> 20% Advance Checkout) + Dynamic CMS Sections (Hero, How It Works, Why Choose Us, FAQs).
*/
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/store/authStore';
import { formatCurrency } from '@/lib/utils';
import {
serviceBookingService,
AvailableSlot,
ServiceBookingPayload,
isDemoSlot,
liveCatalogId,
matchServiceCatalogItem,
} from '@/services/api/serviceBooking';
import { isDemoCatalogId } from '@/lib/demoRepairCatalog';
import { storefrontService } from '@/services/api/storefrontService';
import { setAccessToken } from '@/services/api/client';
import GoogleAuthModal from '@/components/auth/GoogleAuthModal';
import { toast } from 'sonner';
import {
ShieldCheck,
Lock,
Loader2,
ArrowRight,
ArrowLeft,
ChevronDown,
Clock,
HelpCircle,
Check,
Truck,
Store,
Video,
Phone,
MapPin,
KeyRound,
RotateCcw,
Upload,
} from 'lucide-react';
import { AnnouncementBar } from '@/layout/AnnouncementBar';
import { Header } from '@/layout/Header';
import { Navbar } from '@/layout/Navbar';
import { Footer } from '@/layout/Footer';
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
import { FloatingButtons } from '@/components/FloatingButtons';
import { ClientModalsContainer } from '@/components/ClientModalsContainer';
import { PhoneInput } from '@/components/PhoneInput';
import { FormSelect } from '@/components/services/FormSelect';
import { parseMetadata } from '@/lib/layoutConfig';
import {
findServiceSection,
formatDeviceTypeLabel,
getDeviceTypeIcon,
getWhyUsIcon,
isServiceSectionVisible,
normalizeDeviceTypes,
normalizeServiceLayout,
} from '@/lib/servicePageLayout';
type WizardStep = 'selection' | 'schedule' | 'review';
function PageShell({ children }: { children: React.ReactNode }) {
return (
);
}
export default function ServiceBookingPage() {
const router = useRouter();
const { isAuthenticated, accessToken, bootstrapAuth } = useAuthStore();
const [authModalOpen, setAuthModalOpen] = useState(false);
const [activeStep, setActiveStep] = useState('selection');
const [loadingBrands, setLoadingBrands] = useState(false);
const [loadingSeries, setLoadingSeries] = useState(false);
const [loadingModels, setLoadingModels] = useState(false);
const [advancePercent, setAdvancePercent] = useState(20);
const [loadingRepair, setLoadingRepair] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [checkoutOverlay, setCheckoutOverlay] = useState<
null | 'preparing' | 'verifying'
>(null);
const paymentHandledRef = useRef(false);
// Dynamic CMS Layout Sections State
const [layoutCMS, setLayoutCMS] = useState([]);
const [openFaqIdx, setOpenFaqIdx] = useState(0);
const [deviceTypes, setDeviceTypes] = useState([
'mobile',
'laptop',
'tablet',
'smartwatch',
]);
const [loadingDeviceTypes, setLoadingDeviceTypes] = useState(true);
// Catalog Progressive Selections
const [deviceType, setDeviceType] = useState('mobile');
const [brands, setBrands] = useState([]);
const [selectedBrand, setSelectedBrand] = useState(null);
const [seriesList, setSeriesList] = useState([]);
const [selectedSeries, setSelectedSeries] = useState(null);
const [models, setModels] = useState([]);
const [selectedModel, setSelectedModel] = useState(null);
const [repairConfig, setRepairConfig] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null);
const [selectedVariant, setSelectedVariant] = useState(null);
// Scheduling State
const [targetDate, setTargetDate] = useState(new Date().toISOString().split('T')[0]);
const [availableSlots, setAvailableSlots] = useState([]);
const [loadingSlots, setLoadingSlots] = useState(false);
const [selectedSlot, setSelectedSlot] = useState(null);
// Fulfillment & Customer Form State
const [fulfillmentMode, setFulfillmentMode] = useState<'WALK_IN' | 'DELIVERY'>('WALK_IN');
const [deliverySubOption, setDeliverySubOption] = useState<'COURIER' | 'PICKUP'>('PICKUP');
const [custName, setCustName] = useState('');
const [custEmail, setCustEmail] = useState('');
const [altPhone, setAltPhone] = useState('');
const [isWhatsappAlt, setIsWhatsappAlt] = useState(true);
const [deliveryAddress, setDeliveryAddress] = useState('');
// Lock Credentials State
const [lockType, setLockType] = useState<'NONE' | 'PIN' | 'PASSWORD' | 'PATTERN'>('NONE');
const [lockPasscode, setLockPasscode] = useState('');
const [patternNodes, setPatternNodes] = useState([]);
// Pre-dispatch Video Upload State
const [preDispatchVideoFile, setPreDispatchVideoFile] = useState(null);
const [preDispatchVideoId, setPreDispatchVideoId] = useState(null);
const [uploadingVideo, setUploadingVideo] = useState(false);
const handlePatternNodeClick = (node: number) => {
if (patternNodes.includes(node)) {
toast.warning(`Node ${node} is already part of the pattern sequence.`);
return;
}
if (patternNodes.length >= 9) {
toast.warning('Maximum 9 nodes allowed in pattern.');
return;
}
const newSeq = [...patternNodes, node];
setPatternNodes(newSeq);
setLockPasscode(newSeq.join('-'));
};
const resetPattern = () => {
setPatternNodes([]);
setLockPasscode('');
};
const fulfillmentType = fulfillmentMode === 'WALK_IN'
? 'WALK_IN'
: (deliverySubOption === 'PICKUP' ? 'DOORSTEP_PICKUP' : 'COURIER');
const pickupFee = fulfillmentType === 'DOORSTEP_PICKUP' ? 250 : 0;
const basePrice = selectedVariant?.price || 0;
const totalQuote = basePrice + pickupFee;
const advancePrice = Math.round(totalQuote * (advancePercent / 100));
const remainingPrice = totalQuote - advancePrice;
// Fetch dynamic advance percent from CMS settings
useEffect(() => {
storefrontService.getSettings().then((s) => {
if (s.advance_percent !== undefined && s.advance_percent !== null) {
setAdvancePercent(Number(s.advance_percent));
}
}).catch(() => {}); // graceful fallback — keeps 20%
}, []);
useEffect(() => {
void bootstrapAuth();
}, [bootstrapAuth]);
useEffect(() => {
if (accessToken) {
setAccessToken(accessToken);
}
}, [accessToken]);
// Load CMS Service Layout Configuration from Backend Admin CMS
useEffect(() => {
const loadLayout = async () => {
try {
const cms = await serviceBookingService.fetchServicePageLayout();
setLayoutCMS(normalizeServiceLayout(cms));
} catch {
// Graceful fallback to rich defaults if offline or empty
}
};
void loadLayout();
}, []);
// Device categories from catalog API (admin-managed catalog)
useEffect(() => {
const loadDeviceTypes = async () => {
setLoadingDeviceTypes(true);
try {
const types = normalizeDeviceTypes(
await serviceBookingService.fetchDeviceTypes()
);
setDeviceTypes(types);
setDeviceType((prev) => (types.includes(prev) ? prev : types[0]));
} catch {
setDeviceTypes(['mobile', 'laptop', 'tablet', 'smartwatch']);
} finally {
setLoadingDeviceTypes(false);
}
};
void loadDeviceTypes();
}, []);
// Load Brands when Device Type changes
useEffect(() => {
if (!deviceType) return;
loadBrands(deviceType);
}, [deviceType]);
const loadBrands = async (type: string) => {
setLoadingBrands(true);
try {
const data = await serviceBookingService.fetchBrands(type);
setBrands(data);
setSelectedBrand(null);
setSelectedSeries(null);
setSelectedModel(null);
setRepairConfig(null);
setSelectedCategory(null);
setSelectedVariant(null);
} catch (err: any) {
toast.error(err.message || 'Failed to load brands.');
} finally {
setLoadingBrands(false);
}
};
const handleDeviceTypeChange = (type: string) => {
setDeviceType(type);
setSelectedBrand(null);
setSelectedSeries(null);
setSelectedModel(null);
setRepairConfig(null);
setSelectedCategory(null);
setSelectedVariant(null);
};
const handleSelectBrand = async (brand: any) => {
setSelectedBrand(brand);
setSelectedSeries(null);
setSelectedModel(null);
setRepairConfig(null);
setSelectedCategory(null);
setSelectedVariant(null);
setLoadingSeries(true);
try {
const series = await serviceBookingService.fetchSeriesByBrand(brand.brand_id, deviceType);
setSeriesList(series);
} catch (err: any) {
toast.error(err.message || 'Failed to load device series.');
} finally {
setLoadingSeries(false);
}
};
const handleSelectSeries = async (series: any) => {
setSelectedSeries(series);
setSelectedModel(null);
setRepairConfig(null);
setSelectedCategory(null);
setSelectedVariant(null);
setLoadingModels(true);
try {
const mdlList = await serviceBookingService.fetchModelsBySeries(series.series_id);
setModels(mdlList);
} catch (err: any) {
toast.error(err.message || 'Failed to load models.');
} finally {
setLoadingModels(false);
}
};
const handleSelectModel = async (model: any) => {
setSelectedModel(model);
setSelectedCategory(null);
setSelectedVariant(null);
setLoadingRepair(true);
try {
const config = await serviceBookingService.fetchRepairConfig(model.model_id);
setRepairConfig(config);
if (config.categories && config.categories.length > 0) {
setSelectedCategory(config.categories[0]);
if (config.categories[0].variants && config.categories[0].variants.length > 0) {
setSelectedVariant(config.categories[0].variants[0]);
}
}
} catch (err: any) {
toast.error(err.message || 'Failed to load repair options.');
} finally {
setLoadingRepair(false);
}
};
// Slot querying triggered when moving to Schedule step
useEffect(() => {
if (activeStep === 'schedule' && selectedVariant) {
loadSlots();
}
}, [activeStep, targetDate, selectedVariant, selectedCategory]);
const loadSlots = async () => {
setLoadingSlots(true);
setSelectedSlot(null);
try {
let serviceId = 'OTHER_SERVICE';
try {
const catalog = await serviceBookingService.fetchServiceCatalog();
serviceId =
matchServiceCatalogItem(catalog, selectedCategory?.category_name)?.service_id ||
catalog[0]?.service_id ||
'OTHER_SERVICE';
} catch {
/* keep OTHER_SERVICE */
}
const slots = await serviceBookingService.fetchAvailableSlots(serviceId, targetDate);
setAvailableSlots(slots);
} catch (err: any) {
toast.error(err.message || 'Failed to load scheduling slots.');
} finally {
setLoadingSlots(false);
}
};
const loadRazorpayScript = (): Promise => {
return new Promise((resolve) => {
if ((window as any).Razorpay) {
resolve(true);
return;
}
const script = document.createElement('script');
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
script.onload = () => resolve(true);
script.onerror = () => resolve(false);
document.body.appendChild(script);
});
};
const handleCreateBooking = async () => {
if (!isAuthenticated) {
toast.info('Please sign in to confirm your booking.');
setAuthModalOpen(true);
return;
}
if (!selectedVariant || !selectedModel) {
toast.warning('Please select your device and repair option.');
return;
}
if (fulfillmentMode === 'WALK_IN' && !selectedSlot) {
toast.warning('Please pick an appointment date and slot for Walk-in fulfillment.');
return;
}
if (fulfillmentMode === 'DELIVERY' && (!deliveryAddress || !deliveryAddress.trim())) {
toast.warning('Please enter your delivery/pickup address.');
return;
}
if (lockType === 'PIN') {
if (!/^\d{4,8}$/.test(lockPasscode.trim())) {
toast.error('PIN must be strictly 4 to 8 numeric digits (0-9).');
return;
}
} else if (lockType === 'PATTERN') {
if (patternNodes.length < 4) {
toast.error('Pattern lock must consist of at least 4 nodes.');
return;
}
}
paymentHandledRef.current = false;
setSubmitting(true);
setCheckoutOverlay('preparing');
try {
let videoId = preDispatchVideoId;
if (preDispatchVideoFile && !videoId) {
setUploadingVideo(true);
try {
const uploaded = await serviceBookingService.uploadMediaFile(preDispatchVideoFile);
videoId = uploaded.file_id;
} catch (err: any) {
toast.error(err.message || 'Pre-courier video upload failed.');
setSubmitting(false);
setCheckoutOverlay(null);
return;
} finally {
setUploadingVideo(false);
}
}
const catalog = await serviceBookingService.fetchServiceCatalog().catch(() => []);
const catalogService = matchServiceCatalogItem(
catalog,
selectedCategory?.category_name
);
const usingDemoRepair =
isDemoCatalogId(selectedVariant.variant_id) ||
isDemoCatalogId(selectedCategory?.service_type_id) ||
isDemoCatalogId(selectedCategory?.repair_service_id);
const serviceId = usingDemoRepair
? catalogService?.service_id || 'OTHER_SERVICE'
: catalogService?.service_id;
const customServiceName = [
selectedCategory?.category_name,
selectedVariant.name,
]
.filter(Boolean)
.join(' — ');
const canLockSlot =
fulfillmentMode === 'WALK_IN' && selectedSlot && !isDemoSlot(selectedSlot);
const preferredSlotNote =
fulfillmentMode === 'WALK_IN' && selectedSlot && !canLockSlot
? `Preferred walk-in slot: ${selectedSlot.start_time} – ${selectedSlot.end_time}`
: undefined;
const payload: ServiceBookingPayload = {
new_device: {
brand: selectedBrand?.name || 'Generic',
model: selectedModel.name,
device_type: deviceType,
notes: preferredSlotNote,
},
service_id: serviceId || 'OTHER_SERVICE',
custom_service_name: customServiceName || undefined,
customer_name: custName.trim() || undefined,
customer_email: custEmail.trim() || undefined,
device_type: deviceType,
brand_id: liveCatalogId(selectedBrand?.brand_id),
series_id: liveCatalogId(selectedSeries?.series_id),
model_id: liveCatalogId(selectedModel.model_id),
service_type_id: liveCatalogId(selectedCategory?.service_type_id),
repair_service_id: liveCatalogId(selectedCategory?.repair_service_id),
repair_variant_id: liveCatalogId(selectedVariant.variant_id),
source: 'ONLINE',
fulfillment_type: fulfillmentType,
alt_phone: altPhone || undefined,
is_whatsapp_alt: isWhatsappAlt,
delivery_address: fulfillmentMode === 'DELIVERY' ? deliveryAddress.trim() : undefined,
pre_dispatch_video_id: videoId || undefined,
lock_type: lockType,
lock_passcode: lockType !== 'NONE' ? lockPasscode : undefined,
appointment: canLockSlot
? {
scheduled_start: selectedSlot.start_time,
scheduled_end: selectedSlot.end_time,
}
: null,
};
const bookingRes = await serviceBookingService.createServiceBooking(payload);
serviceBookingService.rememberLocalJob({
job_id: bookingRes.job_id,
job_no: bookingRes.job_no,
});
const rzpLoaded = await loadRazorpayScript();
if (!rzpLoaded) {
throw new Error('Razorpay SDK failed to load. Please check your network connection.');
}
const pmtAmount = bookingRes.advance_deposit || advancePrice;
const pmtOrder = await serviceBookingService.initiateServicePayment(bookingRes.job_id, {
payment_type: 'ADVANCE',
amount: Math.max(1, Math.round(Number(pmtAmount) || 0)),
provider: 'RAZORPAY',
});
const rzpKey =
pmtOrder.rzp_key_id ||
process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID ||
'rzp_test_TTUzPFYF0hRV89';
const goToSuccess = (
extra: Record,
overlay: 'preparing' | 'verifying' | null = 'verifying'
) => {
paymentHandledRef.current = true;
if (overlay) setCheckoutOverlay(overlay);
const qs = new URLSearchParams({
job_no: bookingRes.job_no,
job_id: bookingRes.job_id,
payment_id: String(pmtOrder.payment_id || ''),
amount: String(pmtOrder.amount || Math.round(selectedVariant.price * 0.2)),
method: 'Razorpay',
...extra,
});
router.replace(`/services/success?${qs.toString()}`);
};
const options = {
key: rzpKey,
amount: pmtOrder.amount,
currency: pmtOrder.currency || 'INR',
name: 'iFixKart Commerce',
description: `${advancePercent}% Advance Booking Deposit (${bookingRes.job_no})`,
order_id: pmtOrder.rzp_order_id,
redirect: false,
handler: (response: any) => {
goToSuccess({
rzp_payment_id: String(response.razorpay_payment_id || ''),
rzp_order_id: String(response.razorpay_order_id || ''),
rzp_signature: String(response.razorpay_signature || ''),
verify: '1',
});
},
modal: {
escape: true,
handleback: true,
confirm_close: true,
ondismiss: () => {
if (paymentHandledRef.current) return;
setCheckoutOverlay(null);
setSubmitting(false);
toast.info('Payment window closed. Booking saved as pending.');
router.push(
`/services/success?job_no=${bookingRes.job_no}&job_id=${bookingRes.job_id}&status=PENDING`
);
}
},
theme: {
color: '#2563eb'
}
};
try {
const rzp = new (window as any).Razorpay(options);
rzp.on('payment.failed', function (response: any) {
if (paymentHandledRef.current) return;
setCheckoutOverlay(null);
setSubmitting(false);
toast.error(response.error?.description || 'Payment transaction failed.');
});
rzp.open();
} catch (sdkError: any) {
setCheckoutOverlay(null);
setSubmitting(false);
toast.error(sdkError.message || 'Error opening payment checkout.');
}
} catch (err: any) {
setCheckoutOverlay(null);
setSubmitting(false);
toast.error(err.message || 'Booking process failed. Please try again.');
}
};
const howItWorksCMS = findServiceSection(layoutCMS, 'service_how_it_works');
const whyUsCMS = findServiceSection(layoutCMS, 'service_why_us');
const faqCMS = findServiceSection(layoutCMS, 'service_faq');
const howMeta = parseMetadata(howItWorksCMS);
const whyMeta = parseMetadata(whyUsCMS);
const faqMeta = parseMetadata(faqCMS);
const showHow = isServiceSectionVisible(layoutCMS, 'service_how_it_works');
const showWhy = isServiceSectionVisible(layoutCMS, 'service_why_us');
const showFaq = isServiceSectionVisible(layoutCMS, 'service_faq');
const howTitle = howItWorksCMS?.title || 'How it works';
const howSubtitle =
howItWorksCMS?.subtitle || 'Get your device restored in 3 simple steps';
const howSteps = howMeta.steps || [
{
number: '1',
title: 'Check Price',
desc: 'Select your phone model and choice of repairs to see your custom instant pricing quote.',
},
{
number: '2',
title: 'Schedule Service',
desc: 'Choose a convenient date and location - your home, office, or our local service workshop.',
},
{
number: '3',
title: 'Get Device Repaired',
desc: 'Our certified technician handles the repair on-site. Test it, pay, and receive your 6-month warranty.',
},
];
const whyTitle = whyUsCMS?.title || 'Why Choose iFixKart?';
const whySubtitle =
whyUsCMS?.subtitle || 'Top tier hardware repair guarantees';
const whyCards = whyMeta.cards || [
{
title: 'Premium Quality Parts',
desc: 'We source only certified top-grade components to guarantee perfect compatibility and longevity.',
icon: 'ShieldCheck',
},
{
title: 'Instant On-Site Repair',
desc: 'No need to leave your phone at a shop for days. Most repairs completed in under 45 minutes.',
icon: 'Clock',
},
{
title: '6 Months Warranty',
desc: 'Every screen and spare part replacement includes our worry-free protection warranty cover.',
icon: 'ThumbsUp',
},
];
const faqTitle = faqCMS?.title || 'Frequently Asked Questions';
const faqSubtitle = faqCMS?.subtitle || 'Got questions? We have answers.';
const faqList = faqMeta.faqs || [
{
question: 'What happens when I place my booking?',
answer:
'Our support rep confirms the time slot. A technician is assigned and visits your address to handle the repair on-site.',
},
{
question: 'How do I pay for the service?',
answer:
'You pay only after the repair is complete and verified. We accept UPI, Cards, Cash, and online payments.',
},
{
question: 'Is there a warranty on parts replaced?',
answer:
'Yes! All screens and battery replacements include a worry-free 6-month replacement warranty cover.',
},
{
question: 'Will my data remain secure?',
answer:
'We strictly ensure data security. Repair is conducted in front of you, ensuring zero access to storage components.',
},
];
const cmsBlocks = useMemo(() => {
const blocks: Array<{ key: string; order: number }> = [];
if (showHow) {
blocks.push({
key: 'how',
order: Number(howItWorksCMS?.display_order) || 13,
});
}
if (showWhy) {
blocks.push({
key: 'why',
order: Number(whyUsCMS?.display_order) || 14,
});
}
if (showFaq) {
blocks.push({
key: 'faq',
order: Number(faqCMS?.display_order) || 15,
});
}
return blocks.sort((a, b) => a.order - b.order);
}, [
showHow,
showWhy,
showFaq,
howItWorksCMS?.display_order,
whyUsCMS?.display_order,
faqCMS?.display_order,
]);
const stepIndex = ['selection', 'schedule', 'review'].indexOf(activeStep);
const wizardSteps: { id: WizardStep; label: string; num: number }[] = [
{ id: 'selection', label: 'Repair', num: 1 },
{ id: 'schedule', label: 'Schedule', num: 2 },
{ id: 'review', label: 'Checkout', num: 3 },
];
const tileClass = (selected: boolean) =>
`p-3.5 border rounded-lg text-center transition-all cursor-pointer ${
selected
? 'border-primary bg-primary/5 text-primary shadow-sm'
: 'border-gray-200 bg-white text-[#1a1a1a] hover:border-primary/40 hover:bg-gray-50'
}`;
const fieldLabel = 'block font-semibold text-[#1a1a1a] text-[11px] mb-1.5';
const fieldHint = 'text-gray-500 text-[12px] mt-0.5 mb-3';
const selectClass =
'w-full min-h-[46px] bg-white border border-gray-200 rounded-lg px-3.5 py-2.5 text-[13px] text-[#1a1a1a] outline-none focus:border-primary focus:ring-2 focus:ring-primary/15';
const primaryBtn =
'inline-flex items-center justify-center gap-1.5 bg-primary hover:brightness-95 text-white font-bold text-[13px] py-3 px-5 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer';
const ghostBtn =
'inline-flex items-center justify-center gap-1.5 border border-gray-200 text-[#555] font-semibold text-[13px] py-2.5 px-4 rounded-lg hover:bg-gray-50 transition cursor-pointer';
return (
Book a repair
Choose your device, service, and appointment in a few steps.
{wizardSteps.map((s, idx) => {
const done = idx < stepIndex;
const active = s.id === activeStep;
const locked = idx > stepIndex;
return (
{idx > 0 && (
)}
);
})}
{activeStep === 'selection' && (
)}
{activeStep === 'schedule' && (
)}
{activeStep === 'review' && selectedVariant && (
Customer Details & Review
Provide contact details, delivery address, and device lock info before payment.
{/* Customer Contact & Delivery Form */}
Customer Contact Information
{fulfillmentMode === 'DELIVERY' && (
)}
{/* Additional Delivery Security Controls */}
{fulfillmentMode === 'DELIVERY' && (
Device Dispatch & Lock Security
{/* Pre-Courier Condition Video Upload */}
Upload a short video (≤ 30 seconds) capturing your device condition before dispatch.
{
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
if (file.size > 50 * 1024 * 1024) {
toast.error('Video file size exceeds 50MB limit.');
return;
}
setPreDispatchVideoFile(file);
toast.success(`Selected video: ${file.name}`);
}
}}
className="text-[12px] text-gray-600 block"
/>
{preDispatchVideoFile && (
Video attached: {preDispatchVideoFile.name}
)}
{/* Device Lock Credential Picker */}
{(['NONE', 'PIN', 'PASSWORD', 'PATTERN'] as const).map((type) => (
))}
{lockType === 'PIN' && (
setLockPasscode(e.target.value)}
className="max-w-xs px-3 py-2 text-[13px] border border-gray-200 rounded-lg focus:outline-none focus:border-primary font-mono"
/>
)}
{lockType === 'PASSWORD' && (
setLockPasscode(e.target.value)}
className="max-w-md px-3 py-2 text-[13px] border border-gray-200 rounded-lg focus:outline-none focus:border-primary"
/>
)}
{lockType === 'PATTERN' && (
Draw 3x3 Pattern (Min 4 nodes)
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((node) => {
const idx = patternNodes.indexOf(node);
const isSelected = idx !== -1;
return (
);
})}
{patternNodes.length > 0 ? (
Pattern Sequence: {patternNodes.join(' → ')}
) : (
Click nodes in sequence to record pattern
)}
)}
)}
{/* Order Summary & Deposit Breakdown Card */}
Device & Model
{selectedBrand?.name} {selectedModel?.name}
Fulfillment Mode
{fulfillmentType}
{fulfillmentMode === 'WALK_IN' && selectedSlot && (
Scheduled time: {new Date(selectedSlot.start_time).toLocaleString()}
Warranty: {selectedVariant.warranty_days} Days
)}
Base repair quote
{formatCurrency(basePrice)}
{fulfillmentType === 'DOORSTEP_PICKUP' && (
Doorstep pickup fee
+₹250
)}
Total Quote
{formatCurrency(totalQuote)}
20% Advance Deposit
{formatCurrency(advancePrice)}
Remaining balance due at return
{formatCurrency(remainingPrice)}
)}
{cmsBlocks.map((block) => {
if (block.key === 'how') {
return (
= 3 ? 'md:grid-cols-3' : howSteps.length === 2 ? 'md:grid-cols-2' : ''
}`}
>
{howSteps.map((s: any, idx: number) => (
{s.number || idx + 1}
{s.title}
{s.desc}
))}
);
}
if (block.key === 'why') {
return (
= 3 ? 'md:grid-cols-3' : whyCards.length === 2 ? 'md:grid-cols-2' : ''
}`}
>
{whyCards.map((c: any, idx: number) => {
const Icon = getWhyUsIcon(c.icon);
return (
);
})}
);
}
return (
FAQ
{faqTitle}
{faqSubtitle}
{faqList.map((faq: any, idx: number) => {
const isOpen = openFaqIdx === idx;
return (
{isOpen && (
{faq.answer}
)}
);
})}
);
})}
setAuthModalOpen(false)} />
{checkoutOverlay && (
{checkoutOverlay === 'verifying'
? 'Verifying payment'
: 'Opening payment'}
Please wait a moment.
)}
);
}